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
53 lines
1.9 KiB
TypeScript
53 lines
1.9 KiB
TypeScript
export const SERVER_TIME_ZONE = process.env.NEXT_PUBLIC_SERVER_TIME_ZONE ?? "Europe/Madrid";
|
|
|
|
export function getDateKeyInTimeZone(date = new Date(), timeZone = SERVER_TIME_ZONE): string {
|
|
const formatter = new Intl.DateTimeFormat("en-CA", {
|
|
timeZone,
|
|
year: "numeric",
|
|
month: "2-digit",
|
|
day: "2-digit",
|
|
});
|
|
|
|
const parts = formatter.formatToParts(date);
|
|
const year = parts.find((part) => part.type === "year")?.value;
|
|
const month = parts.find((part) => part.type === "month")?.value;
|
|
const day = parts.find((part) => part.type === "day")?.value;
|
|
|
|
if (!year || !month || !day) {
|
|
return date.toISOString().slice(0, 10);
|
|
}
|
|
|
|
return `${year}-${month}-${day}`;
|
|
}
|
|
|
|
export function secondsUntilNextPuzzle(now = new Date(), timeZone = SERVER_TIME_ZONE): number {
|
|
const formatter = new Intl.DateTimeFormat("en-GB", {
|
|
timeZone,
|
|
hour12: false,
|
|
hour: "2-digit",
|
|
minute: "2-digit",
|
|
second: "2-digit",
|
|
});
|
|
const parts = formatter.formatToParts(now);
|
|
const hour = Number(parts.find((part) => part.type === "hour")?.value ?? 0) % 24;
|
|
const minute = Number(parts.find((part) => part.type === "minute")?.value ?? 0);
|
|
const second = Number(parts.find((part) => part.type === "second")?.value ?? 0);
|
|
const elapsedToday = hour * 3600 + minute * 60 + second;
|
|
return Math.max(1, 86400 - elapsedToday);
|
|
}
|
|
|
|
export function formatCountdown(totalSeconds: number): string {
|
|
const hours = Math.floor(totalSeconds / 3600);
|
|
const minutes = Math.floor((totalSeconds % 3600) / 60);
|
|
const seconds = totalSeconds % 60;
|
|
return `${hours.toString().padStart(2, "0")}:${minutes.toString().padStart(2, "0")}:${seconds
|
|
.toString()
|
|
.padStart(2, "0")}`;
|
|
}
|
|
|
|
export function formatElapsed(seconds: number): string {
|
|
const minutes = Math.floor(seconds / 60);
|
|
const remainder = seconds % 60;
|
|
return `${minutes}:${remainder.toString().padStart(2, "0")}`;
|
|
}
|