104 lines
3.2 KiB
TypeScript
104 lines
3.2 KiB
TypeScript
// 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(() => {
|
||
fetch("/api/support/badges", { cache: "no-store", credentials: "include" })
|
||
.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>
|
||
);
|
||
}
|