Files
helix/src/routes/projects/new/+page.server.ts
Rene De Ren c3d978a7eb feat: initial HELIX scaffold — R&D showcase platform
SvelteKit 2 + Svelte 5 + TypeScript site. SQLite via Drizzle. Gitea OAuth
for authoring (RnD org-gated). Pure SVG + CSS DNA helix on landing.

What lands
- Landing hero with animated two-strand SVG helix + tagline
- /projects + /projects/[slug] (markdown body, dashboard embed allowlist)
- /posts + /posts/[slug]
- Auth-gated /projects/new + /posts/new forms
- Gitea OAuth flow (state, code exchange, org-membership check, sessions)
- Sliding-window cookie sessions (SHA-256 hashed token storage)
- Dockerfile + docker-compose with named-volume SQLite
- Idempotent seed (EVOLV + HELIX projects, welcome post)

Stack notes
- Tailwind v3 (Node 18 compat; v4 needs Node 20+)
- drizzle-orm 0.45+ (patched, no SQL-identifier escape vuln)
- marked for markdown; iframe embeds gated by DASHBOARD_ALLOWED_HOSTS

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 11:01:12 +02:00

81 lines
2.7 KiB
TypeScript

import { fail, redirect } from '@sveltejs/kit';
import type { Actions, PageServerLoad } from './$types';
import { db } from '$lib/server/db';
import { projects, projectLinks } from '$lib/server/db/schema';
import { LINK_KINDS } from '$lib/config';
import { newId, slugify } from '$lib/markdown';
import { eq } from 'drizzle-orm';
export const load: PageServerLoad = async ({ locals }) => {
if (!locals.user) redirect(302, '/login');
return {};
};
export const actions: Actions = {
default: async ({ request, locals }) => {
const data = await request.formData();
const title = (data.get('title') ?? '').toString().trim();
const slugRaw = (data.get('slug') ?? '').toString().trim();
const summary = (data.get('summary') ?? '').toString().trim();
const bodyMd = (data.get('body_md') ?? '').toString();
const coverUrl = (data.get('cover_url') ?? '').toString().trim() || null;
const values = { title, slug: slugRaw, summary, body_md: bodyMd, cover_url: coverUrl };
if (!locals.user) return fail(401, { error: 'Not authenticated', values });
if (!title) return fail(400, { error: 'Title is required.', values });
if (!summary) return fail(400, { error: 'Summary is required.', values });
const slug = slugify(slugRaw || title);
if (!slug) return fail(400, { error: 'Slug could not be generated.', values });
const exists = db.select({ id: projects.id }).from(projects).where(eq(projects.slug, slug)).get();
if (exists) {
return fail(400, { error: `A project with slug "${slug}" already exists.`, values });
}
const kinds = data.getAll('link_kind').map(String);
const labels = data.getAll('link_label').map(String);
const urls = data.getAll('link_url').map(String);
const linksToInsert: { kind: string; label: string; url: string; position: number }[] = [];
for (let i = 0; i < kinds.length; i++) {
const k = kinds[i];
const l = (labels[i] ?? '').trim();
const u = (urls[i] ?? '').trim();
if (!u) continue;
if (!(LINK_KINDS as readonly string[]).includes(k)) continue;
linksToInsert.push({ kind: k, label: l || u, url: u, position: i });
}
const id = newId('prj');
db.insert(projects)
.values({
id,
slug,
title,
summary,
bodyMd,
coverUrl,
authorId: locals.user.id,
status: 'published'
})
.run();
for (const link of linksToInsert) {
db.insert(projectLinks)
.values({
id: newId('lnk'),
projectId: id,
kind: link.kind as (typeof LINK_KINDS)[number],
label: link.label,
url: link.url,
position: link.position
})
.run();
}
redirect(303, `/projects/${slug}`);
}
};