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>
This commit is contained in:
80
src/routes/projects/new/+page.server.ts
Normal file
80
src/routes/projects/new/+page.server.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
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}`);
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user