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.
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
{
|
||||
"tabWidth": 2,
|
||||
"useTabs": false
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"]
|
||||
@@ -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);
|
||||
@@ -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/
|
||||
@@ -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
|
||||
@@ -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.
|
||||
@@ -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 });
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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<{
|
||||
@@ -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";`)
|
||||
}
|
||||
@@ -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.')
|
||||
})
|
||||
})
|
||||
@@ -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()
|
||||
})
|
||||
})
|
||||
@@ -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"
|
||||
],
|
||||
}
|
||||
@@ -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'],
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,4 @@
|
||||
// Any setup scripts you might need go here
|
||||
|
||||
// Load .env files
|
||||
import 'dotenv/config'
|
||||
@@ -0,0 +1 @@
|
||||
engine-strict=true
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -0,0 +1 @@
|
||||
// place files you want to import through the `$lib` alias in this folder.
|
||||
@@ -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>];
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,6 @@
|
||||
import { clsx, type ClassValue } from "clsx";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs));
|
||||
}
|
||||
@@ -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()}
|
||||
@@ -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 — 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>
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
@@ -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>
|
||||
@@ -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 |
@@ -0,0 +1,3 @@
|
||||
# allow crawling everything by default
|
||||
User-agent: *
|
||||
Disallow:
|
||||
@@ -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;
|
||||
@@ -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
|
||||
}
|
||||
@@ -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" })],
|
||||
});
|
||||
@@ -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:
|
||||
@@ -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);
|
||||
});
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
@@ -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/*"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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:*"
|
||||
}
|
||||
}
|
||||
@@ -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";
|
||||
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": "@xevion/typescript-config/base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src"
|
||||
},
|
||||
"include": ["src/**/*"]
|
||||
}
|
||||
@@ -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:*"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
// This file will be populated by Payload's generate:types command
|
||||
// Placeholder export
|
||||
export type PayloadTypes = Record<string, any>;
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": "@xevion/typescript-config/base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src"
|
||||
},
|
||||
"include": ["src/**/*"]
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"name": "@xevion/typescript-config",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"license": "MIT",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
}
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
export default {
|
||||
plugins: {
|
||||
"@tailwindcss/postcss": {},
|
||||
...(process.env.NODE_ENV === "production" ? { cssnano: {} } : {}),
|
||||
},
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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 — 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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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})`);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
|
||||
|
||||
export const importMap = {
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
@@ -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 },
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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 },
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||