|
| 1 | +"use client"; |
| 2 | + |
| 3 | +import { useEffect, useState } from "react"; |
| 4 | +import { useReducedMotion } from "framer-motion"; |
| 5 | + |
| 6 | +import { NoteGenProductShowcase } from "@/app/[lang]/(home)/hero"; |
| 7 | + |
| 8 | +type DemoLanguage = "cn" | "en"; |
| 9 | + |
| 10 | +const demoCopy = { |
| 11 | + cn: { |
| 12 | + section: "晚餐安排", |
| 13 | + sentence: "湖滨餐厅已预约 19:00 靠窗座位。", |
| 14 | + mobileTitle: "周末杭州行程", |
| 15 | + mobileIntro: "两天以西湖、象山和城西为主,减少跨区往返。", |
| 16 | + mobileOverview: "行程概览", |
| 17 | + mobilePoints: ["周六:西湖慢走、象山校区", "周日:法喜寺、天目里"], |
| 18 | + mobileSection: "晚餐安排", |
| 19 | + mobileDay: "周六 · 西湖与象山", |
| 20 | + mobileSchedule: [ |
| 21 | + ["09:00–12:00", "西湖慢走", "从曲院风荷出发,沿北山街走到孤山。"], |
| 22 | + ["12:15–13:30", "午餐", "在孤山附近简单吃面,避开热门餐厅排队。"], |
| 23 | + ["15:00–17:30", "中国美院象山校区", "从西湖打车前往象山,重点看建筑与公共空间。"], |
| 24 | + ["19:00", "杭帮菜晚餐", "选择收藏里的餐厅,提前预约靠窗座位。"], |
| 25 | + ], |
| 26 | + mobileNextDay: "周日 · 法喜寺与天目里", |
| 27 | + remoteDevice: "iPhone", |
| 28 | + }, |
| 29 | + en: { |
| 30 | + section: "Dinner plan", |
| 31 | + sentence: "Window table booked by the lake for 19:00.", |
| 32 | + mobileTitle: "Weekend in Hangzhou", |
| 33 | + mobileIntro: "Two relaxed days around West Lake, Xiangshan, and the west side.", |
| 34 | + mobileOverview: "Itinerary overview", |
| 35 | + mobilePoints: ["Saturday: West Lake and Xiangshan", "Sunday: Faxi Temple and Tianmuli"], |
| 36 | + mobileSection: "Dinner plan", |
| 37 | + mobileDay: "Saturday · West Lake and Xiangshan", |
| 38 | + mobileSchedule: [ |
| 39 | + ["09:00–12:00", "West Lake walk", "Start at Quyuan Garden and follow Beishan Road to Solitary Hill."], |
| 40 | + ["12:15–13:30", "Lunch", "Grab noodles near Solitary Hill and skip the busiest queues."], |
| 41 | + ["15:00–17:30", "Xiangshan campus", "Explore the architecture and public spaces around the campus."], |
| 42 | + ["19:00", "Hangzhou dinner", "Use the saved restaurant and reserve a window table."], |
| 43 | + ], |
| 44 | + mobileNextDay: "Sunday · Faxi Temple and Tianmuli", |
| 45 | + remoteDevice: "iPhone", |
| 46 | + }, |
| 47 | +} as const; |
| 48 | + |
| 49 | +export function MarkdownSyncDemo({ lang }: { lang: DemoLanguage }) { |
| 50 | + const copy = demoCopy[lang]; |
| 51 | + const reduceMotion = useReducedMotion(); |
| 52 | + const [tick, setTick] = useState(0); |
| 53 | + const sentenceLength = Array.from(copy.sentence).length; |
| 54 | + |
| 55 | + useEffect(() => { |
| 56 | + if (reduceMotion) return; |
| 57 | + const timer = window.setInterval(() => setTick((current) => current + 1), 100); |
| 58 | + return () => window.clearInterval(timer); |
| 59 | + }, [reduceMotion]); |
| 60 | + |
| 61 | + const cycleLength = sentenceLength + 25; |
| 62 | + const cycle = reduceMotion ? sentenceLength + 10 : tick % cycleLength; |
| 63 | + const mobileCount = cycle < sentenceLength |
| 64 | + ? cycle |
| 65 | + : cycle < sentenceLength + 15 |
| 66 | + ? sentenceLength |
| 67 | + : 0; |
| 68 | + const desktopCount = cycle < 5 |
| 69 | + ? 0 |
| 70 | + : cycle < sentenceLength + 5 |
| 71 | + ? Math.min(cycle - 5, sentenceLength) |
| 72 | + : cycle < sentenceLength + 15 |
| 73 | + ? sentenceLength |
| 74 | + : 0; |
| 75 | + const mobileText = Array.from(copy.sentence).slice(0, mobileCount).join(""); |
| 76 | + const desktopText = Array.from(copy.sentence).slice(0, desktopCount).join(""); |
| 77 | + |
| 78 | + return ( |
| 79 | + <div className="relative mx-auto w-full max-w-6xl overflow-visible bg-background"> |
| 80 | + <NoteGenProductShowcase |
| 81 | + className="block h-auto w-full object-contain" |
| 82 | + priority |
| 83 | + /> |
| 84 | + |
| 85 | + <div className="absolute bottom-[11%] left-[30.6%] top-[53.5%] w-[31.5%] overflow-hidden bg-background pr-[2%]"> |
| 86 | + <h4 className="mb-[2.2%] text-[clamp(6px,0.9vw,13px)] font-semibold">{copy.section}</h4> |
| 87 | + <p className="min-h-[1.5em] text-[clamp(5px,0.72vw,10px)] leading-relaxed text-muted-foreground"> |
| 88 | + {desktopText} |
| 89 | + {desktopCount > 0 && desktopCount < sentenceLength ? <RemoteCursor label={copy.remoteDevice} /> : null} |
| 90 | + </p> |
| 91 | + <div className="mt-[6%] flex flex-col gap-[3%] text-[clamp(4px,0.57vw,8px)] leading-relaxed text-muted-foreground"> |
| 92 | + {copy.mobileSchedule.map(([time, item, description]) => ( |
| 93 | + <div key={time}> |
| 94 | + <p className="font-semibold text-foreground"><span>{time}</span> {item}</p> |
| 95 | + <p className="mt-[1.5%]">{description}</p> |
| 96 | + </div> |
| 97 | + ))} |
| 98 | + </div> |
| 99 | + <h4 className="mt-[5%] border-t pt-[3%] text-[clamp(5px,0.72vw,10px)] font-semibold">{copy.mobileNextDay}</h4> |
| 100 | + </div> |
| 101 | + |
| 102 | + <div className="absolute bottom-[9.7%] left-[77.2%] top-[35.1%] w-[19.7%] overflow-hidden bg-background px-[1.5%] py-[2.4%]"> |
| 103 | + <div className="flex h-full flex-col"> |
| 104 | + <h3 className="text-[clamp(7px,1.05vw,15px)] font-bold tracking-tight">{copy.mobileTitle}</h3> |
| 105 | + <p className="mt-[6%] text-[clamp(5px,0.65vw,9px)] leading-relaxed text-muted-foreground">{copy.mobileIntro}</p> |
| 106 | + <h4 className="mt-[8%] text-[clamp(5px,0.72vw,10px)] font-semibold">{copy.mobileOverview}</h4> |
| 107 | + <ul className="mt-[3%] flex list-disc flex-col gap-1 pl-[7%] text-[clamp(5px,0.6vw,8px)] leading-relaxed text-muted-foreground"> |
| 108 | + {copy.mobilePoints.map((point) => <li key={point}>{point}</li>)} |
| 109 | + </ul> |
| 110 | + <h4 className="mt-[10%] text-[clamp(5px,0.72vw,10px)] font-semibold">{copy.mobileSection}</h4> |
| 111 | + <p className="mt-[4%] min-h-[3em] text-[clamp(5px,0.65vw,9px)] leading-relaxed text-muted-foreground"> |
| 112 | + {mobileText} |
| 113 | + {mobileCount > 0 && mobileCount < sentenceLength ? <LocalCaret /> : null} |
| 114 | + </p> |
| 115 | + <h4 className="mt-[8%] text-[clamp(5px,0.72vw,10px)] font-semibold">{copy.mobileDay}</h4> |
| 116 | + <div className="mt-[3%] flex flex-col gap-1.5 text-[clamp(4px,0.55vw,8px)] leading-relaxed text-muted-foreground"> |
| 117 | + {copy.mobileSchedule.map(([time, item]) => ( |
| 118 | + <p key={time}><span className="font-semibold text-foreground">{time}</span> {item}</p> |
| 119 | + ))} |
| 120 | + </div> |
| 121 | + <h4 className="mt-[8%] border-t pt-[5%] text-[clamp(5px,0.72vw,10px)] font-semibold">{copy.mobileNextDay}</h4> |
| 122 | + </div> |
| 123 | + </div> |
| 124 | + </div> |
| 125 | + ); |
| 126 | +} |
| 127 | + |
| 128 | +function RemoteCursor({ label }: { label: string }) { |
| 129 | + const color = "#2563eb"; |
| 130 | + |
| 131 | + return ( |
| 132 | + <span |
| 133 | + className="relative inline-block h-[1.25em] w-0 -ml-px border-l-2 align-text-bottom opacity-50 transition-opacity duration-150 hover:opacity-100" |
| 134 | + style={{ borderColor: color }} |
| 135 | + aria-label={`${label} cursor`} |
| 136 | + > |
| 137 | + <span |
| 138 | + className="absolute bottom-[calc(100%+2px)] left-[-2px] max-w-32 overflow-hidden whitespace-nowrap rounded-[4px_4px_4px_0] px-[5px] py-px text-[clamp(5px,0.6vw,9px)] font-semibold leading-tight text-white" |
| 139 | + style={{ backgroundColor: color }} |
| 140 | + > |
| 141 | + {label} |
| 142 | + </span> |
| 143 | + </span> |
| 144 | + ); |
| 145 | +} |
| 146 | + |
| 147 | +function LocalCaret() { |
| 148 | + return ( |
| 149 | + <span |
| 150 | + aria-hidden="true" |
| 151 | + className="ml-px inline-block h-[1.15em] w-px animate-pulse bg-foreground align-text-bottom" |
| 152 | + /> |
| 153 | + ); |
| 154 | +} |
0 commit comments