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
+3 -1
View File
@@ -1,4 +1,6 @@
<script lang="ts">
import DynamicIcon from '$lib/components/ui/DynamicIcon.svelte';
interface BoardSummary {
id: string;
name: string;
@@ -24,7 +26,7 @@
>
<div class="flex items-start gap-3">
{#if board.icon}
<span class="text-xl">{board.icon}</span>
<DynamicIcon name={board.icon} size={22} />
{:else}
<span class="flex h-8 w-8 items-center justify-center rounded-md bg-muted text-sm text-muted-foreground">
B
+3 -1
View File
@@ -1,4 +1,6 @@
<script lang="ts">
import DynamicIcon from '$lib/components/ui/DynamicIcon.svelte';
interface Props {
name: string;
description: string | null;
@@ -13,7 +15,7 @@
<div class="mb-6 flex items-start justify-between">
<div class="flex items-center gap-3">
{#if icon}
<span class="text-2xl">{icon}</span>
<DynamicIcon name={icon} size={28} />
{/if}
<div>
<h1 class="text-3xl font-bold text-foreground">{name}</h1>
+2 -1
View File
@@ -1,6 +1,7 @@
<script lang="ts">
import { ui } from '$lib/stores/ui.svelte.js';
import { page } from '$app/stores';
import DynamicIcon from '$lib/components/ui/DynamicIcon.svelte';
interface BoardLink {
id: string;
@@ -150,7 +151,7 @@
onclick={() => ui.closeMobileSidebar()}
>
{#if board.icon}
<span class="shrink-0 text-base">{board.icon}</span>
<span class="shrink-0"><DynamicIcon name={board.icon} size={18} /></span>
{:else}
<span
class="flex h-5 w-5 shrink-0 items-center justify-center rounded bg-sidebar-accent text-[10px] font-medium text-sidebar-foreground"
@@ -1,4 +1,6 @@
<script lang="ts">
import DynamicIcon from '$lib/components/ui/DynamicIcon.svelte';
interface Props {
title: string;
icon: string | null;
@@ -29,7 +31,7 @@
</svg>
{#if icon}
<span class="text-base">{icon}</span>
<DynamicIcon name={icon} size={18} />
{/if}
<span class="font-medium text-foreground">{title}</span>
+27
View File
@@ -0,0 +1,27 @@
<script lang="ts">
import * as icons from 'lucide-svelte';
interface Props {
name: string | null;
size?: number;
class?: string;
}
let { name, size = 16, class: className = '' }: Props = $props();
// Convert kebab-case to PascalCase: "layout-dashboard" → "LayoutDashboard"
function toPascalCase(str: string): string {
return str
.split('-')
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
.join('');
}
const iconComponent = $derived(
name ? (icons as Record<string, unknown>)[toPascalCase(name)] ?? null : null
);
</script>
{#if iconComponent}
<svelte:component this={iconComponent} {size} class={className} />
{/if}
+3
View File
@@ -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) {
+3
View File
@@ -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);
+3
View File
@@ -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));