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:
Till JS 2026-03-27 17:35:38 +01:00
parent 8f40de2966
commit ce51fd5fe2
31 changed files with 1623 additions and 211 deletions

View file

@ -50,6 +50,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-subscription-ui": "workspace:*",
"@manacore/shared-tailwind": "workspace:*",

View file

@ -0,0 +1,55 @@
/**
* Guest seed data for the NutriPhi app.
*
* Provides demo meals and default goals for the onboarding experience.
*/
import type { LocalMeal, LocalGoal } from './local-store';
const today = new Date().toISOString().split('T')[0];
export const guestMeals: LocalMeal[] = [
{
id: 'meal-breakfast',
date: today,
mealType: 'breakfast',
inputType: 'text',
description: 'Haferflocken mit Banane und Honig',
confidence: 0.9,
nutrition: {
calories: 380,
protein: 10,
carbohydrates: 68,
fat: 8,
fiber: 6,
sugar: 24,
},
},
{
id: 'meal-lunch',
date: today,
mealType: 'lunch',
inputType: 'text',
description: 'Vollkorn-Sandwich mit Avocado und Ei',
confidence: 0.85,
nutrition: {
calories: 520,
protein: 22,
carbohydrates: 45,
fat: 28,
fiber: 8,
sugar: 4,
},
},
];
export const guestGoals: LocalGoal[] = [
{
id: 'default-goals',
dailyCalories: 2000,
dailyProtein: 60,
dailyCarbs: 250,
dailyFat: 65,
dailyFiber: 30,
},
];

View file

@ -0,0 +1,83 @@
/**
* NutriPhi Local-First Data Layer
*
* Meals, nutrition, goals, and favorites stored locally.
* AI analysis and recommendations remain server-side.
*/
import { createLocalStore, type BaseRecord } from '@manacore/local-store';
import { guestMeals, guestGoals } from './guest-seed';
// ─── Types ──────────────────────────────────────────────────
export interface LocalMeal extends BaseRecord {
date: string;
mealType: 'breakfast' | 'lunch' | 'dinner' | 'snack';
inputType: 'photo' | 'text';
description: string;
portionSize?: string | null;
confidence: number;
nutrition?: {
calories: number;
protein: number;
carbohydrates: number;
fat: number;
fiber: number;
sugar: number;
} | null;
}
export interface LocalGoal extends BaseRecord {
dailyCalories: number;
dailyProtein?: number | null;
dailyCarbs?: number | null;
dailyFat?: number | null;
dailyFiber?: number | null;
}
export interface LocalFavorite extends BaseRecord {
name: string;
description: string;
mealType: 'breakfast' | 'lunch' | 'dinner' | 'snack';
nutrition: {
calories: number;
protein: number;
carbohydrates: number;
fat: number;
fiber: number;
sugar: number;
};
usageCount: number;
}
// ─── Store ──────────────────────────────────────────────────
const SYNC_SERVER_URL = import.meta.env.PUBLIC_SYNC_SERVER_URL || 'http://localhost:3050';
export const nutriphiStore = createLocalStore({
appId: 'nutriphi',
collections: [
{
name: 'meals',
indexes: ['date', 'mealType', '[date+mealType]'],
guestSeed: guestMeals,
},
{
name: 'goals',
indexes: [],
guestSeed: guestGoals,
},
{
name: 'favorites',
indexes: ['mealType', 'usageCount'],
},
],
sync: {
serverUrl: SYNC_SERVER_URL,
},
});
// Typed collection accessors
export const mealCollection = nutriphiStore.collection<LocalMeal>('meals');
export const goalCollection = nutriphiStore.collection<LocalGoal>('goals');
export const favoriteCollection = nutriphiStore.collection<LocalFavorite>('favorites');

View file

@ -8,9 +8,13 @@
import { authStore } from '$lib/stores/auth.svelte';
import { mealsStore } from '$lib/stores/meals.svelte';
import { parseMealInput, formatParsedMealPreview } from '$lib/utils/meal-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 { nutriphiStore } from '$lib/data/local-store';
let { children } = $props();
let showGuestWelcome = $state(false);
// QuickInputBar handlers - search recent meals
async function handleSearch(query: string): Promise<QuickInputItem[]> {
const q = query.toLowerCase();
@ -38,6 +42,16 @@
};
}
async function handleAuthReady() {
await nutriphiStore.initialize();
if (authStore.isAuthenticated) {
nutriphiStore.startSync(() => authStore.getValidToken());
}
if (!authStore.isAuthenticated && shouldShowGuestWelcome('nutriphi')) {
showGuestWelcome = true;
}
}
async function handleCreate(query: string): Promise<void> {
if (!query.trim()) return;
const parsed = parseMealInput(query);
@ -60,7 +74,7 @@
{/if}
</svelte:head>
<AuthGate {authStore} {goto} allowGuest={true}>
<AuthGate {authStore} {goto} allowGuest={true} onReady={handleAuthReady}>
{#if $i18nLoading}
<div class="flex min-h-screen items-center justify-center bg-background">
<div
@ -88,4 +102,13 @@
{/if}
<SessionExpiredBanner locale="de" loginHref="/login" />
{/if}
<GuestWelcomeModal
appId="nutriphi"
visible={showGuestWelcome}
onClose={() => (showGuestWelcome = false)}
onLogin={() => goto('/login')}
onRegister={() => goto('/register')}
locale="de"
/>
</AuthGate>