mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 20:21:09 +02:00
feat(apps): migrate Presi, Picture, Inventar, NutriPhi, Planta, Storage to local-first
Add IndexedDB data layer (Dexie.js via @manacore/local-store) to 6 more apps,
bringing the total to 12/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} + handleAuthReady()
- Add GuestWelcomeModal for first-visit experience
- Add @manacore/local-store dependency
App-specific changes:
- Presi: Rewrite decks store from API to IndexedDB, conditional share button
- Picture: Rewrite gallery + boards pages to read from IndexedDB
- Inventar: Replace manual auth $effect with AuthGate, keep localStorage stores
- NutriPhi: Add onReady handler to existing AuthGate
- Planta: Add allowGuest + sync init to existing AuthGate
- Storage: Add local store init to existing handleAuthReady
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
8f40de2966
commit
ce51fd5fe2
31 changed files with 1623 additions and 211 deletions
|
|
@ -42,6 +42,7 @@
|
|||
"@manacore/shared-help-types": "workspace:*",
|
||||
"@manacore/shared-help-ui": "workspace:*",
|
||||
"@manacore/shared-icons": "workspace:*",
|
||||
"@manacore/local-store": "workspace:*",
|
||||
"@manacore/shared-profile-ui": "workspace:*",
|
||||
"@manacore/shared-stores": "workspace:*",
|
||||
"@manacore/shared-tags": "workspace:*",
|
||||
|
|
|
|||
62
apps/presi/apps/web/src/lib/data/guest-seed.ts
Normal file
62
apps/presi/apps/web/src/lib/data/guest-seed.ts
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
/**
|
||||
* Guest seed data for the Presi app.
|
||||
*
|
||||
* These records are loaded into IndexedDB when a new guest visits the app.
|
||||
* They serve as onboarding content that teaches the user how the app works.
|
||||
*/
|
||||
|
||||
import type { LocalDeck, LocalSlide } from './local-store';
|
||||
|
||||
const ONBOARDING_DECK_ID = 'onboarding-deck';
|
||||
|
||||
export const guestDecks: LocalDeck[] = [
|
||||
{
|
||||
id: ONBOARDING_DECK_ID,
|
||||
title: 'Willkommen bei Presi',
|
||||
description: 'Eine kurze Einführung in die Präsentations-App.',
|
||||
isPublic: false,
|
||||
},
|
||||
];
|
||||
|
||||
export const guestSlides: LocalSlide[] = [
|
||||
{
|
||||
id: 'slide-1',
|
||||
deckId: ONBOARDING_DECK_ID,
|
||||
order: 1,
|
||||
content: {
|
||||
type: 'title',
|
||||
title: 'Willkommen bei Presi!',
|
||||
subtitle: 'Erstelle Präsentationen direkt im Browser.',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'slide-2',
|
||||
deckId: ONBOARDING_DECK_ID,
|
||||
order: 2,
|
||||
content: {
|
||||
type: 'content',
|
||||
title: 'So funktioniert es',
|
||||
bulletPoints: [
|
||||
'Erstelle Decks mit dem + Button',
|
||||
'Füge Slides mit Text, Bildern und Aufzählungen hinzu',
|
||||
'Starte die Präsentation mit dem Play-Button',
|
||||
'Melde dich an, um zu synchronisieren',
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'slide-3',
|
||||
deckId: ONBOARDING_DECK_ID,
|
||||
order: 3,
|
||||
content: {
|
||||
type: 'content',
|
||||
title: 'Tastaturkürzel',
|
||||
bulletPoints: [
|
||||
'Pfeiltasten / A+D — Slides navigieren',
|
||||
'F — Vollbild',
|
||||
'ESC — Präsentation beenden',
|
||||
'T — Themes öffnen',
|
||||
],
|
||||
},
|
||||
},
|
||||
];
|
||||
58
apps/presi/apps/web/src/lib/data/local-store.ts
Normal file
58
apps/presi/apps/web/src/lib/data/local-store.ts
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
/**
|
||||
* Presi — Local-First Data Layer
|
||||
*
|
||||
* Defines the IndexedDB database, collections, and guest seed data.
|
||||
* This is the single source of truth for all Presi data.
|
||||
*/
|
||||
|
||||
import { createLocalStore, type BaseRecord } from '@manacore/local-store';
|
||||
import { guestDecks, guestSlides } from './guest-seed';
|
||||
|
||||
// ─── Types ──────────────────────────────────────────────────
|
||||
|
||||
export interface LocalDeck extends BaseRecord {
|
||||
title: string;
|
||||
description?: string | null;
|
||||
themeId?: string | null;
|
||||
isPublic: boolean;
|
||||
}
|
||||
|
||||
export interface LocalSlide extends BaseRecord {
|
||||
deckId: string;
|
||||
order: number;
|
||||
content: {
|
||||
type: 'title' | 'content' | 'image' | 'split';
|
||||
title?: string;
|
||||
subtitle?: string;
|
||||
body?: string;
|
||||
imageUrl?: string;
|
||||
bulletPoints?: string[];
|
||||
};
|
||||
}
|
||||
|
||||
// ─── Store ──────────────────────────────────────────────────
|
||||
|
||||
const SYNC_SERVER_URL = import.meta.env.PUBLIC_SYNC_SERVER_URL || 'http://localhost:3050';
|
||||
|
||||
export const presiStore = createLocalStore({
|
||||
appId: 'presi',
|
||||
collections: [
|
||||
{
|
||||
name: 'decks',
|
||||
indexes: ['isPublic'],
|
||||
guestSeed: guestDecks,
|
||||
},
|
||||
{
|
||||
name: 'slides',
|
||||
indexes: ['deckId', 'order', '[deckId+order]'],
|
||||
guestSeed: guestSlides,
|
||||
},
|
||||
],
|
||||
sync: {
|
||||
serverUrl: SYNC_SERVER_URL,
|
||||
},
|
||||
});
|
||||
|
||||
// Typed collection accessors
|
||||
export const deckCollection = presiStore.collection<LocalDeck>('decks');
|
||||
export const slideCollection = presiStore.collection<LocalSlide>('slides');
|
||||
|
|
@ -1,4 +1,11 @@
|
|||
import { decksApi, slidesApi } from '$lib/api/client';
|
||||
/**
|
||||
* Decks Store — Local-First with Dexie.js
|
||||
*
|
||||
* All reads and writes go to IndexedDB first.
|
||||
* When authenticated, changes sync to the server in the background.
|
||||
* Same public API as before so components don't need changes.
|
||||
*/
|
||||
|
||||
import type {
|
||||
Deck,
|
||||
Slide,
|
||||
|
|
@ -7,19 +14,51 @@ import type {
|
|||
CreateSlideDto,
|
||||
UpdateSlideDto,
|
||||
} from '@presi/shared';
|
||||
import {
|
||||
deckCollection,
|
||||
slideCollection,
|
||||
type LocalDeck,
|
||||
type LocalSlide,
|
||||
} from '$lib/data/local-store';
|
||||
|
||||
let decks = $state<Deck[]>([]);
|
||||
let currentDeck = $state<Deck | null>(null);
|
||||
let currentSlides = $state<Slide[]>([]);
|
||||
let isLoading = $state(false);
|
||||
let error = $state<string | null>(null);
|
||||
|
||||
/** Convert LocalDeck (IndexedDB) to shared Deck type. */
|
||||
function toDeck(local: LocalDeck): Deck {
|
||||
return {
|
||||
id: local.id,
|
||||
userId: 'local',
|
||||
title: local.title,
|
||||
description: local.description ?? undefined,
|
||||
themeId: local.themeId ?? undefined,
|
||||
isPublic: local.isPublic,
|
||||
createdAt: local.createdAt ?? new Date().toISOString(),
|
||||
updatedAt: local.updatedAt ?? new Date().toISOString(),
|
||||
};
|
||||
}
|
||||
|
||||
/** Convert LocalSlide (IndexedDB) to shared Slide type. */
|
||||
function toSlide(local: LocalSlide): Slide {
|
||||
return {
|
||||
id: local.id,
|
||||
deckId: local.deckId,
|
||||
order: local.order,
|
||||
content: local.content,
|
||||
createdAt: local.createdAt ?? new Date().toISOString(),
|
||||
};
|
||||
}
|
||||
|
||||
function createDecksStore() {
|
||||
let decks = $state<Deck[]>([]);
|
||||
let currentDeck = $state<Deck | null>(null);
|
||||
let currentSlides = $state<Slide[]>([]);
|
||||
let isLoading = $state(false);
|
||||
let error = $state<string | null>(null);
|
||||
|
||||
async function loadDecks() {
|
||||
isLoading = true;
|
||||
error = null;
|
||||
try {
|
||||
decks = await decksApi.getAll();
|
||||
const localDecks = await deckCollection.getAll();
|
||||
decks = localDecks.map(toDeck);
|
||||
} catch (e) {
|
||||
error = e instanceof Error ? e.message : 'Failed to load decks';
|
||||
console.error('Failed to load decks:', e);
|
||||
|
|
@ -32,9 +71,15 @@ function createDecksStore() {
|
|||
isLoading = true;
|
||||
error = null;
|
||||
try {
|
||||
const data = await decksApi.getOne(id);
|
||||
currentDeck = data.deck;
|
||||
currentSlides = data.slides.sort((a, b) => a.order - b.order);
|
||||
const localDeck = await deckCollection.get(id);
|
||||
if (localDeck) {
|
||||
currentDeck = toDeck(localDeck);
|
||||
} else {
|
||||
currentDeck = null;
|
||||
throw new Error('Deck not found');
|
||||
}
|
||||
const localSlides = await slideCollection.getAll({ deckId: id });
|
||||
currentSlides = localSlides.map(toSlide).sort((a, b) => a.order - b.order);
|
||||
} catch (e) {
|
||||
error = e instanceof Error ? e.message : 'Failed to load deck';
|
||||
console.error('Failed to load deck:', e);
|
||||
|
|
@ -47,7 +92,15 @@ function createDecksStore() {
|
|||
isLoading = true;
|
||||
error = null;
|
||||
try {
|
||||
const deck = await decksApi.create(dto);
|
||||
const newLocal: LocalDeck = {
|
||||
id: crypto.randomUUID(),
|
||||
title: dto.title,
|
||||
description: dto.description || null,
|
||||
themeId: dto.themeId || null,
|
||||
isPublic: false,
|
||||
};
|
||||
const inserted = await deckCollection.insert(newLocal);
|
||||
const deck = toDeck(inserted);
|
||||
decks = [deck, ...decks];
|
||||
return deck;
|
||||
} catch (e) {
|
||||
|
|
@ -62,10 +115,19 @@ function createDecksStore() {
|
|||
async function updateDeck(id: string, dto: UpdateDeckDto): Promise<boolean> {
|
||||
error = null;
|
||||
try {
|
||||
const updated = await decksApi.update(id, dto);
|
||||
decks = decks.map((d) => (d.id === id ? updated : d));
|
||||
if (currentDeck?.id === id) {
|
||||
currentDeck = updated;
|
||||
const localUpdates: Partial<LocalDeck> = {};
|
||||
if (dto.title !== undefined) localUpdates.title = dto.title;
|
||||
if (dto.description !== undefined) localUpdates.description = dto.description;
|
||||
if (dto.themeId !== undefined) localUpdates.themeId = dto.themeId;
|
||||
if (dto.isPublic !== undefined) localUpdates.isPublic = dto.isPublic;
|
||||
|
||||
const updated = await deckCollection.update(id, localUpdates);
|
||||
if (updated) {
|
||||
const updatedDeck = toDeck(updated);
|
||||
decks = decks.map((d) => (d.id === id ? updatedDeck : d));
|
||||
if (currentDeck?.id === id) {
|
||||
currentDeck = updatedDeck;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
} catch (e) {
|
||||
|
|
@ -78,7 +140,13 @@ function createDecksStore() {
|
|||
async function deleteDeck(id: string): Promise<boolean> {
|
||||
error = null;
|
||||
try {
|
||||
await decksApi.delete(id);
|
||||
// Delete all slides belonging to this deck
|
||||
const slides = await slideCollection.getAll({ deckId: id });
|
||||
for (const slide of slides) {
|
||||
await slideCollection.delete(slide.id);
|
||||
}
|
||||
|
||||
await deckCollection.delete(id);
|
||||
decks = decks.filter((d) => d.id !== id);
|
||||
if (currentDeck?.id === id) {
|
||||
currentDeck = null;
|
||||
|
|
@ -95,7 +163,15 @@ function createDecksStore() {
|
|||
async function createSlide(deckId: string, dto: CreateSlideDto): Promise<Slide | null> {
|
||||
error = null;
|
||||
try {
|
||||
const slide = await slidesApi.create(deckId, dto);
|
||||
const order = dto.order ?? currentSlides.length + 1;
|
||||
const newLocal: LocalSlide = {
|
||||
id: crypto.randomUUID(),
|
||||
deckId,
|
||||
order,
|
||||
content: dto.content,
|
||||
};
|
||||
const inserted = await slideCollection.insert(newLocal);
|
||||
const slide = toSlide(inserted);
|
||||
currentSlides = [...currentSlides, slide].sort((a, b) => a.order - b.order);
|
||||
return slide;
|
||||
} catch (e) {
|
||||
|
|
@ -108,8 +184,16 @@ function createDecksStore() {
|
|||
async function updateSlide(id: string, dto: UpdateSlideDto): Promise<boolean> {
|
||||
error = null;
|
||||
try {
|
||||
const updated = await slidesApi.update(id, dto);
|
||||
currentSlides = currentSlides.map((s) => (s.id === id ? updated : s));
|
||||
const localUpdates: Partial<LocalSlide> = {};
|
||||
if (dto.content !== undefined) localUpdates.content = dto.content;
|
||||
if (dto.order !== undefined) localUpdates.order = dto.order;
|
||||
|
||||
const updated = await slideCollection.update(id, localUpdates);
|
||||
if (updated) {
|
||||
currentSlides = currentSlides
|
||||
.map((s) => (s.id === id ? toSlide(updated) : s))
|
||||
.sort((a, b) => a.order - b.order);
|
||||
}
|
||||
return true;
|
||||
} catch (e) {
|
||||
error = e instanceof Error ? e.message : 'Failed to update slide';
|
||||
|
|
@ -121,7 +205,7 @@ function createDecksStore() {
|
|||
async function deleteSlide(id: string): Promise<boolean> {
|
||||
error = null;
|
||||
try {
|
||||
await slidesApi.delete(id);
|
||||
await slideCollection.delete(id);
|
||||
currentSlides = currentSlides.filter((s) => s.id !== id);
|
||||
return true;
|
||||
} catch (e) {
|
||||
|
|
@ -134,9 +218,10 @@ function createDecksStore() {
|
|||
async function reorderSlides(slides: { id: string; order: number }[]): Promise<boolean> {
|
||||
error = null;
|
||||
try {
|
||||
await slidesApi.reorder({ slides });
|
||||
// Update local state
|
||||
const orderMap = new Map(slides.map((s) => [s.id, s.order]));
|
||||
for (const { id, order } of slides) {
|
||||
await slideCollection.update(id, { order });
|
||||
}
|
||||
currentSlides = currentSlides
|
||||
.map((s) => ({ ...s, order: orderMap.get(s.id) ?? s.order }))
|
||||
.sort((a, b) => a.order - b.order);
|
||||
|
|
|
|||
|
|
@ -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, TagStrip } from '@manacore/shared-ui';
|
||||
import type { PillNavItem, PillDropdownItem, QuickInputItem } from '@manacore/shared-ui';
|
||||
|
|
@ -17,6 +16,9 @@
|
|||
import { decksStore } from '$lib/stores/decks.svelte';
|
||||
import { presiOnboarding } from '$lib/stores/app-onboarding.svelte';
|
||||
import { MiniOnboardingModal } from '@manacore/shared-app-onboarding';
|
||||
import { SessionExpiredBanner, AuthGate, GuestWelcomeModal } from '@manacore/shared-auth-ui';
|
||||
import { shouldShowGuestWelcome } from '@manacore/shared-auth-ui';
|
||||
import { presiStore } from '$lib/data/local-store';
|
||||
|
||||
// App switcher items
|
||||
const appItems = getPillAppItems('presi');
|
||||
|
|
@ -25,6 +27,12 @@
|
|||
|
||||
let isCollapsed = $state(false);
|
||||
|
||||
// Guest welcome modal state
|
||||
let showGuestWelcome = $state(false);
|
||||
|
||||
// User email for user dropdown — empty string for guests so PillNav shows login button
|
||||
let userEmail = $derived(auth.isAuthenticated ? auth.user?.email || 'Menü' : '');
|
||||
|
||||
// Theme variant dropdown items
|
||||
let themeVariantItems = $derived<PillDropdownItem[]>([
|
||||
...theme.variants.map((variant) => ({
|
||||
|
|
@ -56,9 +64,6 @@
|
|||
);
|
||||
let currentLanguageLabel = $derived(getCurrentLanguageLabel(currentLocale));
|
||||
|
||||
// User email for user dropdown
|
||||
let userEmail = $derived(auth.user?.email);
|
||||
|
||||
// TagStrip visibility
|
||||
let isTagStripVisible = $state(false);
|
||||
function handleTagStripToggle() {
|
||||
|
|
@ -137,16 +142,28 @@
|
|||
goto(`/deck/${item.id}`);
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
// Redirect to login if not authenticated
|
||||
if (!auth.isAuthenticated) {
|
||||
goto('/login');
|
||||
return;
|
||||
async function handleAuthReady() {
|
||||
// Initialize local-first database (opens IndexedDB, seeds guest data)
|
||||
await presiStore.initialize();
|
||||
|
||||
// If authenticated, start syncing to server
|
||||
if (auth.isAuthenticated) {
|
||||
presiStore.startSync(() => auth.getValidToken());
|
||||
}
|
||||
|
||||
// Load user settings and tags
|
||||
await userSettings.load();
|
||||
await tagStore.fetchTags();
|
||||
// Load decks from IndexedDB (guest seed or synced data)
|
||||
await decksStore.loadDecks();
|
||||
|
||||
// Show guest welcome modal on first visit
|
||||
if (!auth.isAuthenticated && shouldShowGuestWelcome('presi')) {
|
||||
showGuestWelcome = true;
|
||||
}
|
||||
|
||||
if (auth.isAuthenticated) {
|
||||
// Load user settings and tags (require auth)
|
||||
await userSettings.load();
|
||||
await tagStore.fetchTags();
|
||||
}
|
||||
|
||||
// Redirect to start page if on root and a custom start page is set
|
||||
const currentPath = window.location.pathname;
|
||||
|
|
@ -160,7 +177,7 @@
|
|||
isCollapsed = true;
|
||||
collapsedStore.set(true);
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:window onkeydown={handleKeydown} />
|
||||
|
|
@ -171,81 +188,98 @@
|
|||
{@render children()}
|
||||
</main>
|
||||
{:else}
|
||||
<!-- Navigation Layout -->
|
||||
<div class="layout-container">
|
||||
<!-- Floating Pill Navigation -->
|
||||
<PillNavigation
|
||||
items={navItems}
|
||||
currentPath={$page.url.pathname}
|
||||
appName="Presi"
|
||||
homeRoute="/"
|
||||
onToggleTheme={handleToggleTheme}
|
||||
isDark={theme.isDark}
|
||||
{isCollapsed}
|
||||
onCollapsedChange={handleCollapsedChange}
|
||||
showThemeToggle={true}
|
||||
showThemeVariants={true}
|
||||
{themeVariantItems}
|
||||
{currentThemeVariantLabel}
|
||||
themeMode={theme.mode}
|
||||
onThemeModeChange={handleThemeModeChange}
|
||||
showLanguageSwitcher={true}
|
||||
{languageItems}
|
||||
{currentLanguageLabel}
|
||||
showLogout={true}
|
||||
onLogout={handleLogout}
|
||||
primaryColor="#64748b"
|
||||
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={auth} {goto} allowGuest={true} onReady={handleAuthReady}>
|
||||
<!-- Navigation Layout -->
|
||||
<div class="layout-container">
|
||||
<!-- Floating Pill Navigation -->
|
||||
<PillNavigation
|
||||
items={navItems}
|
||||
currentPath={$page.url.pathname}
|
||||
appName="Presi"
|
||||
homeRoute="/"
|
||||
onToggleTheme={handleToggleTheme}
|
||||
isDark={theme.isDark}
|
||||
{isCollapsed}
|
||||
onCollapsedChange={handleCollapsedChange}
|
||||
showThemeToggle={true}
|
||||
showThemeVariants={true}
|
||||
{themeVariantItems}
|
||||
{currentThemeVariantLabel}
|
||||
themeMode={theme.mode}
|
||||
onThemeModeChange={handleThemeModeChange}
|
||||
showLanguageSwitcher={true}
|
||||
{languageItems}
|
||||
{currentLanguageLabel}
|
||||
showLogout={auth.isAuthenticated}
|
||||
onLogout={handleLogout}
|
||||
loginHref="/login"
|
||||
primaryColor="#64748b"
|
||||
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}
|
||||
|
||||
<!-- Quick Input Bar -->
|
||||
<QuickInputBar
|
||||
onSearch={handleInputSearch}
|
||||
onSelect={handleInputSelect}
|
||||
placeholder="Präsentation suchen..."
|
||||
emptyText="Keine Decks gefunden"
|
||||
searchingText="Suche..."
|
||||
locale={$locale || 'de'}
|
||||
appIcon="search"
|
||||
bottomOffset="70px"
|
||||
/>
|
||||
|
||||
<!-- Main Content -->
|
||||
<main class="main-content">
|
||||
<div class="content-wrapper">
|
||||
{@render children()}
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<!-- Onboarding Modal -->
|
||||
{#if presiOnboarding.shouldShow}
|
||||
<MiniOnboardingModal store={presiOnboarding} appName="Presi" appEmoji="📊" />
|
||||
{/if}
|
||||
|
||||
<!-- Quick Input Bar -->
|
||||
<QuickInputBar
|
||||
onSearch={handleInputSearch}
|
||||
onSelect={handleInputSelect}
|
||||
placeholder="Präsentation suchen..."
|
||||
emptyText="Keine Decks gefunden"
|
||||
searchingText="Suche..."
|
||||
locale={$locale || 'de'}
|
||||
appIcon="search"
|
||||
bottomOffset="70px"
|
||||
<!-- Guest Welcome Modal -->
|
||||
<GuestWelcomeModal
|
||||
appId="presi"
|
||||
visible={showGuestWelcome}
|
||||
onClose={() => (showGuestWelcome = false)}
|
||||
onLogin={() => goto('/login')}
|
||||
onRegister={() => goto('/register')}
|
||||
locale={($locale || 'de') === 'de' ? 'de' : 'en'}
|
||||
/>
|
||||
|
||||
<!-- Main Content -->
|
||||
<main class="main-content">
|
||||
<div class="content-wrapper">
|
||||
{@render children()}
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<!-- Onboarding Modal -->
|
||||
{#if presiOnboarding.shouldShow}
|
||||
<MiniOnboardingModal store={presiOnboarding} appName="Presi" appEmoji="📊" />
|
||||
{/if}
|
||||
{#if auth.isAuthenticated}
|
||||
<SessionExpiredBanner locale={$locale || 'de'} loginHref="/login" />
|
||||
{/if}
|
||||
</AuthGate>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import { decksStore } from '$lib/stores/decks.svelte';
|
||||
import { PresiEvents } from '@manacore/shared-utils/analytics';
|
||||
|
|
@ -60,10 +59,6 @@
|
|||
];
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
decksStore.loadDecks();
|
||||
});
|
||||
|
||||
async function handleCreateDeck(e: SubmitEvent) {
|
||||
e.preventDefault();
|
||||
if (!newDeckTitle.trim()) return;
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
import { goto } from '$app/navigation';
|
||||
import { browser } from '$app/environment';
|
||||
import { decksStore } from '$lib/stores/decks.svelte';
|
||||
import { auth } from '$lib/stores/auth.svelte';
|
||||
import { PresiEvents } from '@manacore/shared-utils/analytics';
|
||||
import { shareApi } from '$lib/api/client';
|
||||
import type { ShareLink } from '$lib/api/client';
|
||||
|
|
@ -254,13 +255,15 @@
|
|||
<Plus class="w-5 h-5" />
|
||||
Add Slide
|
||||
</button>
|
||||
<button
|
||||
onclick={openShareModal}
|
||||
class="flex items-center gap-2 px-4 py-2 bg-slate-100 dark:bg-slate-700 hover:bg-slate-200 dark:hover:bg-slate-600 text-slate-700 dark:text-slate-200 font-medium rounded-lg transition-colors"
|
||||
>
|
||||
<ShareNetwork class="w-5 h-5" />
|
||||
Share
|
||||
</button>
|
||||
{#if auth.isAuthenticated}
|
||||
<button
|
||||
onclick={openShareModal}
|
||||
class="flex items-center gap-2 px-4 py-2 bg-slate-100 dark:bg-slate-700 hover:bg-slate-200 dark:hover:bg-slate-600 text-slate-700 dark:text-slate-200 font-medium rounded-lg transition-colors"
|
||||
>
|
||||
<ShareNetwork class="w-5 h-5" />
|
||||
Share
|
||||
</button>
|
||||
{/if}
|
||||
{#if decksStore.currentSlides.length > 0}
|
||||
<a
|
||||
href="/present/{deckId}"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue