3ce9ab7bf1
Build and push image / build (push) Successful in 3m13s
Localized home pages (/es, /fr, /de, /it, /pt) with server-rendered intro, how-to-play and FAQ plus FAQPage/WebApplication JSON-LD and hreflang. Puzzle archive at /archive with answer pages (/puzzle/N) for past days only and replays (/play/N) that never touch stats or the daily streak. Challenge loop: shared results link back as /?challenge=<code> and the result screen shows a head-to-head verdict. Also adds robots.txt, sitemap.xml, the PWA manifest and icons, a compact share text with WhatsApp/X/copy channels, a next-puzzle countdown, the social-proof play counter and a spoiler-free /api/teaser for daily posting. Refs #1 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QvQ1ErZNRUcWgCEkJ9uyrn
30 lines
878 B
TypeScript
30 lines
878 B
TypeScript
"use client";
|
|
|
|
import { useEffect, useState } from "react";
|
|
import { formatCountdown, secondsUntilNextPuzzle } from "@/lib/time";
|
|
|
|
type CountdownProps = {
|
|
label: string;
|
|
};
|
|
|
|
export default function Countdown({ label }: CountdownProps) {
|
|
const [seconds, setSeconds] = useState<number | null>(null);
|
|
|
|
useEffect(() => {
|
|
const tick = () => setSeconds(secondsUntilNextPuzzle());
|
|
tick();
|
|
const interval = window.setInterval(tick, 1000);
|
|
return () => window.clearInterval(interval);
|
|
}, []);
|
|
|
|
if (seconds === null) return null;
|
|
|
|
return (
|
|
<p className="flex items-center justify-center gap-2 rounded-lg border border-white/10 bg-white/[0.04] px-3 py-2 text-xs font-bold text-white/70">
|
|
<span aria-hidden>⏳</span>
|
|
{label}
|
|
<span className="font-mono text-sm font-black text-emerald-200">{formatCountdown(seconds)}</span>
|
|
</p>
|
|
);
|
|
}
|