From e0294d80e38c89544ad064e26b1bc33b16c32360 Mon Sep 17 00:00:00 2001 From: gosangam Date: Sun, 30 Aug 2026 02:19:16 +0530 Subject: [PATCH] feat: add stickers collection --- app/(dashboard)/layout.tsx | 2 + app/(dashboard)/reward-gifts/page.tsx | 108 ++++++++++++++++++++++++++ lib/api.ts | 23 ++++++ 3 files changed, 133 insertions(+) create mode 100644 app/(dashboard)/reward-gifts/page.tsx diff --git a/app/(dashboard)/layout.tsx b/app/(dashboard)/layout.tsx index 31bec41..8950d0c 100644 --- a/app/(dashboard)/layout.tsx +++ b/app/(dashboard)/layout.tsx @@ -5,6 +5,7 @@ import Link from "next/link"; import { usePathname, useRouter } from "next/navigation"; import { Flag, + Gift, Megaphone, FolderClosed, LayoutDashboard, @@ -28,6 +29,7 @@ const NAV = [ { href: "/feedback", label: "Feedback", icon: MessageSquare }, { href: "/reports", label: "Reports", icon: Flag }, { href: "/broadcasts", label: "Announcements", icon: Megaphone }, + { href: "/reward-gifts", label: "Gifts", icon: Gift }, ]; export default function DashboardLayout({ diff --git a/app/(dashboard)/reward-gifts/page.tsx b/app/(dashboard)/reward-gifts/page.tsx new file mode 100644 index 0000000..128a75d --- /dev/null +++ b/app/(dashboard)/reward-gifts/page.tsx @@ -0,0 +1,108 @@ +"use client"; + +/** + * /reward-gifts — hand a sticker to everyone, outside the streak. + * + * These arrive wrapped: each devotee sees a blurred tile in their rewards + * gallery, and opening it plays the same celebration a streak reward does. + * Nothing is required of them to earn it. + * + * Sending is idempotent — a repeat reaches only accounts that don't already + * hold the sticker — so a double click tops up rather than duplicating. + */ + +import { useEffect, useState } from "react"; +import Image from "next/image"; +import { toast } from "sonner"; +import { rewardGifts, type GiftableReward } from "@/lib/api"; + +export default function RewardGiftsPage() { + const [catalog, setCatalog] = useState(null); + const [sending, setSending] = useState(null); + + useEffect(() => { + rewardGifts + .catalog() + .then(setCatalog) + .catch(() => { + toast.error("Could not load the reward catalog"); + setCatalog([]); + }); + }, []); + + const send = async (reward: GiftableReward) => { + if ( + !window.confirm( + `Send “${reward.name}” to every devotee? They'll see it wrapped in their gallery.`, + ) + ) { + return; + } + setSending(reward.key); + try { + const { granted } = await rewardGifts.send(reward.key); + toast.success( + granted === 0 + ? "Everyone already has this one" + : `Sent to ${granted} ${granted === 1 ? "devotee" : "devotees"}`, + ); + } catch (err) { + toast.error(err instanceof Error ? err.message : "Could not send the gift"); + } finally { + setSending(null); + } + }; + + return ( +
+
+

+ Occasional gifts +

+

+ Send a sticker to everyone, no streak required. It arrives wrapped + and opens with a celebration. +

+
+ + {catalog === null ? ( +

Loading…

+ ) : catalog.length === 0 ? ( +

+ The reward catalog is empty. +

+ ) : ( +
    + {catalog.map((r) => ( +
  • +
    + {r.name} +
    +

    + {r.name} +

    + +
  • + ))} +
+ )} +
+ ); +} diff --git a/lib/api.ts b/lib/api.ts index 733bb91..e12be89 100644 --- a/lib/api.ts +++ b/lib/api.ts @@ -436,3 +436,26 @@ export const broadcasts = { { method: "POST", body: JSON.stringify({ content_type: contentType }) }, ).then((r) => r.data), }; + +// ─── Reward gifts ─────────────────────────────────────────────────────────── + +/** A sticker from the sādhana catalog, sendable as an occasional gift. */ +export type GiftableReward = { + id: string; + key: string; + name: string; + meaning: string; + image_url: string; +}; + +export const rewardGifts = { + catalog: () => call("/admin/reward-gifts/catalog").then((r) => r.data), + + /** Hand a reward to every account, unopened. Idempotent — a repeat only + * reaches people who don't already have it. */ + send: (rewardKey: string) => + call<{ granted: number }>("/admin/reward-gifts", { + method: "POST", + body: JSON.stringify({ reward_key: rewardKey }), + }).then((r) => r.data), +};