Files
hidden11/components/HomeClient.tsx
T
claude-code 0de85cbc51
Build and push image / build (push) Failing after 15m20s
Localize the archive, add reminders, named challenges and a funnel
Archive and answer pages now exist in all six languages and are prerendered
instead of rendered per request, and each answer page carries a sentence of
real context per player (club, league, nationality, former clubs, honours)
plus Article and BreadcrumbList JSON-LD. The sitemap grows from 121 to 691
URLs, every one with hreflang alternates.

Adds an optional display name to shared results, so a challenge link reads
"<name> challenges you" in the page, the OG image and the head-to-head
verdict; the name is sanitised both when written and when parsed back.

Adds an opt-in daily reminder (Web Push without payload, so no content
encryption) and a PWA install prompt shown once the day is finished, plus
first-party event ingestion at /api/events that mirrors GA4 so the funnel
survives ad blockers.

Wires the missing N8N_PLAYS_WEBHOOK_URL into the deployment along with the
new events, push and search-verification secrets, and ships the n8n
workflows, the Postgres schema with funnel/retention views, and an IndexNow
submitter.

Refs #2 #3 #5 #6 #7 #8 #9 #10

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QvQ1ErZNRUcWgCEkJ9uyrn
2026-08-26 12:41:28 +00:00

677 lines
26 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
import Link from "next/link";
import { useEffect, useMemo, useState } from "react";
import ClueList from "@/components/ClueList";
import Header from "@/components/Header";
import OnboardingModal from "@/components/OnboardingModal";
import Pitch from "@/components/Pitch";
import PlayerSearch from "@/components/PlayerSearch";
import FeedbackModal from "@/components/FeedbackModal";
import ShareResult from "@/components/ShareResult";
import SponsorSlot from "@/components/SponsorSlot";
import StatsPanel from "@/components/StatsPanel";
import { trackEvent } from "@/lib/analytics-events";
import { registerPlay } from "@/lib/plays";
import {
buildDifficultyShareText,
buildGlobalStatsShareText,
formatScoreLine,
parseSharePayload,
payloadScore,
type SharePayload,
} from "@/lib/share";
import { localePath } from "@/lib/seo-content";
import type { SponsorConfig } from "@/lib/sponsor";
import {
DIFFICULTIES,
MAX_ATTEMPTS,
POSITIONS,
getDailyPuzzles,
getFixedGuess,
getPuzzleNumber,
getTodayKey,
isAttemptComplete,
isSolved,
players,
validateGuess,
} from "@/lib/game-engine";
import { SUPPORTED_LOCALES, detectLocale, getDictionary, type Locale } from "@/lib/i18n";
import { quoteForDay } from "@/lib/quotes";
import {
createInitialProgress,
createInitialStats,
loadProgress,
loadStats,
nextDifficulty,
recordDailyStats,
saveProgress,
} from "@/lib/storage";
import type { DailyProgress, Difficulty, GlobalStats, Position } from "@/lib/types";
type HomeProps = {
sponsor: SponsorConfig;
initialLocale?: Locale;
dateKey?: string;
archive?: boolean;
};
export default function Home({ sponsor, initialLocale, dateKey: dateKeyProp, archive = false }: HomeProps) {
const dateKey = useMemo(() => dateKeyProp ?? getTodayKey(), [dateKeyProp]);
const dailyPuzzles = useMemo(() => getDailyPuzzles(dateKey), [dateKey]);
const [locale, setLocale] = useState<Locale>(initialLocale ?? "en");
const [progress, setProgress] = useState<DailyProgress>(() => createInitialProgress());
const [activeDifficulty, setActiveDifficulty] = useState<Difficulty>("easy");
const [cluesCollapsed, setCluesCollapsed] = useState(false);
const [fieldCollapsed, setFieldCollapsed] = useState(false);
const [shareOpen, setShareOpen] = useState(false);
const [onboardingOpen, setOnboardingOpen] = useState(false);
const [started, setStarted] = useState(false);
const [feedbackOpen, setFeedbackOpen] = useState(false);
const [statsOpen, setStatsOpen] = useState(false);
const [revealedSolutions, setRevealedSolutions] = useState<Partial<Record<Difficulty, boolean>>>({});
const [selectedPosition, setSelectedPosition] = useState<Position>("DEF");
const [stats, setStats] = useState<GlobalStats>(() => createInitialStats());
const [draft, setDraft] = useState<Record<Position, string | null>>(() => getFixedGuess(dailyPuzzles[0]));
const [message, setMessage] = useState("");
const [challenge, setChallenge] = useState<SharePayload | null>(null);
const [challengeExpired, setChallengeExpired] = useState(false);
const [earlyShare, setEarlyShare] = useState<Difficulty | null>(null);
const activePuzzle = dailyPuzzles.find((puzzle) => puzzle.difficulty === activeDifficulty) ?? dailyPuzzles[0];
const activeProgress = progress[activeDifficulty];
const isLocked = activeProgress.locked;
const isFinished = activeProgress.solved || activeProgress.attempts.length >= MAX_ATTEMPTS;
const hasFailed = !activeProgress.solved && activeProgress.attempts.length >= MAX_ATTEMPTS;
const solutionRevealed = revealedSolutions[activeDifficulty] === true;
const pitchDraft = solutionRevealed ? activePuzzle.solution : draft;
const t = useMemo(() => getDictionary(locale), [locale]);
const quote = useMemo(() => quoteForDay(dateKey, locale), [dateKey, locale]);
const globalStatsShareText = useMemo(() => buildGlobalStatsShareText(stats, t), [stats, t]);
const totalAttempts = DIFFICULTIES.reduce((sum, difficulty) => sum + progress[difficulty].attempts.length, 0);
useEffect(() => {
if (initialLocale) {
window.localStorage.setItem("hidden11-locale", initialLocale);
} else {
const savedLocale = window.localStorage.getItem("hidden11-locale") as Locale | null;
setLocale(savedLocale ?? detectLocale(window.navigator.language));
}
const loaded = loadProgress(dateKey);
setProgress(loaded);
setStats(loadStats());
const firstPlayable = DIFFICULTIES.find((difficulty) => !loaded[difficulty].locked) ?? "easy";
setActiveDifficulty(firstPlayable);
const seenOnboarding = window.localStorage.getItem("hidden11-onboarding-seen") === "true";
setOnboardingOpen(!seenOnboarding && DIFFICULTIES.every((difficulty) => loaded[difficulty].attempts.length === 0));
}, [dateKey, initialLocale]);
useEffect(() => {
const params = new URLSearchParams(window.location.search);
const code = params.get("challenge");
const storageKey = `hidden11-challenge-${dateKey}`;
if (code) {
const payload = parseSharePayload(code);
if (payload && payload.dateKey === dateKey) {
setChallenge(payload);
window.localStorage.setItem(storageKey, code);
trackEvent("challenge_accepted", { puzzle_number: payload.puzzleNumber });
} else if (payload) {
setChallengeExpired(true);
}
return;
}
const stored = window.localStorage.getItem(storageKey);
if (stored) {
const payload = parseSharePayload(stored);
if (payload && payload.dateKey === dateKey) setChallenge(payload);
}
}, [dateKey]);
useEffect(() => {
setDraft(getFixedGuess(activePuzzle));
const firstEmpty = POSITIONS.find((position) => position !== activePuzzle.fixedPosition) ?? "GK";
setSelectedPosition(firstEmpty);
setMessage("");
}, [activePuzzle]);
useEffect(() => {
if (isLocked || isFinished) return;
const interval = window.setInterval(() => {
setProgress((current) => {
const next = structuredClone(current);
next[activeDifficulty].elapsedSeconds += 1;
saveProgress(dateKey, next);
return next;
});
}, 1000);
return () => window.clearInterval(interval);
}, [activeDifficulty, dateKey, isFinished, isLocked]);
useEffect(() => {
if (!isFinished) return;
// Archive replays never touch global stats or the daily streak.
if (!archive) {
const nextStats = recordDailyStats(dateKey, progress);
setStats(nextStats);
}
setShareOpen(true);
if (DIFFICULTIES.every((difficulty) => progress[difficulty].solved)) {
const completionKey = `hidden11-daily-completed-event-${dateKey}`;
if (window.localStorage.getItem(completionKey) !== "true") {
trackEvent("daily_completed", {
puzzle_number: getPuzzleNumber(dateKey),
total_seconds: DIFFICULTIES.reduce((sum, difficulty) => sum + progress[difficulty].elapsedSeconds, 0),
});
window.localStorage.setItem(completionKey, "true");
}
}
}, [activeDifficulty, archive, dateKey, isFinished, progress]);
function updateProgress(next: DailyProgress) {
setProgress(next);
saveProgress(dateKey, next);
}
function startGame() {
setStarted(true);
window.localStorage.setItem("hidden11-onboarding-seen", "true");
setOnboardingOpen(false);
trackEvent("game_started", { puzzle_number: getPuzzleNumber(dateKey) });
if (!archive) registerPlay(dateKey);
window.requestAnimationFrame(() => document.getElementById("player-search")?.focus());
}
function selectDifficulty(difficulty: Difficulty) {
if (progress[difficulty].locked) return;
setActiveDifficulty(difficulty);
}
function placePlayer(playerId: string) {
if (isLocked || isFinished || selectedPosition === activePuzzle.fixedPosition) return;
if (!started && totalAttempts === 0) {
setStarted(true);
trackEvent("game_started", { puzzle_number: getPuzzleNumber(dateKey) });
if (!archive) registerPlay(dateKey);
}
setDraft((current) => ({ ...current, [selectedPosition]: playerId }));
}
function submitGuess() {
if (isLocked || isFinished) return;
if (!isAttemptComplete(draft)) {
setMessage(t.completeLineup);
return;
}
const guess = validateGuess(activePuzzle, draft);
const solved = isSolved(guess);
const next = structuredClone(progress);
next[activeDifficulty].attempts.push(guess);
next[activeDifficulty].solved = solved;
trackEvent("guess_submitted", {
attempts: next[activeDifficulty].attempts.length,
difficulty: activeDifficulty,
puzzle_number: getPuzzleNumber(dateKey),
solved,
});
if (solved) {
const unlocked = nextDifficulty(activeDifficulty);
if (unlocked) next[unlocked].locked = false;
if (unlocked) {
trackEvent("difficulty_unlocked", { difficulty: unlocked, puzzle_number: getPuzzleNumber(dateKey) });
}
setMessage(unlocked ? t.unlocked(t.difficulty[unlocked]) : t.allCompleted);
if (unlocked) {
setActiveDifficulty(unlocked);
// Prompt a share at the emotional peak, while the rest of the day is pending.
setEarlyShare(activeDifficulty);
}
} else if (next[activeDifficulty].attempts.length >= MAX_ATTEMPTS) {
setMessage(t.noAttemptsLeft);
} else {
setMessage(t.attemptsRemaining(MAX_ATTEMPTS - next[activeDifficulty].attempts.length));
}
updateProgress(next);
}
function resetDraft() {
setDraft(getFixedGuess(activePuzzle));
setSelectedPosition(POSITIONS.find((position) => position !== activePuzzle.fixedPosition) ?? "GK");
}
function revealHint() {
if (isLocked || isFinished) return;
const positionToReveal = POSITIONS.find(
(position) => position !== activePuzzle.fixedPosition && draft[position] !== activePuzzle.solution[position],
);
if (!positionToReveal) {
setMessage(t.noHintAvailable);
return;
}
setDraft((current) => ({
...current,
[positionToReveal]: activePuzzle.solution[positionToReveal],
}));
setSelectedPosition(positionToReveal);
setMessage(t.hintRevealed(positionToReveal));
}
function revealSolution() {
if (!hasFailed) return;
setRevealedSolutions((current) => ({ ...current, [activeDifficulty]: true }));
setDraft(activePuzzle.solution);
setMessage(t.solutionRevealed);
trackEvent("solution_revealed", {
difficulty: activeDifficulty,
puzzle_number: getPuzzleNumber(dateKey),
});
}
function changeLocale(nextLocale: Locale) {
setLocale(nextLocale);
window.localStorage.setItem("hidden11-locale", nextLocale);
}
async function shareEarlyResult(difficulty: Difficulty) {
const text = buildDifficultyShareText(dateKey, difficulty, progress, t);
const homeUrl = window.location.origin.replace(/\/$/, "");
trackEvent("share_clicked", {
channel: "early",
difficulty,
puzzle_number: getPuzzleNumber(dateKey),
});
if (navigator.share) {
try {
await navigator.share({ title: "hidden11", text, url: homeUrl });
} catch {
// user dismissed the native sheet
}
return;
}
window.open(
`https://twitter.com/intent/tweet?text=${encodeURIComponent(text)}&url=${encodeURIComponent(homeUrl)}`,
"_blank",
"noopener,noreferrer",
);
}
async function shareGlobalStats() {
const homeUrl = typeof window === "undefined" ? "https://hidden11.app" : window.location.origin.replace(/\/$/, "");
trackEvent("share_clicked", { source: "global_stats", games_played: stats.gamesPlayed });
if (navigator.share) {
await navigator.share({
title: "hidden11",
text: globalStatsShareText,
url: homeUrl,
});
return;
}
window.open(
`https://twitter.com/intent/tweet?text=${encodeURIComponent(globalStatsShareText)}&url=${encodeURIComponent(homeUrl)}`,
"_blank",
"noopener,noreferrer",
);
}
const actionButtons = (
<div className="flex items-center gap-1.5">
{hasFailed && !solutionRevealed ? (
<button
type="button"
onClick={revealSolution}
className="h-8 rounded-full border border-emerald-300/40 bg-emerald-300/15 px-3 text-[10px] font-black uppercase text-emerald-100 transition hover:bg-emerald-300/25"
>
{t.showSolution}
</button>
) : null}
<button
type="button"
onClick={revealHint}
disabled={isLocked || isFinished}
className="flex h-8 w-8 items-center justify-center rounded-full border border-yellow-300/40 bg-yellow-300/15 text-sm font-black text-yellow-100 transition hover:bg-yellow-300/25 disabled:cursor-not-allowed disabled:opacity-40"
aria-label={t.hint}
title={t.hint}
>
?
</button>
<button
type="button"
onClick={submitGuess}
disabled={isLocked || isFinished}
className="flex h-9 w-9 items-center justify-center rounded-full bg-white text-base font-black text-pitch-950 shadow-lg transition hover:bg-emerald-100 disabled:cursor-not-allowed disabled:opacity-40"
aria-label={t.validate}
title={t.validate}
>
</button>
<button
type="button"
onClick={resetDraft}
disabled={isLocked || isFinished}
className="flex h-8 w-8 items-center justify-center rounded-full border border-white/15 bg-white/[0.06] text-sm font-black text-white transition hover:bg-white/10 disabled:cursor-not-allowed disabled:opacity-40"
aria-label={t.reset}
title={t.reset}
>
</button>
</div>
);
return (
<main className="min-h-screen bg-[radial-gradient(circle_at_top,#115c39_0,#061b12_45%,#04110c_100%)]">
<div className="mx-auto flex min-h-screen w-full max-w-5xl flex-col px-4 py-5 sm:px-6 lg:px-8">
<Header
attemptsUsed={activeProgress.attempts.length}
dateKey={dateKey}
elapsedSeconds={activeProgress.elapsedSeconds}
solved={activeProgress.solved}
t={t}
/>
<div className="mt-3 flex justify-end">
<button
type="button"
onClick={() => setStatsOpen(true)}
className="rounded-full border border-white/10 bg-white/[0.06] px-3 py-1.5 text-xs font-black uppercase text-white/65 transition hover:bg-white/10 hover:text-white"
>
{t.stats}
</button>
</div>
{totalAttempts === 0 && !started ? (
<section className="mt-3 rounded-lg border border-emerald-300/25 bg-emerald-300/10 p-3">
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<div>
<p className="text-sm font-black text-white">{t.firstMoveTitle}</p>
<p className="mt-1 text-xs font-semibold leading-5 text-emerald-50/75">{t.firstMoveLead}</p>
<div className="mt-2 flex gap-1">
<span className="h-4 w-4 rounded bg-emerald-400" title={t.exact} />
<span className="h-4 w-4 rounded bg-yellow-400" title={t.close} />
<span className="h-4 w-4 rounded bg-slate-500" title={t.miss} />
</div>
</div>
<button
type="button"
onClick={startGame}
className="h-10 rounded-lg bg-emerald-400 px-4 text-xs font-black uppercase text-pitch-950 transition hover:bg-emerald-300"
>
{t.playToday}
</button>
</div>
</section>
) : null}
<div className="mt-3 rounded-lg border border-white/10 bg-white/[0.04] px-3 py-2">
<p className="truncate text-xs font-semibold text-white/65">
{quote.text[locale]} <span className="text-white/35"> {quote.author}</span>
</p>
</div>
{challenge ? (
<div className="mt-3 rounded-lg border border-yellow-300/30 bg-yellow-300/10 px-3 py-2">
<p className="text-xs font-bold text-yellow-100">
🎯{" "}
{challenge.name
? t.challengeLeadNamed(challenge.name, formatScoreLine(payloadScore(challenge)))
: t.challengeLead(formatScoreLine(payloadScore(challenge)))}
</p>
</div>
) : null}
{challengeExpired ? (
<div className="mt-3 rounded-lg border border-white/10 bg-white/[0.06] px-3 py-2">
<p className="text-xs font-bold text-white/70"> {t.challengeExpired}</p>
</div>
) : null}
<div className="mt-3">
<SponsorSlot placement="home" sponsor={sponsor} />
</div>
<section
className="mt-3 overflow-hidden rounded-lg border border-white/10 bg-white/[0.06]"
aria-label={t.puzzle}
>
<div className="flex items-stretch">
{DIFFICULTIES.map((difficulty) => (
<button
key={difficulty}
type="button"
onClick={() => selectDifficulty(difficulty)}
className={`relative flex min-h-10 flex-1 flex-col items-center justify-center px-3 py-1.5 text-center transition after:absolute after:right-[-8px] after:top-0 after:z-10 after:h-full after:w-4 after:skew-x-[-18deg] after:border-r after:border-white/10 after:content-[''] last:after:hidden ${
activeDifficulty === difficulty
? "bg-white text-pitch-950 after:bg-white"
: "text-white hover:bg-white/10 after:bg-white/[0.06]"
} ${progress[difficulty].locked ? "cursor-not-allowed opacity-45" : ""}`}
title={
progress[difficulty].locked
? `${t.difficulty[difficulty]} ${t.locked}`
: progress[difficulty].solved
? `${t.difficulty[difficulty]} ${t.solved}`
: `${t.difficulty[difficulty]} ${t.available}`
}
>
<span className="flex items-center justify-center gap-1 text-xs font-black uppercase sm:text-sm">
<span>{t.difficulty[difficulty]}</span>
{progress[difficulty].locked
? "🔒"
: progress[difficulty].solved
? "✓"
: null}
</span>
<span className="mt-0.5 block text-[10px] font-bold opacity-65 sm:text-xs">
{progress[difficulty].locked
? t.locked
: progress[difficulty].solved
? `${progress[difficulty].attempts.length}/${MAX_ATTEMPTS}`
: t.left(MAX_ATTEMPTS - progress[difficulty].attempts.length)}
</span>
</button>
))}
</div>
{message ? <p className="border-t border-white/10 px-2 py-1.5 text-xs font-semibold text-emerald-200">{message}</p> : null}
</section>
{earlyShare ? (
<div className="mt-3 flex items-center justify-between gap-3 rounded-lg border border-emerald-300/30 bg-emerald-300/10 px-3 py-2">
<p className="text-xs font-bold leading-5 text-emerald-100">
🎉 {t.earlySharePrompt(t.difficulty[earlyShare], `${progress[earlyShare].attempts.length}/${MAX_ATTEMPTS}`)}
</p>
<div className="flex flex-none items-center gap-1.5">
<button
type="button"
onClick={() => shareEarlyResult(earlyShare)}
className="h-8 rounded-full bg-emerald-400 px-3 text-[10px] font-black uppercase text-pitch-950 transition hover:bg-emerald-300"
>
{t.shareNow}
</button>
<button
type="button"
onClick={() => setEarlyShare(null)}
className="flex h-8 w-8 items-center justify-center rounded-full border border-white/15 text-sm font-black text-white transition hover:bg-white/10"
aria-label={t.close}
>
×
</button>
</div>
</div>
) : null}
<div className="mt-4">
<PlayerSearch
disabled={isLocked || isFinished}
difficulty={activeDifficulty}
fixedPosition={activePuzzle.fixedPosition}
players={players}
selectedPosition={selectedPosition}
t={t}
onSelectPosition={setSelectedPosition}
onSelect={placePlayer}
/>
</div>
<div className="mt-5 grid flex-1 gap-5 lg:grid-cols-[minmax(0,1.1fr)_minmax(320px,0.9fr)]">
<section className="min-w-0 lg:order-1">
<div className="mb-4 lg:hidden">
<ClueList
clues={activePuzzle.clues}
collapsed={cluesCollapsed}
onToggleCollapsed={() => setCluesCollapsed((current) => !current)}
t={t}
/>
</div>
<Pitch
actions={actionButtons}
collapsed={fieldCollapsed}
draft={pitchDraft}
fixedPosition={activePuzzle.fixedPosition}
lastGuess={activeProgress.attempts[activeProgress.attempts.length - 1]}
onToggleCollapsed={() => setFieldCollapsed((current) => !current)}
selectedPosition={selectedPosition}
solutionRevealed={solutionRevealed}
t={t}
onSelectPosition={setSelectedPosition}
/>
</section>
<aside className="flex min-w-0 flex-col gap-4 lg:order-2">
<div className="hidden lg:block">
<ClueList
clues={activePuzzle.clues}
collapsed={cluesCollapsed}
onToggleCollapsed={() => setCluesCollapsed((current) => !current)}
t={t}
/>
</div>
</aside>
</div>
{isFinished && shareOpen ? (
<ShareResult
challenge={challenge}
dateKey={dateKey}
locale={locale}
onClose={() => setShareOpen(false)}
progress={progress}
quote={quote}
stats={stats}
t={t}
onOpenStats={() => setStatsOpen(true)}
sponsor={sponsor}
/>
) : null}
{statsOpen ? (
<div className="fixed inset-0 z-50 flex items-end bg-black/70 p-3 backdrop-blur-sm sm:items-center sm:justify-center">
<div className="w-full rounded-lg border border-white/10 bg-pitch-950 p-4 shadow-2xl sm:max-w-lg">
<div className="mb-3 flex items-center justify-between">
<h2 className="text-lg font-black text-white">{t.stats}</h2>
<div className="flex items-center gap-2">
<button
type="button"
onClick={shareGlobalStats}
className="h-9 rounded-full bg-emerald-400 px-4 text-xs font-black uppercase tracking-wide text-pitch-950 transition hover:bg-emerald-300"
>
{t.shareNow}
</button>
<button
type="button"
onClick={() => setStatsOpen(false)}
className="h-9 w-9 rounded-full border border-white/15 text-lg font-black text-white transition hover:bg-white/10"
aria-label={t.closeModal}
>
×
</button>
</div>
</div>
<StatsPanel showLead={false} stats={stats} t={t} />
</div>
</div>
) : null}
{feedbackOpen ? (
<FeedbackModal dateKey={dateKey} onClose={() => setFeedbackOpen(false)} t={t} />
) : null}
{onboardingOpen ? (
<OnboardingModal
onClose={() => {
window.localStorage.setItem("hidden11-onboarding-seen", "true");
setOnboardingOpen(false);
}}
onStart={startGame}
t={t}
/>
) : null}
<footer className="mt-8 flex flex-wrap items-center justify-center gap-2 pb-4 text-xs text-white/45">
<span className="mr-1 font-semibold uppercase">{t.language}</span>
{SUPPORTED_LOCALES.map((item) =>
archive ? (
<button
key={item.code}
type="button"
onClick={() => changeLocale(item.code)}
className={`rounded px-2 py-1 font-bold transition ${
locale === item.code
? "bg-white text-pitch-950"
: "border border-white/10 text-white/60 hover:bg-white/10 hover:text-white"
}`}
>
{item.label}
</button>
) : (
// Crawlable links between the localized home pages (hreflang discovery).
<a
key={item.code}
href={localePath(item.code)}
onClick={() => window.localStorage.setItem("hidden11-locale", item.code)}
className={`rounded px-2 py-1 font-bold transition ${
locale === item.code
? "bg-white text-pitch-950"
: "border border-white/10 text-white/60 hover:bg-white/10 hover:text-white"
}`}
>
{item.label}
</a>
),
)}
<Link
href="/archive"
className="rounded px-2 py-1 font-bold transition border border-white/10 text-white/60 hover:bg-white/10 hover:text-white"
>
Archive
</Link>
<button
type="button"
onClick={() => setFeedbackOpen(true)}
className="rounded px-2 py-1 font-bold transition border border-white/10 text-white/60 hover:bg-white/10 hover:text-white"
>
{t.feedback}
</button>
</footer>
</div>
</main>
);
}