Skip to content

Commit bd1b2f4

Browse files
fix(web): add total amount in sponsor card
1 parent 4d5bf0a commit bd1b2f4

File tree

3 files changed

+134
-28
lines changed

3 files changed

+134
-28
lines changed

apps/cli/src/helpers/addons/ruler-setup.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import path from "node:path";
22
import {
3+
autocompleteMultiselect,
34
isCancel,
45
log,
5-
autocompleteMultiselect,
66
spinner,
77
} from "@clack/prompts";
88
import { execa } from "execa";

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

Lines changed: 33 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,15 @@ import {
1212
import Image from "next/image";
1313
import { useState } from "react";
1414
import {
15+
calculateLifetimeContribution,
1516
filterCurrentSponsors,
1617
filterPastSponsors,
1718
filterSpecialSponsors,
19+
filterVisibleSponsors,
1820
formatSponsorUrl,
1921
getSponsorUrl,
2022
isSpecialSponsor,
23+
shouldShowLifetimeTotal,
2124
sortSpecialSponsors,
2225
sortSponsors,
2326
} from "@/lib/sponsor-utils";
@@ -100,7 +103,8 @@ export default function SponsorsSection() {
100103
},
101104
})) || [];
102105

103-
const sortedSponsors = sortSponsors(sponsors);
106+
const visibleSponsors = filterVisibleSponsors(sponsors);
107+
const sortedSponsors = sortSponsors(visibleSponsors);
104108
const currentSponsors = filterCurrentSponsors(sortedSponsors);
105109
const pastSponsors = filterPastSponsors(sortedSponsors);
106110
const specialSponsors = sortSpecialSponsors(
@@ -119,11 +123,11 @@ export default function SponsorsSection() {
119123
<div className="hidden h-px flex-1 bg-border sm:block" />
120124
<div className="flex items-center gap-2">
121125
<span className="text-muted-foreground text-xs">
122-
[{sponsors.length} RECORDS]
126+
[{visibleSponsors.length} RECORDS]
123127
</span>
124128
</div>
125129
</div>
126-
{sponsors.length === 0 ? (
130+
{visibleSponsors.length === 0 ? (
127131
<div className="space-y-4">
128132
<div className="rounded border border-border p-8">
129133
<div className="text-center">
@@ -205,15 +209,20 @@ export default function SponsorsSection() {
205209
{entry.tierName}
206210
</p>
207211
)}
212+
{shouldShowLifetimeTotal(entry) && (
213+
<p className="text-muted-foreground text-xs">
214+
Total: ${calculateLifetimeContribution(entry)}
215+
</p>
216+
)}
208217
</div>
209-
<div className="flex flex-col gap-1">
218+
<div className="flex flex-col">
210219
<a
211220
href={`https://github.com/${entry.sponsor.login}`}
212221
target="_blank"
213222
rel="noopener noreferrer"
214223
className="group flex items-center gap-2 text-muted-foreground text-xs transition-colors hover:text-primary"
215224
>
216-
<Github className="h-4 w-4" />
225+
<Github className="size-3" />
217226
<span className="truncate">
218227
{entry.sponsor.login}
219228
</span>
@@ -226,7 +235,7 @@ export default function SponsorsSection() {
226235
rel="noopener noreferrer"
227236
className="group flex items-center gap-2 text-muted-foreground text-xs transition-colors hover:text-primary"
228237
>
229-
<Globe className="h-4 w-4" />
238+
<Globe className="size-3" />
230239
<span className="truncate">
231240
{formatSponsorUrl(sponsorUrl)}
232241
</span>
@@ -289,15 +298,21 @@ export default function SponsorsSection() {
289298
{entry.tierName}
290299
</p>
291300
)}
301+
{shouldShowLifetimeTotal(entry) && (
302+
<p className="text-muted-foreground text-xs">
303+
Total: $
304+
{calculateLifetimeContribution(entry)}
305+
</p>
306+
)}
292307
</div>
293-
<div className="flex flex-col gap-1">
308+
<div className="flex flex-col">
294309
<a
295310
href={`https://github.com/${entry.sponsor.login}`}
296311
target="_blank"
297312
rel="noopener noreferrer"
298313
className="group flex items-center gap-2 text-muted-foreground text-xs transition-colors hover:text-primary"
299314
>
300-
<Github className="h-4 w-4" />
315+
<Github className="size-3" />
301316
<span className="truncate">
302317
{entry.sponsor.login}
303318
</span>
@@ -313,7 +328,7 @@ export default function SponsorsSection() {
313328
rel="noopener noreferrer"
314329
className="group flex items-center gap-2 text-muted-foreground text-xs transition-colors hover:text-primary"
315330
>
316-
<Globe className="h-4 w-4" />
331+
<Globe className="size-3" />
317332
<span className="truncate">
318333
{formatSponsorUrl(
319334
entry.sponsor.websiteUrl ||
@@ -414,15 +429,21 @@ export default function SponsorsSection() {
414429
{entry.tierName}
415430
</p>
416431
)}
432+
{!entry.isOneTime && (
433+
<p className="text-muted-foreground/50 text-xs">
434+
Total: $
435+
{calculateLifetimeContribution(entry)}
436+
</p>
437+
)}
417438
</div>
418-
<div className="flex flex-col gap-1">
439+
<div className="flex flex-col">
419440
<a
420441
href={`https://github.com/${entry.sponsor.login}`}
421442
target="_blank"
422443
rel="noopener noreferrer"
423444
className="group flex items-center gap-2 text-muted-foreground/70 text-xs transition-colors hover:text-muted-foreground"
424445
>
425-
<Github className="h-4 w-4" />
446+
<Github className="size-3" />
426447
<span className="truncate">
427448
{entry.sponsor.login}
428449
</span>
@@ -435,7 +456,7 @@ export default function SponsorsSection() {
435456
rel="noopener noreferrer"
436457
className="group flex items-center gap-2 text-muted-foreground/70 text-xs transition-colors hover:text-muted-foreground"
437458
>
438-
<Globe className="h-4 w-4" />
459+
<Globe className="size-3" />
439460
<span className="truncate">
440461
{formatSponsorUrl(sponsorUrl)}
441462
</span>

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

Lines changed: 100 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -18,21 +18,85 @@ export const getSponsorAmount = (sponsor: Sponsor): number => {
1818
return sponsor.monthlyDollars;
1919
};
2020

21+
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;
31+
}
32+
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;
45+
};
46+
47+
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;
67+
};
68+
69+
export const filterVisibleSponsors = (sponsors: Sponsor[]): Sponsor[] => {
70+
return sponsors.filter((sponsor) => {
71+
const amount = getSponsorAmount(sponsor);
72+
return amount >= 5;
73+
});
74+
};
75+
2176
export const isSpecialSponsor = (sponsor: Sponsor): boolean => {
2277
const amount = getSponsorAmount(sponsor);
2378
return amount >= SPECIAL_SPONSOR_THRESHOLD;
2479
};
2580

81+
export const isLifetimeSpecialSponsor = (sponsor: Sponsor): boolean => {
82+
const lifetimeAmount = calculateLifetimeContribution(sponsor);
83+
return lifetimeAmount >= SPECIAL_SPONSOR_THRESHOLD;
84+
};
85+
2686
export const sortSponsors = (sponsors: Sponsor[]): Sponsor[] => {
2787
return sponsors.sort((a, b) => {
2888
const aAmount = getSponsorAmount(a);
2989
const bAmount = getSponsorAmount(b);
90+
const aLifetime = calculateLifetimeContribution(a);
91+
const bLifetime = calculateLifetimeContribution(b);
3092
const aIsPast = a.monthlyDollars === -1;
3193
const bIsPast = b.monthlyDollars === -1;
3294
const aIsSpecial = isSpecialSponsor(a);
3395
const bIsSpecial = isSpecialSponsor(b);
96+
const aIsLifetimeSpecial = isLifetimeSpecialSponsor(a);
97+
const bIsLifetimeSpecial = isLifetimeSpecialSponsor(b);
3498

35-
// 1. Special sponsors (>=$100) come first, sorted by amount (highest first)
99+
// 1. Special sponsors (>=$100 current) come first
36100
if (aIsSpecial && !bIsSpecial) return -1;
37101
if (!aIsSpecial && bIsSpecial) return 1;
38102
if (aIsSpecial && bIsSpecial) {
@@ -46,26 +110,40 @@ export const sortSponsors = (sponsors: Sponsor[]): Sponsor[] => {
46110
return new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime();
47111
}
48112

49-
// 2. Current sponsors come before past sponsors
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();
125+
}
126+
127+
// 3. Current sponsors come before past sponsors
50128
if (!aIsPast && bIsPast) return -1;
51129
if (aIsPast && !bIsPast) return 1;
52130

53-
// 3. For current sponsors, sort by amount (highest first)
131+
// 4. For current sponsors, sort by lifetime contribution (highest first)
54132
if (!aIsPast && !bIsPast) {
55-
if (aAmount !== bAmount) {
56-
return bAmount - aAmount;
133+
if (aLifetime !== bLifetime) {
134+
return bLifetime - aLifetime;
57135
}
58-
// If amounts equal, prefer monthly over one-time
136+
// If lifetime amounts equal, prefer monthly over one-time
59137
if (a.isOneTime && !b.isOneTime) return 1;
60138
if (!a.isOneTime && b.isOneTime) return -1;
61139
// Then by creation date (oldest first)
62140
return new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime();
63141
}
64142

65-
// 4. For past sponsors, sort by amount (highest first)
143+
// 5. For past sponsors, sort by lifetime contribution (highest first)
66144
if (aIsPast && bIsPast) {
67-
if (aAmount !== bAmount) {
68-
return bAmount - aAmount;
145+
if (aLifetime !== bLifetime) {
146+
return bLifetime - aLifetime;
69147
}
70148
// Then by creation date (newest first)
71149
return new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime();
@@ -77,15 +155,22 @@ export const sortSponsors = (sponsors: Sponsor[]): Sponsor[] => {
77155

78156
export const sortSpecialSponsors = (sponsors: Sponsor[]): Sponsor[] => {
79157
return sponsors.sort((a, b) => {
80-
const aAmount = getSponsorAmount(a);
81-
const bAmount = getSponsorAmount(b);
158+
const aLifetime = calculateLifetimeContribution(a);
159+
const bLifetime = calculateLifetimeContribution(b);
160+
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;
82167

83-
// Sort by actual amount (highest first)
84-
if (aAmount !== bAmount) {
85-
return bAmount - aAmount;
168+
// Then sort by lifetime contribution (highest first)
169+
if (aLifetime !== bLifetime) {
170+
return bLifetime - aLifetime;
86171
}
87172

88-
// If amounts equal, prefer monthly over one-time
173+
// If lifetime amounts equal, prefer monthly over one-time
89174
if (a.isOneTime && !b.isOneTime) return 1;
90175
if (!a.isOneTime && b.isOneTime) return -1;
91176

0 commit comments

Comments
 (0)