Files
hidden11/components/HomeClient.tsx
T
claude-code 3ce9ab7bf1
Build and push image / build (push) Successful in 3m13s
Add localized SEO pages, puzzle archive and challenge loop
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
2026-08-26 12:23:59 +00:00

674 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">
🎯 {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>
);
}