mirror of
https://github.com/FranP-code/create-better-t-stack.git
synced 2025-10-12 23:52:15 +00:00
fix(web): add total amount in sponsor card
This commit is contained in:
@@ -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";
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user