Create monorepo from known-good production state
This commit is contained in:
commit
c034824338
651 changed files with 120469 additions and 0 deletions
111
app/components/account/SupporterBadges.tsx
Normal file
111
app/components/account/SupporterBadges.tsx
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
// 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(() => {
|
||||
let url = "/api/support/badges";
|
||||
const params = new URLSearchParams();
|
||||
if (email) params.set("email", String(email));
|
||||
if (userId) params.set("userId", String(userId));
|
||||
const qs = params.toString();
|
||||
if (qs) url += `?${qs}`;
|
||||
|
||||
fetch(url, { cache: "no-store" })
|
||||
.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>
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue