refactor: large refactor around monorepo

Just a commit point while I'm testing stuff. Already decided at this
point to simplify and revert away from PayloadCMS.
This commit is contained in:
2026-01-04 13:18:34 -06:00
parent 31b1804fc9
commit af81d8e048
110 changed files with 8392 additions and 12918 deletions
+88
View File
@@ -0,0 +1,88 @@
# Source control
.git
.github
.gitignore
# Environment files - STRICT - no .env files at all
.env*
!.env.example
**/.env*
# Dependencies (installed in container)
node_modules
**/node_modules
# Build outputs (built in container)
.next
.svelte-kit
build
dist
**/dist
**/.next
**/.svelte-kit
**/build
# Cache & temp
.turbo
**/.turbo
.cache
**/.cache
*.tsbuildinfo
# Logs
*.log
npm-debug.log*
yarn-debug.log*
pnpm-debug.log*
lerna-debug.log*
# OS files
.DS_Store
Thumbs.db
# IDE
.vscode
.idea
*.swp
*.swo
*~
# Testing - exclude all test files
coverage
.nyc_output
tests
**/tests
test
**/test
*.test.*
*.spec.*
**/*.test.*
**/*.spec.*
playwright.config.*
playwright-report
test-results
vitest.config.*
vitest.setup.*
# Development configs
.editorconfig
.prettierrc*
.prettierignore
.eslintrc*
eslint.config.*
# Development specific
*.local
tmp
temp
*.tmp
# Documentation and project files
*.md
CHANGELOG*
LICENSE*
docs
**/docs
# Build artifacts
docker-build.log
+12 -19
View File
@@ -1,23 +1,16 @@
# Environment variables for xevion.dev
# Dev defaults are available - no .env file needed for basic development
# See schema in /env/schema.mjs
# Database
DATABASE_URL=postgresql://user:password@localhost:5432/xevion
# Payload CMS (dev defaults match docker-compose credentials)
PAYLOAD_SECRET=your-secret-key-here
DATABASE_URI=postgresql://xevion:xevion_dev_password@localhost:5432/xevion_dev
# Payload CMS
PAYLOAD_SECRET=development-secret-change-in-production
# Optional - enables /api/cron/updated endpoint
GITHUB_API_TOKEN=
# R2 Storage (optional for local dev)
R2_ENDPOINT=https://YOUR_ACCOUNT_ID.r2.cloudflarestorage.com
R2_BUCKET_NAME=xevion-media
R2_ACCESS_KEY_ID=your_access_key
R2_SECRET_ACCESS_KEY=your_secret_key
# Optional - enables /api/healthcheck endpoint
HEALTHCHECK_SECRET=
# Optional - auth for /api/cron/updated (skipped in dev)
CRON_SECRET=
# Optional
PAYLOAD_REVALIDATE_KEY=
TITLE=
# Auto-detected, usually don't need to set
# Application
NODE_ENV=development
PORT=3000
ORIGIN=http://localhost:3000
Vendored
+38 -36
View File
@@ -1,44 +1,46 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
.idea
.log*
# Dependencies
node_modules/
# dependencies
/node_modules
/.pnp
.pnp.js
# Build outputs
.next/
.svelte-kit/
build/
dist/
.turbo/
*.tsbuildinfo
# testing
/coverage
# Environment - only in root
/.env
/.env*.local
# database
/prisma/db.sqlite
/prisma/db.sqlite-journal
# next.js
/.next/
/out/
next-env.d.ts
# production
/build
# misc
.DS_Store
*.pem
# debug
# Logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
pnpm-debug.log*
# local env files
# do not commit any .env files to git, except for the .env.example file. https://create.t3.gg/en/usage/env-variables#using-environment-variables
.env
.env*.local
# OS
.DS_Store
Thumbs.db
# vercel
.vercel
# IDE
.vscode/
.idea/
*.swp
*.swo
*~
# typescript
*.tsbuildinfo
# Testing
coverage/
# Temporary
tmp/
temp/
*.tmp
# Playwright
playwright-report/
test-results/
# Docker
docker-build.log
-4
View File
@@ -1,4 +0,0 @@
{
"tabWidth": 2,
"useTabs": false
}
+88
View File
@@ -0,0 +1,88 @@
{
admin off
auto_https off
persist_config off
log {
format json
output stdout
level INFO
}
servers {
protocols h1 h2 h2c
timeouts {
read_body 10s
read_header 10s
write 30s
idle 120s
}
}
}
# Listen on Railway's PORT or default to 3000
:{$PORT:3000} {
# Security headers
header {
# HSTS (only in production with HTTPS)
Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
# Prevent clickjacking
X-Frame-Options "SAMEORIGIN"
# XSS Protection
X-Content-Type-Options "nosniff"
X-XSS-Protection "1; mode=block"
# Referrer policy
Referrer-Policy "strict-origin-when-cross-origin"
# Content Security Policy (adjust as needed)
Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; connect-src 'self' https:;"
# Permissions policy
Permissions-Policy "geolocation=(), microphone=(), camera=(), payment=(), usb=(), magnetometer=(), gyroscope=()"
# Remove server identification
-Server
-X-Powered-By
}
handle /admin* {
reverse_proxy localhost:5001 {
header_up Host {host}
header_up X-Real-IP {remote_host}
transport http {
read_timeout 60s
write_timeout 60s
}
}
}
handle {
reverse_proxy localhost:5000 {
header_up Host {host}
header_up X-Real-IP {remote_host}
transport http {
read_timeout 30s
write_timeout 30s
}
}
}
# Error handling
handle_errors {
@5xx expression `{http.error.status_code} >= 500`
handle @5xx {
respond "Service temporarily unavailable" 503 {
close
}
}
respond "{http.error.status_code} {http.error.status_text}" {
close
}
}
}
+61
View File
@@ -0,0 +1,61 @@
FROM oven/bun:1-alpine AS base
WORKDIR /app
RUN apk add --no-cache libc6-compat
# Install dependencies
FROM base AS deps
COPY package.json bun.lock ./
COPY packages/db/package.json ./packages/db/
COPY packages/types/package.json ./packages/types/
COPY packages/typescript-config/package.json ./packages/typescript-config/
COPY apps/payload/package.json ./apps/payload/
COPY apps/web/package.json ./apps/web/
RUN bun install --frozen-lockfile
# Build everything
FROM base AS builder
COPY --from=deps /app ./
COPY . .
ENV NEXT_TELEMETRY_DISABLED=1
RUN bun run build
# Compile SvelteKit to standalone executable
RUN bun build --compile --target=bun --minify --outfile=apps/web/web-server ./apps/web/build/index.js
# Production image
FROM oven/bun:1-alpine AS runner
WORKDIR /app
RUN apk add --no-cache caddy && \
addgroup --system --gid 1001 nodejs && \
adduser --system --uid 1001 nextjs
ENV NODE_ENV=production
ENV PORT=3000
ENV NEXT_TELEMETRY_DISABLED=1
# Copy built artifacts
COPY --from=builder --chown=nextjs:nodejs /app/packages/db/dist ./packages/db/dist
COPY --from=builder --chown=nextjs:nodejs /app/packages/db/package.json ./packages/db/
COPY --from=builder --chown=nextjs:nodejs /app/packages/types/dist ./packages/types/dist
COPY --from=builder --chown=nextjs:nodejs /app/packages/types/package.json ./packages/types/
# Payload standalone build
COPY --from=builder --chown=nextjs:nodejs /app/apps/payload/.next/standalone ./apps/payload/.next/standalone
COPY --from=builder --chown=nextjs:nodejs /app/apps/payload/.next/static ./apps/payload/.next/standalone/apps/payload/.next/static
COPY --from=builder --chown=nextjs:nodejs /app/apps/payload/public ./apps/payload/.next/standalone/apps/payload/public
COPY --from=builder --chown=nextjs:nodejs /app/apps/payload/src/migrations ./apps/payload/.next/standalone/apps/payload/src/migrations
# Web standalone executable (no node_modules needed)
COPY --from=builder --chown=nextjs:nodejs /app/apps/web/web-server ./apps/web/
COPY --from=builder --chown=nextjs:nodejs /app/apps/web/build/client ./apps/web/build/client
# Entrypoint
COPY --chown=nextjs:nodejs Caddyfile docker-entrypoint.ts ./
RUN chmod +x docker-entrypoint.ts && \
caddy fmt --overwrite Caddyfile
USER nextjs
EXPOSE ${PORT}
ENTRYPOINT ["bun", "run", "docker-entrypoint.ts"]
+148
View File
@@ -0,0 +1,148 @@
# Default environment variables
default_database_url := "postgresql://xevion:xevion@xevion-db:5432/xevion"
default_payload_secret := "development-secret-change-in-production"
network_name := "xevion-net"
# Build the Docker image
docker-build:
docker build -t xevion.dev .
# Run the Docker container (uses .env if available, otherwise defaults)
docker-run:
#!/bin/sh
docker network create {{network_name}} 2>/dev/null || true
if [ -f .env ]; then
echo "Loading environment from .env file..."
docker run -p 3000:3000 \
--network {{network_name}} \
-e DATABASE_URL="{{default_database_url}}" \
--env-file .env \
xevion.dev | hl -P
else
echo "No .env file found, using defaults..."
docker run -p 3000:3000 \
--network {{network_name}} \
-e DATABASE_URL="{{default_database_url}}" \
-e PAYLOAD_SECRET="{{default_payload_secret}}" \
xevion.dev | hl -P
fi
# Start the PostgreSQL database container
docker-db:
#!/bin/sh
docker network create {{network_name}} 2>/dev/null || true
docker run --name xevion-db \
--network {{network_name}} \
-p 5432:5432 \
-e POSTGRES_USER=xevion \
-e POSTGRES_PASSWORD=xevion \
-e POSTGRES_DB=xevion \
-d postgres
# Stop and remove the database container
docker-db-stop:
docker stop xevion-db && docker rm xevion-db
# Test Docker image with health checks
[script("bun")]
docker-test:
const $ = async (cmd: string[]) => Bun.spawn(cmd).exited;
const $out = (cmd: string[]) => Bun.spawnSync(cmd).stdout.toString();
// Ensure network exists (suppress error if already exists)
Bun.spawnSync(["docker", "network", "create", "{{network_name}}"], { stderr: "ignore" });
// Find available port
const used = new Set(
$out(["ss", "-tuln"]).split("\n").slice(1)
.map(l => l.match(/:(\d+)/)?.[1]).filter(Boolean)
);
const ranges = [[49152, 65535], [10000, 32767], [5000, 9999]];
const available = ranges
.flatMap(([s, e]) => Array.from({ length: e - s + 1 }, (_, i) => s + i))
.filter(p => !used.has(String(p)));
const port = available[~~(Math.random() * available.length)];
console.log(`Using port ${port}`);
// Start container
const container = `xevion-test-${port}`;
const dbUrl = "{{default_database_url}}";
const secret = "{{default_payload_secret}}";
await $([
"docker", "run", "-d", "--name", container,
"--network", "{{network_name}}",
"-p", `${port}:3000`,
"-e", `DATABASE_URL=${dbUrl}`,
"-e", `PAYLOAD_SECRET=${secret}`,
"xevion.dev"
]);
const cleanup = async () => {
console.log("\nCleaning up...");
await $(["docker", "rm", "-f", container]);
};
process.on("SIGINT", async () => { await cleanup(); process.exit(1); });
process.on("SIGTERM", async () => { await cleanup(); process.exit(1); });
const base = `http://localhost:${port}`;
// Poll until success or timeout
const poll = async (
fn: () => Promise<boolean>,
{ timeout = 5000, interval = 1000 } = {}
): Promise<boolean> => {
const start = Date.now();
while (Date.now() - start < timeout) {
if (await fn()) return true;
await Bun.sleep(interval);
}
return false;
};
// Test a route with retries
const test = async (path: string, expect: { status?: number; contains?: string } = {}) => {
const { status = 200, contains } = expect;
let last = { status: 0, reason: "no response" };
const check = async () => {
try {
const res = await fetch(`${base}${path}`);
const text = await res.text();
if (res.status === status && (!contains || text.includes(contains))) return true;
last = { status: res.status, reason: contains && !text.includes(contains)
? `missing "${contains}"` : `status ${res.status}` };
return false;
} catch (e: any) { last = { status: 0, reason: e.code || e.message }; return false; }
};
const ok = await poll(check);
console.log(ok ? `${path}` : `${path} (${last.reason})`);
return ok;
};
// Wait for index
console.log("Waiting for server...");
const ready = await poll(async () => {
try { return (await fetch(base)).ok; } catch { return false; }
}, { timeout: 3000, interval: 100 });
if (!ready) {
console.error("Server failed to start");
await cleanup();
process.exit(1);
}
console.log("Server ready\n");
// Run tests sequentially
const tests = [
() => test("/projects"),
() => test("/blog"),
() => test("/admin", { contains: "Payload" }),
() => test("/admin/api/stats"),
];
const results: boolean[] = [];
for (const t of tests) results.push(await t());
await cleanup();
process.exit(results.every(Boolean) ? 0 : 1);
-29
View File
@@ -1,29 +0,0 @@
# xevion.dev
This is the newest iteration of my personal website.
Instead of focus on playing around or showing off blog posts, this site will focus on presentation,
as a portfolio of what I have learned and what I can do.
## Development
Start the database and dev server:
```bash
pnpm db:start
pnpm dev
```
No `.env` file needed for basic development - sensible defaults are provided. Optional features require environment variables (see `.env.example`).
## Stack
- Hosted by [Vercel][vercel]
- [Next.js][next]
- [tRPC][trpc]
- [TailwindCSS][tailwind]
[vercel]: https://vercel.com
[next]: https://nextjs.org
[trpc]: https://trpc.io/
[tailwind]: https://tailwindcss.com/
File diff suppressed because it is too large Load Diff
+38
View File
@@ -0,0 +1,38 @@
import { dirname } from 'path'
import { fileURLToPath } from 'url'
import { FlatCompat } from '@eslint/eslintrc'
const __filename = fileURLToPath(import.meta.url)
const __dirname = dirname(__filename)
const compat = new FlatCompat({
baseDirectory: __dirname,
})
const eslintConfig = [
...compat.extends('next/core-web-vitals', 'next/typescript'),
{
rules: {
'@typescript-eslint/ban-ts-comment': 'warn',
'@typescript-eslint/no-empty-object-type': 'warn',
'@typescript-eslint/no-explicit-any': 'warn',
'@typescript-eslint/no-unused-vars': [
'warn',
{
vars: 'all',
args: 'after-used',
ignoreRestSiblings: false,
argsIgnorePattern: '^_',
varsIgnorePattern: '^_',
destructuredArrayIgnorePattern: '^_',
caughtErrorsIgnorePattern: '^(_|ignore)',
},
],
},
},
{
ignores: ['.next/'],
},
]
export default eslintConfig
+5
View File
@@ -0,0 +1,5 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
+31
View File
@@ -0,0 +1,31 @@
import { withPayload } from "@payloadcms/next/withPayload";
/** @type {import("next").NextConfig} */
const config = {
output: "standalone",
basePath: "/admin",
reactStrictMode: true,
images: {
remotePatterns: [
{
protocol: "https",
hostname: "img.walters.to",
},
{
protocol: "https",
hostname: "img.xevion.dev",
},
],
},
webpack: (webpackConfig) => {
webpackConfig.resolve.extensionAlias = {
".cjs": [".cts", ".cjs"],
".js": [".ts", ".tsx", ".js", ".jsx"],
".mjs": [".mts", ".mjs"],
};
return webpackConfig;
},
};
export default withPayload(config, { devBundleServerPackages: false });
+63
View File
@@ -0,0 +1,63 @@
{
"name": "@xevion/payload",
"version": "0.0.0",
"private": true,
"description": "PayloadCMS admin for xevion.dev",
"license": "MIT",
"type": "module",
"scripts": {
"build": "cross-env NODE_OPTIONS=\"--no-deprecation --max-old-space-size=8000\" next build",
"deploy": "echo 'Use Docker deployment'",
"dev": "cross-env NODE_OPTIONS=--no-deprecation next dev --port 5001",
"devsafe": "rm -rf .next && cross-env NODE_OPTIONS=--no-deprecation next dev",
"generate:importmap": "cross-env NODE_OPTIONS=--no-deprecation payload generate:importmap",
"generate:types": "cross-env NODE_OPTIONS=--no-deprecation payload generate:types",
"generate:schema": "pnpm run generate:schema:payload && pnpm run generate:schema:sync",
"generate:schema:payload": "cross-env NODE_OPTIONS=--no-deprecation payload generate:db-schema",
"generate:schema:sync": "cp src/payload-generated-schema.ts ../../packages/db/src/schema.ts && pnpm --filter @xevion/db build",
"ii": "pnpm install --ignore-workspace",
"lint": "cross-env NODE_OPTIONS=--no-deprecation next lint",
"payload": "cross-env NODE_OPTIONS=--no-deprecation payload",
"preview": "cross-env NODE_OPTIONS=--no-deprecation next start --port 5001",
"start": "cross-env NODE_OPTIONS=--no-deprecation next start --port 5001",
"test": "pnpm run test:int && pnpm run test:e2e",
"test:e2e": "cross-env NODE_OPTIONS=\"--no-deprecation --no-experimental-strip-types\" pnpm exec playwright test",
"test:int": "cross-env NODE_OPTIONS=--no-deprecation vitest run --config ./vitest.config.mts",
"clean": "rm -rf .next .turbo node_modules"
},
"dependencies": {
"@payloadcms/db-postgres": "3.69.0",
"@payloadcms/next": "3.69.0",
"@payloadcms/richtext-lexical": "3.69.0",
"@payloadcms/storage-s3": "3.69.0",
"@payloadcms/ui": "3.69.0",
"cross-env": "^7.0.3",
"dotenv": "16.4.7",
"graphql": "^16.8.1",
"next": "15.4.10",
"payload": "3.69.0",
"pino": "^9.6.0",
"pino-pretty": "^13.0.0",
"react": "19.2.1",
"react-dom": "19.2.1"
},
"devDependencies": {
"@playwright/test": "1.56.1",
"@testing-library/react": "16.3.0",
"@types/node": "^22.5.4",
"@types/react": "19.2.1",
"@types/react-dom": "19.2.1",
"@vitejs/plugin-react": "4.5.2",
"eslint": "^9.16.0",
"eslint-config-next": "15.4.7",
"jsdom": "26.1.0",
"playwright": "1.56.1",
"playwright-core": "1.56.1",
"vite-tsconfig-paths": "5.1.4",
"vitest": "3.2.3"
},
"engines": {
"node": "^18.20.2 || >=20.9.0",
"pnpm": "^9 || ^10"
}
}
+41
View File
@@ -0,0 +1,41 @@
import { defineConfig, devices } from '@playwright/test'
/**
* Read environment variables from file.
* https://github.com/motdotla/dotenv
*/
import 'dotenv/config'
/**
* See https://playwright.dev/docs/test-configuration.
*/
export default defineConfig({
testDir: './tests/e2e',
/* Fail the build on CI if you accidentally left test.only in the source code. */
forbidOnly: !!process.env.CI,
/* Retry on CI only */
retries: process.env.CI ? 2 : 0,
/* Opt out of parallel tests on CI. */
workers: process.env.CI ? 1 : undefined,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: 'html',
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: {
/* Base URL to use in actions like `await page.goto('/')`. */
// baseURL: 'http://localhost:3000',
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: 'on-first-retry',
},
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'], channel: 'chromium' },
},
],
webServer: {
command: 'pnpm dev',
reuseExistingServer: true,
url: 'http://localhost:3000',
},
})
@@ -0,0 +1,19 @@
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
import config from '@payload-config'
import '@payloadcms/next/css'
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)
@@ -0,0 +1,8 @@
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
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)
@@ -0,0 +1,47 @@
import configPromise from "@payload-config";
import { getPayload } from "payload";
import { NextRequest, NextResponse } from "next/server";
// Force this route to be dynamic
export const dynamic = "force-dynamic";
export async function GET(_request: NextRequest) {
try {
const payload = await getPayload({ config: configPromise });
// Fetch statistics about the content
const [projectsResult, technologiesResult, linksResult] = await Promise.all(
[
payload.count({ collection: "projects" }),
payload.count({ collection: "technologies" }),
payload.count({ collection: "links" }),
],
);
// Get featured projects count
const featuredProjects = await payload.count({
collection: "projects",
where: { featured: { equals: true } },
});
return NextResponse.json({
stats: {
projects: {
total: projectsResult.totalDocs,
featured: featuredProjects.totalDocs,
},
technologies: technologiesResult.totalDocs,
links: linksResult.totalDocs,
},
timestamp: new Date().toISOString(),
});
} catch (error) {
return NextResponse.json(
{
error: "Failed to fetch stats",
details: error instanceof Error ? error.message : "Unknown error",
},
{ status: 500 },
);
}
}
@@ -0,0 +1,5 @@
import { CollectionCards as CollectionCards_ab83ff7e88da8d3530831f296ec4756a } from '@payloadcms/ui/rsc'
export const importMap = {
'@payloadcms/ui/rsc#CollectionCards': CollectionCards_ab83ff7e88da8d3530831f296ec4756a,
}
@@ -1,12 +1,13 @@
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
import config from "../../payload.config";
import config from "@payload-config";
import "@payloadcms/next/css";
import type { ServerFunctionClient } from "payload";
import { handleServerFunctions, RootLayout } from "@payloadcms/next/layouts";
import React from "react";
import { importMap } from "./admin/importMap.js";
import { importMap } from "./importMap.js";
import "./custom.scss";
type Args = {
children: React.ReactNode;
@@ -2,9 +2,9 @@
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
import type { Metadata } from "next";
import config from "../../../../payload.config";
import config from "@payload-config";
import { NotFoundPage, generatePageMetadata } from "@payloadcms/next/views";
import { importMap } from "../importMap";
import { importMap } from "./importMap";
type Args = {
params: Promise<{
@@ -2,9 +2,9 @@
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
import type { Metadata } from "next";
import config from "../../../../payload.config";
import config from "@payload-config";
import { RootPage, generatePageMetadata } from "@payloadcms/next/views";
import { importMap } from "../importMap";
import { importMap } from "./importMap";
type Args = {
params: Promise<{
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,218 @@
import { MigrateUpArgs, MigrateDownArgs, sql } from '@payloadcms/db-postgres'
export async function up({ db, payload, req }: MigrateUpArgs): Promise<void> {
await db.execute(sql`
CREATE TYPE "public"."enum_projects_status" AS ENUM('draft', 'published', 'archived');
CREATE TABLE "users_sessions" (
"_order" integer NOT NULL,
"_parent_id" integer NOT NULL,
"id" varchar PRIMARY KEY NOT NULL,
"created_at" timestamp(3) with time zone,
"expires_at" timestamp(3) with time zone NOT NULL
);
CREATE TABLE "users" (
"id" serial PRIMARY KEY NOT NULL,
"updated_at" timestamp(3) with time zone DEFAULT now() NOT NULL,
"created_at" timestamp(3) with time zone DEFAULT now() NOT NULL,
"email" varchar NOT NULL,
"reset_password_token" varchar,
"reset_password_expiration" timestamp(3) with time zone,
"salt" varchar,
"hash" varchar,
"login_attempts" numeric DEFAULT 0,
"lock_until" timestamp(3) with time zone
);
CREATE TABLE "media" (
"id" serial PRIMARY KEY NOT NULL,
"alt" varchar NOT NULL,
"updated_at" timestamp(3) with time zone DEFAULT now() NOT NULL,
"created_at" timestamp(3) with time zone DEFAULT now() NOT NULL,
"url" varchar,
"thumbnail_u_r_l" varchar,
"filename" varchar,
"mime_type" varchar,
"filesize" numeric,
"width" numeric,
"height" numeric,
"focal_x" numeric,
"focal_y" numeric
);
CREATE TABLE "projects" (
"id" serial PRIMARY KEY NOT NULL,
"name" varchar NOT NULL,
"description" varchar NOT NULL,
"short_description" varchar NOT NULL,
"icon" varchar,
"status" "enum_projects_status" DEFAULT 'draft' NOT NULL,
"featured" boolean DEFAULT false,
"autocheck_updated" boolean DEFAULT false,
"last_updated" timestamp(3) with time zone,
"wakatime_offset" numeric,
"banner_image_id" integer,
"updated_at" timestamp(3) with time zone DEFAULT now() NOT NULL,
"created_at" timestamp(3) with time zone DEFAULT now() NOT NULL
);
CREATE TABLE "projects_rels" (
"id" serial PRIMARY KEY NOT NULL,
"order" integer,
"parent_id" integer NOT NULL,
"path" varchar NOT NULL,
"technologies_id" integer
);
CREATE TABLE "technologies" (
"id" serial PRIMARY KEY NOT NULL,
"name" varchar NOT NULL,
"url" varchar,
"updated_at" timestamp(3) with time zone DEFAULT now() NOT NULL,
"created_at" timestamp(3) with time zone DEFAULT now() NOT NULL
);
CREATE TABLE "links" (
"id" serial PRIMARY KEY NOT NULL,
"url" varchar NOT NULL,
"icon" varchar,
"description" varchar,
"project_id" integer NOT NULL,
"updated_at" timestamp(3) with time zone DEFAULT now() NOT NULL,
"created_at" timestamp(3) with time zone DEFAULT now() NOT NULL
);
CREATE TABLE "payload_kv" (
"id" serial PRIMARY KEY NOT NULL,
"key" varchar NOT NULL,
"data" jsonb NOT NULL
);
CREATE TABLE "payload_locked_documents" (
"id" serial PRIMARY KEY NOT NULL,
"global_slug" varchar,
"updated_at" timestamp(3) with time zone DEFAULT now() NOT NULL,
"created_at" timestamp(3) with time zone DEFAULT now() NOT NULL
);
CREATE TABLE "payload_locked_documents_rels" (
"id" serial PRIMARY KEY NOT NULL,
"order" integer,
"parent_id" integer NOT NULL,
"path" varchar NOT NULL,
"users_id" integer,
"media_id" integer,
"projects_id" integer,
"technologies_id" integer,
"links_id" integer
);
CREATE TABLE "payload_preferences" (
"id" serial PRIMARY KEY NOT NULL,
"key" varchar,
"value" jsonb,
"updated_at" timestamp(3) with time zone DEFAULT now() NOT NULL,
"created_at" timestamp(3) with time zone DEFAULT now() NOT NULL
);
CREATE TABLE "payload_preferences_rels" (
"id" serial PRIMARY KEY NOT NULL,
"order" integer,
"parent_id" integer NOT NULL,
"path" varchar NOT NULL,
"users_id" integer
);
CREATE TABLE "payload_migrations" (
"id" serial PRIMARY KEY NOT NULL,
"name" varchar,
"batch" numeric,
"updated_at" timestamp(3) with time zone DEFAULT now() NOT NULL,
"created_at" timestamp(3) with time zone DEFAULT now() NOT NULL
);
CREATE TABLE "metadata" (
"id" serial PRIMARY KEY NOT NULL,
"tagline" varchar NOT NULL,
"resume_id" integer NOT NULL,
"resume_filename" varchar,
"updated_at" timestamp(3) with time zone,
"created_at" timestamp(3) with time zone
);
ALTER TABLE "users_sessions" ADD CONSTRAINT "users_sessions_parent_id_fk" FOREIGN KEY ("_parent_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;
ALTER TABLE "projects" ADD CONSTRAINT "projects_banner_image_id_media_id_fk" FOREIGN KEY ("banner_image_id") REFERENCES "public"."media"("id") ON DELETE set null ON UPDATE no action;
ALTER TABLE "projects_rels" ADD CONSTRAINT "projects_rels_parent_fk" FOREIGN KEY ("parent_id") REFERENCES "public"."projects"("id") ON DELETE cascade ON UPDATE no action;
ALTER TABLE "projects_rels" ADD CONSTRAINT "projects_rels_technologies_fk" FOREIGN KEY ("technologies_id") REFERENCES "public"."technologies"("id") ON DELETE cascade ON UPDATE no action;
ALTER TABLE "links" ADD CONSTRAINT "links_project_id_projects_id_fk" FOREIGN KEY ("project_id") REFERENCES "public"."projects"("id") ON DELETE set null ON UPDATE no action;
ALTER TABLE "payload_locked_documents_rels" ADD CONSTRAINT "payload_locked_documents_rels_parent_fk" FOREIGN KEY ("parent_id") REFERENCES "public"."payload_locked_documents"("id") ON DELETE cascade ON UPDATE no action;
ALTER TABLE "payload_locked_documents_rels" ADD CONSTRAINT "payload_locked_documents_rels_users_fk" FOREIGN KEY ("users_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;
ALTER TABLE "payload_locked_documents_rels" ADD CONSTRAINT "payload_locked_documents_rels_media_fk" FOREIGN KEY ("media_id") REFERENCES "public"."media"("id") ON DELETE cascade ON UPDATE no action;
ALTER TABLE "payload_locked_documents_rels" ADD CONSTRAINT "payload_locked_documents_rels_projects_fk" FOREIGN KEY ("projects_id") REFERENCES "public"."projects"("id") ON DELETE cascade ON UPDATE no action;
ALTER TABLE "payload_locked_documents_rels" ADD CONSTRAINT "payload_locked_documents_rels_technologies_fk" FOREIGN KEY ("technologies_id") REFERENCES "public"."technologies"("id") ON DELETE cascade ON UPDATE no action;
ALTER TABLE "payload_locked_documents_rels" ADD CONSTRAINT "payload_locked_documents_rels_links_fk" FOREIGN KEY ("links_id") REFERENCES "public"."links"("id") ON DELETE cascade ON UPDATE no action;
ALTER TABLE "payload_preferences_rels" ADD CONSTRAINT "payload_preferences_rels_parent_fk" FOREIGN KEY ("parent_id") REFERENCES "public"."payload_preferences"("id") ON DELETE cascade ON UPDATE no action;
ALTER TABLE "payload_preferences_rels" ADD CONSTRAINT "payload_preferences_rels_users_fk" FOREIGN KEY ("users_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;
ALTER TABLE "metadata" ADD CONSTRAINT "metadata_resume_id_media_id_fk" FOREIGN KEY ("resume_id") REFERENCES "public"."media"("id") ON DELETE set null ON UPDATE no action;
CREATE INDEX "users_sessions_order_idx" ON "users_sessions" USING btree ("_order");
CREATE INDEX "users_sessions_parent_id_idx" ON "users_sessions" USING btree ("_parent_id");
CREATE INDEX "users_updated_at_idx" ON "users" USING btree ("updated_at");
CREATE INDEX "users_created_at_idx" ON "users" USING btree ("created_at");
CREATE UNIQUE INDEX "users_email_idx" ON "users" USING btree ("email");
CREATE INDEX "media_updated_at_idx" ON "media" USING btree ("updated_at");
CREATE INDEX "media_created_at_idx" ON "media" USING btree ("created_at");
CREATE UNIQUE INDEX "media_filename_idx" ON "media" USING btree ("filename");
CREATE INDEX "projects_banner_image_idx" ON "projects" USING btree ("banner_image_id");
CREATE INDEX "projects_updated_at_idx" ON "projects" USING btree ("updated_at");
CREATE INDEX "projects_created_at_idx" ON "projects" USING btree ("created_at");
CREATE INDEX "projects_rels_order_idx" ON "projects_rels" USING btree ("order");
CREATE INDEX "projects_rels_parent_idx" ON "projects_rels" USING btree ("parent_id");
CREATE INDEX "projects_rels_path_idx" ON "projects_rels" USING btree ("path");
CREATE INDEX "projects_rels_technologies_id_idx" ON "projects_rels" USING btree ("technologies_id");
CREATE INDEX "technologies_updated_at_idx" ON "technologies" USING btree ("updated_at");
CREATE INDEX "technologies_created_at_idx" ON "technologies" USING btree ("created_at");
CREATE INDEX "links_project_idx" ON "links" USING btree ("project_id");
CREATE INDEX "links_updated_at_idx" ON "links" USING btree ("updated_at");
CREATE INDEX "links_created_at_idx" ON "links" USING btree ("created_at");
CREATE UNIQUE INDEX "payload_kv_key_idx" ON "payload_kv" USING btree ("key");
CREATE INDEX "payload_locked_documents_global_slug_idx" ON "payload_locked_documents" USING btree ("global_slug");
CREATE INDEX "payload_locked_documents_updated_at_idx" ON "payload_locked_documents" USING btree ("updated_at");
CREATE INDEX "payload_locked_documents_created_at_idx" ON "payload_locked_documents" USING btree ("created_at");
CREATE INDEX "payload_locked_documents_rels_order_idx" ON "payload_locked_documents_rels" USING btree ("order");
CREATE INDEX "payload_locked_documents_rels_parent_idx" ON "payload_locked_documents_rels" USING btree ("parent_id");
CREATE INDEX "payload_locked_documents_rels_path_idx" ON "payload_locked_documents_rels" USING btree ("path");
CREATE INDEX "payload_locked_documents_rels_users_id_idx" ON "payload_locked_documents_rels" USING btree ("users_id");
CREATE INDEX "payload_locked_documents_rels_media_id_idx" ON "payload_locked_documents_rels" USING btree ("media_id");
CREATE INDEX "payload_locked_documents_rels_projects_id_idx" ON "payload_locked_documents_rels" USING btree ("projects_id");
CREATE INDEX "payload_locked_documents_rels_technologies_id_idx" ON "payload_locked_documents_rels" USING btree ("technologies_id");
CREATE INDEX "payload_locked_documents_rels_links_id_idx" ON "payload_locked_documents_rels" USING btree ("links_id");
CREATE INDEX "payload_preferences_key_idx" ON "payload_preferences" USING btree ("key");
CREATE INDEX "payload_preferences_updated_at_idx" ON "payload_preferences" USING btree ("updated_at");
CREATE INDEX "payload_preferences_created_at_idx" ON "payload_preferences" USING btree ("created_at");
CREATE INDEX "payload_preferences_rels_order_idx" ON "payload_preferences_rels" USING btree ("order");
CREATE INDEX "payload_preferences_rels_parent_idx" ON "payload_preferences_rels" USING btree ("parent_id");
CREATE INDEX "payload_preferences_rels_path_idx" ON "payload_preferences_rels" USING btree ("path");
CREATE INDEX "payload_preferences_rels_users_id_idx" ON "payload_preferences_rels" USING btree ("users_id");
CREATE INDEX "payload_migrations_updated_at_idx" ON "payload_migrations" USING btree ("updated_at");
CREATE INDEX "payload_migrations_created_at_idx" ON "payload_migrations" USING btree ("created_at");
CREATE INDEX "metadata_resume_idx" ON "metadata" USING btree ("resume_id");`)
}
export async function down({ db, payload, req }: MigrateDownArgs): Promise<void> {
await db.execute(sql`
DROP TABLE "users_sessions" CASCADE;
DROP TABLE "users" CASCADE;
DROP TABLE "media" CASCADE;
DROP TABLE "projects" CASCADE;
DROP TABLE "projects_rels" CASCADE;
DROP TABLE "technologies" CASCADE;
DROP TABLE "links" CASCADE;
DROP TABLE "payload_kv" CASCADE;
DROP TABLE "payload_locked_documents" CASCADE;
DROP TABLE "payload_locked_documents_rels" CASCADE;
DROP TABLE "payload_preferences" CASCADE;
DROP TABLE "payload_preferences_rels" CASCADE;
DROP TABLE "payload_migrations" CASCADE;
DROP TABLE "metadata" CASCADE;
DROP TYPE "public"."enum_projects_status";`)
}
+9
View File
@@ -0,0 +1,9 @@
import * as migration_20260104_100459 from './20260104_100459';
export const migrations = [
{
up: migration_20260104_100459.up,
down: migration_20260104_100459.down,
name: '20260104_100459'
},
];
@@ -1,5 +1,5 @@
/* tslint:disable */
/* eslint-disable */
/**
* This file was automatically generated by Payload.
* DO NOT MODIFY IT BY HAND. Instead, modify your source Payload config,
@@ -263,6 +263,16 @@ export const links = pgTable(
],
);
export const payload_kv = pgTable(
"payload_kv",
{
id: serial("id").primaryKey(),
key: varchar("key").notNull(),
data: jsonb("data").notNull(),
},
(columns) => [uniqueIndex("payload_kv_key_idx").on(columns.key)],
);
export const payload_locked_documents = pgTable(
"payload_locked_documents",
{
@@ -502,6 +512,7 @@ export const relations_links = relations(links, ({ one }) => ({
relationName: "project",
}),
}));
export const relations_payload_kv = relations(payload_kv, () => ({}));
export const relations_payload_locked_documents_rels = relations(
payload_locked_documents_rels,
({ one }) => ({
@@ -589,6 +600,7 @@ type DatabaseSchema = {
projects_rels: typeof projects_rels;
technologies: typeof technologies;
links: typeof links;
payload_kv: typeof payload_kv;
payload_locked_documents: typeof payload_locked_documents;
payload_locked_documents_rels: typeof payload_locked_documents_rels;
payload_preferences: typeof payload_preferences;
@@ -602,6 +614,7 @@ type DatabaseSchema = {
relations_projects: typeof relations_projects;
relations_technologies: typeof relations_technologies;
relations_links: typeof relations_links;
relations_payload_kv: typeof relations_payload_kv;
relations_payload_locked_documents_rels: typeof relations_payload_locked_documents_rels;
relations_payload_locked_documents: typeof relations_payload_locked_documents;
relations_payload_preferences_rels: typeof relations_payload_preferences_rels;
@@ -72,6 +72,7 @@ export interface Config {
projects: Project;
technologies: Technology;
links: Link;
'payload-kv': PayloadKv;
'payload-locked-documents': PayloadLockedDocument;
'payload-preferences': PayloadPreference;
'payload-migrations': PayloadMigration;
@@ -83,6 +84,7 @@ export interface Config {
projects: ProjectsSelect<false> | ProjectsSelect<true>;
technologies: TechnologiesSelect<false> | TechnologiesSelect<true>;
links: LinksSelect<false> | LinksSelect<true>;
'payload-kv': PayloadKvSelect<false> | PayloadKvSelect<true>;
'payload-locked-documents': PayloadLockedDocumentsSelect<false> | PayloadLockedDocumentsSelect<true>;
'payload-preferences': PayloadPreferencesSelect<false> | PayloadPreferencesSelect<true>;
'payload-migrations': PayloadMigrationsSelect<false> | PayloadMigrationsSelect<true>;
@@ -90,6 +92,7 @@ export interface Config {
db: {
defaultIDType: number;
};
fallbackLocale: null;
globals: {
metadata: Metadatum;
};
@@ -219,6 +222,23 @@ export interface Link {
updatedAt: string;
createdAt: string;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "payload-kv".
*/
export interface PayloadKv {
id: number;
key: string;
data:
| {
[k: string]: unknown;
}
| unknown[]
| string
| number
| boolean
| null;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "payload-locked-documents".
@@ -369,6 +389,14 @@ export interface LinksSelect<T extends boolean = true> {
updatedAt?: T;
createdAt?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "payload-kv_select".
*/
export interface PayloadKvSelect<T extends boolean = true> {
key?: T;
data?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "payload-locked-documents_select".
@@ -1,11 +1,10 @@
// storage-adapter-import-placeholder
import { postgresAdapter } from "@payloadcms/db-postgres";
import { payloadCloudPlugin } from "@payloadcms/payload-cloud";
import { lexicalEditor } from "@payloadcms/richtext-lexical";
import path from "path";
import { buildConfig } from "payload";
import { fileURLToPath } from "url";
import sharp from "sharp";
import { s3Storage } from "@payloadcms/storage-s3";
import { migrations } from "./migrations";
import { Users } from "./collections/Users";
import { Media } from "./collections/Media";
@@ -13,7 +12,6 @@ import { Projects } from "./collections/Projects";
import { Technologies } from "./collections/Technologies";
import { Links } from "./collections/Links";
import { Metadata } from "./globals/Metadata";
import { env } from "./env/server.mjs";
const filename = fileURLToPath(import.meta.url);
const dirname = path.dirname(filename);
@@ -21,22 +19,43 @@ const dirname = path.dirname(filename);
export default buildConfig({
admin: {
user: Users.slug,
importMap: {
baseDir: path.resolve(dirname),
},
},
routes: {
admin: "/",
},
collections: [Users, Media, Projects, Technologies, Links],
globals: [Metadata],
editor: lexicalEditor(),
secret: env.PAYLOAD_SECRET,
secret: process.env.PAYLOAD_SECRET || "",
typescript: {
outputFile: path.resolve(dirname, "payload-types.ts"),
},
db: postgresAdapter({
prodMigrations: migrations,
pool: {
connectionString: env.DATABASE_URI,
connectionString: process.env.DATABASE_URL,
},
}),
sharp,
graphQL: {
disablePlaygroundInProduction: true,
},
plugins: [
payloadCloudPlugin(),
// storage-adapter-placeholder
s3Storage({
collections: {
media: true,
},
bucket: process.env.R2_BUCKET_NAME!,
config: {
endpoint: process.env.R2_ENDPOINT,
region: "auto",
credentials: {
accessKeyId: process.env.R2_ACCESS_KEY_ID!,
secretAccessKey: process.env.R2_SECRET_ACCESS_KEY!,
},
},
}),
],
});
@@ -0,0 +1,20 @@
import { test, expect, Page } from '@playwright/test'
test.describe('Frontend', () => {
let page: Page
test.beforeAll(async ({ browser }, testInfo) => {
const context = await browser.newContext()
page = await context.newPage()
})
test('can go on homepage', async ({ page }) => {
await page.goto('http://localhost:3000')
await expect(page).toHaveTitle(/Payload Blank Template/)
const headging = page.locator('h1').first()
await expect(headging).toHaveText('Welcome to your new project.')
})
})
+20
View File
@@ -0,0 +1,20 @@
import { getPayload, Payload } from 'payload'
import config from '@/payload.config'
import { describe, it, beforeAll, expect } from 'vitest'
let payload: Payload
describe('API', () => {
beforeAll(async () => {
const payloadConfig = await config
payload = await getPayload({ config: payloadConfig })
})
it('fetches users', async () => {
const users = await payload.find({
collection: 'users',
})
expect(users).toBeDefined()
})
})
+22 -6
View File
@@ -1,10 +1,15 @@
{
"compilerOptions": {
"baseUrl": ".",
"lib": ["DOM", "DOM.Iterable", "ES2022"],
"lib": [
"DOM",
"DOM.Iterable",
"ES2022"
],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"strictNullChecks": false,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
@@ -19,11 +24,22 @@
}
],
"paths": {
"@/*": ["./src/*"],
"@payload-config": ["./src/payload.config.ts"]
"@/*": [
"./src/*"
],
"@payload-config": [
"./src/payload.config.ts"
]
},
"target": "ES2022"
"target": "ES2022",
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts"
],
"exclude": [
"node_modules"
],
}
+12
View File
@@ -0,0 +1,12 @@
import { defineConfig } from 'vitest/config'
import react from '@vitejs/plugin-react'
import tsconfigPaths from 'vite-tsconfig-paths'
export default defineConfig({
plugins: [tsconfigPaths(), react()],
test: {
environment: 'jsdom',
setupFiles: ['./vitest.setup.ts'],
include: ['tests/int/**/*.int.spec.ts'],
},
})
+4
View File
@@ -0,0 +1,4 @@
// Any setup scripts you might need go here
// Load .env files
import 'dotenv/config'
+1
View File
@@ -0,0 +1 @@
engine-strict=true
+46
View File
@@ -0,0 +1,46 @@
{
"name": "@xevion/web",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite dev --port 5000",
"build": "vite build",
"preview": "vite preview --port 5000",
"start": "node build/index.js",
"prepare": "svelte-kit sync || echo ''",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
"deploy": "echo 'Use Docker deployment'",
"clean": "rm -rf .svelte-kit build .turbo node_modules"
},
"dependencies": {
"@fontsource-variable/inter": "^5.1.0",
"@fontsource/hanken-grotesk": "^5.1.0",
"@fontsource/schibsted-grotesk": "^5.2.8",
"@xevion/db": "workspace:*",
"bits-ui": "^2.8.2",
"clsx": "^2.1.1",
"drizzle-orm": "^0.44.7",
"p5": "^1.11.7",
"pino": "^9.6.0",
"pino-pretty": "^13.0.0",
"postgres": "^3.4.5",
"svelte-wrap-balancer": "^0.0.4",
"tailwind-merge": "^3.3.1"
},
"devDependencies": {
"@iconify/json": "^2.2.424",
"@sveltejs/adapter-node": "^5.2.9",
"@sveltejs/kit": "^2.21.0",
"@sveltejs/vite-plugin-svelte": "^6.2.1",
"@tailwindcss/vite": "^4.1.11",
"@types/p5": "^1.7.7",
"svelte": "^5.45.6",
"svelte-adapter-bun": "^1.0.1",
"svelte-check": "^4.3.4",
"tailwindcss": "^4.1.11",
"unplugin-icons": "^22.5.0",
"vite": "^7.2.6"
}
}
@@ -11,9 +11,7 @@
--drop-shadow-extreme: 0 0 50px black;
/* Font families */
--font-inter: "Inter", sans-serif;
--font-roboto: "Roboto", sans-serif;
--font-mono: "Roboto Mono", monospace;
--font-inter: "Inter Variable", sans-serif;
--font-hanken: "Hanken Grotesk", sans-serif;
--font-schibsted: "Schibsted Grotesk", sans-serif;
@@ -25,7 +23,7 @@
/* Animations */
--animate-bg-fast: fade 0.5s ease-in-out 0.5s forwards;
--animate-bg: fade 1.2s ease-in-out 1.1s forwards;
--animate-bg: fade 2.5s ease-in-out 1.5s forwards;
--animate-fade-in: fade-in 2.5s ease-in-out forwards;
--animate-title: title 3s ease-out forwards;
--animate-fade-left: fade-left 3s ease-in-out forwards;
@@ -105,16 +103,6 @@ body {
@apply font-inter overflow-x-hidden text-white;
}
.description {
hyphens: auto;
}
@media (min-width: 768px) {
.description {
hyphens: none;
}
}
body {
@apply h-full;
}
+11
View File
@@ -0,0 +1,11 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
%sveltekit.head%
</head>
<body data-sveltekit-preload-data="hover">
<div style="display: contents">%sveltekit.body%</div>
</body>
</html>
+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="107" height="128" viewBox="0 0 107 128"><title>svelte-logo</title><path d="M94.157 22.819c-10.4-14.885-30.94-19.297-45.792-9.835L22.282 29.608A29.92 29.92 0 0 0 8.764 49.65a31.5 31.5 0 0 0 3.108 20.231 30 30 0 0 0-4.477 11.183 31.9 31.9 0 0 0 5.448 24.116c10.402 14.887 30.942 19.297 45.791 9.835l26.083-16.624A29.92 29.92 0 0 0 98.235 78.35a31.53 31.53 0 0 0-3.105-20.232 30 30 0 0 0 4.474-11.182 31.88 31.88 0 0 0-5.447-24.116" style="fill:#ff3e00"/><path d="M45.817 106.582a20.72 20.72 0 0 1-22.237-8.243 19.17 19.17 0 0 1-3.277-14.503 18 18 0 0 1 .624-2.435l.49-1.498 1.337.981a33.6 33.6 0 0 0 10.203 5.098l.97.294-.09.968a5.85 5.85 0 0 0 1.052 3.878 6.24 6.24 0 0 0 6.695 2.485 5.8 5.8 0 0 0 1.603-.704L69.27 76.28a5.43 5.43 0 0 0 2.45-3.631 5.8 5.8 0 0 0-.987-4.371 6.24 6.24 0 0 0-6.698-2.487 5.7 5.7 0 0 0-1.6.704l-9.953 6.345a19 19 0 0 1-5.296 2.326 20.72 20.72 0 0 1-22.237-8.243 19.17 19.17 0 0 1-3.277-14.502 17.99 17.99 0 0 1 8.13-12.052l26.081-16.623a19 19 0 0 1 5.3-2.329 20.72 20.72 0 0 1 22.237 8.243 19.17 19.17 0 0 1 3.277 14.503 18 18 0 0 1-.624 2.435l-.49 1.498-1.337-.98a33.6 33.6 0 0 0-10.203-5.1l-.97-.294.09-.968a5.86 5.86 0 0 0-1.052-3.878 6.24 6.24 0 0 0-6.696-2.485 5.8 5.8 0 0 0-1.602.704L37.73 51.72a5.42 5.42 0 0 0-2.449 3.63 5.79 5.79 0 0 0 .986 4.372 6.24 6.24 0 0 0 6.698 2.486 5.8 5.8 0 0 0 1.602-.704l9.952-6.342a19 19 0 0 1 5.295-2.328 20.72 20.72 0 0 1 22.237 8.242 19.17 19.17 0 0 1 3.277 14.503 18 18 0 0 1-8.13 12.053l-26.081 16.622a19 19 0 0 1-5.3 2.328" style="fill:#fff"/></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

@@ -0,0 +1,25 @@
<script lang="ts">
import { cn } from "$lib/utils";
import type { Snippet } from "svelte";
import Dots from "./Dots.svelte";
let {
class: className = "",
backgroundClass = "",
children,
}: {
class?: string;
backgroundClass?: string;
children?: Snippet;
} = $props();
</script>
<div class="pointer-events-none fixed inset-0 -z-20 bg-black"></div>
<Dots class={[
backgroundClass
]} />
<main class={cn("relative min-h-screen text-zinc-50", className)}>
{#if children}
{@render children()}
{/if}
</main>
+360
View File
@@ -0,0 +1,360 @@
<script lang="ts">
import { cn } from "$lib/utils";
import type { ClassValue } from "clsx";
import { onMount, onDestroy } from "svelte";
let { class: className = "" }: { class?: ClassValue } = $props();
let canvas: HTMLCanvasElement;
let gl: WebGLRenderingContext | null = null;
let animationId: number | null = null;
// Noise sampling scale - larger values create smoother, more gradual flow patterns
const SCALE = 1000;
// Maximum displacement distance from grid position (in pixels)
const LENGTH = 10;
// Distance between grid points (in pixels) - controls dot density
const SPACING = 20;
// Global animation speed multiplier - higher values make everything move faster
const TIMESCALE = 10.25 / 1000;
// Rotation/angle animation speed multiplier
const ANGLE_TIME_SCALE = 2.0;
// Pulsing/length animation speed multiplier
const LENGTH_TIME_SCALE = 1.5;
// Base opacity of dots (0-1)
const OPACITY = 0.9;
// Radius of each dot (in pixels)
const RADIUS = 3.5;
// How much opacity varies with angle (0-1)
const ANGLE_OPACITY_AMPLITUDE = 0.8;
// Minimum opacity from angle calculation
const ANGLE_OPACITY_FLOOR = 0.1;
// Lower bound of random per-dot opacity
const RANDOM_OPACITY_MIN = 0.5;
// Upper bound of random per-dot opacity
const RANDOM_OPACITY_MAX = 1.0;
// Simplex noise GLSL implementation
const vertexShader = `
attribute vec2 a_position;
void main() {
gl_Position = vec4(a_position, 0.0, 1.0);
}
`;
const fragmentShader = `
precision mediump float;
uniform vec2 u_resolution;
uniform float u_time;
uniform float u_seed;
uniform float u_dpr;
uniform float u_scale;
uniform float u_length;
uniform float u_spacing;
uniform float u_opacity;
uniform float u_radius;
uniform float u_angleTimeScale;
uniform float u_lengthTimeScale;
uniform float u_angleOpacityAmp;
uniform float u_angleOpacityFloor;
uniform float u_randomOpacityMin;
uniform float u_randomOpacityMax;
const float PI = 3.14159265359;
// Simplex 3D noise
vec3 mod289(vec3 x) { return x - floor(x * (1.0 / 289.0)) * 289.0; }
vec4 mod289(vec4 x) { return x - floor(x * (1.0 / 289.0)) * 289.0; }
vec4 permute(vec4 x) { return mod289(((x*34.0)+1.0)*x); }
vec4 taylorInvSqrt(vec4 r) { return 1.79284291400159 - 0.85373472095314 * r; }
float snoise(vec3 v) {
const vec2 C = vec2(1.0/6.0, 1.0/3.0);
const vec4 D = vec4(0.0, 0.5, 1.0, 2.0);
vec3 i = floor(v + dot(v, C.yyy));
vec3 x0 = v - i + dot(i, C.xxx);
vec3 g = step(x0.yzx, x0.xyz);
vec3 l = 1.0 - g;
vec3 i1 = min(g.xyz, l.zxy);
vec3 i2 = max(g.xyz, l.zxy);
vec3 x1 = x0 - i1 + C.xxx;
vec3 x2 = x0 - i2 + C.yyy;
vec3 x3 = x0 - D.yyy;
i = mod289(i + u_seed);
vec4 p = permute(permute(permute(
i.z + vec4(0.0, i1.z, i2.z, 1.0))
+ i.y + vec4(0.0, i1.y, i2.y, 1.0))
+ i.x + vec4(0.0, i1.x, i2.x, 1.0));
float n_ = 0.142857142857;
vec3 ns = n_ * D.wyz - D.xzx;
vec4 j = p - 49.0 * floor(p * ns.z * ns.z);
vec4 x_ = floor(j * ns.z);
vec4 y_ = floor(j - 7.0 * x_);
vec4 x = x_ *ns.x + ns.yyyy;
vec4 y = y_ *ns.x + ns.yyyy;
vec4 h = 1.0 - abs(x) - abs(y);
vec4 b0 = vec4(x.xy, y.xy);
vec4 b1 = vec4(x.zw, y.zw);
vec4 s0 = floor(b0)*2.0 + 1.0;
vec4 s1 = floor(b1)*2.0 + 1.0;
vec4 sh = -step(h, vec4(0.0));
vec4 a0 = b0.xzyw + s0.xzyw*sh.xxyy;
vec4 a1 = b1.xzyw + s1.xzyw*sh.zzww;
vec3 p0 = vec3(a0.xy, h.x);
vec3 p1 = vec3(a0.zw, h.y);
vec3 p2 = vec3(a1.xy, h.z);
vec3 p3 = vec3(a1.zw, h.w);
vec4 norm = taylorInvSqrt(vec4(dot(p0,p0), dot(p1,p1), dot(p2,p2), dot(p3,p3)));
p0 *= norm.x;
p1 *= norm.y;
p2 *= norm.z;
p3 *= norm.w;
vec4 m = max(0.6 - vec4(dot(x0,x0), dot(x1,x1), dot(x2,x2), dot(x3,x3)), 0.0);
m = m * m;
return 42.0 * dot(m*m, vec4(dot(p0,x0), dot(p1,x1), dot(p2,x2), dot(p3,x3)));
}
// Hash function for random per-point opacity
float hash(vec2 p) {
return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453);
}
// Convert snoise [-1,1] to p5-style noise [0,1]
float noise01(vec3 v) {
return (snoise(v) + 1.0) * 0.5;
}
void main() {
vec2 pixelCoord = gl_FragCoord.xy;
// Find nearest grid point (account for DPR)
float spacing = u_spacing * u_dpr;
float scaleDpr = u_scale * u_dpr;
vec2 gridCoord = floor(pixelCoord / spacing) * spacing;
// Calculate distance to all nearby grid points (9 neighbors)
float minDist = 1000000.0;
vec2 closestPoint = vec2(0.0);
float pointOpacity = 0.0;
for (float dx = -1.0; dx <= 1.0; dx += 1.0) {
for (float dy = -1.0; dy <= 1.0; dy += 1.0) {
vec2 testGrid = gridCoord + vec2(dx * spacing, dy * spacing);
// Get force direction at this grid point (matching original p5 formula)
// Original: (noise(x/SCALE, y/SCALE, z) - 0.5) * 2 * TWO_PI
float rad = (noise01(vec3(testGrid / scaleDpr, u_time * u_angleTimeScale)) - 0.5) * 4.0 * PI;
// Original: (noise(x/SCALE, y/SCALE, z*2) + 0.5) * LENGTH
float len = (noise01(vec3(testGrid / scaleDpr, u_time * u_lengthTimeScale)) + 0.5) * u_length * u_dpr;
// Calculate displaced position
vec2 displacedPoint = testGrid + vec2(cos(rad), sin(rad)) * len;
float dist = distance(pixelCoord, displacedPoint);
if (dist < minDist) {
minDist = dist;
closestPoint = testGrid;
pointOpacity = hash(testGrid) * (u_randomOpacityMax - u_randomOpacityMin) + u_randomOpacityMin;
}
}
}
// Recalculate angle for opacity calculation
float rad = (noise01(vec3(closestPoint / scaleDpr, u_time * u_angleTimeScale)) - 0.5) * 4.0 * PI;
// Draw circle with configurable radius
float circle = 1.0 - smoothstep(0.0, u_radius * u_dpr, minDist);
// Calculate opacity based on angle
float angleOpacity = (abs(cos(rad)) * u_angleOpacityAmp + u_angleOpacityFloor) * pointOpacity * u_opacity;
// Light gray dots with calculated opacity
gl_FragColor = vec4(vec3(200.0/255.0), circle * angleOpacity);
}
`;
function createShader(gl: WebGLRenderingContext, type: number, source: string): WebGLShader | null {
const shader = gl.createShader(type);
if (!shader) return null;
gl.shaderSource(shader, source);
gl.compileShader(shader);
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
console.error('Shader compile error:', gl.getShaderInfoLog(shader));
gl.deleteShader(shader);
return null;
}
return shader;
}
function createProgram(gl: WebGLRenderingContext, vertexShader: WebGLShader, fragmentShader: WebGLShader): WebGLProgram | null {
const program = gl.createProgram();
if (!program) return null;
gl.attachShader(program, vertexShader);
gl.attachShader(program, fragmentShader);
gl.linkProgram(program);
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
console.error('Program link error:', gl.getProgramInfoLog(program));
gl.deleteProgram(program);
return null;
}
return program;
}
function resizeCanvas() {
if (!canvas) return;
const dpr = window.devicePixelRatio || 1;
canvas.width = window.innerWidth * dpr;
canvas.height = window.innerHeight * dpr;
canvas.style.width = `${window.innerWidth}px`;
canvas.style.height = `${window.innerHeight}px`;
if (gl) {
gl.viewport(0, 0, canvas.width, canvas.height);
}
}
onMount(() => {
gl = canvas.getContext('webgl', { alpha: true, premultipliedAlpha: false });
if (!gl) {
console.error('WebGL not supported');
return;
}
console.log('WebGL context created');
const vShader = createShader(gl, gl.VERTEX_SHADER, vertexShader);
const fShader = createShader(gl, gl.FRAGMENT_SHADER, fragmentShader);
if (!vShader || !fShader) {
console.error('Shader creation failed');
return;
}
console.log('Shaders compiled successfully');
const program = createProgram(gl, vShader, fShader);
if (!program) return;
// Set up geometry (full screen quad)
const positionBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
-1, -1,
1, -1,
-1, 1,
-1, 1,
1, -1,
1, 1,
]), gl.STATIC_DRAW);
const positionLocation = gl.getAttribLocation(program, 'a_position');
const resolutionLocation = gl.getUniformLocation(program, 'u_resolution');
const timeLocation = gl.getUniformLocation(program, 'u_time');
const seedLocation = gl.getUniformLocation(program, 'u_seed');
const dprLocation = gl.getUniformLocation(program, 'u_dpr');
const scaleLocation = gl.getUniformLocation(program, 'u_scale');
const lengthLocation = gl.getUniformLocation(program, 'u_length');
const spacingLocation = gl.getUniformLocation(program, 'u_spacing');
const opacityLocation = gl.getUniformLocation(program, 'u_opacity');
const radiusLocation = gl.getUniformLocation(program, 'u_radius');
const angleTimeScaleLocation = gl.getUniformLocation(program, 'u_angleTimeScale');
const lengthTimeScaleLocation = gl.getUniformLocation(program, 'u_lengthTimeScale');
const angleOpacityAmpLocation = gl.getUniformLocation(program, 'u_angleOpacityAmp');
const angleOpacityFloorLocation = gl.getUniformLocation(program, 'u_angleOpacityFloor');
const randomOpacityMinLocation = gl.getUniformLocation(program, 'u_randomOpacityMin');
const randomOpacityMaxLocation = gl.getUniformLocation(program, 'u_randomOpacityMax');
gl.useProgram(program);
gl.enableVertexAttribArray(positionLocation);
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
gl.vertexAttribPointer(positionLocation, 2, gl.FLOAT, false, 0, 0);
// Enable blending for transparency
gl.enable(gl.BLEND);
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
const dpr = window.devicePixelRatio || 1;
// Set static uniforms (these don't change per frame)
gl.uniform1f(seedLocation, Math.random() * 1000);
gl.uniform1f(dprLocation, dpr);
gl.uniform1f(scaleLocation, SCALE);
gl.uniform1f(lengthLocation, LENGTH);
gl.uniform1f(spacingLocation, SPACING);
gl.uniform1f(opacityLocation, OPACITY);
gl.uniform1f(radiusLocation, RADIUS);
gl.uniform1f(angleTimeScaleLocation, ANGLE_TIME_SCALE);
gl.uniform1f(lengthTimeScaleLocation, LENGTH_TIME_SCALE);
gl.uniform1f(angleOpacityAmpLocation, ANGLE_OPACITY_AMPLITUDE);
gl.uniform1f(angleOpacityFloorLocation, ANGLE_OPACITY_FLOOR);
gl.uniform1f(randomOpacityMinLocation, RANDOM_OPACITY_MIN);
gl.uniform1f(randomOpacityMaxLocation, RANDOM_OPACITY_MAX);
resizeCanvas();
let startTime = Date.now();
function render() {
if (!gl || !canvas) return;
const time = (Date.now() - startTime) / 1000 * TIMESCALE;
gl.uniform2f(resolutionLocation, canvas.width, canvas.height);
gl.uniform1f(timeLocation, time);
gl.clearColor(0, 0, 0, 0);
gl.clear(gl.COLOR_BUFFER_BIT);
gl.drawArrays(gl.TRIANGLES, 0, 6);
animationId = requestAnimationFrame(render);
}
render();
window.addEventListener('resize', resizeCanvas);
return () => {
window.removeEventListener('resize', resizeCanvas);
};
});
onDestroy(() => {
if (animationId !== null) {
cancelAnimationFrame(animationId);
}
if (gl) {
gl.getExtension('WEBGL_lose_context')?.loseContext();
}
});
</script>
<!-- Dots overlay with fade-in animation -->
<canvas
bind:this={canvas}
class={cn(
"pointer-events-none fixed inset-0 -z-10",
className
)}
></canvas>
+1
View File
@@ -0,0 +1 @@
// place files you want to import through the `$lib` alias in this folder.
+33
View File
@@ -0,0 +1,33 @@
import { drizzle } from "drizzle-orm/postgres-js";
import postgres from "postgres";
import * as schema from "@xevion/db";
import { projectsRelations } from "@xevion/db";
const extendedSchema = {
...schema,
relations_projects: projectsRelations,
};
let _db: ReturnType<typeof drizzle> | null = null;
export function getDb() {
if (!_db) {
const connectionString = process.env.DATABASE_URL;
if (!connectionString) {
throw new Error("DATABASE_URL environment variable is not set");
}
const sql = postgres(connectionString);
_db = drizzle(sql, { schema: extendedSchema });
}
return _db;
}
// For backward compatibility
export const db = new Proxy({} as ReturnType<typeof drizzle>, {
get(target, prop) {
return getDb()[prop as keyof ReturnType<typeof drizzle>];
},
});
+6
View File
@@ -0,0 +1,6 @@
import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
+21
View File
@@ -0,0 +1,21 @@
<script lang="ts">
import "../app.css";
import "@fontsource-variable/inter";
import "@fontsource/hanken-grotesk/900.css";
import "@fontsource/schibsted-grotesk/400.css";
import "@fontsource/schibsted-grotesk/500.css";
import "@fontsource/schibsted-grotesk/600.css";
let { children } = $props();
</script>
<svelte:head>
<link rel="icon" href="/favicon.ico" />
<title>Xevion.dev</title>
<meta
name="description"
content="The personal website of Xevion, a full-stack software developer."
/>
</svelte:head>
{@render children()}
+85
View File
@@ -0,0 +1,85 @@
<script lang="ts">
import AppWrapper from "$lib/components/AppWrapper.svelte";
import IconSimpleIconsGithub from "~icons/simple-icons/github";
import IconSimpleIconsLinkedin from "~icons/simple-icons/linkedin";
import IconSimpleIconsDiscord from "~icons/simple-icons/discord";
import MaterialSymbolsMailRounded from "~icons/material-symbols/mail-rounded";
import MaterialSymbolsVpnKey from "~icons/material-symbols/vpn-key";
import IconLucideRss from "~icons/lucide/rss";
</script>
<AppWrapper class="overflow-x-hidden font-schibsted">
<!-- Top Navigation Bar -->
<div class="flex w-full justify-end items-center pt-5 px-6 pb-9">
<div class="flex gap-4 items-center">
<!-- <a href="/rss" class="text-zinc-400 hover:text-zinc-200">
<IconLucideRss class="size-5" />
</a> -->
</div>
</div>
<!-- Main Content -->
<div class="flex items-center flex-col">
<div
class="max-w-2xl mx-6 border-b border-zinc-700 divide-y divide-zinc-700"
>
<!-- Name & Occupation -->
<div class="flex flex-col pb-4">
<span class="text-3xl font-bold text-white">Ryan Walters,</span>
<span class="text-2xl font-normal text-zinc-400">
Full-Stack Software Engineer
</span>
</div>
<div class="py-4 text-zinc-200">
<p class="text-[0.95em]">
A fanatical software engineer with expertise and passion for sound,
scalable and high-performance applications. I'm always working on
something new. <br />
Sometimes innovative &mdash; sometimes crazy.
</p>
</div>
<div class="py-3">
<span class="text-zinc-200">Connect with me</span>
<div class="flex gap-x-2 pl-3 pt-3 pb-2">
<a
href="https://github.com/Xevion"
class="flex items-center gap-x-1.5 px-1.5 py-1 rounded-sm bg-zinc-900 shadow-sm hover:bg-zinc-800 transition-colors"
>
<IconSimpleIconsGithub class="size-4 text-zinc-300" />
<span class="text-sm text-zinc-100">GitHub</span>
</a>
<a
href="https://linkedin.com/in/ryancwalters"
class="flex items-center gap-x-1.5 px-1.5 py-1 rounded-sm bg-zinc-900 shadow-sm hover:bg-zinc-800 transition-colors"
>
<IconSimpleIconsLinkedin class="size-4 text-zinc-300" />
<span class="text-sm text-zinc-100">LinkedIn</span>
</a>
<a
href="#"
class="flex items-center gap-x-1.5 px-1.5 py-1 rounded-sm bg-zinc-900 shadow-sm hover:bg-zinc-800 transition-colors"
>
<IconSimpleIconsDiscord class="size-4 text-zinc-300" />
<span class="text-sm text-zinc-100">Discord</span>
</a>
<a
href="mailto:your.email@example.com"
class="flex items-center gap-x-1.5 px-1.5 py-1 rounded-sm bg-zinc-900 shadow-sm hover:bg-zinc-800 transition-colors"
>
<MaterialSymbolsMailRounded class="size-4.5 text-zinc-300" />
<span class="text-sm text-zinc-100">Email</span>
</a>
<a
href="#"
class="flex items-center gap-x-1.5 px-1.5 py-1 rounded-sm bg-zinc-900 shadow-sm hover:bg-zinc-800 transition-colors"
>
<MaterialSymbolsVpnKey class="size-4.5 text-zinc-300" />
<span class="text-sm text-zinc-100">PGP Key</span>
</a>
</div>
</div>
</div>
</div>
</AppWrapper>
+12
View File
@@ -0,0 +1,12 @@
<script lang="ts">
import AppWrapper from "$lib/components/AppWrapper.svelte";
</script>
<AppWrapper>
<div class="flex min-h-screen items-center justify-center">
<div class="text-center">
<h1 class="font-hanken text-4xl text-zinc-200 md:text-5xl">Blog</h1>
<p class="mt-4 text-lg text-zinc-400">Coming soon...</p>
</div>
</div>
</AppWrapper>
@@ -0,0 +1,21 @@
import { getDb } from "$lib/server/db";
import { projects } from "@xevion/db";
import { eq, desc } from "drizzle-orm";
import type { PageServerLoad } from "./$types";
export const load: PageServerLoad = async (event) => {
const db = getDb(event);
// Use Drizzle relations for efficient join query
const projectsWithLinks = await db.query.projects.findMany({
where: eq(projects.status, "published"),
orderBy: [desc(projects.updatedAt)],
with: {
links: true,
},
});
return {
projects: projectsWithLinks,
};
};
+61
View File
@@ -0,0 +1,61 @@
<script lang="ts">
import AppWrapper from "$lib/components/AppWrapper.svelte";
import Balancer from "svelte-wrap-balancer";
import { cn } from "$lib/utils";
let { data } = $props();
</script>
<AppWrapper>
<div
class="relative z-10 mx-auto grid grid-cols-1 justify-center gap-y-4 px-4 py-20 align-middle sm:grid-cols-2 md:max-w-[50rem] lg:max-w-[75rem] lg:grid-cols-3 lg:gap-y-9"
>
<div class="mb-3 text-center sm:col-span-2 md:mb-5 lg:col-span-3 lg:mb-7">
<h1 class="pb-3 font-hanken text-4xl text-zinc-200 opacity-100 md:text-5xl">
Projects
</h1>
<Balancer>
<p class="text-lg text-zinc-400">
created, maintained, or contributed to by me...
</p>
</Balancer>
</div>
{#each data.projects as project (project.id)}
{@const links = project.links}
{@const useAnchor = links.length > 0}
{@const href = useAnchor ? links[0].url : undefined}
<div class="max-w-fit">
<svelte:element
this={useAnchor ? "a" : "div"}
{href}
target={useAnchor ? "_blank" : undefined}
rel={useAnchor ? "noreferrer" : undefined}
title={project.name}
class="flex items-center justify-start overflow-hidden rounded bg-black/10 pb-2.5 pl-3 pr-5 pt-1 text-zinc-400 transition-colors hover:bg-zinc-500/10 hover:text-zinc-50"
>
<div class="flex h-full w-14 items-center justify-center pr-5">
<i
class={cn(
project.icon ?? "fa-heart",
"fa-solid text-3xl text-opacity-80 saturate-0"
)}
></i>
</div>
<div class="overflow-hidden">
<span class="text-sm md:text-base lg:text-lg">
{project.name}
</span>
<p
class="truncate text-xs opacity-70 md:text-sm lg:text-base"
title={project.shortDescription}
>
{project.shortDescription}
</p>
</div>
</svelte:element>
</div>
{/each}
</div>
</AppWrapper>
+15
View File
@@ -0,0 +1,15 @@
import { redirect } from "@sveltejs/kit";
import { getDb } from "$lib/server/db";
import { metadata } from "@xevion/db";
import type { RequestHandler } from "./$types";
export const GET: RequestHandler = async (event) => {
const db = getDb(event);
// TODO: Query the metadata global for resume URL
// For now, redirect to a placeholder
// const meta = await db.select().from(metadata).limit(1);
// Placeholder redirect until we have the schema set up
redirect(302, "https://example.com/resume.pdf");
};

Before

Width:  |  Height:  |  Size: 5.2 KiB

After

Width:  |  Height:  |  Size: 5.2 KiB

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Before

Width:  |  Height:  |  Size: 4.7 KiB

After

Width:  |  Height:  |  Size: 4.7 KiB

Before

Width:  |  Height:  |  Size: 352 B

After

Width:  |  Height:  |  Size: 352 B

Before

Width:  |  Height:  |  Size: 648 B

After

Width:  |  Height:  |  Size: 648 B

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

+3
View File
@@ -0,0 +1,3 @@
# allow crawling everything by default
User-agent: *
Disallow:
+19
View File
@@ -0,0 +1,19 @@
import adapter from "svelte-adapter-bun";
import { vitePreprocess } from "@sveltejs/vite-plugin-svelte";
/** @type {import('@sveltejs/kit').Config} */
const config = {
preprocess: vitePreprocess(),
kit: {
adapter: adapter({
out: "build",
precompress: false,
}),
alias: {
$components: "src/lib/components",
},
},
};
export default config;
+21
View File
@@ -0,0 +1,21 @@
{
"extends": "./.svelte-kit/tsconfig.json",
"compilerOptions": {
"rewriteRelativeImportExtensions": true,
"allowJs": true,
"checkJs": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"skipLibCheck": true,
"sourceMap": true,
"strict": true,
"moduleResolution": "bundler",
"types": ["unplugin-icons/types/svelte"]
}
// Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias
// except $lib which is handled by https://svelte.dev/docs/kit/configuration#files
//
// To make changes to top-level options such as include and exclude, we recommend extending
// the generated config; see https://svelte.dev/docs/kit/configuration#typescript
}
+8
View File
@@ -0,0 +1,8 @@
import { sveltekit } from "@sveltejs/kit/vite";
import tailwindcss from "@tailwindcss/vite";
import { defineConfig } from "vite";
import Icons from "unplugin-icons/vite";
export default defineConfig({
plugins: [tailwindcss(), sveltekit(), Icons({ compiler: "svelte" })],
});
+2674
View File
File diff suppressed because it is too large Load Diff
-23
View File
@@ -1,23 +0,0 @@
version: "3.8"
services:
postgres:
image: postgres:16-alpine
container_name: xevion-postgres
restart: unless-stopped
ports:
- "5432:5432"
environment:
POSTGRES_USER: xevion
POSTGRES_PASSWORD: xevion_dev_password
POSTGRES_DB: xevion_dev
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U xevion -d xevion_dev"]
interval: 10s
timeout: 5s
retries: 5
volumes:
postgres_data:
+218
View File
@@ -0,0 +1,218 @@
#!/usr/bin/env bun
/**
* Production entrypoint for xevion.dev Docker container.
* Manages environment validation, database migrations, and service orchestration.
*/
import { spawn, type Subprocess } from "bun";
const log = (
level: "info" | "warn" | "error",
message: string,
data?: Record<string, unknown>,
) => {
const entry = {
level,
timestamp: new Date().toISOString(),
message,
...data,
};
console.log(JSON.stringify(entry));
};
function validateEnvironment(): void {
log("info", "starting deployment", { service: "entrypoint" });
const required = ["DATABASE_URL", "PAYLOAD_SECRET"] as const;
const optional = ["R2_BUCKET_NAME"] as const;
for (const key of required) {
if (!process.env[key]) {
log("error", "missing required environment variable", {
service: "entrypoint",
variable: key,
});
process.exit(1);
}
}
const missing: string[] = [];
for (const key of optional) {
if (!process.env[key]) {
missing.push(key);
}
}
if (missing.length > 0) {
log("warn", "missing optional environment variables", {
service: "entrypoint",
variables: missing,
});
}
log("info", "environment validated", { service: "entrypoint" });
}
async function runMigrations(): Promise<void> {
log("info", "migrations will run on first payload start", {
service: "entrypoint",
});
}
function displayConfig(): void {
const config = {
service: "entrypoint",
port: process.env.PORT || "3000",
nodeEnv: process.env.NODE_ENV || "production",
r2Bucket: process.env.R2_BUCKET_NAME || null,
runtime: `Bun ${Bun.version}`,
};
log("info", "configuration", config);
}
interface Service {
name: string;
proc: Subprocess;
}
async function startServices(): Promise<Service[]> {
const port = process.env.PORT || "3000";
const services: Service[] = [];
const caddy = spawn({
cmd: ["caddy", "run", "--config", "Caddyfile"],
cwd: "/app",
stdout: "pipe",
stderr: "pipe",
});
services.push({ name: "caddy", proc: caddy });
log("info", "service started", { service: "caddy", pid: caddy.pid, port });
// Transform Caddy's JSON logs to match our format
(async () => {
for await (const chunk of caddy.stderr) {
const lines = new TextDecoder().decode(chunk).trim().split("\n");
for (const line of lines) {
if (!line) continue;
try {
const caddyLog = JSON.parse(line);
log(caddyLog.level || "info", caddyLog.msg || caddyLog.message, {
service: "caddy",
logger: caddyLog.logger,
...Object.fromEntries(
Object.entries(caddyLog).filter(
([k]) =>
!["level", "ts", "msg", "message", "logger"].includes(k),
),
),
});
} catch {
// Not JSON, log as-is
log("info", line, { service: "caddy" });
}
}
}
})();
const payload = spawn({
cmd: ["bun", "/app/apps/payload/.next/standalone/apps/payload/server.js"],
cwd: "/app",
env: {
...process.env,
PORT: "5001",
HOSTNAME: "0.0.0.0",
NEXT_TELEMETRY_DISABLED: "1",
},
stdout: "pipe",
stderr: "pipe",
});
services.push({ name: "payload", proc: payload });
log("info", "service started", {
service: "payload",
pid: payload.pid,
port: 5001,
});
// Log stdout and stderr from Next.js
(async () => {
for await (const chunk of payload.stdout) {
const line = new TextDecoder().decode(chunk).trim();
if (line) log("info", line, { service: "payload" });
}
})();
(async () => {
for await (const chunk of payload.stderr) {
const line = new TextDecoder().decode(chunk).trim();
if (line) log("error", line, { service: "payload" });
}
})();
const web = spawn({
cmd: ["/app/apps/web/web-server"],
cwd: "/app",
env: { ...process.env, PORT: "5000", VERBOSE: "false" },
stdout: "pipe",
stderr: "inherit",
});
services.push({ name: "web", proc: web });
log("info", "service started", { service: "web", pid: web.pid, port: 5000 });
log("info", "all services started", { service: "entrypoint" });
return services;
}
function setupShutdownHandlers(services: Service[]): void {
const cleanup = async () => {
log("info", "shutdown signal received", { service: "entrypoint" });
for (const service of services) {
try {
service.proc.kill();
} catch {
// Process already dead
}
}
await Promise.allSettled(services.map((s) => s.proc.exited));
log("info", "all services stopped", { service: "entrypoint" });
process.exit(0);
};
process.on("SIGTERM", cleanup);
process.on("SIGINT", cleanup);
}
async function main(): Promise<void> {
validateEnvironment();
await runMigrations();
displayConfig();
const services = await startServices();
setupShutdownHandlers(services);
const results = await Promise.allSettled(services.map((s) => s.proc.exited));
const failures = results.filter(
(r) => r.status === "rejected" || r.value !== 0,
);
if (failures.length > 0) {
log("error", "service exited unexpectedly", {
service: "entrypoint",
count: failures.length,
});
process.exit(1);
}
}
main().catch((error) => {
log("error", "fatal error", {
service: "entrypoint",
error: error.message,
stack: error.stack,
});
process.exit(1);
});
-19
View File
@@ -1,19 +0,0 @@
import { dirname } from "path";
import { fileURLToPath } from "url";
import { FlatCompat } from "@eslint/eslintrc";
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const compat = new FlatCompat({
baseDirectory: __dirname,
});
const eslintConfig = [
...compat.extends("next/core-web-vitals"),
{
ignores: [".next/**", "out/**", "build/**", "next-env.d.ts"],
},
];
export default eslintConfig;
-96
View File
@@ -1,96 +0,0 @@
import { withPayload } from "@payloadcms/next/withPayload";
// @ts-check
/**
* Run `build` or `dev` with `SKIP_ENV_VALIDATION` to skip env validation.
* This is especially useful for Docker builds.
*/
if (!process.env.SKIP_ENV_VALIDATION) await import("./src/env/server.mjs");
/**
*
* @param {string} text The string to search around with the pattern in.
* @param {string} pattern The strict, text only pattern to search for.
* @param {number} nth The index of the pattern to find.
* @returns
*/
function nthIndex(text, pattern, nth) {
const L = text.length;
let i = -1;
while (nth-- && i++ < L) {
i = text.indexOf(pattern, i);
if (i < 0) break;
}
return i;
}
const v2_redirects = [
"/2020/12/04/jekyll-github-pages-and-azabani",
"/2021/02/25/project-facelift-new-and-old",
"/2022/03/29/runnerspace-built-in-under-30-hours",
"/2022/07/16/restricted-memory-and-data-framing-tricks",
"/drafts/2022-09-19-presenting-to-humans/",
"/photography",
].map((url) => {
// If the URL starts with /2, redirect to the new blog. Otherwise, redirect to the old v2 blog to maintain URLs.
if (url.startsWith("/2"))
return {
source: url,
destination: `https://undefined.behavio.rs/posts${url.slice(
nthIndex(url, "/", 4),
)}`,
permanent: false,
};
return {
source: url,
destination: `https://v2.xevion.dev${url}`,
permanent: false,
};
});
/** @type {import("next").NextConfig} */
const config = {
reactStrictMode: true,
images: {
remotePatterns: [
{
protocol: "https",
hostname: "img.walters.to",
},
{
protocol: "https",
hostname: "img.xevion.dev",
},
{
protocol: "https",
hostname: "api.xevion.dev",
},
],
},
turbopack: {
resolveExtensions: ['.tsx', '.ts', '.jsx', '.js', '.mjs', '.json'],
},
async redirects() {
// Source cannot end with / slash
return [
...[
"resume.pdf",
"resume.docx",
"resume.txt",
"resum",
"resumee",
"cv",
"cover.pdf",
"cv.docx",
"cv.pdf",
].map((ext) => ({
source: `/${ext}`,
destination: "/resume",
permanent: true,
})),
...v2_redirects,
];
},
};
export default withPayload(config);
+29 -55
View File
@@ -3,62 +3,36 @@
"private": true,
"type": "module",
"scripts": {
"preinstall": "npx only-allow pnpm",
"build": "next build",
"dev": "next dev --turbopack",
"lint": "eslint .",
"start": "next start",
"db:start": "docker compose up -d",
"db:stop": "docker compose down",
"db:reset": "docker compose down -v && docker compose up -d"
},
"dependencies": {
"@fontsource-variable/inter": "^5.1.0",
"@fontsource-variable/roboto": "^5.1.0",
"@fontsource-variable/roboto-mono": "^5.1.0",
"@fontsource/hanken-grotesk": "^5.1.0",
"@fontsource/schibsted-grotesk": "^5.2.8",
"@icons-pack/react-simple-icons": "^13.8.0",
"@mantine/hooks": "^8",
"@next/eslint-plugin-next": "^15.1.1",
"@octokit/core": "^7.0.5",
"@payloadcms/db-postgres": "^3.61.1",
"@payloadcms/next": "^3.61.1",
"@payloadcms/payload-cloud": "^3.61.1",
"@payloadcms/richtext-lexical": "^3.61.1",
"@radix-ui/themes": "^3.2.1",
"@tanstack/react-query": "^5.90",
"@vercel/analytics": "^1.5.0",
"clsx": "^2.1.1",
"cssnano": "^7.1.1",
"graphql": "^16.11.0",
"lucide-react": "^0.548.0",
"next": "^15.5.6",
"p5i": "^0.6.0",
"payload": "^3.61.1",
"radix-ui": "^1.4.3",
"react": "19.2.0",
"react-dom": "19.2.0",
"react-icons": "^5.5.0",
"react-markdown": "^10.1.0",
"react-wrap-balancer": "^1",
"sharp": "^0.34",
"superjson": "^2.2",
"tailwind-merge": "^3.3.1",
"zod": "^4.1.12"
"build": "turbo run build",
"dev": "turbo run dev --filter=@xevion/web --env-mode=loose",
"dev:local": "turbo run dev --filter=@xevion/web --env-mode=loose",
"dev:remote": "turbo run dev:remote --filter=@xevion/web --env-mode=loose",
"dev:payload": "turbo run dev --filter=@xevion/payload --env-mode=loose",
"preview": "turbo run preview --filter=@xevion/web",
"preview:payload": "turbo run preview --filter=@xevion/payload",
"lint": "turbo run lint",
"check": "turbo run check",
"type-check": "turbo run type-check",
"test": "turbo run test",
"deploy": "turbo run deploy",
"clean": "turbo run clean && rm -rf node_modules .turbo",
"graph": "turbo run build --graph=dependency-graph.html",
"graph:dev": "turbo run dev --graph=dev-graph.html --dry-run",
"payload": "bun --filter=@xevion/payload payload",
"docker:build": "docker build -t xevion.dev .",
"docker:run": "docker run -p 3000:3000 --env-file <(cat .env 2>/dev/null || echo 'DATABASE_URL=postgresql://xevion:xevion@host.docker.internal:5432/xevion'; echo 'PAYLOAD_SECRET=development-secret-change-in-production'; echo 'ORIGIN=http://localhost:3000') xevion.dev",
"docker:db": "docker run --name xevion-db -p 5432:5432 -e POSTGRES_USER=xevion -e POSTGRES_PASSWORD=xevion -e POSTGRES_DB=xevion -d postgres"
},
"dependencies": {},
"devDependencies": {
"@eslint/eslintrc": "^3.3.1",
"@eslint/js": "^9.38.0",
"@tailwindcss/postcss": "^4",
"@types/node": "^24.9.1",
"@types/react": "^19.2.2",
"@types/react-dom": "^19.2.2",
"eslint": "^9.38.0",
"eslint-config-next": "^15.1.1",
"prettier": "^3.6.2",
"tailwindcss": "^4",
"typescript": "^5.7.2"
"@types/bun": "^1.3.5",
"prettier": "^3.7.4",
"turbo": "^2.3.4",
"typescript": "^5.9.3"
},
"packageManager": "pnpm@9.15.1+sha512.1acb565e6193efbebda772702950469150cf12bcc764262e7587e71d19dc98a423dff9536e57ea44c49bdf790ff694e83c27be5faa23d67e0c033b583be4bfcf"
"packageManager": "bun@1.3.5",
"workspaces": [
"apps/*",
"packages/*"
]
}
+25
View File
@@ -0,0 +1,25 @@
{
"name": "@xevion/db",
"version": "0.0.0",
"private": true,
"type": "module",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
}
},
"scripts": {
"build": "tsc",
"dev": "tsc --watch",
"type-check": "tsc --noEmit",
"clean": "rm -rf dist .turbo node_modules *.tsbuildinfo"
},
"dependencies": {
"drizzle-orm": "0.44.7",
"@payloadcms/db-postgres": "^3.69.0"
},
"devDependencies": {
"@xevion/typescript-config": "workspace:*"
}
}
+5
View File
@@ -0,0 +1,5 @@
// Export the schema generated by PayloadCMS
export * from "./schema";
// Export custom relations that extend Payload's auto-generated schema
export { projectsRelations } from "./relations";
+24
View File
@@ -0,0 +1,24 @@
// Custom Drizzle relations that extend the auto-generated Payload schema
// This file adds the reverse relationships that Payload doesn't auto-generate
import { relations } from "drizzle-orm";
import { projects, links, technologies, projects_rels, media } from "./schema";
// Override the auto-generated relations_projects to add links
export const projectsRelations = relations(projects, ({ one, many }) => ({
bannerImage: one(media, {
fields: [projects.bannerImage],
references: [media.id],
relationName: "bannerImage",
}),
_rels: many(projects_rels, {
relationName: "_rels",
}),
// Add the reverse relationship for links
links: many(links, {
relationName: "project",
}),
}));
// Note: Other relations are auto-generated and exported from schema.ts
// We only override the ones that need custom reverse relationships
+630
View File
@@ -0,0 +1,630 @@
/* tslint:disable */
/* eslint-disable */
/**
* This file was automatically generated by Payload.
* DO NOT MODIFY IT BY HAND. Instead, modify your source Payload config,
* and re-run `payload generate:db-schema` to regenerate this file.
*/
import type {} from "@payloadcms/db-postgres";
import {
pgTable,
index,
uniqueIndex,
foreignKey,
integer,
varchar,
timestamp,
serial,
numeric,
boolean,
jsonb,
pgEnum,
} from "@payloadcms/db-postgres/drizzle/pg-core";
import { sql, relations } from "@payloadcms/db-postgres/drizzle";
export const enum_projects_status = pgEnum("enum_projects_status", [
"draft",
"published",
"archived",
]);
export const users_sessions = pgTable(
"users_sessions",
{
_order: integer("_order").notNull(),
_parentID: integer("_parent_id").notNull(),
id: varchar("id").primaryKey(),
createdAt: timestamp("created_at", {
mode: "string",
withTimezone: true,
precision: 3,
}),
expiresAt: timestamp("expires_at", {
mode: "string",
withTimezone: true,
precision: 3,
}).notNull(),
},
(columns) => [
index("users_sessions_order_idx").on(columns._order),
index("users_sessions_parent_id_idx").on(columns._parentID),
foreignKey({
columns: [columns["_parentID"]],
foreignColumns: [users.id],
name: "users_sessions_parent_id_fk",
}).onDelete("cascade"),
],
);
export const users = pgTable(
"users",
{
id: serial("id").primaryKey(),
updatedAt: timestamp("updated_at", {
mode: "string",
withTimezone: true,
precision: 3,
})
.defaultNow()
.notNull(),
createdAt: timestamp("created_at", {
mode: "string",
withTimezone: true,
precision: 3,
})
.defaultNow()
.notNull(),
email: varchar("email").notNull(),
resetPasswordToken: varchar("reset_password_token"),
resetPasswordExpiration: timestamp("reset_password_expiration", {
mode: "string",
withTimezone: true,
precision: 3,
}),
salt: varchar("salt"),
hash: varchar("hash"),
loginAttempts: numeric("login_attempts", { mode: "number" }).default(0),
lockUntil: timestamp("lock_until", {
mode: "string",
withTimezone: true,
precision: 3,
}),
},
(columns) => [
index("users_updated_at_idx").on(columns.updatedAt),
index("users_created_at_idx").on(columns.createdAt),
uniqueIndex("users_email_idx").on(columns.email),
],
);
export const media = pgTable(
"media",
{
id: serial("id").primaryKey(),
alt: varchar("alt").notNull(),
updatedAt: timestamp("updated_at", {
mode: "string",
withTimezone: true,
precision: 3,
})
.defaultNow()
.notNull(),
createdAt: timestamp("created_at", {
mode: "string",
withTimezone: true,
precision: 3,
})
.defaultNow()
.notNull(),
url: varchar("url"),
thumbnailURL: varchar("thumbnail_u_r_l"),
filename: varchar("filename"),
mimeType: varchar("mime_type"),
filesize: numeric("filesize", { mode: "number" }),
width: numeric("width", { mode: "number" }),
height: numeric("height", { mode: "number" }),
focalX: numeric("focal_x", { mode: "number" }),
focalY: numeric("focal_y", { mode: "number" }),
},
(columns) => [
index("media_updated_at_idx").on(columns.updatedAt),
index("media_created_at_idx").on(columns.createdAt),
uniqueIndex("media_filename_idx").on(columns.filename),
],
);
export const projects = pgTable(
"projects",
{
id: serial("id").primaryKey(),
name: varchar("name").notNull(),
description: varchar("description").notNull(),
shortDescription: varchar("short_description").notNull(),
icon: varchar("icon"),
status: enum_projects_status("status").notNull().default("draft"),
featured: boolean("featured").default(false),
autocheckUpdated: boolean("autocheck_updated").default(false),
lastUpdated: timestamp("last_updated", {
mode: "string",
withTimezone: true,
precision: 3,
}),
wakatimeOffset: numeric("wakatime_offset", { mode: "number" }),
bannerImage: integer("banner_image_id").references(() => media.id, {
onDelete: "set null",
}),
updatedAt: timestamp("updated_at", {
mode: "string",
withTimezone: true,
precision: 3,
})
.defaultNow()
.notNull(),
createdAt: timestamp("created_at", {
mode: "string",
withTimezone: true,
precision: 3,
})
.defaultNow()
.notNull(),
},
(columns) => [
index("projects_banner_image_idx").on(columns.bannerImage),
index("projects_updated_at_idx").on(columns.updatedAt),
index("projects_created_at_idx").on(columns.createdAt),
],
);
export const projects_rels = pgTable(
"projects_rels",
{
id: serial("id").primaryKey(),
order: integer("order"),
parent: integer("parent_id").notNull(),
path: varchar("path").notNull(),
technologiesID: integer("technologies_id"),
},
(columns) => [
index("projects_rels_order_idx").on(columns.order),
index("projects_rels_parent_idx").on(columns.parent),
index("projects_rels_path_idx").on(columns.path),
index("projects_rels_technologies_id_idx").on(columns.technologiesID),
foreignKey({
columns: [columns["parent"]],
foreignColumns: [projects.id],
name: "projects_rels_parent_fk",
}).onDelete("cascade"),
foreignKey({
columns: [columns["technologiesID"]],
foreignColumns: [technologies.id],
name: "projects_rels_technologies_fk",
}).onDelete("cascade"),
],
);
export const technologies = pgTable(
"technologies",
{
id: serial("id").primaryKey(),
name: varchar("name").notNull(),
url: varchar("url"),
updatedAt: timestamp("updated_at", {
mode: "string",
withTimezone: true,
precision: 3,
})
.defaultNow()
.notNull(),
createdAt: timestamp("created_at", {
mode: "string",
withTimezone: true,
precision: 3,
})
.defaultNow()
.notNull(),
},
(columns) => [
index("technologies_updated_at_idx").on(columns.updatedAt),
index("technologies_created_at_idx").on(columns.createdAt),
],
);
export const links = pgTable(
"links",
{
id: serial("id").primaryKey(),
url: varchar("url").notNull(),
icon: varchar("icon"),
description: varchar("description"),
project: integer("project_id")
.notNull()
.references(() => projects.id, {
onDelete: "set null",
}),
updatedAt: timestamp("updated_at", {
mode: "string",
withTimezone: true,
precision: 3,
})
.defaultNow()
.notNull(),
createdAt: timestamp("created_at", {
mode: "string",
withTimezone: true,
precision: 3,
})
.defaultNow()
.notNull(),
},
(columns) => [
index("links_project_idx").on(columns.project),
index("links_updated_at_idx").on(columns.updatedAt),
index("links_created_at_idx").on(columns.createdAt),
],
);
export const payload_kv = pgTable(
"payload_kv",
{
id: serial("id").primaryKey(),
key: varchar("key").notNull(),
data: jsonb("data").notNull(),
},
(columns) => [uniqueIndex("payload_kv_key_idx").on(columns.key)],
);
export const payload_locked_documents = pgTable(
"payload_locked_documents",
{
id: serial("id").primaryKey(),
globalSlug: varchar("global_slug"),
updatedAt: timestamp("updated_at", {
mode: "string",
withTimezone: true,
precision: 3,
})
.defaultNow()
.notNull(),
createdAt: timestamp("created_at", {
mode: "string",
withTimezone: true,
precision: 3,
})
.defaultNow()
.notNull(),
},
(columns) => [
index("payload_locked_documents_global_slug_idx").on(columns.globalSlug),
index("payload_locked_documents_updated_at_idx").on(columns.updatedAt),
index("payload_locked_documents_created_at_idx").on(columns.createdAt),
],
);
export const payload_locked_documents_rels = pgTable(
"payload_locked_documents_rels",
{
id: serial("id").primaryKey(),
order: integer("order"),
parent: integer("parent_id").notNull(),
path: varchar("path").notNull(),
usersID: integer("users_id"),
mediaID: integer("media_id"),
projectsID: integer("projects_id"),
technologiesID: integer("technologies_id"),
linksID: integer("links_id"),
},
(columns) => [
index("payload_locked_documents_rels_order_idx").on(columns.order),
index("payload_locked_documents_rels_parent_idx").on(columns.parent),
index("payload_locked_documents_rels_path_idx").on(columns.path),
index("payload_locked_documents_rels_users_id_idx").on(columns.usersID),
index("payload_locked_documents_rels_media_id_idx").on(columns.mediaID),
index("payload_locked_documents_rels_projects_id_idx").on(
columns.projectsID,
),
index("payload_locked_documents_rels_technologies_id_idx").on(
columns.technologiesID,
),
index("payload_locked_documents_rels_links_id_idx").on(columns.linksID),
foreignKey({
columns: [columns["parent"]],
foreignColumns: [payload_locked_documents.id],
name: "payload_locked_documents_rels_parent_fk",
}).onDelete("cascade"),
foreignKey({
columns: [columns["usersID"]],
foreignColumns: [users.id],
name: "payload_locked_documents_rels_users_fk",
}).onDelete("cascade"),
foreignKey({
columns: [columns["mediaID"]],
foreignColumns: [media.id],
name: "payload_locked_documents_rels_media_fk",
}).onDelete("cascade"),
foreignKey({
columns: [columns["projectsID"]],
foreignColumns: [projects.id],
name: "payload_locked_documents_rels_projects_fk",
}).onDelete("cascade"),
foreignKey({
columns: [columns["technologiesID"]],
foreignColumns: [technologies.id],
name: "payload_locked_documents_rels_technologies_fk",
}).onDelete("cascade"),
foreignKey({
columns: [columns["linksID"]],
foreignColumns: [links.id],
name: "payload_locked_documents_rels_links_fk",
}).onDelete("cascade"),
],
);
export const payload_preferences = pgTable(
"payload_preferences",
{
id: serial("id").primaryKey(),
key: varchar("key"),
value: jsonb("value"),
updatedAt: timestamp("updated_at", {
mode: "string",
withTimezone: true,
precision: 3,
})
.defaultNow()
.notNull(),
createdAt: timestamp("created_at", {
mode: "string",
withTimezone: true,
precision: 3,
})
.defaultNow()
.notNull(),
},
(columns) => [
index("payload_preferences_key_idx").on(columns.key),
index("payload_preferences_updated_at_idx").on(columns.updatedAt),
index("payload_preferences_created_at_idx").on(columns.createdAt),
],
);
export const payload_preferences_rels = pgTable(
"payload_preferences_rels",
{
id: serial("id").primaryKey(),
order: integer("order"),
parent: integer("parent_id").notNull(),
path: varchar("path").notNull(),
usersID: integer("users_id"),
},
(columns) => [
index("payload_preferences_rels_order_idx").on(columns.order),
index("payload_preferences_rels_parent_idx").on(columns.parent),
index("payload_preferences_rels_path_idx").on(columns.path),
index("payload_preferences_rels_users_id_idx").on(columns.usersID),
foreignKey({
columns: [columns["parent"]],
foreignColumns: [payload_preferences.id],
name: "payload_preferences_rels_parent_fk",
}).onDelete("cascade"),
foreignKey({
columns: [columns["usersID"]],
foreignColumns: [users.id],
name: "payload_preferences_rels_users_fk",
}).onDelete("cascade"),
],
);
export const payload_migrations = pgTable(
"payload_migrations",
{
id: serial("id").primaryKey(),
name: varchar("name"),
batch: numeric("batch", { mode: "number" }),
updatedAt: timestamp("updated_at", {
mode: "string",
withTimezone: true,
precision: 3,
})
.defaultNow()
.notNull(),
createdAt: timestamp("created_at", {
mode: "string",
withTimezone: true,
precision: 3,
})
.defaultNow()
.notNull(),
},
(columns) => [
index("payload_migrations_updated_at_idx").on(columns.updatedAt),
index("payload_migrations_created_at_idx").on(columns.createdAt),
],
);
export const metadata = pgTable(
"metadata",
{
id: serial("id").primaryKey(),
tagline: varchar("tagline").notNull(),
resume: integer("resume_id")
.notNull()
.references(() => media.id, {
onDelete: "set null",
}),
resumeFilename: varchar("resume_filename"),
updatedAt: timestamp("updated_at", {
mode: "string",
withTimezone: true,
precision: 3,
}),
createdAt: timestamp("created_at", {
mode: "string",
withTimezone: true,
precision: 3,
}),
},
(columns) => [index("metadata_resume_idx").on(columns.resume)],
);
export const relations_users_sessions = relations(
users_sessions,
({ one }) => ({
_parentID: one(users, {
fields: [users_sessions._parentID],
references: [users.id],
relationName: "sessions",
}),
}),
);
export const relations_users = relations(users, ({ many }) => ({
sessions: many(users_sessions, {
relationName: "sessions",
}),
}));
export const relations_media = relations(media, () => ({}));
export const relations_projects_rels = relations(projects_rels, ({ one }) => ({
parent: one(projects, {
fields: [projects_rels.parent],
references: [projects.id],
relationName: "_rels",
}),
technologiesID: one(technologies, {
fields: [projects_rels.technologiesID],
references: [technologies.id],
relationName: "technologies",
}),
}));
export const relations_projects = relations(projects, ({ one, many }) => ({
bannerImage: one(media, {
fields: [projects.bannerImage],
references: [media.id],
relationName: "bannerImage",
}),
_rels: many(projects_rels, {
relationName: "_rels",
}),
}));
export const relations_technologies = relations(technologies, () => ({}));
export const relations_links = relations(links, ({ one }) => ({
project: one(projects, {
fields: [links.project],
references: [projects.id],
relationName: "project",
}),
}));
export const relations_payload_kv = relations(payload_kv, () => ({}));
export const relations_payload_locked_documents_rels = relations(
payload_locked_documents_rels,
({ one }) => ({
parent: one(payload_locked_documents, {
fields: [payload_locked_documents_rels.parent],
references: [payload_locked_documents.id],
relationName: "_rels",
}),
usersID: one(users, {
fields: [payload_locked_documents_rels.usersID],
references: [users.id],
relationName: "users",
}),
mediaID: one(media, {
fields: [payload_locked_documents_rels.mediaID],
references: [media.id],
relationName: "media",
}),
projectsID: one(projects, {
fields: [payload_locked_documents_rels.projectsID],
references: [projects.id],
relationName: "projects",
}),
technologiesID: one(technologies, {
fields: [payload_locked_documents_rels.technologiesID],
references: [technologies.id],
relationName: "technologies",
}),
linksID: one(links, {
fields: [payload_locked_documents_rels.linksID],
references: [links.id],
relationName: "links",
}),
}),
);
export const relations_payload_locked_documents = relations(
payload_locked_documents,
({ many }) => ({
_rels: many(payload_locked_documents_rels, {
relationName: "_rels",
}),
}),
);
export const relations_payload_preferences_rels = relations(
payload_preferences_rels,
({ one }) => ({
parent: one(payload_preferences, {
fields: [payload_preferences_rels.parent],
references: [payload_preferences.id],
relationName: "_rels",
}),
usersID: one(users, {
fields: [payload_preferences_rels.usersID],
references: [users.id],
relationName: "users",
}),
}),
);
export const relations_payload_preferences = relations(
payload_preferences,
({ many }) => ({
_rels: many(payload_preferences_rels, {
relationName: "_rels",
}),
}),
);
export const relations_payload_migrations = relations(
payload_migrations,
() => ({}),
);
export const relations_metadata = relations(metadata, ({ one }) => ({
resume: one(media, {
fields: [metadata.resume],
references: [media.id],
relationName: "resume",
}),
}));
type DatabaseSchema = {
enum_projects_status: typeof enum_projects_status;
users_sessions: typeof users_sessions;
users: typeof users;
media: typeof media;
projects: typeof projects;
projects_rels: typeof projects_rels;
technologies: typeof technologies;
links: typeof links;
payload_kv: typeof payload_kv;
payload_locked_documents: typeof payload_locked_documents;
payload_locked_documents_rels: typeof payload_locked_documents_rels;
payload_preferences: typeof payload_preferences;
payload_preferences_rels: typeof payload_preferences_rels;
payload_migrations: typeof payload_migrations;
metadata: typeof metadata;
relations_users_sessions: typeof relations_users_sessions;
relations_users: typeof relations_users;
relations_media: typeof relations_media;
relations_projects_rels: typeof relations_projects_rels;
relations_projects: typeof relations_projects;
relations_technologies: typeof relations_technologies;
relations_links: typeof relations_links;
relations_payload_kv: typeof relations_payload_kv;
relations_payload_locked_documents_rels: typeof relations_payload_locked_documents_rels;
relations_payload_locked_documents: typeof relations_payload_locked_documents;
relations_payload_preferences_rels: typeof relations_payload_preferences_rels;
relations_payload_preferences: typeof relations_payload_preferences;
relations_payload_migrations: typeof relations_payload_migrations;
relations_metadata: typeof relations_metadata;
};
declare module "@payloadcms/db-postgres" {
export interface GeneratedDatabaseSchema {
schema: DatabaseSchema;
}
}
+8
View File
@@ -0,0 +1,8 @@
{
"extends": "@xevion/typescript-config/base.json",
"compilerOptions": {
"outDir": "./dist",
"rootDir": "./src"
},
"include": ["src/**/*"]
}
+21
View File
@@ -0,0 +1,21 @@
{
"name": "@xevion/types",
"version": "0.0.0",
"private": true,
"type": "module",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
}
},
"scripts": {
"build": "tsc",
"dev": "tsc --watch",
"type-check": "tsc --noEmit",
"clean": "rm -rf dist .turbo node_modules *.tsbuildinfo"
},
"devDependencies": {
"@xevion/typescript-config": "workspace:*"
}
}
+3
View File
@@ -0,0 +1,3 @@
// This file will be populated by Payload's generate:types command
// Placeholder export
export type PayloadTypes = Record<string, any>;
+8
View File
@@ -0,0 +1,8 @@
{
"extends": "@xevion/typescript-config/base.json",
"compilerOptions": {
"outDir": "./dist",
"rootDir": "./src"
},
"include": ["src/**/*"]
}
+17
View File
@@ -0,0 +1,17 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"compilerOptions": {
"target": "ES2022",
"lib": ["ES2022"],
"module": "ESNext",
"moduleResolution": "bundler",
"esModuleInterop": true,
"strict": true,
"skipLibCheck": true,
"resolveJsonModule": true,
"isolatedModules": true,
"verbatimModuleSyntax": true,
"declaration": true,
"declarationMap": true
}
}
+9
View File
@@ -0,0 +1,9 @@
{
"name": "@xevion/typescript-config",
"version": "0.0.0",
"private": true,
"license": "MIT",
"publishConfig": {
"access": "public"
}
}
-11258
View File
File diff suppressed because it is too large Load Diff
-6
View File
@@ -1,6 +0,0 @@
export default {
plugins: {
"@tailwindcss/postcss": {},
...(process.env.NODE_ENV === "production" ? { cssnano: {} } : {}),
},
};
-36
View File
@@ -1,36 +0,0 @@
import React from "react";
// Fontsource imports
import "@fontsource-variable/inter";
import "@fontsource-variable/roboto";
import "@fontsource-variable/roboto-mono";
import "@fontsource/hanken-grotesk/900.css";
import "@fontsource/schibsted-grotesk/400.css";
import "@fontsource/schibsted-grotesk/500.css";
import "@fontsource/schibsted-grotesk/600.css";
import "@radix-ui/themes/styles.css";
import "@/styles/globals.css";
import type { Metadata } from "next";
import { Providers } from "./providers";
export const metadata: Metadata = {
title: "Xevion.dev",
description:
"The personal website of Xevion, a full-stack software developer.",
applicationName: "xevion.dev",
};
export default async function RootLayout(props: { children: React.ReactNode }) {
const { children } = props;
return (
<html lang="en">
<body>
<Providers>
<main>{children}</main>
</Providers>
</body>
</html>
);
}
-122
View File
@@ -1,122 +0,0 @@
import AppWrapper from "@/components/AppWrapper";
import { Flex, Button, Text, Container, Box } from "@radix-ui/themes";
import Link from "next/link";
import { SiGithub, IconType } from "@icons-pack/react-simple-icons";
import { SiLinkedin } from "react-icons/si";
import { Rss } from "lucide-react";
function NavLink({
href,
children,
}: {
href: string;
children: React.ReactNode;
}) {
return (
<Link href={href}>
<Text size="3" className="text-(--gray-11) hover:text-(--gray-12)">
{children}
</Text>
</Link>
);
}
function IconLink({
href,
icon: Icon,
}: {
href: string;
icon: React.ElementType;
}) {
return (
<Link href={href} className="text-(--gray-11) hover:text-(--gray-12)">
<Icon className="size-5" />
</Link>
);
}
function SocialLink({
href,
icon: IconComponent,
children,
}: {
href: string;
icon: React.ElementType;
children: React.ReactNode;
}) {
return (
<Link href={href}>
<Flex
align="center"
className="gap-x-1.5 px-1.5 py-1 rounded-xs bg-zinc-900 shadow-sm hover:bg-zinc-800 transition-colors"
>
<IconComponent className="size-4 text-zinc-300" />
<Text size="2" className="text-zinc-100">
{children}
</Text>
</Flex>
</Link>
);
}
export default async function HomePage() {
return (
<AppWrapper
className="overflow-x-hidden font-schibsted"
dotsClassName="animate-bg"
>
{/* Top Navigation Bar */}
<Flex justify="end" align="center" width="100%" pt="5" px="6" pb="9">
<Flex gap="4" align="center">
<NavLink href="/projects">Projects</NavLink>
<NavLink href="/blog">Blog</NavLink>
<IconLink href="https://github.com/Xevion" icon={SiGithub} />
<IconLink href="/rss" icon={Rss} />
</Flex>
</Flex>
{/* Main Content */}
<Flex align="center" direction="column">
<Box className="max-w-2xl mx-6 border-b border-(--gray-7) divide-y divide-(--gray-7)">
{/* Name & Job Title */}
<Flex direction="column" pb="4">
<Text size="6" weight="bold" highContrast>
Ryan Walters,
</Text>
<Text
size="6"
weight="regular"
style={{
color: "var(--gray-11)",
}}
>
Software Engineer
</Text>
</Flex>
<Box py="4" className="text-(--gray-12)">
<Text style={{ fontSize: "0.95em" }}>
A fanatical software engineer with expertise and passion for
sound, scalable and high-performance applications. I'm always
working on something new. <br />
Sometimes innovative &mdash; sometimes crazy.
</Text>
</Box>
<Box py="3">
<Text>Find me on</Text>
<Flex gapX="2" pl="3" pt="3" pb="2">
<SocialLink href="https://github.com/Xevion" icon={SiGithub}>
GitHub
</SocialLink>
<SocialLink
href="https://linkedin.com/in/ryancwalters"
icon={SiLinkedin}
>
LinkedIn
</SocialLink>
</Flex>
</Box>
</Box>
</Flex>
</AppWrapper>
);
}
-96
View File
@@ -1,96 +0,0 @@
import AppWrapper from "@/components/AppWrapper";
import { cn } from "@/utils/helpers";
import Link from "next/link";
import Balancer from "react-wrap-balancer";
import { getPayload } from "payload";
import config from "../../../payload.config";
import type { Link as PayloadLink } from "@/payload-types";
export const dynamic = "force-dynamic"; // Don't prerender at build time
export default async function ProjectsPage() {
const payloadConfig = await config;
const payload = await getPayload({ config: payloadConfig });
// Fetch all projects
const { docs: projects } = await payload.find({
collection: "projects",
where: {
status: {
equals: "published",
},
},
sort: "-updatedAt",
});
// Fetch all links in one query (fixes N+1 problem)
const { docs: allLinks } = await payload.find({
collection: "links",
});
// Group links by project ID
const linksByProject = new Map<number, PayloadLink[]>();
for (const link of allLinks) {
const projectId =
typeof link.project === "number" ? link.project : link.project.id;
if (!linksByProject.has(projectId)) {
linksByProject.set(projectId, []);
}
linksByProject.get(projectId)!.push(link);
}
return (
<AppWrapper dotsClassName="animate-bg-fast">
<div className="relative z-10 mx-auto grid grid-cols-1 justify-center gap-y-4 px-4 py-20 align-middle sm:grid-cols-2 md:max-w-[50rem] lg:max-w-[75rem] lg:grid-cols-3 lg:gap-y-9">
<div className="mb-3 text-center sm:col-span-2 md:mb-5 lg:col-span-3 lg:mb-7">
<h1 className="pb-3 font-hanken text-4xl text-zinc-200 opacity-100 md:text-5xl">
Projects
</h1>
<Balancer className="text-lg text-zinc-400">
created, maintained, or contributed to by me...
</Balancer>
</div>
{projects.map(({ id, name, shortDescription: description, icon }) => {
const links = linksByProject.get(id) ?? [];
const useAnchor = links.length > 0;
const DynamicLink = useAnchor ? Link : "div";
const linkProps = useAnchor
? { href: links[0]!.url, target: "_blank", rel: "noreferrer" }
: {};
return (
<div className="max-w-fit" key={id}>
{/* @ts-expect-error because div can't accept href */}
<DynamicLink
key={name}
title={name}
className="flex items-center justify-start overflow-hidden rounded bg-black/10 pb-2.5 pl-3 pr-5 pt-1 text-zinc-400 transition-colors hover:bg-zinc-500/10 hover:text-zinc-50"
{...linkProps}
>
<div className="flex h-full w-14 items-center justify-center pr-5">
<i
className={cn(
icon ?? "fa-heart",
"fa-solid text-3xl text-opacity-80 saturate-0",
)}
></i>
</div>
<div className="overflow-hidden">
<span className="text-sm md:text-base lg:text-lg">
{name}
</span>
<p
className="truncate text-xs opacity-70 md:text-sm lg:text-base"
title={description}
>
{description}
</p>
</div>
</DynamicLink>
</div>
);
})}
</div>
</AppWrapper>
);
}
-17
View File
@@ -1,17 +0,0 @@
"use client";
import { Analytics } from "@vercel/analytics/react";
import { Provider as BalancerProvider } from "react-wrap-balancer";
import { Theme } from "@radix-ui/themes";
export function Providers({ children }: { children: React.ReactNode }) {
return (
// @ts-expect-error - Radix UI Themes has React 19 type compatibility issues
<Theme appearance="dark">
<BalancerProvider>
{children}
<Analytics />
</BalancerProvider>
</Theme>
);
}
-36
View File
@@ -1,36 +0,0 @@
import { getPayload } from "payload";
import config from "../../../payload.config";
import { redirect } from "next/navigation";
export const dynamic = "force-dynamic"; // Don't prerender at build time
type Metadata = {
tagline: string;
resume: {
id: string;
url: string;
filename: string;
};
resumeFilename?: string;
};
export default async function ResumePage() {
try {
const payloadConfig = await config;
const payload = await getPayload({ config: payloadConfig });
// @ts-ignore - Globals will be typed after first database connection
const metadata = (await payload.findGlobal({
slug: "metadata",
})) as Metadata;
if (!metadata.resume?.url) {
throw new Error("Resume URL not found");
}
redirect(metadata.resume.url);
} catch (error) {
console.error("Failed to acquire resume asset URL", error);
throw new Error(`Failed to acquire resume (${error})`);
}
}
-164
View File
@@ -1,164 +0,0 @@
:root {
--font-mono: "Roboto Mono", monospace;
}
* {
box-sizing: border-box;
}
html {
font-size: 18px;
line-height: 32px;
background: rgb(0, 0, 0);
-webkit-font-smoothing: antialiased;
}
html,
body,
#app {
height: 100%;
}
body {
font-family: system-ui;
font-size: 18px;
line-height: 32px;
margin: 0;
color: rgb(1000, 1000, 1000);
@media (max-width: 1024px) {
font-size: 15px;
line-height: 24px;
}
}
img {
max-width: 100%;
height: auto;
display: block;
}
h1 {
margin: 40px 0;
font-size: 64px;
line-height: 70px;
font-weight: bold;
@media (max-width: 1024px) {
margin: 24px 0;
font-size: 42px;
line-height: 42px;
}
@media (max-width: 768px) {
font-size: 38px;
line-height: 38px;
}
@media (max-width: 400px) {
font-size: 32px;
line-height: 32px;
}
}
p {
margin: 24px 0;
@media (max-width: 1024px) {
margin: calc(var(--base) * 0.75) 0;
}
}
a {
color: currentColor;
&:focus {
opacity: 0.8;
outline: none;
}
&:active {
opacity: 0.7;
outline: none;
}
}
svg {
vertical-align: middle;
}
.home {
display: flex;
flex-direction: column;
justify-content: space-between;
align-items: center;
height: 100vh;
padding: 45px;
max-width: 1024px;
margin: 0 auto;
overflow: hidden;
@media (max-width: 400px) {
padding: 24px;
}
.content {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
flex-grow: 1;
h1 {
text-align: center;
}
}
.links {
display: flex;
align-items: center;
gap: 12px;
a {
text-decoration: none;
padding: 0.25rem 0.5rem;
border-radius: 4px;
}
.admin {
color: rgb(0, 0, 0);
background: rgb(1000, 1000, 1000);
border: 1px solid rgb(0, 0, 0);
}
.docs {
color: rgb(1000, 1000, 1000);
background: rgb(0, 0, 0);
border: 1px solid rgb(1000, 1000, 1000);
}
}
.footer {
display: flex;
align-items: center;
gap: 8px;
@media (max-width: 1024px) {
flex-direction: column;
gap: 6px;
}
p {
margin: 0;
}
.codeLink {
text-decoration: none;
padding: 0 0.5rem;
background: rgb(60, 60, 60);
border-radius: 4px;
}
}
}
-5
View File
@@ -1,5 +0,0 @@
export const importMap = {
}
-19
View File
@@ -1,19 +0,0 @@
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
import config from "../../../../payload.config";
import "@payloadcms/next/css";
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);
@@ -1,7 +0,0 @@
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
import config from "../../../../payload.config";
import "@payloadcms/next/css";
import { GRAPHQL_PLAYGROUND_GET } from "@payloadcms/next/routes";
export const GET = GRAPHQL_PLAYGROUND_GET(config);
-8
View File
@@ -1,8 +0,0 @@
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
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);
-299
View File
@@ -1,299 +0,0 @@
import { NextResponse } from "next/server";
import { getPayload } from "payload";
import config from "../../../../payload.config";
import { Octokit } from "@octokit/core";
const octokit = new Octokit({
auth: process.env.GITHUB_API_TOKEN,
request: {
fetch: (url: string | URL, options: RequestInit) => {
console.log(`${options.method} ${url}`);
return fetch(url, options);
},
},
});
type ProjectResult = {
id: number;
previousUpdated: Date | null;
latestUpdated: Date | null;
};
function getRepository(url: string): [string, string] | null {
const pattern = /github\.com\/([^/]+)\/([^/]+)/;
const match = url.match(pattern);
if (match === null) return null;
return [match[1]!, match[2]!];
}
function isFulfilled<T>(
result: PromiseSettledResult<T>,
): result is PromiseFulfilledResult<T> {
return result.status === "fulfilled";
}
function isRejected<T>(
result: PromiseSettledResult<T>,
): result is PromiseRejectedResult {
return result.status === "rejected";
}
async function handleProject({
id: project_id,
urls,
date_updated: previousUpdated,
}: {
id: number;
urls: string[];
date_updated: Date | null;
}): Promise<ProjectResult> {
const allBranches = await Promise.all(
urls.map(async (url) => {
const details = getRepository(url);
if (!details) {
return [];
}
const [owner, repo] = details;
const branches = await octokit.request(
"GET /repos/{owner}/{repo}/branches",
{
owner,
repo,
headers: {
"X-GitHub-Api-Version": "2022-11-28",
},
},
);
return branches.data.map((branch) => ({
branch: branch.name,
owner: owner,
repo: repo,
}));
}),
);
const latestCommits = allBranches
.flat()
.map(async ({ owner, repo, branch }) => {
const commits = await octokit.request(
"GET /repos/{owner}/{repo}/commits",
{
owner,
repo,
sha: branch,
per_page: 1,
headers: {
"X-GitHub-Api-Version": "2022-11-28",
},
},
);
const latestCommit = commits.data[0];
if (latestCommit == null) {
console.warn({
target: `${owner}/${repo}@${branch}`,
message: "No commits available",
});
return null;
}
if (latestCommit.commit.author == null) {
console.warn({
target: `${owner}/${repo}@${branch}`,
sha: latestCommit.sha,
commit: latestCommit.commit.message,
url: latestCommit.html_url,
message: "No author available",
});
return null;
} else if (latestCommit.commit.author.date == null) {
console.warn({
target: `${owner}/${repo}@${branch}`,
sha: latestCommit.sha,
commit: latestCommit.commit.message,
url: latestCommit.html_url,
message: "No date available",
});
return null;
}
return new Date(latestCommit.commit.author.date);
});
const results = await Promise.allSettled(latestCommits);
results.filter(isRejected).forEach((result) => {
console.error("Failed to fetch latest commit date", result.reason);
});
const latestUpdated = results
.filter(isFulfilled)
.map((v) => v.value)
.filter((v) => v != null)
.reduce((previous: Date | null, current: Date) => {
if (previous == null) return current;
return current > previous ? current : previous;
}, null);
if (latestUpdated == null) {
console.error("Unable to acquire the latest commit date for project");
return {
id: project_id,
previousUpdated,
latestUpdated: null,
};
}
if (latestUpdated != null && latestUpdated < new Date("2015-01-01")) {
console.error("Invalid commit date acquired", latestUpdated);
return {
id: project_id,
previousUpdated,
latestUpdated: null,
};
}
const result = { id: project_id, previousUpdated, latestUpdated: null };
if (previousUpdated == null || latestUpdated > previousUpdated) {
const payloadConfig = await config;
const payload = await getPayload({ config: payloadConfig });
await payload.update({
collection: "projects",
id: project_id,
data: {
lastUpdated: latestUpdated.toISOString(),
},
});
return {
...result,
latestUpdated,
};
}
return result;
}
export async function GET(req: Request) {
const { CRON_SECRET, GITHUB_API_TOKEN } = process.env;
if (!GITHUB_API_TOKEN) {
return NextResponse.json(
{
error: "Service unavailable",
message: "GITHUB_API_TOKEN not configured",
},
{ status: 503 },
);
}
if (process.env.NODE_ENV === "production") {
if (!CRON_SECRET) {
return NextResponse.json(
{ error: "Server misconfiguration" },
{ status: 500 },
);
}
const authHeader = req.headers.get("authorization");
const url = new URL(req.url);
const secretQueryParam = url.searchParams.get("secret");
if (
authHeader !== `Bearer ${CRON_SECRET}` &&
secretQueryParam !== CRON_SECRET
) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
}
let request_count = 0;
octokit.hook.before("request", async () => {
request_count++;
});
try {
const payloadConfig = await config;
const payload = await getPayload({ config: payloadConfig });
const { docs: projects } = await payload.find({
collection: "projects",
});
const { docs: allLinks } = await payload.find({
collection: "links",
});
const eligibleProjects = projects
.map((project) => {
if (!project.autocheckUpdated) return null;
const urls = allLinks
.filter((link) => {
const projectId =
typeof link.project === "number" ? link.project : link.project.id;
return projectId === project.id;
})
.map((link) => link.url)
.filter((url) => url.includes("github.com"));
if (urls.length === 0) return null;
return {
id: project.id,
name: project.name,
date_updated:
project.lastUpdated != null ? new Date(project.lastUpdated) : null,
urls,
};
})
.filter((project) => project !== null);
const projectPromises = eligibleProjects.map((project) =>
handleProject({
id: project.id,
urls: project.urls,
date_updated: project.date_updated,
}),
);
const results = await Promise.allSettled(projectPromises);
const isFailed = results.filter(isRejected).length > results.length * 0.1;
type Response = {
request_count: number;
errors: { project_name: string; reason: string }[];
ignored: number[];
changed: { project_name: number; previous: Date | null; latest: Date }[];
};
const fulfilled = results.filter(isFulfilled);
const response: Response = {
request_count,
errors: results.filter(isRejected).map((r) => ({
project_name: "unknown",
reason: r.reason,
})),
ignored: fulfilled
.filter((r) => r.value.latestUpdated == null)
.map((r) => r.value.id),
changed: fulfilled
.filter((r) => r.value.latestUpdated != null)
.map((r) => ({
project_name: r.value.id,
previous: r.value.previousUpdated,
latest: r.value.latestUpdated!,
})),
};
return NextResponse.json(response, { status: !isFailed ? 200 : 500 });
} catch (error) {
return NextResponse.json({ error }, { status: 500 });
}
}
-40
View File
@@ -1,40 +0,0 @@
import { getPayload } from "payload";
import config from "../../../payload.config";
import { NextResponse } from "next/server";
export async function GET(req: Request) {
const healthcheckSecret = process.env.HEALTHCHECK_SECRET;
if (!healthcheckSecret) {
return NextResponse.json(
{
error: "Service unavailable",
message: "HEALTHCHECK_SECRET not configured",
},
{ status: 503 },
);
}
const secret = req.headers.get("authorization");
if (secret !== healthcheckSecret) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
try {
// Try a simple Payload API call (fetch one project)
const payloadConfig = await config;
const payload = await getPayload({ config: payloadConfig });
await payload.find({
collection: "projects",
limit: 1,
});
return NextResponse.json({ status: "ok" }, { status: 200 });
} catch (error) {
return NextResponse.json(
{ error: "Payload CMS unhealthy", details: String(error) },
{ status: 500 },
);
}
}
-84
View File
@@ -1,84 +0,0 @@
import { NextResponse } from "next/server";
import { revalidatePath } from "next/cache";
import { z } from "zod";
const requestSchema = z.object({
collection: z.string(),
doc: z.object({
id: z.string().or(z.number()),
}),
});
function getPathsToRevalidate(collection: string): string[] {
switch (collection) {
case "projects":
return ["/projects"];
case "metadata":
return ["/"];
case "technologies":
return ["/projects"];
case "links":
return ["/projects"];
default:
return [];
}
}
export async function POST(req: Request) {
const revalidateKey = process.env.PAYLOAD_REVALIDATE_KEY;
const authHeader = req.headers.get("authorization");
if (!authHeader || authHeader !== `Bearer ${revalidateKey}`) {
return NextResponse.json({ message: "Invalid token" }, { status: 401 });
}
try {
const body = await req.json();
const { success, data, error } = requestSchema.safeParse(body);
if (!success) {
console.error({ message: "Invalid JSON body", error });
return NextResponse.json(
{ message: "Invalid JSON body", error },
{ status: 400 },
);
}
const paths = getPathsToRevalidate(data.collection);
if (paths.length === 0) {
return NextResponse.json(
{ revalidated: false, message: "No paths to revalidate" },
{ status: 404 },
);
}
// Revalidate all paths
try {
for (const path of paths) {
revalidatePath(path);
}
} catch (error) {
console.error({ message: "Error while revalidating", error });
return NextResponse.json(
{
revalidated: false,
message: "Error while revalidating",
paths,
},
{ status: 500 },
);
}
return NextResponse.json({ revalidated: true, paths }, { status: 200 });
} catch (error) {
console.error({
message: "Error while preparing to revalidate",
error,
});
return NextResponse.json(
{ message: "Error revalidating" },
{ status: 500 },
);
}
}
-30
View File
@@ -1,30 +0,0 @@
"use client";
import { cn } from "@/utils/helpers";
import dynamic from "next/dynamic";
import type { FunctionComponent, ReactNode } from "react";
type WrapperProps = {
className?: string;
dotsClassName?: string;
children?: ReactNode;
};
const DotsDynamic = dynamic(() => import("@/components/Dots"), { ssr: false });
const AppWrapper: FunctionComponent<WrapperProps> = ({
children,
className,
dotsClassName,
}: WrapperProps) => {
return (
<main
className={cn("relative min-h-screen bg-black text-zinc-50", className)}
>
<DotsDynamic className={dotsClassName} />
{children}
</main>
);
};
export default AppWrapper;

Some files were not shown because too many files have changed in this diff Show More