feat(web): update sponsors logic

This commit is contained in:
Aman Varshney
2025-08-30 17:56:00 +05:30
parent 881834ce24
commit 383ea6ff33
12 changed files with 230 additions and 382 deletions

View File

@@ -11,60 +11,29 @@ import {
import Image from "next/image";
import { useState } from "react";
import {
calculateLifetimeContribution,
filterCurrentSponsors,
filterPastSponsors,
filterSpecialSponsors,
filterVisibleSponsors,
formatSponsorUrl,
getSponsorUrl,
isSpecialSponsor,
shouldShowLifetimeTotal,
sortSpecialSponsors,
sortSponsors,
} from "@/lib/sponsor-utils";
type SponsorEntry = {
sponsor: {
login: string;
name: string;
avatarUrl: string;
websiteUrl?: string;
linkUrl: string;
customLogoUrl?: string;
type: string;
};
isOneTime: boolean;
monthlyDollars: number;
privacyLevel: string;
tierName: string;
createdAt: string;
provider: string;
};
import type { SponsorsData } from "@/lib/types";
export default function SponsorsSection({
sponsors,
sponsorsData,
}: {
sponsors: Array<SponsorEntry>;
sponsorsData: SponsorsData;
}) {
const [showPastSponsors, setShowPastSponsors] = useState(false);
const sponsorsData =
sponsors.map((sponsor) => ({
...sponsor,
sponsor: {
...sponsor.sponsor,
customLogoUrl: sponsor.sponsor.customLogoUrl || "",
},
})) || [];
const visibleSponsors = filterVisibleSponsors(sponsorsData);
const sortedSponsors = sortSponsors(visibleSponsors);
const currentSponsors = filterCurrentSponsors(sortedSponsors);
const pastSponsors = filterPastSponsors(sortSponsors(sponsorsData));
const specialSponsors = sortSpecialSponsors(
filterSpecialSponsors(currentSponsors),
);
const allCurrentSponsors = [
...sponsorsData.specialSponsors,
...sponsorsData.sponsors,
];
const visibleSponsors = filterVisibleSponsors(allCurrentSponsors);
const pastSponsors = sponsorsData.pastSponsors;
const specialSponsors = sortSpecialSponsors(sponsorsData.specialSponsors);
return (
<div className="">
@@ -117,15 +86,11 @@ export default function SponsorsSection({
<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">
{specialSponsors.map((entry, index) => {
const since = new Date(entry.createdAt).toLocaleDateString(
undefined,
{ year: "numeric", month: "short" },
);
const sponsorUrl = getSponsorUrl(entry);
return (
<div
key={entry.sponsor.login}
key={entry.githubId}
className="rounded border border-border"
style={{ animationDelay: `${index * 50}ms` }}
>
@@ -135,7 +100,7 @@ export default function SponsorsSection({
<div className="ml-auto flex items-center gap-2 text-muted-foreground text-xs">
<span>SPECIAL</span>
<span></span>
<span>SINCE {since.toUpperCase()}</span>
<span>{entry.sinceWhen.toUpperCase()}</span>
</div>
</div>
</div>
@@ -143,11 +108,8 @@ export default function SponsorsSection({
<div className="flex gap-4">
<div className="flex-shrink-0">
<Image
src={
entry.sponsor.customLogoUrl ||
entry.sponsor.avatarUrl
}
alt={entry.sponsor.name || entry.sponsor.login}
src={entry.avatarUrl}
alt={entry.name}
width={100}
height={100}
className="rounded border border-border transition-colors duration-300"
@@ -157,33 +119,38 @@ export default function SponsorsSection({
<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}
{entry.name}
</h3>
{entry.tierName && (
{shouldShowLifetimeTotal(entry) ? (
<>
{entry.tierName && (
<p className="text-primary text-xs">
{entry.tierName}
</p>
)}
<p className="text-muted-foreground text-xs">
Total: {entry.formattedAmount}
</p>
</>
) : (
<p className="text-primary text-xs">
{entry.tierName}
</p>
)}
{shouldShowLifetimeTotal(entry) && (
<p className="text-muted-foreground text-xs">
Total: ${calculateLifetimeContribution(entry)}
</p>
)}
</div>
<div className="flex flex-col">
<a
href={`https://github.com/${entry.sponsor.login}`}
href={entry.githubUrl}
target="_blank"
rel="noopener noreferrer"
className="group flex items-center gap-2 text-muted-foreground text-xs transition-colors hover:text-primary"
>
<Github className="size-3" />
<span className="truncate">
{entry.sponsor.login}
{entry.githubId}
</span>
</a>
{(entry.sponsor.websiteUrl ||
entry.sponsor.linkUrl) && (
{entry.websiteUrl && (
<a
href={sponsorUrl}
target="_blank"
@@ -207,98 +174,91 @@ export default function SponsorsSection({
</div>
)}
{currentSponsors.filter((s) => !isSpecialSponsor(s)).length > 0 && (
{sponsorsData.sponsors.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">
{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>
{sponsorsData.sponsors.map((entry, index) => {
const sponsorUrl = getSponsorUrl(entry);
return (
<div
key={entry.githubId}
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>{entry.sinceWhen.toUpperCase()}</span>
</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>
)}
{shouldShowLifetimeTotal(entry) && (
</div>
<div className="p-4">
<div className="flex gap-4">
<div className="flex-shrink-0">
<Image
src={entry.avatarUrl}
alt={entry.name}
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.name}
</h3>
{shouldShowLifetimeTotal(entry) ? (
<>
{entry.tierName && (
<p className="text-primary text-xs">
{entry.tierName}
</p>
)}
<p className="text-muted-foreground text-xs">
Total: $
{calculateLifetimeContribution(entry)}
Total: {entry.formattedAmount}
</p>
)}
</div>
<div className="flex flex-col">
</>
) : (
<p className="text-primary text-xs">
{entry.tierName}
</p>
)}
</div>
<div className="flex flex-col">
<a
href={entry.githubUrl}
target="_blank"
rel="noopener noreferrer"
className="group flex items-center gap-2 text-muted-foreground text-xs transition-colors hover:text-primary"
>
<Github className="size-3" />
<span className="truncate">
{entry.githubId}
</span>
</a>
{entry.websiteUrl && (
<a
href={`https://github.com/${entry.sponsor.login}`}
href={sponsorUrl}
target="_blank"
rel="noopener noreferrer"
className="group flex items-center gap-2 text-muted-foreground text-xs transition-colors hover:text-primary"
>
<Github className="size-3" />
<Globe className="size-3" />
<span className="truncate">
{entry.sponsor.login}
{formatSponsorUrl(sponsorUrl)}
</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="size-3" />
<span className="truncate">
{formatSponsorUrl(
entry.sponsor.websiteUrl ||
entry.sponsor.linkUrl,
)}
</span>
</a>
)}
</div>
)}
</div>
</div>
</div>
</div>
);
})}
</div>
);
})}
</div>
</div>
)}
@@ -330,16 +290,12 @@ export default function SponsorsSection({
{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" },
);
const wasSpecial = isSpecialSponsor(entry);
const sponsorUrl = getSponsorUrl(entry);
return (
<div
key={entry.sponsor.login}
key={entry.githubId}
className="rounded border border-border/70 bg-muted/20"
style={{ animationDelay: `${index * 50}ms` }}
>
@@ -355,7 +311,7 @@ export default function SponsorsSection({
<div className="ml-auto flex items-center gap-2 text-muted-foreground text-xs">
{wasSpecial && <span>SPECIAL</span>}
{wasSpecial && <span></span>}
<span>SINCE {since.toUpperCase()}</span>
<span>{entry.sinceWhen.toUpperCase()}</span>
</div>
</div>
</div>
@@ -363,11 +319,8 @@ export default function SponsorsSection({
<div className="flex gap-4">
<div className="flex-shrink-0">
<Image
src={
entry.sponsor.customLogoUrl ||
entry.sponsor.avatarUrl
}
alt={entry.sponsor.name || entry.sponsor.login}
src={entry.avatarUrl}
alt={entry.name}
width={80}
height={80}
className="rounded border border-border/70"
@@ -377,34 +330,38 @@ export default function SponsorsSection({
<div className="grid grid-cols-1 grid-rows-[1fr_auto] justify-between">
<div>
<h3 className="truncate font-semibold text-muted-foreground text-sm">
{entry.sponsor.name || entry.sponsor.login}
{entry.name}
</h3>
{entry.tierName && (
{shouldShowLifetimeTotal(entry) ? (
<>
{entry.tierName && (
<p className="text-muted-foreground/70 text-xs">
{entry.tierName}
</p>
)}
<p className="text-muted-foreground/50 text-xs">
Total: {entry.formattedAmount}
</p>
</>
) : (
<p className="text-muted-foreground/70 text-xs">
{entry.tierName}
</p>
)}
{!entry.isOneTime && (
<p className="text-muted-foreground/50 text-xs">
Total: $
{calculateLifetimeContribution(entry)}
</p>
)}
</div>
<div className="flex flex-col">
<a
href={`https://github.com/${entry.sponsor.login}`}
href={entry.githubUrl}
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="size-3" />
<span className="truncate">
{entry.sponsor.login}
{entry.githubId}
</span>
</a>
{(entry.sponsor.websiteUrl ||
entry.sponsor.linkUrl) && (
{entry.websiteUrl && (
<a
href={sponsorUrl}
target="_blank"

View File

@@ -2,6 +2,7 @@ export const dynamic = "force-static";
import { api } from "@better-t-stack/backend/convex/_generated/api";
import { fetchQuery } from "convex/nextjs";
import { fetchSponsors } from "@/lib/sponsors";
import CommandSection from "./_components/command-section";
import Footer from "./_components/footer";
import HeroSection from "./_components/hero-section";
@@ -10,7 +11,7 @@ import StatsSection from "./_components/stats-section";
import Testimonials from "./_components/testimonials";
export default async function HomePage() {
const sponsors = await fetchQuery(api.sponsors.getSponsors);
const sponsorsData = await fetchSponsors();
const fetchedTweets = await fetchQuery(api.testimonials.getTweets);
const fetchedVideos = await fetchQuery(api.testimonials.getVideos);
const videos = fetchedVideos.map((v) => ({
@@ -31,7 +32,7 @@ export default async function HomePage() {
<HeroSection />
<CommandSection />
<StatsSection analyticsData={minimalAnalyticsData} />
<SponsorsSection sponsors={sponsors} />
<SponsorsSection sponsorsData={sponsorsData} />
<Testimonials tweets={tweets} videos={videos} />
</div>
<Footer />

View File

@@ -8,30 +8,24 @@ import {
HoverCardTrigger,
} from "@/components/ui/hover-card";
import {
filterCurrentSponsors,
filterSpecialSponsors,
formatSponsorUrl,
getSponsorUrl,
sortSpecialSponsors,
} from "@/lib/sponsor-utils";
import type { Sponsor } from "@/lib/types";
import type { Sponsor, SponsorsData } from "@/lib/types";
export function SpecialSponsorBanner() {
const [specialSponsors, setSpecialSponsors] = useState<Sponsor[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetch("https://sponsors.amanv.dev/sponsors.json")
fetch("https://sponsors.better-t-stack.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 currentSponsors = filterCurrentSponsors(sponsorsData);
const specials = sortSpecialSponsors(
filterSpecialSponsors(currentSponsors),
);
.then((data: SponsorsData) => {
const specials = sortSpecialSponsors(data.specialSponsors);
setSpecialSponsors(specials);
setLoading(false);
})
@@ -63,30 +57,21 @@ export function SpecialSponsorBanner() {
<div>
<div className="no-scrollbar grid grid-cols-4 items-center gap-2 overflow-x-auto whitespace-nowrap py-1">
{specialSponsors.map((entry) => {
const displayName = entry.sponsor.name || entry.sponsor.login;
const imgSrc = entry.sponsor.customLogoUrl || entry.sponsor.avatarUrl;
const since = new Date(entry.createdAt).toLocaleDateString(
undefined,
{
year: "numeric",
month: "short",
},
);
const sponsorUrl = getSponsorUrl(entry);
return (
<HoverCard key={entry.sponsor.login}>
<HoverCard key={entry.githubId}>
<HoverCardTrigger asChild>
<a
href={entry.sponsor.websiteUrl || sponsorUrl}
href={entry.websiteUrl || sponsorUrl}
target="_blank"
rel="noopener noreferrer"
aria-label={displayName}
aria-label={entry.name}
className="inline-flex"
>
<Image
src={imgSrc}
alt={displayName}
src={entry.avatarUrl}
alt={entry.name}
width={66}
height={66}
className="size-12 rounded border border-border"
@@ -105,13 +90,13 @@ export function SpecialSponsorBanner() {
<div className="ml-auto text-muted-foreground text-xs">
<span>SPECIAL</span>
<span className="px-1"></span>
<span>SINCE {since.toUpperCase()}</span>
<span>{entry.sinceWhen.toUpperCase()}</span>
</div>
</div>
<div className="flex gap-3">
<Image
src={imgSrc}
alt={displayName}
src={entry.avatarUrl}
alt={entry.name}
width={80}
height={80}
className="rounded border border-border"
@@ -120,7 +105,7 @@ export function SpecialSponsorBanner() {
<div className="grid grid-cols-1 grid-rows-[1fr_auto]">
<div>
<h3 className="truncate font-semibold text-sm">
{displayName}
{entry.name}
</h3>
{entry.tierName ? (
<p className="text-primary text-xs">
@@ -130,17 +115,15 @@ export function SpecialSponsorBanner() {
</div>
<div className="flex flex-col gap-1">
<a
href={`https://github.com/${entry.sponsor.login}`}
href={entry.githubUrl}
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 className="truncate">{entry.githubId}</span>
</a>
{entry.sponsor.websiteUrl || entry.sponsor.linkUrl ? (
{entry.websiteUrl ? (
<a
href={sponsorUrl}
target="_blank"

View File

@@ -3,67 +3,30 @@ import type { Sponsor } from "@/lib/types";
export const SPECIAL_SPONSOR_THRESHOLD = 100;
export const getSponsorAmount = (sponsor: Sponsor): number => {
// For past sponsors, return 0
if (sponsor.monthlyDollars === -1) {
return 0;
// If totalProcessedAmount exists, use it, otherwise parse from tierName
if (sponsor.totalProcessedAmount !== undefined) {
return sponsor.totalProcessedAmount;
}
// For one-time sponsors, parse the actual amount from tierName
if (sponsor.isOneTime && sponsor.tierName) {
const match = sponsor.tierName.match(/\$(\d+(?:\.\d+)?)/);
return match ? Number.parseFloat(match[1]) : sponsor.monthlyDollars;
}
// For monthly sponsors, use monthlyDollars
return sponsor.monthlyDollars;
// Parse amount from tierName as fallback
const match = sponsor.tierName.match(/\$(\d+(?:\.\d+)?)/);
return match ? Number.parseFloat(match[1]) : 0;
};
export const calculateLifetimeContribution = (sponsor: Sponsor): number => {
// For past sponsors, return 0
if (sponsor.monthlyDollars === -1) {
return 0;
// If totalProcessedAmount exists, use it, otherwise parse from tierName
if (sponsor.totalProcessedAmount !== undefined) {
return sponsor.totalProcessedAmount;
}
// For one-time sponsors, return the one-time amount
if (sponsor.isOneTime && sponsor.tierName) {
const match = sponsor.tierName.match(/\$(\d+(?:\.\d+)?)/);
return match ? Number.parseFloat(match[1]) : 0;
}
// For monthly sponsors, calculate total contribution since they started
const startDate = new Date(sponsor.createdAt);
const currentDate = new Date();
const monthsSinceStart = Math.max(
1,
Math.floor(
(currentDate.getTime() - startDate.getTime()) /
(1000 * 60 * 60 * 24 * 30.44),
),
);
return sponsor.monthlyDollars * monthsSinceStart;
// Parse amount from tierName as fallback
const match = sponsor.tierName.match(/\$(\d+(?:\.\d+)?)/);
return match ? Number.parseFloat(match[1]) : 0;
};
export const shouldShowLifetimeTotal = (sponsor: Sponsor): boolean => {
// Don't show for past sponsors
if (sponsor.monthlyDollars === -1) {
return false;
}
// Don't show for one-time sponsors
if (sponsor.isOneTime) {
return false;
}
// Don't show for first month sponsors
const startDate = new Date(sponsor.createdAt);
const currentDate = new Date();
const monthsSinceStart = Math.floor(
(currentDate.getTime() - startDate.getTime()) /
(1000 * 60 * 60 * 24 * 30.44),
);
return monthsSinceStart > 1;
// Only show lifetime total if totalProcessedAmount exists
return sponsor.totalProcessedAmount !== undefined;
};
export const filterVisibleSponsors = (sponsors: Sponsor[]): Sponsor[] => {
@@ -87,69 +50,27 @@ export const sortSponsors = (sponsors: Sponsor[]): Sponsor[] => {
return sponsors.sort((a, b) => {
const aAmount = getSponsorAmount(a);
const bAmount = getSponsorAmount(b);
const aLifetime = calculateLifetimeContribution(a);
const bLifetime = calculateLifetimeContribution(b);
const aIsPast = a.monthlyDollars === -1;
const bIsPast = b.monthlyDollars === -1;
const aIsSpecial = isSpecialSponsor(a);
const bIsSpecial = isSpecialSponsor(b);
const aIsLifetimeSpecial = isLifetimeSpecialSponsor(a);
const bIsLifetimeSpecial = isLifetimeSpecialSponsor(b);
// 1. Special sponsors (>=$100 current) come first
// 1. Special sponsors (>=$100) come first
if (aIsSpecial && !bIsSpecial) return -1;
if (!aIsSpecial && bIsSpecial) return 1;
if (aIsSpecial && bIsSpecial) {
if (aAmount !== bAmount) {
return bAmount - aAmount;
}
// If amounts equal, prefer monthly over one-time
if (a.isOneTime && !b.isOneTime) return 1;
if (!a.isOneTime && b.isOneTime) return -1;
// Then by creation date (oldest first)
return new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime();
// If amounts equal, sort by name
return a.name.localeCompare(b.name);
}
// 2. Lifetime special sponsors (>=$100 total) come next
if (aIsLifetimeSpecial && !bIsLifetimeSpecial) return -1;
if (!aIsLifetimeSpecial && bIsLifetimeSpecial) return 1;
if (aIsLifetimeSpecial && bIsLifetimeSpecial) {
if (aLifetime !== bLifetime) {
return bLifetime - aLifetime;
}
// If lifetime amounts equal, prefer monthly over one-time
if (a.isOneTime && !b.isOneTime) return 1;
if (!a.isOneTime && b.isOneTime) return -1;
// Then by creation date (oldest first)
return new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime();
// 2. Regular sponsors sorted by amount (highest first)
if (aAmount !== bAmount) {
return bAmount - aAmount;
}
// 3. Current sponsors come before past sponsors
if (!aIsPast && bIsPast) return -1;
if (aIsPast && !bIsPast) return 1;
// 4. For current sponsors, sort by lifetime contribution (highest first)
if (!aIsPast && !bIsPast) {
if (aLifetime !== bLifetime) {
return bLifetime - aLifetime;
}
// If lifetime amounts equal, prefer monthly over one-time
if (a.isOneTime && !b.isOneTime) return 1;
if (!a.isOneTime && b.isOneTime) return -1;
// Then by creation date (oldest first)
return new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime();
}
// 5. For past sponsors, sort by lifetime contribution (highest first)
if (aIsPast && bIsPast) {
if (aLifetime !== bLifetime) {
return bLifetime - aLifetime;
}
// Then by creation date (newest first)
return new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime();
}
return 0;
// 3. If amounts equal, sort by name
return a.name.localeCompare(b.name);
});
};
@@ -158,33 +79,24 @@ export const sortSpecialSponsors = (sponsors: Sponsor[]): Sponsor[] => {
const aLifetime = calculateLifetimeContribution(a);
const bLifetime = calculateLifetimeContribution(b);
// First, prioritize current special sponsors
const aIsSpecial = isSpecialSponsor(a);
const bIsSpecial = isSpecialSponsor(b);
if (aIsSpecial && !bIsSpecial) return -1;
if (!aIsSpecial && bIsSpecial) return 1;
// Then sort by lifetime contribution (highest first)
// Sort by lifetime contribution (highest first)
if (aLifetime !== bLifetime) {
return bLifetime - aLifetime;
}
// If lifetime amounts equal, prefer monthly over one-time
if (a.isOneTime && !b.isOneTime) return 1;
if (!a.isOneTime && b.isOneTime) return -1;
// Then by creation date (oldest first)
return new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime();
// If amounts equal, sort by name
return a.name.localeCompare(b.name);
});
};
export const filterCurrentSponsors = (sponsors: Sponsor[]): Sponsor[] => {
return sponsors.filter((sponsor) => sponsor.monthlyDollars !== -1);
// In the new structure, all sponsors in the main arrays are current
return sponsors;
};
export const filterPastSponsors = (sponsors: Sponsor[]): Sponsor[] => {
return sponsors.filter((sponsor) => sponsor.monthlyDollars === -1);
export const filterPastSponsors = (_sponsors: Sponsor[]): Sponsor[] => {
// Past sponsors are handled separately in the new structure
return [];
};
export const filterSpecialSponsors = (sponsors: Sponsor[]): Sponsor[] => {
@@ -196,11 +108,7 @@ export const filterRegularSponsors = (sponsors: Sponsor[]): Sponsor[] => {
};
export const getSponsorUrl = (sponsor: Sponsor): string => {
return (
sponsor.sponsor.websiteUrl ||
sponsor.sponsor.linkUrl ||
`https://github.com/${sponsor.sponsor.login}`
);
return sponsor.websiteUrl || sponsor.githubUrl;
};
export const formatSponsorUrl = (url: string): string => {

View File

@@ -0,0 +1,35 @@
import type { SponsorsData } from "./types";
const SPONSORS_URL = "https://sponsors.better-t-stack.dev/sponsors.json";
export async function fetchSponsors(): Promise<SponsorsData> {
try {
const response = await fetch(SPONSORS_URL, {
next: { revalidate: 3600 },
});
if (!response.ok) {
throw new Error(`Failed to fetch sponsors: ${response.status}`);
}
const data = await response.json();
return data as SponsorsData;
} catch (error) {
console.error("Error fetching sponsors:", error);
return {
generated_at: new Date().toISOString(),
summary: {
total_sponsors: 0,
total_lifetime_amount: 0,
total_current_monthly: 0,
special_sponsors: 0,
current_sponsors: 0,
past_sponsors: 0,
top_sponsor: { name: "", amount: 0 },
},
specialSponsors: [],
sponsors: [],
pastSponsors: [],
};
}
}

View File

@@ -25,19 +25,33 @@ export interface TechEdge {
}
export interface Sponsor {
sponsor: {
login: string;
name: string;
avatarUrl: string;
websiteUrl?: string;
linkUrl: string;
customLogoUrl: string;
type: string;
};
isOneTime: boolean;
monthlyDollars: number;
privacyLevel: string;
name: string;
githubId: string;
avatarUrl: string;
websiteUrl?: string;
githubUrl: string;
tierName: string;
createdAt: string;
provider: string;
totalProcessedAmount?: number;
sinceWhen: string;
transactionCount: number;
formattedAmount?: string;
}
export interface SponsorsData {
generated_at: string;
summary: {
total_sponsors: number;
total_lifetime_amount: number;
total_current_monthly: number;
special_sponsors: number;
current_sponsors: number;
past_sponsors: number;
top_sponsor: {
name: string;
amount: number;
};
};
specialSponsors: Sponsor[];
sponsors: Sponsor[];
pastSponsors: Sponsor[];
}

View File

@@ -12,7 +12,6 @@ import type * as healthCheck from "../healthCheck.js";
import type * as hooks from "../hooks.js";
import type * as http from "../http.js";
import type * as showcase from "../showcase.js";
import type * as sponsors from "../sponsors.js";
import type * as stats from "../stats.js";
import type * as testimonials from "../testimonials.js";
@@ -35,7 +34,6 @@ declare const fullApi: ApiFromModules<{
hooks: typeof hooks;
http: typeof http;
showcase: typeof showcase;
sponsors: typeof sponsors;
stats: typeof stats;
testimonials: typeof testimonials;
}>;

View File

@@ -2,24 +2,6 @@ import { defineSchema, defineTable } from "convex/server";
import { v } from "convex/values";
export default defineSchema({
sponsors: defineTable({
sponsor: v.object({
login: v.string(),
name: v.string(),
avatarUrl: v.string(),
websiteUrl: v.optional(v.string()),
linkUrl: v.string(),
customLogoUrl: v.optional(v.string()),
type: v.string(),
}),
isOneTime: v.boolean(),
monthlyDollars: v.number(),
privacyLevel: v.string(),
tierName: v.string(),
createdAt: v.string(),
provider: v.string(),
}),
videos: defineTable({
embedId: v.string(),
title: v.string(),

View File

@@ -1,30 +0,0 @@
import { v } from "convex/values";
import { query } from "./_generated/server";
export const getSponsors = query({
args: {},
returns: v.array(
v.object({
_id: v.id("sponsors"),
_creationTime: v.number(),
sponsor: v.object({
login: v.string(),
name: v.string(),
avatarUrl: v.string(),
websiteUrl: v.optional(v.string()),
linkUrl: v.string(),
customLogoUrl: v.optional(v.string()),
type: v.string(),
}),
isOneTime: v.boolean(),
monthlyDollars: v.number(),
privacyLevel: v.string(),
tierName: v.string(),
createdAt: v.string(),
provider: v.string(),
}),
),
handler: async (ctx) => {
return await ctx.db.query("sponsors").collect();
},
});

View File

0
test-lifetime-fix.js Normal file
View File

View File