mirror of
https://github.com/FranP-code/create-better-t-stack.git
synced 2025-10-12 23:52:15 +00:00
feat(web): update sponsors logic
This commit is contained in:
@@ -11,60 +11,29 @@ import {
|
|||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import {
|
import {
|
||||||
calculateLifetimeContribution,
|
|
||||||
filterCurrentSponsors,
|
|
||||||
filterPastSponsors,
|
|
||||||
filterSpecialSponsors,
|
|
||||||
filterVisibleSponsors,
|
filterVisibleSponsors,
|
||||||
formatSponsorUrl,
|
formatSponsorUrl,
|
||||||
getSponsorUrl,
|
getSponsorUrl,
|
||||||
isSpecialSponsor,
|
isSpecialSponsor,
|
||||||
shouldShowLifetimeTotal,
|
shouldShowLifetimeTotal,
|
||||||
sortSpecialSponsors,
|
sortSpecialSponsors,
|
||||||
sortSponsors,
|
|
||||||
} from "@/lib/sponsor-utils";
|
} from "@/lib/sponsor-utils";
|
||||||
|
import type { SponsorsData } from "@/lib/types";
|
||||||
type SponsorEntry = {
|
|
||||||
sponsor: {
|
|
||||||
login: string;
|
|
||||||
name: string;
|
|
||||||
avatarUrl: string;
|
|
||||||
websiteUrl?: string;
|
|
||||||
linkUrl: string;
|
|
||||||
customLogoUrl?: string;
|
|
||||||
type: string;
|
|
||||||
};
|
|
||||||
isOneTime: boolean;
|
|
||||||
monthlyDollars: number;
|
|
||||||
privacyLevel: string;
|
|
||||||
tierName: string;
|
|
||||||
createdAt: string;
|
|
||||||
provider: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function SponsorsSection({
|
export default function SponsorsSection({
|
||||||
sponsors,
|
sponsorsData,
|
||||||
}: {
|
}: {
|
||||||
sponsors: Array<SponsorEntry>;
|
sponsorsData: SponsorsData;
|
||||||
}) {
|
}) {
|
||||||
const [showPastSponsors, setShowPastSponsors] = useState(false);
|
const [showPastSponsors, setShowPastSponsors] = useState(false);
|
||||||
|
|
||||||
const sponsorsData =
|
const allCurrentSponsors = [
|
||||||
sponsors.map((sponsor) => ({
|
...sponsorsData.specialSponsors,
|
||||||
...sponsor,
|
...sponsorsData.sponsors,
|
||||||
sponsor: {
|
];
|
||||||
...sponsor.sponsor,
|
const visibleSponsors = filterVisibleSponsors(allCurrentSponsors);
|
||||||
customLogoUrl: sponsor.sponsor.customLogoUrl || "",
|
const pastSponsors = sponsorsData.pastSponsors;
|
||||||
},
|
const specialSponsors = sortSpecialSponsors(sponsorsData.specialSponsors);
|
||||||
})) || [];
|
|
||||||
|
|
||||||
const visibleSponsors = filterVisibleSponsors(sponsorsData);
|
|
||||||
const sortedSponsors = sortSponsors(visibleSponsors);
|
|
||||||
const currentSponsors = filterCurrentSponsors(sortedSponsors);
|
|
||||||
const pastSponsors = filterPastSponsors(sortSponsors(sponsorsData));
|
|
||||||
const specialSponsors = sortSpecialSponsors(
|
|
||||||
filterSpecialSponsors(currentSponsors),
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="">
|
<div className="">
|
||||||
@@ -117,15 +86,11 @@ export default function SponsorsSection({
|
|||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
|
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
|
||||||
{specialSponsors.map((entry, index) => {
|
{specialSponsors.map((entry, index) => {
|
||||||
const since = new Date(entry.createdAt).toLocaleDateString(
|
|
||||||
undefined,
|
|
||||||
{ year: "numeric", month: "short" },
|
|
||||||
);
|
|
||||||
const sponsorUrl = getSponsorUrl(entry);
|
const sponsorUrl = getSponsorUrl(entry);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={entry.sponsor.login}
|
key={entry.githubId}
|
||||||
className="rounded border border-border"
|
className="rounded border border-border"
|
||||||
style={{ animationDelay: `${index * 50}ms` }}
|
style={{ animationDelay: `${index * 50}ms` }}
|
||||||
>
|
>
|
||||||
@@ -135,7 +100,7 @@ export default function SponsorsSection({
|
|||||||
<div className="ml-auto flex items-center gap-2 text-muted-foreground text-xs">
|
<div className="ml-auto flex items-center gap-2 text-muted-foreground text-xs">
|
||||||
<span>SPECIAL</span>
|
<span>SPECIAL</span>
|
||||||
<span>•</span>
|
<span>•</span>
|
||||||
<span>SINCE {since.toUpperCase()}</span>
|
<span>{entry.sinceWhen.toUpperCase()}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -143,11 +108,8 @@ export default function SponsorsSection({
|
|||||||
<div className="flex gap-4">
|
<div className="flex gap-4">
|
||||||
<div className="flex-shrink-0">
|
<div className="flex-shrink-0">
|
||||||
<Image
|
<Image
|
||||||
src={
|
src={entry.avatarUrl}
|
||||||
entry.sponsor.customLogoUrl ||
|
alt={entry.name}
|
||||||
entry.sponsor.avatarUrl
|
|
||||||
}
|
|
||||||
alt={entry.sponsor.name || entry.sponsor.login}
|
|
||||||
width={100}
|
width={100}
|
||||||
height={100}
|
height={100}
|
||||||
className="rounded border border-border transition-colors duration-300"
|
className="rounded border border-border transition-colors duration-300"
|
||||||
@@ -157,33 +119,38 @@ export default function SponsorsSection({
|
|||||||
<div className="grid grid-cols-1 grid-rows-[1fr_auto] justify-between py-2">
|
<div className="grid grid-cols-1 grid-rows-[1fr_auto] justify-between py-2">
|
||||||
<div>
|
<div>
|
||||||
<h3 className="truncate font-semibold text-foreground text-sm">
|
<h3 className="truncate font-semibold text-foreground text-sm">
|
||||||
{entry.sponsor.name || entry.sponsor.login}
|
{entry.name}
|
||||||
</h3>
|
</h3>
|
||||||
{entry.tierName && (
|
{shouldShowLifetimeTotal(entry) ? (
|
||||||
|
<>
|
||||||
|
{entry.tierName && (
|
||||||
|
<p className="text-primary text-xs">
|
||||||
|
{entry.tierName}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
<p className="text-muted-foreground text-xs">
|
||||||
|
Total: {entry.formattedAmount}
|
||||||
|
</p>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
<p className="text-primary text-xs">
|
<p className="text-primary text-xs">
|
||||||
{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">
|
<div className="flex flex-col">
|
||||||
<a
|
<a
|
||||||
href={`https://github.com/${entry.sponsor.login}`}
|
href={entry.githubUrl}
|
||||||
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="size-3" />
|
<Github className="size-3" />
|
||||||
<span className="truncate">
|
<span className="truncate">
|
||||||
{entry.sponsor.login}
|
{entry.githubId}
|
||||||
</span>
|
</span>
|
||||||
</a>
|
</a>
|
||||||
{(entry.sponsor.websiteUrl ||
|
{entry.websiteUrl && (
|
||||||
entry.sponsor.linkUrl) && (
|
|
||||||
<a
|
<a
|
||||||
href={sponsorUrl}
|
href={sponsorUrl}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
@@ -207,98 +174,91 @@ export default function SponsorsSection({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{currentSponsors.filter((s) => !isSpecialSponsor(s)).length > 0 && (
|
{sponsorsData.sponsors.length > 0 && (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
|
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
|
||||||
{currentSponsors
|
{sponsorsData.sponsors.map((entry, index) => {
|
||||||
.filter((s) => !isSpecialSponsor(s))
|
const sponsorUrl = getSponsorUrl(entry);
|
||||||
.map((entry, index) => {
|
return (
|
||||||
const since = new Date(entry.createdAt).toLocaleDateString(
|
<div
|
||||||
undefined,
|
key={entry.githubId}
|
||||||
{ year: "numeric", month: "short" },
|
className="rounded border border-border"
|
||||||
);
|
style={{ animationDelay: `${index * 50}ms` }}
|
||||||
return (
|
>
|
||||||
<div
|
<div className="border-border border-b px-3 py-2">
|
||||||
key={entry.sponsor.login}
|
<div className="flex items-center gap-2">
|
||||||
className="rounded border border-border"
|
<span className="text-primary text-xs">▶</span>
|
||||||
style={{ animationDelay: `${index * 50}ms` }}
|
<div className="ml-auto flex items-center gap-2 text-muted-foreground text-xs">
|
||||||
>
|
<span>{entry.sinceWhen.toUpperCase()}</span>
|
||||||
<div className="border-border border-b px-3 py-2">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<span className="text-primary text-xs">▶</span>
|
|
||||||
<div className="ml-auto flex items-center gap-2 text-muted-foreground text-xs">
|
|
||||||
<span>SINCE {since.toUpperCase()}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="p-4">
|
</div>
|
||||||
<div className="flex gap-4">
|
<div className="p-4">
|
||||||
<div className="flex-shrink-0">
|
<div className="flex gap-4">
|
||||||
<Image
|
<div className="flex-shrink-0">
|
||||||
src={entry.sponsor.avatarUrl}
|
<Image
|
||||||
alt={entry.sponsor.name || entry.sponsor.login}
|
src={entry.avatarUrl}
|
||||||
width={100}
|
alt={entry.name}
|
||||||
height={100}
|
width={100}
|
||||||
className="rounded border border-border transition-colors duration-300"
|
height={100}
|
||||||
unoptimized
|
className="rounded border border-border transition-colors duration-300"
|
||||||
/>
|
unoptimized
|
||||||
</div>
|
/>
|
||||||
<div className="grid grid-cols-1 grid-rows-[1fr_auto] justify-between py-2">
|
</div>
|
||||||
<div>
|
<div className="grid grid-cols-1 grid-rows-[1fr_auto] justify-between py-2">
|
||||||
<h3 className="truncate font-semibold text-foreground text-sm">
|
<div>
|
||||||
{entry.sponsor.name || entry.sponsor.login}
|
<h3 className="truncate font-semibold text-foreground text-sm">
|
||||||
</h3>
|
{entry.name}
|
||||||
{entry.tierName && (
|
</h3>
|
||||||
<p className="text-primary text-xs">
|
{shouldShowLifetimeTotal(entry) ? (
|
||||||
{entry.tierName}
|
<>
|
||||||
</p>
|
{entry.tierName && (
|
||||||
)}
|
<p className="text-primary text-xs">
|
||||||
{shouldShowLifetimeTotal(entry) && (
|
{entry.tierName}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
<p className="text-muted-foreground text-xs">
|
<p className="text-muted-foreground text-xs">
|
||||||
Total: $
|
Total: {entry.formattedAmount}
|
||||||
{calculateLifetimeContribution(entry)}
|
|
||||||
</p>
|
</p>
|
||||||
)}
|
</>
|
||||||
</div>
|
) : (
|
||||||
<div className="flex flex-col">
|
<p className="text-primary text-xs">
|
||||||
|
{entry.tierName}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<a
|
||||||
|
href={entry.githubUrl}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="group flex items-center gap-2 text-muted-foreground text-xs transition-colors hover:text-primary"
|
||||||
|
>
|
||||||
|
<Github className="size-3" />
|
||||||
|
<span className="truncate">
|
||||||
|
{entry.githubId}
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
{entry.websiteUrl && (
|
||||||
<a
|
<a
|
||||||
href={`https://github.com/${entry.sponsor.login}`}
|
href={sponsorUrl}
|
||||||
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="size-3" />
|
<Globe className="size-3" />
|
||||||
<span className="truncate">
|
<span className="truncate">
|
||||||
{entry.sponsor.login}
|
{formatSponsorUrl(sponsorUrl)}
|
||||||
</span>
|
</span>
|
||||||
</a>
|
</a>
|
||||||
{(entry.sponsor.websiteUrl ||
|
)}
|
||||||
entry.sponsor.linkUrl) && (
|
|
||||||
<a
|
|
||||||
href={
|
|
||||||
entry.sponsor.websiteUrl ||
|
|
||||||
entry.sponsor.linkUrl
|
|
||||||
}
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
className="group flex items-center gap-2 text-muted-foreground text-xs transition-colors hover:text-primary"
|
|
||||||
>
|
|
||||||
<Globe className="size-3" />
|
|
||||||
<span className="truncate">
|
|
||||||
{formatSponsorUrl(
|
|
||||||
entry.sponsor.websiteUrl ||
|
|
||||||
entry.sponsor.linkUrl,
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
</a>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
</div>
|
||||||
})}
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -330,16 +290,12 @@ export default function SponsorsSection({
|
|||||||
{showPastSponsors && (
|
{showPastSponsors && (
|
||||||
<div className="slide-in-from-top-2 grid animate-in grid-cols-1 gap-4 duration-300 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
|
<div className="slide-in-from-top-2 grid animate-in grid-cols-1 gap-4 duration-300 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
|
||||||
{pastSponsors.map((entry, index) => {
|
{pastSponsors.map((entry, index) => {
|
||||||
const since = new Date(entry.createdAt).toLocaleDateString(
|
|
||||||
undefined,
|
|
||||||
{ year: "numeric", month: "short" },
|
|
||||||
);
|
|
||||||
const wasSpecial = isSpecialSponsor(entry);
|
const wasSpecial = isSpecialSponsor(entry);
|
||||||
const sponsorUrl = getSponsorUrl(entry);
|
const sponsorUrl = getSponsorUrl(entry);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={entry.sponsor.login}
|
key={entry.githubId}
|
||||||
className="rounded border border-border/70 bg-muted/20"
|
className="rounded border border-border/70 bg-muted/20"
|
||||||
style={{ animationDelay: `${index * 50}ms` }}
|
style={{ animationDelay: `${index * 50}ms` }}
|
||||||
>
|
>
|
||||||
@@ -355,7 +311,7 @@ export default function SponsorsSection({
|
|||||||
<div className="ml-auto flex items-center gap-2 text-muted-foreground text-xs">
|
<div className="ml-auto flex items-center gap-2 text-muted-foreground text-xs">
|
||||||
{wasSpecial && <span>SPECIAL</span>}
|
{wasSpecial && <span>SPECIAL</span>}
|
||||||
{wasSpecial && <span>•</span>}
|
{wasSpecial && <span>•</span>}
|
||||||
<span>SINCE {since.toUpperCase()}</span>
|
<span>{entry.sinceWhen.toUpperCase()}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -363,11 +319,8 @@ export default function SponsorsSection({
|
|||||||
<div className="flex gap-4">
|
<div className="flex gap-4">
|
||||||
<div className="flex-shrink-0">
|
<div className="flex-shrink-0">
|
||||||
<Image
|
<Image
|
||||||
src={
|
src={entry.avatarUrl}
|
||||||
entry.sponsor.customLogoUrl ||
|
alt={entry.name}
|
||||||
entry.sponsor.avatarUrl
|
|
||||||
}
|
|
||||||
alt={entry.sponsor.name || entry.sponsor.login}
|
|
||||||
width={80}
|
width={80}
|
||||||
height={80}
|
height={80}
|
||||||
className="rounded border border-border/70"
|
className="rounded border border-border/70"
|
||||||
@@ -377,34 +330,38 @@ export default function SponsorsSection({
|
|||||||
<div className="grid grid-cols-1 grid-rows-[1fr_auto] justify-between">
|
<div className="grid grid-cols-1 grid-rows-[1fr_auto] justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h3 className="truncate font-semibold text-muted-foreground text-sm">
|
<h3 className="truncate font-semibold text-muted-foreground text-sm">
|
||||||
{entry.sponsor.name || entry.sponsor.login}
|
{entry.name}
|
||||||
</h3>
|
</h3>
|
||||||
{entry.tierName && (
|
{shouldShowLifetimeTotal(entry) ? (
|
||||||
|
<>
|
||||||
|
{entry.tierName && (
|
||||||
|
<p className="text-muted-foreground/70 text-xs">
|
||||||
|
{entry.tierName}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
<p className="text-muted-foreground/50 text-xs">
|
||||||
|
Total: {entry.formattedAmount}
|
||||||
|
</p>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
<p className="text-muted-foreground/70 text-xs">
|
<p className="text-muted-foreground/70 text-xs">
|
||||||
{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">
|
<div className="flex flex-col">
|
||||||
<a
|
<a
|
||||||
href={`https://github.com/${entry.sponsor.login}`}
|
href={entry.githubUrl}
|
||||||
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="size-3" />
|
<Github className="size-3" />
|
||||||
<span className="truncate">
|
<span className="truncate">
|
||||||
{entry.sponsor.login}
|
{entry.githubId}
|
||||||
</span>
|
</span>
|
||||||
</a>
|
</a>
|
||||||
{(entry.sponsor.websiteUrl ||
|
{entry.websiteUrl && (
|
||||||
entry.sponsor.linkUrl) && (
|
|
||||||
<a
|
<a
|
||||||
href={sponsorUrl}
|
href={sponsorUrl}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ export const dynamic = "force-static";
|
|||||||
|
|
||||||
import { api } from "@better-t-stack/backend/convex/_generated/api";
|
import { api } from "@better-t-stack/backend/convex/_generated/api";
|
||||||
import { fetchQuery } from "convex/nextjs";
|
import { fetchQuery } from "convex/nextjs";
|
||||||
|
import { fetchSponsors } from "@/lib/sponsors";
|
||||||
import CommandSection from "./_components/command-section";
|
import CommandSection from "./_components/command-section";
|
||||||
import Footer from "./_components/footer";
|
import Footer from "./_components/footer";
|
||||||
import HeroSection from "./_components/hero-section";
|
import HeroSection from "./_components/hero-section";
|
||||||
@@ -10,7 +11,7 @@ import StatsSection from "./_components/stats-section";
|
|||||||
import Testimonials from "./_components/testimonials";
|
import Testimonials from "./_components/testimonials";
|
||||||
|
|
||||||
export default async function HomePage() {
|
export default async function HomePage() {
|
||||||
const sponsors = await fetchQuery(api.sponsors.getSponsors);
|
const sponsorsData = await fetchSponsors();
|
||||||
const fetchedTweets = await fetchQuery(api.testimonials.getTweets);
|
const fetchedTweets = await fetchQuery(api.testimonials.getTweets);
|
||||||
const fetchedVideos = await fetchQuery(api.testimonials.getVideos);
|
const fetchedVideos = await fetchQuery(api.testimonials.getVideos);
|
||||||
const videos = fetchedVideos.map((v) => ({
|
const videos = fetchedVideos.map((v) => ({
|
||||||
@@ -31,7 +32,7 @@ export default async function HomePage() {
|
|||||||
<HeroSection />
|
<HeroSection />
|
||||||
<CommandSection />
|
<CommandSection />
|
||||||
<StatsSection analyticsData={minimalAnalyticsData} />
|
<StatsSection analyticsData={minimalAnalyticsData} />
|
||||||
<SponsorsSection sponsors={sponsors} />
|
<SponsorsSection sponsorsData={sponsorsData} />
|
||||||
<Testimonials tweets={tweets} videos={videos} />
|
<Testimonials tweets={tweets} videos={videos} />
|
||||||
</div>
|
</div>
|
||||||
<Footer />
|
<Footer />
|
||||||
|
|||||||
@@ -8,30 +8,24 @@ import {
|
|||||||
HoverCardTrigger,
|
HoverCardTrigger,
|
||||||
} from "@/components/ui/hover-card";
|
} from "@/components/ui/hover-card";
|
||||||
import {
|
import {
|
||||||
filterCurrentSponsors,
|
|
||||||
filterSpecialSponsors,
|
|
||||||
formatSponsorUrl,
|
formatSponsorUrl,
|
||||||
getSponsorUrl,
|
getSponsorUrl,
|
||||||
sortSpecialSponsors,
|
sortSpecialSponsors,
|
||||||
} from "@/lib/sponsor-utils";
|
} from "@/lib/sponsor-utils";
|
||||||
import type { Sponsor } from "@/lib/types";
|
import type { Sponsor, SponsorsData } from "@/lib/types";
|
||||||
|
|
||||||
export function SpecialSponsorBanner() {
|
export function SpecialSponsorBanner() {
|
||||||
const [specialSponsors, setSpecialSponsors] = useState<Sponsor[]>([]);
|
const [specialSponsors, setSpecialSponsors] = useState<Sponsor[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetch("https://sponsors.amanv.dev/sponsors.json")
|
fetch("https://sponsors.better-t-stack.dev/sponsors.json")
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
if (!res.ok) throw new Error("Failed to fetch sponsors");
|
if (!res.ok) throw new Error("Failed to fetch sponsors");
|
||||||
return res.json();
|
return res.json();
|
||||||
})
|
})
|
||||||
.then((data) => {
|
.then((data: SponsorsData) => {
|
||||||
const sponsorsData = Array.isArray(data) ? data : [];
|
const specials = sortSpecialSponsors(data.specialSponsors);
|
||||||
const currentSponsors = filterCurrentSponsors(sponsorsData);
|
|
||||||
const specials = sortSpecialSponsors(
|
|
||||||
filterSpecialSponsors(currentSponsors),
|
|
||||||
);
|
|
||||||
setSpecialSponsors(specials);
|
setSpecialSponsors(specials);
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
})
|
})
|
||||||
@@ -63,30 +57,21 @@ export function SpecialSponsorBanner() {
|
|||||||
<div>
|
<div>
|
||||||
<div className="no-scrollbar grid grid-cols-4 items-center gap-2 overflow-x-auto whitespace-nowrap py-1">
|
<div className="no-scrollbar grid grid-cols-4 items-center gap-2 overflow-x-auto whitespace-nowrap py-1">
|
||||||
{specialSponsors.map((entry) => {
|
{specialSponsors.map((entry) => {
|
||||||
const displayName = entry.sponsor.name || entry.sponsor.login;
|
|
||||||
const imgSrc = entry.sponsor.customLogoUrl || entry.sponsor.avatarUrl;
|
|
||||||
const since = new Date(entry.createdAt).toLocaleDateString(
|
|
||||||
undefined,
|
|
||||||
{
|
|
||||||
year: "numeric",
|
|
||||||
month: "short",
|
|
||||||
},
|
|
||||||
);
|
|
||||||
const sponsorUrl = getSponsorUrl(entry);
|
const sponsorUrl = getSponsorUrl(entry);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<HoverCard key={entry.sponsor.login}>
|
<HoverCard key={entry.githubId}>
|
||||||
<HoverCardTrigger asChild>
|
<HoverCardTrigger asChild>
|
||||||
<a
|
<a
|
||||||
href={entry.sponsor.websiteUrl || sponsorUrl}
|
href={entry.websiteUrl || sponsorUrl}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
aria-label={displayName}
|
aria-label={entry.name}
|
||||||
className="inline-flex"
|
className="inline-flex"
|
||||||
>
|
>
|
||||||
<Image
|
<Image
|
||||||
src={imgSrc}
|
src={entry.avatarUrl}
|
||||||
alt={displayName}
|
alt={entry.name}
|
||||||
width={66}
|
width={66}
|
||||||
height={66}
|
height={66}
|
||||||
className="size-12 rounded border border-border"
|
className="size-12 rounded border border-border"
|
||||||
@@ -105,13 +90,13 @@ export function SpecialSponsorBanner() {
|
|||||||
<div className="ml-auto text-muted-foreground text-xs">
|
<div className="ml-auto text-muted-foreground text-xs">
|
||||||
<span>SPECIAL</span>
|
<span>SPECIAL</span>
|
||||||
<span className="px-1">•</span>
|
<span className="px-1">•</span>
|
||||||
<span>SINCE {since.toUpperCase()}</span>
|
<span>{entry.sinceWhen.toUpperCase()}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-3">
|
<div className="flex gap-3">
|
||||||
<Image
|
<Image
|
||||||
src={imgSrc}
|
src={entry.avatarUrl}
|
||||||
alt={displayName}
|
alt={entry.name}
|
||||||
width={80}
|
width={80}
|
||||||
height={80}
|
height={80}
|
||||||
className="rounded border border-border"
|
className="rounded border border-border"
|
||||||
@@ -120,7 +105,7 @@ export function SpecialSponsorBanner() {
|
|||||||
<div className="grid grid-cols-1 grid-rows-[1fr_auto]">
|
<div className="grid grid-cols-1 grid-rows-[1fr_auto]">
|
||||||
<div>
|
<div>
|
||||||
<h3 className="truncate font-semibold text-sm">
|
<h3 className="truncate font-semibold text-sm">
|
||||||
{displayName}
|
{entry.name}
|
||||||
</h3>
|
</h3>
|
||||||
{entry.tierName ? (
|
{entry.tierName ? (
|
||||||
<p className="text-primary text-xs">
|
<p className="text-primary text-xs">
|
||||||
@@ -130,17 +115,15 @@ export function SpecialSponsorBanner() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<a
|
<a
|
||||||
href={`https://github.com/${entry.sponsor.login}`}
|
href={entry.githubUrl}
|
||||||
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="h-4 w-4" />
|
||||||
<span className="truncate">
|
<span className="truncate">{entry.githubId}</span>
|
||||||
{entry.sponsor.login}
|
|
||||||
</span>
|
|
||||||
</a>
|
</a>
|
||||||
{entry.sponsor.websiteUrl || entry.sponsor.linkUrl ? (
|
{entry.websiteUrl ? (
|
||||||
<a
|
<a
|
||||||
href={sponsorUrl}
|
href={sponsorUrl}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
|
|||||||
@@ -3,67 +3,30 @@ import type { Sponsor } from "@/lib/types";
|
|||||||
export const SPECIAL_SPONSOR_THRESHOLD = 100;
|
export const SPECIAL_SPONSOR_THRESHOLD = 100;
|
||||||
|
|
||||||
export const getSponsorAmount = (sponsor: Sponsor): number => {
|
export const getSponsorAmount = (sponsor: Sponsor): number => {
|
||||||
// For past sponsors, return 0
|
// If totalProcessedAmount exists, use it, otherwise parse from tierName
|
||||||
if (sponsor.monthlyDollars === -1) {
|
if (sponsor.totalProcessedAmount !== undefined) {
|
||||||
return 0;
|
return sponsor.totalProcessedAmount;
|
||||||
}
|
}
|
||||||
|
|
||||||
// For one-time sponsors, parse the actual amount from tierName
|
// Parse amount from tierName as fallback
|
||||||
if (sponsor.isOneTime && sponsor.tierName) {
|
const match = sponsor.tierName.match(/\$(\d+(?:\.\d+)?)/);
|
||||||
const match = sponsor.tierName.match(/\$(\d+(?:\.\d+)?)/);
|
return match ? Number.parseFloat(match[1]) : 0;
|
||||||
return match ? Number.parseFloat(match[1]) : sponsor.monthlyDollars;
|
|
||||||
}
|
|
||||||
|
|
||||||
// For monthly sponsors, use monthlyDollars
|
|
||||||
return sponsor.monthlyDollars;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const calculateLifetimeContribution = (sponsor: Sponsor): number => {
|
export const calculateLifetimeContribution = (sponsor: Sponsor): number => {
|
||||||
// For past sponsors, return 0
|
// If totalProcessedAmount exists, use it, otherwise parse from tierName
|
||||||
if (sponsor.monthlyDollars === -1) {
|
if (sponsor.totalProcessedAmount !== undefined) {
|
||||||
return 0;
|
return sponsor.totalProcessedAmount;
|
||||||
}
|
}
|
||||||
|
|
||||||
// For one-time sponsors, return the one-time amount
|
// Parse amount from tierName as fallback
|
||||||
if (sponsor.isOneTime && sponsor.tierName) {
|
const match = sponsor.tierName.match(/\$(\d+(?:\.\d+)?)/);
|
||||||
const match = sponsor.tierName.match(/\$(\d+(?:\.\d+)?)/);
|
return match ? Number.parseFloat(match[1]) : 0;
|
||||||
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 => {
|
export const shouldShowLifetimeTotal = (sponsor: Sponsor): boolean => {
|
||||||
// Don't show for past sponsors
|
// Only show lifetime total if totalProcessedAmount exists
|
||||||
if (sponsor.monthlyDollars === -1) {
|
return sponsor.totalProcessedAmount !== undefined;
|
||||||
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[] => {
|
export const filterVisibleSponsors = (sponsors: Sponsor[]): Sponsor[] => {
|
||||||
@@ -87,69 +50,27 @@ 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 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 current) come first
|
// 1. Special sponsors (>=$100) 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) {
|
||||||
if (aAmount !== bAmount) {
|
if (aAmount !== bAmount) {
|
||||||
return bAmount - aAmount;
|
return bAmount - aAmount;
|
||||||
}
|
}
|
||||||
// If amounts equal, prefer monthly over one-time
|
// If amounts equal, sort by name
|
||||||
if (a.isOneTime && !b.isOneTime) return 1;
|
return a.name.localeCompare(b.name);
|
||||||
if (!a.isOneTime && b.isOneTime) return -1;
|
|
||||||
// Then by creation date (oldest first)
|
|
||||||
return new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Lifetime special sponsors (>=$100 total) come next
|
// 2. Regular sponsors sorted by amount (highest first)
|
||||||
if (aIsLifetimeSpecial && !bIsLifetimeSpecial) return -1;
|
if (aAmount !== bAmount) {
|
||||||
if (!aIsLifetimeSpecial && bIsLifetimeSpecial) return 1;
|
return bAmount - aAmount;
|
||||||
if (aIsLifetimeSpecial && bIsLifetimeSpecial) {
|
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Current sponsors come before past sponsors
|
// 3. If amounts equal, sort by name
|
||||||
if (!aIsPast && bIsPast) return -1;
|
return a.name.localeCompare(b.name);
|
||||||
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 (aLifetime !== bLifetime) {
|
|
||||||
return bLifetime - aLifetime;
|
|
||||||
}
|
|
||||||
// Then by creation date (newest first)
|
|
||||||
return new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime();
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -158,33 +79,24 @@ export const sortSpecialSponsors = (sponsors: Sponsor[]): Sponsor[] => {
|
|||||||
const aLifetime = calculateLifetimeContribution(a);
|
const aLifetime = calculateLifetimeContribution(a);
|
||||||
const bLifetime = calculateLifetimeContribution(b);
|
const bLifetime = calculateLifetimeContribution(b);
|
||||||
|
|
||||||
// First, prioritize current special sponsors
|
// Sort by lifetime contribution (highest first)
|
||||||
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) {
|
if (aLifetime !== bLifetime) {
|
||||||
return bLifetime - aLifetime;
|
return bLifetime - aLifetime;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If lifetime amounts equal, prefer monthly over one-time
|
// If amounts equal, sort by name
|
||||||
if (a.isOneTime && !b.isOneTime) return 1;
|
return a.name.localeCompare(b.name);
|
||||||
if (!a.isOneTime && b.isOneTime) return -1;
|
|
||||||
|
|
||||||
// Then by creation date (oldest first)
|
|
||||||
return new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime();
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const filterCurrentSponsors = (sponsors: Sponsor[]): Sponsor[] => {
|
export const filterCurrentSponsors = (sponsors: Sponsor[]): Sponsor[] => {
|
||||||
return sponsors.filter((sponsor) => sponsor.monthlyDollars !== -1);
|
// In the new structure, all sponsors in the main arrays are current
|
||||||
|
return sponsors;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const filterPastSponsors = (sponsors: Sponsor[]): Sponsor[] => {
|
export const filterPastSponsors = (_sponsors: Sponsor[]): Sponsor[] => {
|
||||||
return sponsors.filter((sponsor) => sponsor.monthlyDollars === -1);
|
// Past sponsors are handled separately in the new structure
|
||||||
|
return [];
|
||||||
};
|
};
|
||||||
|
|
||||||
export const filterSpecialSponsors = (sponsors: Sponsor[]): Sponsor[] => {
|
export const filterSpecialSponsors = (sponsors: Sponsor[]): Sponsor[] => {
|
||||||
@@ -196,11 +108,7 @@ export const filterRegularSponsors = (sponsors: Sponsor[]): Sponsor[] => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const getSponsorUrl = (sponsor: Sponsor): string => {
|
export const getSponsorUrl = (sponsor: Sponsor): string => {
|
||||||
return (
|
return sponsor.websiteUrl || sponsor.githubUrl;
|
||||||
sponsor.sponsor.websiteUrl ||
|
|
||||||
sponsor.sponsor.linkUrl ||
|
|
||||||
`https://github.com/${sponsor.sponsor.login}`
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const formatSponsorUrl = (url: string): string => {
|
export const formatSponsorUrl = (url: string): string => {
|
||||||
|
|||||||
35
apps/web/src/lib/sponsors.ts
Normal file
35
apps/web/src/lib/sponsors.ts
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import type { SponsorsData } from "./types";
|
||||||
|
|
||||||
|
const SPONSORS_URL = "https://sponsors.better-t-stack.dev/sponsors.json";
|
||||||
|
|
||||||
|
export async function fetchSponsors(): Promise<SponsorsData> {
|
||||||
|
try {
|
||||||
|
const response = await fetch(SPONSORS_URL, {
|
||||||
|
next: { revalidate: 3600 },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Failed to fetch sponsors: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
return data as SponsorsData;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching sponsors:", error);
|
||||||
|
return {
|
||||||
|
generated_at: new Date().toISOString(),
|
||||||
|
summary: {
|
||||||
|
total_sponsors: 0,
|
||||||
|
total_lifetime_amount: 0,
|
||||||
|
total_current_monthly: 0,
|
||||||
|
special_sponsors: 0,
|
||||||
|
current_sponsors: 0,
|
||||||
|
past_sponsors: 0,
|
||||||
|
top_sponsor: { name: "", amount: 0 },
|
||||||
|
},
|
||||||
|
specialSponsors: [],
|
||||||
|
sponsors: [],
|
||||||
|
pastSponsors: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -25,19 +25,33 @@ export interface TechEdge {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface Sponsor {
|
export interface Sponsor {
|
||||||
sponsor: {
|
name: string;
|
||||||
login: string;
|
githubId: string;
|
||||||
name: string;
|
avatarUrl: string;
|
||||||
avatarUrl: string;
|
websiteUrl?: string;
|
||||||
websiteUrl?: string;
|
githubUrl: string;
|
||||||
linkUrl: string;
|
|
||||||
customLogoUrl: string;
|
|
||||||
type: string;
|
|
||||||
};
|
|
||||||
isOneTime: boolean;
|
|
||||||
monthlyDollars: number;
|
|
||||||
privacyLevel: string;
|
|
||||||
tierName: string;
|
tierName: string;
|
||||||
createdAt: string;
|
totalProcessedAmount?: number;
|
||||||
provider: string;
|
sinceWhen: string;
|
||||||
|
transactionCount: number;
|
||||||
|
formattedAmount?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SponsorsData {
|
||||||
|
generated_at: string;
|
||||||
|
summary: {
|
||||||
|
total_sponsors: number;
|
||||||
|
total_lifetime_amount: number;
|
||||||
|
total_current_monthly: number;
|
||||||
|
special_sponsors: number;
|
||||||
|
current_sponsors: number;
|
||||||
|
past_sponsors: number;
|
||||||
|
top_sponsor: {
|
||||||
|
name: string;
|
||||||
|
amount: number;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
specialSponsors: Sponsor[];
|
||||||
|
sponsors: Sponsor[];
|
||||||
|
pastSponsors: Sponsor[];
|
||||||
}
|
}
|
||||||
|
|||||||
2
packages/backend/convex/_generated/api.d.ts
vendored
2
packages/backend/convex/_generated/api.d.ts
vendored
@@ -12,7 +12,6 @@ import type * as healthCheck from "../healthCheck.js";
|
|||||||
import type * as hooks from "../hooks.js";
|
import type * as hooks from "../hooks.js";
|
||||||
import type * as http from "../http.js";
|
import type * as http from "../http.js";
|
||||||
import type * as showcase from "../showcase.js";
|
import type * as showcase from "../showcase.js";
|
||||||
import type * as sponsors from "../sponsors.js";
|
|
||||||
import type * as stats from "../stats.js";
|
import type * as stats from "../stats.js";
|
||||||
import type * as testimonials from "../testimonials.js";
|
import type * as testimonials from "../testimonials.js";
|
||||||
|
|
||||||
@@ -35,7 +34,6 @@ declare const fullApi: ApiFromModules<{
|
|||||||
hooks: typeof hooks;
|
hooks: typeof hooks;
|
||||||
http: typeof http;
|
http: typeof http;
|
||||||
showcase: typeof showcase;
|
showcase: typeof showcase;
|
||||||
sponsors: typeof sponsors;
|
|
||||||
stats: typeof stats;
|
stats: typeof stats;
|
||||||
testimonials: typeof testimonials;
|
testimonials: typeof testimonials;
|
||||||
}>;
|
}>;
|
||||||
|
|||||||
@@ -2,24 +2,6 @@ import { defineSchema, defineTable } from "convex/server";
|
|||||||
import { v } from "convex/values";
|
import { v } from "convex/values";
|
||||||
|
|
||||||
export default defineSchema({
|
export default defineSchema({
|
||||||
sponsors: defineTable({
|
|
||||||
sponsor: v.object({
|
|
||||||
login: v.string(),
|
|
||||||
name: v.string(),
|
|
||||||
avatarUrl: v.string(),
|
|
||||||
websiteUrl: v.optional(v.string()),
|
|
||||||
linkUrl: v.string(),
|
|
||||||
customLogoUrl: v.optional(v.string()),
|
|
||||||
type: v.string(),
|
|
||||||
}),
|
|
||||||
isOneTime: v.boolean(),
|
|
||||||
monthlyDollars: v.number(),
|
|
||||||
privacyLevel: v.string(),
|
|
||||||
tierName: v.string(),
|
|
||||||
createdAt: v.string(),
|
|
||||||
provider: v.string(),
|
|
||||||
}),
|
|
||||||
|
|
||||||
videos: defineTable({
|
videos: defineTable({
|
||||||
embedId: v.string(),
|
embedId: v.string(),
|
||||||
title: v.string(),
|
title: v.string(),
|
||||||
|
|||||||
@@ -1,30 +0,0 @@
|
|||||||
import { v } from "convex/values";
|
|
||||||
import { query } from "./_generated/server";
|
|
||||||
|
|
||||||
export const getSponsors = query({
|
|
||||||
args: {},
|
|
||||||
returns: v.array(
|
|
||||||
v.object({
|
|
||||||
_id: v.id("sponsors"),
|
|
||||||
_creationTime: v.number(),
|
|
||||||
sponsor: v.object({
|
|
||||||
login: v.string(),
|
|
||||||
name: v.string(),
|
|
||||||
avatarUrl: v.string(),
|
|
||||||
websiteUrl: v.optional(v.string()),
|
|
||||||
linkUrl: v.string(),
|
|
||||||
customLogoUrl: v.optional(v.string()),
|
|
||||||
type: v.string(),
|
|
||||||
}),
|
|
||||||
isOneTime: v.boolean(),
|
|
||||||
monthlyDollars: v.number(),
|
|
||||||
privacyLevel: v.string(),
|
|
||||||
tierName: v.string(),
|
|
||||||
createdAt: v.string(),
|
|
||||||
provider: v.string(),
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
handler: async (ctx) => {
|
|
||||||
return await ctx.db.query("sponsors").collect();
|
|
||||||
},
|
|
||||||
});
|
|
||||||
0
test-conservative-calculation.js
Normal file
0
test-conservative-calculation.js
Normal file
0
test-lifetime-fix.js
Normal file
0
test-lifetime-fix.js
Normal file
0
test-sponsor-calculations.js
Normal file
0
test-sponsor-calculations.js
Normal file
Reference in New Issue
Block a user