Spaces:
Sleeping
Sleeping
| import { useEffect, useMemo, useState } from 'react'; | |
| import type { DemoLocale } from './types'; | |
| type MarketingScene = { | |
| id: 'brief' | 'copy' | 'visual'; | |
| durationMs: number; | |
| }; | |
| type MarketingFeatureDemoProps = { | |
| locale: DemoLocale; | |
| isBusy: boolean; | |
| }; | |
| const COPY = { | |
| en: { | |
| title: 'Marketing Demo', | |
| busy: 'Running', | |
| hidden: 'Hidden', | |
| play: 'Play', | |
| pause: 'Pause', | |
| replay: 'Replay', | |
| hide: 'Hide', | |
| show: 'Show', | |
| videoName: 'marketing_demo.webm', | |
| step: 'Step', | |
| lanes: { | |
| brief: 'Brief', | |
| copy: 'Copy', | |
| visual: 'Visual', | |
| }, | |
| captions: { | |
| brief: 'Collect product + tone inputs', | |
| copy: 'Stream campaign copy blocks', | |
| visual: 'Generate image variations', | |
| }, | |
| words: { | |
| briefChips: ['Product', 'Tone', 'Channel'], | |
| copyChips: ['Headline', 'Caption', 'CTA', 'Tags'], | |
| visualCards: ['Hero', 'Social'], | |
| streamWords: ['Drafting', 'Refining', 'Scoring', 'Polishing'], | |
| visualImages: [ | |
| 'https://upload.wikimedia.org/wikipedia/commons/d/dd/Eggs_in_basket_2020_G1.jpg', | |
| 'https://upload.wikimedia.org/wikipedia/commons/c/c2/Fresh_Vegetables_2.jpg', | |
| ], | |
| }, | |
| }, | |
| 'zh-TW': { | |
| title: '行銷導覽', | |
| busy: '執行中', | |
| hidden: '已隱藏', | |
| play: '播放', | |
| pause: '暫停', | |
| replay: '重播', | |
| hide: '隱藏', | |
| show: '顯示', | |
| videoName: '行銷導覽影片.webm', | |
| step: '步驟', | |
| lanes: { | |
| brief: '需求', | |
| copy: '文案', | |
| visual: '素材', | |
| }, | |
| captions: { | |
| brief: '收集產品與語氣設定', | |
| copy: '串流活動文案區塊', | |
| visual: '生成多版本圖片', | |
| }, | |
| words: { | |
| briefChips: ['產品', '語氣', '渠道'], | |
| copyChips: ['標題', '內文', 'CTA', '標籤'], | |
| visualCards: ['主視覺', '社群版'], | |
| streamWords: ['生成', '潤稿', '評估', '優化'], | |
| visualImages: [ | |
| 'https://upload.wikimedia.org/wikipedia/commons/d/dd/Eggs_in_basket_2020_G1.jpg', | |
| 'https://upload.wikimedia.org/wikipedia/commons/c/c2/Fresh_Vegetables_2.jpg', | |
| ], | |
| }, | |
| }, | |
| } as const; | |
| const SCENES: MarketingScene[] = [ | |
| { id: 'brief', durationMs: 1650 }, | |
| { id: 'copy', durationMs: 1750 }, | |
| { id: 'visual', durationMs: 1700 }, | |
| ]; | |
| export default function MarketingFeatureDemo({ locale, isBusy }: MarketingFeatureDemoProps) { | |
| const copy = COPY[locale]; | |
| const scenes = useMemo(() => SCENES, []); | |
| const [sceneIndex, setSceneIndex] = useState(0); | |
| const [elapsedMs, setElapsedMs] = useState(0); | |
| const [isPlaying, setIsPlaying] = useState(true); | |
| const [isCollapsed, setIsCollapsed] = useState(false); | |
| const [reducedMotion, setReducedMotion] = useState(false); | |
| useEffect(() => { | |
| const media = window.matchMedia('(prefers-reduced-motion: reduce)'); | |
| const apply = () => setReducedMotion(media.matches); | |
| apply(); | |
| if (media.addEventListener) { | |
| media.addEventListener('change', apply); | |
| return () => media.removeEventListener('change', apply); | |
| } | |
| media.addListener(apply); | |
| return () => media.removeListener(apply); | |
| }, []); | |
| useEffect(() => { | |
| if (reducedMotion) setIsPlaying(false); | |
| }, [reducedMotion]); | |
| useEffect(() => { | |
| setSceneIndex(0); | |
| setElapsedMs(0); | |
| setIsCollapsed(false); | |
| if (!reducedMotion) setIsPlaying(true); | |
| }, [locale, reducedMotion]); | |
| useEffect(() => { | |
| if (!isBusy) return; | |
| setIsCollapsed(true); | |
| setIsPlaying(false); | |
| }, [isBusy]); | |
| useEffect(() => { | |
| if (!isPlaying || isCollapsed || isBusy || reducedMotion) return; | |
| const timer = window.setInterval(() => { | |
| setElapsedMs((prev) => { | |
| const scene = scenes[sceneIndex]; | |
| if (!scene) return 0; | |
| const next = prev + 120; | |
| if (next < scene.durationMs) return next; | |
| setSceneIndex((current) => (current + 1) % scenes.length); | |
| return 0; | |
| }); | |
| }, 120); | |
| return () => window.clearInterval(timer); | |
| }, [isPlaying, isCollapsed, isBusy, reducedMotion, scenes, sceneIndex]); | |
| const activeScene = scenes[sceneIndex] ?? scenes[0]; | |
| const totalDuration = scenes.reduce((sum, scene) => sum + scene.durationMs, 0); | |
| const previousDuration = scenes.slice(0, sceneIndex).reduce((sum, scene) => sum + scene.durationMs, 0); | |
| const progress = totalDuration > 0 ? Math.round(((previousDuration + elapsedMs) / totalDuration) * 100) : 0; | |
| const sceneCaption = copy.captions[activeScene.id]; | |
| const streamOffset = Math.floor(elapsedMs / 420); | |
| const activeStreamWords = copy.words.streamWords.map( | |
| (_word, index) => copy.words.streamWords[(streamOffset + index) % copy.words.streamWords.length] | |
| ); | |
| const replay = () => { | |
| setSceneIndex(0); | |
| setElapsedMs(0); | |
| if (!reducedMotion && !isBusy) setIsPlaying(true); | |
| }; | |
| if (isCollapsed) { | |
| return ( | |
| <section className="marketing-walk-collapsed" aria-live="polite"> | |
| <div className="marketing-walk-mini-markers" aria-hidden="true"> | |
| <span /> | |
| <span /> | |
| <span /> | |
| </div> | |
| <p>{isBusy ? copy.busy : copy.hidden}</p> | |
| {!isBusy ? ( | |
| <button | |
| type="button" | |
| className="ghost-btn" | |
| onClick={() => { | |
| setIsCollapsed(false); | |
| replay(); | |
| }} | |
| > | |
| {copy.show} | |
| </button> | |
| ) : null} | |
| </section> | |
| ); | |
| } | |
| return ( | |
| <section className="marketing-walk-video" aria-label={copy.title}> | |
| <header className="marketing-walk-head"> | |
| <div> | |
| <h3>{copy.title}</h3> | |
| <small> | |
| {copy.step} {sceneIndex + 1}/{scenes.length} | |
| </small> | |
| </div> | |
| <div className="marketing-walk-actions"> | |
| <button | |
| type="button" | |
| className="marketing-walk-icon-btn" | |
| onClick={() => setIsPlaying((prev) => !prev)} | |
| disabled={isBusy || reducedMotion} | |
| aria-label={isPlaying ? copy.pause : copy.play} | |
| > | |
| {isPlaying ? '||' : '>'} | |
| </button> | |
| <button type="button" className="marketing-walk-icon-btn" onClick={replay} disabled={isBusy} aria-label={copy.replay}> | |
| R | |
| </button> | |
| <button type="button" className="marketing-walk-icon-btn" onClick={() => setIsCollapsed(true)} aria-label={copy.hide}> | |
| X | |
| </button> | |
| </div> | |
| </header> | |
| <div className="marketing-walk-frame" data-scene={activeScene.id}> | |
| <div className="marketing-walk-windowbar"> | |
| <span /> | |
| <span /> | |
| <span /> | |
| <small>{copy.videoName}</small> | |
| </div> | |
| <div className="marketing-walk-canvas"> | |
| <section className="marketing-walk-lane brief"> | |
| <span className="marketing-walk-lane-index">1</span> | |
| <small className="marketing-walk-lane-label">{copy.lanes.brief}</small> | |
| <div className="marketing-walk-chip-row"> | |
| {copy.words.briefChips.map((item) => ( | |
| <span key={item}>{item}</span> | |
| ))} | |
| </div> | |
| <div className="marketing-walk-line lg" /> | |
| <div className="marketing-walk-line md" /> | |
| <p>{locale === 'zh-TW' ? '產品資訊與活動目標已整理。' : 'Product context and campaign goal prepared.'}</p> | |
| </section> | |
| <section className="marketing-walk-lane copy"> | |
| <span className="marketing-walk-lane-index">2</span> | |
| <small className="marketing-walk-lane-label">{copy.lanes.copy}</small> | |
| <div className="marketing-walk-copy-card"> | |
| {activeStreamWords.slice(0, 3).map((word, index) => ( | |
| <div key={`${word}-${index}`} className={`marketing-walk-stream-pill ${index === 0 ? 'lg' : index === 1 ? 'sm' : 'md'}`}> | |
| <span>{word}</span> | |
| </div> | |
| ))} | |
| </div> | |
| <div className="marketing-walk-chip-row"> | |
| {copy.words.copyChips.map((item) => ( | |
| <span key={item}>{item}</span> | |
| ))} | |
| </div> | |
| </section> | |
| <section className="marketing-walk-lane visual"> | |
| <span className="marketing-walk-lane-index">3</span> | |
| <small className="marketing-walk-lane-label">{copy.lanes.visual}</small> | |
| <div className="marketing-walk-visual-grid"> | |
| {copy.words.visualCards.map((label, index) => ( | |
| <article key={label}> | |
| <img | |
| src={copy.words.visualImages[index % copy.words.visualImages.length]} | |
| alt={label} | |
| loading="lazy" | |
| referrerPolicy="no-referrer" | |
| /> | |
| <small>{label}</small> | |
| </article> | |
| ))} | |
| </div> | |
| <p>{locale === 'zh-TW' ? '輸出可下載圖像與套用按鈕。' : 'Download-ready assets and apply actions.'}</p> | |
| </section> | |
| </div> | |
| </div> | |
| <footer className="marketing-walk-meta"> | |
| <div className="marketing-walk-progress-track" aria-hidden="true"> | |
| <span style={{ width: `${Math.min(100, Math.max(0, progress))}%` }} /> | |
| </div> | |
| <p className="marketing-walk-caption">{sceneCaption}</p> | |
| <div className="marketing-walk-scene-dots"> | |
| {scenes.map((scene, index) => ( | |
| <button | |
| key={scene.id} | |
| type="button" | |
| className={`marketing-walk-scene-dot ${index === sceneIndex ? 'active' : index < sceneIndex ? 'complete' : ''}`} | |
| onClick={() => { | |
| setSceneIndex(index); | |
| setElapsedMs(0); | |
| }} | |
| aria-label={`${copy.step} ${index + 1}`} | |
| > | |
| <span /> | |
| </button> | |
| ))} | |
| </div> | |
| </footer> | |
| </section> | |
| ); | |
| } | |