2026-07-09 21:07:34 -04:00
|
|
|
|
// components/account/SupporterBadges.tsx
|
|
|
|
|
|
"use client";
|
|
|
|
|
|
|
|
|
|
|
|
import { useEffect, useState } from "react";
|
|
|
|
|
|
|
|
|
|
|
|
type SupportBadge = {
|
|
|
|
|
|
provider: "kofi" | "patreon" | "mighty" | string;
|
|
|
|
|
|
active: boolean; // currently entitled (status==="active" && renews_at >= now)
|
|
|
|
|
|
kind: "member" | "one_time" | "inactive";
|
|
|
|
|
|
label: string; // e.g., "Ko-fi • Bronze" or "Ko-fi Supporter"
|
|
|
|
|
|
tier?: string | null;
|
|
|
|
|
|
renews_at?: string | null;
|
|
|
|
|
|
started_at?: string | null;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
function providerIcon(p: string) {
|
|
|
|
|
|
// Swap these for real icons later if you want
|
|
|
|
|
|
if (p === "kofi") return "☕";
|
|
|
|
|
|
if (p === "patreon") return "🅿️";
|
|
|
|
|
|
if (p === "mighty") return "💬";
|
|
|
|
|
|
return "🎖️";
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export default function SupporterBadges({
|
|
|
|
|
|
email,
|
|
|
|
|
|
userId,
|
|
|
|
|
|
}: {
|
|
|
|
|
|
email?: string | null;
|
|
|
|
|
|
userId?: string | null;
|
|
|
|
|
|
}) {
|
|
|
|
|
|
const [badges, setBadges] = useState<SupportBadge[] | null>(null);
|
|
|
|
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
2026-07-09 22:10:26 -04:00
|
|
|
|
fetch("/api/support/badges", { cache: "no-store", credentials: "include" })
|
2026-07-09 21:07:34 -04:00
|
|
|
|
.then((r) => (r.ok ? r.json() : Promise.reject(r)))
|
|
|
|
|
|
.then((json) => setBadges(Array.isArray(json?.badges) ? json.badges : []))
|
|
|
|
|
|
.catch(async (e) => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const t = await e.text();
|
|
|
|
|
|
setError(t || String(e));
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
setError(String(e));
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
}, [email, userId]);
|
|
|
|
|
|
|
|
|
|
|
|
if (error) {
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div className="mt-4 rounded-lg border border-red-300/50 bg-red-50 p-3 text-sm text-red-800">
|
|
|
|
|
|
Couldn’t load supporter badges.
|
|
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (!badges) {
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div className="mt-4 animate-pulse rounded-lg border p-3 text-sm opacity-60">
|
|
|
|
|
|
Loading supporter badges…
|
|
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (badges.length === 0) {
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div className="mt-4 rounded-lg border p-3 text-sm opacity-70">
|
|
|
|
|
|
No supporter badges yet.
|
|
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div className="mt-4 space-y-2">
|
|
|
|
|
|
<div className="text-sm font-medium opacity-80">Supporter Badges</div>
|
|
|
|
|
|
<div className="flex flex-wrap gap-2">
|
|
|
|
|
|
{badges.map((b, i) => (
|
|
|
|
|
|
<span
|
|
|
|
|
|
key={`${b.provider}-${i}`}
|
|
|
|
|
|
className={[
|
|
|
|
|
|
"inline-flex items-center gap-1 rounded-full border px-3 py-1 text-sm",
|
|
|
|
|
|
b.active
|
|
|
|
|
|
? "border-green-300 bg-green-50 text-green-900"
|
|
|
|
|
|
: b.kind === "one_time"
|
|
|
|
|
|
? "border-amber-300 bg-amber-50 text-amber-900"
|
|
|
|
|
|
: "border-slate-300 bg-slate-50 text-slate-700",
|
|
|
|
|
|
].join(" ")}
|
|
|
|
|
|
title={
|
|
|
|
|
|
b.active
|
|
|
|
|
|
? b.renews_at
|
|
|
|
|
|
? `Active • renews by ${new Date(b.renews_at).toLocaleDateString()}`
|
|
|
|
|
|
: "Active"
|
|
|
|
|
|
: b.kind === "one_time"
|
|
|
|
|
|
? "One-time support"
|
|
|
|
|
|
: "Inactive"
|
|
|
|
|
|
}
|
|
|
|
|
|
>
|
|
|
|
|
|
<span>{providerIcon(b.provider)}</span>
|
|
|
|
|
|
<span>{b.label}</span>
|
|
|
|
|
|
</span>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|