This commit is contained in:
非凡主儿 2025-10-22 21:03:55 +08:00
commit 4032b642db
6 changed files with 103 additions and 18 deletions

View File

@ -46,7 +46,7 @@ const FamousTemplate: React.FC<FamousTemplateProps> = ({ showTabs = true }) => {
return (
<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">
Inspiration Lab
</h2>
@ -58,7 +58,7 @@ const FamousTemplate: React.FC<FamousTemplateProps> = ({ showTabs = true }) => {
type="button"
data-alt={`template-tab-${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
? "border-white/60 bg-white/80 text-slate-900"
: "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
data-alt="template-item"
key={t.id}
className="relative h-40 group"
className="relative h-40 group [container-type:inline-size]"
onMouseEnter={(e) => {
const v = e.currentTarget.querySelector('video') as HTMLVideoElement | null
v?.play?.()
@ -150,7 +150,7 @@ const FamousTemplate: React.FC<FamousTemplateProps> = ({ showTabs = true }) => {
<button
type="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>) => {
e.stopPropagation()
const id = t.id || t.template_id

View File

@ -5,11 +5,11 @@ import { useDeviceType } from '@/hooks/useDeviceType';
export default function HomeBanner() {
const { isDesktop } = useDeviceType();
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 */}
<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 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 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] 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 />
</div>
</div>

View File

@ -61,7 +61,7 @@ const MyMovies: React.FC = () => {
return (
<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>
<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>

View File

@ -26,6 +26,8 @@ import { StoryTemplateEntity } from '@/app/service/domain/Entities';
import { useUploadFile } from '@/app/service/domain/service';
import { useAppDispatch, useAppSelector } from '@/lib/store/hooks';
import { clearSelection } from '@/lib/store/creationTemplateSlice';
import { Tooltip } from "antd";
import { useTypewriterText } from '@/hooks/useTypewriterText';
export default function VideoCreationForm() {
const [photos, setPhotos] = useState<PhotoItem[]>([]);
@ -75,6 +77,22 @@ export default function VideoCreationForm() {
return isTemplateSelected.freeInput && isTemplateSelected.freeInput.length > 0;
}, [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 sceneInputRef = useRef<HTMLInputElement>(null);
const propInputRef = useRef<HTMLInputElement>(null);
@ -404,7 +422,10 @@ export default function VideoCreationForm() {
data-alt="main-text-input"
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"
placeholder={isTemplateSelected?.freeInput[0].user_tips || "Describe the story you want to make..."}
style={{
minHeight: '6.25rem'
}}
placeholder={dynamicPlaceholder}
value={inputText}
onChange={(e) => setInputText(e.target.value)}
/>
@ -566,12 +587,14 @@ export default function VideoCreationForm() {
</div>
}
>
<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' : ''}`}
>
<span className="text-base font-bold">@</span>
</button>
<Tooltip placement="top" title="Inspiration Lab">
<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' : ''}`}
>
<span className="text-base font-bold">@</span>
</button>
</Tooltip>
</Popover>
{/* Configuration - Desktop: Full Panel, Mobile: Setting Icon */}

View File

@ -5,10 +5,10 @@ import FamousTemplate from '@/components/FamousTemplate';
export default function CreateVideo() {
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='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='py-2'>
<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='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%)]'>
<VideoCreationForm />
</div>

View 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;
}