TypeScript 97.6%
SQL 1.4%
JavaScript 0.5%
1"use client";23import { useEffect, useState } from "react";4import Link from "next/link";5import { Gift, Users, ChevronRight, Flame } from "lucide-react";6import type { DailyRewardStatus } from "@spinza/shared";7import { api } from "@/lib/api";8import { toast, useSession } from "@/lib/store";9import { useApi } from "@/lib/use-api";10import { countdown, cn } from "@/lib/utils";11import { Button, Credits, Skeleton } from "@/components/ui";1213interface ClaimResponse {14 amount: number;15 streakDay: number;16 balance: number;17 nextAvailableAt: string;18 xp: { xp: number; level: number; leveledUp?: boolean } | null;19}2021/** Lobby strip: daily reward availability + live players. */22export function RewardsStrip({ livePlayers }: { livePlayers: number }) {23 const { data, loading, error, setData, reload } = useApi<DailyRewardStatus>("/api/rewards/daily");24 const setBalance = useSession((s) => s.setBalance);25 const setUserXp = useSession((s) => s.setUserXp);26 const [claiming, setClaiming] = useState(false);27 const [, tick] = useState(0);2829 useEffect(() => {30 if (!data || data.available) return;31 const t = setInterval(() => tick((n) => n + 1), 1000);32 return () => clearInterval(t);33 }, [data]);3435 const claim = async () => {36 if (claiming) return;37 setClaiming(true);38 try {39 const res = await api<ClaimResponse>("/api/rewards/daily/claim", { method: "POST" });40 setBalance(res.balance);41 if (res.xp) setUserXp(res.xp.xp, res.xp.level);42 toast({ title: `+${res.amount.toLocaleString("en-US")} SC`, description: `Daily reward · day ${res.streakDay} of ${data?.schedule.length ?? 7}`, tone: "credit" });43 setData((d) => (d ? { ...d, available: false, claimedToday: true, streakDay: res.streakDay, nextAvailableAt: res.nextAvailableAt } : d));44 } catch (e) {45 toast({ title: "Could not claim", description: e instanceof Error ? e.message : undefined, tone: "danger" });46 void reload();47 } finally {48 setClaiming(false);49 }50 };5152 return (53 <div className="grid gap-3 sm:grid-cols-[1fr_auto]">54 <div className={cn("surface flex items-center gap-4 rounded-lg p-3 pl-4", data?.available && "border-accent/40 shadow-glow")}>55 <span className={cn("grid h-11 w-11 shrink-0 place-items-center rounded-md", data?.available ? "bg-accent text-bg" : "metal text-accent-2")}>56 <Gift className="h-5 w-5" />57 </span>58 <div className="min-w-0 flex-1">59 {loading && !data ? (60 <>61 <Skeleton className="h-4 w-40" />62 <Skeleton className="mt-2 h-3 w-24" />63 </>64 ) : error && !data ? (65 <>66 <div className="text-[15px] font-semibold">Daily reward</div>67 <div className="text-[13px] text-fg-3">Unavailable right now.</div>68 </>69 ) : data ? (70 <>71 <div className="flex items-center gap-2 text-[15px] font-semibold tracking-tight">72 {data.available ? "Your daily reward is ready" : "Daily reward claimed"}73 {data.streakDay > 1 ? (74 <span className="inline-flex items-center gap-0.5 rounded-full bg-surface-2 px-2 py-0.5 text-[11px] font-bold text-fg-2">75 <Flame className="h-3 w-3 text-accent" /> {data.streakDay}-day streak76 </span>77 ) : null}78 </div>79 <div className="text-[13px] text-fg-3">80 {data.available ? (81 <>82 Claim <Credits amount={data.nextAmount} size="sm" /> · day {Math.min(data.streakDay + 1, data.schedule.length)} of {data.schedule.length}83 </>84 ) : (85 <>86 Next reward <Credits amount={data.nextAmount} size="sm" /> in <span className="tabular text-fg-2">{countdown(data.nextAvailableAt)}</span>87 </>88 )}89 </div>90 </>91 ) : null}92 </div>93 {data?.available ? (94 <Button variant="accent" size="md" onClick={claim} loading={claiming} className="shrink-0">95 Claim96 </Button>97 ) : (98 <Link href="/rewards" className="tap grid h-11 w-11 place-items-center rounded-md text-fg-3 hover:bg-surface-2 hover:text-fg focus-ring" aria-label="Open rewards">99 <ChevronRight className="h-5 w-5" />100 </Link>101 )}102 </div>103 <div className="surface flex items-center gap-3 rounded-lg px-4 py-3">104 <span className="relative grid h-9 w-9 place-items-center rounded-full bg-surface-2 text-success">105 <Users className="h-4 w-4" />106 <span className="absolute -right-0.5 -top-0.5 h-2.5 w-2.5 rounded-full bg-success shadow-[0_0_10px_rgba(61,220,151,0.9)] animate-pulse-soft" />107 </span>108 <div>109 <div className="text-[15px] font-semibold tabular">{livePlayers.toLocaleString("en-US")}</div>110 <div className="text-[12px] text-fg-3">{livePlayers === 1 ? "player" : "players"} live now</div>111 </div>112 </div>113 </div>114 );115}116