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 (
|
||||
<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
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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 */}
|
||||
|
||||
@ -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>
|
||||
|
||||
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