EventBuzz_Website/src/pages/LeaderboardPage.tsx

144 lines
5.6 KiB
TypeScript

import { useEffect, useState } from "react";
import { Award } from "lucide-react";
import { Link, useSearchParams } from "react-router-dom";
import { getVisitorEventLeaderboard } from "../lib/api";
import { getAttendeeToken } from "../lib/attendeeAuth";
import { HeaderMobile } from "../components/HeaderMobile";
import { MobileShell } from "../components/MobileShell";
import { readStoredVisitorContext } from "../types/visitorContext";
import type { VisitorLeaderboardRow } from "../lib/api";
function Medal({ tier }: { tier: "gold" | "silver" | "bronze" }) {
const cls =
tier === "gold"
? "text-amber-500"
: tier === "silver"
? "text-slate-400"
: "text-amber-800";
return <Award className={`size-6 ${cls}`} strokeWidth={1.5} />;
}
function medalForRank(rank: number): "gold" | "silver" | "bronze" | null {
if (rank === 1) return "gold";
if (rank === 2) return "silver";
if (rank === 3) return "bronze";
return null;
}
export function LeaderboardPage() {
const [searchParams] = useSearchParams();
const urlEventId = searchParams.get("event_id")?.trim() || "";
const ctx = readStoredVisitorContext();
const eventId = urlEventId || ctx?.eventId || "";
const [rows, setRows] = useState<VisitorLeaderboardRow[]>([]);
const [yourRank, setYourRank] = useState<number | null>(null);
const [yourTotal, setYourTotal] = useState<number | null>(null);
const [loading, setLoading] = useState(false);
const [err, setErr] = useState<string | null>(null);
useEffect(() => {
if (!eventId) {
setRows([]);
setYourRank(null);
setYourTotal(null);
return;
}
let cancelled = false;
setLoading(true);
setErr(null);
getVisitorEventLeaderboard(eventId, { limit: 50 })
.then((r) => {
if (cancelled) return;
setRows(r.leaderboard);
if (getAttendeeToken()) {
const y = r.your;
setYourRank(y?.rank ?? null);
setYourTotal(typeof y?.total === "number" ? y.total : null);
} else {
setYourRank(null);
setYourTotal(null);
}
})
.catch((e) => {
if (!cancelled) setErr(e instanceof Error ? e.message : "Could not load leaderboard.");
})
.finally(() => {
if (!cancelled) setLoading(false);
});
return () => {
cancelled = true;
};
}, [eventId]);
const top5 = rows.slice(0, 5);
const signedIn = Boolean(getAttendeeToken());
return (
<MobileShell className="relative">
<HeaderMobile logoUrl={ctx?.event?.logoUrl} eventId={eventId || null} />
<div className="eb-gradient-auth flex flex-1 flex-col items-center px-6 pb-10 pt-6">
<Award className="size-[68px] text-amber-300" strokeWidth={1.25} />
<h1 className="mt-1 font-inter text-xl font-semibold text-white">Leaderboard</h1>
<div className="mt-6 w-full max-w-[354px] rounded-xl bg-whit card-bg shadow-[0_8px_12px_6px_rgba(0,0,0,0.15),0_4px_4px_rgba(0,0,0,0.3)]">
<div className="p-4">
<div className="eb-rank-highlight flex flex-col gap-2 rounded-3xl border-4 border-white px-4 py-4 text-white shadow-[0_9px_16px_rgba(0,0,0,0.14)]">
<p className="text-center font-inter text-base font-semibold uppercase tracking-wide">Your rank</p>
<div className="flex items-center justify-center gap-4">
<span className="font-inter text-5xl font-bold drop-shadow-[0_4px_4px_rgba(0,0,0,0.25)]">
{!signedIn ? "—" : loading ? "…" : yourRank != null ? yourRank : "—"}
</span>
<span className="rounded-xl bg-[#cff7d3] px-4 py-2 text-sm font-medium text-[#009951] shadow-sm">
{!signedIn
? "Sign in"
: loading
? "…"
: yourTotal != null
? `${yourTotal} points`
: "0 points"}
</span>
</div>
</div>
<h2 className="mt-6 font-inter text-base font-semibold text-[#24282f]">Top participants</h2>
{err ? (
<p className="mt-3 text-xs text-red-600">{err}</p>
) : loading && rows.length === 0 ? (
<p className="mt-3 text-xs text-[rgba(36,40,47,0.67)]">Loading…</p>
) : top5.length === 0 ? (
<p className="mt-3 text-xs text-[rgba(36,40,47,0.67)]">
No scores yet. Complete activities and earn points when organizers enable rewards.
</p>
) : (
<ul className="mt-3 flex flex-col gap-3">
{top5.map((row) => {
const medal = medalForRank(row.rank);
return (
<li
key={row.participantId}
className="flex items-center gap-3 border-b border-[#ededed] pb-3 last:border-0"
>
<span className="shrink-0 rounded bg-[#ebffee] px-3 py-2 text-xs font-medium text-[#009951] shadow-sm">
#{row.rank} · {row.total} pts
</span>
<span className="min-w-0 flex-1 text-sm font-semibold text-[#24282f]">{row.displayName}</span>
{medal ? <Medal tier={medal} /> : null}
</li>
);
})}
</ul>
)}
</div>
</div>
<Link
to={eventId ? `/rewards?event_id=${encodeURIComponent(eventId)}` : "/rewards"}
className="mt-6 font-inter font-semibold text-[var(--theme-text-color)] underline"
>
Rewards
</Link>
</div>
</MobileShell>
);
}