mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-16 08:59:39 +02:00
fix(web): add appReady gate to prevent auth race condition in all apps
In Svelte, child onMount fires before parent onMount. Pages that fetch data in onMount race against the layout's authStore.initialize(). Added appReady state gate to layouts so children don't mount until auth is confirmed. Affects: todo, contacts, clock, photos, zitare, planta. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
e3115b302d
commit
000b74af9f
7 changed files with 498 additions and 432 deletions
|
|
@ -38,6 +38,9 @@
|
|||
|
||||
let { children } = $props();
|
||||
|
||||
// Auth gate - prevent children from mounting before auth is confirmed
|
||||
let appReady = $state(false);
|
||||
|
||||
// CommandBar state
|
||||
let commandBarOpen = $state(false);
|
||||
|
||||
|
|
@ -262,66 +265,75 @@
|
|||
if (currentPath === '/' && userSettings.startPage && userSettings.startPage !== '/') {
|
||||
goto(userSettings.startPage, { replaceState: true });
|
||||
}
|
||||
|
||||
// Auth confirmed - allow children to render
|
||||
appReady = true;
|
||||
});
|
||||
</script>
|
||||
|
||||
<svelte:window onkeydown={handleKeydown} />
|
||||
|
||||
<div class="layout-container">
|
||||
<PillNavigation
|
||||
items={navItems}
|
||||
currentPath={$page.url.pathname}
|
||||
appName="Clock"
|
||||
homeRoute="/"
|
||||
onToggleTheme={handleToggleTheme}
|
||||
{isDark}
|
||||
{isCollapsed}
|
||||
onCollapsedChange={handleCollapsedChange}
|
||||
showThemeToggle={true}
|
||||
showThemeVariants={true}
|
||||
{themeVariantItems}
|
||||
{currentThemeVariantLabel}
|
||||
themeMode={theme.mode}
|
||||
onThemeModeChange={handleThemeModeChange}
|
||||
showLanguageSwitcher={true}
|
||||
{languageItems}
|
||||
{currentLanguageLabel}
|
||||
showLogout={true}
|
||||
onLogout={handleLogout}
|
||||
loginHref="/login"
|
||||
primaryColor="#f59e0b"
|
||||
showAppSwitcher={true}
|
||||
{appItems}
|
||||
{userEmail}
|
||||
settingsHref="/settings"
|
||||
manaHref="/mana"
|
||||
profileHref="/profile"
|
||||
allAppsHref="/apps"
|
||||
/>
|
||||
{#if !appReady}
|
||||
<div class="flex items-center justify-center h-screen bg-background">
|
||||
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-primary"></div>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="layout-container">
|
||||
<PillNavigation
|
||||
items={navItems}
|
||||
currentPath={$page.url.pathname}
|
||||
appName="Clock"
|
||||
homeRoute="/"
|
||||
onToggleTheme={handleToggleTheme}
|
||||
{isDark}
|
||||
{isCollapsed}
|
||||
onCollapsedChange={handleCollapsedChange}
|
||||
showThemeToggle={true}
|
||||
showThemeVariants={true}
|
||||
{themeVariantItems}
|
||||
{currentThemeVariantLabel}
|
||||
themeMode={theme.mode}
|
||||
onThemeModeChange={handleThemeModeChange}
|
||||
showLanguageSwitcher={true}
|
||||
{languageItems}
|
||||
{currentLanguageLabel}
|
||||
showLogout={true}
|
||||
onLogout={handleLogout}
|
||||
loginHref="/login"
|
||||
primaryColor="#f59e0b"
|
||||
showAppSwitcher={true}
|
||||
{appItems}
|
||||
{userEmail}
|
||||
settingsHref="/settings"
|
||||
manaHref="/mana"
|
||||
profileHref="/profile"
|
||||
allAppsHref="/apps"
|
||||
/>
|
||||
|
||||
<main class="main-content bg-background">
|
||||
<div class="content-wrapper">
|
||||
{@render children()}
|
||||
</div>
|
||||
</main>
|
||||
<main class="main-content bg-background">
|
||||
<div class="content-wrapper">
|
||||
{@render children()}
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- Global Command Bar (Cmd/K) -->
|
||||
<CommandBar
|
||||
bind:open={commandBarOpen}
|
||||
onClose={() => (commandBarOpen = false)}
|
||||
onSearch={handleCommandBarSearch}
|
||||
onSelect={handleCommandBarSelect}
|
||||
quickActions={commandBarQuickActions}
|
||||
placeholder="Schnellzugriff..."
|
||||
emptyText="Keine Ergebnisse"
|
||||
searchingText="Suche..."
|
||||
/>
|
||||
<!-- Global Command Bar (Cmd/K) -->
|
||||
<CommandBar
|
||||
bind:open={commandBarOpen}
|
||||
onClose={() => (commandBarOpen = false)}
|
||||
onSearch={handleCommandBarSearch}
|
||||
onSelect={handleCommandBarSelect}
|
||||
quickActions={commandBarQuickActions}
|
||||
placeholder="Schnellzugriff..."
|
||||
emptyText="Keine Ergebnisse"
|
||||
searchingText="Suche..."
|
||||
/>
|
||||
|
||||
<!-- Onboarding Modal -->
|
||||
{#if clockOnboarding.shouldShow}
|
||||
<MiniOnboardingModal store={clockOnboarding} appName="Uhr" appEmoji="⏰" />
|
||||
{/if}
|
||||
</div>
|
||||
<!-- Onboarding Modal -->
|
||||
{#if clockOnboarding.shouldShow}
|
||||
<MiniOnboardingModal store={clockOnboarding} appName="Uhr" appEmoji="⏰" />
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.layout-container {
|
||||
|
|
|
|||
|
|
@ -68,6 +68,9 @@
|
|||
|
||||
let { children } = $props();
|
||||
|
||||
// Auth gate - prevent children from mounting before auth is confirmed
|
||||
let appReady = $state(false);
|
||||
|
||||
// Show toolbar only on main contacts page
|
||||
const showContactsToolbar = $derived($page.url.pathname === '/');
|
||||
|
||||
|
|
@ -286,117 +289,126 @@
|
|||
// Load tags (used by TagStrip and Quick-Create)
|
||||
await tagsStore.fetchTags();
|
||||
availableTags = tagsStore.tags.map((t) => ({ id: t.id, name: t.name }));
|
||||
|
||||
// Auth confirmed - allow children to render
|
||||
appReady = true;
|
||||
});
|
||||
</script>
|
||||
|
||||
<svelte:window onkeydown={handleKeydown} />
|
||||
|
||||
<SplitPaneContainer>
|
||||
<!-- Navigation Layout -->
|
||||
<div class="layout-container">
|
||||
<a
|
||||
href="#main-content"
|
||||
class="sr-only focus:not-sr-only focus:fixed focus:top-2 focus:left-2 focus:z-[100] focus:rounded-lg focus:bg-primary focus:px-4 focus:py-2 focus:text-white"
|
||||
>
|
||||
Zum Inhalt springen
|
||||
</a>
|
||||
|
||||
<!-- UI Elements (hidden in immersive mode) -->
|
||||
{#if !contactsSettings.immersiveModeEnabled}
|
||||
<!-- Floating Pill Navigation (at bottom) -->
|
||||
<PillNavigation
|
||||
items={navItems}
|
||||
currentPath={$page.url.pathname}
|
||||
appName="Contacts"
|
||||
homeRoute="/"
|
||||
onToggleTheme={handleToggleTheme}
|
||||
{isDark}
|
||||
desktopPosition="bottom"
|
||||
showThemeToggle={true}
|
||||
showThemeVariants={true}
|
||||
{themeVariantItems}
|
||||
{currentThemeVariantLabel}
|
||||
themeMode={theme.mode}
|
||||
onThemeModeChange={handleThemeModeChange}
|
||||
showLanguageSwitcher={true}
|
||||
{languageItems}
|
||||
{currentLanguageLabel}
|
||||
showLogout={true}
|
||||
onLogout={handleLogout}
|
||||
loginHref="/login"
|
||||
primaryColor="#3b82f6"
|
||||
showAppSwitcher={true}
|
||||
{appItems}
|
||||
{userEmail}
|
||||
settingsHref="/settings"
|
||||
manaHref="/mana"
|
||||
profileHref="/profile"
|
||||
allAppsHref="/apps"
|
||||
onOpenInPanel={handleOpenInPanel}
|
||||
ariaLabel="Hauptnavigation"
|
||||
/>
|
||||
|
||||
<!-- TagStrip (above PillNav) -->
|
||||
<TagStrip />
|
||||
|
||||
<!-- Global Quick Input Bar -->
|
||||
<QuickInputBar
|
||||
onSearch={handleSearch}
|
||||
onSelect={handleSelect}
|
||||
onSearchChange={(query) => contactsFilterStore.setSearchQuery(query)}
|
||||
placeholder="Neuer Kontakt oder suchen..."
|
||||
emptyText="Keine Kontakte gefunden"
|
||||
searchingText="Suche..."
|
||||
searchText="Suchen"
|
||||
onCreate={handleCreate}
|
||||
onParseCreate={handleParseCreate}
|
||||
createText="Erstellen"
|
||||
deferSearch={true}
|
||||
locale={$locale || 'de'}
|
||||
appIcon="contacts"
|
||||
bottomOffset={inputBarBottomOffset}
|
||||
hasFabRight={showContactsToolbar}
|
||||
/>
|
||||
|
||||
<!-- Contacts Toolbar (FAB + expandable bar) - only on main page -->
|
||||
{#if showContactsToolbar}
|
||||
<ContactsToolbar contacts={contactsStore.contacts} />
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
<!-- Immersive Mode Toggle (always visible) -->
|
||||
<ImmersiveModeToggle
|
||||
isImmersive={contactsSettings.immersiveModeEnabled}
|
||||
onToggle={() => contactsSettings.toggleImmersiveMode()}
|
||||
/>
|
||||
|
||||
<!-- Main Content -->
|
||||
<main
|
||||
id="main-content"
|
||||
class="main-content bg-background"
|
||||
class:immersive={contactsSettings.immersiveModeEnabled}
|
||||
>
|
||||
<div class="content-wrapper" class:immersive={contactsSettings.immersiveModeEnabled}>
|
||||
{@render children()}
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- Contact Detail Modal -->
|
||||
{#if showContactModal && modalContactId}
|
||||
<ContactDetailModal contactId={modalContactId} onClose={handleCloseContactModal} />
|
||||
{/if}
|
||||
|
||||
<!-- New Contact Modal -->
|
||||
{#if newContactModalStore.isOpen}
|
||||
<NewContactModal onClose={() => newContactModalStore.close()} />
|
||||
{/if}
|
||||
|
||||
<!-- Onboarding Modal -->
|
||||
{#if contactsOnboarding.shouldShow}
|
||||
<MiniOnboardingModal store={contactsOnboarding} appName="Kontakte" appEmoji="👥" />
|
||||
{/if}
|
||||
{#if !appReady}
|
||||
<div class="flex items-center justify-center h-screen bg-background">
|
||||
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-primary"></div>
|
||||
</div>
|
||||
</SplitPaneContainer>
|
||||
{:else}
|
||||
<SplitPaneContainer>
|
||||
<!-- Navigation Layout -->
|
||||
<div class="layout-container">
|
||||
<a
|
||||
href="#main-content"
|
||||
class="sr-only focus:not-sr-only focus:fixed focus:top-2 focus:left-2 focus:z-[100] focus:rounded-lg focus:bg-primary focus:px-4 focus:py-2 focus:text-white"
|
||||
>
|
||||
Zum Inhalt springen
|
||||
</a>
|
||||
|
||||
<!-- UI Elements (hidden in immersive mode) -->
|
||||
{#if !contactsSettings.immersiveModeEnabled}
|
||||
<!-- Floating Pill Navigation (at bottom) -->
|
||||
<PillNavigation
|
||||
items={navItems}
|
||||
currentPath={$page.url.pathname}
|
||||
appName="Contacts"
|
||||
homeRoute="/"
|
||||
onToggleTheme={handleToggleTheme}
|
||||
{isDark}
|
||||
desktopPosition="bottom"
|
||||
showThemeToggle={true}
|
||||
showThemeVariants={true}
|
||||
{themeVariantItems}
|
||||
{currentThemeVariantLabel}
|
||||
themeMode={theme.mode}
|
||||
onThemeModeChange={handleThemeModeChange}
|
||||
showLanguageSwitcher={true}
|
||||
{languageItems}
|
||||
{currentLanguageLabel}
|
||||
showLogout={true}
|
||||
onLogout={handleLogout}
|
||||
loginHref="/login"
|
||||
primaryColor="#3b82f6"
|
||||
showAppSwitcher={true}
|
||||
{appItems}
|
||||
{userEmail}
|
||||
settingsHref="/settings"
|
||||
manaHref="/mana"
|
||||
profileHref="/profile"
|
||||
allAppsHref="/apps"
|
||||
onOpenInPanel={handleOpenInPanel}
|
||||
ariaLabel="Hauptnavigation"
|
||||
/>
|
||||
|
||||
<!-- TagStrip (above PillNav) -->
|
||||
<TagStrip />
|
||||
|
||||
<!-- Global Quick Input Bar -->
|
||||
<QuickInputBar
|
||||
onSearch={handleSearch}
|
||||
onSelect={handleSelect}
|
||||
onSearchChange={(query) => contactsFilterStore.setSearchQuery(query)}
|
||||
placeholder="Neuer Kontakt oder suchen..."
|
||||
emptyText="Keine Kontakte gefunden"
|
||||
searchingText="Suche..."
|
||||
searchText="Suchen"
|
||||
onCreate={handleCreate}
|
||||
onParseCreate={handleParseCreate}
|
||||
createText="Erstellen"
|
||||
deferSearch={true}
|
||||
locale={$locale || 'de'}
|
||||
appIcon="contacts"
|
||||
bottomOffset={inputBarBottomOffset}
|
||||
hasFabRight={showContactsToolbar}
|
||||
/>
|
||||
|
||||
<!-- Contacts Toolbar (FAB + expandable bar) - only on main page -->
|
||||
{#if showContactsToolbar}
|
||||
<ContactsToolbar contacts={contactsStore.contacts} />
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
<!-- Immersive Mode Toggle (always visible) -->
|
||||
<ImmersiveModeToggle
|
||||
isImmersive={contactsSettings.immersiveModeEnabled}
|
||||
onToggle={() => contactsSettings.toggleImmersiveMode()}
|
||||
/>
|
||||
|
||||
<!-- Main Content -->
|
||||
<main
|
||||
id="main-content"
|
||||
class="main-content bg-background"
|
||||
class:immersive={contactsSettings.immersiveModeEnabled}
|
||||
>
|
||||
<div class="content-wrapper" class:immersive={contactsSettings.immersiveModeEnabled}>
|
||||
{@render children()}
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- Contact Detail Modal -->
|
||||
{#if showContactModal && modalContactId}
|
||||
<ContactDetailModal contactId={modalContactId} onClose={handleCloseContactModal} />
|
||||
{/if}
|
||||
|
||||
<!-- New Contact Modal -->
|
||||
{#if newContactModalStore.isOpen}
|
||||
<NewContactModal onClose={() => newContactModalStore.close()} />
|
||||
{/if}
|
||||
|
||||
<!-- Onboarding Modal -->
|
||||
{#if contactsOnboarding.shouldShow}
|
||||
<MiniOnboardingModal store={contactsOnboarding} appName="Kontakte" appEmoji="👥" />
|
||||
{/if}
|
||||
</div>
|
||||
</SplitPaneContainer>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.layout-container {
|
||||
|
|
|
|||
|
|
@ -15,6 +15,9 @@
|
|||
|
||||
let { children } = $props();
|
||||
|
||||
// Auth gate - prevent children from mounting before auth is confirmed
|
||||
let appReady = $state(false);
|
||||
|
||||
let isDark = $derived(theme.isDark);
|
||||
let userEmail = $derived(authStore.user?.email || 'Menu');
|
||||
|
||||
|
|
@ -88,50 +91,59 @@
|
|||
|
||||
// Load initial data
|
||||
await Promise.all([photoStore.loadStats(), albumStore.loadAlbums(), tagStore.loadTags()]);
|
||||
|
||||
// Auth confirmed - allow children to render
|
||||
appReady = true;
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="layout-container">
|
||||
<PillNavigation
|
||||
items={navItems}
|
||||
currentPath={$page.url.pathname}
|
||||
appName="Photos"
|
||||
homeRoute="/"
|
||||
onToggleTheme={handleToggleTheme}
|
||||
{isDark}
|
||||
desktopPosition="bottom"
|
||||
showThemeToggle={true}
|
||||
showThemeVariants={true}
|
||||
{themeVariantItems}
|
||||
{currentThemeVariantLabel}
|
||||
themeMode={theme.mode}
|
||||
onThemeModeChange={handleThemeModeChange}
|
||||
showLogout={true}
|
||||
onLogout={handleLogout}
|
||||
loginHref="/login"
|
||||
primaryColor="#8b5cf6"
|
||||
{userEmail}
|
||||
settingsHref="/settings"
|
||||
/>
|
||||
{#if !appReady}
|
||||
<div class="flex items-center justify-center h-screen bg-background">
|
||||
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-primary"></div>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="layout-container">
|
||||
<PillNavigation
|
||||
items={navItems}
|
||||
currentPath={$page.url.pathname}
|
||||
appName="Photos"
|
||||
homeRoute="/"
|
||||
onToggleTheme={handleToggleTheme}
|
||||
{isDark}
|
||||
desktopPosition="bottom"
|
||||
showThemeToggle={true}
|
||||
showThemeVariants={true}
|
||||
{themeVariantItems}
|
||||
{currentThemeVariantLabel}
|
||||
themeMode={theme.mode}
|
||||
onThemeModeChange={handleThemeModeChange}
|
||||
showLogout={true}
|
||||
onLogout={handleLogout}
|
||||
loginHref="/login"
|
||||
primaryColor="#8b5cf6"
|
||||
{userEmail}
|
||||
settingsHref="/settings"
|
||||
/>
|
||||
|
||||
<!-- Quick Input Bar -->
|
||||
<QuickInputBar
|
||||
onSearch={handleInputSearch}
|
||||
onSelect={handleInputSelect}
|
||||
placeholder="Album oder Tag suchen..."
|
||||
emptyText="Nichts gefunden"
|
||||
searchingText="Suche..."
|
||||
locale={$locale || 'de'}
|
||||
appIcon="search"
|
||||
bottomOffset="70px"
|
||||
/>
|
||||
<!-- Quick Input Bar -->
|
||||
<QuickInputBar
|
||||
onSearch={handleInputSearch}
|
||||
onSelect={handleInputSelect}
|
||||
placeholder="Album oder Tag suchen..."
|
||||
emptyText="Nichts gefunden"
|
||||
searchingText="Suche..."
|
||||
locale={$locale || 'de'}
|
||||
appIcon="search"
|
||||
bottomOffset="70px"
|
||||
/>
|
||||
|
||||
<main class="main-content bg-background">
|
||||
<div class="content-wrapper">
|
||||
{@render children()}
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
<main class="main-content bg-background">
|
||||
<div class="content-wrapper">
|
||||
{@render children()}
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.layout-container {
|
||||
|
|
|
|||
|
|
@ -10,6 +10,9 @@
|
|||
|
||||
let { children } = $props();
|
||||
|
||||
// Auth gate - prevent children from mounting before auth is confirmed
|
||||
let appReady = $state(false);
|
||||
|
||||
// Navigation items for Planta
|
||||
const navItems: PillNavItem[] = [
|
||||
{ href: '/dashboard', label: 'Meine Pflanzen', icon: 'document' },
|
||||
|
|
@ -51,14 +54,20 @@
|
|||
goto(`/plant/${item.id}`);
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
onMount(async () => {
|
||||
// Initialize auth state from stored tokens
|
||||
await authStore.initialize();
|
||||
if (!authStore.isAuthenticated) {
|
||||
goto('/login');
|
||||
return;
|
||||
}
|
||||
|
||||
// Auth confirmed - allow children to render
|
||||
appReady = true;
|
||||
});
|
||||
</script>
|
||||
|
||||
{#if authStore.isAuthenticated}
|
||||
{#if appReady}
|
||||
<div class="layout-container">
|
||||
<PillNavigation
|
||||
items={navItems}
|
||||
|
|
|
|||
|
|
@ -55,6 +55,9 @@
|
|||
|
||||
let { children } = $props();
|
||||
|
||||
// Auth gate - prevent children from mounting before auth is confirmed
|
||||
let appReady = $state(false);
|
||||
|
||||
// QuickInputBar search - search tasks
|
||||
async function handleSearch(query: string): Promise<QuickInputItem[]> {
|
||||
if (!query.trim()) return [];
|
||||
|
|
@ -306,161 +309,170 @@
|
|||
} catch {
|
||||
// localStorage not available
|
||||
}
|
||||
|
||||
// Auth confirmed - allow children to render
|
||||
appReady = true;
|
||||
});
|
||||
</script>
|
||||
|
||||
<svelte:window onkeydown={handleKeydown} />
|
||||
|
||||
<SplitPaneContainer>
|
||||
<div class="layout-container">
|
||||
<a
|
||||
href="#main-content"
|
||||
class="sr-only focus:not-sr-only focus:fixed focus:top-2 focus:left-2 focus:z-[100] focus:rounded-lg focus:bg-primary focus:px-4 focus:py-2 focus:text-white"
|
||||
>
|
||||
Zum Inhalt springen
|
||||
</a>
|
||||
{#if !appReady}
|
||||
<div class="flex items-center justify-center h-screen bg-background">
|
||||
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-primary"></div>
|
||||
</div>
|
||||
{:else}
|
||||
<SplitPaneContainer>
|
||||
<div class="layout-container">
|
||||
<a
|
||||
href="#main-content"
|
||||
class="sr-only focus:not-sr-only focus:fixed focus:top-2 focus:left-2 focus:z-[100] focus:rounded-lg focus:bg-primary focus:px-4 focus:py-2 focus:text-white"
|
||||
>
|
||||
Zum Inhalt springen
|
||||
</a>
|
||||
|
||||
<!-- UI Elements (hidden in immersive mode) -->
|
||||
{#if !todoSettings.immersiveModeEnabled}
|
||||
<!-- PillNav (shown/hidden via FAB) -->
|
||||
{#if !isPillNavCollapsed}
|
||||
<PillNavigation
|
||||
items={navItems}
|
||||
prependElements={[viewTabGroup]}
|
||||
currentPath={$page.url.pathname}
|
||||
appName="Todo"
|
||||
homeRoute="/"
|
||||
onToggleTheme={handleToggleTheme}
|
||||
{isDark}
|
||||
showThemeToggle={true}
|
||||
showThemeVariants={true}
|
||||
{themeVariantItems}
|
||||
{currentThemeVariantLabel}
|
||||
themeMode={theme.mode}
|
||||
onThemeModeChange={handleThemeModeChange}
|
||||
showLanguageSwitcher={true}
|
||||
{languageItems}
|
||||
{currentLanguageLabel}
|
||||
showLogout={true}
|
||||
onLogout={handleLogout}
|
||||
loginHref="/login"
|
||||
primaryColor="#8b5cf6"
|
||||
showAppSwitcher={true}
|
||||
{appItems}
|
||||
{userEmail}
|
||||
settingsHref="/settings"
|
||||
manaHref="/mana"
|
||||
profileHref="/profile"
|
||||
allAppsHref="/apps"
|
||||
themesHref="/themes"
|
||||
spiralHref="/spiral"
|
||||
onOpenInPanel={handleOpenInPanel}
|
||||
ariaLabel="Hauptnavigation"
|
||||
/>
|
||||
<!-- UI Elements (hidden in immersive mode) -->
|
||||
{#if !todoSettings.immersiveModeEnabled}
|
||||
<!-- PillNav (shown/hidden via FAB) -->
|
||||
{#if !isPillNavCollapsed}
|
||||
<PillNavigation
|
||||
items={navItems}
|
||||
prependElements={[viewTabGroup]}
|
||||
currentPath={$page.url.pathname}
|
||||
appName="Todo"
|
||||
homeRoute="/"
|
||||
onToggleTheme={handleToggleTheme}
|
||||
{isDark}
|
||||
showThemeToggle={true}
|
||||
showThemeVariants={true}
|
||||
{themeVariantItems}
|
||||
{currentThemeVariantLabel}
|
||||
themeMode={theme.mode}
|
||||
onThemeModeChange={handleThemeModeChange}
|
||||
showLanguageSwitcher={true}
|
||||
{languageItems}
|
||||
{currentLanguageLabel}
|
||||
showLogout={true}
|
||||
onLogout={handleLogout}
|
||||
loginHref="/login"
|
||||
primaryColor="#8b5cf6"
|
||||
showAppSwitcher={true}
|
||||
{appItems}
|
||||
{userEmail}
|
||||
settingsHref="/settings"
|
||||
manaHref="/mana"
|
||||
profileHref="/profile"
|
||||
allAppsHref="/apps"
|
||||
themesHref="/themes"
|
||||
spiralHref="/spiral"
|
||||
onOpenInPanel={handleOpenInPanel}
|
||||
ariaLabel="Hauptnavigation"
|
||||
/>
|
||||
|
||||
<!-- TagStrip (above PillNav, always visible when PillNav is open) -->
|
||||
<TagStrip filterStripVisible={isFilterStripVisible} />
|
||||
<!-- TagStrip (above PillNav, always visible when PillNav is open) -->
|
||||
<TagStrip filterStripVisible={isFilterStripVisible} />
|
||||
|
||||
<!-- TaskFilters strip (shown when Filter pill is active in PillNav) -->
|
||||
{#if isFilterStripVisible}
|
||||
<TaskFilters
|
||||
variant="strip"
|
||||
selectedPriorities={viewStore.filterPriorities}
|
||||
selectedProjectId={viewStore.filterProjectId}
|
||||
selectedLabelIds={viewStore.filterLabelIds}
|
||||
searchQuery={viewStore.filterSearchQuery}
|
||||
onPrioritiesChange={(p: TaskPriority[]) => viewStore.setFilterPriorities(p)}
|
||||
onProjectChange={(id: string | null) => viewStore.setFilterProjectId(id)}
|
||||
onLabelsChange={(ids: string[]) => viewStore.setFilterLabelIds(ids)}
|
||||
onSearchChange={(q: string) => viewStore.setFilterSearchQuery(q)}
|
||||
onClearFilters={() => viewStore.clearFilters()}
|
||||
sortBy={viewStore.sortBy}
|
||||
onSortChange={(s: SortBy) => viewStore.setSort(s, viewStore.sortOrder)}
|
||||
showSort={true}
|
||||
showCompleted={true}
|
||||
showKanbanNav={true}
|
||||
isCompletedVisible={viewStore.showCompleted}
|
||||
onToggleCompleted={() => viewStore.toggleShowCompleted()}
|
||||
<!-- TaskFilters strip (shown when Filter pill is active in PillNav) -->
|
||||
{#if isFilterStripVisible}
|
||||
<TaskFilters
|
||||
variant="strip"
|
||||
selectedPriorities={viewStore.filterPriorities}
|
||||
selectedProjectId={viewStore.filterProjectId}
|
||||
selectedLabelIds={viewStore.filterLabelIds}
|
||||
searchQuery={viewStore.filterSearchQuery}
|
||||
onPrioritiesChange={(p: TaskPriority[]) => viewStore.setFilterPriorities(p)}
|
||||
onProjectChange={(id: string | null) => viewStore.setFilterProjectId(id)}
|
||||
onLabelsChange={(ids: string[]) => viewStore.setFilterLabelIds(ids)}
|
||||
onSearchChange={(q: string) => viewStore.setFilterSearchQuery(q)}
|
||||
onClearFilters={() => viewStore.clearFilters()}
|
||||
sortBy={viewStore.sortBy}
|
||||
onSortChange={(s: SortBy) => viewStore.setSort(s, viewStore.sortOrder)}
|
||||
showSort={true}
|
||||
showCompleted={true}
|
||||
showKanbanNav={true}
|
||||
isCompletedVisible={viewStore.showCompleted}
|
||||
onToggleCompleted={() => viewStore.toggleShowCompleted()}
|
||||
/>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
<!-- Global Quick Input Bar - only on list and kanban views -->
|
||||
{#if $page.url.pathname === '/' || $page.url.pathname === '/kanban'}
|
||||
<QuickInputBar
|
||||
onSearch={handleSearch}
|
||||
onSelect={handleSelect}
|
||||
placeholder="Neue Aufgabe oder suchen..."
|
||||
emptyText="Keine Aufgaben gefunden"
|
||||
searchingText="Suche..."
|
||||
searchText="Suchen"
|
||||
onCreate={handleCreate}
|
||||
onParseCreate={handleParseCreate}
|
||||
createText="Erstellen"
|
||||
deferSearch={true}
|
||||
locale={$locale || 'de'}
|
||||
appIcon="todo"
|
||||
hasFabRight={true}
|
||||
bottomOffset={isPillNavCollapsed ? '16px' : isFilterStripVisible ? '180px' : '110px'}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<!-- FAB to toggle PillNav visibility -->
|
||||
<button
|
||||
class="pillnav-fab"
|
||||
onclick={handlePillNavToggle}
|
||||
title={isPillNavCollapsed ? 'Navigation anzeigen' : 'Navigation ausblenden'}
|
||||
aria-label={isPillNavCollapsed ? 'Navigation anzeigen' : 'Navigation ausblenden'}
|
||||
>
|
||||
{#if isPillNavCollapsed}
|
||||
<!-- Menu icon -->
|
||||
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24" class="fab-icon">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M4 6h16M4 12h16M4 18h16"
|
||||
/>
|
||||
</svg>
|
||||
{:else}
|
||||
<!-- Close icon -->
|
||||
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24" class="fab-icon">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M6 18L18 6M6 6l12 12"
|
||||
/>
|
||||
</svg>
|
||||
{/if}
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
<!-- Global Quick Input Bar - only on list and kanban views -->
|
||||
{#if $page.url.pathname === '/' || $page.url.pathname === '/kanban'}
|
||||
<QuickInputBar
|
||||
onSearch={handleSearch}
|
||||
onSelect={handleSelect}
|
||||
placeholder="Neue Aufgabe oder suchen..."
|
||||
emptyText="Keine Aufgaben gefunden"
|
||||
searchingText="Suche..."
|
||||
searchText="Suchen"
|
||||
onCreate={handleCreate}
|
||||
onParseCreate={handleParseCreate}
|
||||
createText="Erstellen"
|
||||
deferSearch={true}
|
||||
locale={$locale || 'de'}
|
||||
appIcon="todo"
|
||||
hasFabRight={true}
|
||||
bottomOffset={isPillNavCollapsed ? '16px' : isFilterStripVisible ? '180px' : '110px'}
|
||||
/>
|
||||
{/if}
|
||||
<!-- Immersive Mode Toggle (always visible) -->
|
||||
<ImmersiveModeToggle
|
||||
isImmersive={todoSettings.immersiveModeEnabled}
|
||||
onToggle={() => todoSettings.toggleImmersiveMode()}
|
||||
/>
|
||||
|
||||
<!-- FAB to toggle PillNav visibility -->
|
||||
<button
|
||||
class="pillnav-fab"
|
||||
onclick={handlePillNavToggle}
|
||||
title={isPillNavCollapsed ? 'Navigation anzeigen' : 'Navigation ausblenden'}
|
||||
aria-label={isPillNavCollapsed ? 'Navigation anzeigen' : 'Navigation ausblenden'}
|
||||
>
|
||||
{#if isPillNavCollapsed}
|
||||
<!-- Menu icon -->
|
||||
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24" class="fab-icon">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M4 6h16M4 12h16M4 18h16"
|
||||
/>
|
||||
</svg>
|
||||
{:else}
|
||||
<!-- Close icon -->
|
||||
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24" class="fab-icon">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M6 18L18 6M6 6l12 12"
|
||||
/>
|
||||
</svg>
|
||||
{/if}
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
<!-- Immersive Mode Toggle (always visible) -->
|
||||
<ImmersiveModeToggle
|
||||
isImmersive={todoSettings.immersiveModeEnabled}
|
||||
onToggle={() => todoSettings.toggleImmersiveMode()}
|
||||
/>
|
||||
|
||||
<main
|
||||
id="main-content"
|
||||
class="main-content bg-background"
|
||||
class:immersive={todoSettings.immersiveModeEnabled}
|
||||
>
|
||||
<div
|
||||
class="content-wrapper"
|
||||
class:full-width={$page.url.pathname === '/kanban'}
|
||||
<main
|
||||
id="main-content"
|
||||
class="main-content bg-background"
|
||||
class:immersive={todoSettings.immersiveModeEnabled}
|
||||
>
|
||||
{@render children()}
|
||||
</div>
|
||||
</main>
|
||||
<!-- Onboarding Modal -->
|
||||
{#if todoOnboarding.shouldShow}
|
||||
<MiniOnboardingModal store={todoOnboarding} appName="Todo" appEmoji="✅" />
|
||||
{/if}
|
||||
</div>
|
||||
</SplitPaneContainer>
|
||||
<div
|
||||
class="content-wrapper"
|
||||
class:full-width={$page.url.pathname === '/kanban'}
|
||||
class:immersive={todoSettings.immersiveModeEnabled}
|
||||
>
|
||||
{@render children()}
|
||||
</div>
|
||||
</main>
|
||||
<!-- Onboarding Modal -->
|
||||
{#if todoOnboarding.shouldShow}
|
||||
<MiniOnboardingModal store={todoOnboarding} appName="Todo" appEmoji="✅" />
|
||||
{/if}
|
||||
</div>
|
||||
</SplitPaneContainer>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.layout-container {
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@
|
|||
import { de } from 'date-fns/locale';
|
||||
import { Sparkle, ArrowDown } from '@manacore/shared-icons';
|
||||
import { tasksStore } from '$lib/stores/tasks.svelte';
|
||||
import { authStore } from '$lib/stores/auth.svelte';
|
||||
import { viewStore } from '$lib/stores/view.svelte';
|
||||
import { applyTaskFilters } from '$lib/utils/task-filters';
|
||||
import TaskList from '$lib/components/TaskList.svelte';
|
||||
|
|
@ -30,11 +29,6 @@
|
|||
onMount(async () => {
|
||||
viewStore.setToday();
|
||||
|
||||
// Wait for auth to be initialized (layout onMount runs after page onMount in Svelte)
|
||||
while (!authStore.initialized) {
|
||||
await new Promise((r) => setTimeout(r, 50));
|
||||
}
|
||||
|
||||
try {
|
||||
// Fetch tasks (works in both guest and authenticated mode)
|
||||
await tasksStore.fetchAllTasks();
|
||||
|
|
|
|||
|
|
@ -40,6 +40,9 @@
|
|||
|
||||
let { children } = $props();
|
||||
|
||||
// Auth gate - prevent children from mounting before auth is confirmed
|
||||
let appReady = $state(false);
|
||||
|
||||
// Use theme store's isDark directly
|
||||
let isDark = $derived(theme.isDark);
|
||||
|
||||
|
|
@ -209,7 +212,10 @@
|
|||
zitareSettings.togglePillNav();
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
onMount(async () => {
|
||||
// Initialize auth state from stored tokens
|
||||
await authStore.initialize();
|
||||
|
||||
// Initialize settings
|
||||
zitareSettings.initialize();
|
||||
|
||||
|
|
@ -220,6 +226,9 @@
|
|||
listsStore.loadLists();
|
||||
}
|
||||
|
||||
// Auth confirmed - allow children to render
|
||||
appReady = true;
|
||||
|
||||
// Add keyboard listener
|
||||
window.addEventListener('keydown', handleKeydown);
|
||||
|
||||
|
|
@ -229,98 +238,104 @@
|
|||
});
|
||||
</script>
|
||||
|
||||
<div class="layout-container">
|
||||
{#if !zitareSettings.immersiveModeEnabled}
|
||||
<!-- PillNav (shown/hidden via FAB) -->
|
||||
{#if !zitareSettings.pillNavCollapsed}
|
||||
<PillNavigation
|
||||
items={navItems}
|
||||
currentPath={$page.url.pathname}
|
||||
appName="Zitare"
|
||||
homeRoute="/"
|
||||
onToggleTheme={handleToggleTheme}
|
||||
{isDark}
|
||||
showThemeToggle={true}
|
||||
showThemeVariants={true}
|
||||
{themeVariantItems}
|
||||
{currentThemeVariantLabel}
|
||||
themeMode={theme.mode}
|
||||
onThemeModeChange={handleThemeModeChange}
|
||||
showLanguageSwitcher={true}
|
||||
{languageItems}
|
||||
{currentLanguageLabel}
|
||||
showLogout={authStore.isAuthenticated}
|
||||
onLogout={handleLogout}
|
||||
loginHref="/login"
|
||||
primaryColor="#8b5cf6"
|
||||
showAppSwitcher={true}
|
||||
{appItems}
|
||||
{userEmail}
|
||||
settingsHref="/settings"
|
||||
manaHref="/mana"
|
||||
profileHref="/profile"
|
||||
allAppsHref="/apps"
|
||||
{#if !appReady}
|
||||
<div class="flex items-center justify-center h-screen bg-background">
|
||||
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-primary"></div>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="layout-container">
|
||||
{#if !zitareSettings.immersiveModeEnabled}
|
||||
<!-- PillNav (shown/hidden via FAB) -->
|
||||
{#if !zitareSettings.pillNavCollapsed}
|
||||
<PillNavigation
|
||||
items={navItems}
|
||||
currentPath={$page.url.pathname}
|
||||
appName="Zitare"
|
||||
homeRoute="/"
|
||||
onToggleTheme={handleToggleTheme}
|
||||
{isDark}
|
||||
showThemeToggle={true}
|
||||
showThemeVariants={true}
|
||||
{themeVariantItems}
|
||||
{currentThemeVariantLabel}
|
||||
themeMode={theme.mode}
|
||||
onThemeModeChange={handleThemeModeChange}
|
||||
showLanguageSwitcher={true}
|
||||
{languageItems}
|
||||
{currentLanguageLabel}
|
||||
showLogout={authStore.isAuthenticated}
|
||||
onLogout={handleLogout}
|
||||
loginHref="/login"
|
||||
primaryColor="#8b5cf6"
|
||||
showAppSwitcher={true}
|
||||
{appItems}
|
||||
{userEmail}
|
||||
settingsHref="/settings"
|
||||
manaHref="/mana"
|
||||
profileHref="/profile"
|
||||
allAppsHref="/apps"
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<!-- Global Quick Input Bar -->
|
||||
<QuickInputBar
|
||||
onSearch={handleSearch}
|
||||
onSelect={handleSelect}
|
||||
onCreate={handleCreate}
|
||||
onParseCreate={handleParseCreate}
|
||||
placeholder={$_('search.placeholder')}
|
||||
emptyText={$_('search.noResults')}
|
||||
searchingText={$_('search.searching')}
|
||||
createText={$_('search.create')}
|
||||
deferSearch={true}
|
||||
locale={$locale || 'de'}
|
||||
appIcon="quote"
|
||||
bottomOffset={inputBarBottomOffset}
|
||||
hasFabRight={true}
|
||||
/>
|
||||
|
||||
<!-- FAB to toggle PillNav visibility -->
|
||||
<button
|
||||
class="pillnav-fab"
|
||||
onclick={handlePillNavToggle}
|
||||
title={zitareSettings.pillNavCollapsed ? $_('nav.showNav') : $_('nav.hideNav')}
|
||||
>
|
||||
{#if zitareSettings.pillNavCollapsed}
|
||||
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24" class="fab-icon">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M4 6h16M4 12h16M4 18h16"
|
||||
/>
|
||||
</svg>
|
||||
{:else}
|
||||
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24" class="fab-icon">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M6 18L18 6M6 6l12 12"
|
||||
/>
|
||||
</svg>
|
||||
{/if}
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
<!-- Global Quick Input Bar -->
|
||||
<QuickInputBar
|
||||
onSearch={handleSearch}
|
||||
onSelect={handleSelect}
|
||||
onCreate={handleCreate}
|
||||
onParseCreate={handleParseCreate}
|
||||
placeholder={$_('search.placeholder')}
|
||||
emptyText={$_('search.noResults')}
|
||||
searchingText={$_('search.searching')}
|
||||
createText={$_('search.create')}
|
||||
deferSearch={true}
|
||||
locale={$locale || 'de'}
|
||||
appIcon="quote"
|
||||
bottomOffset={inputBarBottomOffset}
|
||||
hasFabRight={true}
|
||||
<!-- Immersive Mode Toggle (always visible) -->
|
||||
<ImmersiveModeToggle
|
||||
isImmersive={zitareSettings.immersiveModeEnabled}
|
||||
onToggle={() => zitareSettings.toggleImmersiveMode()}
|
||||
/>
|
||||
|
||||
<!-- FAB to toggle PillNav visibility -->
|
||||
<button
|
||||
class="pillnav-fab"
|
||||
onclick={handlePillNavToggle}
|
||||
title={zitareSettings.pillNavCollapsed ? $_('nav.showNav') : $_('nav.hideNav')}
|
||||
>
|
||||
{#if zitareSettings.pillNavCollapsed}
|
||||
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24" class="fab-icon">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M4 6h16M4 12h16M4 18h16"
|
||||
/>
|
||||
</svg>
|
||||
{:else}
|
||||
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24" class="fab-icon">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M6 18L18 6M6 6l12 12"
|
||||
/>
|
||||
</svg>
|
||||
{/if}
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
<!-- Immersive Mode Toggle (always visible) -->
|
||||
<ImmersiveModeToggle
|
||||
isImmersive={zitareSettings.immersiveModeEnabled}
|
||||
onToggle={() => zitareSettings.toggleImmersiveMode()}
|
||||
/>
|
||||
|
||||
<!-- Main content -->
|
||||
<main class="main-content bg-background" class:immersive={zitareSettings.immersiveModeEnabled}>
|
||||
<div class="content-wrapper" class:immersive={zitareSettings.immersiveModeEnabled}>
|
||||
{@render children()}
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
<!-- Main content -->
|
||||
<main class="main-content bg-background" class:immersive={zitareSettings.immersiveModeEnabled}>
|
||||
<div class="content-wrapper" class:immersive={zitareSettings.immersiveModeEnabled}>
|
||||
{@render children()}
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.layout-container {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue