mirror of
https://github.com/FranP-code/create-better-t-stack.git
synced 2025-10-12 23:52:15 +00:00
feat(web): Add past sponsors section with toggle visibility
This commit is contained in:
@@ -1,4 +1,11 @@
|
|||||||
import { Github, Globe, Heart, Terminal } from "lucide-react";
|
import {
|
||||||
|
ChevronDown,
|
||||||
|
ChevronUp,
|
||||||
|
Github,
|
||||||
|
Globe,
|
||||||
|
Heart,
|
||||||
|
Terminal,
|
||||||
|
} from "lucide-react";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import type { Sponsor } from "@/lib/types";
|
import type { Sponsor } from "@/lib/types";
|
||||||
@@ -7,6 +14,7 @@ export default function SponsorsSection() {
|
|||||||
const [sponsors, setSponsors] = useState<Sponsor[]>([]);
|
const [sponsors, setSponsors] = useState<Sponsor[]>([]);
|
||||||
const [loadingSponsors, setLoadingSponsors] = useState(true);
|
const [loadingSponsors, setLoadingSponsors] = useState(true);
|
||||||
const [sponsorError, setSponsorError] = useState<string | null>(null);
|
const [sponsorError, setSponsorError] = useState<string | null>(null);
|
||||||
|
const [showPastSponsors, setShowPastSponsors] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetch("https://sponsors.amanv.dev/sponsors.json")
|
fetch("https://sponsors.amanv.dev/sponsors.json")
|
||||||
@@ -31,6 +39,15 @@ export default function SponsorsSection() {
|
|||||||
return 0;
|
return 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (a.monthlyDollars === -1 && b.monthlyDollars !== -1) return 1;
|
||||||
|
if (a.monthlyDollars !== -1 && b.monthlyDollars === -1) return -1;
|
||||||
|
|
||||||
|
if (a.monthlyDollars === -1 && b.monthlyDollars === -1) {
|
||||||
|
return (
|
||||||
|
new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const aIsMonthly = !a.isOneTime;
|
const aIsMonthly = !a.isOneTime;
|
||||||
const bIsMonthly = !b.isOneTime;
|
const bIsMonthly = !b.isOneTime;
|
||||||
|
|
||||||
@@ -49,6 +66,13 @@ export default function SponsorsSection() {
|
|||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const currentSponsors = sponsors.filter(
|
||||||
|
(sponsor) => sponsor.monthlyDollars !== -1,
|
||||||
|
);
|
||||||
|
const pastSponsors = sponsors.filter(
|
||||||
|
(sponsor) => sponsor.monthlyDollars === -1,
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mb-12">
|
<div className="mb-12">
|
||||||
<div className="mb-6 flex flex-wrap items-center justify-between gap-2 sm:flex-nowrap">
|
<div className="mb-6 flex flex-wrap items-center justify-between gap-2 sm:flex-nowrap">
|
||||||
@@ -124,99 +148,227 @@ export default function SponsorsSection() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="space-y-6">
|
<div className="space-y-8">
|
||||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
|
{currentSponsors.length > 0 && (
|
||||||
{sponsors.map((entry, index) => {
|
<div className="space-y-4">
|
||||||
const since = new Date(entry.createdAt).toLocaleDateString(
|
<div className="flex items-center gap-2">
|
||||||
undefined,
|
<span className="text-primary text-sm">▶</span>
|
||||||
{ year: "numeric", month: "short" },
|
<span className="font-semibold text-foreground text-sm">
|
||||||
);
|
ACTIVE_SPONSORS.EXE
|
||||||
return (
|
</span>
|
||||||
<div
|
<span className="text-muted-foreground text-xs">
|
||||||
key={entry.sponsor.login}
|
({currentSponsors.length})
|
||||||
className="rounded border border-border"
|
</span>
|
||||||
style={{ animationDelay: `${index * 50}ms` }}
|
</div>
|
||||||
>
|
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
|
||||||
<div className="border-border border-b px-3 py-2">
|
{currentSponsors.map((entry, index) => {
|
||||||
<div className="flex items-center gap-2">
|
const since = new Date(entry.createdAt).toLocaleDateString(
|
||||||
<span className="text-primary text-xs">▶</span>
|
undefined,
|
||||||
<div className="ml-auto flex items-center gap-2 text-muted-foreground text-xs">
|
{ year: "numeric", month: "short" },
|
||||||
<span>{entry.isOneTime ? "ONE-TIME" : "MONTHLY"}</span>
|
);
|
||||||
<span>•</span>
|
return (
|
||||||
<span>SINCE {since.toUpperCase()}</span>
|
<div
|
||||||
</div>
|
key={entry.sponsor.login}
|
||||||
</div>
|
className="rounded border border-border"
|
||||||
</div>
|
style={{ animationDelay: `${index * 50}ms` }}
|
||||||
<div className="p-4">
|
>
|
||||||
<div className="flex items-center gap-4">
|
<div className="border-border border-b px-3 py-2">
|
||||||
<div className="flex-shrink-0">
|
<div className="flex items-center gap-2">
|
||||||
<Image
|
<span className="text-primary text-xs">▶</span>
|
||||||
src={entry.sponsor.avatarUrl}
|
<div className="ml-auto flex items-center gap-2 text-muted-foreground text-xs">
|
||||||
alt={entry.sponsor.name || entry.sponsor.login}
|
<span>
|
||||||
width={100}
|
{entry.isOneTime ? "ONE-TIME" : "MONTHLY"}
|
||||||
height={100}
|
|
||||||
className="rounded border border-border transition-colors duration-300"
|
|
||||||
unoptimized
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="min-w-0 flex-1 space-y-2">
|
|
||||||
<div>
|
|
||||||
<h3 className="truncate font-semibold text-foreground text-sm">
|
|
||||||
{entry.sponsor.name || entry.sponsor.login}
|
|
||||||
</h3>
|
|
||||||
{entry.tierName && (
|
|
||||||
<p className=" text-primary text-xs">
|
|
||||||
{entry.tierName}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col gap-1">
|
|
||||||
<a
|
|
||||||
href={`https://github.com/${entry.sponsor.login}`}
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
className="group flex items-center gap-2 text-muted-foreground text-xs transition-colors hover:text-primary"
|
|
||||||
>
|
|
||||||
<Github className="h-4 w-4" />
|
|
||||||
<span className="truncate">
|
|
||||||
{entry.sponsor.login}
|
|
||||||
</span>
|
</span>
|
||||||
</a>
|
<span>•</span>
|
||||||
{(entry.sponsor.websiteUrl ||
|
<span>SINCE {since.toUpperCase()}</span>
|
||||||
entry.sponsor.linkUrl) && (
|
</div>
|
||||||
<a
|
</div>
|
||||||
href={
|
</div>
|
||||||
entry.sponsor.websiteUrl ||
|
<div className="p-4">
|
||||||
entry.sponsor.linkUrl
|
<div className="flex items-center gap-4">
|
||||||
}
|
<div className="flex-shrink-0">
|
||||||
target="_blank"
|
<Image
|
||||||
rel="noopener noreferrer"
|
src={entry.sponsor.avatarUrl}
|
||||||
className="group flex items-center gap-2 text-muted-foreground text-xs transition-colors hover:text-primary"
|
alt={entry.sponsor.name || entry.sponsor.login}
|
||||||
>
|
width={100}
|
||||||
<Globe className="h-4 w-4" />
|
height={100}
|
||||||
<span className="truncate">
|
className="rounded border border-border transition-colors duration-300"
|
||||||
{(
|
unoptimized
|
||||||
entry.sponsor.websiteUrl ||
|
/>
|
||||||
entry.sponsor.linkUrl
|
</div>
|
||||||
)
|
<div className="min-w-0 flex-1 space-y-2">
|
||||||
?.replace(/^https?:\/\//, "")
|
<div>
|
||||||
?.replace(/\/$/, "")}
|
<h3 className="truncate font-semibold text-foreground text-sm">
|
||||||
</span>
|
{entry.sponsor.name || entry.sponsor.login}
|
||||||
</a>
|
</h3>
|
||||||
)}
|
{entry.tierName && (
|
||||||
|
<p className=" text-primary text-xs">
|
||||||
{/* <div className="flex items-center gap-2 text-muted-foreground text-xs">
|
{entry.tierName}
|
||||||
<span className="text-xs">👤</span>
|
</p>
|
||||||
<span>{entry.sponsor.type.toUpperCase()}</span>
|
)}
|
||||||
</div> */}
|
</div>
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<a
|
||||||
|
href={`https://github.com/${entry.sponsor.login}`}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="group flex items-center gap-2 text-muted-foreground text-xs transition-colors hover:text-primary"
|
||||||
|
>
|
||||||
|
<Github className="h-4 w-4" />
|
||||||
|
<span className="truncate">
|
||||||
|
{entry.sponsor.login}
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
{(entry.sponsor.websiteUrl ||
|
||||||
|
entry.sponsor.linkUrl) && (
|
||||||
|
<a
|
||||||
|
href={
|
||||||
|
entry.sponsor.websiteUrl ||
|
||||||
|
entry.sponsor.linkUrl
|
||||||
|
}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="group flex items-center gap-2 text-muted-foreground text-xs transition-colors hover:text-primary"
|
||||||
|
>
|
||||||
|
<Globe className="h-4 w-4" />
|
||||||
|
<span className="truncate">
|
||||||
|
{(
|
||||||
|
entry.sponsor.websiteUrl ||
|
||||||
|
entry.sponsor.linkUrl
|
||||||
|
)
|
||||||
|
?.replace(/^https?:\/\//, "")
|
||||||
|
?.replace(/\/$/, "")}
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{pastSponsors.length > 0 && (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setShowPastSponsors(!showPastSponsors)}
|
||||||
|
className="flex w-full items-center gap-2 rounded p-2 text-left transition-colors hover:bg-muted/50"
|
||||||
|
>
|
||||||
|
{showPastSponsors ? (
|
||||||
|
<ChevronUp className="h-4 w-4 text-muted-foreground" />
|
||||||
|
) : (
|
||||||
|
<ChevronDown className="h-4 w-4 text-muted-foreground" />
|
||||||
|
)}
|
||||||
|
<span className="font-semibold text-muted-foreground text-sm">
|
||||||
|
PAST_SPONSORS.ARCHIVE
|
||||||
|
</span>
|
||||||
|
<span className="text-muted-foreground text-xs">
|
||||||
|
({pastSponsors.length})
|
||||||
|
</span>
|
||||||
|
<div className="mx-2 h-px flex-1 bg-border" />
|
||||||
|
<span className="text-muted-foreground text-xs">
|
||||||
|
{showPastSponsors ? "HIDE" : "SHOW"}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{showPastSponsors && (
|
||||||
|
<div className="slide-in-from-top-2 grid animate-in grid-cols-1 gap-4 duration-300 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
|
||||||
|
{pastSponsors.map((entry, index) => {
|
||||||
|
const since = new Date(entry.createdAt).toLocaleDateString(
|
||||||
|
undefined,
|
||||||
|
{ year: "numeric", month: "short" },
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={entry.sponsor.login}
|
||||||
|
className="rounded border border-border/70 bg-muted/20"
|
||||||
|
style={{ animationDelay: `${index * 50}ms` }}
|
||||||
|
>
|
||||||
|
<div className="border-border/70 border-b px-3 py-2">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="text-muted-foreground text-xs">
|
||||||
|
◆
|
||||||
|
</span>
|
||||||
|
<div className="ml-auto flex items-center gap-2 text-muted-foreground text-xs">
|
||||||
|
<span>PAST</span>
|
||||||
|
<span>•</span>
|
||||||
|
<span>SINCE {since.toUpperCase()}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="p-4">
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<div className="flex-shrink-0">
|
||||||
|
<Image
|
||||||
|
src={entry.sponsor.avatarUrl}
|
||||||
|
alt={entry.sponsor.name || entry.sponsor.login}
|
||||||
|
width={80}
|
||||||
|
height={80}
|
||||||
|
className="rounded border border-border/70 transition-colors duration-300"
|
||||||
|
unoptimized
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="min-w-0 flex-1 space-y-2">
|
||||||
|
<div>
|
||||||
|
<h3 className="truncate font-semibold text-muted-foreground text-sm">
|
||||||
|
{entry.sponsor.name || entry.sponsor.login}
|
||||||
|
</h3>
|
||||||
|
{entry.tierName && (
|
||||||
|
<p className="text-muted-foreground/70 text-xs">
|
||||||
|
{entry.tierName}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<a
|
||||||
|
href={`https://github.com/${entry.sponsor.login}`}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="group flex items-center gap-2 text-muted-foreground/70 text-xs transition-colors hover:text-muted-foreground"
|
||||||
|
>
|
||||||
|
<Github className="h-4 w-4" />
|
||||||
|
<span className="truncate">
|
||||||
|
{entry.sponsor.login}
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
{(entry.sponsor.websiteUrl ||
|
||||||
|
entry.sponsor.linkUrl) && (
|
||||||
|
<a
|
||||||
|
href={
|
||||||
|
entry.sponsor.websiteUrl ||
|
||||||
|
entry.sponsor.linkUrl
|
||||||
|
}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="group flex items-center gap-2 text-muted-foreground/70 text-xs transition-colors hover:text-muted-foreground"
|
||||||
|
>
|
||||||
|
<Globe className="h-4 w-4" />
|
||||||
|
<span className="truncate">
|
||||||
|
{(
|
||||||
|
entry.sponsor.websiteUrl ||
|
||||||
|
entry.sponsor.linkUrl
|
||||||
|
)
|
||||||
|
?.replace(/^https?:\/\//, "")
|
||||||
|
?.replace(/\/$/, "")}
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
);
|
)}
|
||||||
})}
|
</div>
|
||||||
</div>
|
)}
|
||||||
|
|
||||||
<div className="rounded border border-border p-4">
|
<div className="rounded border border-border p-4">
|
||||||
<a
|
<a
|
||||||
|
|||||||
Reference in New Issue
Block a user