更新 模板样式和input逐字效果

This commit is contained in:
moux1024 2025-10-22 20:24:01 +08:00
parent 5eed018991
commit f1c0414915
4 changed files with 85 additions and 6 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

@ -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

@ -27,6 +27,7 @@ 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[]>([]);
@ -76,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);
@ -405,7 +422,7 @@ 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..."}
placeholder={dynamicPlaceholder}
value={inputText}
onChange={(e) => setInputText(e.target.value)}
/>

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