"use client"; /** * RewardsGallery — the collected-gifts section on /sadhna-streak. Shows each * unique reward the devotee has earned as a tappable tile (image + short * name); tapping opens a detail dialog with the full name + symbolic * meaning + which streak day it was earned. Remaining catalog slots show as * locked tiles to entice completion. * * Built to read in both themes (card/foreground tokens), keep names legible * (line-clamped, never overflowing), and resize the large R2 images via * next/image. */ import { useEffect, useState } from "react"; import { createPortal } from "react-dom"; import Image from "next/image"; import { Gift } from "lucide-react"; import type { SadhnaRewardGrant } from "@/lib/sadhna"; import { useMounted } from "@/lib/use-mounted"; export function RewardsGallery({ collected, nextRewardDate, }: { collected: SadhnaRewardGrant[]; nextRewardDate: string | null; }) { const [detail, setDetail] = useState(null); // De-dupe by reward (collected is newest-first, so the first wins). const seen = new Set(); const unique: SadhnaRewardGrant[] = []; for (const g of collected) { if (!seen.has(g.reward.key)) { seen.add(g.reward.key); unique.push(g); } } return (

Sacred Rewards

{unique.length === 0 ? (

No gifts yet. Keep your sādhana streak — a sacred reward arrives every 7 days.

) : (
{unique.map((g) => ( ))}
)} {nextRewardDate ? (

Next gift on{" "} {fmtLong(nextRewardDate)}

) : null} {detail ? ( setDetail(null)} /> ) : null}
); } function RewardDetail({ grant, onClose, }: { grant: SadhnaRewardGrant; onClose: () => void; }) { const mounted = useMounted(); useEffect(() => { const onKey = (e: KeyboardEvent) => e.key === "Escape" && onClose(); window.addEventListener("keydown", onKey); const prev = document.body.style.overflow; document.body.style.overflow = "hidden"; return () => { window.removeEventListener("keydown", onKey); document.body.style.overflow = prev; }; }, [onClose]); if (!mounted) return null; const r = grant.reward; return createPortal(
{r.name}

{r.name}

{r.meaning ? (

{r.meaning}

) : null}

Earned on day {grant.milestone} of your streak

, document.body, ); } /** The evocative term before the em-dash, for compact tile labels. */ function shortName(name: string): string { const parts = name.split("—"); return (parts[0] ?? name).trim(); } function fmtLong(date: string): string { const d = new Date(`${date}T00:00:00Z`); if (Number.isNaN(d.getTime())) return ""; return d.toLocaleDateString(undefined, { month: "short", day: "numeric", timeZone: "UTC", }); }