mirror of
https://github.com/FranP-code/create-better-t-stack.git
synced 2025-10-12 23:52:15 +00:00
feat(cli): add alchemy and improve cli tooling and structure (#520)
This commit is contained in:
@@ -1,3 +1,5 @@
|
||||
import { api } from "@better-t-stack/backend/convex/_generated/api";
|
||||
import { useQueryWithStatus } from "@better-t-stack/backend/convex/hooks";
|
||||
import {
|
||||
ChevronDown,
|
||||
ChevronUp,
|
||||
@@ -8,12 +10,10 @@ import {
|
||||
Terminal,
|
||||
} from "lucide-react";
|
||||
import Image from "next/image";
|
||||
// import Link from "next/link";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useState } from "react";
|
||||
import {
|
||||
filterCurrentSponsors,
|
||||
filterPastSponsors,
|
||||
filterRegularSponsors,
|
||||
filterSpecialSponsors,
|
||||
formatSponsorUrl,
|
||||
getSponsorUrl,
|
||||
@@ -21,39 +21,91 @@ import {
|
||||
sortSpecialSponsors,
|
||||
sortSponsors,
|
||||
} from "@/lib/sponsor-utils";
|
||||
import type { Sponsor } from "@/lib/types";
|
||||
|
||||
export default function SponsorsSection() {
|
||||
const [sponsors, setSponsors] = useState<Sponsor[]>([]);
|
||||
const [loadingSponsors, setLoadingSponsors] = useState(true);
|
||||
const [sponsorError, setSponsorError] = useState<string | null>(null);
|
||||
const [showPastSponsors, setShowPastSponsors] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
fetch("https://sponsors.amanv.dev/sponsors.json")
|
||||
.then((res) => {
|
||||
if (!res.ok) throw new Error("Failed to fetch sponsors");
|
||||
return res.json();
|
||||
})
|
||||
.then((data) => {
|
||||
const sponsorsData = Array.isArray(data) ? data : [];
|
||||
const sortedSponsors = sortSponsors(sponsorsData);
|
||||
setSponsors(sortedSponsors);
|
||||
setLoadingSponsors(false);
|
||||
})
|
||||
.catch(() => {
|
||||
setSponsorError("Could not load sponsors");
|
||||
setLoadingSponsors(false);
|
||||
});
|
||||
}, []);
|
||||
const sponsorsQuery = useQueryWithStatus(api.sponsors.getSponsors);
|
||||
|
||||
const currentSponsors = filterCurrentSponsors(sponsors);
|
||||
const pastSponsors = filterPastSponsors(sponsors);
|
||||
if (sponsorsQuery.isPending) {
|
||||
return (
|
||||
<div className="mb-12">
|
||||
<div className="mb-6 flex flex-wrap items-center justify-between gap-2 sm:flex-nowrap">
|
||||
<div className="flex items-center gap-2">
|
||||
<Terminal className="h-5 w-5 text-primary" />
|
||||
<span className="font-bold text-lg sm:text-xl">
|
||||
SPONSORS_DATABASE.JSON
|
||||
</span>
|
||||
</div>
|
||||
<div className="hidden h-px flex-1 bg-border sm:block" />
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-muted-foreground text-xs">
|
||||
[LOADING... RECORDS]
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="rounded border border-border p-8">
|
||||
<div className="flex items-center justify-center gap-2">
|
||||
<div className="h-2 w-2 animate-pulse rounded-full bg-primary" />
|
||||
<span className="text-muted-foreground">LOADING_SPONSORS.SH</span>
|
||||
<div className="h-2 w-2 animate-pulse rounded-full bg-primary" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (sponsorsQuery.isError) {
|
||||
return (
|
||||
<div className="mb-12">
|
||||
<div className="mb-6 flex flex-wrap items-center justify-between gap-2 sm:flex-nowrap">
|
||||
<div className="flex items-center gap-2">
|
||||
<Terminal className="h-5 w-5 text-primary" />
|
||||
<span className="font-bold text-lg sm:text-xl">
|
||||
SPONSORS_DATABASE.JSON
|
||||
</span>
|
||||
</div>
|
||||
<div className="hidden h-px flex-1 bg-border sm:block" />
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-muted-foreground text-xs">
|
||||
[ERROR RECORDS]
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="rounded border border-border p-8">
|
||||
<div className="text-center">
|
||||
<div className="mb-4 flex items-center justify-center gap-2">
|
||||
<span className="text-destructive">
|
||||
ERROR_LOADING_SPONSORS.NULL
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-center gap-2 text-sm">
|
||||
<span className="text-primary">$</span>
|
||||
<span className="text-muted-foreground">
|
||||
Please try again later!
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const sponsors =
|
||||
sponsorsQuery.data?.map((sponsor) => ({
|
||||
...sponsor,
|
||||
sponsor: {
|
||||
...sponsor.sponsor,
|
||||
customLogoUrl: sponsor.sponsor.customLogoUrl || "",
|
||||
},
|
||||
})) || [];
|
||||
|
||||
const sortedSponsors = sortSponsors(sponsors);
|
||||
const currentSponsors = filterCurrentSponsors(sortedSponsors);
|
||||
const pastSponsors = filterPastSponsors(sortedSponsors);
|
||||
const specialSponsors = sortSpecialSponsors(
|
||||
filterSpecialSponsors(currentSponsors),
|
||||
);
|
||||
const regularSponsors = filterRegularSponsors(currentSponsors);
|
||||
|
||||
return (
|
||||
<div className="mb-12">
|
||||
@@ -65,37 +117,24 @@ export default function SponsorsSection() {
|
||||
</span>
|
||||
</div>
|
||||
<div className="hidden h-px flex-1 bg-border sm:block" />
|
||||
<span className="w-full text-right text-muted-foreground text-xs sm:w-auto sm:text-left">
|
||||
[{loadingSponsors ? "LOADING..." : sponsors.length} RECORDS]
|
||||
</span>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-muted-foreground text-xs">
|
||||
[{sponsors.length} RECORDS]
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
{loadingSponsors ? (
|
||||
<div className="rounded border border-border p-8">
|
||||
<div className="flex items-center justify-center gap-2">
|
||||
<div className="h-2 w-2 animate-pulse rounded-full bg-primary" />
|
||||
<span className=" text-muted-foreground">LOADING_SPONSORS.SH</span>
|
||||
<div className="h-2 w-2 animate-pulse rounded-full bg-primary" />
|
||||
</div>
|
||||
</div>
|
||||
) : sponsorError ? (
|
||||
<div className="rounded border border-border bg-destructive/10 p-8">
|
||||
<div className="flex items-center justify-center gap-2">
|
||||
<span className="text-destructive">✗</span>
|
||||
<span className=" text-destructive">ERROR: {sponsorError}</span>
|
||||
</div>
|
||||
</div>
|
||||
) : sponsors.length === 0 ? (
|
||||
{sponsors.length === 0 ? (
|
||||
<div className="space-y-4">
|
||||
<div className="rounded border border-border p-8">
|
||||
<div className="text-center">
|
||||
<div className="mb-4 flex items-center justify-center gap-2">
|
||||
<span className=" text-muted-foreground">
|
||||
<span className="text-muted-foreground">
|
||||
NO_SPONSORS_FOUND.NULL
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-center gap-2 text-sm">
|
||||
<span className="text-primary">$</span>
|
||||
<span className=" text-muted-foreground">
|
||||
<span className="text-muted-foreground">
|
||||
Be the first to support this project!
|
||||
</span>
|
||||
</div>
|
||||
@@ -162,7 +201,7 @@ export default function SponsorsSection() {
|
||||
{entry.sponsor.name || entry.sponsor.login}
|
||||
</h3>
|
||||
{entry.tierName && (
|
||||
<p className=" text-primary text-xs">
|
||||
<p className="text-primary text-xs">
|
||||
{entry.tierName}
|
||||
</p>
|
||||
)}
|
||||
@@ -203,90 +242,93 @@ export default function SponsorsSection() {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{regularSponsors.length > 0 && (
|
||||
|
||||
{currentSponsors.filter((s) => !isSpecialSponsor(s)).length > 0 && (
|
||||
<div className="space-y-4">
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
|
||||
{regularSponsors.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"
|
||||
style={{ animationDelay: `${index * 50}ms` }}
|
||||
>
|
||||
<div className="border-border border-b px-3 py-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-primary text-xs">▶</span>
|
||||
<div className="ml-auto flex items-center gap-2 text-muted-foreground text-xs">
|
||||
<span>SINCE {since.toUpperCase()}</span>
|
||||
{currentSponsors
|
||||
.filter((s) => !isSpecialSponsor(s))
|
||||
.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"
|
||||
style={{ animationDelay: `${index * 50}ms` }}
|
||||
>
|
||||
<div className="border-border border-b px-3 py-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-primary text-xs">▶</span>
|
||||
<div className="ml-auto flex items-center gap-2 text-muted-foreground text-xs">
|
||||
<span>SINCE {since.toUpperCase()}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-4">
|
||||
<div className="flex gap-4">
|
||||
<div className="flex-shrink-0">
|
||||
<Image
|
||||
src={entry.sponsor.avatarUrl}
|
||||
alt={entry.sponsor.name || entry.sponsor.login}
|
||||
width={100}
|
||||
height={100}
|
||||
className="rounded border border-border transition-colors duration-300"
|
||||
unoptimized
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 grid-rows-[1fr_auto] justify-between py-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 className="p-4">
|
||||
<div className="flex gap-4">
|
||||
<div className="flex-shrink-0">
|
||||
<Image
|
||||
src={entry.sponsor.avatarUrl}
|
||||
alt={entry.sponsor.name || entry.sponsor.login}
|
||||
width={100}
|
||||
height={100}
|
||||
className="rounded border border-border transition-colors duration-300"
|
||||
unoptimized
|
||||
/>
|
||||
</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) && (
|
||||
<div className="grid grid-cols-1 grid-rows-[1fr_auto] justify-between py-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={
|
||||
entry.sponsor.websiteUrl ||
|
||||
entry.sponsor.linkUrl
|
||||
}
|
||||
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"
|
||||
>
|
||||
<Globe className="h-4 w-4" />
|
||||
<Github className="h-4 w-4" />
|
||||
<span className="truncate">
|
||||
{formatSponsorUrl(
|
||||
entry.sponsor.websiteUrl ||
|
||||
entry.sponsor.linkUrl,
|
||||
)}
|
||||
{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">
|
||||
{formatSponsorUrl(
|
||||
entry.sponsor.websiteUrl ||
|
||||
entry.sponsor.linkUrl,
|
||||
)}
|
||||
</span>
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user