diff --git a/prisma/seed.ts b/prisma/seed.ts index 057df37..149de36 100644 --- a/prisma/seed.ts +++ b/prisma/seed.ts @@ -29,10 +29,10 @@ async function main() { // --- Admin User --- const adminPassword = await bcrypt.hash('admin123', 12); const admin = await prisma.user.upsert({ - where: { email: 'admin@localhost' }, + where: { email: 'admin@launcher.local' }, update: {}, create: { - email: 'admin@localhost', + email: 'admin@launcher.local', password: adminPassword, displayName: 'Administrator', role: 'admin', @@ -44,10 +44,10 @@ async function main() { // --- Regular User --- const userPassword = await bcrypt.hash('user123', 12); const regularUser = await prisma.user.upsert({ - where: { email: 'user@localhost' }, + where: { email: 'user@launcher.local' }, update: {}, create: { - email: 'user@localhost', + email: 'user@launcher.local', password: userPassword, displayName: 'Demo User', role: 'user', diff --git a/src/lib/components/board/BoardCard.svelte b/src/lib/components/board/BoardCard.svelte index 87999c4..b1e17f8 100644 --- a/src/lib/components/board/BoardCard.svelte +++ b/src/lib/components/board/BoardCard.svelte @@ -1,4 +1,6 @@ + +{#if iconComponent} + +{/if} diff --git a/src/lib/stores/search.svelte.ts b/src/lib/stores/search.svelte.ts index e80d410..69e6c29 100644 --- a/src/lib/stores/search.svelte.ts +++ b/src/lib/stores/search.svelte.ts @@ -31,7 +31,10 @@ class SearchStore { window.addEventListener('keydown', handleKeyDown); } + } + /** Must be called from within a component to set up reactive search effect */ + initEffects() { $effect(() => { const q = this.query; if (q.length < 2) { diff --git a/src/lib/stores/theme.svelte.ts b/src/lib/stores/theme.svelte.ts index 3c06a6c..48e6540 100644 --- a/src/lib/stores/theme.svelte.ts +++ b/src/lib/stores/theme.svelte.ts @@ -56,7 +56,10 @@ class ThemeStore { this.#systemPreference = e.matches ? 'dark' : 'light'; }); } + } + /** Must be called from within a component to set up persistence and DOM effects */ + initEffects() { $effect(() => { if (typeof window === 'undefined') return; localStorage.setItem(THEME_STORAGE_KEY, this.mode); diff --git a/src/lib/stores/ui.svelte.ts b/src/lib/stores/ui.svelte.ts index 77178d1..d8a6c23 100644 --- a/src/lib/stores/ui.svelte.ts +++ b/src/lib/stores/ui.svelte.ts @@ -40,7 +40,10 @@ class UiStore { window.addEventListener('resize', handleResize); } + } + /** Must be called from within a component to set up persistence effects */ + initEffects() { $effect(() => { if (typeof window === 'undefined') return; localStorage.setItem(SIDEBAR_COLLAPSED_KEY, String(this.sidebarCollapsed)); diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index 044b59d..61cadd3 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -5,9 +5,17 @@ import MainLayout from '$lib/components/layout/MainLayout.svelte'; import { page } from '$app/stores'; import { fade } from 'svelte/transition'; + import { theme } from '$lib/stores/theme.svelte'; + import { ui } from '$lib/stores/ui.svelte'; + import { search } from '$lib/stores/search.svelte'; let { data, children }: { data: LayoutData; children: Snippet } = $props(); + // Initialize store effects within component context + theme.initEffects(); + ui.initEffects(); + search.initEffects(); + // Pages that should NOT have the main layout (login, register) const noLayoutPaths = ['/login', '/register']; const showLayout = $derived(!noLayoutPaths.includes($page.url.pathname)); diff --git a/src/routes/boards/new/+page.server.ts b/src/routes/boards/new/+page.server.ts new file mode 100644 index 0000000..c9b03e3 --- /dev/null +++ b/src/routes/boards/new/+page.server.ts @@ -0,0 +1,41 @@ +import type { PageServerLoad, Actions } from './$types.js'; +import { redirect, fail } from '@sveltejs/kit'; +import { superValidate, message } from 'sveltekit-superforms'; +import { zod } from '$lib/utils/zod-adapter.js'; +import { createBoardSchema } from '$lib/utils/validators.js'; +import * as boardService from '$lib/server/services/boardService.js'; + +export const load: PageServerLoad = async ({ locals }) => { + if (!locals.user || locals.user.role !== 'admin') { + throw redirect(302, '/boards'); + } + + const form = await superValidate(zod(createBoardSchema)); + return { form }; +}; + +export const actions: Actions = { + default: async ({ request, locals }) => { + if (!locals.user || locals.user.role !== 'admin') { + return fail(403, { error: 'Forbidden' }); + } + + const form = await superValidate(request, zod(createBoardSchema)); + if (!form.valid) { + return fail(400, { form }); + } + + try { + const board = await boardService.createBoard({ + ...form.data, + createdById: locals.user.id + }); + throw redirect(302, `/boards/${board.id}`); + } catch (err) { + if (err && typeof err === 'object' && 'status' in err && err.status === 302) { + throw err; + } + return message(form, 'Failed to create board', { status: 500 }); + } + } +}; diff --git a/src/routes/boards/new/+page.svelte b/src/routes/boards/new/+page.svelte new file mode 100644 index 0000000..57d7616 --- /dev/null +++ b/src/routes/boards/new/+page.svelte @@ -0,0 +1,88 @@ + + + + New Board — Web App Launcher + + +
+
+ + +

New Board

+ +
+
+ + + {#if $errors.name}

{$errors.name}

{/if} +
+ +
+ + +
+ +
+ + +
+ +
+ + + +
+ +
+ + Cancel + + +
+
+
+
diff --git a/vite.config.ts b/vite.config.ts index 6d65e6a..e922b4f 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -4,6 +4,10 @@ import { defineConfig } from 'vitest/config'; export default defineConfig({ plugins: [tailwindcss(), sveltekit()], + server: { + port: 5181, + host: '0.0.0.0' + }, test: { include: ['src/**/*.{test,spec}.{js,ts}'], environment: 'node',