diff --git a/apps/web/app/api/sponsors-webhook/route.ts b/apps/web/app/api/sponsors-webhook/route.ts new file mode 100644 index 00000000..9c29a1e7 --- /dev/null +++ b/apps/web/app/api/sponsors-webhook/route.ts @@ -0,0 +1,18 @@ +import { revalidatePath } from "next/cache" + +export async function POST(request: Request) { + try { + const text = await request.text() + console.log("[GitHub] Webhook received", text) + + revalidatePath("/test") + } catch (error: any) { + return new Response(`Webhook error: ${error.message}`, { + status: 400, + }) + } + + return new Response("Success!", { + status: 200, + }) +} diff --git a/apps/web/app/landing/sponsors.tsx b/apps/web/app/landing/sponsors.tsx index c63a072e..1747c074 100644 --- a/apps/web/app/landing/sponsors.tsx +++ b/apps/web/app/landing/sponsors.tsx @@ -8,6 +8,8 @@ import Image from "next/image" import Link from "next/link" import sponsorData from "./sponsors.json" import { Check, CheckCheck, GithubIcon, Heart, Star } from "lucide-react" +import { cn } from "@/lib/utils" +import { TimeAgo } from "@/components/time-ago" export function Pricing() { const current = 625 @@ -84,6 +86,65 @@ export function Pricing() { ) } +export async function LatestSponsor({ className }: { className?: string }) { + const GITHUB_TOKEN = process.env.GITHUB_TOKEN + if (!GITHUB_TOKEN) { + throw new Error("Missing process.env.GITHUB_TOKEN") + } + + const r = await fetch("https://api.github.com/graphql", { + method: "POST", + body: JSON.stringify({ query: latestSponsorsQuery }), + headers: { Authorization: "bearer " + GITHUB_TOKEN }, + }) + if (!r.ok) { + throw new Error(`Failed to fetch: ${r.status} ${r.statusText}`) + } + const { data, errors } = await r.json() + if (errors) { + throw new Error(JSON.stringify(errors)) + } + + const sponsors = data.organization.sponsorshipsAsMaintainer.edges + if (!sponsors.length) { + throw new Error("No sponsors found") + } + + const latest = sponsors[0].node + + return ( + + {latest.sponsorEntity.name} +
+ {/*
{new Date().toString()}
*/} +
+ Latest sponsor ยท +
+
+ {latest.sponsorEntity.name || latest.sponsorEntity.login} +
+
+ Sponsoring {latest.tier.name}{" "} +
+
+ {/*
{JSON.stringify(latest, null, 2)}
*/} +
+ ) +} + export function TopSponsors({ title = "Top Sponsors", scale = 1, @@ -440,3 +501,37 @@ function BrowserStack() { ) } + +const latestSponsorsQuery = `query { + organization(login: "code-hike") { + sponsorshipsAsMaintainer(first: 50, orderBy: {field: CREATED_AT, direction: DESC}, activeOnly: false) { + edges { + node { + createdAt + privacyLevel + tier { + name + monthlyPriceInDollars + } + sponsorEntity { + ... on User { + login + name + avatarUrl + websiteUrl + location + } + ... on Organization { + login + name + avatarUrl + websiteUrl + location + } + } + } + } + } + } +} +` diff --git a/apps/web/app/page.tsx b/apps/web/app/page.tsx index 3f700551..a43fb71b 100644 --- a/apps/web/app/page.tsx +++ b/apps/web/app/page.tsx @@ -1,6 +1,7 @@ import Link from "next/link" import { AllSponsors, + LatestSponsor, PoweredBy, Pricing, TopSponsors, @@ -38,7 +39,9 @@ export default function HomePage() { - +

Sponsors

+ + diff --git a/apps/web/app/test/page.tsx b/apps/web/app/test/page.tsx index ba5fc4a5..c0c3cecd 100644 --- a/apps/web/app/test/page.tsx +++ b/apps/web/app/test/page.tsx @@ -1,9 +1,12 @@ +import { LatestSponsor } from "../landing/sponsors" import Content from "./content.md" import { Code } from "@/components/code" export default function Page() { return (
+
{new Date().toString()}
+
) diff --git a/apps/web/components/time-ago.tsx b/apps/web/components/time-ago.tsx new file mode 100644 index 00000000..5f3f2ed3 --- /dev/null +++ b/apps/web/components/time-ago.tsx @@ -0,0 +1,45 @@ +"use client" + +export function TimeAgo({ date }: { date: string }) { + const time = new Date(date) + return ( + + ) +} + +const MINUTE = 60 +const HOUR = MINUTE * 60 +const DAY = HOUR * 24 +const WEEK = DAY * 7 +const MONTH = DAY * 30 +const YEAR = DAY * 365 + +function getTimeAgo(date: Date) { + const secondsAgo = Math.round((Date.now() - Number(date)) / 1000) + + if (secondsAgo < MINUTE) { + return secondsAgo + ` second${secondsAgo !== 1 ? "s" : ""} ago` + } + + let divisor + let unit = "" + + if (secondsAgo < HOUR) { + ;[divisor, unit] = [MINUTE, "minute"] + } else if (secondsAgo < DAY) { + ;[divisor, unit] = [HOUR, "hour"] + } else if (secondsAgo < WEEK) { + ;[divisor, unit] = [DAY, "day"] + } else if (secondsAgo < MONTH) { + ;[divisor, unit] = [WEEK, "week"] + } else if (secondsAgo < YEAR) { + ;[divisor, unit] = [MONTH, "month"] + } else { + ;[divisor, unit] = [YEAR, "year"] + } + + const count = Math.floor(secondsAgo / divisor) + return `${count} ${unit}${count > 1 ? "s" : ""} ago` +} diff --git a/apps/web/next.config.mjs b/apps/web/next.config.mjs index cc3ac2bc..aa920802 100644 --- a/apps/web/next.config.mjs +++ b/apps/web/next.config.mjs @@ -48,6 +48,12 @@ const config = { port: "", pathname: "/**", }, + { + protocol: "https", + hostname: "avatars.githubusercontent.com", + port: "", + pathname: "/**", + }, ], }, }