diff --git a/apps/cli/src/helpers/addons/ruler-setup.ts b/apps/cli/src/helpers/addons/ruler-setup.ts index f63aab4..b92cf4d 100644 --- a/apps/cli/src/helpers/addons/ruler-setup.ts +++ b/apps/cli/src/helpers/addons/ruler-setup.ts @@ -1,8 +1,8 @@ import path from "node:path"; import { + autocompleteMultiselect, isCancel, log, - autocompleteMultiselect, spinner, } from "@clack/prompts"; import { execa } from "execa"; diff --git a/apps/web/src/app/(home)/_components/sponsors-section.tsx b/apps/web/src/app/(home)/_components/sponsors-section.tsx index 1ceca52..66552ad 100644 --- a/apps/web/src/app/(home)/_components/sponsors-section.tsx +++ b/apps/web/src/app/(home)/_components/sponsors-section.tsx @@ -12,12 +12,15 @@ 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"; @@ -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 pastSponsors = filterPastSponsors(sortedSponsors); const specialSponsors = sortSpecialSponsors( @@ -119,11 +123,11 @@ export default function SponsorsSection() {
- [{sponsors.length} RECORDS] + [{visibleSponsors.length} RECORDS]
- {sponsors.length === 0 ? ( + {visibleSponsors.length === 0 ? (
@@ -205,15 +209,20 @@ export default function SponsorsSection() { {entry.tierName}

)} + {shouldShowLifetimeTotal(entry) && ( +

+ Total: ${calculateLifetimeContribution(entry)} +

+ )}
-
+
- + {entry.sponsor.login} @@ -226,7 +235,7 @@ export default function SponsorsSection() { rel="noopener noreferrer" className="group flex items-center gap-2 text-muted-foreground text-xs transition-colors hover:text-primary" > - + {formatSponsorUrl(sponsorUrl)} @@ -289,15 +298,21 @@ export default function SponsorsSection() { {entry.tierName}

)} + {shouldShowLifetimeTotal(entry) && ( +

+ Total: $ + {calculateLifetimeContribution(entry)} +

+ )}
-
+
- + {entry.sponsor.login} @@ -313,7 +328,7 @@ export default function SponsorsSection() { rel="noopener noreferrer" className="group flex items-center gap-2 text-muted-foreground text-xs transition-colors hover:text-primary" > - + {formatSponsorUrl( entry.sponsor.websiteUrl || @@ -414,15 +429,21 @@ export default function SponsorsSection() { {entry.tierName}

)} + {!entry.isOneTime && ( +

+ Total: $ + {calculateLifetimeContribution(entry)} +

+ )}
-
+
- + {entry.sponsor.login} @@ -435,7 +456,7 @@ export default function SponsorsSection() { rel="noopener noreferrer" className="group flex items-center gap-2 text-muted-foreground/70 text-xs transition-colors hover:text-muted-foreground" > - + {formatSponsorUrl(sponsorUrl)} diff --git a/apps/web/src/lib/sponsor-utils.ts b/apps/web/src/lib/sponsor-utils.ts index 249217d..2fb56d2 100644 --- a/apps/web/src/lib/sponsor-utils.ts +++ b/apps/web/src/lib/sponsor-utils.ts @@ -18,21 +18,85 @@ export const getSponsorAmount = (sponsor: Sponsor): number => { 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 => { const amount = getSponsorAmount(sponsor); 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[] => { 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) 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) { @@ -46,26 +110,40 @@ export const sortSponsors = (sponsors: Sponsor[]): Sponsor[] => { return new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime(); } - // 2. Current sponsors come before past sponsors - if (!aIsPast && bIsPast) return -1; - if (aIsPast && !bIsPast) return 1; - - // 3. For current sponsors, sort by amount (highest first) - if (!aIsPast && !bIsPast) { - if (aAmount !== bAmount) { - return bAmount - aAmount; + // 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 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; // Then by creation date (oldest first) 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 (aAmount !== bAmount) { - return bAmount - aAmount; + if (aLifetime !== bLifetime) { + return bLifetime - aLifetime; } // Then by creation date (newest first) 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[] => { return sponsors.sort((a, b) => { - const aAmount = getSponsorAmount(a); - const bAmount = getSponsorAmount(b); + const aLifetime = calculateLifetimeContribution(a); + const bLifetime = calculateLifetimeContribution(b); - // Sort by actual amount (highest first) - if (aAmount !== bAmount) { - return bAmount - aAmount; + // 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) + 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;