fix(web): add total amount in sponsor card

This commit is contained in:
Aman Varshney
2025-08-23 15:45:44 +05:30
parent 4d5bf0a06b
commit bd1b2f4f72
3 changed files with 138 additions and 32 deletions

View File

@@ -1,8 +1,8 @@
import path from "node:path"; import path from "node:path";
import { import {
autocompleteMultiselect,
isCancel, isCancel,
log, log,
autocompleteMultiselect,
spinner, spinner,
} from "@clack/prompts"; } from "@clack/prompts";
import { execa } from "execa"; import { execa } from "execa";

View File

@@ -12,12 +12,15 @@ import {
import Image from "next/image"; import Image from "next/image";
import { useState } from "react"; import { useState } from "react";
import { import {
calculateLifetimeContribution,
filterCurrentSponsors, filterCurrentSponsors,
filterPastSponsors, filterPastSponsors,
filterSpecialSponsors, filterSpecialSponsors,
filterVisibleSponsors,
formatSponsorUrl, formatSponsorUrl,
getSponsorUrl, getSponsorUrl,
isSpecialSponsor, isSpecialSponsor,
shouldShowLifetimeTotal,
sortSpecialSponsors, sortSpecialSponsors,
sortSponsors, sortSponsors,
} from "@/lib/sponsor-utils"; } from "@/lib/sponsor-utils";
@@ -100,7 +103,8 @@ export default function SponsorsSection() {
}, },
})) || []; })) || [];
const sortedSponsors = sortSponsors(sponsors); const visibleSponsors = filterVisibleSponsors(sponsors);
const sortedSponsors = sortSponsors(visibleSponsors);
const currentSponsors = filterCurrentSponsors(sortedSponsors); const currentSponsors = filterCurrentSponsors(sortedSponsors);
const pastSponsors = filterPastSponsors(sortedSponsors); const pastSponsors = filterPastSponsors(sortedSponsors);
const specialSponsors = sortSpecialSponsors( const specialSponsors = sortSpecialSponsors(
@@ -119,11 +123,11 @@ export default function SponsorsSection() {
<div className="hidden h-px flex-1 bg-border sm:block" /> <div className="hidden h-px flex-1 bg-border sm:block" />
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span className="text-muted-foreground text-xs"> <span className="text-muted-foreground text-xs">
[{sponsors.length} RECORDS] [{visibleSponsors.length} RECORDS]
</span> </span>
</div> </div>
</div> </div>
{sponsors.length === 0 ? ( {visibleSponsors.length === 0 ? (
<div className="space-y-4"> <div className="space-y-4">
<div className="rounded border border-border p-8"> <div className="rounded border border-border p-8">
<div className="text-center"> <div className="text-center">
@@ -205,15 +209,20 @@ export default function SponsorsSection() {
{entry.tierName} {entry.tierName}
</p> </p>
)} )}
{shouldShowLifetimeTotal(entry) && (
<p className="text-muted-foreground text-xs">
Total: ${calculateLifetimeContribution(entry)}
</p>
)}
</div> </div>
<div className="flex flex-col gap-1"> <div className="flex flex-col">
<a <a
href={`https://github.com/${entry.sponsor.login}`} href={`https://github.com/${entry.sponsor.login}`}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="group flex items-center gap-2 text-muted-foreground text-xs transition-colors hover:text-primary" className="group flex items-center gap-2 text-muted-foreground text-xs transition-colors hover:text-primary"
> >
<Github className="h-4 w-4" /> <Github className="size-3" />
<span className="truncate"> <span className="truncate">
{entry.sponsor.login} {entry.sponsor.login}
</span> </span>
@@ -226,7 +235,7 @@ export default function SponsorsSection() {
rel="noopener noreferrer" rel="noopener noreferrer"
className="group flex items-center gap-2 text-muted-foreground text-xs transition-colors hover:text-primary" className="group flex items-center gap-2 text-muted-foreground text-xs transition-colors hover:text-primary"
> >
<Globe className="h-4 w-4" /> <Globe className="size-3" />
<span className="truncate"> <span className="truncate">
{formatSponsorUrl(sponsorUrl)} {formatSponsorUrl(sponsorUrl)}
</span> </span>
@@ -289,15 +298,21 @@ export default function SponsorsSection() {
{entry.tierName} {entry.tierName}
</p> </p>
)} )}
{shouldShowLifetimeTotal(entry) && (
<p className="text-muted-foreground text-xs">
Total: $
{calculateLifetimeContribution(entry)}
</p>
)}
</div> </div>
<div className="flex flex-col gap-1"> <div className="flex flex-col">
<a <a
href={`https://github.com/${entry.sponsor.login}`} href={`https://github.com/${entry.sponsor.login}`}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="group flex items-center gap-2 text-muted-foreground text-xs transition-colors hover:text-primary" className="group flex items-center gap-2 text-muted-foreground text-xs transition-colors hover:text-primary"
> >
<Github className="h-4 w-4" /> <Github className="size-3" />
<span className="truncate"> <span className="truncate">
{entry.sponsor.login} {entry.sponsor.login}
</span> </span>
@@ -313,7 +328,7 @@ export default function SponsorsSection() {
rel="noopener noreferrer" rel="noopener noreferrer"
className="group flex items-center gap-2 text-muted-foreground text-xs transition-colors hover:text-primary" className="group flex items-center gap-2 text-muted-foreground text-xs transition-colors hover:text-primary"
> >
<Globe className="h-4 w-4" /> <Globe className="size-3" />
<span className="truncate"> <span className="truncate">
{formatSponsorUrl( {formatSponsorUrl(
entry.sponsor.websiteUrl || entry.sponsor.websiteUrl ||
@@ -414,15 +429,21 @@ export default function SponsorsSection() {
{entry.tierName} {entry.tierName}
</p> </p>
)} )}
{!entry.isOneTime && (
<p className="text-muted-foreground/50 text-xs">
Total: $
{calculateLifetimeContribution(entry)}
</p>
)}
</div> </div>
<div className="flex flex-col gap-1"> <div className="flex flex-col">
<a <a
href={`https://github.com/${entry.sponsor.login}`} href={`https://github.com/${entry.sponsor.login}`}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="group flex items-center gap-2 text-muted-foreground/70 text-xs transition-colors hover:text-muted-foreground" 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" /> <Github className="size-3" />
<span className="truncate"> <span className="truncate">
{entry.sponsor.login} {entry.sponsor.login}
</span> </span>
@@ -435,7 +456,7 @@ export default function SponsorsSection() {
rel="noopener noreferrer" rel="noopener noreferrer"
className="group flex items-center gap-2 text-muted-foreground/70 text-xs transition-colors hover:text-muted-foreground" 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" /> <Globe className="size-3" />
<span className="truncate"> <span className="truncate">
{formatSponsorUrl(sponsorUrl)} {formatSponsorUrl(sponsorUrl)}
</span> </span>

View File

@@ -18,21 +18,85 @@ export const getSponsorAmount = (sponsor: Sponsor): number => {
return sponsor.monthlyDollars; return sponsor.monthlyDollars;
}; };
export const calculateLifetimeContribution = (sponsor: Sponsor): number => {
// For past sponsors, return 0
if (sponsor.monthlyDollars === -1) {
return 0;
}
// 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;
};
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;
};
export const filterVisibleSponsors = (sponsors: Sponsor[]): Sponsor[] => {
return sponsors.filter((sponsor) => {
const amount = getSponsorAmount(sponsor);
return amount >= 5;
});
};
export const isSpecialSponsor = (sponsor: Sponsor): boolean => { export const isSpecialSponsor = (sponsor: Sponsor): boolean => {
const amount = getSponsorAmount(sponsor); const amount = getSponsorAmount(sponsor);
return amount >= SPECIAL_SPONSOR_THRESHOLD; return amount >= SPECIAL_SPONSOR_THRESHOLD;
}; };
export const isLifetimeSpecialSponsor = (sponsor: Sponsor): boolean => {
const lifetimeAmount = calculateLifetimeContribution(sponsor);
return lifetimeAmount >= SPECIAL_SPONSOR_THRESHOLD;
};
export const sortSponsors = (sponsors: Sponsor[]): Sponsor[] => { export const sortSponsors = (sponsors: Sponsor[]): Sponsor[] => {
return sponsors.sort((a, b) => { return sponsors.sort((a, b) => {
const aAmount = getSponsorAmount(a); const aAmount = getSponsorAmount(a);
const bAmount = getSponsorAmount(b); const bAmount = getSponsorAmount(b);
const aLifetime = calculateLifetimeContribution(a);
const bLifetime = calculateLifetimeContribution(b);
const aIsPast = a.monthlyDollars === -1; const aIsPast = a.monthlyDollars === -1;
const bIsPast = b.monthlyDollars === -1; const bIsPast = b.monthlyDollars === -1;
const aIsSpecial = isSpecialSponsor(a); const aIsSpecial = isSpecialSponsor(a);
const bIsSpecial = isSpecialSponsor(b); const bIsSpecial = isSpecialSponsor(b);
const aIsLifetimeSpecial = isLifetimeSpecialSponsor(a);
const bIsLifetimeSpecial = isLifetimeSpecialSponsor(b);
// 1. Special sponsors (>=$100) come first, sorted by amount (highest first) // 1. Special sponsors (>=$100 current) come first
if (aIsSpecial && !bIsSpecial) return -1; if (aIsSpecial && !bIsSpecial) return -1;
if (!aIsSpecial && bIsSpecial) return 1; if (!aIsSpecial && bIsSpecial) return 1;
if (aIsSpecial && bIsSpecial) { if (aIsSpecial && bIsSpecial) {
@@ -46,26 +110,40 @@ export const sortSponsors = (sponsors: Sponsor[]): Sponsor[] => {
return new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime(); return new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime();
} }
// 2. Current sponsors come before past sponsors // 2. Lifetime special sponsors (>=$100 total) come next
if (!aIsPast && bIsPast) return -1; if (aIsLifetimeSpecial && !bIsLifetimeSpecial) return -1;
if (aIsPast && !bIsPast) return 1; if (!aIsLifetimeSpecial && bIsLifetimeSpecial) return 1;
if (aIsLifetimeSpecial && bIsLifetimeSpecial) {
// 3. For current sponsors, sort by amount (highest first) if (aLifetime !== bLifetime) {
if (!aIsPast && !bIsPast) { return bLifetime - aLifetime;
if (aAmount !== bAmount) {
return bAmount - aAmount;
} }
// If amounts equal, prefer monthly over one-time // If lifetime amounts equal, prefer monthly over one-time
if (a.isOneTime && !b.isOneTime) return 1; if (a.isOneTime && !b.isOneTime) return 1;
if (!a.isOneTime && b.isOneTime) return -1; if (!a.isOneTime && b.isOneTime) return -1;
// Then by creation date (oldest first) // Then by creation date (oldest first)
return new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime(); return new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime();
} }
// 4. For past sponsors, sort by amount (highest first) // 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 (aIsPast && bIsPast) {
if (aAmount !== bAmount) { if (aLifetime !== bLifetime) {
return bAmount - aAmount; return bLifetime - aLifetime;
} }
// Then by creation date (newest first) // Then by creation date (newest first)
return new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime(); return new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime();
@@ -77,15 +155,22 @@ export const sortSponsors = (sponsors: Sponsor[]): Sponsor[] => {
export const sortSpecialSponsors = (sponsors: Sponsor[]): Sponsor[] => { export const sortSpecialSponsors = (sponsors: Sponsor[]): Sponsor[] => {
return sponsors.sort((a, b) => { return sponsors.sort((a, b) => {
const aAmount = getSponsorAmount(a); const aLifetime = calculateLifetimeContribution(a);
const bAmount = getSponsorAmount(b); const bLifetime = calculateLifetimeContribution(b);
// Sort by actual amount (highest first) // First, prioritize current special sponsors
if (aAmount !== bAmount) { const aIsSpecial = isSpecialSponsor(a);
return bAmount - aAmount; const bIsSpecial = isSpecialSponsor(b);
if (aIsSpecial && !bIsSpecial) return -1;
if (!aIsSpecial && bIsSpecial) return 1;
// Then sort by lifetime contribution (highest first)
if (aLifetime !== bLifetime) {
return bLifetime - aLifetime;
} }
// If amounts equal, prefer monthly over one-time // If lifetime amounts equal, prefer monthly over one-time
if (a.isOneTime && !b.isOneTime) return 1; if (a.isOneTime && !b.isOneTime) return 1;
if (!a.isOneTime && b.isOneTime) return -1; if (!a.isOneTime && b.isOneTime) return -1;