Skip to content

Commit 383ea6f

Browse files
feat(web): update sponsors logic
1 parent 881834c commit 383ea6f

File tree

12 files changed

+230
-382
lines changed

12 files changed

+230
-382
lines changed

apps/web/src/app/(home)/_components/sponsors-section.tsx

Lines changed: 117 additions & 160 deletions
Large diffs are not rendered by default.

apps/web/src/app/(home)/page.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ export const dynamic = "force-static";
22

33
import { api } from "@better-t-stack/backend/convex/_generated/api";
44
import { fetchQuery } from "convex/nextjs";
5+
import { fetchSponsors } from "@/lib/sponsors";
56
import CommandSection from "./_components/command-section";
67
import Footer from "./_components/footer";
78
import HeroSection from "./_components/hero-section";
@@ -10,7 +11,7 @@ import StatsSection from "./_components/stats-section";
1011
import Testimonials from "./_components/testimonials";
1112

1213
export default async function HomePage() {
13-
const sponsors = await fetchQuery(api.sponsors.getSponsors);
14+
const sponsorsData = await fetchSponsors();
1415
const fetchedTweets = await fetchQuery(api.testimonials.getTweets);
1516
const fetchedVideos = await fetchQuery(api.testimonials.getVideos);
1617
const videos = fetchedVideos.map((v) => ({
@@ -31,7 +32,7 @@ export default async function HomePage() {
3132
<HeroSection />
3233
<CommandSection />
3334
<StatsSection analyticsData={minimalAnalyticsData} />
34-
<SponsorsSection sponsors={sponsors} />
35+
<SponsorsSection sponsorsData={sponsorsData} />
3536
<Testimonials tweets={tweets} videos={videos} />
3637
</div>
3738
<Footer />

apps/web/src/components/special-sponsor-banner.tsx

Lines changed: 16 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -8,30 +8,24 @@ import {
88
HoverCardTrigger,
99
} from "@/components/ui/hover-card";
1010
import {
11-
filterCurrentSponsors,
12-
filterSpecialSponsors,
1311
formatSponsorUrl,
1412
getSponsorUrl,
1513
sortSpecialSponsors,
1614
} from "@/lib/sponsor-utils";
17-
import type { Sponsor } from "@/lib/types";
15+
import type { Sponsor, SponsorsData } from "@/lib/types";
1816

1917
export function SpecialSponsorBanner() {
2018
const [specialSponsors, setSpecialSponsors] = useState<Sponsor[]>([]);
2119
const [loading, setLoading] = useState(true);
2220

2321
useEffect(() => {
24-
fetch("https://sponsors.amanv.dev/sponsors.json")
22+
fetch("https://sponsors.better-t-stack.dev/sponsors.json")
2523
.then((res) => {
2624
if (!res.ok) throw new Error("Failed to fetch sponsors");
2725
return res.json();
2826
})
29-
.then((data) => {
30-
const sponsorsData = Array.isArray(data) ? data : [];
31-
const currentSponsors = filterCurrentSponsors(sponsorsData);
32-
const specials = sortSpecialSponsors(
33-
filterSpecialSponsors(currentSponsors),
34-
);
27+
.then((data: SponsorsData) => {
28+
const specials = sortSpecialSponsors(data.specialSponsors);
3529
setSpecialSponsors(specials);
3630
setLoading(false);
3731
})
@@ -63,30 +57,21 @@ export function SpecialSponsorBanner() {
6357
<div>
6458
<div className="no-scrollbar grid grid-cols-4 items-center gap-2 overflow-x-auto whitespace-nowrap py-1">
6559
{specialSponsors.map((entry) => {
66-
const displayName = entry.sponsor.name || entry.sponsor.login;
67-
const imgSrc = entry.sponsor.customLogoUrl || entry.sponsor.avatarUrl;
68-
const since = new Date(entry.createdAt).toLocaleDateString(
69-
undefined,
70-
{
71-
year: "numeric",
72-
month: "short",
73-
},
74-
);
7560
const sponsorUrl = getSponsorUrl(entry);
7661

7762
return (
78-
<HoverCard key={entry.sponsor.login}>
63+
<HoverCard key={entry.githubId}>
7964
<HoverCardTrigger asChild>
8065
<a
81-
href={entry.sponsor.websiteUrl || sponsorUrl}
66+
href={entry.websiteUrl || sponsorUrl}
8267
target="_blank"
8368
rel="noopener noreferrer"
84-
aria-label={displayName}
69+
aria-label={entry.name}
8570
className="inline-flex"
8671
>
8772
<Image
88-
src={imgSrc}
89-
alt={displayName}
73+
src={entry.avatarUrl}
74+
alt={entry.name}
9075
width={66}
9176
height={66}
9277
className="size-12 rounded border border-border"
@@ -105,13 +90,13 @@ export function SpecialSponsorBanner() {
10590
<div className="ml-auto text-muted-foreground text-xs">
10691
<span>SPECIAL</span>
10792
<span className="px-1"></span>
108-
<span>SINCE {since.toUpperCase()}</span>
93+
<span>{entry.sinceWhen.toUpperCase()}</span>
10994
</div>
11095
</div>
11196
<div className="flex gap-3">
11297
<Image
113-
src={imgSrc}
114-
alt={displayName}
98+
src={entry.avatarUrl}
99+
alt={entry.name}
115100
width={80}
116101
height={80}
117102
className="rounded border border-border"
@@ -120,7 +105,7 @@ export function SpecialSponsorBanner() {
120105
<div className="grid grid-cols-1 grid-rows-[1fr_auto]">
121106
<div>
122107
<h3 className="truncate font-semibold text-sm">
123-
{displayName}
108+
{entry.name}
124109
</h3>
125110
{entry.tierName ? (
126111
<p className="text-primary text-xs">
@@ -130,17 +115,15 @@ export function SpecialSponsorBanner() {
130115
</div>
131116
<div className="flex flex-col gap-1">
132117
<a
133-
href={`https://github.com/${entry.sponsor.login}`}
118+
href={entry.githubUrl}
134119
target="_blank"
135120
rel="noopener noreferrer"
136121
className="group flex items-center gap-2 text-muted-foreground text-xs transition-colors hover:text-primary"
137122
>
138123
<Github className="h-4 w-4" />
139-
<span className="truncate">
140-
{entry.sponsor.login}
141-
</span>
124+
<span className="truncate">{entry.githubId}</span>
142125
</a>
143-
{entry.sponsor.websiteUrl || entry.sponsor.linkUrl ? (
126+
{entry.websiteUrl ? (
144127
<a
145128
href={sponsorUrl}
146129
target="_blank"

apps/web/src/lib/sponsor-utils.ts

Lines changed: 31 additions & 123 deletions
Original file line numberDiff line numberDiff line change
@@ -3,67 +3,30 @@ import type { Sponsor } from "@/lib/types";
33
export const SPECIAL_SPONSOR_THRESHOLD = 100;
44

55
export const getSponsorAmount = (sponsor: Sponsor): number => {
6-
// For past sponsors, return 0
7-
if (sponsor.monthlyDollars === -1) {
8-
return 0;
6+
// If totalProcessedAmount exists, use it, otherwise parse from tierName
7+
if (sponsor.totalProcessedAmount !== undefined) {
8+
return sponsor.totalProcessedAmount;
99
}
1010

11-
// For one-time sponsors, parse the actual amount from tierName
12-
if (sponsor.isOneTime && sponsor.tierName) {
13-
const match = sponsor.tierName.match(/\$(\d+(?:\.\d+)?)/);
14-
return match ? Number.parseFloat(match[1]) : sponsor.monthlyDollars;
15-
}
16-
17-
// For monthly sponsors, use monthlyDollars
18-
return sponsor.monthlyDollars;
11+
// Parse amount from tierName as fallback
12+
const match = sponsor.tierName.match(/\$(\d+(?:\.\d+)?)/);
13+
return match ? Number.parseFloat(match[1]) : 0;
1914
};
2015

2116
export const calculateLifetimeContribution = (sponsor: Sponsor): number => {
22-
// For past sponsors, return 0
23-
if (sponsor.monthlyDollars === -1) {
24-
return 0;
25-
}
26-
27-
// For one-time sponsors, return the one-time amount
28-
if (sponsor.isOneTime && sponsor.tierName) {
29-
const match = sponsor.tierName.match(/\$(\d+(?:\.\d+)?)/);
30-
return match ? Number.parseFloat(match[1]) : 0;
17+
// If totalProcessedAmount exists, use it, otherwise parse from tierName
18+
if (sponsor.totalProcessedAmount !== undefined) {
19+
return sponsor.totalProcessedAmount;
3120
}
3221

33-
// For monthly sponsors, calculate total contribution since they started
34-
const startDate = new Date(sponsor.createdAt);
35-
const currentDate = new Date();
36-
const monthsSinceStart = Math.max(
37-
1,
38-
Math.floor(
39-
(currentDate.getTime() - startDate.getTime()) /
40-
(1000 * 60 * 60 * 24 * 30.44),
41-
),
42-
);
43-
44-
return sponsor.monthlyDollars * monthsSinceStart;
22+
// Parse amount from tierName as fallback
23+
const match = sponsor.tierName.match(/\$(\d+(?:\.\d+)?)/);
24+
return match ? Number.parseFloat(match[1]) : 0;
4525
};
4626

4727
export const shouldShowLifetimeTotal = (sponsor: Sponsor): boolean => {
48-
// Don't show for past sponsors
49-
if (sponsor.monthlyDollars === -1) {
50-
return false;
51-
}
52-
53-
// Don't show for one-time sponsors
54-
if (sponsor.isOneTime) {
55-
return false;
56-
}
57-
58-
// Don't show for first month sponsors
59-
const startDate = new Date(sponsor.createdAt);
60-
const currentDate = new Date();
61-
const monthsSinceStart = Math.floor(
62-
(currentDate.getTime() - startDate.getTime()) /
63-
(1000 * 60 * 60 * 24 * 30.44),
64-
);
65-
66-
return monthsSinceStart > 1;
28+
// Only show lifetime total if totalProcessedAmount exists
29+
return sponsor.totalProcessedAmount !== undefined;
6730
};
6831

6932
export const filterVisibleSponsors = (sponsors: Sponsor[]): Sponsor[] => {
@@ -87,69 +50,27 @@ export const sortSponsors = (sponsors: Sponsor[]): Sponsor[] => {
8750
return sponsors.sort((a, b) => {
8851
const aAmount = getSponsorAmount(a);
8952
const bAmount = getSponsorAmount(b);
90-
const aLifetime = calculateLifetimeContribution(a);
91-
const bLifetime = calculateLifetimeContribution(b);
92-
const aIsPast = a.monthlyDollars === -1;
93-
const bIsPast = b.monthlyDollars === -1;
9453
const aIsSpecial = isSpecialSponsor(a);
9554
const bIsSpecial = isSpecialSponsor(b);
96-
const aIsLifetimeSpecial = isLifetimeSpecialSponsor(a);
97-
const bIsLifetimeSpecial = isLifetimeSpecialSponsor(b);
9855

99-
// 1. Special sponsors (>=$100 current) come first
56+
// 1. Special sponsors (>=$100) come first
10057
if (aIsSpecial && !bIsSpecial) return -1;
10158
if (!aIsSpecial && bIsSpecial) return 1;
10259
if (aIsSpecial && bIsSpecial) {
10360
if (aAmount !== bAmount) {
10461
return bAmount - aAmount;
10562
}
106-
// If amounts equal, prefer monthly over one-time
107-
if (a.isOneTime && !b.isOneTime) return 1;
108-
if (!a.isOneTime && b.isOneTime) return -1;
109-
// Then by creation date (oldest first)
110-
return new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime();
111-
}
112-
113-
// 2. Lifetime special sponsors (>=$100 total) come next
114-
if (aIsLifetimeSpecial && !bIsLifetimeSpecial) return -1;
115-
if (!aIsLifetimeSpecial && bIsLifetimeSpecial) return 1;
116-
if (aIsLifetimeSpecial && bIsLifetimeSpecial) {
117-
if (aLifetime !== bLifetime) {
118-
return bLifetime - aLifetime;
119-
}
120-
// If lifetime amounts equal, prefer monthly over one-time
121-
if (a.isOneTime && !b.isOneTime) return 1;
122-
if (!a.isOneTime && b.isOneTime) return -1;
123-
// Then by creation date (oldest first)
124-
return new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime();
63+
// If amounts equal, sort by name
64+
return a.name.localeCompare(b.name);
12565
}
12666

127-
// 3. Current sponsors come before past sponsors
128-
if (!aIsPast && bIsPast) return -1;
129-
if (aIsPast && !bIsPast) return 1;
130-
131-
// 4. For current sponsors, sort by lifetime contribution (highest first)
132-
if (!aIsPast && !bIsPast) {
133-
if (aLifetime !== bLifetime) {
134-
return bLifetime - aLifetime;
135-
}
136-
// If lifetime amounts equal, prefer monthly over one-time
137-
if (a.isOneTime && !b.isOneTime) return 1;
138-
if (!a.isOneTime && b.isOneTime) return -1;
139-
// Then by creation date (oldest first)
140-
return new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime();
67+
// 2. Regular sponsors sorted by amount (highest first)
68+
if (aAmount !== bAmount) {
69+
return bAmount - aAmount;
14170
}
14271

143-
// 5. For past sponsors, sort by lifetime contribution (highest first)
144-
if (aIsPast && bIsPast) {
145-
if (aLifetime !== bLifetime) {
146-
return bLifetime - aLifetime;
147-
}
148-
// Then by creation date (newest first)
149-
return new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime();
150-
}
151-
152-
return 0;
72+
// 3. If amounts equal, sort by name
73+
return a.name.localeCompare(b.name);
15374
});
15475
};
15576

@@ -158,33 +79,24 @@ export const sortSpecialSponsors = (sponsors: Sponsor[]): Sponsor[] => {
15879
const aLifetime = calculateLifetimeContribution(a);
15980
const bLifetime = calculateLifetimeContribution(b);
16081

161-
// First, prioritize current special sponsors
162-
const aIsSpecial = isSpecialSponsor(a);
163-
const bIsSpecial = isSpecialSponsor(b);
164-
165-
if (aIsSpecial && !bIsSpecial) return -1;
166-
if (!aIsSpecial && bIsSpecial) return 1;
167-
168-
// Then sort by lifetime contribution (highest first)
82+
// Sort by lifetime contribution (highest first)
16983
if (aLifetime !== bLifetime) {
17084
return bLifetime - aLifetime;
17185
}
17286

173-
// If lifetime amounts equal, prefer monthly over one-time
174-
if (a.isOneTime && !b.isOneTime) return 1;
175-
if (!a.isOneTime && b.isOneTime) return -1;
176-
177-
// Then by creation date (oldest first)
178-
return new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime();
87+
// If amounts equal, sort by name
88+
return a.name.localeCompare(b.name);
17989
});
18090
};
18191

18292
export const filterCurrentSponsors = (sponsors: Sponsor[]): Sponsor[] => {
183-
return sponsors.filter((sponsor) => sponsor.monthlyDollars !== -1);
93+
// In the new structure, all sponsors in the main arrays are current
94+
return sponsors;
18495
};
18596

186-
export const filterPastSponsors = (sponsors: Sponsor[]): Sponsor[] => {
187-
return sponsors.filter((sponsor) => sponsor.monthlyDollars === -1);
97+
export const filterPastSponsors = (_sponsors: Sponsor[]): Sponsor[] => {
98+
// Past sponsors are handled separately in the new structure
99+
return [];
188100
};
189101

190102
export const filterSpecialSponsors = (sponsors: Sponsor[]): Sponsor[] => {
@@ -196,11 +108,7 @@ export const filterRegularSponsors = (sponsors: Sponsor[]): Sponsor[] => {
196108
};
197109

198110
export const getSponsorUrl = (sponsor: Sponsor): string => {
199-
return (
200-
sponsor.sponsor.websiteUrl ||
201-
sponsor.sponsor.linkUrl ||
202-
`https://github.com/${sponsor.sponsor.login}`
203-
);
111+
return sponsor.websiteUrl || sponsor.githubUrl;
204112
};
205113

206114
export const formatSponsorUrl = (url: string): string => {

apps/web/src/lib/sponsors.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import type { SponsorsData } from "./types";
2+
3+
const SPONSORS_URL = "https://sponsors.better-t-stack.dev/sponsors.json";
4+
5+
export async function fetchSponsors(): Promise<SponsorsData> {
6+
try {
7+
const response = await fetch(SPONSORS_URL, {
8+
next: { revalidate: 3600 },
9+
});
10+
11+
if (!response.ok) {
12+
throw new Error(`Failed to fetch sponsors: ${response.status}`);
13+
}
14+
15+
const data = await response.json();
16+
return data as SponsorsData;
17+
} catch (error) {
18+
console.error("Error fetching sponsors:", error);
19+
return {
20+
generated_at: new Date().toISOString(),
21+
summary: {
22+
total_sponsors: 0,
23+
total_lifetime_amount: 0,
24+
total_current_monthly: 0,
25+
special_sponsors: 0,
26+
current_sponsors: 0,
27+
past_sponsors: 0,
28+
top_sponsor: { name: "", amount: 0 },
29+
},
30+
specialSponsors: [],
31+
sponsors: [],
32+
pastSponsors: [],
33+
};
34+
}
35+
}

0 commit comments

Comments
 (0)