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
18 changes: 18 additions & 0 deletions apps/web/src/app/changelog/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import type { ReactNode } from "react";

import { CommandMenuProvider } from "@/components/command-menu";
import { NavigationBar } from "@/components/layout/navigation-bar";
import { PageTransition } from "@/components/layout/page-transition";

export default function MarketingLayout({ children }: { children: ReactNode }) {
return (
<CommandMenuProvider>
<div className="dark bg-background text-foreground min-h-dvh overflow-x-hidden">
<NavigationBar stars={null} />
<main className="lg:overflow-hidden">
<PageTransition>{children}</PageTransition>
</main>
</div>
</CommandMenuProvider>
);
}
51 changes: 51 additions & 0 deletions apps/web/src/app/changelog/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import type { Metadata } from "next";

import { ChangelogFooter } from "@/components/changelog/changelog-footer";
import { ChangelogRelease } from "@/components/changelog/changelog-release";
import { ChangelogSidebar } from "@/components/changelog/changelog-sidebar";
import { SectionShell } from "@/components/layout/section";
import { getReleases } from "@/lib/github";
import type { GitHubRelease } from "@/lib/releases";

export const metadata: Metadata = {
title: "Changelog – PayKit",
description: "Stay up to date with the latest changes to PayKit.",
};

export default async function ChangelogPage() {
const releases = (await getReleases()) as GitHubRelease[];
const latestRelease = releases[0];

return (
<div className="mt-11 flex min-h-[calc(100dvh-2.75rem)] flex-col lg:mt-12 lg:h-[calc(100dvh-3rem)] lg:min-h-0 lg:overflow-hidden">
<SectionShell className="grid min-h-0 flex-1 grid-cols-1 lg:grid-cols-[minmax(20rem,38%)_minmax(0,1fr)]">
<ChangelogSidebar latestRelease={latestRelease} releaseCount={releases.length} />

<div className="min-w-0 lg:flex lg:min-h-0 lg:flex-col lg:overflow-hidden">
<div className="lg:min-h-0 lg:flex-1 lg:overflow-y-auto lg:overscroll-contain">
<div className="px-5 py-10 sm:px-8 lg:px-12 lg:py-12 xl:px-16">
<div className="mb-10 flex items-center gap-4">
<h2 className="text-foreground/35 shrink-0 font-mono text-[0.65rem] tracking-[0.25em] uppercase">
Changelog
</h2>
<div className="bg-border/80 h-px flex-1" />
</div>

{releases.length === 0 ? (
<p className="text-foreground/45 text-sm">No releases found yet.</p>
) : (
<div>
{releases.map((release) => (
<ChangelogRelease key={release.id} release={release} />
))}
</div>
)}
</div>

<ChangelogFooter className="mt-0" />
</div>
</div>
</SectionShell>
</div>
);
}
6 changes: 6 additions & 0 deletions apps/web/src/app/sitemap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,11 @@ export default function sitemap(): MetadataRoute.Sitemap {
priority: 1,
},
...docsRoutes,
{
url: URLs.changelog,
lastModified: new Date(),
changeFrequency: "weekly",
priority: 1,
},
];
}
86 changes: 86 additions & 0 deletions apps/web/src/components/changelog/changelog-footer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
"use client";

import { ArrowRight, Github } from "lucide-react";
import Link from "next/link";

import { Icons } from "@/components/icons";
import { URLs } from "@/lib/consts";
import { cn } from "@/lib/utils";

type FooterLink = { label: string; href: string; external?: boolean };

const footerLinks: FooterLink[] = [
{ label: "Docs", href: "/docs" },
{ label: "Contact", href: "/contact" },
{ label: "Discord", href: URLs.discord, external: true },
{ label: "Changelog", href: "/changelog" },
];

type ChangelogFooterProps = {
className?: string;
};

export function ChangelogFooter({ className }: ChangelogFooterProps) {
const year = new Date().getFullYear();

return (
<footer className={cn("border-border/60 mt-16 border-t border-dashed", className)}>
<div className="px-5 py-8 sm:px-8 lg:px-12 lg:py-10 xl:px-16">
<Link
href={`${URLs.githubRepo}/releases`}
target="_blank"
rel="noopener noreferrer"
className="text-foreground/45 hover:text-foreground/75 group inline-flex items-center gap-1.5 font-mono text-xs transition-colors"
>
View all releases on GitHub
<ArrowRight className="size-3 transition-transform group-hover:translate-x-0.5" />
</Link>

<div className="mt-8 flex flex-col gap-5 sm:flex-row sm:items-center sm:justify-between">
<nav className="flex flex-wrap items-center gap-x-1 gap-y-1">
{footerLinks.map((link, index) => (
<span key={link.label} className="flex items-center">
<Link
href={link.href}
target={link.external ? "_blank" : undefined}
rel={link.external ? "noopener noreferrer" : undefined}
className="text-foreground/40 hover:text-foreground/70 text-xs transition-colors"
>
{link.label}
</Link>
{index < footerLinks.length - 1 ? (
<span className="text-foreground/15 mx-2.5 text-xs select-none">/</span>
) : null}
</span>
))}
</nav>

<div className="flex flex-wrap items-center gap-3">
<span className="text-foreground/40 text-xs">© {year} PayKit</span>
<span className="text-foreground/15 hidden h-3 w-px bg-border sm:block" aria-hidden />
<div className="flex items-center gap-2.5">
<Link
href={URLs.x}
target="_blank"
rel="noopener noreferrer"
aria-label="PayKit on X"
className="text-foreground/35 hover:text-foreground/65 transition-colors"
>
<Icons.XIcon className="size-3.5" />
</Link>
<Link
href={URLs.githubRepo}
target="_blank"
rel="noopener noreferrer"
aria-label="PayKit on GitHub"
className="text-foreground/35 hover:text-foreground/65 transition-colors"
>
<Github className="size-3.5" />
</Link>
</div>
</div>
</div>
</div>
</footer>
);
}
48 changes: 48 additions & 0 deletions apps/web/src/components/changelog/changelog-release.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import Link from "next/link";

import { ReleaseBody } from "@/lib/html-parser";
import type { GitHubRelease } from "@/lib/releases";
import { formatReleaseDate } from "@/lib/releases";

type ChangelogReleaseProps = {
release: GitHubRelease;
};

export function ChangelogRelease({ release }: ChangelogReleaseProps) {
return (
<article
id={release.tag_name.replace(/[^a-zA-Z0-9-_]/g, "-")}
className="border-border/60 scroll-mt-28 border-b border-dashed py-10 last:border-b-0 lg:py-12"
>
<header className="flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between">
<div className="space-y-2">
<div className="flex flex-wrap items-baseline gap-2.5">
<Link href={release.html_url}>
<h2 className="text-foreground/90 font-mono text-xl font-semibold tracking-tight sm:text-2xl">
{release.tag_name}
</h2>
</Link>

<div className="flex shrink-0 flex-col items-start gap-2 sm:items-end">
<time
dateTime={release.published_at}
className="text-foreground/40 font-mono text-xs tracking-wide"
>
{formatReleaseDate(release.published_at)}
</time>
</div>
</div>
{release.name && release.name !== release.tag_name ? (
<p className="text-foreground/55 text-sm leading-snug">{release.name}</p>
) : null}
</div>
</header>

{release.body ? (
<div className="mt-6">
<ReleaseBody body={release.body} />
</div>
) : null}
</article>
);
}
69 changes: 69 additions & 0 deletions apps/web/src/components/changelog/changelog-sidebar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { ExternalLink, Github, HistoryIcon } from "lucide-react";
import Link from "next/link";

import { URLs } from "@/lib/consts";
import type { GitHubRelease } from "@/lib/releases";
import { cn } from "@/lib/utils";

type ChangelogSidebarProps = {
latestRelease: GitHubRelease | undefined;
releaseCount: number;
};

export function ChangelogSidebar({ latestRelease, releaseCount }: ChangelogSidebarProps) {
return (
<aside
className={cn(
"border-border/60 relative min-w-0 shrink-0 overflow-hidden border-b lg:border-r lg:border-b-0",
"lg:flex lg:h-full lg:flex-col lg:justify-center",
)}
>
<div className="relative space-y-2 px-5 py-10 sm:px-8 lg:px-10 lg:py-12 xl:px-12">
<p className="text-foreground/45 flex items-center gap-2 font-mono text-xs tracking-wide">
<HistoryIcon className="size-3.5 shrink-0" aria-hidden />
Changelog
</p>
<h1 className="text-foreground/90 text-2xl font-semibold tracking-tight sm:text-3xl">
All changes, fixes, and updates
</h1>
<p className="text-foreground/45 max-w-sm text-sm leading-relaxed sm:text-base">
Every release shipped to PayKit, straight from GitHub.
</p>
</div>

<div className="relative mt-8 space-y-6 px-5 pb-10 sm:px-8 lg:mt-0 lg:px-10 lg:pb-12 xl:px-12">
{latestRelease ? (
<div className="border-border/80 space-y-2 border-t pt-6">
<div className="flex items-center justify-between gap-3">
<p className="text-foreground/35 font-mono text-[0.65rem] tracking-[0.2em] uppercase">
Latest
</p>
<p className="text-foreground/80 font-mono text-sm">{latestRelease.tag_name}</p>
</div>
</div>
) : null}

<div className="border-border/80 space-y-3 border-t pt-6">
<p className="text-foreground/35 font-mono text-[0.65rem] tracking-[0.2em] uppercase">
Source
</p>
<Link
href={`${URLs.githubRepo}/releases`}
target="_blank"
rel="noopener noreferrer"
className="text-foreground/50 hover:text-foreground/80 group flex items-center gap-2 text-sm transition-colors"
>
<Github className="size-3.5 shrink-0" aria-hidden />
<span className="font-mono text-xs tracking-wide uppercase">GitHub releases</span>
<ExternalLink className="text-foreground/25 group-hover:text-foreground/50 size-3 shrink-0 transition-colors" />
</Link>
{releaseCount > 0 ? (
<p className="text-foreground/35 font-mono text-xs">
{releaseCount} release{releaseCount === 1 ? "" : "s"} loaded
</p>
) : null}
</div>
</div>
</aside>
);
}
34 changes: 34 additions & 0 deletions apps/web/src/components/changelog/release-contributors.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import Link from "next/link";

type ReleaseContributorsProps = {
contributors: string[];
};

export function ReleaseContributors({ contributors }: ReleaseContributorsProps) {
if (contributors.length === 0) return null;

return (
<section className="mt-2 pt-6">
<h3 className="text-foreground/80 text-sm font-medium">Contributors</h3>
<p className="text-foreground/40 mt-1 text-xs leading-relaxed">
Thanks to everyone who contributed to this release.
</p>
<ul className="mt-4 flex flex-wrap gap-2">
{contributors.map((username) => (
<li key={username}>
<Link
href={`https://github.com/${username}`}
target="_blank"
rel="noopener noreferrer"
className="group"
>
<span className="text-foreground/60 group-hover:text-foreground/85 font-mono text-xs">
@{username}
</span>
</Link>
</li>
))}
</ul>
</section>
);
}
1 change: 1 addition & 0 deletions apps/web/src/components/layout/navigation-bar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ function NavLink({
const navTabs: NavItem[] = [
{ name: "readme", href: "/" },
{ name: "docs", href: "/docs", path: "/docs" },
{ name: "changelog", href: "/changelog" },
];

const dropdownLinks: NavItem[] = [
Expand Down
1 change: 1 addition & 0 deletions apps/web/src/components/sections/footer-section.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ const navLinks = [
{ label: "Docs", href: "/docs" },
{ label: "Contact", href: "/contact" },
{ label: "Author", href: URLs.authorX, external: true },
{ label: "Changelog", href: "/changelog" },
];

const socialLinks = [
Expand Down
2 changes: 2 additions & 0 deletions apps/web/src/env.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,15 @@ export const env = createEnv({
RESEND_API_KEY: z.string().min(1),
RESEND_FROM_EMAIL: z.string().email().default("contact@paykit.sh"),
RESEND_TO_EMAIL: z.string().email().default("contact@paykit.sh"),
GITHUB_TOKEN: z.string().min(1).optional(),
},
client: {},
runtimeEnv: {
NODE_ENV: process.env.NODE_ENV,
RESEND_API_KEY: process.env.RESEND_API_KEY,
RESEND_FROM_EMAIL: process.env.RESEND_FROM_EMAIL,
RESEND_TO_EMAIL: process.env.RESEND_TO_EMAIL,
GITHUB_TOKEN: process.env.GITHUB_TOKEN,
},
skipValidation: !!process.env.SKIP_ENV_VALIDATION,
emptyStringAsUndefined: true,
Expand Down
1 change: 1 addition & 0 deletions apps/web/src/lib/consts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export const URLs = {
discord: "https://discord.gg/nzy9NPpFNU",
authorGitHub: "https://github.com/maxktz",
authorX: "https://x.com/maxktz",
changelog: "https://paykit.sh/changelog",
} as const;

export const VERSION_TEXT = "v0.1 beta";
Expand Down
17 changes: 17 additions & 0 deletions apps/web/src/lib/github.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { env } from "@/env";
import type { GitHubRelease } from "@/lib/releases";

export async function getReleases(): Promise<GitHubRelease[]> {
const res = await fetch("https://api.github.com/repos/getpaykit/paykit/releases", {
next: { revalidate: 3600 },
signal: AbortSignal.timeout(10_000),
headers: {
...(env.GITHUB_TOKEN && { Authorization: `Bearer ${env.GITHUB_TOKEN}` }),
Accept: "application/vnd.github.v3+json",
},
});
Comment thread
anirudhprmar marked this conversation as resolved.

if (!res.ok) throw new Error("Failed to fetch releases");

return res.json() as Promise<GitHubRelease[]>;
}
Loading
Loading