fix: resolve runtime errors and missing routes

- Fix $effect orphan error: move $effect calls from store constructors
  to initEffects() methods called from component context
- Fix icon rendering: create DynamicIcon component to render Lucide icons
  from name strings instead of displaying raw text
- Add /boards/new route for board creation
- Fix seed emails (admin@launcher.local / user@launcher.local) to pass
  Zod email validation
This commit is contained in:
2026-03-24 22:39:23 +03:00
parent e6b50fb4f1
commit bb3b1a5db7
13 changed files with 192 additions and 8 deletions
+8
View File
@@ -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));
+41
View File
@@ -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 });
}
}
};
+88
View File
@@ -0,0 +1,88 @@
<script lang="ts">
import type { PageData } from './$types.js';
import { superForm } from 'sveltekit-superforms';
let { data }: { data: PageData } = $props();
const { form, errors, enhance, submitting } = superForm(data.form);
</script>
<svelte:head>
<title>New Board — Web App Launcher</title>
</svelte:head>
<div class="p-6">
<div class="mx-auto max-w-2xl">
<div class="mb-6">
<a href="/boards" class="text-sm text-muted-foreground hover:text-foreground">
&larr; Back to Boards
</a>
</div>
<h1 class="mb-6 text-3xl font-bold text-foreground">New Board</h1>
<form method="POST" use:enhance class="space-y-4 rounded-xl border border-border bg-card p-6">
<div>
<label for="name" class="mb-1 block text-sm font-medium text-foreground">Name</label>
<input
id="name"
name="name"
type="text"
bind:value={$form.name}
class="w-full rounded-lg border border-input bg-background px-3 py-2 text-sm text-foreground placeholder:text-muted-foreground focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary"
placeholder="My Dashboard"
required
/>
{#if $errors.name}<p class="mt-1 text-xs text-destructive">{$errors.name}</p>{/if}
</div>
<div>
<label for="description" class="mb-1 block text-sm font-medium text-foreground">Description</label>
<input
id="description"
name="description"
type="text"
bind:value={$form.description}
class="w-full rounded-lg border border-input bg-background px-3 py-2 text-sm text-foreground placeholder:text-muted-foreground focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary"
placeholder="Optional description"
/>
</div>
<div>
<label for="icon" class="mb-1 block text-sm font-medium text-foreground">Icon (Lucide name)</label>
<input
id="icon"
name="icon"
type="text"
bind:value={$form.icon}
class="w-full rounded-lg border border-input bg-background px-3 py-2 text-sm text-foreground placeholder:text-muted-foreground focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary"
placeholder="layout-dashboard"
/>
</div>
<div class="flex items-center gap-4">
<label class="flex items-center gap-2 text-sm text-foreground">
<input type="checkbox" name="isDefault" bind:checked={$form.isDefault} class="rounded" />
Default board
</label>
<label class="flex items-center gap-2 text-sm text-foreground">
<input type="checkbox" name="isGuestAccessible" bind:checked={$form.isGuestAccessible} class="rounded" />
Guest accessible
</label>
</div>
<div class="flex justify-end gap-3 pt-2">
<a href="/boards" class="rounded-lg border border-border px-4 py-2 text-sm text-foreground hover:bg-accent">
Cancel
</a>
<button
type="submit"
disabled={$submitting}
class="rounded-lg bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90 disabled:opacity-50"
>
{$submitting ? 'Creating...' : 'Create Board'}
</button>
</div>
</form>
</div>
</div>