mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 21:01:08 +02:00
refactor(auth): centralize appReady pattern into AuthGate component
Replace copy-pasted appReady/loading/redirect logic in all 13 layouts with a shared AuthGate component. Supports guest mode, onReady callback for app-specific data loading, and configurable login redirect. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
31af413b77
commit
336cfedd0b
15 changed files with 270 additions and 407 deletions
|
|
@ -1,7 +1,6 @@
|
|||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import { page } from '$app/stores';
|
||||
import { onMount } from 'svelte';
|
||||
import { locale } from 'svelte-i18n';
|
||||
import { PillNavigation, InputBarHelpModal, ImmersiveModeToggle } from '@manacore/shared-ui';
|
||||
import {
|
||||
|
|
@ -55,7 +54,7 @@
|
|||
import { voiceRecordingStore } from '$lib/stores/voice-recording.svelte';
|
||||
import { calendarOnboarding } from '$lib/stores/app-onboarding.svelte';
|
||||
import { MiniOnboardingModal } from '@manacore/shared-app-onboarding';
|
||||
import { SessionExpiredBanner } from '@manacore/shared-auth-ui';
|
||||
import { SessionExpiredBanner, AuthGate } from '@manacore/shared-auth-ui';
|
||||
|
||||
// App switcher items
|
||||
const appItems = getPillAppItems('calendar');
|
||||
|
|
@ -70,9 +69,6 @@
|
|||
|
||||
let { children } = $props();
|
||||
|
||||
// Auth gate - prevent children from mounting before auth is confirmed
|
||||
let appReady = $state(false);
|
||||
|
||||
// InputBar search - search events
|
||||
async function handleSearch(query: string): Promise<QuickInputItem[]> {
|
||||
if (!query.trim()) return [];
|
||||
|
|
@ -431,19 +427,7 @@
|
|||
);
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
// Initialize auth state from stored tokens
|
||||
await authStore.initialize();
|
||||
|
||||
// Redirect to login if not authenticated
|
||||
if (!authStore.isAuthenticated) {
|
||||
goto('/login');
|
||||
return;
|
||||
}
|
||||
|
||||
// Auth confirmed - allow children to render
|
||||
appReady = true;
|
||||
|
||||
async function handleAuthReady() {
|
||||
// Initialize split-panel from URL/localStorage
|
||||
splitPanel.initialize();
|
||||
|
||||
|
|
@ -459,26 +443,20 @@
|
|||
|
||||
// Note: Birthdays are loaded via reactive $effect when showBirthdays is enabled
|
||||
|
||||
// Redirect to start page if on root and a custom start page is set (only if authenticated)
|
||||
if (authStore.isAuthenticated) {
|
||||
const currentPath = window.location.pathname;
|
||||
if (currentPath === '/' && userSettings.startPage && userSettings.startPage !== '/') {
|
||||
goto(userSettings.startPage, { replaceState: true });
|
||||
}
|
||||
// Redirect to start page if on root and a custom start page is set
|
||||
const currentPath = window.location.pathname;
|
||||
if (currentPath === '/' && userSettings.startPage && userSettings.startPage !== '/') {
|
||||
goto(userSettings.startPage, { replaceState: true });
|
||||
}
|
||||
|
||||
// Initialize mobile state
|
||||
updateMobileState();
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:window onkeydown={handleKeydown} onresize={updateMobileState} />
|
||||
|
||||
{#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}
|
||||
<AuthGate {authStore} {goto} onReady={handleAuthReady}>
|
||||
<SplitPaneContainer>
|
||||
<div class="layout-container">
|
||||
<a
|
||||
|
|
@ -598,7 +576,7 @@
|
|||
<MiniOnboardingModal store={calendarOnboarding} appName="Kalender" appEmoji="📅" />
|
||||
{/if}
|
||||
<SessionExpiredBanner locale={$locale || 'de'} loginHref="/login" />
|
||||
{/if}
|
||||
</AuthGate>
|
||||
|
||||
<style>
|
||||
.layout-container {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import { page } from '$app/stores';
|
||||
import { locale } from 'svelte-i18n';
|
||||
|
|
@ -24,14 +23,13 @@
|
|||
import type { LayoutData } from './$types';
|
||||
import { chatOnboarding } from '$lib/stores/app-onboarding.svelte';
|
||||
import { MiniOnboardingModal } from '@manacore/shared-app-onboarding';
|
||||
import { SessionExpiredBanner } from '@manacore/shared-auth-ui';
|
||||
import { SessionExpiredBanner, AuthGate } from '@manacore/shared-auth-ui';
|
||||
|
||||
// App switcher items
|
||||
const appItems = getPillAppItems('chat');
|
||||
|
||||
let { children, data }: { children: any; data: LayoutData } = $props();
|
||||
|
||||
let isChecking = $state(true);
|
||||
let isCollapsed = $state(false);
|
||||
|
||||
// Use theme store's isDark directly
|
||||
|
|
@ -144,15 +142,7 @@
|
|||
goto('/login');
|
||||
}
|
||||
|
||||
// Initialize on mount - enforce login
|
||||
onMount(async () => {
|
||||
// Initialize auth and redirect if not authenticated
|
||||
await authStore.initialize();
|
||||
if (!authStore.isAuthenticated) {
|
||||
goto('/login');
|
||||
return;
|
||||
}
|
||||
|
||||
async function handleAuthReady() {
|
||||
// Initialize theme
|
||||
theme.initialize();
|
||||
|
||||
|
|
@ -176,24 +166,12 @@
|
|||
if (currentPath === '/chat' && userSettings.startPage && userSettings.startPage !== '/chat') {
|
||||
goto(userSettings.startPage, { replaceState: true });
|
||||
}
|
||||
|
||||
isChecking = false;
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:window onkeydown={handleKeydown} />
|
||||
|
||||
{#if isChecking}
|
||||
<!-- Loading state while checking auth -->
|
||||
<div class="flex min-h-screen items-center justify-center bg-background">
|
||||
<div class="text-center">
|
||||
<div
|
||||
class="mb-4 inline-block h-12 w-12 animate-spin rounded-full border-4 border-solid border-primary border-r-transparent"
|
||||
></div>
|
||||
<p class="text-muted-foreground">Laden...</p>
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<AuthGate {authStore} {goto} onReady={handleAuthReady}>
|
||||
<!-- Navigation Layout -->
|
||||
<div class="layout-container">
|
||||
<!-- Floating Pill Navigation -->
|
||||
|
|
@ -245,7 +223,7 @@
|
|||
{/if}
|
||||
</div>
|
||||
<SessionExpiredBanner locale={$locale || 'de'} loginHref="/login" />
|
||||
{/if}
|
||||
</AuthGate>
|
||||
|
||||
<style>
|
||||
.layout-container {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import { page } from '$app/stores';
|
||||
import { onMount } from 'svelte';
|
||||
import { locale } from 'svelte-i18n';
|
||||
import { PillNavigation, CommandBar } from '@manacore/shared-ui';
|
||||
import type {
|
||||
|
|
@ -32,16 +31,13 @@
|
|||
import { timersApi } from '$lib/api/timers';
|
||||
import { clockOnboarding } from '$lib/stores/app-onboarding.svelte';
|
||||
import { MiniOnboardingModal } from '@manacore/shared-app-onboarding';
|
||||
import { SessionExpiredBanner } from '@manacore/shared-auth-ui';
|
||||
import { SessionExpiredBanner, AuthGate } from '@manacore/shared-auth-ui';
|
||||
|
||||
// App switcher items
|
||||
const appItems = getPillAppItems('clock');
|
||||
|
||||
let { children } = $props();
|
||||
|
||||
// Auth gate - prevent children from mounting before auth is confirmed
|
||||
let appReady = $state(false);
|
||||
|
||||
// CommandBar state
|
||||
let commandBarOpen = $state(false);
|
||||
|
||||
|
|
@ -235,14 +231,7 @@
|
|||
goto('/login');
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
// Initialize auth and redirect if not authenticated
|
||||
await authStore.initialize();
|
||||
if (!authStore.isAuthenticated) {
|
||||
goto('/login');
|
||||
return;
|
||||
}
|
||||
|
||||
async function handleAuthReady() {
|
||||
// Initialize collapsed state from localStorage
|
||||
const savedCollapsed = localStorage.getItem('clock-nav-collapsed');
|
||||
if (savedCollapsed === 'true') {
|
||||
|
|
@ -266,19 +255,12 @@
|
|||
if (currentPath === '/' && userSettings.startPage && userSettings.startPage !== '/') {
|
||||
goto(userSettings.startPage, { replaceState: true });
|
||||
}
|
||||
|
||||
// Auth confirmed - allow children to render
|
||||
appReady = true;
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:window onkeydown={handleKeydown} />
|
||||
|
||||
{#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}
|
||||
<AuthGate {authStore} {goto} onReady={handleAuthReady}>
|
||||
<div class="layout-container">
|
||||
<PillNavigation
|
||||
items={navItems}
|
||||
|
|
@ -335,7 +317,7 @@
|
|||
{/if}
|
||||
</div>
|
||||
<SessionExpiredBanner locale={$locale || 'de'} loginHref="/login" />
|
||||
{/if}
|
||||
</AuthGate>
|
||||
|
||||
<style>
|
||||
.layout-container {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import { page } from '$app/stores';
|
||||
import { onMount } from 'svelte';
|
||||
import { locale } from 'svelte-i18n';
|
||||
import { PillNavigation, QuickInputBar, ImmersiveModeToggle } from '@manacore/shared-ui';
|
||||
import {
|
||||
|
|
@ -46,7 +45,7 @@
|
|||
import { tagsStore } from '$lib/stores/tags.svelte';
|
||||
import { contactsOnboarding } from '$lib/stores/app-onboarding.svelte';
|
||||
import { MiniOnboardingModal } from '@manacore/shared-app-onboarding';
|
||||
import { SessionExpiredBanner } from '@manacore/shared-auth-ui';
|
||||
import { SessionExpiredBanner, AuthGate } from '@manacore/shared-auth-ui';
|
||||
|
||||
// Tags state for Quick-Create
|
||||
let availableTags = $state<{ id: string; name: string }[]>([]);
|
||||
|
|
@ -69,9 +68,6 @@
|
|||
|
||||
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 === '/');
|
||||
|
||||
|
|
@ -268,14 +264,7 @@
|
|||
previousOnboardingShow = showing;
|
||||
});
|
||||
|
||||
onMount(async () => {
|
||||
// Initialize auth and redirect if not authenticated
|
||||
await authStore.initialize();
|
||||
if (!authStore.isAuthenticated) {
|
||||
goto('/login');
|
||||
return;
|
||||
}
|
||||
|
||||
async function handleAuthReady() {
|
||||
// Initialize split-panel from URL/localStorage
|
||||
splitPanel.initialize();
|
||||
|
||||
|
|
@ -290,19 +279,12 @@
|
|||
// 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} />
|
||||
|
||||
{#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}
|
||||
<AuthGate {authStore} {goto} onReady={handleAuthReady}>
|
||||
<SplitPaneContainer>
|
||||
<!-- Navigation Layout -->
|
||||
<div class="layout-container">
|
||||
|
|
@ -410,7 +392,7 @@
|
|||
</div>
|
||||
</SplitPaneContainer>
|
||||
<SessionExpiredBanner locale={$locale || 'de'} loginHref="/login" />
|
||||
{/if}
|
||||
</AuthGate>
|
||||
|
||||
<style>
|
||||
.layout-container {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import { page } from '$app/stores';
|
||||
import { onMount } from 'svelte';
|
||||
import { PillNavigation, QuickInputBar, DevBuildBadge } from '@manacore/shared-ui';
|
||||
import type {
|
||||
PillNavItem,
|
||||
|
|
@ -27,7 +26,7 @@
|
|||
import { playlistStore } from '$lib/stores/playlist.svelte';
|
||||
import { projectStore } from '$lib/stores/project.svelte';
|
||||
import { parseSongInput, formatParsedSongPreview } from '$lib/utils/song-parser';
|
||||
import { SessionExpiredBanner } from '@manacore/shared-auth-ui';
|
||||
import { SessionExpiredBanner, AuthGate } from '@manacore/shared-auth-ui';
|
||||
import MiniPlayer from '$lib/components/MiniPlayer.svelte';
|
||||
import FullPlayer from '$lib/components/FullPlayer.svelte';
|
||||
import QueuePanel from '$lib/components/QueuePanel.svelte';
|
||||
|
|
@ -167,25 +166,14 @@
|
|||
goto('/projects');
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
await authStore.initialize();
|
||||
if (!authStore.isAuthenticated) {
|
||||
goto('/login');
|
||||
return;
|
||||
}
|
||||
async function handleAuthReady() {
|
||||
splitPanel.initialize();
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:window onkeydown={handleKeydown} />
|
||||
|
||||
{#if !authStore.isAuthenticated}
|
||||
<div class="min-h-screen flex items-center justify-center bg-background">
|
||||
<div
|
||||
class="w-8 h-8 border-4 border-primary border-t-transparent rounded-full animate-spin"
|
||||
></div>
|
||||
</div>
|
||||
{:else}
|
||||
<AuthGate {authStore} {goto} onReady={handleAuthReady}>
|
||||
<SplitPaneContainer>
|
||||
<div class="layout-container">
|
||||
<a
|
||||
|
|
@ -251,7 +239,7 @@
|
|||
</div>
|
||||
</SplitPaneContainer>
|
||||
<SessionExpiredBanner locale="de" loginHref="/login" />
|
||||
{/if}
|
||||
</AuthGate>
|
||||
|
||||
<style>
|
||||
.layout-container {
|
||||
|
|
|
|||
|
|
@ -8,14 +8,9 @@
|
|||
import { authStore } from '$lib/stores/auth.svelte';
|
||||
import { mealsStore } from '$lib/stores/meals.svelte';
|
||||
import { parseMealInput, formatParsedMealPreview } from '$lib/utils/meal-parser';
|
||||
import { SessionExpiredBanner } from '@manacore/shared-auth-ui';
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
import { SessionExpiredBanner, AuthGate } from '@manacore/shared-auth-ui';
|
||||
let { children } = $props();
|
||||
|
||||
let loading = $state(true);
|
||||
let appReady = $derived(!loading && !$i18nLoading);
|
||||
|
||||
// QuickInputBar handlers - search recent meals
|
||||
async function handleSearch(query: string): Promise<QuickInputItem[]> {
|
||||
const q = query.toLowerCase();
|
||||
|
|
@ -55,46 +50,42 @@
|
|||
});
|
||||
goto(`/add?${params.toString()}`);
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
authStore.initialize().then(() => {
|
||||
loading = false;
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
{#if appReady}
|
||||
{#if !$i18nLoading}
|
||||
<title>{$t('app.name')} - {$t('app.tagline')}</title>
|
||||
{:else}
|
||||
<title>NutriPhi</title>
|
||||
{/if}
|
||||
</svelte:head>
|
||||
|
||||
{#if !appReady}
|
||||
<div class="flex min-h-screen items-center justify-center bg-background">
|
||||
<div
|
||||
class="h-8 w-8 animate-spin rounded-full border-4 border-primary border-t-transparent"
|
||||
></div>
|
||||
</div>
|
||||
{:else}
|
||||
{@render children()}
|
||||
<AuthGate {authStore} {goto} allowGuest={true}>
|
||||
{#if $i18nLoading}
|
||||
<div class="flex min-h-screen items-center justify-center bg-background">
|
||||
<div
|
||||
class="h-8 w-8 animate-spin rounded-full border-4 border-primary border-t-transparent"
|
||||
></div>
|
||||
</div>
|
||||
{:else}
|
||||
{@render children()}
|
||||
|
||||
{#if authStore.isAuthenticated}
|
||||
<QuickInputBar
|
||||
onSearch={handleSearch}
|
||||
onSelect={handleSelect}
|
||||
onParseCreate={handleParseCreate}
|
||||
onCreate={handleCreate}
|
||||
placeholder="Mahlzeit eingeben..."
|
||||
emptyText="Keine Mahlzeiten gefunden"
|
||||
searchingText="Suche..."
|
||||
createText="Analysieren"
|
||||
deferSearch={true}
|
||||
locale="de"
|
||||
appIcon="search"
|
||||
bottomOffset="70px"
|
||||
/>
|
||||
{#if authStore.isAuthenticated}
|
||||
<QuickInputBar
|
||||
onSearch={handleSearch}
|
||||
onSelect={handleSelect}
|
||||
onParseCreate={handleParseCreate}
|
||||
onCreate={handleCreate}
|
||||
placeholder="Mahlzeit eingeben..."
|
||||
emptyText="Keine Mahlzeiten gefunden"
|
||||
searchingText="Suche..."
|
||||
createText="Analysieren"
|
||||
deferSearch={true}
|
||||
locale="de"
|
||||
appIcon="search"
|
||||
bottomOffset="70px"
|
||||
/>
|
||||
{/if}
|
||||
<SessionExpiredBanner locale="de" loginHref="/login" />
|
||||
{/if}
|
||||
<SessionExpiredBanner locale="de" loginHref="/login" />
|
||||
{/if}
|
||||
</AuthGate>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import { page } from '$app/stores';
|
||||
import { onMount } from 'svelte';
|
||||
import { _, locale } from 'svelte-i18n';
|
||||
import { PillNavigation, QuickInputBar } from '@manacore/shared-ui';
|
||||
import type { PillNavItem, PillDropdownItem, QuickInputItem } from '@manacore/shared-ui';
|
||||
|
|
@ -12,13 +11,10 @@
|
|||
import { tagStore } from '$lib/stores/tags.svelte';
|
||||
import { THEME_DEFINITIONS, DEFAULT_THEME_VARIANTS } from '@manacore/shared-theme';
|
||||
import type { ThemeVariant } from '@manacore/shared-theme';
|
||||
import { SessionExpiredBanner } from '@manacore/shared-auth-ui';
|
||||
import { SessionExpiredBanner, AuthGate } from '@manacore/shared-auth-ui';
|
||||
|
||||
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');
|
||||
|
||||
|
|
@ -83,26 +79,12 @@
|
|||
}
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
await authStore.initialize();
|
||||
if (!authStore.isAuthenticated) {
|
||||
goto('/login');
|
||||
return;
|
||||
}
|
||||
|
||||
// Load initial data
|
||||
async function handleAuthReady() {
|
||||
await Promise.all([photoStore.loadStats(), albumStore.loadAlbums(), tagStore.loadTags()]);
|
||||
|
||||
// Auth confirmed - allow children to render
|
||||
appReady = true;
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
{#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}
|
||||
<AuthGate {authStore} {goto} onReady={handleAuthReady}>
|
||||
<div class="layout-container">
|
||||
<PillNavigation
|
||||
items={navItems}
|
||||
|
|
@ -145,7 +127,7 @@
|
|||
</main>
|
||||
</div>
|
||||
<SessionExpiredBanner locale={$locale || 'de'} loginHref="/login" />
|
||||
{/if}
|
||||
</AuthGate>
|
||||
|
||||
<style>
|
||||
.layout-container {
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@
|
|||
import { isUIVisible, toggleUI, showKeyboardShortcuts } from '$lib/stores/ui';
|
||||
import { pictureOnboarding } from '$lib/stores/app-onboarding.svelte';
|
||||
import { MiniOnboardingModal } from '@manacore/shared-app-onboarding';
|
||||
import { SessionExpiredBanner } from '@manacore/shared-auth-ui';
|
||||
import { SessionExpiredBanner, AuthGate } from '@manacore/shared-auth-ui';
|
||||
import { viewMode, setViewMode } from '$lib/stores/view';
|
||||
import type { ViewMode } from '$lib/stores/view';
|
||||
import { browser } from '$app/environment';
|
||||
|
|
@ -60,34 +60,23 @@
|
|||
theme.setMode(mode);
|
||||
}
|
||||
|
||||
// Client-side auth check
|
||||
$effect(() => {
|
||||
if (authStore.initialized && !authStore.loading && !authStore.user) {
|
||||
goto('/auth/login');
|
||||
}
|
||||
});
|
||||
async function handleAuthReady() {
|
||||
await userSettings.load();
|
||||
|
||||
// Load user settings when authenticated
|
||||
$effect(() => {
|
||||
if (authStore.initialized && authStore.user) {
|
||||
userSettings.load().then(() => {
|
||||
// Redirect to start page if on /app and a custom start page is set
|
||||
const currentPath = window.location.pathname;
|
||||
if (
|
||||
currentPath === '/app' &&
|
||||
userSettings.startPage &&
|
||||
userSettings.startPage !== '/' &&
|
||||
userSettings.startPage !== '/app'
|
||||
) {
|
||||
// Prepend /app if the start page doesn't include it
|
||||
const targetPath = userSettings.startPage.startsWith('/app')
|
||||
? userSettings.startPage
|
||||
: `/app${userSettings.startPage}`;
|
||||
goto(targetPath, { replaceState: true });
|
||||
}
|
||||
});
|
||||
// Redirect to start page if on /app and a custom start page is set
|
||||
const currentPath = window.location.pathname;
|
||||
if (
|
||||
currentPath === '/app' &&
|
||||
userSettings.startPage &&
|
||||
userSettings.startPage !== '/' &&
|
||||
userSettings.startPage !== '/app'
|
||||
) {
|
||||
const targetPath = userSettings.startPage.startsWith('/app')
|
||||
? userSettings.startPage
|
||||
: `/app${userSettings.startPage}`;
|
||||
goto(targetPath, { replaceState: true });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Base navigation items (Mana is in user dropdown via manaHref)
|
||||
const baseNavItems: PillNavItem[] = [
|
||||
|
|
@ -236,16 +225,7 @@
|
|||
|
||||
<svelte:window on:keydown={handleKeyDown} />
|
||||
|
||||
{#if authStore.loading}
|
||||
<div class="flex min-h-screen items-center justify-center">
|
||||
<div class="text-center">
|
||||
<div
|
||||
class="mb-4 inline-block h-12 w-12 animate-spin rounded-full border-4 border-solid border-blue-600 border-r-transparent"
|
||||
></div>
|
||||
<p class="text-gray-600">Loading...</p>
|
||||
</div>
|
||||
</div>
|
||||
{:else if authStore.user}
|
||||
<AuthGate {authStore} {goto} loginHref="/auth/login" onReady={handleAuthReady}>
|
||||
<div class="min-h-screen" style="background-color: hsl(var(--color-background));">
|
||||
<!-- PillNavigation (conditionally visible) -->
|
||||
{#if $isUIVisible}
|
||||
|
|
@ -297,7 +277,7 @@
|
|||
{/if}
|
||||
</div>
|
||||
<SessionExpiredBanner locale={$locale || 'de'} loginHref="/auth/login" />
|
||||
{/if}
|
||||
</AuthGate>
|
||||
|
||||
<style>
|
||||
.main-content {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import { page } from '$app/stores';
|
||||
import { onMount } from 'svelte';
|
||||
import { PillNavigation, QuickInputBar } from '@manacore/shared-ui';
|
||||
import type { PillNavItem, QuickInputItem, CreatePreview } from '@manacore/shared-ui';
|
||||
import { theme } from '$lib/stores/theme';
|
||||
|
|
@ -12,13 +11,10 @@
|
|||
resolvePlantData,
|
||||
formatParsedPlantPreview,
|
||||
} from '$lib/utils/plant-parser';
|
||||
import { SessionExpiredBanner } from '@manacore/shared-auth-ui';
|
||||
import { SessionExpiredBanner, AuthGate } from '@manacore/shared-auth-ui';
|
||||
|
||||
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' },
|
||||
|
|
@ -85,21 +81,9 @@
|
|||
goto(`/plant/${plant.id}`);
|
||||
}
|
||||
}
|
||||
|
||||
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 appReady}
|
||||
<AuthGate {authStore} {goto}>
|
||||
<div class="layout-container">
|
||||
<PillNavigation
|
||||
items={navItems}
|
||||
|
|
@ -137,13 +121,7 @@
|
|||
</main>
|
||||
</div>
|
||||
<SessionExpiredBanner locale="de" loginHref="/login" />
|
||||
{:else}
|
||||
<div class="flex min-h-screen items-center justify-center">
|
||||
<div
|
||||
class="h-12 w-12 animate-spin rounded-full border-4 border-solid border-primary border-r-transparent"
|
||||
></div>
|
||||
</div>
|
||||
{/if}
|
||||
</AuthGate>
|
||||
|
||||
<style>
|
||||
.layout-container {
|
||||
|
|
|
|||
|
|
@ -1,25 +1,20 @@
|
|||
<script lang="ts">
|
||||
import '../app.css';
|
||||
import '$lib/i18n';
|
||||
import { onMount } from 'svelte';
|
||||
import { isLoading as i18nLoading, _ as t } from 'svelte-i18n';
|
||||
import { skillStore } from '$lib/stores/skills.svelte';
|
||||
import { achievementStore } from '$lib/stores/achievements.svelte';
|
||||
import { authStore } from '$lib/stores/auth.svelte';
|
||||
import { MiniOnboardingModal } from '@manacore/shared-app-onboarding';
|
||||
import { skilltreeOnboarding } from '$lib/stores/app-onboarding.svelte';
|
||||
import { SessionExpiredBanner } from '@manacore/shared-auth-ui';
|
||||
import { SessionExpiredBanner, AuthGate } from '@manacore/shared-auth-ui';
|
||||
|
||||
let { children } = $props();
|
||||
|
||||
let loading = $state(true);
|
||||
let appReady = $derived(!loading && !$i18nLoading);
|
||||
|
||||
onMount(async () => {
|
||||
await Promise.all([authStore.initialize(), skillStore.initialize()]);
|
||||
async function handleAuthReady() {
|
||||
await skillStore.initialize();
|
||||
await achievementStore.initialize();
|
||||
loading = false;
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
|
|
@ -27,20 +22,22 @@
|
|||
<meta name="description" content="Track your skills like a game. Level up in real life." />
|
||||
</svelte:head>
|
||||
|
||||
{#if !appReady}
|
||||
<div class="flex min-h-screen items-center justify-center bg-gray-900">
|
||||
<div class="text-center">
|
||||
<div class="mb-4 text-6xl">🌳</div>
|
||||
<div class="text-xl text-gray-300">{$t('app.loading')}</div>
|
||||
<AuthGate {authStore} allowGuest={true} onReady={handleAuthReady}>
|
||||
{#if $i18nLoading}
|
||||
<div class="flex min-h-screen items-center justify-center bg-gray-900">
|
||||
<div class="text-center">
|
||||
<div class="mb-4 text-6xl">🌳</div>
|
||||
<div class="text-xl text-gray-300">Loading...</div>
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="min-h-screen bg-gray-900 text-gray-100">
|
||||
{@render children()}
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="min-h-screen bg-gray-900 text-gray-100">
|
||||
{@render children()}
|
||||
</div>
|
||||
|
||||
{#if skilltreeOnboarding.shouldShow}
|
||||
<MiniOnboardingModal store={skilltreeOnboarding} appName="SkillTree" appEmoji="🌳" />
|
||||
{#if skilltreeOnboarding.shouldShow}
|
||||
<MiniOnboardingModal store={skilltreeOnboarding} appName="SkillTree" appEmoji="🌳" />
|
||||
{/if}
|
||||
<SessionExpiredBanner locale="de" loginHref="/login" />
|
||||
{/if}
|
||||
<SessionExpiredBanner locale="de" loginHref="/login" />
|
||||
{/if}
|
||||
</AuthGate>
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@
|
|||
import { ToastContainer } from '@manacore/shared-ui';
|
||||
import { storageOnboarding } from '$lib/stores/app-onboarding.svelte';
|
||||
import { MiniOnboardingModal } from '@manacore/shared-app-onboarding';
|
||||
import { SessionExpiredBanner } from '@manacore/shared-auth-ui';
|
||||
import { SessionExpiredBanner, AuthGate } from '@manacore/shared-auth-ui';
|
||||
import '../app.css';
|
||||
|
||||
// App switcher items
|
||||
|
|
@ -24,7 +24,6 @@
|
|||
|
||||
let { children } = $props();
|
||||
|
||||
let loading = $state(true);
|
||||
let isCollapsed = $state(false);
|
||||
|
||||
// Use theme store's isDark directly
|
||||
|
|
@ -130,33 +129,24 @@
|
|||
goto('/login');
|
||||
}
|
||||
|
||||
async function handleAuthReady() {
|
||||
// Initialize theme
|
||||
theme.initialize();
|
||||
|
||||
// Load user settings
|
||||
await userSettings.load();
|
||||
|
||||
// Initialize collapsed state from localStorage
|
||||
const savedCollapsed = localStorage.getItem('storage-nav-collapsed');
|
||||
if (savedCollapsed === 'true') {
|
||||
isCollapsed = true;
|
||||
collapsedStore.set(true);
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
// Setup global error handling
|
||||
const cleanupErrorHandler = setupGlobalErrorHandler();
|
||||
|
||||
// Initialize async operations
|
||||
const init = async () => {
|
||||
// Initialize theme
|
||||
theme.initialize();
|
||||
|
||||
// Initialize auth
|
||||
await authStore.initialize();
|
||||
|
||||
// Load user settings
|
||||
await userSettings.load();
|
||||
|
||||
// Initialize collapsed state from localStorage
|
||||
const savedCollapsed = localStorage.getItem('storage-nav-collapsed');
|
||||
if (savedCollapsed === 'true') {
|
||||
isCollapsed = true;
|
||||
collapsedStore.set(true);
|
||||
}
|
||||
|
||||
loading = false;
|
||||
};
|
||||
|
||||
init();
|
||||
|
||||
return cleanupErrorHandler;
|
||||
});
|
||||
</script>
|
||||
|
|
@ -165,71 +155,58 @@
|
|||
|
||||
<ToastContainer />
|
||||
|
||||
{#if loading}
|
||||
<div
|
||||
class="flex min-h-screen items-center justify-center bg-background"
|
||||
role="status"
|
||||
aria-live="polite"
|
||||
aria-busy="true"
|
||||
>
|
||||
<div class="text-center">
|
||||
<div
|
||||
class="mb-4 inline-block h-12 w-12 animate-spin rounded-full border-4 border-solid border-primary border-r-transparent"
|
||||
aria-hidden="true"
|
||||
></div>
|
||||
<p class="text-muted-foreground">Laden...</p>
|
||||
<AuthGate {authStore} {goto} allowGuest={true} onReady={handleAuthReady}>
|
||||
{#if isAuthPage}
|
||||
<!-- Auth pages without navigation -->
|
||||
{@render children()}
|
||||
{:else}
|
||||
<!-- Navigation Layout -->
|
||||
<div class="layout-container">
|
||||
<PillNavigation
|
||||
items={navItems}
|
||||
currentPath={$page.url.pathname}
|
||||
appName="Storage"
|
||||
homeRoute="/files"
|
||||
onToggleTheme={handleToggleTheme}
|
||||
{isDark}
|
||||
{isCollapsed}
|
||||
onCollapsedChange={handleCollapsedChange}
|
||||
showThemeToggle={true}
|
||||
showThemeVariants={true}
|
||||
{themeVariantItems}
|
||||
{currentThemeVariantLabel}
|
||||
themeMode={theme.mode}
|
||||
onThemeModeChange={handleThemeModeChange}
|
||||
showLanguageSwitcher={true}
|
||||
{languageItems}
|
||||
{currentLanguageLabel}
|
||||
showLogout={authStore.isAuthenticated}
|
||||
onLogout={handleLogout}
|
||||
loginHref="/login"
|
||||
primaryColor="#3b82f6"
|
||||
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>
|
||||
|
||||
<!-- Onboarding Modal -->
|
||||
{#if storageOnboarding.shouldShow}
|
||||
<MiniOnboardingModal store={storageOnboarding} appName="Storage" appEmoji="☁️" />
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{:else if isAuthPage}
|
||||
<!-- Auth pages without navigation -->
|
||||
{@render children()}
|
||||
{:else}
|
||||
<!-- Navigation Layout -->
|
||||
<div class="layout-container">
|
||||
<PillNavigation
|
||||
items={navItems}
|
||||
currentPath={$page.url.pathname}
|
||||
appName="Storage"
|
||||
homeRoute="/files"
|
||||
onToggleTheme={handleToggleTheme}
|
||||
{isDark}
|
||||
{isCollapsed}
|
||||
onCollapsedChange={handleCollapsedChange}
|
||||
showThemeToggle={true}
|
||||
showThemeVariants={true}
|
||||
{themeVariantItems}
|
||||
{currentThemeVariantLabel}
|
||||
themeMode={theme.mode}
|
||||
onThemeModeChange={handleThemeModeChange}
|
||||
showLanguageSwitcher={true}
|
||||
{languageItems}
|
||||
{currentLanguageLabel}
|
||||
showLogout={authStore.isAuthenticated}
|
||||
onLogout={handleLogout}
|
||||
loginHref="/login"
|
||||
primaryColor="#3b82f6"
|
||||
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>
|
||||
|
||||
<!-- Onboarding Modal -->
|
||||
{#if storageOnboarding.shouldShow}
|
||||
<MiniOnboardingModal store={storageOnboarding} appName="Storage" appEmoji="☁️" />
|
||||
{/if}
|
||||
</div>
|
||||
<SessionExpiredBanner locale={$locale || 'de'} loginHref="/login" />
|
||||
{/if}
|
||||
<SessionExpiredBanner locale={$locale || 'de'} loginHref="/login" />
|
||||
{/if}
|
||||
</AuthGate>
|
||||
|
||||
<style>
|
||||
.layout-container {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import { page } from '$app/stores';
|
||||
import { onMount } from 'svelte';
|
||||
import { locale } from 'svelte-i18n';
|
||||
import { PillNavigation, QuickInputBar, ImmersiveModeToggle } from '@manacore/shared-ui';
|
||||
import {
|
||||
|
|
@ -40,7 +39,7 @@
|
|||
import { parseTaskInput, resolveTaskIds, formatParsedTaskPreview } from '$lib/utils/task-parser';
|
||||
import { todoOnboarding } from '$lib/stores/app-onboarding.svelte';
|
||||
import { MiniOnboardingModal } from '@manacore/shared-app-onboarding';
|
||||
import { SessionExpiredBanner } from '@manacore/shared-auth-ui';
|
||||
import { SessionExpiredBanner, AuthGate } from '@manacore/shared-auth-ui';
|
||||
import { TodoEvents } from '@manacore/shared-utils/analytics';
|
||||
|
||||
// App switcher items
|
||||
|
|
@ -56,9 +55,6 @@
|
|||
|
||||
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 [];
|
||||
|
|
@ -277,14 +273,7 @@
|
|||
goto('/login');
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
// Initialize auth and redirect if not authenticated
|
||||
await authStore.initialize();
|
||||
if (!authStore.isAuthenticated) {
|
||||
goto('/login');
|
||||
return;
|
||||
}
|
||||
|
||||
async function handleAuthReady() {
|
||||
// Initialize split-panel from URL/localStorage
|
||||
splitPanel.initialize();
|
||||
|
||||
|
|
@ -310,19 +299,12 @@
|
|||
} catch {
|
||||
// localStorage not available
|
||||
}
|
||||
|
||||
// Auth confirmed - allow children to render
|
||||
appReady = true;
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:window onkeydown={handleKeydown} />
|
||||
|
||||
{#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}
|
||||
<AuthGate {authStore} {goto} onReady={handleAuthReady}>
|
||||
<SplitPaneContainer>
|
||||
<div class="layout-container">
|
||||
<a
|
||||
|
|
@ -474,7 +456,7 @@
|
|||
</div>
|
||||
</SplitPaneContainer>
|
||||
<SessionExpiredBanner locale={$locale || 'de'} loginHref="/login" />
|
||||
{/if}
|
||||
</AuthGate>
|
||||
|
||||
<style>
|
||||
.layout-container {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import { page } from '$app/stores';
|
||||
import { onMount } from 'svelte';
|
||||
import { locale, _ } from 'svelte-i18n';
|
||||
import { PillNavigation, QuickInputBar, ImmersiveModeToggle } from '@manacore/shared-ui';
|
||||
import type { PillNavItem, PillDropdownItem, QuickInputItem } from '@manacore/shared-ui';
|
||||
|
|
@ -26,7 +25,7 @@
|
|||
import { getLanguageDropdownItems, getCurrentLanguageLabel } from '@manacore/shared-i18n';
|
||||
import { getPillAppItems } from '@manacore/shared-branding';
|
||||
import { setLocale, supportedLocales } from '$lib/i18n';
|
||||
import { SessionExpiredBanner } from '@manacore/shared-auth-ui';
|
||||
import { SessionExpiredBanner, AuthGate } from '@manacore/shared-auth-ui';
|
||||
import { QUOTES, type Quote } from '@zitare/content';
|
||||
|
||||
// App switcher items
|
||||
|
|
@ -41,9 +40,6 @@
|
|||
|
||||
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);
|
||||
|
||||
|
|
@ -213,10 +209,7 @@
|
|||
zitareSettings.togglePillNav();
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
// Initialize auth state from stored tokens
|
||||
await authStore.initialize();
|
||||
|
||||
async function handleAuthReady() {
|
||||
// Initialize settings
|
||||
zitareSettings.initialize();
|
||||
|
||||
|
|
@ -226,24 +219,12 @@
|
|||
favoritesStore.load();
|
||||
listsStore.loadLists();
|
||||
}
|
||||
|
||||
// Auth confirmed - allow children to render
|
||||
appReady = true;
|
||||
|
||||
// Add keyboard listener
|
||||
window.addEventListener('keydown', handleKeydown);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('keydown', handleKeydown);
|
||||
};
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
{#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}
|
||||
<svelte:window onkeydown={handleKeydown} />
|
||||
|
||||
<AuthGate {authStore} {goto} allowGuest={true} onReady={handleAuthReady}>
|
||||
<div class="layout-container">
|
||||
{#if !zitareSettings.immersiveModeEnabled}
|
||||
<!-- PillNav (shown/hidden via FAB) -->
|
||||
|
|
@ -337,7 +318,7 @@
|
|||
</main>
|
||||
</div>
|
||||
<SessionExpiredBanner locale={$locale || 'de'} loginHref="/login" />
|
||||
{/if}
|
||||
</AuthGate>
|
||||
|
||||
<style>
|
||||
.layout-container {
|
||||
|
|
|
|||
86
packages/shared-auth-ui/src/components/AuthGate.svelte
Normal file
86
packages/shared-auth-ui/src/components/AuthGate.svelte
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
<!--
|
||||
AuthGate - Centralized auth initialization and loading gate.
|
||||
|
||||
Handles:
|
||||
- Auth store initialization
|
||||
- Loading spinner while checking auth
|
||||
- Redirect to login if not authenticated (unless allowGuest)
|
||||
- Calling onReady callback after auth is confirmed
|
||||
- Rendering children only when ready
|
||||
|
||||
Usage:
|
||||
<AuthGate authStore={authStore} onReady={loadAppData}>
|
||||
<AppContent />
|
||||
</AuthGate>
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import type { Snippet } from 'svelte';
|
||||
|
||||
/**
|
||||
* Minimal interface that all app auth stores must satisfy.
|
||||
* Every app's authStore (e.g. `$lib/stores/auth.svelte`) already matches this.
|
||||
*/
|
||||
interface AuthStoreInterface {
|
||||
initialize(): Promise<void>;
|
||||
readonly isAuthenticated: boolean;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
/** The app's auth store instance (must have initialize() and isAuthenticated) */
|
||||
authStore: AuthStoreInterface;
|
||||
/** Path to redirect to when not authenticated (default: '/login') */
|
||||
loginHref?: string;
|
||||
/** If true, render children even when not authenticated (for guest-mode apps) */
|
||||
allowGuest?: boolean;
|
||||
/** Callback invoked after auth is confirmed, before children are rendered.
|
||||
* Use this for loading app-specific data (projects, calendars, etc.) */
|
||||
onReady?: () => void | Promise<void>;
|
||||
/** SvelteKit goto function for client-side navigation. Falls back to window.location. */
|
||||
goto?: (url: string, opts?: Record<string, unknown>) => unknown;
|
||||
/** Content to render when ready */
|
||||
children: Snippet;
|
||||
}
|
||||
|
||||
let {
|
||||
authStore,
|
||||
loginHref = '/login',
|
||||
allowGuest = false,
|
||||
onReady,
|
||||
goto: gotoFn,
|
||||
children,
|
||||
}: Props = $props();
|
||||
|
||||
let ready = $state(false);
|
||||
|
||||
function navigate(url: string) {
|
||||
if (gotoFn) {
|
||||
gotoFn(url);
|
||||
} else if (typeof window !== 'undefined') {
|
||||
window.location.href = url;
|
||||
}
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
await authStore.initialize();
|
||||
|
||||
if (!authStore.isAuthenticated && !allowGuest) {
|
||||
navigate(loginHref);
|
||||
return;
|
||||
}
|
||||
|
||||
if (onReady) {
|
||||
await onReady();
|
||||
}
|
||||
|
||||
ready = true;
|
||||
});
|
||||
</script>
|
||||
|
||||
{#if !ready}
|
||||
<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}
|
||||
{@render children()}
|
||||
{/if}
|
||||
|
|
@ -9,6 +9,7 @@ export { default as AppleSignInButton } from './components/AppleSignInButton.sve
|
|||
export { default as GuestWelcomeModal } from './components/GuestWelcomeModal.svelte';
|
||||
export { default as AuthGateModal } from './components/AuthGateModal.svelte';
|
||||
export { default as SessionExpiredBanner } from './components/SessionExpiredBanner.svelte';
|
||||
export { default as AuthGate } from './components/AuthGate.svelte';
|
||||
|
||||
// Utilities
|
||||
export {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue