Polish sharing experience
Build and push image / build (push) Successful in 1m20s

This commit is contained in:
alexandrev-tibco
2026-05-04 22:28:05 +02:00
parent 8d4a5cb20f
commit 0ac9b06a6d
8 changed files with 208 additions and 96 deletions
+15 -6
View File
@@ -3,20 +3,29 @@ import type { ReactNode } from "react";
import "./globals.css"; import "./globals.css";
export const metadata: Metadata = { export const metadata: Metadata = {
title: "hidden11", title: {
description: "Daily football lineup puzzle with evolving difficulty, sharper clues and shareable results.", default: "hidden11",
template: "%s | hidden11",
},
description: "Daily football lineup puzzle with sharper clues, harder challenges and shareable results.",
metadataBase: new URL("https://hidden11.app"), metadataBase: new URL("https://hidden11.app"),
applicationName: "hidden11",
alternates: {
canonical: "https://hidden11.app",
},
keywords: ["football puzzle", "soccer puzzle", "daily game", "lineup puzzle", "hidden11"],
icons: { icons: {
icon: "/favicon.svg", icon: "/favicon.svg",
}, },
openGraph: { openGraph: {
title: "hidden11", 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", url: "https://hidden11.app",
siteName: "hidden11", siteName: "hidden11",
locale: "en_US",
images: [ images: [
{ {
url: "/opengraph-image", url: "https://hidden11.app/opengraph-image",
width: 1200, width: 1200,
height: 630, height: 630,
alt: "hidden11 daily football puzzle", alt: "hidden11 daily football puzzle",
@@ -27,8 +36,8 @@ export const metadata: Metadata = {
twitter: { twitter: {
card: "summary_large_image", card: "summary_large_image",
title: "hidden11", 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.",
images: ["/twitter-image"], images: ["https://hidden11.app/twitter-image"],
}, },
}; };
+138 -50
View File
@@ -15,48 +15,96 @@ export default function OpenGraphImage() {
display: "flex", display: "flex",
width: "100%", width: "100%",
height: "100%", height: "100%",
background: background: "linear-gradient(135deg, #04110c 0%, #0d2d1f 42%, #145c3a 100%)",
"radial-gradient(circle at top, #1f8f5d 0%, #0a2e1f 35%, #04110c 100%)",
color: "#f7fff9", color: "#f7fff9",
padding: 48, padding: 42,
fontFamily: "Arial", 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 <div
style={{ style={{
display: "flex", display: "flex",
width: "100%", width: "100%",
border: "1px solid rgba(255,255,255,0.14)", border: "1px solid rgba(255,255,255,0.14)",
borderRadius: 28, borderRadius: 34,
padding: 36, padding: 34,
background: "rgba(2, 14, 10, 0.54)", 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", flexDirection: "column", gap: 14 }}>
<div <div
style={{ style={{
display: "flex", display: "flex",
width: 120, width: 148,
justifyContent: "center", justifyContent: "center",
borderRadius: 999, borderRadius: 999,
background: "rgba(255,255,255,0.1)", background: "rgba(255,255,255,0.1)",
padding: "10px 16px", padding: "10px 18px",
fontSize: 20, fontSize: 18,
fontWeight: 700, fontWeight: 900,
letterSpacing: 2, letterSpacing: 2.4,
textTransform: "uppercase", textTransform: "uppercase",
}} }}
> >
Daily Daily Puzzle
</div> </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" }}> <div style={{ display: "flex", alignItems: "flex-end", gap: 8 }}>
Daily football lineup puzzle with Easy, Medium and Hard cards that are actually worth solving. <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", 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> </div>
<div style={{ display: "flex", gap: 16 }}> <div style={{ display: "flex", gap: 14 }}>
{["Easy", "Medium", "Hard"].map((label, index) => ( {["Easy", "Medium", "Hard"].map((label, index) => (
<div <div
key={label} key={label}
@@ -64,16 +112,22 @@ export default function OpenGraphImage() {
display: "flex", display: "flex",
flexDirection: "column", flexDirection: "column",
gap: 8, gap: 8,
width: 168, width: 172,
padding: 18, padding: 18,
borderRadius: 22, 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", 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: 18, fontWeight: 800, textTransform: "uppercase" }}>{label}</div>
<div style={{ display: "flex", fontSize: 16, lineHeight: 1.3, opacity: 0.9 }}> <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>
</div> </div>
))} ))}
@@ -83,57 +137,91 @@ export default function OpenGraphImage() {
<div <div
style={{ style={{
display: "flex", display: "flex",
width: 360, width: 368,
marginLeft: 28, marginLeft: 16,
borderRadius: 28, borderRadius: 28,
padding: 24, 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)", 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 <div
style={{ style={{
display: "flex", display: "flex",
flexDirection: "column", flexDirection: "column",
width: "100%", width: "100%",
gap: 14, gap: 12,
justifyContent: "space-between", justifyContent: "space-between",
flex: 1,
}} }}
> >
{[ {[
["", "ST", ""], { label: "Easy", time: "1:24", color: "#3ee696" },
["WING", "", "MID"], { label: "Medium", time: "2:08", color: "#facc15" },
["", "DEF", ""], { label: "Hard", time: "3:41", color: "#ffffff" },
["", "GK", ""], ].map((card) => (
].map((row, rowIndex) => ( <div
<div key={rowIndex} style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}> key={card.label}
{row.map((cell, cellIndex) => style={{
cell ? ( display: "flex",
flexDirection: "column",
gap: 10,
borderRadius: 22,
background: "rgba(255,255,255,0.06)",
border: "1px solid rgba(255,255,255,0.08)",
padding: 16,
}}
>
<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 <div
key={cell} key={index}
style={{ style={{
display: "flex", display: "flex",
width: 82, width: 36,
height: 82, height: 36,
borderRadius: 999, borderRadius: 10,
background: (rowIndex + cellIndex) % 2 === 0 ? "#39d98a" : "#facc15", background: card.color,
color: "#08160f", boxShadow: "inset 0 -6px 0 rgba(0,0,0,0.18)",
fontSize: 18,
fontWeight: 900,
alignItems: "center",
justifyContent: "center",
border: "3px solid rgba(255,255,255,0.25)",
}} }}
> />
{cell} ))}
</div> </div>
) : (
<div key={`${rowIndex}-${cellIndex}`} style={{ display: "flex", width: 82, height: 82 }} />
),
)}
</div> </div>
))} ))}
</div> </div>
<div style={{ display: "flex", marginTop: 16, fontSize: 16, color: "#b8d8c6" }}>hidden11.app</div>
</div> </div>
</div> </div>
</div> </div>
+39 -8
View File
@@ -7,6 +7,7 @@ import Pitch from "@/components/Pitch";
import PlayerSearch from "@/components/PlayerSearch"; import PlayerSearch from "@/components/PlayerSearch";
import ShareResult from "@/components/ShareResult"; import ShareResult from "@/components/ShareResult";
import StatsPanel from "@/components/StatsPanel"; import StatsPanel from "@/components/StatsPanel";
import { buildShareText, buildShareUrl } from "@/lib/share";
import { import {
DIFFICULTIES, DIFFICULTIES,
MAX_ATTEMPTS, MAX_ATTEMPTS,
@@ -53,6 +54,11 @@ export default function Home() {
const isFinished = activeProgress.solved || activeProgress.attempts.length >= MAX_ATTEMPTS; const isFinished = activeProgress.solved || activeProgress.attempts.length >= MAX_ATTEMPTS;
const t = useMemo(() => getDictionary(locale), [locale]); const t = useMemo(() => getDictionary(locale), [locale]);
const quote = useMemo(() => quoteForDay(dateKey, locale), [dateKey, 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(() => { useEffect(() => {
const savedLocale = window.localStorage.getItem("hidden11-locale") as Locale | null; const savedLocale = window.localStorage.getItem("hidden11-locale") as Locale | null;
@@ -167,6 +173,22 @@ export default function Home() {
window.localStorage.setItem("hidden11-locale", nextLocale); 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 = ( const actionButtons = (
<div className="flex items-center gap-1.5"> <div className="flex items-center gap-1.5">
<button <button
@@ -341,14 +363,23 @@ 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="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"> <div className="mb-3 flex items-center justify-between">
<h2 className="text-lg font-black text-white">{t.stats}</h2> <h2 className="text-lg font-black text-white">{t.stats}</h2>
<button <div className="flex items-center gap-2">
type="button" <button
onClick={() => setStatsOpen(false)} type="button"
className="h-9 w-9 rounded-full border border-white/15 text-lg font-black text-white transition hover:bg-white/10" onClick={shareCurrentResult}
aria-label={t.closeModal} 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>
<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> </div>
<StatsPanel stats={stats} t={t} /> <StatsPanel stats={stats} t={t} />
</div> </div>
+1
View File
@@ -0,0 +1 @@
export { default, alt, contentType, size } from "../../result/[shareCode]/opengraph-image";
+1
View File
@@ -0,0 +1 @@
export { default, generateMetadata } from "../../result/[shareCode]/page";
+1
View File
@@ -0,0 +1 @@
export { default, alt, contentType, size } from "../../result/[shareCode]/twitter-image";
-29
View File
@@ -16,7 +16,6 @@ type ShareResultProps = {
}; };
export default function ShareResult({ dateKey, locale, onClose, progress, quote, stats, t }: ShareResultProps) { export default function ShareResult({ dateKey, locale, onClose, progress, quote, stats, t }: ShareResultProps) {
const [copied, setCopied] = useState(false);
const [shared, setShared] = useState(false); const [shared, setShared] = useState(false);
const [origin, setOrigin] = useState("https://hidden11.app"); const [origin, setOrigin] = useState("https://hidden11.app");
const shareUrl = useMemo(() => buildShareUrl(dateKey, progress, origin), [dateKey, origin, progress]); 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); setOrigin(window.location.origin);
}, []); }, []);
async function copy() {
await navigator.clipboard.writeText(shareText);
setCopied(true);
window.setTimeout(() => setCopied(false), 1600);
}
async function share() { async function share() {
if (navigator.share) { if (navigator.share) {
await navigator.share({ await navigator.share({
title: "hidden11", title: "hidden11",
text: shareText, text: shareText,
url: shareUrl,
}); });
setShared(true); setShared(true);
window.setTimeout(() => setShared(false), 1600); window.setTimeout(() => setShared(false), 1600);
@@ -98,27 +90,6 @@ export default function ShareResult({ dateKey, locale, onClose, progress, quote,
</pre> </pre>
</div> </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>
</div> </div>
); );
+13 -3
View File
@@ -25,6 +25,8 @@ type SharePayload = {
}; };
export function buildShareText(dateKey: string, progress: DailyProgress, t: Dictionary, shareUrl?: string): string { 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 rows = DIFFICULTIES.map((difficulty) => {
const item = progress[difficulty]; const item = progress[difficulty];
const label = t.difficulty[difficulty]; const label = t.difficulty[difficulty];
@@ -43,10 +45,18 @@ export function buildShareText(dateKey: string, progress: DailyProgress, t: Dict
.join("\n") .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 { 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>), }, {} as Record<Difficulty, ShareDifficultySummary>),
}; };
return `${origin.replace(/\/$/, "")}/result/${encodeSharePayload(payload)}`; return `${origin.replace(/\/$/, "")}/r/${encodeSharePayload(payload)}`;
} }
export function parseSharePayload(shareCode: string): SharePayload | null { export function parseSharePayload(shareCode: string): SharePayload | null {