mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-18 08:09:40 +02:00
feat(apps): migrate Chat, Questions, Mukke, Context, Photos to local-first
Add IndexedDB data layer to 5 more Tier 2 apps, bringing the total to
17/22 apps with local-first architecture.
For each app:
- Create local-store.ts with typed collections and sync config
- Create guest-seed.ts with onboarding data for guest mode
- Update layout with AuthGate allowGuest={true} + local store init
- Add GuestWelcomeModal for first-visit experience
App-specific changes:
- Chat: Add store init to existing AuthGate, keep session-based guest mode
- Questions: Replace manual onMount auth with AuthGate wrapper
- Mukke: Add allowGuest to existing AuthGate, audio files stay server-side
- Context: Replace manual onMount auth with AuthGate, AI stays server-side
- Photos: Add allowGuest to existing AuthGate, photos stay on mana-media
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
a70359dcce
commit
8d880f1fb6
21 changed files with 827 additions and 174 deletions
|
|
@ -538,6 +538,11 @@ Logged in: App → IndexedDB → UI → SyncEngine → mana-sync (Go) → Postg
|
|||
| NutriPhi | meals, goals, favorites | Done |
|
||||
| Planta | plants, plantPhotos, wateringSchedules, wateringLogs | Done |
|
||||
| Storage | files, folders, tags, fileTags | Done |
|
||||
| Chat | conversations, messages, templates | Done |
|
||||
| Questions | collections, questions, answers | Done |
|
||||
| Mukke | songs, playlists, playlistSongs, projects, markers | Done |
|
||||
| Context | spaces, documents | Done |
|
||||
| Photos | albums, albumItems, favorites, tags, photoTags | Done |
|
||||
|
||||
### Dev Commands (Local-First Stack)
|
||||
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@
|
|||
"@manacore/shared-app-onboarding": "workspace:*",
|
||||
"@manacore/shared-auth": "workspace:*",
|
||||
"@manacore/shared-auth-ui": "workspace:*",
|
||||
"@manacore/local-store": "workspace:*",
|
||||
"@manacore/shared-branding": "workspace:*",
|
||||
"@manacore/shared-error-tracking": "workspace:*",
|
||||
"@manacore/shared-feedback-service": "workspace:*",
|
||||
|
|
|
|||
30
apps/chat/apps/web/src/lib/data/guest-seed.ts
Normal file
30
apps/chat/apps/web/src/lib/data/guest-seed.ts
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
/**
|
||||
* Guest seed data for the Chat app.
|
||||
*
|
||||
* Provides a demo conversation to showcase the chat experience.
|
||||
*/
|
||||
|
||||
import type { LocalConversation, LocalMessage } from './local-store';
|
||||
|
||||
const DEMO_CONVERSATION_ID = 'demo-welcome';
|
||||
|
||||
export const guestConversations: LocalConversation[] = [
|
||||
{
|
||||
id: DEMO_CONVERSATION_ID,
|
||||
title: 'Willkommen bei Chat!',
|
||||
conversationMode: 'free',
|
||||
documentMode: false,
|
||||
isArchived: false,
|
||||
isPinned: true,
|
||||
},
|
||||
];
|
||||
|
||||
export const guestMessages: LocalMessage[] = [
|
||||
{
|
||||
id: 'msg-1',
|
||||
conversationId: DEMO_CONVERSATION_ID,
|
||||
sender: 'assistant',
|
||||
messageText:
|
||||
'Hallo! Ich bin dein KI-Assistent. Du kannst mir Fragen stellen, Texte schreiben lassen oder einfach ein Gespräch führen. Melde dich an, um deine Unterhaltungen zu speichern und zu synchronisieren.',
|
||||
},
|
||||
];
|
||||
71
apps/chat/apps/web/src/lib/data/local-store.ts
Normal file
71
apps/chat/apps/web/src/lib/data/local-store.ts
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
/**
|
||||
* Chat — Local-First Data Layer
|
||||
*
|
||||
* Conversations, messages, and templates stored locally.
|
||||
* LLM streaming and model management remain server-side.
|
||||
*/
|
||||
|
||||
import { createLocalStore, type BaseRecord } from '@manacore/local-store';
|
||||
import { guestConversations, guestMessages } from './guest-seed';
|
||||
|
||||
// ─── Types ──────────────────────────────────────────────────
|
||||
|
||||
export interface LocalConversation extends BaseRecord {
|
||||
title?: string | null;
|
||||
modelId?: string | null;
|
||||
templateId?: string | null;
|
||||
spaceId?: string | null;
|
||||
conversationMode: 'free' | 'guided' | 'template';
|
||||
documentMode: boolean;
|
||||
isArchived: boolean;
|
||||
isPinned: boolean;
|
||||
}
|
||||
|
||||
export interface LocalMessage extends BaseRecord {
|
||||
conversationId: string;
|
||||
sender: 'user' | 'assistant' | 'system';
|
||||
messageText: string;
|
||||
}
|
||||
|
||||
export interface LocalTemplate extends BaseRecord {
|
||||
name: string;
|
||||
description: string;
|
||||
systemPrompt: string;
|
||||
initialQuestion?: string | null;
|
||||
modelId?: string | null;
|
||||
color: string;
|
||||
isDefault: boolean;
|
||||
documentMode: boolean;
|
||||
}
|
||||
|
||||
// ─── Store ──────────────────────────────────────────────────
|
||||
|
||||
const SYNC_SERVER_URL = import.meta.env.PUBLIC_SYNC_SERVER_URL || 'http://localhost:3050';
|
||||
|
||||
export const chatStore = createLocalStore({
|
||||
appId: 'chat',
|
||||
collections: [
|
||||
{
|
||||
name: 'conversations',
|
||||
indexes: ['isArchived', 'isPinned', 'spaceId', 'templateId'],
|
||||
guestSeed: guestConversations,
|
||||
},
|
||||
{
|
||||
name: 'messages',
|
||||
indexes: ['conversationId', 'sender', '[conversationId+sender]'],
|
||||
guestSeed: guestMessages,
|
||||
},
|
||||
{
|
||||
name: 'templates',
|
||||
indexes: ['isDefault'],
|
||||
},
|
||||
],
|
||||
sync: {
|
||||
serverUrl: SYNC_SERVER_URL,
|
||||
},
|
||||
});
|
||||
|
||||
// Typed collection accessors
|
||||
export const conversationCollection = chatStore.collection<LocalConversation>('conversations');
|
||||
export const messageCollection = chatStore.collection<LocalMessage>('messages');
|
||||
export const templateCollection = chatStore.collection<LocalTemplate>('templates');
|
||||
|
|
@ -24,7 +24,9 @@
|
|||
import type { LayoutData } from './$types';
|
||||
import { chatOnboarding } from '$lib/stores/app-onboarding.svelte';
|
||||
import { MiniOnboardingModal } from '@manacore/shared-app-onboarding';
|
||||
import { SessionExpiredBanner, AuthGate } from '@manacore/shared-auth-ui';
|
||||
import { SessionExpiredBanner, AuthGate, GuestWelcomeModal } from '@manacore/shared-auth-ui';
|
||||
import { shouldShowGuestWelcome } from '@manacore/shared-auth-ui';
|
||||
import { chatStore } from '$lib/data/local-store';
|
||||
|
||||
// App switcher items
|
||||
const appItems = getPillAppItems('chat');
|
||||
|
|
@ -156,7 +158,18 @@
|
|||
goto('/login');
|
||||
}
|
||||
|
||||
let showGuestWelcome = $state(false);
|
||||
|
||||
async function handleAuthReady() {
|
||||
// Initialize local-first database
|
||||
await chatStore.initialize();
|
||||
if (authStore.isAuthenticated) {
|
||||
chatStore.startSync(() => authStore.getValidToken());
|
||||
}
|
||||
if (!authStore.isAuthenticated && shouldShowGuestWelcome('chat')) {
|
||||
showGuestWelcome = true;
|
||||
}
|
||||
|
||||
// Initialize theme
|
||||
theme.initialize();
|
||||
|
||||
|
|
@ -167,6 +180,8 @@
|
|||
collapsedStore.set(true);
|
||||
}
|
||||
|
||||
if (!authStore.isAuthenticated) return;
|
||||
|
||||
// Load user settings and tags
|
||||
await userSettings.load();
|
||||
await tagStore.fetchTags();
|
||||
|
|
@ -186,7 +201,7 @@
|
|||
|
||||
<svelte:window onkeydown={handleKeydown} />
|
||||
|
||||
<AuthGate {authStore} {goto} onReady={handleAuthReady}>
|
||||
<AuthGate {authStore} {goto} allowGuest={true} onReady={handleAuthReady}>
|
||||
<!-- Navigation Layout -->
|
||||
<div class="layout-container">
|
||||
<!-- Floating Pill Navigation -->
|
||||
|
|
@ -256,7 +271,18 @@
|
|||
<MiniOnboardingModal store={chatOnboarding} appName="Chat" appEmoji="💬" />
|
||||
{/if}
|
||||
|
||||
<SessionExpiredBanner locale={$locale || 'de'} loginHref="/login" />
|
||||
{#if authStore.isAuthenticated}
|
||||
<SessionExpiredBanner locale={$locale || 'de'} loginHref="/login" />
|
||||
{/if}
|
||||
|
||||
<GuestWelcomeModal
|
||||
appId="chat"
|
||||
visible={showGuestWelcome}
|
||||
onClose={() => (showGuestWelcome = false)}
|
||||
onLogin={() => goto('/login')}
|
||||
onRegister={() => goto('/register')}
|
||||
locale={($locale || 'de') === 'de' ? 'de' : 'en'}
|
||||
/>
|
||||
</AuthGate>
|
||||
|
||||
<style>
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@
|
|||
"@manacore/shared-app-onboarding": "workspace:*",
|
||||
"@manacore/shared-auth": "workspace:*",
|
||||
"@manacore/shared-auth-ui": "workspace:*",
|
||||
"@manacore/local-store": "workspace:*",
|
||||
"@manacore/shared-branding": "workspace:*",
|
||||
"@manacore/shared-error-tracking": "workspace:*",
|
||||
"@manacore/shared-feedback-service": "workspace:*",
|
||||
|
|
|
|||
43
apps/context/apps/web/src/lib/data/guest-seed.ts
Normal file
43
apps/context/apps/web/src/lib/data/guest-seed.ts
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
/**
|
||||
* Guest seed data for the Context app.
|
||||
*
|
||||
* Provides a demo space with sample documents.
|
||||
*/
|
||||
|
||||
import type { LocalSpace, LocalDocument } from './local-store';
|
||||
|
||||
const DEMO_SPACE_ID = 'demo-workspace';
|
||||
|
||||
export const guestSpaces: LocalSpace[] = [
|
||||
{
|
||||
id: DEMO_SPACE_ID,
|
||||
name: 'Mein Workspace',
|
||||
description: 'Beispiel-Space zum Kennenlernen von Context.',
|
||||
pinned: true,
|
||||
prefix: 'W',
|
||||
},
|
||||
];
|
||||
|
||||
export const guestDocuments: LocalDocument[] = [
|
||||
{
|
||||
id: 'doc-welcome',
|
||||
spaceId: DEMO_SPACE_ID,
|
||||
title: 'Willkommen bei Context',
|
||||
content:
|
||||
'Context ist dein KI-gestütztes Dokumenten-Management. Erstelle Texte, sammle Kontexte und nutze KI-Prompts.\n\nMelde dich an, um deine Dokumente zu synchronisieren.',
|
||||
type: 'text',
|
||||
shortId: 'WD1',
|
||||
pinned: true,
|
||||
metadata: { tags: ['einführung'], wordCount: 22 },
|
||||
},
|
||||
{
|
||||
id: 'doc-prompt',
|
||||
spaceId: DEMO_SPACE_ID,
|
||||
title: 'Beispiel-Prompt',
|
||||
content: 'Fasse den folgenden Text in 3 Stichpunkten zusammen:\n\n{text}',
|
||||
type: 'prompt',
|
||||
shortId: 'WP1',
|
||||
pinned: false,
|
||||
metadata: { tags: ['vorlage'] },
|
||||
},
|
||||
];
|
||||
59
apps/context/apps/web/src/lib/data/local-store.ts
Normal file
59
apps/context/apps/web/src/lib/data/local-store.ts
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
/**
|
||||
* Context — Local-First Data Layer
|
||||
*
|
||||
* Spaces and documents stored locally.
|
||||
* AI generation and token management remain server-side.
|
||||
*/
|
||||
|
||||
import { createLocalStore, type BaseRecord } from '@manacore/local-store';
|
||||
import { guestSpaces, guestDocuments } from './guest-seed';
|
||||
|
||||
// ─── Types ──────────────────────────────────────────────────
|
||||
|
||||
export interface LocalSpace extends BaseRecord {
|
||||
name: string;
|
||||
description?: string | null;
|
||||
settings?: Record<string, unknown> | null;
|
||||
pinned: boolean;
|
||||
prefix: string;
|
||||
}
|
||||
|
||||
export interface LocalDocument extends BaseRecord {
|
||||
spaceId?: string | null;
|
||||
title: string;
|
||||
content: string;
|
||||
type: 'text' | 'context' | 'prompt';
|
||||
shortId?: string | null;
|
||||
pinned: boolean;
|
||||
metadata?: {
|
||||
tags?: string[];
|
||||
wordCount?: number;
|
||||
} | null;
|
||||
}
|
||||
|
||||
// ─── Store ──────────────────────────────────────────────────
|
||||
|
||||
const SYNC_SERVER_URL = import.meta.env.PUBLIC_SYNC_SERVER_URL || 'http://localhost:3050';
|
||||
|
||||
export const contextStore = createLocalStore({
|
||||
appId: 'context',
|
||||
collections: [
|
||||
{
|
||||
name: 'spaces',
|
||||
indexes: ['pinned', 'prefix'],
|
||||
guestSeed: guestSpaces,
|
||||
},
|
||||
{
|
||||
name: 'documents',
|
||||
indexes: ['spaceId', 'type', 'pinned', 'title', '[spaceId+type]'],
|
||||
guestSeed: guestDocuments,
|
||||
},
|
||||
],
|
||||
sync: {
|
||||
serverUrl: SYNC_SERVER_URL,
|
||||
},
|
||||
});
|
||||
|
||||
// Typed collection accessors
|
||||
export const spaceCollection = contextStore.collection<LocalSpace>('spaces');
|
||||
export const documentCollection = contextStore.collection<LocalDocument>('documents');
|
||||
|
|
@ -28,6 +28,9 @@
|
|||
import { setLocale, supportedLocales } from '$lib/i18n';
|
||||
import { contextOnboarding } from '$lib/stores/app-onboarding.svelte';
|
||||
import { MiniOnboardingModal } from '@manacore/shared-app-onboarding';
|
||||
import { AuthGate, GuestWelcomeModal } from '@manacore/shared-auth-ui';
|
||||
import { shouldShowGuestWelcome } from '@manacore/shared-auth-ui';
|
||||
import { contextStore } from '$lib/data/local-store';
|
||||
import { tagStore } from '$lib/stores/tags.svelte';
|
||||
|
||||
const appItems = getPillAppItems('context');
|
||||
|
|
@ -212,11 +215,15 @@
|
|||
goto('/login');
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
await authStore.initialize();
|
||||
if (!authStore.isAuthenticated) {
|
||||
goto('/login');
|
||||
return;
|
||||
let showGuestWelcome = $state(false);
|
||||
|
||||
async function handleAuthReady() {
|
||||
await contextStore.initialize();
|
||||
if (authStore.isAuthenticated) {
|
||||
contextStore.startSync(() => authStore.getValidToken());
|
||||
}
|
||||
if (!authStore.isAuthenticated && shouldShowGuestWelcome('context')) {
|
||||
showGuestWelcome = true;
|
||||
}
|
||||
|
||||
const savedCollapsed = localStorage.getItem('context-nav-collapsed');
|
||||
|
|
@ -225,90 +232,99 @@
|
|||
collapsedStore.set(true);
|
||||
}
|
||||
|
||||
await userSettings.load();
|
||||
|
||||
// Load tags
|
||||
await tagStore.fetchTags();
|
||||
|
||||
// Pre-load data for CommandBar search
|
||||
await Promise.all([spacesStore.load(), documentsStore.load()]);
|
||||
});
|
||||
if (authStore.isAuthenticated) {
|
||||
await userSettings.load();
|
||||
await tagStore.fetchTags();
|
||||
await Promise.all([spacesStore.load(), documentsStore.load()]);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:window onkeydown={handleKeydown} />
|
||||
|
||||
<div class="layout-container">
|
||||
<PillNavigation
|
||||
items={navItems}
|
||||
currentPath={$page.url.pathname}
|
||||
appName="Context"
|
||||
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="#0ea5e9"
|
||||
showAppSwitcher={true}
|
||||
{appItems}
|
||||
{userEmail}
|
||||
settingsHref="/settings"
|
||||
manaHref="/mana"
|
||||
profileHref="/profile"
|
||||
themesHref="/themes"
|
||||
helpHref="/help"
|
||||
allAppsHref="/apps"
|
||||
/>
|
||||
|
||||
<!-- TagStrip (above PillNav, toggled via Tags pill) -->
|
||||
{#if isTagStripVisible}
|
||||
<TagStrip
|
||||
tags={tagStore.tags.map((t) => ({
|
||||
id: t.id,
|
||||
name: t.name,
|
||||
color: t.color || '#3b82f6',
|
||||
}))}
|
||||
selectedIds={[]}
|
||||
onToggle={() => {}}
|
||||
onClear={() => {}}
|
||||
managementHref="/tags"
|
||||
loading={tagStore.loading}
|
||||
<AuthGate {authStore} {goto} allowGuest={true} onReady={handleAuthReady}>
|
||||
<div class="layout-container">
|
||||
<PillNavigation
|
||||
items={navItems}
|
||||
currentPath={$page.url.pathname}
|
||||
appName="Context"
|
||||
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="#0ea5e9"
|
||||
showAppSwitcher={true}
|
||||
{appItems}
|
||||
{userEmail}
|
||||
settingsHref="/settings"
|
||||
manaHref="/mana"
|
||||
profileHref="/profile"
|
||||
themesHref="/themes"
|
||||
helpHref="/help"
|
||||
allAppsHref="/apps"
|
||||
/>
|
||||
|
||||
<!-- TagStrip (above PillNav, toggled via Tags pill) -->
|
||||
{#if isTagStripVisible}
|
||||
<TagStrip
|
||||
tags={tagStore.tags.map((t) => ({
|
||||
id: t.id,
|
||||
name: t.name,
|
||||
color: t.color || '#3b82f6',
|
||||
}))}
|
||||
selectedIds={[]}
|
||||
onToggle={() => {}}
|
||||
onClear={() => {}}
|
||||
managementHref="/tags"
|
||||
loading={tagStore.loading}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<main class="main-content bg-background">
|
||||
<div class="content-wrapper">
|
||||
{@render children()}
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<CommandBar
|
||||
bind:open={commandBarOpen}
|
||||
onClose={() => (commandBarOpen = false)}
|
||||
onSearch={handleCommandBarSearch}
|
||||
onSelect={handleCommandBarSelect}
|
||||
quickActions={commandBarQuickActions}
|
||||
placeholder="Schnellzugriff..."
|
||||
emptyText="Keine Ergebnisse"
|
||||
searchingText="Suche..."
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Onboarding Modal -->
|
||||
{#if contextOnboarding.shouldShow}
|
||||
<MiniOnboardingModal store={contextOnboarding} appName="Context" appEmoji="📄" />
|
||||
{/if}
|
||||
|
||||
<main class="main-content bg-background">
|
||||
<div class="content-wrapper">
|
||||
{@render children()}
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<CommandBar
|
||||
bind:open={commandBarOpen}
|
||||
onClose={() => (commandBarOpen = false)}
|
||||
onSearch={handleCommandBarSearch}
|
||||
onSelect={handleCommandBarSelect}
|
||||
quickActions={commandBarQuickActions}
|
||||
placeholder="Schnellzugriff..."
|
||||
emptyText="Keine Ergebnisse"
|
||||
searchingText="Suche..."
|
||||
<GuestWelcomeModal
|
||||
appId="context"
|
||||
visible={showGuestWelcome}
|
||||
onClose={() => (showGuestWelcome = false)}
|
||||
onLogin={() => goto('/login')}
|
||||
onRegister={() => goto('/register')}
|
||||
locale={($locale || 'de') === 'de' ? 'de' : 'en'}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Onboarding Modal -->
|
||||
{#if contextOnboarding.shouldShow}
|
||||
<MiniOnboardingModal store={contextOnboarding} appName="Context" appEmoji="📄" />
|
||||
{/if}
|
||||
</AuthGate>
|
||||
|
||||
<style>
|
||||
.layout-container {
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@
|
|||
"@manacore/shared-api-client": "workspace:*",
|
||||
"@manacore/shared-auth": "workspace:*",
|
||||
"@manacore/shared-auth-ui": "workspace:*",
|
||||
"@manacore/local-store": "workspace:*",
|
||||
"@manacore/shared-branding": "workspace:*",
|
||||
"@manacore/shared-error-tracking": "workspace:*",
|
||||
"@manacore/shared-feedback-service": "workspace:^",
|
||||
|
|
|
|||
15
apps/mukke/apps/web/src/lib/data/guest-seed.ts
Normal file
15
apps/mukke/apps/web/src/lib/data/guest-seed.ts
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
/**
|
||||
* Guest seed data for the Mukke app.
|
||||
*
|
||||
* Provides a demo playlist. Songs require upload, so no audio seeds.
|
||||
*/
|
||||
|
||||
import type { LocalPlaylist } from './local-store';
|
||||
|
||||
export const guestPlaylists: LocalPlaylist[] = [
|
||||
{
|
||||
id: 'playlist-favorites',
|
||||
name: 'Meine Favoriten',
|
||||
description: 'Lade Songs hoch und füge sie zu dieser Playlist hinzu.',
|
||||
},
|
||||
];
|
||||
98
apps/mukke/apps/web/src/lib/data/local-store.ts
Normal file
98
apps/mukke/apps/web/src/lib/data/local-store.ts
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
/**
|
||||
* Mukke — Local-First Data Layer
|
||||
*
|
||||
* Song metadata, playlists, projects, and markers stored locally.
|
||||
* Audio file upload/streaming remains server-side.
|
||||
*/
|
||||
|
||||
import { createLocalStore, type BaseRecord } from '@manacore/local-store';
|
||||
import { guestPlaylists } from './guest-seed';
|
||||
|
||||
// ─── Types ──────────────────────────────────────────────────
|
||||
|
||||
export interface LocalSong extends BaseRecord {
|
||||
title: string;
|
||||
artist?: string | null;
|
||||
album?: string | null;
|
||||
albumArtist?: string | null;
|
||||
genre?: string | null;
|
||||
trackNumber?: number | null;
|
||||
year?: number | null;
|
||||
duration?: number | null;
|
||||
storagePath: string;
|
||||
coverArtPath?: string | null;
|
||||
fileSize?: number | null;
|
||||
bpm?: number | null;
|
||||
favorite: boolean;
|
||||
playCount: number;
|
||||
lastPlayedAt?: string | null;
|
||||
}
|
||||
|
||||
export interface LocalPlaylist extends BaseRecord {
|
||||
name: string;
|
||||
description?: string | null;
|
||||
coverArtPath?: string | null;
|
||||
}
|
||||
|
||||
export interface LocalPlaylistSong extends BaseRecord {
|
||||
playlistId: string;
|
||||
songId: string;
|
||||
sortOrder: number;
|
||||
}
|
||||
|
||||
export interface LocalProject extends BaseRecord {
|
||||
title: string;
|
||||
description?: string | null;
|
||||
songId?: string | null;
|
||||
}
|
||||
|
||||
export interface LocalMarker extends BaseRecord {
|
||||
beatId: string;
|
||||
type: 'verse' | 'hook' | 'bridge' | 'intro' | 'outro' | 'drop' | 'breakdown' | 'custom';
|
||||
label?: string | null;
|
||||
startTime: number;
|
||||
endTime?: number | null;
|
||||
color?: string | null;
|
||||
sortOrder: number;
|
||||
}
|
||||
|
||||
// ─── Store ──────────────────────────────────────────────────
|
||||
|
||||
const SYNC_SERVER_URL = import.meta.env.PUBLIC_SYNC_SERVER_URL || 'http://localhost:3050';
|
||||
|
||||
export const mukkeStore = createLocalStore({
|
||||
appId: 'mukke',
|
||||
collections: [
|
||||
{
|
||||
name: 'songs',
|
||||
indexes: ['artist', 'album', 'genre', 'favorite', 'title'],
|
||||
},
|
||||
{
|
||||
name: 'playlists',
|
||||
indexes: ['name'],
|
||||
guestSeed: guestPlaylists,
|
||||
},
|
||||
{
|
||||
name: 'playlistSongs',
|
||||
indexes: ['playlistId', 'songId', 'sortOrder', '[playlistId+sortOrder]'],
|
||||
},
|
||||
{
|
||||
name: 'projects',
|
||||
indexes: ['title', 'songId'],
|
||||
},
|
||||
{
|
||||
name: 'markers',
|
||||
indexes: ['beatId', 'type', 'sortOrder'],
|
||||
},
|
||||
],
|
||||
sync: {
|
||||
serverUrl: SYNC_SERVER_URL,
|
||||
},
|
||||
});
|
||||
|
||||
// Typed collection accessors
|
||||
export const songCollection = mukkeStore.collection<LocalSong>('songs');
|
||||
export const playlistCollection = mukkeStore.collection<LocalPlaylist>('playlists');
|
||||
export const playlistSongCollection = mukkeStore.collection<LocalPlaylistSong>('playlistSongs');
|
||||
export const projectCollection = mukkeStore.collection<LocalProject>('projects');
|
||||
export const markerCollection = mukkeStore.collection<LocalMarker>('markers');
|
||||
|
|
@ -26,7 +26,9 @@
|
|||
import { playlistStore } from '$lib/stores/playlist.svelte';
|
||||
import { projectStore } from '$lib/stores/project.svelte';
|
||||
import { parseSongInput, formatParsedSongPreview } from '$lib/utils/song-parser';
|
||||
import { SessionExpiredBanner, AuthGate } from '@manacore/shared-auth-ui';
|
||||
import { SessionExpiredBanner, AuthGate, GuestWelcomeModal } from '@manacore/shared-auth-ui';
|
||||
import { shouldShowGuestWelcome } from '@manacore/shared-auth-ui';
|
||||
import { mukkeStore } from '$lib/data/local-store';
|
||||
import { tagStore } from '$lib/stores/tags.svelte';
|
||||
import MiniPlayer from '$lib/components/MiniPlayer.svelte';
|
||||
import FullPlayer from '$lib/components/FullPlayer.svelte';
|
||||
|
|
@ -180,15 +182,26 @@
|
|||
goto('/projects');
|
||||
}
|
||||
|
||||
let showGuestWelcome = $state(false);
|
||||
|
||||
async function handleAuthReady() {
|
||||
await mukkeStore.initialize();
|
||||
if (authStore.isAuthenticated) {
|
||||
mukkeStore.startSync(() => authStore.getValidToken());
|
||||
}
|
||||
if (!authStore.isAuthenticated && shouldShowGuestWelcome('mukke')) {
|
||||
showGuestWelcome = true;
|
||||
}
|
||||
splitPanel.initialize();
|
||||
await tagStore.fetchTags();
|
||||
if (authStore.isAuthenticated) {
|
||||
await tagStore.fetchTags();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:window onkeydown={handleKeydown} />
|
||||
|
||||
<AuthGate {authStore} {goto} onReady={handleAuthReady}>
|
||||
<AuthGate {authStore} {goto} allowGuest={true} onReady={handleAuthReady}>
|
||||
<SplitPaneContainer>
|
||||
<div class="layout-container">
|
||||
<a
|
||||
|
|
@ -272,7 +285,18 @@
|
|||
<DevBuildBadge commitHash={__BUILD_HASH__} buildTime={__BUILD_TIME__} />
|
||||
</div>
|
||||
</SplitPaneContainer>
|
||||
<SessionExpiredBanner locale="de" loginHref="/login" />
|
||||
{#if authStore.isAuthenticated}
|
||||
<SessionExpiredBanner locale="de" loginHref="/login" />
|
||||
{/if}
|
||||
|
||||
<GuestWelcomeModal
|
||||
appId="mukke"
|
||||
visible={showGuestWelcome}
|
||||
onClose={() => (showGuestWelcome = false)}
|
||||
onLogin={() => goto('/login')}
|
||||
onRegister={() => goto('/register')}
|
||||
locale="de"
|
||||
/>
|
||||
</AuthGate>
|
||||
|
||||
<style>
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@
|
|||
"@manacore/shared-api-client": "workspace:*",
|
||||
"@manacore/shared-auth": "workspace:*",
|
||||
"@manacore/shared-auth-ui": "workspace:*",
|
||||
"@manacore/local-store": "workspace:*",
|
||||
"@manacore/shared-branding": "workspace:*",
|
||||
"@manacore/shared-error-tracking": "workspace:*",
|
||||
"@manacore/shared-feedback-service": "workspace:*",
|
||||
|
|
|
|||
34
apps/photos/apps/web/src/lib/data/guest-seed.ts
Normal file
34
apps/photos/apps/web/src/lib/data/guest-seed.ts
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
/**
|
||||
* Guest seed data for the Photos app.
|
||||
*
|
||||
* Provides demo albums and tags. Actual photos require mana-media.
|
||||
*/
|
||||
|
||||
import type { LocalAlbum, LocalTag } from './local-store';
|
||||
|
||||
export const guestAlbums: LocalAlbum[] = [
|
||||
{
|
||||
id: 'album-favorites',
|
||||
name: 'Favoriten',
|
||||
description: 'Deine Lieblingsfotos an einem Ort.',
|
||||
isAutoGenerated: false,
|
||||
},
|
||||
];
|
||||
|
||||
export const guestTags: LocalTag[] = [
|
||||
{
|
||||
id: 'tag-nature',
|
||||
name: 'Natur',
|
||||
color: '#22c55e',
|
||||
},
|
||||
{
|
||||
id: 'tag-people',
|
||||
name: 'Menschen',
|
||||
color: '#3b82f6',
|
||||
},
|
||||
{
|
||||
id: 'tag-travel',
|
||||
name: 'Reisen',
|
||||
color: '#f59e0b',
|
||||
},
|
||||
];
|
||||
82
apps/photos/apps/web/src/lib/data/local-store.ts
Normal file
82
apps/photos/apps/web/src/lib/data/local-store.ts
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
/**
|
||||
* Photos — Local-First Data Layer
|
||||
*
|
||||
* Albums, favorites, and tags stored locally.
|
||||
* Photo files remain on mana-media (server-side).
|
||||
*/
|
||||
|
||||
import { createLocalStore, type BaseRecord } from '@manacore/local-store';
|
||||
import { guestAlbums, guestTags } from './guest-seed';
|
||||
|
||||
// ─── Types ──────────────────────────────────────────────────
|
||||
|
||||
export interface LocalAlbum extends BaseRecord {
|
||||
name: string;
|
||||
description?: string | null;
|
||||
coverMediaId?: string | null;
|
||||
isAutoGenerated: boolean;
|
||||
autoGenerateType?: 'date' | 'location' | 'camera' | null;
|
||||
autoGenerateValue?: string | null;
|
||||
}
|
||||
|
||||
export interface LocalAlbumItem extends BaseRecord {
|
||||
albumId: string;
|
||||
mediaId: string;
|
||||
sortOrder: number;
|
||||
}
|
||||
|
||||
export interface LocalFavorite extends BaseRecord {
|
||||
mediaId: string;
|
||||
}
|
||||
|
||||
export interface LocalTag extends BaseRecord {
|
||||
name: string;
|
||||
color?: string | null;
|
||||
}
|
||||
|
||||
export interface LocalPhotoTag extends BaseRecord {
|
||||
mediaId: string;
|
||||
tagId: string;
|
||||
}
|
||||
|
||||
// ─── Store ──────────────────────────────────────────────────
|
||||
|
||||
const SYNC_SERVER_URL = import.meta.env.PUBLIC_SYNC_SERVER_URL || 'http://localhost:3050';
|
||||
|
||||
export const photosStore = createLocalStore({
|
||||
appId: 'photos',
|
||||
collections: [
|
||||
{
|
||||
name: 'albums',
|
||||
indexes: ['isAutoGenerated', 'name'],
|
||||
guestSeed: guestAlbums,
|
||||
},
|
||||
{
|
||||
name: 'albumItems',
|
||||
indexes: ['albumId', 'mediaId', 'sortOrder', '[albumId+sortOrder]'],
|
||||
},
|
||||
{
|
||||
name: 'favorites',
|
||||
indexes: ['mediaId'],
|
||||
},
|
||||
{
|
||||
name: 'tags',
|
||||
indexes: ['name'],
|
||||
guestSeed: guestTags,
|
||||
},
|
||||
{
|
||||
name: 'photoTags',
|
||||
indexes: ['mediaId', 'tagId', '[mediaId+tagId]'],
|
||||
},
|
||||
],
|
||||
sync: {
|
||||
serverUrl: SYNC_SERVER_URL,
|
||||
},
|
||||
});
|
||||
|
||||
// Typed collection accessors
|
||||
export const albumCollection = photosStore.collection<LocalAlbum>('albums');
|
||||
export const albumItemCollection = photosStore.collection<LocalAlbumItem>('albumItems');
|
||||
export const favoriteCollection = photosStore.collection<LocalFavorite>('favorites');
|
||||
export const tagCollection = photosStore.collection<LocalTag>('tags');
|
||||
export const photoTagCollection = photosStore.collection<LocalPhotoTag>('photoTags');
|
||||
|
|
@ -11,7 +11,9 @@
|
|||
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, AuthGate } from '@manacore/shared-auth-ui';
|
||||
import { SessionExpiredBanner, AuthGate, GuestWelcomeModal } from '@manacore/shared-auth-ui';
|
||||
import { shouldShowGuestWelcome } from '@manacore/shared-auth-ui';
|
||||
import { photosStore } from '$lib/data/local-store';
|
||||
|
||||
let { children } = $props();
|
||||
|
||||
|
|
@ -106,12 +108,21 @@
|
|||
}
|
||||
}
|
||||
|
||||
let showGuestWelcome = $state(false);
|
||||
|
||||
async function handleAuthReady() {
|
||||
await Promise.all([photoStore.loadStats(), albumStore.loadAlbums(), tagStore.loadTags()]);
|
||||
await photosStore.initialize();
|
||||
if (authStore.isAuthenticated) {
|
||||
photosStore.startSync(() => authStore.getValidToken());
|
||||
await Promise.all([photoStore.loadStats(), albumStore.loadAlbums(), tagStore.loadTags()]);
|
||||
}
|
||||
if (!authStore.isAuthenticated && shouldShowGuestWelcome('photos')) {
|
||||
showGuestWelcome = true;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<AuthGate {authStore} {goto} onReady={handleAuthReady}>
|
||||
<AuthGate {authStore} {goto} allowGuest={true} onReady={handleAuthReady}>
|
||||
<div class="layout-container">
|
||||
<PillNavigation
|
||||
items={navItems}
|
||||
|
|
@ -171,7 +182,18 @@
|
|||
</div>
|
||||
</main>
|
||||
</div>
|
||||
<SessionExpiredBanner locale={$locale || 'de'} loginHref="/login" />
|
||||
{#if authStore.isAuthenticated}
|
||||
<SessionExpiredBanner locale={$locale || 'de'} loginHref="/login" />
|
||||
{/if}
|
||||
|
||||
<GuestWelcomeModal
|
||||
appId="photos"
|
||||
visible={showGuestWelcome}
|
||||
onClose={() => (showGuestWelcome = false)}
|
||||
onLogin={() => goto('/login')}
|
||||
onRegister={() => goto('/register')}
|
||||
locale={($locale || 'de') === 'de' ? 'de' : 'en'}
|
||||
/>
|
||||
</AuthGate>
|
||||
|
||||
<style>
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@
|
|||
"@manacore/shared-types": "workspace:*",
|
||||
"@manacore/shared-utils": "workspace:*",
|
||||
"@manacore/shared-auth-ui": "workspace:*",
|
||||
"@manacore/local-store": "workspace:*",
|
||||
"@manacore/shared-branding": "workspace:*",
|
||||
"@manacore/shared-error-tracking": "workspace:*",
|
||||
"@manacore/shared-i18n": "workspace:*",
|
||||
|
|
|
|||
43
apps/questions/apps/web/src/lib/data/guest-seed.ts
Normal file
43
apps/questions/apps/web/src/lib/data/guest-seed.ts
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
/**
|
||||
* Guest seed data for the Questions app.
|
||||
*
|
||||
* Provides demo collections and questions for the onboarding experience.
|
||||
*/
|
||||
|
||||
import type { LocalCollection, LocalQuestion } from './local-store';
|
||||
|
||||
const DEMO_COLLECTION_ID = 'demo-research';
|
||||
|
||||
export const guestCollections: LocalCollection[] = [
|
||||
{
|
||||
id: DEMO_COLLECTION_ID,
|
||||
name: 'Erste Recherche',
|
||||
description: 'Beispiel-Sammlung zum Kennenlernen.',
|
||||
color: '#6366f1',
|
||||
icon: 'search',
|
||||
isDefault: true,
|
||||
sortOrder: 0,
|
||||
},
|
||||
];
|
||||
|
||||
export const guestQuestions: LocalQuestion[] = [
|
||||
{
|
||||
id: 'q-1',
|
||||
collectionId: DEMO_COLLECTION_ID,
|
||||
title: 'Was ist Local-First Software?',
|
||||
description: 'Wie funktioniert der Ansatz und welche Vorteile hat er?',
|
||||
status: 'open',
|
||||
priority: 'normal',
|
||||
tags: ['tech', 'architektur'],
|
||||
researchDepth: 'standard',
|
||||
},
|
||||
{
|
||||
id: 'q-2',
|
||||
collectionId: DEMO_COLLECTION_ID,
|
||||
title: 'Welche Datenbanken eignen sich für Offline-First Apps?',
|
||||
status: 'open',
|
||||
priority: 'normal',
|
||||
tags: ['tech', 'datenbank'],
|
||||
researchDepth: 'quick',
|
||||
},
|
||||
];
|
||||
71
apps/questions/apps/web/src/lib/data/local-store.ts
Normal file
71
apps/questions/apps/web/src/lib/data/local-store.ts
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
/**
|
||||
* Questions — Local-First Data Layer
|
||||
*
|
||||
* Questions, collections, and answers stored locally.
|
||||
* Research (via mana-search) remains server-side.
|
||||
*/
|
||||
|
||||
import { createLocalStore, type BaseRecord } from '@manacore/local-store';
|
||||
import { guestCollections, guestQuestions } from './guest-seed';
|
||||
|
||||
// ─── Types ──────────────────────────────────────────────────
|
||||
|
||||
export interface LocalCollection extends BaseRecord {
|
||||
name: string;
|
||||
description?: string | null;
|
||||
color: string;
|
||||
icon: string;
|
||||
isDefault: boolean;
|
||||
sortOrder: number;
|
||||
}
|
||||
|
||||
export interface LocalQuestion extends BaseRecord {
|
||||
collectionId?: string | null;
|
||||
title: string;
|
||||
description?: string | null;
|
||||
status: 'open' | 'researching' | 'answered' | 'archived';
|
||||
priority: 'low' | 'normal' | 'high' | 'urgent';
|
||||
tags: string[];
|
||||
researchDepth: 'quick' | 'standard' | 'deep';
|
||||
}
|
||||
|
||||
export interface LocalAnswer extends BaseRecord {
|
||||
questionId: string;
|
||||
researchResultId?: string | null;
|
||||
content: string;
|
||||
citations: Array<{ sourceId: string; text: string }>;
|
||||
rating?: number | null;
|
||||
isAccepted: boolean;
|
||||
}
|
||||
|
||||
// ─── Store ──────────────────────────────────────────────────
|
||||
|
||||
const SYNC_SERVER_URL = import.meta.env.PUBLIC_SYNC_SERVER_URL || 'http://localhost:3050';
|
||||
|
||||
export const questionsAppStore = createLocalStore({
|
||||
appId: 'questions',
|
||||
collections: [
|
||||
{
|
||||
name: 'collections',
|
||||
indexes: ['sortOrder', 'isDefault'],
|
||||
guestSeed: guestCollections,
|
||||
},
|
||||
{
|
||||
name: 'questions',
|
||||
indexes: ['collectionId', 'status', 'priority', '[collectionId+status]'],
|
||||
guestSeed: guestQuestions,
|
||||
},
|
||||
{
|
||||
name: 'answers',
|
||||
indexes: ['questionId', 'isAccepted'],
|
||||
},
|
||||
],
|
||||
sync: {
|
||||
serverUrl: SYNC_SERVER_URL,
|
||||
},
|
||||
});
|
||||
|
||||
// Typed collection accessors
|
||||
export const collectionCollection = questionsAppStore.collection<LocalCollection>('collections');
|
||||
export const questionCollection = questionsAppStore.collection<LocalQuestion>('questions');
|
||||
export const answerCollection = questionsAppStore.collection<LocalAnswer>('answers');
|
||||
|
|
@ -8,6 +8,9 @@
|
|||
import { apiClient } from '$lib/api/client';
|
||||
import { questionsApi } from '$lib/api/questions';
|
||||
import { theme } from '$lib/stores/theme';
|
||||
import { AuthGate, GuestWelcomeModal } from '@manacore/shared-auth-ui';
|
||||
import { shouldShowGuestWelcome } from '@manacore/shared-auth-ui';
|
||||
import { questionsAppStore } from '$lib/data/local-store';
|
||||
import { PillNavigation, QuickInputBar, TagStrip } from '@manacore/shared-ui';
|
||||
import type {
|
||||
PillNavItem,
|
||||
|
|
@ -41,34 +44,29 @@
|
|||
// User email for nav
|
||||
let userEmail = $derived(authStore.user?.email || 'Menu');
|
||||
|
||||
onMount(async () => {
|
||||
// Initialize auth and redirect if not authenticated
|
||||
await authStore.initialize();
|
||||
if (!authStore.isAuthenticated) {
|
||||
goto('/login');
|
||||
return;
|
||||
let showGuestWelcome = $state(false);
|
||||
|
||||
async function handleAuthReady() {
|
||||
await questionsAppStore.initialize();
|
||||
if (authStore.isAuthenticated) {
|
||||
questionsAppStore.startSync(() => authStore.getValidToken());
|
||||
const token = await authStore.getValidToken();
|
||||
apiClient.setAccessToken(token);
|
||||
await collectionsStore.load();
|
||||
await questionsStore.load();
|
||||
await tagStore.fetchTags();
|
||||
}
|
||||
if (!authStore.isAuthenticated && shouldShowGuestWelcome('questions')) {
|
||||
showGuestWelcome = true;
|
||||
}
|
||||
|
||||
// Set API token
|
||||
const token = await authStore.getValidToken();
|
||||
apiClient.setAccessToken(token);
|
||||
|
||||
// Load initial data
|
||||
await collectionsStore.load();
|
||||
await questionsStore.load();
|
||||
await tagStore.fetchTags();
|
||||
|
||||
// Initialize mobile state
|
||||
updateMobileState();
|
||||
|
||||
// Restore nav mode from localStorage
|
||||
if (browser) {
|
||||
const savedCollapsed = localStorage.getItem('questions-nav-collapsed');
|
||||
if (savedCollapsed === 'true') {
|
||||
isCollapsed = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function handleSignOut() {
|
||||
await authStore.signOut();
|
||||
|
|
@ -199,71 +197,82 @@
|
|||
|
||||
<svelte:window onresize={updateMobileState} />
|
||||
|
||||
<div class="layout-container">
|
||||
<!-- TagStrip (above PillNav, toggled via Tags pill) -->
|
||||
{#if isTagStripVisible}
|
||||
<TagStrip
|
||||
tags={tagStore.tags.map((t) => ({
|
||||
id: t.id,
|
||||
name: t.name,
|
||||
color: t.color || '#3b82f6',
|
||||
}))}
|
||||
selectedIds={[]}
|
||||
onToggle={() => {}}
|
||||
onClear={() => {}}
|
||||
managementHref="/tags"
|
||||
loading={tagStore.loading}
|
||||
<AuthGate {authStore} {goto} allowGuest={true} onReady={handleAuthReady}>
|
||||
<div class="layout-container">
|
||||
<!-- TagStrip (above PillNav, toggled via Tags pill) -->
|
||||
{#if isTagStripVisible}
|
||||
<TagStrip
|
||||
tags={tagStore.tags.map((t) => ({
|
||||
id: t.id,
|
||||
name: t.name,
|
||||
color: t.color || '#3b82f6',
|
||||
}))}
|
||||
selectedIds={[]}
|
||||
onToggle={() => {}}
|
||||
onClear={() => {}}
|
||||
managementHref="/tags"
|
||||
loading={tagStore.loading}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<!-- Navigation -->
|
||||
<PillNavigation
|
||||
items={navItems}
|
||||
currentPath={$page.url.pathname}
|
||||
appName="Questions"
|
||||
homeRoute="/"
|
||||
onToggleTheme={handleToggleTheme}
|
||||
{isDark}
|
||||
{isCollapsed}
|
||||
onCollapsedChange={handleCollapsedChange}
|
||||
showThemeToggle={true}
|
||||
showLogout={true}
|
||||
onLogout={handleSignOut}
|
||||
loginHref="/login"
|
||||
primaryColor="#8b5cf6"
|
||||
showAppSwitcher={true}
|
||||
{appItems}
|
||||
{userEmail}
|
||||
settingsHref="/settings"
|
||||
themesHref="/themes"
|
||||
helpHref="/help"
|
||||
profileHref="/profile"
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<!-- Navigation -->
|
||||
<PillNavigation
|
||||
items={navItems}
|
||||
currentPath={$page.url.pathname}
|
||||
appName="Questions"
|
||||
homeRoute="/"
|
||||
onToggleTheme={handleToggleTheme}
|
||||
{isDark}
|
||||
{isCollapsed}
|
||||
onCollapsedChange={handleCollapsedChange}
|
||||
showThemeToggle={true}
|
||||
showLogout={true}
|
||||
onLogout={handleSignOut}
|
||||
loginHref="/login"
|
||||
primaryColor="#8b5cf6"
|
||||
showAppSwitcher={true}
|
||||
{appItems}
|
||||
{userEmail}
|
||||
settingsHref="/settings"
|
||||
themesHref="/themes"
|
||||
helpHref="/help"
|
||||
profileHref="/profile"
|
||||
<!-- Quick Input Bar -->
|
||||
<QuickInputBar
|
||||
onSearch={handleSearch}
|
||||
onSelect={handleSelect}
|
||||
placeholder="New question or search..."
|
||||
emptyText="No questions found"
|
||||
searchingText="Searching..."
|
||||
searchText="Search"
|
||||
onCreate={handleCreate}
|
||||
onParseCreate={handleParseCreate}
|
||||
createText="Create"
|
||||
deferSearch={true}
|
||||
locale={$locale || 'en'}
|
||||
appIcon="help-circle"
|
||||
bottomOffset={isMobile ? '70px' : '70px'}
|
||||
/>
|
||||
|
||||
<!-- Main Content -->
|
||||
<main class="main-content bg-background">
|
||||
<div class="content-wrapper">
|
||||
{@render children()}
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<GuestWelcomeModal
|
||||
appId="questions"
|
||||
visible={showGuestWelcome}
|
||||
onClose={() => (showGuestWelcome = false)}
|
||||
onLogin={() => goto('/login')}
|
||||
onRegister={() => goto('/register')}
|
||||
locale="de"
|
||||
/>
|
||||
|
||||
<!-- Quick Input Bar -->
|
||||
<QuickInputBar
|
||||
onSearch={handleSearch}
|
||||
onSelect={handleSelect}
|
||||
placeholder="New question or search..."
|
||||
emptyText="No questions found"
|
||||
searchingText="Searching..."
|
||||
searchText="Search"
|
||||
onCreate={handleCreate}
|
||||
onParseCreate={handleParseCreate}
|
||||
createText="Create"
|
||||
deferSearch={true}
|
||||
locale={$locale || 'en'}
|
||||
appIcon="help-circle"
|
||||
bottomOffset={isMobile ? '70px' : '70px'}
|
||||
/>
|
||||
|
||||
<!-- Main Content -->
|
||||
<main class="main-content bg-background">
|
||||
<div class="content-wrapper">
|
||||
{@render children()}
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</AuthGate>
|
||||
|
||||
<style>
|
||||
.layout-container {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue