forked from 77media/video-flow
Merge branch 'dev' of https://git.qikongjian.com/77media/video-flow into dev
This commit is contained in:
commit
4032b642db
@ -46,7 +46,7 @@ const FamousTemplate: React.FC<FamousTemplateProps> = ({ showTabs = true }) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<section data-alt="famous-template" className="w-full">
|
<section data-alt="famous-template" className="w-full">
|
||||||
<div data-alt="famous-template-header" className={`mb-4 items-center ${isDesktop ? 'flex' : 'block'}`}>
|
<div data-alt="famous-template-header" className={`items-center ${isDesktop ? 'flex' : 'block'}`}>
|
||||||
<h2 data-alt="famous-template-title" className="text-xl py-4 font-semibold text-white">
|
<h2 data-alt="famous-template-title" className="text-xl py-4 font-semibold text-white">
|
||||||
Inspiration Lab
|
Inspiration Lab
|
||||||
</h2>
|
</h2>
|
||||||
@ -58,7 +58,7 @@ const FamousTemplate: React.FC<FamousTemplateProps> = ({ showTabs = true }) => {
|
|||||||
type="button"
|
type="button"
|
||||||
data-alt={`template-tab-${tab}`}
|
data-alt={`template-tab-${tab}`}
|
||||||
onClick={() => setActiveTab(tab)}
|
onClick={() => setActiveTab(tab)}
|
||||||
className={`px-3 py-1 rounded-none text-sm transition-colors border ${
|
className={`px-3 py-0.5 rounded-none text-sm transition-colors border ${
|
||||||
activeTab === tab
|
activeTab === tab
|
||||||
? "border-white/60 bg-white/80 text-slate-900"
|
? "border-white/60 bg-white/80 text-slate-900"
|
||||||
: "border-white/20 text-white/80 hover:border-white/40 hover:bg-white/10"
|
: "border-white/20 text-white/80 hover:border-white/40 hover:bg-white/10"
|
||||||
@ -83,7 +83,7 @@ const FamousTemplate: React.FC<FamousTemplateProps> = ({ showTabs = true }) => {
|
|||||||
<div
|
<div
|
||||||
data-alt="template-item"
|
data-alt="template-item"
|
||||||
key={t.id}
|
key={t.id}
|
||||||
className="relative h-40 group"
|
className="relative h-40 group [container-type:inline-size]"
|
||||||
onMouseEnter={(e) => {
|
onMouseEnter={(e) => {
|
||||||
const v = e.currentTarget.querySelector('video') as HTMLVideoElement | null
|
const v = e.currentTarget.querySelector('video') as HTMLVideoElement | null
|
||||||
v?.play?.()
|
v?.play?.()
|
||||||
@ -150,7 +150,7 @@ const FamousTemplate: React.FC<FamousTemplateProps> = ({ showTabs = true }) => {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
data-alt="try-this-button"
|
data-alt="try-this-button"
|
||||||
className="absolute top-4 left-1/2 -translate-x-1/2 z-20 opacity-0 group-hover:opacity-100 transition-opacity duration-300 ease-out bg-white/40 text-white hover:bg-white hover:text-black text-slate-900 text-xs font-medium px-4 py-2 rounded-full border-white/60 shadow-sm"
|
className="absolute top-4 left-1/2 -translate-x-1/2 z-20 opacity-0 group-hover:opacity-100 transition-opacity duration-300 ease-out bg-white/40 text-white hover:bg-white hover:text-black text-slate-900 text-xs font-medium px-[clamp(0.5rem,3cqw,0.75rem)] py-[clamp(0.25rem,1cqh,0.5rem)] rounded-full border-white/60 shadow-sm"
|
||||||
onClick={(e: React.MouseEvent<HTMLButtonElement>) => {
|
onClick={(e: React.MouseEvent<HTMLButtonElement>) => {
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
const id = t.id || t.template_id
|
const id = t.id || t.template_id
|
||||||
|
|||||||
@ -5,11 +5,11 @@ import { useDeviceType } from '@/hooks/useDeviceType';
|
|||||||
export default function HomeBanner() {
|
export default function HomeBanner() {
|
||||||
const { isDesktop } = useDeviceType();
|
const { isDesktop } = useDeviceType();
|
||||||
return (
|
return (
|
||||||
<div data-alt="home-banner-wrapper" className="sticky top-0 z-50 w-full mx-auto p-0 overflow-hidden bg-gradient-to-b from-black/80 to-black/10">
|
<div data-alt="home-banner-wrapper" className="sticky top-0 z-50 w-full mx-auto p-0 overflow-hidden bg-gradient-to-b from-black/80 to-black/10 pt-14">
|
||||||
{/* Base content - always present under the banner */}
|
{/* Base content - always present under the banner */}
|
||||||
<div className='p-1 text-center text-2xl font-bold tracking-tight sm:text-3xl'>Your idea. A movie. In minutes.</div>
|
<div className='p-1 text-center text-2xl font-bold tracking-tight sm:text-3xl'>Your idea. A movie. In minutes.</div>
|
||||||
<div className='mx-auto w-full max-w-[600px] px-3 py-2 text-center text-xs text-gray-400 sm:px-16 sm:text-sm'>Our AI turns sparks into full-blown stories — fast & free.</div>
|
<div className='mx-auto w-full max-w-[600px] px-3 py-4 text-center text-xs text-gray-400 sm:px-16 sm:text-sm'>Our AI turns sparks into full-blown stories — fast & free.</div>
|
||||||
<div data-alt="home-banner-base" className={`space-y-4 mx-auto w-full max-w-[900px] px-3 sm:px-16 relative py-2 px-12 flex items-center justify-center bg-[radial-gradient(ellipse_at_center,rgba(106,244,249,0.28)_0%,rgba(106,244,249,0.14)_35%,transparent_70%)] ${isDesktop ? 'px-24' : 'px-3'}`}>
|
<div data-alt="home-banner-base" className={`space-y-4 mx-auto w-full max-w-[900px] sm:px-16 relative pt-2 pb-12 px-12 flex items-center justify-center bg-[radial-gradient(ellipse_at_center,rgba(106,244,249,0.28)_0%,rgba(106,244,249,0.14)_35%,transparent_70%)] ${isDesktop ? 'px-24' : 'px-3'}`}>
|
||||||
<VideoCreationForm />
|
<VideoCreationForm />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -61,7 +61,7 @@ const MyMovies: React.FC = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<section data-alt="my-movies" className="w-full">
|
<section data-alt="my-movies" className="w-full">
|
||||||
<div data-alt="my-movies-header" className="mb-4 flex items-center justify-between">
|
<div data-alt="my-movies-header" className="flex items-center justify-between">
|
||||||
<h2 data-alt="my-movies-title" className="text-xl py-4 font-semibold text-white">My Projects</h2>
|
<h2 data-alt="my-movies-title" className="text-xl py-4 font-semibold text-white">My Projects</h2>
|
||||||
<Link data-alt="all-movies-link" href="/movies" className="text-sm px-2 border rounded-full text-blue-400 hover:text-blue-300">All movies →</Link>
|
<Link data-alt="all-movies-link" href="/movies" className="text-sm px-2 border rounded-full text-blue-400 hover:text-blue-300">All movies →</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -26,6 +26,8 @@ import { StoryTemplateEntity } from '@/app/service/domain/Entities';
|
|||||||
import { useUploadFile } from '@/app/service/domain/service';
|
import { useUploadFile } from '@/app/service/domain/service';
|
||||||
import { useAppDispatch, useAppSelector } from '@/lib/store/hooks';
|
import { useAppDispatch, useAppSelector } from '@/lib/store/hooks';
|
||||||
import { clearSelection } from '@/lib/store/creationTemplateSlice';
|
import { clearSelection } from '@/lib/store/creationTemplateSlice';
|
||||||
|
import { Tooltip } from "antd";
|
||||||
|
import { useTypewriterText } from '@/hooks/useTypewriterText';
|
||||||
|
|
||||||
export default function VideoCreationForm() {
|
export default function VideoCreationForm() {
|
||||||
const [photos, setPhotos] = useState<PhotoItem[]>([]);
|
const [photos, setPhotos] = useState<PhotoItem[]>([]);
|
||||||
@ -75,6 +77,22 @@ export default function VideoCreationForm() {
|
|||||||
return isTemplateSelected.freeInput && isTemplateSelected.freeInput.length > 0;
|
return isTemplateSelected.freeInput && isTemplateSelected.freeInput.length > 0;
|
||||||
}, [isTemplateSelected]);
|
}, [isTemplateSelected]);
|
||||||
|
|
||||||
|
/** Dynamic placeholder typing from template freeInput tips */
|
||||||
|
const placeholderList = useMemo(() => {
|
||||||
|
if (isTemplateSelected?.freeInput?.length) {
|
||||||
|
return (isTemplateSelected.freeInput
|
||||||
|
.map((i) => i.user_tips)
|
||||||
|
.filter(Boolean) as string[]);
|
||||||
|
}
|
||||||
|
return ['Describe the story you want to make...'];
|
||||||
|
}, [isTemplateSelected]);
|
||||||
|
|
||||||
|
const dynamicPlaceholder = useTypewriterText(placeholderList, {
|
||||||
|
typingMs: 36,
|
||||||
|
pauseMs: 2200,
|
||||||
|
resetMs: 300,
|
||||||
|
});
|
||||||
|
|
||||||
const characterInputRef = useRef<HTMLInputElement>(null);
|
const characterInputRef = useRef<HTMLInputElement>(null);
|
||||||
const sceneInputRef = useRef<HTMLInputElement>(null);
|
const sceneInputRef = useRef<HTMLInputElement>(null);
|
||||||
const propInputRef = useRef<HTMLInputElement>(null);
|
const propInputRef = useRef<HTMLInputElement>(null);
|
||||||
@ -404,7 +422,10 @@ export default function VideoCreationForm() {
|
|||||||
data-alt="main-text-input"
|
data-alt="main-text-input"
|
||||||
ref={mainTextInputRef}
|
ref={mainTextInputRef}
|
||||||
className="w-full h-full bg-transparent text-gray-300 text-base placeholder:italic placeholder-gray-400 resize-none outline-none border-none"
|
className="w-full h-full bg-transparent text-gray-300 text-base placeholder:italic placeholder-gray-400 resize-none outline-none border-none"
|
||||||
placeholder={isTemplateSelected?.freeInput[0].user_tips || "Describe the story you want to make..."}
|
style={{
|
||||||
|
minHeight: '6.25rem'
|
||||||
|
}}
|
||||||
|
placeholder={dynamicPlaceholder}
|
||||||
value={inputText}
|
value={inputText}
|
||||||
onChange={(e) => setInputText(e.target.value)}
|
onChange={(e) => setInputText(e.target.value)}
|
||||||
/>
|
/>
|
||||||
@ -566,12 +587,14 @@ export default function VideoCreationForm() {
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
<Tooltip placement="top" title="Inspiration Lab">
|
||||||
<button
|
<button
|
||||||
data-alt="mention-button"
|
data-alt="mention-button"
|
||||||
className={`w-8 h-8 rounded-full border border-white/20 bg-transparent hover:bg-white/5 hover:border-cyan-400/60 transition-all duration-200 flex items-center justify-center text-gray-300 hover:text-cyan-400 ${isTemplateSelected ? 'text-yellow-500' : ''}`}
|
className={`w-8 h-8 rounded-full border border-white/20 bg-transparent hover:bg-white/5 hover:border-cyan-400/60 transition-all duration-200 flex items-center justify-center text-gray-300 hover:text-cyan-400 ${isTemplateSelected ? 'text-yellow-500' : ''}`}
|
||||||
>
|
>
|
||||||
<span className="text-base font-bold">@</span>
|
<span className="text-base font-bold">@</span>
|
||||||
</button>
|
</button>
|
||||||
|
</Tooltip>
|
||||||
</Popover>
|
</Popover>
|
||||||
|
|
||||||
{/* Configuration - Desktop: Full Panel, Mobile: Setting Icon */}
|
{/* Configuration - Desktop: Full Panel, Mobile: Setting Icon */}
|
||||||
|
|||||||
@ -5,10 +5,10 @@ import FamousTemplate from '@/components/FamousTemplate';
|
|||||||
|
|
||||||
export default function CreateVideo() {
|
export default function CreateVideo() {
|
||||||
return (
|
return (
|
||||||
<div data-alt="create-video-page" className="w-full">
|
<div data-alt="create-video-page" className="w-full pt-14">
|
||||||
<div className='p-1 text-center text-2xl font-bold tracking-tight sm:text-3xl'>Your idea. A movie. In minutes.</div>
|
<div className='p-1 text-center text-2xl font-bold tracking-tight sm:text-3xl'>Your idea. A movie. In minutes.</div>
|
||||||
<div className='mx-auto w-full max-w-[600px] px-3 py-2 text-center text-xs text-gray-400 sm:px-16 sm:text-sm'>Our AI turns sparks into full-blown stories — fast & free.</div>
|
<div className='mx-auto w-full max-w-[600px] px-3 py-4 text-center text-xs text-gray-400 sm:px-16 sm:text-sm'>Our AI turns sparks into full-blown stories — fast & free.</div>
|
||||||
<div className='py-2'>
|
<div className='pt-2 pb-12'>
|
||||||
<div className='space-y-4 mx-auto w-full max-w-[900px] px-3 sm:px-16 bg-[radial-gradient(ellipse_at_center,rgba(106,244,249,0.28)_0%,rgba(106,244,249,0.14)_35%,transparent_70%)]'>
|
<div className='space-y-4 mx-auto w-full max-w-[900px] px-3 sm:px-16 bg-[radial-gradient(ellipse_at_center,rgba(106,244,249,0.28)_0%,rgba(106,244,249,0.14)_35%,transparent_70%)]'>
|
||||||
<VideoCreationForm />
|
<VideoCreationForm />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
62
hooks/useTypewriterText.ts
Normal file
62
hooks/useTypewriterText.ts
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
import { useEffect, useMemo, useState } from 'react';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders a typewriter-style text that cycles through a list of strings.
|
||||||
|
* It types each string character-by-character, pauses, clears, then proceeds to the next.
|
||||||
|
* @param {string[]} strings - The list of sentences to cycle through.
|
||||||
|
* @param {{ typingMs?: number; pauseMs?: number; resetMs?: number }} [options] - Timing options.
|
||||||
|
* @returns {string} - The current text to display.
|
||||||
|
*/
|
||||||
|
export function useTypewriterText(
|
||||||
|
strings: string[],
|
||||||
|
options?: { typingMs?: number; pauseMs?: number; resetMs?: number }
|
||||||
|
): string {
|
||||||
|
const typingMs = options?.typingMs ?? 40;
|
||||||
|
const pauseMs = options?.pauseMs ?? 1200;
|
||||||
|
const resetMs = options?.resetMs ?? 300;
|
||||||
|
|
||||||
|
const [text, setText] = useState('');
|
||||||
|
const [listIndex, setListIndex] = useState(0);
|
||||||
|
const [charIndex, setCharIndex] = useState(0);
|
||||||
|
const [phase, setPhase] = useState<'typing' | 'pausing' | 'clearing'>('typing');
|
||||||
|
const key = useMemo(() => (strings && strings.length ? strings.join('|') : ''), [strings]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setText('');
|
||||||
|
setListIndex(0);
|
||||||
|
setCharIndex(0);
|
||||||
|
setPhase('typing');
|
||||||
|
}, [key]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const list = strings && strings.length ? strings : ['Describe the story you want to make...'];
|
||||||
|
const full = list[listIndex % list.length] ?? '';
|
||||||
|
let timer: number | undefined;
|
||||||
|
|
||||||
|
if (phase === 'typing') {
|
||||||
|
if (charIndex <= full.length) {
|
||||||
|
setText(full.slice(0, charIndex));
|
||||||
|
timer = window.setTimeout(() => setCharIndex(charIndex + 1), typingMs);
|
||||||
|
} else {
|
||||||
|
setPhase('pausing');
|
||||||
|
}
|
||||||
|
} else if (phase === 'pausing') {
|
||||||
|
timer = window.setTimeout(() => setPhase('clearing'), pauseMs);
|
||||||
|
} else {
|
||||||
|
setText('');
|
||||||
|
timer = window.setTimeout(() => {
|
||||||
|
setListIndex((listIndex + 1) % list.length);
|
||||||
|
setCharIndex(0);
|
||||||
|
setPhase('typing');
|
||||||
|
}, resetMs);
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (timer) window.clearTimeout(timer);
|
||||||
|
};
|
||||||
|
}, [strings, listIndex, charIndex, phase, typingMs, pauseMs, resetMs]);
|
||||||
|
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
Loading…
x
Reference in New Issue
Block a user