Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,6 @@ NEXT_PUBLIC_SITE_URL=your-site-url
CLARITY_API_TOKEN=your-clarity-api-token
NEXT_PUBLIC_CLARITY_PROJECT_ID=your-clarity-project-id

GITHUB_CONTRIBUTIONS_API_URL=your-github-contributions-api-url
GITHUB_CONTRIBUTIONS_API_URL=your-github-contributions-api-url
GITHUB_TOKEN=your-github-token
GITHUB_TOKEN=your-github-personal-access-token
78 changes: 78 additions & 0 deletions app/(app)/sponsors/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import Image from "next/image";
import Link from "next/link";

import { Section } from "@/components/ui/section";
import { Title } from "@/components/ui/title";
import { LINK } from "@/constants/links";
import { ROUTES } from "@/constants/routes";
import { getSponsors } from "@/lib/github/sponsors";
import { BreadcrumbJsonLd, sponsorsBreadcrumbs } from "@/seo/json-ld";
import { createMetadata } from "@/seo/metadata";

const DESCRIPTION = "Support my work and open-source projects.";

export const metadata = createMetadata({
canonical: ROUTES.SPONSORS,
description: DESCRIPTION,
title: "Sponsors",
});

const SponsorsPage = async () => {
const sponsors = await getSponsors();

return (
<>
<BreadcrumbJsonLd items={sponsorsBreadcrumbs()} />
<header className="animate-slide-in space-y-2 px-4 py-6">
<Title className="text-xl font-medium italic">{"sponsors."}</Title>
<p className="text-muted-foreground text-sm">{DESCRIPTION}</p>
</header>
<Section className="delay-100 flex flex-col gap-4 py-2">
{sponsors.length > 0 ? (
<div className="flex flex-col gap-3">
{sponsors.map((sponsor) => (
<Link
key={sponsor.login}
href={sponsor.websiteUrl ?? sponsor.url}
target="_blank"
rel="noopener"
className="group flex items-center gap-3 rounded-lg border p-3 transition-colors hover:bg-muted/50"
>
<Image
src={sponsor.avatarUrl}
alt={sponsor.name}
width={40}
height={40}
className="rounded-full"
/>
<div className="flex flex-col">
<span className="text-sm font-semibold group-hover:text-primary transition-colors">
{sponsor.name}
</span>
<span className="text-muted-foreground text-xs">
@{sponsor.login}
</span>
</div>
</Link>
))}
</div>
) : (
<p className="text-muted-foreground text-sm">
No sponsors yet.{" "}
<Link
href={LINK.SPONSOR}
target="_blank"
rel="noopener"
className="text-primary underline underline-offset-4"
>
Become the first
</Link>
.
</p>
)}
</Section>
</>
);
};

export default SponsorsPage;
12 changes: 9 additions & 3 deletions app/sitemap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,10 +61,16 @@ export default function sitemap(): MetadataRoute.Sitemap {
url: absoluteUrl(ROUTES.USES),
},
{
changeFrequency: "weekly",
changeFrequency: "monthly",
lastModified: new Date(),
priority: 0.9,
url: absoluteUrl(ROUTES.STATS),
priority: 0.8,
url: absoluteUrl(ROUTES.TESTIMONIALS),
},
{
changeFrequency: "monthly",
lastModified: new Date(),
priority: 0.7,
url: absoluteUrl(ROUTES.SPONSORS),
},
...projectEntries,
...craftEntries,
Expand Down
1 change: 1 addition & 0 deletions env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export const env = createEnv({
server: {
CLARITY_API_TOKEN: z.string().optional(),
GITHUB_CONTRIBUTIONS_API_URL: z.url().optional(),
GITHUB_TOKEN: z.string().optional(),
},
shared: {
NODE_ENV: z.enum(["development", "production", "test"]),
Expand Down
112 changes: 112 additions & 0 deletions lib/github/sponsors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import "server-only";
import { unstable_cache } from "next/cache";

import { GITHUB } from "@/constants/links";
import { env } from "@/env";

interface GitHubSponsor {
name: string;
login: string;
avatarUrl: string;
url: string;
websiteUrl: string | null;
}

interface SponsorsResponse {
data: {
user: {
sponsorsListing: {
sponsors: {
nodes: (
| {
__typename: "User";
login: string;
name: string | null;
avatarUrl: string;
url: string;
websiteUrl: string | null;
}
| {
__typename: "Organization";
login: string;
name: string | null;
avatarUrl: string;
url: string;
websiteUrl: string | null;
}
)[];
};
};
};
};
}

const SPONSORS_QUERY = `
query GetSponsors($login: String!) {
user(login: $login) {
sponsorsListing {
sponsors(first: 100) {
nodes {
... on User {
__typename
login
name
avatarUrl
url
websiteUrl
}
... on Organization {
__typename
login
name
avatarUrl
url
websiteUrl
}
}
}
}
}
}
`;

const fetchSponsors = async (): Promise<GitHubSponsor[]> => {
if (!env.GITHUB_TOKEN) {
return [];
}

try {
const response = await fetch("https://api.github.com/graphql", {
body: JSON.stringify({
query: SPONSORS_QUERY,
variables: { login: GITHUB.user },
}),
headers: {
Authorization: `bearer ${env.GITHUB_TOKEN}`,
"Content-Type": "application/json",
},
method: "POST",
});

if (!response.ok) {
return [];
}

const json = (await response.json()) as SponsorsResponse;
const nodes = json.data?.user?.sponsorsListing?.sponsors?.nodes ?? [];

return nodes.map((node) => ({
avatarUrl: node.avatarUrl,
login: node.login,
name: node.name ?? node.login,
url: node.url,
websiteUrl: node.websiteUrl,
}));
} catch {
return [];
}
};

export const getSponsors = unstable_cache(fetchSponsors, ["github-sponsors"], {
revalidate: 86_400,
});
Loading