feat: Next 16 + Payload 3 scaffold with Kaniko CI and Swarm deploy (#1)
Some checks failed
ci/woodpecker/push/web Pipeline failed

Co-authored-by: Jason Woltje <jason@diversecanvas.com>
Co-committed-by: Jason Woltje <jason@diversecanvas.com>
This commit was merged in pull request #1.
This commit is contained in:
2026-04-14 03:21:17 +00:00
committed by jason.woltje
parent c800bef739
commit 8c5a25e976
51 changed files with 9353 additions and 0 deletions

View File

@@ -0,0 +1,27 @@
import { SiteHeader } from "@/components/SiteHeader";
import { SiteFooter } from "@/components/SiteFooter";
export const metadata = { title: "About" };
export default function AboutPage() {
return (
<>
<SiteHeader />
<main className="mx-auto max-w-7xl px-6 py-24">
<span className="mb-6 block font-label text-xs uppercase tracking-[0.4em] text-tertiary">
02 // PROFILE
</span>
<h1 className="mb-8 font-headline text-5xl font-bold tracking-tighter md:text-7xl">
About
</h1>
<p className="max-w-3xl font-body text-xl text-on-surface-variant">
Engineering growth through technological mastery and strategic
leadership. Content sourced from Payload CMS (global:{" "}
<code className="font-label text-primary">about</code>) populated on
first publish.
</p>
</main>
<SiteFooter />
</>
);
}

View File

@@ -0,0 +1,26 @@
import { SiteHeader } from "@/components/SiteHeader";
import { SiteFooter } from "@/components/SiteFooter";
export const metadata = { title: "Contact" };
export default function ContactPage() {
return (
<>
<SiteHeader />
<main className="mx-auto max-w-3xl px-6 py-24">
<span className="mb-6 block font-label text-xs uppercase tracking-[0.4em] text-primary">
05 // CONTACT
</span>
<h1 className="mb-8 font-headline text-5xl font-bold tracking-tighter md:text-7xl">
Open channel
</h1>
<p className="font-body text-xl text-on-surface-variant">
Form wiring, Turnstile, and Payload submission persistence land in a
follow-up PR (UI-06). For now, direct email lives in the Payload{" "}
<code className="font-label text-primary">contact</code> global.
</p>
</main>
<SiteFooter />
</>
);
}

View File

@@ -0,0 +1,52 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
html {
@apply bg-background text-on-background font-body antialiased;
color-scheme: dark;
}
body {
@apply min-h-screen selection:bg-primary/30 selection:text-on-primary-container;
}
}
@layer components {
/* DESIGN.md: "Ghost Border" — containment that is felt rather than seen. */
.ghost-border {
border: 1px solid rgba(71, 72, 77, 0.15);
}
/* DESIGN.md: "Glass & Gradient" — frosted terminal effect. */
.glass-card {
background-color: rgba(42, 44, 50, 0.6);
backdrop-filter: blur(24px);
-webkit-backdrop-filter: blur(24px);
}
/* DESIGN.md: neon CTA — "lit from within" glow. */
.neon-cta {
background: linear-gradient(135deg, #81ecff 0%, #00e3fd 100%);
color: #005762;
box-shadow:
0 0 32px 4px rgba(129, 236, 255, 0.25),
0 0 4px 1px rgba(129, 236, 255, 0.5);
}
/* DESIGN.md: technical grid background pattern. */
.technical-grid {
background-image: radial-gradient(
rgba(129, 236, 255, 0.15) 1px,
transparent 1px
);
background-size: 32px 32px;
}
/* DESIGN.md: hero radial gradient ambient. */
.hero-gradient {
background:
radial-gradient(circle at top right, rgba(129, 236, 255, 0.08), transparent 40%),
radial-gradient(circle at bottom left, rgba(216, 115, 255, 0.05), transparent 40%);
}
}

View File

@@ -0,0 +1,57 @@
import type { Metadata } from "next";
import { Space_Grotesk, Inter } from "next/font/google";
import "./globals.css";
const spaceGrotesk = Space_Grotesk({
subsets: ["latin"],
weight: ["400", "500", "700"],
variable: "--font-headline",
display: "swap",
});
const inter = Inter({
subsets: ["latin"],
weight: ["400", "500", "600", "700"],
variable: "--font-body",
display: "swap",
});
export const metadata: Metadata = {
metadataBase: new URL(
process.env.NEXT_PUBLIC_SITE_URL ?? "http://localhost:3000",
),
title: {
default: "Jason Woltje",
template: "%s — Jason Woltje",
},
description:
"A multidisciplinary architect of digital ecosystems. Engineering growth through technological mastery and strategic leadership.",
};
const BUILD_SHA = process.env.NEXT_PUBLIC_BUILD_SHA ?? "dev";
const BUILD_REV = process.env.NEXT_PUBLIC_BUILD_REV ?? "local";
export default function FrontendLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html
lang="en"
className={`dark ${spaceGrotesk.variable} ${inter.variable}`}
style={{ ["--font-label" as string]: "var(--font-headline)" }}
suppressHydrationWarning
>
<body>
{children}
<div
aria-hidden
className="pointer-events-none fixed bottom-3 right-4 z-50 font-label text-[10px] uppercase tracking-[0.2em] text-tertiary/80"
>
REV: {BUILD_REV} · SHA: {BUILD_SHA}
</div>
</body>
</html>
);
}

View File

@@ -0,0 +1,40 @@
import { SiteHeader } from "@/components/SiteHeader";
import { SiteFooter } from "@/components/SiteFooter";
import { StatusTerminal } from "@/components/StatusTerminal";
export default function HomePage() {
return (
<>
<SiteHeader />
<main>
<section className="technical-grid hero-gradient relative flex min-h-[92vh] flex-col justify-center overflow-hidden px-6">
<StatusTerminal className="absolute left-6 top-8 md:left-12" />
<div className="mx-auto grid w-full max-w-7xl grid-cols-1 gap-12 pt-20 lg:grid-cols-12">
<div className="lg:col-span-8">
<span className="mb-6 block font-label text-xs uppercase tracking-[0.4em] text-primary">
01 // THE MANIFESTO
</span>
<h1 className="mb-8 font-headline text-5xl font-bold leading-[0.9] tracking-tighter text-on-surface md:text-8xl">
I WILL FIND
<br />
<span className="bg-gradient-to-r from-primary to-secondary bg-clip-text text-transparent">
A WAY,
</span>
<br />
OR I WILL
<br />
MAKE ONE.
</h1>
<p className="max-w-2xl font-body text-xl leading-relaxed text-on-surface-variant md:text-2xl">
A multidisciplinary architect of digital ecosystems and
agricultural infrastructures. Executing vision through
precision.
</p>
</div>
</div>
</section>
</main>
<SiteFooter />
</>
);
}

View File

@@ -0,0 +1,45 @@
import { notFound } from "next/navigation";
import { SiteHeader } from "@/components/SiteHeader";
import { SiteFooter } from "@/components/SiteFooter";
type Params = { slug: string };
export async function generateMetadata({
params,
}: {
params: Promise<Params>;
}) {
const { slug } = await params;
return { title: slug };
}
export default async function ProjectDetailPage({
params,
}: {
params: Promise<Params>;
}) {
const { slug } = await params;
if (!slug) notFound();
return (
<>
<SiteHeader />
<main className="mx-auto max-w-4xl px-6 py-24">
<span className="mb-4 block font-label text-xs uppercase tracking-[0.4em] text-primary">
PROJECT //{" "}
<code className="text-on-surface-variant">{slug}</code>
</span>
<h1 className="mb-6 font-headline text-4xl font-bold tracking-tight md:text-6xl">
Project detail
</h1>
<p className="font-body text-lg text-on-surface-variant">
Slug: <code className="font-label text-primary">{slug}</code>. Body
will render from the Payload{" "}
<code className="font-label text-primary">projects</code> collection
once wired.
</p>
</main>
<SiteFooter />
</>
);
}

View File

@@ -0,0 +1,26 @@
import { SiteHeader } from "@/components/SiteHeader";
import { SiteFooter } from "@/components/SiteFooter";
export const metadata = { title: "Projects" };
export default function ProjectsIndexPage() {
return (
<>
<SiteHeader />
<main className="mx-auto max-w-7xl px-6 py-24">
<span className="mb-6 block font-label text-xs uppercase tracking-[0.4em] text-primary">
03 // PROJECTS
</span>
<h1 className="mb-8 font-headline text-5xl font-bold tracking-tighter md:text-7xl">
Strategic Verticals
</h1>
<p className="max-w-3xl font-body text-xl text-on-surface-variant">
Content sourced from Payload CMS collection{" "}
<code className="font-label text-primary">projects</code> rendered
here once the bento-grid section component lands in a follow-up PR.
</p>
</main>
<SiteFooter />
</>
);
}

View File

@@ -0,0 +1,28 @@
import { SiteHeader } from "@/components/SiteHeader";
import { SiteFooter } from "@/components/SiteFooter";
export const metadata = { title: "Resume" };
export default function ResumePage() {
return (
<>
<SiteHeader />
<main className="mx-auto max-w-4xl px-6 py-24">
<span className="mb-6 block font-label text-xs uppercase tracking-[0.4em] text-tertiary">
06 // CURRICULUM
</span>
<h1 className="mb-8 font-headline text-5xl font-bold tracking-tighter md:text-7xl">
Resume
</h1>
<p className="font-body text-xl text-on-surface-variant">
Sourced from Payload{" "}
<code className="font-label text-primary">resume</code> global. PDF
export wires up at{" "}
<code className="font-label text-primary">/resume.pdf</code> in
UI-07.
</p>
</main>
<SiteFooter />
</>
);
}

View File

@@ -0,0 +1,43 @@
import { notFound } from "next/navigation";
import { SiteHeader } from "@/components/SiteHeader";
import { SiteFooter } from "@/components/SiteFooter";
type Params = { slug: string };
export async function generateMetadata({
params,
}: {
params: Promise<Params>;
}) {
const { slug } = await params;
return { title: slug };
}
export default async function PostDetailPage({
params,
}: {
params: Promise<Params>;
}) {
const { slug } = await params;
if (!slug) notFound();
return (
<>
<SiteHeader />
<article className="mx-auto max-w-3xl px-6 py-24">
<span className="mb-4 block font-label text-xs uppercase tracking-[0.4em] text-secondary">
POST //{" "}
<code className="text-on-surface-variant">{slug}</code>
</span>
<h1 className="mb-6 font-headline text-4xl font-bold tracking-tight md:text-6xl">
Post detail
</h1>
<p className="font-body text-lg text-on-surface-variant">
Body renders from Payload{" "}
<code className="font-label text-primary">posts</code> once wired.
</p>
</article>
<SiteFooter />
</>
);
}

View File

@@ -0,0 +1,25 @@
import { SiteHeader } from "@/components/SiteHeader";
import { SiteFooter } from "@/components/SiteFooter";
export const metadata = { title: "Writing" };
export default function WritingIndexPage() {
return (
<>
<SiteHeader />
<main className="mx-auto max-w-7xl px-6 py-24">
<span className="mb-6 block font-label text-xs uppercase tracking-[0.4em] text-secondary">
04 // WRITING
</span>
<h1 className="mb-8 font-headline text-5xl font-bold tracking-tighter md:text-7xl">
Signal
</h1>
<p className="max-w-3xl font-body text-xl text-on-surface-variant">
Long-form from the Payload{" "}
<code className="font-label text-primary">posts</code> collection.
</p>
</main>
<SiteFooter />
</>
);
}

View File

@@ -0,0 +1,18 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import type { Metadata } from "next";
import config from "@payload-config";
import { NotFoundPage, generatePageMetadata } from "@payloadcms/next/views";
import { importMap } from "../importMap";
type Args = {
params: Promise<{ [key: string]: string | string[] | undefined }>;
searchParams: Promise<{ [key: string]: string | string[] | undefined }>;
};
export const generateMetadata = ({ params, searchParams }: Args): Promise<Metadata> =>
generatePageMetadata({ config, params: params as any, searchParams: searchParams as any });
const NotFound = ({ params, searchParams }: Args) =>
NotFoundPage({ config, params: params as any, searchParams: searchParams as any, importMap });
export default NotFound;

View File

@@ -0,0 +1,18 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import type { Metadata } from "next";
import config from "@payload-config";
import { RootPage, generatePageMetadata } from "@payloadcms/next/views";
import { importMap } from "../importMap";
type Args = {
params: Promise<{ [key: string]: string | string[] | undefined }>;
searchParams: Promise<{ [key: string]: string | string[] | undefined }>;
};
export const generateMetadata = ({ params, searchParams }: Args): Promise<Metadata> =>
generatePageMetadata({ config, params: params as any, searchParams: searchParams as any });
const Page = ({ params, searchParams }: Args) =>
RootPage({ config, params: params as any, searchParams: searchParams as any, importMap });
export default Page;

View File

@@ -0,0 +1,2 @@
// Auto-generated stub. Regenerate with `pnpm generate:importmap` after adding custom components.
export const importMap = {};

View File

@@ -0,0 +1,16 @@
import config from "@payload-config";
import {
REST_DELETE,
REST_GET,
REST_OPTIONS,
REST_PATCH,
REST_POST,
REST_PUT,
} from "@payloadcms/next/routes";
export const GET = REST_GET(config);
export const POST = REST_POST(config);
export const DELETE = REST_DELETE(config);
export const PATCH = REST_PATCH(config);
export const PUT = REST_PUT(config);
export const OPTIONS = REST_OPTIONS(config);

View File

@@ -0,0 +1,4 @@
import config from "@payload-config";
import { GRAPHQL_PLAYGROUND_GET } from "@payloadcms/next/routes";
export const GET = GRAPHQL_PLAYGROUND_GET(config);

View File

@@ -0,0 +1,5 @@
import config from "@payload-config";
import { GRAPHQL_POST, REST_OPTIONS } from "@payloadcms/next/routes";
export const POST = GRAPHQL_POST(config);
export const OPTIONS = REST_OPTIONS(config);

View File

@@ -0,0 +1,16 @@
import { NextResponse } from "next/server";
export const runtime = "nodejs";
export const dynamic = "force-dynamic";
export function GET() {
return NextResponse.json(
{
status: "ok",
buildSha: process.env.NEXT_PUBLIC_BUILD_SHA ?? "dev",
buildRev: process.env.NEXT_PUBLIC_BUILD_REV ?? "local",
timestamp: new Date().toISOString(),
},
{ status: 200 },
);
}

View File

@@ -0,0 +1 @@
/* Payload admin UI customizations. Keep minimal for v0.0.x. */

View File

@@ -0,0 +1,22 @@
/* Payload admin layout — do not modify unless you understand the Payload 3 requirements. */
import type { ServerFunctionClient } from "payload";
import config from "@payload-config";
import { RootLayout, handleServerFunctions } from "@payloadcms/next/layouts";
import "@payloadcms/next/css";
import { importMap } from "./admin/importMap";
import "./custom.scss";
type Args = { children: React.ReactNode };
const serverFunction: ServerFunctionClient = async function (args) {
"use server";
return handleServerFunctions({ ...args, config, importMap });
};
const Layout = ({ children }: Args) => (
<RootLayout config={config} importMap={importMap} serverFunction={serverFunction}>
{children}
</RootLayout>
);
export default Layout;

View File

@@ -0,0 +1,21 @@
import type { CollectionConfig } from "payload";
export const Categories: CollectionConfig = {
slug: "categories",
access: { read: () => true },
admin: { useAsTitle: "name", defaultColumns: ["name", "slug", "accent"] },
fields: [
{ name: "name", type: "text", required: true },
{ name: "slug", type: "text", required: true, unique: true, index: true },
{
name: "accent",
type: "select",
defaultValue: "primary",
options: [
{ label: "Primary (cyan)", value: "primary" },
{ label: "Secondary (magenta)", value: "secondary" },
{ label: "Tertiary (green)", value: "tertiary" },
],
},
],
};

View File

@@ -0,0 +1,33 @@
import type { CollectionConfig } from "payload";
export const ContactSubmissions: CollectionConfig = {
slug: "contactSubmissions",
access: {
read: ({ req: { user } }) => Boolean(user),
update: ({ req: { user } }) => Boolean(user),
delete: ({ req: { user } }) => Boolean(user),
create: () => true,
},
admin: {
useAsTitle: "name",
defaultColumns: ["name", "email", "status", "submittedAt"],
},
fields: [
{ name: "name", type: "text", required: true },
{ name: "email", type: "email", required: true },
{ name: "brief", type: "textarea", required: true },
{ name: "source", type: "text" },
{ name: "submittedAt", type: "date", defaultValue: () => new Date() },
{ name: "ipHash", type: "text" },
{
name: "status",
type: "select",
defaultValue: "new",
options: [
{ label: "New", value: "new" },
{ label: "Replied", value: "replied" },
{ label: "Spam", value: "spam" },
],
},
],
};

36
src/collections/Gear.ts Normal file
View File

@@ -0,0 +1,36 @@
import type { CollectionConfig } from "payload";
export const Gear: CollectionConfig = {
slug: "gear",
access: { read: () => true },
admin: {
useAsTitle: "name",
defaultColumns: ["name", "type"],
description: "Music / maker gear — decorative only for v0.0.x",
},
fields: [
{ name: "name", type: "text", required: true },
{
name: "type",
type: "select",
options: [
{ label: "Daily driver", value: "daily-driver" },
{ label: "Interface", value: "interface" },
{ label: "Monitor", value: "monitor" },
{ label: "Workbench", value: "workbench" },
],
},
{ name: "notes", type: "textarea" },
{ name: "image", type: "upload", relationTo: "media" },
{
name: "accent",
type: "select",
defaultValue: "tertiary",
options: [
{ label: "Primary", value: "primary" },
{ label: "Secondary", value: "secondary" },
{ label: "Tertiary", value: "tertiary" },
],
},
],
};

26
src/collections/Media.ts Normal file
View File

@@ -0,0 +1,26 @@
import type { CollectionConfig } from "payload";
export const Media: CollectionConfig = {
slug: "media",
access: {
read: () => true,
},
admin: {
useAsTitle: "alt",
},
upload: {
staticDir: "media",
imageSizes: [
{ name: "thumb", width: 400, height: 400, position: "centre" },
{ name: "card", width: 800, height: undefined, position: "centre" },
{ name: "hero", width: 1600, height: undefined, position: "centre" },
{ name: "og", width: 1200, height: 630, position: "centre" },
],
adminThumbnail: "thumb",
mimeTypes: ["image/*"],
},
fields: [
{ name: "alt", type: "text", required: true },
{ name: "credit", type: "text" },
],
};

43
src/collections/Posts.ts Normal file
View File

@@ -0,0 +1,43 @@
import type { CollectionConfig } from "payload";
export const Posts: CollectionConfig = {
slug: "posts",
access: {
read: ({ req: { user } }) =>
user ? true : { status: { equals: "published" } },
},
admin: {
useAsTitle: "title",
defaultColumns: ["title", "status", "publishedAt"],
},
versions: { drafts: true },
fields: [
{ name: "title", type: "text", required: true },
{ name: "slug", type: "text", required: true, unique: true, index: true },
{ name: "excerpt", type: "textarea" },
{ name: "body", type: "richText" },
{ name: "category", type: "relationship", relationTo: "categories" },
{ name: "heroImage", type: "upload", relationTo: "media" },
{ name: "tags", type: "array", fields: [{ name: "tag", type: "text" }] },
{
name: "status",
type: "select",
defaultValue: "draft",
required: true,
options: [
{ label: "Draft", value: "draft" },
{ label: "Published", value: "published" },
],
},
{ name: "publishedAt", type: "date" },
{
name: "seo",
type: "group",
fields: [
{ name: "title", type: "text" },
{ name: "description", type: "textarea" },
{ name: "image", type: "upload", relationTo: "media" },
],
},
],
};

View File

@@ -0,0 +1,64 @@
import type { CollectionConfig } from "payload";
export const Projects: CollectionConfig = {
slug: "projects",
access: {
read: ({ req: { user } }) =>
user ? true : { status: { equals: "published" } },
},
admin: {
useAsTitle: "title",
defaultColumns: ["title", "status", "featured", "order", "publishedAt"],
},
versions: { drafts: true },
fields: [
{ name: "title", type: "text", required: true },
{ name: "slug", type: "text", required: true, unique: true, index: true },
{
name: "role",
type: "select",
options: [
{ label: "Founder / CEO", value: "founder" },
{ label: "Consultant", value: "consultant" },
{ label: "Engineer", value: "engineer" },
{ label: "Infrastructure", value: "infra" },
],
},
{ name: "category", type: "relationship", relationTo: "categories" },
{ name: "summary", type: "textarea" },
{ name: "body", type: "richText" },
{ name: "stack", type: "array", fields: [{ name: "name", type: "text" }] },
{ name: "heroImage", type: "upload", relationTo: "media" },
{
name: "gallery",
type: "array",
fields: [
{ name: "image", type: "upload", relationTo: "media", required: true },
{ name: "caption", type: "text" },
],
},
{ name: "externalUrl", type: "text" },
{ name: "featured", type: "checkbox", defaultValue: false },
{ name: "order", type: "number", defaultValue: 0 },
{
name: "status",
type: "select",
defaultValue: "draft",
required: true,
options: [
{ label: "Draft", value: "draft" },
{ label: "Published", value: "published" },
],
},
{ name: "publishedAt", type: "date" },
{
name: "seo",
type: "group",
fields: [
{ name: "title", type: "text" },
{ name: "description", type: "textarea" },
{ name: "image", type: "upload", relationTo: "media" },
],
},
],
};

21
src/collections/Users.ts Normal file
View File

@@ -0,0 +1,21 @@
import type { CollectionConfig } from "payload";
export const Users: CollectionConfig = {
slug: "users",
admin: {
useAsTitle: "email",
defaultColumns: ["email", "role"],
},
auth: true,
fields: [
{
name: "role",
type: "select",
defaultValue: "admin",
options: [{ label: "Admin", value: "admin" }],
access: {
update: ({ req: { user } }) => user?.role === "admin",
},
},
],
};

View File

@@ -0,0 +1,14 @@
export function SiteFooter() {
return (
<footer className="mt-24 border-t border-outline-variant/10 bg-surface-container-lowest">
<div className="mx-auto flex max-w-7xl flex-col gap-6 px-6 py-12 md:flex-row md:items-center md:justify-between">
<div className="font-headline text-sm uppercase tracking-[0.2em] text-on-surface-variant">
© {new Date().getFullYear()} Jason Woltje · All rights reserved
</div>
<div className="font-label text-[10px] uppercase tracking-[0.2em] text-tertiary/80">
LATENCY: 42ms · CORE STATUS: NOMINAL
</div>
</div>
</footer>
);
}

View File

@@ -0,0 +1,35 @@
import Link from "next/link";
const links = [
{ label: "Home", href: "/" },
{ label: "Projects", href: "/projects" },
{ label: "Writing", href: "/writing" },
{ label: "About", href: "/about" },
{ label: "Contact", href: "/contact" },
];
export function SiteHeader() {
return (
<header className="sticky top-0 z-50 w-full bg-background/80 backdrop-blur-xl">
<nav className="mx-auto flex max-w-7xl items-center justify-between px-6 py-4">
<Link
href="/"
className="font-headline text-xl font-bold uppercase tracking-tighter text-primary"
>
JASON WOLTJE
</Link>
<div className="hidden items-center gap-8 md:flex">
{links.map((link) => (
<Link
key={link.href}
href={link.href}
className="font-label text-[14px] uppercase tracking-tighter text-on-surface-variant transition-colors hover:text-primary"
>
{link.label}
</Link>
))}
</div>
</nav>
</header>
);
}

View File

@@ -0,0 +1,23 @@
const BUILD_SHA = process.env.NEXT_PUBLIC_BUILD_SHA ?? "dev";
const BUILD_REV = process.env.NEXT_PUBLIC_BUILD_REV ?? "local";
type Props = {
location?: string;
status?: string;
className?: string;
};
export function StatusTerminal({
location = "39.0997° N, 94.5786° W",
status = "ONLINE",
className = "",
}: Props) {
return (
<div className={`flex items-center gap-3 ${className}`}>
<span className="flex h-2 w-2 rounded-full bg-tertiary shadow-[0_0_8px_#8eff71]" />
<span className="font-label text-[10px] uppercase tracking-[0.2em] text-tertiary">
LOC: {location} · STATUS: {status} · REV: {BUILD_REV} · SHA: {BUILD_SHA}
</span>
</div>
);
}

27
src/globals/About.ts Normal file
View File

@@ -0,0 +1,27 @@
import type { GlobalConfig } from "payload";
export const About: GlobalConfig = {
slug: "about",
access: { read: () => true },
fields: [
{ name: "intro", type: "richText" },
{ name: "makerMindset", type: "richText" },
{ name: "soundtrack", type: "richText" },
{
name: "gearRefs",
type: "relationship",
relationTo: "gear",
hasMany: true,
},
{
name: "timeline",
type: "array",
fields: [
{ name: "year", type: "text", required: true },
{ name: "title", type: "text", required: true },
{ name: "note", type: "textarea" },
],
},
{ name: "portrait", type: "upload", relationTo: "media" },
],
};

33
src/globals/Contact.ts Normal file
View File

@@ -0,0 +1,33 @@
import type { GlobalConfig } from "payload";
export const Contact: GlobalConfig = {
slug: "contact",
access: { read: () => true },
fields: [
{
name: "availabilityBadge",
type: "text",
defaultValue: "Accepting new inquiries",
},
{ name: "timezoneLabel", type: "text", defaultValue: "America/Chicago" },
{ name: "directEmail", type: "email" },
{
name: "socialLinks",
type: "array",
fields: [
{ name: "label", type: "text", required: true },
{ name: "href", type: "text", required: true },
{ name: "icon", type: "text" },
],
},
{
name: "newsletterEnabled",
type: "checkbox",
defaultValue: false,
admin: {
description:
"Enable the newsletter subscribe UI. Keep false until Mautic is deployed.",
},
},
],
};

35
src/globals/Home.ts Normal file
View File

@@ -0,0 +1,35 @@
import type { GlobalConfig } from "payload";
export const Home: GlobalConfig = {
slug: "home",
access: { read: () => true },
fields: [
{ name: "heroPrefix", type: "text", defaultValue: "01 // THE MANIFESTO" },
{ name: "heroHeadline", type: "richText" },
{ name: "heroSub", type: "textarea" },
{
name: "ctas",
type: "array",
fields: [
{ name: "label", type: "text", required: true },
{ name: "href", type: "text", required: true },
{
name: "style",
type: "select",
defaultValue: "primary",
options: [
{ label: "Primary (neon)", value: "primary" },
{ label: "Secondary", value: "secondary" },
{ label: "Ghost", value: "ghost" },
],
},
],
},
{
name: "featuredProjects",
type: "relationship",
relationTo: "projects",
hasMany: true,
},
],
};

21
src/globals/Navigation.ts Normal file
View File

@@ -0,0 +1,21 @@
import type { GlobalConfig } from "payload";
export const Navigation: GlobalConfig = {
slug: "navigation",
access: { read: () => true },
fields: [
{
name: "primaryLinks",
type: "array",
fields: [
{ name: "label", type: "text", required: true },
{ name: "href", type: "text", required: true },
],
},
{
name: "footerStatusText",
type: "text",
defaultValue: "LATENCY: 42ms | CORE STATUS: NOMINAL",
},
],
};

46
src/globals/Resume.ts Normal file
View File

@@ -0,0 +1,46 @@
import type { GlobalConfig } from "payload";
export const Resume: GlobalConfig = {
slug: "resume",
access: { read: () => true },
fields: [
{ name: "summary", type: "textarea" },
{
name: "experience",
type: "array",
fields: [
{ name: "company", type: "text", required: true },
{ name: "role", type: "text", required: true },
{ name: "startDate", type: "date" },
{ name: "endDate", type: "date" },
{ name: "current", type: "checkbox", defaultValue: false },
{
name: "bullets",
type: "array",
fields: [{ name: "text", type: "textarea" }],
},
],
},
{
name: "skills",
type: "array",
fields: [
{ name: "category", type: "text", required: true },
{
name: "items",
type: "array",
fields: [{ name: "name", type: "text" }],
},
],
},
{
name: "education",
type: "array",
fields: [
{ name: "institution", type: "text", required: true },
{ name: "credential", type: "text" },
{ name: "year", type: "text" },
],
},
],
};

25
src/globals/SEO.ts Normal file
View File

@@ -0,0 +1,25 @@
import type { GlobalConfig } from "payload";
export const SEO: GlobalConfig = {
slug: "seo",
access: { read: () => true },
fields: [
{ name: "siteTitle", type: "text", defaultValue: "Jason Woltje" },
{
name: "defaultDescription",
type: "textarea",
defaultValue:
"A multidisciplinary architect of digital ecosystems. Engineering growth through technological mastery and strategic leadership.",
},
{ name: "defaultOgImage", type: "upload", relationTo: "media" },
{ name: "twitterHandle", type: "text" },
{
name: "jsonLdPerson",
type: "json",
admin: {
description:
"Schema.org Person JSON-LD. Injected verbatim into the home page <head>.",
},
},
],
};

66
src/payload.config.ts Normal file
View File

@@ -0,0 +1,66 @@
import path from "node:path";
import { fileURLToPath } from "node:url";
import { buildConfig } from "payload";
import { postgresAdapter } from "@payloadcms/db-postgres";
import { lexicalEditor } from "@payloadcms/richtext-lexical";
import sharp from "sharp";
import { Users } from "@/collections/Users";
import { Media } from "@/collections/Media";
import { Categories } from "@/collections/Categories";
import { Projects } from "@/collections/Projects";
import { Posts } from "@/collections/Posts";
import { Gear } from "@/collections/Gear";
import { ContactSubmissions } from "@/collections/ContactSubmissions";
import { Home } from "@/globals/Home";
import { About } from "@/globals/About";
import { Contact } from "@/globals/Contact";
import { Resume } from "@/globals/Resume";
import { Navigation } from "@/globals/Navigation";
import { SEO } from "@/globals/SEO";
const filename = fileURLToPath(import.meta.url);
const dirname = path.dirname(filename);
export default buildConfig({
serverURL: process.env.PAYLOAD_PUBLIC_SERVER_URL,
secret: process.env.PAYLOAD_SECRET || "",
admin: {
user: "users",
meta: {
titleSuffix: "— Jason Woltje",
},
},
editor: lexicalEditor({}),
db: postgresAdapter({
pool: {
connectionString: process.env.DATABASE_URI,
},
}),
collections: [
Users,
Media,
Categories,
Projects,
Posts,
Gear,
ContactSubmissions,
],
globals: [Home, About, Contact, Resume, Navigation, SEO],
sharp,
typescript: {
outputFile: path.resolve(dirname, "payload-types.ts"),
},
graphQL: {
schemaOutputFile: path.resolve(dirname, "generated-schema.graphql"),
},
});