Compare commits
3 Commits
fix/docker
...
feat/scaff
| Author | SHA1 | Date | |
|---|---|---|---|
| d5c3145440 | |||
| 7ae8d18b7d | |||
| 462d938297 |
19
.dockerignore
Normal file
19
.dockerignore
Normal file
@@ -0,0 +1,19 @@
|
||||
**/node_modules
|
||||
**/.next
|
||||
**/dist
|
||||
**/coverage
|
||||
.git
|
||||
.gitignore
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
*.log
|
||||
Dockerfile*
|
||||
docker-compose*.yml
|
||||
.woodpecker
|
||||
.mosaic
|
||||
docs
|
||||
README.md
|
||||
LICENSE
|
||||
design-samples
|
||||
images
|
||||
48
.env.example
Normal file
48
.env.example
Normal file
@@ -0,0 +1,48 @@
|
||||
# =============================================================================
|
||||
# jasonwoltje.com — environment variables
|
||||
# =============================================================================
|
||||
# Actual values live in Portainer stack env vars (prod) or .env (local dev).
|
||||
# Never commit .env — see .gitignore.
|
||||
|
||||
# ---- Payload / Database ----
|
||||
# Local dev convenience: pnpm dev reads these directly.
|
||||
# In prod, DATABASE_URI is composed in docker-compose.swarm.yml from PAYLOAD_POSTGRES_*.
|
||||
DATABASE_URI=postgres://payload:payload@localhost:5432/payload
|
||||
PAYLOAD_POSTGRES_USER=payload
|
||||
PAYLOAD_POSTGRES_PASSWORD=replace-me-local-only
|
||||
PAYLOAD_POSTGRES_DB=payload
|
||||
PAYLOAD_SECRET=replace-with-32-plus-char-random-string
|
||||
|
||||
# ---- Public URLs ----
|
||||
PAYLOAD_PUBLIC_SERVER_URL=http://localhost:3000
|
||||
NEXT_PUBLIC_SITE_URL=http://localhost:3000
|
||||
|
||||
# ---- Build-time metadata (Status Terminal) ----
|
||||
# CI overrides these during docker-build; local dev falls back to "dev" / "local".
|
||||
NEXT_PUBLIC_BUILD_SHA=dev
|
||||
NEXT_PUBLIC_BUILD_REV=local
|
||||
|
||||
# ---- Cloudflare Turnstile (contact form CAPTCHA) ----
|
||||
TURNSTILE_SITE_KEY=
|
||||
TURNSTILE_SECRET_KEY=
|
||||
|
||||
# ---- Umami analytics (self-hosted; empty disables tracker) ----
|
||||
NEXT_PUBLIC_UMAMI_SRC=
|
||||
NEXT_PUBLIC_UMAMI_WEBSITE_ID=
|
||||
|
||||
# ---- Contact form email (choose one path) ----
|
||||
# Option A: Resend
|
||||
RESEND_API_KEY=
|
||||
RESEND_FROM=no-reply@jasonwoltje.com
|
||||
RESEND_TO=jason@diversecanvas.com
|
||||
|
||||
# Option B: SMTP relay
|
||||
# SMTP_HOST=
|
||||
# SMTP_PORT=587
|
||||
# SMTP_USER=
|
||||
# SMTP_PASSWORD=
|
||||
# SMTP_FROM=no-reply@jasonwoltje.com
|
||||
# SMTP_TO=jason@diversecanvas.com
|
||||
|
||||
# ---- Mautic newsletter (not deployed yet; leave empty) ----
|
||||
MAUTIC_FORM_URL=
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -59,3 +59,7 @@ scripts/local/
|
||||
|
||||
# design samples zip (HTML + DESIGN.md kept as reference)
|
||||
design-samples/*.zip
|
||||
|
||||
# QA/automation auto-generated reports (runtime artifacts)
|
||||
docs/reports/qa-automation/
|
||||
docs/reports/typecheck/
|
||||
|
||||
155
.woodpecker/web.yml
Normal file
155
.woodpecker/web.yml
Normal file
@@ -0,0 +1,155 @@
|
||||
when:
|
||||
- event: [push, pull_request, manual]
|
||||
- event: tag
|
||||
|
||||
variables:
|
||||
- &node_image "node:24-alpine"
|
||||
- &install_deps |
|
||||
corepack enable
|
||||
pnpm config set store-dir /tmp/pnpm-store
|
||||
pnpm install --frozen-lockfile
|
||||
- &enable_pnpm |
|
||||
corepack enable
|
||||
pnpm config set store-dir /tmp/pnpm-store
|
||||
- &kaniko_setup |
|
||||
mkdir -p /kaniko/.docker
|
||||
echo "{\"auths\":{\"git.mosaicstack.dev\":{\"username\":\"$$GITEA_USER\",\"password\":\"$$GITEA_TOKEN\"}}}" > /kaniko/.docker/config.json
|
||||
|
||||
steps:
|
||||
install:
|
||||
image: *node_image
|
||||
commands:
|
||||
- *install_deps
|
||||
|
||||
lint:
|
||||
image: *node_image
|
||||
commands:
|
||||
- *enable_pnpm
|
||||
- pnpm lint
|
||||
depends_on:
|
||||
- install
|
||||
|
||||
typecheck:
|
||||
image: *node_image
|
||||
commands:
|
||||
- *enable_pnpm
|
||||
- pnpm typecheck
|
||||
depends_on:
|
||||
- install
|
||||
|
||||
build:
|
||||
image: *node_image
|
||||
environment:
|
||||
NODE_ENV: "production"
|
||||
NEXT_PUBLIC_BUILD_SHA: ${CI_COMMIT_SHA:0:8}
|
||||
NEXT_PUBLIC_BUILD_REV: ${CI_COMMIT_BRANCH:-${CI_COMMIT_TAG}}
|
||||
commands:
|
||||
- *enable_pnpm
|
||||
- pnpm build
|
||||
depends_on:
|
||||
- lint
|
||||
- typecheck
|
||||
|
||||
security-audit:
|
||||
image: *node_image
|
||||
commands:
|
||||
- *enable_pnpm
|
||||
- pnpm audit --prod --audit-level=high || true
|
||||
depends_on:
|
||||
- install
|
||||
|
||||
docker-build:
|
||||
image: gcr.io/kaniko-project/executor:debug
|
||||
environment:
|
||||
GITEA_USER:
|
||||
from_secret: gitea_username
|
||||
GITEA_TOKEN:
|
||||
from_secret: gitea_token
|
||||
CI_COMMIT_BRANCH: ${CI_COMMIT_BRANCH}
|
||||
CI_COMMIT_TAG: ${CI_COMMIT_TAG}
|
||||
CI_COMMIT_SHA: ${CI_COMMIT_SHA}
|
||||
commands:
|
||||
- *kaniko_setup
|
||||
- |
|
||||
set -e
|
||||
IMAGE="git.mosaicstack.dev/jason.woltje/professional-website"
|
||||
SHORT_SHA="$${CI_COMMIT_SHA:0:8}"
|
||||
DESTINATIONS="--destination $$IMAGE:sha-$$SHORT_SHA"
|
||||
if [ -n "$$CI_COMMIT_TAG" ]; then
|
||||
DESTINATIONS="$$DESTINATIONS --destination $$IMAGE:$$CI_COMMIT_TAG"
|
||||
fi
|
||||
if [ "$$CI_COMMIT_BRANCH" = "main" ]; then
|
||||
DESTINATIONS="$$DESTINATIONS --destination $$IMAGE:latest"
|
||||
elif [ "$$CI_COMMIT_BRANCH" = "develop" ]; then
|
||||
DESTINATIONS="$$DESTINATIONS --destination $$IMAGE:dev"
|
||||
fi
|
||||
/kaniko/executor \
|
||||
--context . \
|
||||
--dockerfile Dockerfile \
|
||||
--build-arg NEXT_PUBLIC_BUILD_SHA=sha-$$SHORT_SHA \
|
||||
--build-arg NEXT_PUBLIC_BUILD_REV=$${CI_COMMIT_TAG:-$$CI_COMMIT_BRANCH} \
|
||||
$$DESTINATIONS
|
||||
when:
|
||||
- branch: [main, develop]
|
||||
event: [push, manual]
|
||||
- event: tag
|
||||
depends_on:
|
||||
- build
|
||||
- security-audit
|
||||
|
||||
security-trivy:
|
||||
image: aquasec/trivy:latest
|
||||
environment:
|
||||
GITEA_USER:
|
||||
from_secret: gitea_username
|
||||
GITEA_TOKEN:
|
||||
from_secret: gitea_token
|
||||
CI_COMMIT_BRANCH: ${CI_COMMIT_BRANCH}
|
||||
CI_COMMIT_TAG: ${CI_COMMIT_TAG}
|
||||
CI_COMMIT_SHA: ${CI_COMMIT_SHA}
|
||||
commands:
|
||||
- |
|
||||
set -e
|
||||
IMAGE="git.mosaicstack.dev/jason.woltje/professional-website"
|
||||
if [ -n "$$CI_COMMIT_TAG" ]; then
|
||||
SCAN_TAG="$$CI_COMMIT_TAG"
|
||||
else
|
||||
SCAN_TAG="sha-$${CI_COMMIT_SHA:0:8}"
|
||||
fi
|
||||
mkdir -p ~/.docker
|
||||
echo "{\"auths\":{\"git.mosaicstack.dev\":{\"username\":\"$$GITEA_USER\",\"password\":\"$$GITEA_TOKEN\"}}}" > ~/.docker/config.json
|
||||
trivy image --exit-code 1 --severity HIGH,CRITICAL --ignore-unfixed \
|
||||
$$IMAGE:$$SCAN_TAG
|
||||
when:
|
||||
- branch: [main, develop]
|
||||
event: [push, manual]
|
||||
- event: tag
|
||||
depends_on:
|
||||
- docker-build
|
||||
|
||||
link-package:
|
||||
image: alpine:3
|
||||
environment:
|
||||
GITEA_TOKEN:
|
||||
from_secret: gitea_token
|
||||
commands:
|
||||
- apk add --no-cache curl
|
||||
- |
|
||||
set -e
|
||||
STATUS=$$(curl -s -o /dev/null -w "%{http_code}" -X POST \
|
||||
-H "Authorization: token $$GITEA_TOKEN" \
|
||||
"https://git.mosaicstack.dev/api/v1/packages/jason.woltje/container/professional-website/-/link/professional-website")
|
||||
if [ "$$STATUS" = "201" ] || [ "$$STATUS" = "204" ]; then
|
||||
echo "Package linked"
|
||||
elif [ "$$STATUS" = "400" ]; then
|
||||
echo "Package already linked (OK)"
|
||||
else
|
||||
echo "Unexpected response: $$STATUS"
|
||||
exit 1
|
||||
fi
|
||||
when:
|
||||
- branch: [main, develop]
|
||||
event: [push, manual]
|
||||
- event: tag
|
||||
depends_on:
|
||||
- security-trivy
|
||||
51
Dockerfile
Normal file
51
Dockerfile
Normal file
@@ -0,0 +1,51 @@
|
||||
# syntax=docker/dockerfile:1.7
|
||||
# =============================================================================
|
||||
# jasonwoltje.com — Next.js 16 + Payload 3 production image
|
||||
# Multi-stage, non-root, standalone Next output. Built by Kaniko in Woodpecker.
|
||||
# =============================================================================
|
||||
|
||||
ARG NODE_VERSION=24-alpine
|
||||
|
||||
# ---- deps ----
|
||||
FROM node:${NODE_VERSION} AS deps
|
||||
RUN apk add --no-cache libc6-compat
|
||||
WORKDIR /app
|
||||
RUN corepack enable && corepack prepare pnpm@10.31.0 --activate
|
||||
COPY package.json pnpm-lock.yaml* ./
|
||||
RUN --mount=type=cache,target=/pnpm-store \
|
||||
pnpm config set store-dir /pnpm-store && \
|
||||
pnpm install --frozen-lockfile
|
||||
|
||||
# ---- build ----
|
||||
FROM node:${NODE_VERSION} AS build
|
||||
WORKDIR /app
|
||||
RUN corepack enable && corepack prepare pnpm@10.31.0 --activate
|
||||
COPY --from=deps /app/node_modules ./node_modules
|
||||
COPY . .
|
||||
ARG NEXT_PUBLIC_BUILD_SHA=unknown
|
||||
ARG NEXT_PUBLIC_BUILD_REV=unknown
|
||||
ENV NEXT_PUBLIC_BUILD_SHA=${NEXT_PUBLIC_BUILD_SHA} \
|
||||
NEXT_PUBLIC_BUILD_REV=${NEXT_PUBLIC_BUILD_REV} \
|
||||
NEXT_TELEMETRY_DISABLED=1 \
|
||||
NODE_ENV=production
|
||||
RUN pnpm build
|
||||
|
||||
# ---- runner ----
|
||||
FROM node:${NODE_VERSION} AS runner
|
||||
WORKDIR /app
|
||||
ENV NODE_ENV=production \
|
||||
NEXT_TELEMETRY_DISABLED=1 \
|
||||
PORT=3000 \
|
||||
HOSTNAME=0.0.0.0
|
||||
RUN addgroup -g 1001 -S nodejs && adduser -S -u 1001 -G nodejs nextjs
|
||||
RUN apk add --no-cache wget && \
|
||||
mkdir -p /app/media && \
|
||||
chown -R nextjs:nodejs /app
|
||||
COPY --from=build --chown=nextjs:nodejs /app/public ./public
|
||||
COPY --from=build --chown=nextjs:nodejs /app/.next/standalone ./
|
||||
COPY --from=build --chown=nextjs:nodejs /app/.next/static ./.next/static
|
||||
USER nextjs
|
||||
EXPOSE 3000
|
||||
HEALTHCHECK --interval=30s --timeout=5s --retries=3 --start-period=45s \
|
||||
CMD wget -qO- http://127.0.0.1:3000/api/health || exit 1
|
||||
CMD ["node", "server.js"]
|
||||
110
docker-compose.swarm.yml
Normal file
110
docker-compose.swarm.yml
Normal file
@@ -0,0 +1,110 @@
|
||||
# =============================================================================
|
||||
# jasonwoltje.com — Docker Swarm / Portainer stack
|
||||
# =============================================================================
|
||||
#
|
||||
# Deploy target: w-docker0 (10.1.1.45), Portainer endpoint 7.
|
||||
# Routing:
|
||||
# jasonwoltje.com / www.jasonwoltje.com -> web (Next.js + Payload CMS)
|
||||
#
|
||||
# Ingress pattern (mirrors MosaicStack):
|
||||
# Edge Traefik (10.1.1.43) terminates TLS
|
||||
# -> per-swarm Traefik on w-docker0 on entrypoint "web" (HTTP)
|
||||
# -> web:3000
|
||||
#
|
||||
# Usage (Portainer):
|
||||
# Stacks -> Add Stack -> Git repository
|
||||
# URL: https://git.mosaicstack.dev/jason.woltje/professional-website
|
||||
# Compose path: docker-compose.swarm.yml
|
||||
# Env vars: see .env.example (all required unless marked optional)
|
||||
# Deploy
|
||||
#
|
||||
# Image tag rule: WEB_IMAGE_TAG MUST be an immutable tag (sha-<8> or vX.Y.Z).
|
||||
# Never point this stack at `latest`.
|
||||
# =============================================================================
|
||||
|
||||
services:
|
||||
web:
|
||||
image: git.mosaicstack.dev/jason.woltje/professional-website:${WEB_IMAGE_TAG}
|
||||
environment:
|
||||
DATABASE_URI: postgresql://${PAYLOAD_POSTGRES_USER}:${PAYLOAD_POSTGRES_PASSWORD}@jasonwoltje_postgres:5432/${PAYLOAD_POSTGRES_DB}
|
||||
PAYLOAD_SECRET: ${PAYLOAD_SECRET}
|
||||
PAYLOAD_PUBLIC_SERVER_URL: https://${SITE_DOMAIN:-jasonwoltje.com}
|
||||
NEXT_PUBLIC_SITE_URL: https://${SITE_DOMAIN:-jasonwoltje.com}
|
||||
NEXT_PUBLIC_BUILD_SHA: ${WEB_IMAGE_TAG}
|
||||
NEXT_PUBLIC_BUILD_REV: ${WEB_IMAGE_TAG}
|
||||
TURNSTILE_SITE_KEY: ${TURNSTILE_SITE_KEY:-}
|
||||
TURNSTILE_SECRET_KEY: ${TURNSTILE_SECRET_KEY:-}
|
||||
NEXT_PUBLIC_UMAMI_SRC: ${NEXT_PUBLIC_UMAMI_SRC:-}
|
||||
NEXT_PUBLIC_UMAMI_WEBSITE_ID: ${NEXT_PUBLIC_UMAMI_WEBSITE_ID:-}
|
||||
RESEND_API_KEY: ${RESEND_API_KEY:-}
|
||||
RESEND_FROM: ${RESEND_FROM:-no-reply@jasonwoltje.com}
|
||||
RESEND_TO: ${RESEND_TO:-jason@diversecanvas.com}
|
||||
MAUTIC_FORM_URL: ${MAUTIC_FORM_URL:-}
|
||||
volumes:
|
||||
- media:/app/media
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:3000/api/health || exit 1"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 60s
|
||||
networks:
|
||||
- internal
|
||||
- traefik-public
|
||||
deploy:
|
||||
replicas: 1
|
||||
update_config:
|
||||
parallelism: 1
|
||||
delay: 30s
|
||||
order: start-first
|
||||
restart_policy:
|
||||
condition: on-failure
|
||||
delay: 5s
|
||||
max_attempts: 5
|
||||
window: 120s
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.http.routers.jasonwoltje.rule=Host(`${SITE_DOMAIN:-jasonwoltje.com}`) || Host(`www.${SITE_DOMAIN:-jasonwoltje.com}`)"
|
||||
- "traefik.http.routers.jasonwoltje.entrypoints=web"
|
||||
- "traefik.http.services.jasonwoltje.loadbalancer.server.port=3000"
|
||||
# www -> apex 301
|
||||
- "traefik.http.middlewares.jasonwoltje-www-redirect.redirectregex.regex=^https?://www\\.${SITE_DOMAIN:-jasonwoltje.com}/(.*)"
|
||||
- "traefik.http.middlewares.jasonwoltje-www-redirect.redirectregex.replacement=https://${SITE_DOMAIN:-jasonwoltje.com}/$${1}"
|
||||
- "traefik.http.middlewares.jasonwoltje-www-redirect.redirectregex.permanent=true"
|
||||
- "traefik.http.routers.jasonwoltje.middlewares=jasonwoltje-www-redirect"
|
||||
|
||||
postgres:
|
||||
image: postgres:17-alpine
|
||||
environment:
|
||||
POSTGRES_DB: ${PAYLOAD_POSTGRES_DB:-payload}
|
||||
POSTGRES_USER: ${PAYLOAD_POSTGRES_USER:-payload}
|
||||
POSTGRES_PASSWORD: ${PAYLOAD_POSTGRES_PASSWORD}
|
||||
volumes:
|
||||
- postgres-data:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER}"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
start_period: 30s
|
||||
networks:
|
||||
- internal
|
||||
deploy:
|
||||
replicas: 1
|
||||
restart_policy:
|
||||
condition: on-failure
|
||||
delay: 5s
|
||||
max_attempts: 5
|
||||
placement:
|
||||
constraints:
|
||||
- node.role == manager
|
||||
|
||||
volumes:
|
||||
postgres-data:
|
||||
media:
|
||||
|
||||
networks:
|
||||
internal:
|
||||
driver: overlay
|
||||
traefik-public:
|
||||
external: true
|
||||
29
docker-compose.yml
Normal file
29
docker-compose.yml
Normal file
@@ -0,0 +1,29 @@
|
||||
# =============================================================================
|
||||
# jasonwoltje.com — local dev Postgres
|
||||
# =============================================================================
|
||||
# Brings up just Postgres for local `pnpm dev`. The Next app runs on the host
|
||||
# via `pnpm dev` (not in-container) for fast iteration.
|
||||
#
|
||||
# docker compose up -d # start Postgres
|
||||
# pnpm dev # run Next + Payload on host
|
||||
# =============================================================================
|
||||
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:17-alpine
|
||||
environment:
|
||||
POSTGRES_DB: ${PAYLOAD_POSTGRES_DB:-payload}
|
||||
POSTGRES_USER: ${PAYLOAD_POSTGRES_USER:-payload}
|
||||
POSTGRES_PASSWORD: ${PAYLOAD_POSTGRES_PASSWORD:-payload}
|
||||
ports:
|
||||
- "5432:5432"
|
||||
volumes:
|
||||
- postgres-data:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER}"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
volumes:
|
||||
postgres-data:
|
||||
18
eslint.config.mjs
Normal file
18
eslint.config.mjs
Normal file
@@ -0,0 +1,18 @@
|
||||
import nextCoreWebVitals from "eslint-config-next/core-web-vitals";
|
||||
import nextTypescript from "eslint-config-next/typescript";
|
||||
|
||||
const config = [
|
||||
...nextCoreWebVitals,
|
||||
...nextTypescript,
|
||||
{
|
||||
ignores: [
|
||||
".next/**",
|
||||
"node_modules/**",
|
||||
"dist/**",
|
||||
"src/payload-types.ts",
|
||||
"src/app/(payload)/admin/importMap.js",
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export default config;
|
||||
6
next-env.d.ts
vendored
Normal file
6
next-env.d.ts
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
/// <reference types="next" />
|
||||
/// <reference types="next/image-types/global" />
|
||||
import "./.next/types/routes.d.ts";
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
||||
21
next.config.ts
Normal file
21
next.config.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { withPayload } from "@payloadcms/next/withPayload";
|
||||
import type { NextConfig } from "next";
|
||||
import path from "node:path";
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
output: "standalone",
|
||||
reactStrictMode: true,
|
||||
turbopack: {
|
||||
root: path.resolve("."),
|
||||
},
|
||||
images: {
|
||||
remotePatterns: [
|
||||
{
|
||||
protocol: "https",
|
||||
hostname: "jasonwoltje.com",
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export default withPayload(nextConfig, { devBundleServerPackages: false });
|
||||
50
package.json
Normal file
50
package.json
Normal file
@@ -0,0 +1,50 @@
|
||||
{
|
||||
"name": "professional-website",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"description": "Jason Woltje professional website — Payload CMS 3 + Next.js 16",
|
||||
"license": "UNLICENSED",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "eslint .",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"payload": "payload",
|
||||
"generate:types": "payload generate:types",
|
||||
"generate:importmap": "payload generate:importmap",
|
||||
"test": "echo \"no tests yet\" && exit 0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@payloadcms/db-postgres": "^3.50.0",
|
||||
"@payloadcms/next": "^3.50.0",
|
||||
"@payloadcms/richtext-lexical": "^3.50.0",
|
||||
"@payloadcms/ui": "^3.50.0",
|
||||
"graphql": "^16.11.0",
|
||||
"lucide-react": "^0.468.0",
|
||||
"next": "16.2.2",
|
||||
"payload": "^3.50.0",
|
||||
"react": "19.2.0",
|
||||
"react-dom": "19.2.0",
|
||||
"sharp": "^0.34.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/eslintrc": "^3.3.5",
|
||||
"@tailwindcss/container-queries": "^0.1.1",
|
||||
"@tailwindcss/forms": "^0.5.10",
|
||||
"@types/node": "^22.10.0",
|
||||
"@types/react": "^19.0.0",
|
||||
"@types/react-dom": "^19.0.0",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"eslint": "^9.18.0",
|
||||
"eslint-config-next": "16.1.6",
|
||||
"postcss": "^8.5.0",
|
||||
"tailwindcss": "^3.4.17",
|
||||
"typescript": "^5.7.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.9.0"
|
||||
},
|
||||
"packageManager": "pnpm@10.31.0"
|
||||
}
|
||||
7645
pnpm-lock.yaml
generated
Normal file
7645
pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
8
postcss.config.js
Normal file
8
postcss.config.js
Normal file
@@ -0,0 +1,8 @@
|
||||
const config = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
27
src/app/(frontend)/about/page.tsx
Normal file
27
src/app/(frontend)/about/page.tsx
Normal 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 />
|
||||
</>
|
||||
);
|
||||
}
|
||||
26
src/app/(frontend)/contact/page.tsx
Normal file
26
src/app/(frontend)/contact/page.tsx
Normal 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 />
|
||||
</>
|
||||
);
|
||||
}
|
||||
52
src/app/(frontend)/globals.css
Normal file
52
src/app/(frontend)/globals.css
Normal 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%);
|
||||
}
|
||||
}
|
||||
57
src/app/(frontend)/layout.tsx
Normal file
57
src/app/(frontend)/layout.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
40
src/app/(frontend)/page.tsx
Normal file
40
src/app/(frontend)/page.tsx
Normal 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 />
|
||||
</>
|
||||
);
|
||||
}
|
||||
45
src/app/(frontend)/projects/[slug]/page.tsx
Normal file
45
src/app/(frontend)/projects/[slug]/page.tsx
Normal 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 />
|
||||
</>
|
||||
);
|
||||
}
|
||||
26
src/app/(frontend)/projects/page.tsx
Normal file
26
src/app/(frontend)/projects/page.tsx
Normal 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 />
|
||||
</>
|
||||
);
|
||||
}
|
||||
28
src/app/(frontend)/resume/page.tsx
Normal file
28
src/app/(frontend)/resume/page.tsx
Normal 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 />
|
||||
</>
|
||||
);
|
||||
}
|
||||
43
src/app/(frontend)/writing/[slug]/page.tsx
Normal file
43
src/app/(frontend)/writing/[slug]/page.tsx
Normal 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 />
|
||||
</>
|
||||
);
|
||||
}
|
||||
25
src/app/(frontend)/writing/page.tsx
Normal file
25
src/app/(frontend)/writing/page.tsx
Normal 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 />
|
||||
</>
|
||||
);
|
||||
}
|
||||
18
src/app/(payload)/admin/[[...segments]]/not-found.tsx
Normal file
18
src/app/(payload)/admin/[[...segments]]/not-found.tsx
Normal 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;
|
||||
18
src/app/(payload)/admin/[[...segments]]/page.tsx
Normal file
18
src/app/(payload)/admin/[[...segments]]/page.tsx
Normal 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;
|
||||
2
src/app/(payload)/admin/importMap.js
Normal file
2
src/app/(payload)/admin/importMap.js
Normal file
@@ -0,0 +1,2 @@
|
||||
// Auto-generated stub. Regenerate with `pnpm generate:importmap` after adding custom components.
|
||||
export const importMap = {};
|
||||
16
src/app/(payload)/api/[...slug]/route.ts
Normal file
16
src/app/(payload)/api/[...slug]/route.ts
Normal 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);
|
||||
4
src/app/(payload)/api/graphql-playground/route.ts
Normal file
4
src/app/(payload)/api/graphql-playground/route.ts
Normal 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);
|
||||
5
src/app/(payload)/api/graphql/route.ts
Normal file
5
src/app/(payload)/api/graphql/route.ts
Normal 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);
|
||||
16
src/app/(payload)/api/health/route.ts
Normal file
16
src/app/(payload)/api/health/route.ts
Normal 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 },
|
||||
);
|
||||
}
|
||||
1
src/app/(payload)/custom.scss
Normal file
1
src/app/(payload)/custom.scss
Normal file
@@ -0,0 +1 @@
|
||||
/* Payload admin UI customizations. Keep minimal for v0.0.x. */
|
||||
22
src/app/(payload)/layout.tsx
Normal file
22
src/app/(payload)/layout.tsx
Normal 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;
|
||||
21
src/collections/Categories.ts
Normal file
21
src/collections/Categories.ts
Normal 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" },
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
33
src/collections/ContactSubmissions.ts
Normal file
33
src/collections/ContactSubmissions.ts
Normal 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
36
src/collections/Gear.ts
Normal 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
26
src/collections/Media.ts
Normal 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
43
src/collections/Posts.ts
Normal 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" },
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
64
src/collections/Projects.ts
Normal file
64
src/collections/Projects.ts
Normal 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
21
src/collections/Users.ts
Normal 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",
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
14
src/components/SiteFooter.tsx
Normal file
14
src/components/SiteFooter.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
35
src/components/SiteHeader.tsx
Normal file
35
src/components/SiteHeader.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
23
src/components/StatusTerminal.tsx
Normal file
23
src/components/StatusTerminal.tsx
Normal 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
27
src/globals/About.ts
Normal 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
33
src/globals/Contact.ts
Normal 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
35
src/globals/Home.ts
Normal 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
21
src/globals/Navigation.ts
Normal 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
46
src/globals/Resume.ts
Normal 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
25
src/globals/SEO.ts
Normal 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
66
src/payload.config.ts
Normal 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"),
|
||||
},
|
||||
});
|
||||
102
tailwind.config.ts
Normal file
102
tailwind.config.ts
Normal file
@@ -0,0 +1,102 @@
|
||||
import type { Config } from "tailwindcss";
|
||||
import forms from "@tailwindcss/forms";
|
||||
import containerQueries from "@tailwindcss/container-queries";
|
||||
|
||||
// Tokens ported verbatim from design-samples/stitch_jasonwoltje.com/*/code.html
|
||||
// Full token rationale in design-samples/stitch_jasonwoltje.com/silicon_ethos/DESIGN.md
|
||||
const stitchColors = {
|
||||
primary: "#81ecff",
|
||||
"primary-dim": "#00d4ec",
|
||||
"primary-fixed": "#00e3fd",
|
||||
"primary-fixed-dim": "#00d4ec",
|
||||
"primary-container": "#00e3fd",
|
||||
"on-primary": "#005762",
|
||||
"on-primary-container": "#004d57",
|
||||
"on-primary-fixed": "#003840",
|
||||
"on-primary-fixed-variant": "#005762",
|
||||
|
||||
secondary: "#d873ff",
|
||||
"secondary-dim": "#bc00fb",
|
||||
"secondary-fixed": "#f1c1ff",
|
||||
"secondary-fixed-dim": "#ebadff",
|
||||
"secondary-container": "#9900ce",
|
||||
"on-secondary": "#39004f",
|
||||
"on-secondary-container": "#fff5fc",
|
||||
"on-secondary-fixed": "#580078",
|
||||
"on-secondary-fixed-variant": "#8400b2",
|
||||
|
||||
tertiary: "#8eff71",
|
||||
"tertiary-dim": "#2be800",
|
||||
"tertiary-fixed": "#2ff801",
|
||||
"tertiary-fixed-dim": "#2be800",
|
||||
"tertiary-container": "#2ff801",
|
||||
"on-tertiary": "#0d6100",
|
||||
"on-tertiary-container": "#0b5800",
|
||||
"on-tertiary-fixed": "#064200",
|
||||
"on-tertiary-fixed-variant": "#0d6200",
|
||||
|
||||
error: "#ff716c",
|
||||
"error-dim": "#d7383b",
|
||||
"error-container": "#9f0519",
|
||||
"on-error": "#490006",
|
||||
"on-error-container": "#ffa8a3",
|
||||
|
||||
background: "#0d0e12",
|
||||
"on-background": "#f7f5fc",
|
||||
|
||||
surface: "#0d0e12",
|
||||
"surface-dim": "#0d0e12",
|
||||
"surface-bright": "#2a2c32",
|
||||
"surface-variant": "#24252c",
|
||||
"surface-tint": "#81ecff",
|
||||
"surface-container-lowest": "#000000",
|
||||
"surface-container-low": "#121318",
|
||||
"surface-container": "#18191e",
|
||||
"surface-container-high": "#1e1f25",
|
||||
"surface-container-highest": "#24252c",
|
||||
"on-surface": "#f7f5fc",
|
||||
"on-surface-variant": "#abaab0",
|
||||
|
||||
"inverse-surface": "#faf8ff",
|
||||
"inverse-on-surface": "#54555a",
|
||||
"inverse-primary": "#006976",
|
||||
|
||||
outline: "#75757a",
|
||||
"outline-variant": "#47484d",
|
||||
};
|
||||
|
||||
const config: Config = {
|
||||
darkMode: "class",
|
||||
content: ["./src/**/*.{ts,tsx,mdx}"],
|
||||
theme: {
|
||||
extend: {
|
||||
colors: stitchColors,
|
||||
borderRadius: {
|
||||
DEFAULT: "0.125rem",
|
||||
sm: "0.125rem",
|
||||
md: "0.375rem",
|
||||
lg: "0.25rem",
|
||||
xl: "0.5rem",
|
||||
"2xl": "0.75rem",
|
||||
full: "9999px",
|
||||
},
|
||||
fontFamily: {
|
||||
headline: ["var(--font-headline)", "Space Grotesk", "sans-serif"],
|
||||
body: ["var(--font-body)", "Inter", "sans-serif"],
|
||||
label: ["var(--font-label)", "Space Grotesk", "monospace"],
|
||||
},
|
||||
boxShadow: {
|
||||
"neon-primary":
|
||||
"0 0 32px 4px rgba(129, 236, 255, 0.25), 0 0 4px 1px rgba(129, 236, 255, 0.5)",
|
||||
"neon-secondary":
|
||||
"0 0 32px 4px rgba(216, 115, 255, 0.25), 0 0 4px 1px rgba(216, 115, 255, 0.5)",
|
||||
},
|
||||
backdropBlur: {
|
||||
"2xl": "24px",
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [forms, containerQueries],
|
||||
};
|
||||
|
||||
export default config;
|
||||
47
tsconfig.json
Normal file
47
tsconfig.json
Normal file
@@ -0,0 +1,47 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"lib": [
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"esnext"
|
||||
],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "react-jsx",
|
||||
"incremental": true,
|
||||
"plugins": [
|
||||
{
|
||||
"name": "next"
|
||||
}
|
||||
],
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": [
|
||||
"./src/*"
|
||||
],
|
||||
"@payload-config": [
|
||||
"./src/payload.config.ts"
|
||||
]
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
"next-env.d.ts",
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
".next/types/**/*.ts",
|
||||
".next/dev/types/**/*.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
".next",
|
||||
"dist"
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user