This commit is contained in:
+15
-6
@@ -3,20 +3,29 @@ import type { ReactNode } from "react";
|
||||
import "./globals.css";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "hidden11",
|
||||
description: "Daily football lineup puzzle with evolving difficulty, sharper clues and shareable results.",
|
||||
title: {
|
||||
default: "hidden11",
|
||||
template: "%s | hidden11",
|
||||
},
|
||||
description: "Daily football lineup puzzle with sharper clues, harder challenges and shareable results.",
|
||||
metadataBase: new URL("https://hidden11.app"),
|
||||
applicationName: "hidden11",
|
||||
alternates: {
|
||||
canonical: "https://hidden11.app",
|
||||
},
|
||||
keywords: ["football puzzle", "soccer puzzle", "daily game", "lineup puzzle", "hidden11"],
|
||||
icons: {
|
||||
icon: "/favicon.svg",
|
||||
},
|
||||
openGraph: {
|
||||
title: "hidden11",
|
||||
description: "Daily football lineup puzzle. Beat Easy, Medium and Hard, then share the card.",
|
||||
description: "Beat Easy, Medium and Hard in the daily football lineup puzzle and share the result.",
|
||||
url: "https://hidden11.app",
|
||||
siteName: "hidden11",
|
||||
locale: "en_US",
|
||||
images: [
|
||||
{
|
||||
url: "/opengraph-image",
|
||||
url: "https://hidden11.app/opengraph-image",
|
||||
width: 1200,
|
||||
height: 630,
|
||||
alt: "hidden11 daily football puzzle",
|
||||
@@ -27,8 +36,8 @@ export const metadata: Metadata = {
|
||||
twitter: {
|
||||
card: "summary_large_image",
|
||||
title: "hidden11",
|
||||
description: "Daily football lineup puzzle. Beat Easy, Medium and Hard, then share the card.",
|
||||
images: ["/twitter-image"],
|
||||
description: "Beat Easy, Medium and Hard in the daily football lineup puzzle and share the result.",
|
||||
images: ["https://hidden11.app/twitter-image"],
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
+136
-48
@@ -15,48 +15,96 @@ export default function OpenGraphImage() {
|
||||
display: "flex",
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
background:
|
||||
"radial-gradient(circle at top, #1f8f5d 0%, #0a2e1f 35%, #04110c 100%)",
|
||||
background: "linear-gradient(135deg, #04110c 0%, #0d2d1f 42%, #145c3a 100%)",
|
||||
color: "#f7fff9",
|
||||
padding: 48,
|
||||
padding: 42,
|
||||
fontFamily: "Arial",
|
||||
position: "relative",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: -90,
|
||||
right: -90,
|
||||
width: 320,
|
||||
height: 320,
|
||||
borderRadius: 999,
|
||||
background: "rgba(62, 230, 150, 0.18)",
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
bottom: -120,
|
||||
left: -80,
|
||||
width: 360,
|
||||
height: 360,
|
||||
borderRadius: 999,
|
||||
background: "rgba(250, 204, 21, 0.12)",
|
||||
}}
|
||||
/>
|
||||
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
width: "100%",
|
||||
border: "1px solid rgba(255,255,255,0.14)",
|
||||
borderRadius: 28,
|
||||
padding: 36,
|
||||
background: "rgba(2, 14, 10, 0.54)",
|
||||
borderRadius: 34,
|
||||
padding: 34,
|
||||
background: "rgba(2, 14, 10, 0.68)",
|
||||
boxShadow: "0 30px 80px rgba(0,0,0,0.28)",
|
||||
}}
|
||||
>
|
||||
<div style={{ display: "flex", flexDirection: "column", flex: 1, justifyContent: "space-between" }}>
|
||||
<div style={{ display: "flex", flexDirection: "column", flex: 1, justifyContent: "space-between", paddingRight: 22 }}>
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 14 }}>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
width: 120,
|
||||
width: 148,
|
||||
justifyContent: "center",
|
||||
borderRadius: 999,
|
||||
background: "rgba(255,255,255,0.1)",
|
||||
padding: "10px 16px",
|
||||
fontSize: 20,
|
||||
fontWeight: 700,
|
||||
letterSpacing: 2,
|
||||
padding: "10px 18px",
|
||||
fontSize: 18,
|
||||
fontWeight: 900,
|
||||
letterSpacing: 2.4,
|
||||
textTransform: "uppercase",
|
||||
}}
|
||||
>
|
||||
Daily
|
||||
Daily Puzzle
|
||||
</div>
|
||||
<div style={{ display: "flex", fontSize: 108, fontWeight: 900, letterSpacing: -4 }}>hidden11</div>
|
||||
<div style={{ display: "flex", maxWidth: 620, fontSize: 34, lineHeight: 1.2, color: "#d7fbe7" }}>
|
||||
Daily football lineup puzzle with Easy, Medium and Hard cards that are actually worth solving.
|
||||
|
||||
<div style={{ display: "flex", alignItems: "flex-end", gap: 8 }}>
|
||||
<div style={{ display: "flex", fontSize: 102, fontWeight: 900, letterSpacing: -4 }}>hidden</div>
|
||||
<div style={{ display: "flex", gap: 8, marginBottom: 14 }}>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
width: 34,
|
||||
height: 74,
|
||||
borderRadius: 16,
|
||||
background: "linear-gradient(180deg, #48e7a0 0%, #10b968 100%)",
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
width: 34,
|
||||
height: 74,
|
||||
borderRadius: 16,
|
||||
background: "linear-gradient(180deg, #ffe86a 0%, #facc15 100%)",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ display: "flex", gap: 16 }}>
|
||||
<div style={{ display: "flex", maxWidth: 620, fontSize: 34, lineHeight: 1.18, color: "#d7fbe7" }}>
|
||||
Daily football lineup puzzle with clues, history links and difficulty levels that actually escalate.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ display: "flex", gap: 14 }}>
|
||||
{["Easy", "Medium", "Hard"].map((label, index) => (
|
||||
<div
|
||||
key={label}
|
||||
@@ -64,16 +112,22 @@ export default function OpenGraphImage() {
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: 8,
|
||||
width: 168,
|
||||
width: 172,
|
||||
padding: 18,
|
||||
borderRadius: 22,
|
||||
background: index === 2 ? "#d4ff4d" : "rgba(255,255,255,0.09)",
|
||||
background:
|
||||
index === 0
|
||||
? "rgba(72, 231, 160, 0.16)"
|
||||
: index === 1
|
||||
? "rgba(255,255,255,0.09)"
|
||||
: "#d4ff4d",
|
||||
color: index === 2 ? "#0b1a12" : "#f7fff9",
|
||||
border: "1px solid rgba(255,255,255,0.08)",
|
||||
}}
|
||||
>
|
||||
<div style={{ display: "flex", fontSize: 18, fontWeight: 800, textTransform: "uppercase" }}>{label}</div>
|
||||
<div style={{ display: "flex", fontSize: 16, lineHeight: 1.3, opacity: 0.9 }}>
|
||||
{index === 0 ? "Sharper club clues" : index === 1 ? "Less obvious player pool" : "Career-history riddles"}
|
||||
{index === 0 ? "Fast entry" : index === 1 ? "Less obvious pool" : "Career-history riddles"}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
@@ -83,57 +137,91 @@ export default function OpenGraphImage() {
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
width: 360,
|
||||
marginLeft: 28,
|
||||
width: 368,
|
||||
marginLeft: 16,
|
||||
borderRadius: 28,
|
||||
padding: 24,
|
||||
background: "linear-gradient(180deg, rgba(8,41,28,0.95), rgba(5,22,15,0.98))",
|
||||
background: "linear-gradient(180deg, rgba(8,41,28,0.98), rgba(5,22,15,1))",
|
||||
border: "1px solid rgba(255,255,255,0.12)",
|
||||
flexDirection: "column",
|
||||
}}
|
||||
>
|
||||
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: 18 }}>
|
||||
<div style={{ display: "flex", flexDirection: "column" }}>
|
||||
<div style={{ display: "flex", fontSize: 16, fontWeight: 800, textTransform: "uppercase", letterSpacing: 1.6, color: "#8ddfb6" }}>
|
||||
Today
|
||||
</div>
|
||||
<div style={{ display: "flex", marginTop: 4, fontSize: 26, fontWeight: 900 }}>
|
||||
hidden11 #124
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
borderRadius: 999,
|
||||
background: "#3ee696",
|
||||
color: "#082015",
|
||||
fontSize: 15,
|
||||
fontWeight: 900,
|
||||
padding: "10px 14px",
|
||||
textTransform: "uppercase",
|
||||
}}
|
||||
>
|
||||
Shareable
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
width: "100%",
|
||||
gap: 14,
|
||||
gap: 12,
|
||||
justifyContent: "space-between",
|
||||
flex: 1,
|
||||
}}
|
||||
>
|
||||
{[
|
||||
["", "ST", ""],
|
||||
["WING", "", "MID"],
|
||||
["", "DEF", ""],
|
||||
["", "GK", ""],
|
||||
].map((row, rowIndex) => (
|
||||
<div key={rowIndex} style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
|
||||
{row.map((cell, cellIndex) =>
|
||||
cell ? (
|
||||
{ label: "Easy", time: "1:24", color: "#3ee696" },
|
||||
{ label: "Medium", time: "2:08", color: "#facc15" },
|
||||
{ label: "Hard", time: "3:41", color: "#ffffff" },
|
||||
].map((card) => (
|
||||
<div
|
||||
key={cell}
|
||||
key={card.label}
|
||||
style={{
|
||||
display: "flex",
|
||||
width: 82,
|
||||
height: 82,
|
||||
borderRadius: 999,
|
||||
background: (rowIndex + cellIndex) % 2 === 0 ? "#39d98a" : "#facc15",
|
||||
color: "#08160f",
|
||||
fontSize: 18,
|
||||
fontWeight: 900,
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
border: "3px solid rgba(255,255,255,0.25)",
|
||||
flexDirection: "column",
|
||||
gap: 10,
|
||||
borderRadius: 22,
|
||||
background: "rgba(255,255,255,0.06)",
|
||||
border: "1px solid rgba(255,255,255,0.08)",
|
||||
padding: 16,
|
||||
}}
|
||||
>
|
||||
{cell}
|
||||
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
|
||||
<div style={{ display: "flex", fontSize: 18, fontWeight: 800, textTransform: "uppercase" }}>{card.label}</div>
|
||||
<div style={{ display: "flex", fontSize: 16, fontWeight: 700, color: "#d7fbe7" }}>{card.time}</div>
|
||||
</div>
|
||||
<div style={{ display: "flex", gap: 8 }}>
|
||||
{Array.from({ length: 5 }).map((_, index) => (
|
||||
<div
|
||||
key={index}
|
||||
style={{
|
||||
display: "flex",
|
||||
width: 36,
|
||||
height: 36,
|
||||
borderRadius: 10,
|
||||
background: card.color,
|
||||
boxShadow: "inset 0 -6px 0 rgba(0,0,0,0.18)",
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div key={`${rowIndex}-${cellIndex}`} style={{ display: "flex", width: 82, height: 82 }} />
|
||||
),
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div style={{ display: "flex", marginTop: 16, fontSize: 16, color: "#b8d8c6" }}>hidden11.app</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -7,6 +7,7 @@ import Pitch from "@/components/Pitch";
|
||||
import PlayerSearch from "@/components/PlayerSearch";
|
||||
import ShareResult from "@/components/ShareResult";
|
||||
import StatsPanel from "@/components/StatsPanel";
|
||||
import { buildShareText, buildShareUrl } from "@/lib/share";
|
||||
import {
|
||||
DIFFICULTIES,
|
||||
MAX_ATTEMPTS,
|
||||
@@ -53,6 +54,11 @@ export default function Home() {
|
||||
const isFinished = activeProgress.solved || activeProgress.attempts.length >= MAX_ATTEMPTS;
|
||||
const t = useMemo(() => getDictionary(locale), [locale]);
|
||||
const quote = useMemo(() => quoteForDay(dateKey, locale), [dateKey, locale]);
|
||||
const shareUrl = useMemo(
|
||||
() => buildShareUrl(dateKey, progress, typeof window === "undefined" ? "https://hidden11.app" : window.location.origin),
|
||||
[dateKey, progress],
|
||||
);
|
||||
const shareText = useMemo(() => buildShareText(dateKey, progress, t, shareUrl), [dateKey, progress, shareUrl, t]);
|
||||
|
||||
useEffect(() => {
|
||||
const savedLocale = window.localStorage.getItem("hidden11-locale") as Locale | null;
|
||||
@@ -167,6 +173,22 @@ export default function Home() {
|
||||
window.localStorage.setItem("hidden11-locale", nextLocale);
|
||||
}
|
||||
|
||||
async function shareCurrentResult() {
|
||||
if (navigator.share) {
|
||||
await navigator.share({
|
||||
title: "hidden11",
|
||||
text: shareText,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
window.open(
|
||||
`https://twitter.com/intent/tweet?text=${encodeURIComponent(shareText)}`,
|
||||
"_blank",
|
||||
"noopener,noreferrer",
|
||||
);
|
||||
}
|
||||
|
||||
const actionButtons = (
|
||||
<div className="flex items-center gap-1.5">
|
||||
<button
|
||||
@@ -341,6 +363,14 @@ export default function Home() {
|
||||
<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={shareCurrentResult}
|
||||
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)}
|
||||
@@ -350,6 +380,7 @@ export default function Home() {
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<StatsPanel stats={stats} t={t} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export { default, alt, contentType, size } from "../../result/[shareCode]/opengraph-image";
|
||||
@@ -0,0 +1 @@
|
||||
export { default, generateMetadata } from "../../result/[shareCode]/page";
|
||||
@@ -0,0 +1 @@
|
||||
export { default, alt, contentType, size } from "../../result/[shareCode]/twitter-image";
|
||||
@@ -16,7 +16,6 @@ type ShareResultProps = {
|
||||
};
|
||||
|
||||
export default function ShareResult({ dateKey, locale, onClose, progress, quote, stats, t }: ShareResultProps) {
|
||||
const [copied, setCopied] = useState(false);
|
||||
const [shared, setShared] = useState(false);
|
||||
const [origin, setOrigin] = useState("https://hidden11.app");
|
||||
const shareUrl = useMemo(() => buildShareUrl(dateKey, progress, origin), [dateKey, origin, progress]);
|
||||
@@ -26,18 +25,11 @@ export default function ShareResult({ dateKey, locale, onClose, progress, quote,
|
||||
setOrigin(window.location.origin);
|
||||
}, []);
|
||||
|
||||
async function copy() {
|
||||
await navigator.clipboard.writeText(shareText);
|
||||
setCopied(true);
|
||||
window.setTimeout(() => setCopied(false), 1600);
|
||||
}
|
||||
|
||||
async function share() {
|
||||
if (navigator.share) {
|
||||
await navigator.share({
|
||||
title: "hidden11",
|
||||
text: shareText,
|
||||
url: shareUrl,
|
||||
});
|
||||
setShared(true);
|
||||
window.setTimeout(() => setShared(false), 1600);
|
||||
@@ -98,27 +90,6 @@ export default function ShareResult({ dateKey, locale, onClose, progress, quote,
|
||||
</pre>
|
||||
</div>
|
||||
|
||||
<div className="border-t border-white/10 bg-pitch-950/95 p-4 backdrop-blur">
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={share}
|
||||
className="h-11 rounded-md bg-emerald-400 px-3 text-sm font-black text-pitch-950 transition hover:bg-emerald-300"
|
||||
>
|
||||
{shared ? t.copied : t.shareNow}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={copy}
|
||||
className="h-11 rounded-md bg-white px-3 text-sm font-black text-pitch-950 transition hover:bg-emerald-100"
|
||||
>
|
||||
{copied ? t.copied : t.copy}
|
||||
</button>
|
||||
</div>
|
||||
<div className="mt-2 rounded-md border border-white/10 bg-black/20 px-3 py-2 text-[11px] text-white/65">
|
||||
{shareUrl}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
+13
-3
@@ -25,6 +25,8 @@ type SharePayload = {
|
||||
};
|
||||
|
||||
export function buildShareText(dateKey: string, progress: DailyProgress, t: Dictionary, shareUrl?: string): string {
|
||||
const resultUrl = shareUrl ?? "https://hidden11.app";
|
||||
const homeUrl = "https://hidden11.app";
|
||||
const rows = DIFFICULTIES.map((difficulty) => {
|
||||
const item = progress[difficulty];
|
||||
const label = t.difficulty[difficulty];
|
||||
@@ -43,10 +45,18 @@ export function buildShareText(dateKey: string, progress: DailyProgress, t: Dict
|
||||
.join("\n")
|
||||
: "⬛⬛⬛⬛⬛";
|
||||
|
||||
return `${label} ${result} · ${time}\n${grids}`;
|
||||
return `${label.padEnd(6)} ${result} · ${time}\n${grids}`;
|
||||
});
|
||||
|
||||
return [`hidden11 #${getPuzzleNumber(dateKey)}`, "", ...rows, "", shareUrl ?? "https://hidden11.app"].join("\n");
|
||||
return [
|
||||
"┏━ hidden11.app ━┓",
|
||||
`⚽ hidden11 #${getPuzzleNumber(dateKey)}`,
|
||||
"",
|
||||
...rows,
|
||||
"",
|
||||
`Play: ${homeUrl}`,
|
||||
`Result: ${resultUrl}`,
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
export function buildShareUrl(dateKey: string, progress: DailyProgress, origin = "https://hidden11.app"): string {
|
||||
@@ -71,7 +81,7 @@ export function buildShareUrl(dateKey: string, progress: DailyProgress, origin =
|
||||
}, {} as Record<Difficulty, ShareDifficultySummary>),
|
||||
};
|
||||
|
||||
return `${origin.replace(/\/$/, "")}/result/${encodeSharePayload(payload)}`;
|
||||
return `${origin.replace(/\/$/, "")}/r/${encodeSharePayload(payload)}`;
|
||||
}
|
||||
|
||||
export function parseSharePayload(shareCode: string): SharePayload | null {
|
||||
|
||||
Reference in New Issue
Block a user