feat(apps): add unified Apps page and PillNavigation to all web apps

- Add AppsPage component to shared-ui for displaying all Mana apps
- Add allAppsHref prop to PillNavigation with "Alle Apps" link in dropdown
- Integrate PillNavigation in archived apps (maerchenzauber, news, uload, wisekeep)
- Add /apps route to all web apps (active and archived)
- Replace custom sidebars/headers with unified PillNavigation

Apps updated:
- Active: chat, manacore, manadeck, picture, presi, zitare
- Archived: maerchenzauber, memoro, news, nutriphi, uload, wisekeep

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Till-JS 2025-12-01 14:48:00 +01:00
parent 1f2d21e005
commit 4e4db4612c
25 changed files with 1354 additions and 538 deletions

View file

@ -25,122 +25,51 @@
let { children } = $props();
let loading = $state(true);
let isSidebarCollapsed = $state(false);
let isMobileMenuOpen = $state(false);
let showKeyboardShortcuts = $state(false);
let isSidebarMode = $state(false);
let isCollapsed = $state(false);
let isDark = $state(false);
// Keyboard shortcuts configuration
const navRoutes: Record<string, string> = {
'1': '/dashboard', // Dashboard
'2': '/stories', // Stories
'3': '/characters', // Characters
'4': '/discover', // Discover
'5': '/settings', // Settings
};
const actionRoutes: Record<string, string> = {
n: '/stories/create', // New Story
s: '/stories/create', // New Story (alternative)
c: '/characters/create', // New Character
};
// Shortcut descriptions for help modal
const shortcutGroups = [
{
title: 'Navigation',
shortcuts: [
{ keys: ['Cmd/Ctrl', '1'], description: 'Dashboard' },
{ keys: ['Cmd/Ctrl', '2'], description: 'Geschichten' },
{ keys: ['Cmd/Ctrl', '3'], description: 'Charaktere' },
{ keys: ['Cmd/Ctrl', '4'], description: 'Entdecken' },
{ keys: ['Cmd/Ctrl', '5'], description: 'Einstellungen' },
],
},
{
title: 'Aktionen',
shortcuts: [
{ keys: ['Cmd/Ctrl', 'N'], description: 'Neue Geschichte' },
{ keys: ['Cmd/Ctrl', 'Shift', 'C'], description: 'Neuer Charakter' },
{ keys: ['?'], description: 'Tastaturkürzel anzeigen' },
],
},
{
title: 'Allgemein',
shortcuts: [
{ keys: ['Esc'], description: 'Menü/Modal schließen' },
{ keys: ['B'], description: 'Seitenleiste ein-/ausblenden' },
],
},
];
// Navigation shortcuts (Ctrl+1-5)
const navRoutes = ['/dashboard', '/stories', '/characters', '/discover', '/settings'];
function handleKeydown(event: KeyboardEvent) {
// Don't handle if user is typing in an input
const target = event.target as HTMLElement;
if (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.isContentEditable) {
return;
}
// ? to show keyboard shortcuts
if (event.key === '?' && !event.ctrlKey && !event.metaKey) {
event.preventDefault();
showKeyboardShortcuts = !showKeyboardShortcuts;
return;
}
// ESC to close modals/menus
if (event.key === 'Escape') {
if (showKeyboardShortcuts) {
showKeyboardShortcuts = false;
return;
}
if (isMobileMenuOpen) {
isMobileMenuOpen = false;
return;
}
}
// B to toggle sidebar (without modifiers)
if (event.key === 'b' && !event.ctrlKey && !event.metaKey && !event.shiftKey) {
event.preventDefault();
handleSidebarToggle();
return;
}
// Ctrl/Cmd + key shortcuts
if ((event.ctrlKey || event.metaKey) && !event.altKey) {
// Ctrl/Cmd + number for navigation
const route = navRoutes[event.key];
if (route) {
if ((event.ctrlKey || event.metaKey) && !event.shiftKey && !event.altKey) {
const num = parseInt(event.key);
if (num >= 1 && num <= 5) {
event.preventDefault();
goto(route);
return;
}
// Ctrl/Cmd + Shift + C for new character
if (event.shiftKey && event.key.toLowerCase() === 'c') {
event.preventDefault();
goto('/characters/create');
return;
}
// Ctrl/Cmd + N for new story
if (!event.shiftKey && event.key.toLowerCase() === 'n') {
event.preventDefault();
goto('/stories/create');
return;
const route = navRoutes[num - 1];
if (route) {
goto(route);
}
}
}
}
function handleSidebarToggle() {
isSidebarCollapsed = !isSidebarCollapsed;
function handleModeChange(isSidebar: boolean) {
isSidebarMode = isSidebar;
if (typeof localStorage !== 'undefined') {
localStorage.setItem('maerchenzauber-sidebar-collapsed', String(isSidebarCollapsed));
localStorage.setItem('maerchenzauber-nav-sidebar', String(isSidebar));
}
}
function handleMobileMenuToggle() {
isMobileMenuOpen = !isMobileMenuOpen;
function handleCollapsedChange(collapsed: boolean) {
isCollapsed = collapsed;
if (typeof localStorage !== 'undefined') {
localStorage.setItem('maerchenzauber-nav-collapsed', String(collapsed));
}
}
function handleToggleTheme() {
isDark = !isDark;
document.documentElement.classList.toggle('dark', isDark);
if (typeof localStorage !== 'undefined') {
localStorage.setItem('maerchenzauber-dark-mode', String(isDark));
}
}
async function handleLogout() {
@ -148,7 +77,6 @@
goto('/login');
}
// Client-side auth guard
onMount(async () => {
await authStore.initialize();
@ -157,11 +85,20 @@
return;
}
// Restore sidebar state from localStorage
// Restore nav mode from localStorage
if (typeof localStorage !== 'undefined') {
const savedCollapsed = localStorage.getItem('maerchenzauber-sidebar-collapsed');
const savedSidebar = localStorage.getItem('maerchenzauber-nav-sidebar');
if (savedSidebar === 'true') {
isSidebarMode = true;
}
const savedCollapsed = localStorage.getItem('maerchenzauber-nav-collapsed');
if (savedCollapsed === 'true') {
isSidebarCollapsed = true;
isCollapsed = true;
}
const savedDark = localStorage.getItem('maerchenzauber-dark-mode');
if (savedDark === 'true') {
isDark = true;
document.documentElement.classList.add('dark');
}
}
@ -172,10 +109,7 @@
<svelte:window onkeydown={handleKeydown} />
{#if loading}
<!-- Loading State -->
<div
class="flex min-h-screen items-center justify-center bg-gradient-to-br from-pink-50 to-purple-50 dark:from-gray-900 dark:to-gray-800"
>
<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-pink-500 border-r-transparent"
@ -184,127 +118,47 @@
</div>
</div>
{:else}
<!-- Main Layout -->
<div
class="flex min-h-screen bg-gradient-to-br from-pink-50/50 to-purple-50/50 dark:from-gray-900 dark:to-gray-800"
>
<!-- Sidebar (Desktop) -->
<Sidebar
isCollapsed={isSidebarCollapsed}
<div class="flex min-h-screen flex-col">
<PillNavigation
items={navItems}
currentPath={$page.url.pathname}
onToggle={handleSidebarToggle}
appName="Märchenzauber"
homeRoute="/dashboard"
onLogout={handleLogout}
/>
<!-- Mobile Menu Overlay -->
{#if isMobileMenuOpen}
<div
class="fixed inset-0 z-40 bg-black/50 lg:hidden"
onclick={() => (isMobileMenuOpen = false)}
onkeydown={(e) => e.key === 'Escape' && (isMobileMenuOpen = false)}
role="button"
tabindex="0"
></div>
<div class="fixed inset-y-0 left-0 z-50 w-64 lg:hidden">
<Sidebar
isCollapsed={false}
currentPath={$page.url.pathname}
onToggle={() => (isMobileMenuOpen = false)}
onLogout={handleLogout}
isMobile={true}
/>
</div>
{/if}
<!-- Main Content Area -->
<div
class="flex flex-1 flex-col transition-all duration-300"
class:lg:ml-64={!isSidebarCollapsed}
class:lg:ml-20={isSidebarCollapsed}
onToggleTheme={handleToggleTheme}
{isDark}
{isSidebarMode}
onModeChange={handleModeChange}
{isCollapsed}
onCollapsedChange={handleCollapsedChange}
showThemeToggle={true}
primaryColor="#ec4899"
showAppSwitcher={true}
{appItems}
{userEmail}
settingsHref="/settings"
manaHref="/subscription"
profileHref="/profile"
allAppsHref="/apps"
>
<!-- Header -->
<Header onMenuClick={handleMobileMenuToggle} onLogout={handleLogout} />
{#snippet logo()}
<span class="text-xl"></span>
<span class="pill-label font-bold">Märchenzauber</span>
{/snippet}
</PillNavigation>
<!-- Page Content -->
<main class="flex-1 overflow-auto p-4 lg:p-6">
<main
class="main-content flex-1 transition-all duration-300 {isCollapsed
? ''
: isSidebarMode
? 'pl-[180px]'
: 'pt-20'}"
>
<div class="container mx-auto px-4 py-8">
{@render children()}
</main>
</div>
</div>
</main>
</div>
<!-- Toast Notifications -->
<ToastContainer />
<!-- Keyboard Shortcuts Modal -->
{#if showKeyboardShortcuts}
<div
class="fixed inset-0 z-[200] flex items-center justify-center bg-black/50 backdrop-blur-sm"
onclick={() => (showKeyboardShortcuts = false)}
onkeydown={(e) => e.key === 'Escape' && (showKeyboardShortcuts = false)}
role="button"
tabindex="0"
>
<div
class="mx-4 max-h-[80vh] w-full max-w-lg overflow-y-auto rounded-2xl bg-white p-6 shadow-2xl dark:bg-gray-800"
onclick={(e) => e.stopPropagation()}
onkeydown={(e) => e.stopPropagation()}
role="dialog"
>
<div class="mb-6 flex items-center justify-between">
<h2 class="text-xl font-bold text-gray-800 dark:text-gray-200">Tastaturkürzel</h2>
<button
onclick={() => (showKeyboardShortcuts = false)}
class="rounded-lg p-2 text-gray-400 hover:bg-gray-100 hover:text-gray-600 dark:hover:bg-gray-700 dark:hover:text-gray-300"
aria-label="Schließen"
>
<svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M6 18L18 6M6 6l12 12"
/>
</svg>
</button>
</div>
<div class="space-y-6">
{#each shortcutGroups as group}
<div>
<h3
class="mb-3 text-sm font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400"
>
{group.title}
</h3>
<div class="space-y-2">
{#each group.shortcuts as shortcut}
<div
class="flex items-center justify-between rounded-lg bg-gray-50 px-3 py-2 dark:bg-gray-700/50"
>
<span class="text-sm text-gray-700 dark:text-gray-300"
>{shortcut.description}</span
>
<div class="flex gap-1">
{#each shortcut.keys as key}
<kbd
class="rounded bg-gray-200 px-2 py-1 text-xs font-medium text-gray-700 dark:bg-gray-600 dark:text-gray-300"
>
{key}
</kbd>
{/each}
</div>
</div>
{/each}
</div>
</div>
{/each}
</div>
<p class="mt-6 text-center text-xs text-gray-500 dark:text-gray-400">
Drücke <kbd class="rounded bg-gray-200 px-1.5 py-0.5 text-xs dark:bg-gray-600">?</kbd> um dieses
Menü zu öffnen
</p>
</div>
</div>
{/if}
{/if}

View file

@ -0,0 +1,17 @@
<script lang="ts">
import { AppsPage } from '@manacore/shared-ui';
</script>
<svelte:head>
<title>Alle Apps - Märchenzauber</title>
</svelte:head>
<div class="apps-page-wrapper">
<AppsPage currentAppId="maerchenzauber" locale="de" title="Alle Apps" />
</div>
<style>
.apps-page-wrapper {
min-height: 100%;
}
</style>

View file

@ -2,135 +2,153 @@
import { goto } from '$app/navigation';
import { page } from '$app/stores';
import { authStore } from '$lib/stores/auth.svelte';
import { onMount } from 'svelte';
import { PillNavigation } from '@manacore/shared-ui';
import type { PillNavItem } from '@manacore/shared-ui';
import { getPillAppItems } from '@manacore/shared-branding';
let { children } = $props();
const navItems = [
{ href: '/feed', label: 'Feed', icon: 'feed' },
{ href: '/summaries', label: 'Zusammenfassungen', icon: 'summaries' },
{ href: '/in-depth', label: 'In-Depth', icon: 'indepth' },
{ href: '/saved', label: 'Gespeichert', icon: 'saved' },
// App switcher items
const appItems = getPillAppItems('news');
// User email for dropdown
let userEmail = $derived(authStore.user?.email);
// Navigation items for News
const navItems: PillNavItem[] = [
{ href: '/feed', label: 'Feed', icon: 'rss' },
{ href: '/summaries', label: 'Zusammenfassungen', icon: 'document' },
{ href: '/in-depth', label: 'In-Depth', icon: 'book' },
{ href: '/saved', label: 'Gespeichert', icon: 'bookmark' },
{ href: '/settings', label: 'Einstellungen', icon: 'settings' },
];
let loading = $state(true);
let isSidebarMode = $state(false);
let isCollapsed = $state(false);
let isDark = $state(false);
// Navigation shortcuts (Ctrl+1-5)
const navRoutes = ['/feed', '/summaries', '/in-depth', '/saved', '/settings'];
function handleKeydown(event: KeyboardEvent) {
const target = event.target as HTMLElement;
if (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.isContentEditable) {
return;
}
if ((event.ctrlKey || event.metaKey) && !event.shiftKey && !event.altKey) {
const num = parseInt(event.key);
if (num >= 1 && num <= 5) {
event.preventDefault();
const route = navRoutes[num - 1];
if (route) {
goto(route);
}
}
}
}
function handleModeChange(isSidebar: boolean) {
isSidebarMode = isSidebar;
if (typeof localStorage !== 'undefined') {
localStorage.setItem('news-nav-sidebar', String(isSidebar));
}
}
function handleCollapsedChange(collapsed: boolean) {
isCollapsed = collapsed;
if (typeof localStorage !== 'undefined') {
localStorage.setItem('news-nav-collapsed', String(collapsed));
}
}
function handleToggleTheme() {
isDark = !isDark;
document.documentElement.classList.toggle('dark', isDark);
if (typeof localStorage !== 'undefined') {
localStorage.setItem('news-dark-mode', String(isDark));
}
}
async function handleLogout() {
await authStore.logout();
goto('/auth/login');
}
onMount(() => {
// Restore nav mode from localStorage
if (typeof localStorage !== 'undefined') {
const savedSidebar = localStorage.getItem('news-nav-sidebar');
if (savedSidebar === 'true') {
isSidebarMode = true;
}
const savedCollapsed = localStorage.getItem('news-nav-collapsed');
if (savedCollapsed === 'true') {
isCollapsed = true;
}
const savedDark = localStorage.getItem('news-dark-mode');
if (savedDark === 'true') {
isDark = true;
document.documentElement.classList.add('dark');
}
}
loading = false;
});
</script>
<div class="min-h-screen flex">
<!-- Sidebar -->
<aside class="w-64 bg-background-card border-r border-border flex flex-col">
<!-- Logo -->
<div class="p-4 border-b border-border">
<a href="/feed" class="flex items-center gap-2">
<svg
class="w-8 h-8 text-primary"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="1.5"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M12 7.5h1.5m-1.5 3h1.5m-7.5 3h7.5m-7.5 3h7.5m3-9h3.375c.621 0 1.125.504 1.125 1.125V18a2.25 2.25 0 01-2.25 2.25M16.5 7.5V18a2.25 2.25 0 002.25 2.25M16.5 7.5V4.875c0-.621-.504-1.125-1.125-1.125H4.125C3.504 3.75 3 4.254 3 4.875V18a2.25 2.25 0 002.25 2.25h13.5M6 7.5h3v3H6v-3z"
/>
</svg>
<span class="font-bold text-lg">News Hub</span>
</a>
<svelte:window onkeydown={handleKeydown} />
{#if 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-500 border-r-transparent"
></div>
<p class="text-gray-600 dark:text-gray-400">Laden...</p>
</div>
</div>
{:else}
<div class="flex min-h-screen flex-col">
<PillNavigation
items={navItems}
currentPath={$page.url.pathname}
appName="News"
homeRoute="/feed"
onLogout={handleLogout}
onToggleTheme={handleToggleTheme}
{isDark}
{isSidebarMode}
onModeChange={handleModeChange}
{isCollapsed}
onCollapsedChange={handleCollapsedChange}
showThemeToggle={true}
primaryColor="#3b82f6"
showAppSwitcher={true}
{appItems}
{userEmail}
settingsHref="/settings"
manaHref="/subscription"
profileHref="/profile"
allAppsHref="/apps"
>
{#snippet logo()}
<span class="text-xl">📰</span>
<span class="pill-label font-bold">News</span>
{/snippet}
</PillNavigation>
<!-- Navigation -->
<nav class="flex-1 p-4 space-y-1">
{#each navItems as item}
<a
href={item.href}
class="flex items-center gap-3 px-3 py-2 rounded-lg transition-colors {$page.url.pathname.startsWith(
item.href
)
? 'bg-primary/10 text-primary'
: 'text-text-secondary hover:bg-background-card-hover hover:text-text-primary'}"
>
{#if item.icon === 'feed'}
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="1.5"
d="M6 5c7.18 0 13 5.82 13 13M6 11a7 7 0 017 7m-6 0a1 1 0 11-2 0 1 1 0 012 0z"
/>
</svg>
{:else if item.icon === 'summaries'}
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="1.5"
d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-3 7h3m-3 4h3m-6-4h.01M9 16h.01"
/>
</svg>
{:else if item.icon === 'indepth'}
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="1.5"
d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253"
/>
</svg>
{:else if item.icon === 'saved'}
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="1.5"
d="M5 5a2 2 0 012-2h10a2 2 0 012 2v16l-7-3.5L5 21V5z"
/>
</svg>
{/if}
<span>{item.label}</span>
</a>
{/each}
</nav>
<!-- User Menu -->
<div class="p-4 border-t border-border">
<a
href="/profile"
class="flex items-center gap-3 px-3 py-2 rounded-lg text-text-secondary hover:bg-background-card-hover hover:text-text-primary transition-colors"
>
<div class="w-8 h-8 bg-primary/20 rounded-full flex items-center justify-center">
<span class="text-primary text-sm font-medium">
{authStore.user?.name?.[0]?.toUpperCase() ||
authStore.user?.email?.[0]?.toUpperCase() ||
'?'}
</span>
</div>
<div class="flex-1 min-w-0">
<p class="text-sm font-medium truncate">{authStore.user?.name || 'User'}</p>
<p class="text-xs text-text-muted truncate">{authStore.user?.email}</p>
</div>
</a>
<button
onclick={handleLogout}
class="w-full mt-2 flex items-center gap-3 px-3 py-2 rounded-lg text-text-secondary hover:bg-red-500/10 hover:text-red-400 transition-colors"
>
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="1.5"
d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1"
/>
</svg>
<span>Abmelden</span>
</button>
</div>
</aside>
<!-- Main Content -->
<main class="flex-1 overflow-auto">
{@render children()}
</main>
</div>
<main
class="main-content flex-1 transition-all duration-300 {isCollapsed
? ''
: isSidebarMode
? 'pl-[180px]'
: 'pt-20'}"
>
<div class="container mx-auto px-4 py-8">
{@render children()}
</div>
</main>
</div>
{/if}

View file

@ -0,0 +1,17 @@
<script lang="ts">
import { AppsPage } from '@manacore/shared-ui';
</script>
<svelte:head>
<title>Alle Apps - News</title>
</svelte:head>
<div class="apps-page-wrapper">
<AppsPage currentAppId="news" locale="de" title="Alle Apps" />
</div>
<style>
.apps-page-wrapper {
min-height: 100%;
}
</style>

View file

@ -1,39 +1,100 @@
<script lang="ts">
import Navigation from '$lib/components/Navigation.svelte';
import FloatingSidebar from '$lib/components/FloatingSidebar.svelte';
import MobileSidebar from '$lib/components/MobileSidebar.svelte';
import AccountSwitcher from '$lib/components/AccountSwitcher.svelte';
import WorkspaceSwitcher from '$lib/components/WorkspaceSwitcher.svelte';
import NotificationBell from '$lib/components/NotificationBell.svelte';
import { goto } from '$app/navigation';
import { page } from '$app/stores';
import { onMount } from 'svelte';
import { accountsStore } from '$lib/stores/accounts';
import { workspacesStore } from '$lib/stores/workspaces';
import { activeWorkspace } from '$lib/stores/activeWorkspace';
import type { LayoutData } from './$types';
import { PillNavigation } from '@manacore/shared-ui';
import type { PillNavItem } from '@manacore/shared-ui';
import { getPillAppItems } from '@manacore/shared-branding';
let { data, children }: { data: LayoutData; children: any } = $props();
let sidebarCollapsed = $state(false);
let mounted = $state(false);
let mobileMenuOpen = $state(false);
// App switcher items
const appItems = getPillAppItems('uload');
// User email for dropdown
let userEmail = $derived(data.user?.email);
// Navigation items for uload
const navItems: PillNavItem[] = [
{ href: '/', label: 'Dashboard', icon: 'home' },
{ href: '/links', label: 'Links', icon: 'link' },
{ href: '/analytics', label: 'Analytics', icon: 'chart' },
{ href: '/teams', label: 'Teams', icon: 'users' },
{ href: '/settings', label: 'Settings', icon: 'settings' },
];
let loading = $state(true);
let isSidebarMode = $state(false);
let isCollapsed = $state(false);
let isDark = $state(false);
// Navigation shortcuts (Ctrl+1-5)
const navRoutes = ['/', '/links', '/analytics', '/teams', '/settings'];
function handleKeydown(event: KeyboardEvent) {
const target = event.target as HTMLElement;
if (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.isContentEditable) {
return;
}
if ((event.ctrlKey || event.metaKey) && !event.shiftKey && !event.altKey) {
const num = parseInt(event.key);
if (num >= 1 && num <= 5) {
event.preventDefault();
const route = navRoutes[num - 1];
if (route) {
goto(route);
}
}
}
}
function handleModeChange(isSidebar: boolean) {
isSidebarMode = isSidebar;
if (typeof localStorage !== 'undefined') {
localStorage.setItem('uload-nav-sidebar', String(isSidebar));
}
}
function handleCollapsedChange(collapsed: boolean) {
isCollapsed = collapsed;
if (typeof localStorage !== 'undefined') {
localStorage.setItem('uload-nav-collapsed', String(collapsed));
}
}
function handleToggleTheme() {
isDark = !isDark;
document.documentElement.classList.toggle('dark', isDark);
if (typeof localStorage !== 'undefined') {
localStorage.setItem('uload-dark-mode', String(isDark));
}
}
async function handleLogout() {
// Clear local storage and redirect
if (typeof localStorage !== 'undefined') {
localStorage.removeItem('auth_token');
}
goto('/login');
}
// Watch for URL workspace parameter changes
$effect(() => {
const urlWorkspaceId = $page.url.searchParams.get('workspace');
if (urlWorkspaceId) {
// URL parameter takes precedence
activeWorkspace.initFromUrl(urlWorkspaceId);
}
});
onMount(() => {
mounted = true;
// Initialize both stores during migration
if (data.user) {
// Old accounts store for backwards compatibility
accountsStore.init(data.user, data.sharedAccounts || [], data.viewingAs);
// New workspaces store
workspacesStore.init(
data.user,
data.personalWorkspace,
@ -41,11 +102,9 @@
data.currentWorkspaceId
);
// Initialize active workspace from URL or localStorage
const urlWorkspaceId = $page.url.searchParams.get('workspace');
if (urlWorkspaceId) {
activeWorkspace.initFromUrl(urlWorkspaceId);
// Try to find workspace data
const workspace =
data.teamWorkspaces?.find((w) => w.id === urlWorkspaceId) ||
(data.personalWorkspace?.id === urlWorkspaceId ? data.personalWorkspace : null);
@ -55,93 +114,78 @@
}
}
if (typeof window !== 'undefined') {
const stored = localStorage.getItem('sidebar-collapsed');
if (stored !== null) {
sidebarCollapsed = stored === 'true';
// Restore nav mode from localStorage
if (typeof localStorage !== 'undefined') {
const savedSidebar = localStorage.getItem('uload-nav-sidebar');
if (savedSidebar === 'true') {
isSidebarMode = true;
}
const savedCollapsed = localStorage.getItem('uload-nav-collapsed');
if (savedCollapsed === 'true') {
isCollapsed = true;
}
const savedDark = localStorage.getItem('uload-dark-mode');
if (savedDark === 'true') {
isDark = true;
document.documentElement.classList.add('dark');
}
// Listen for storage changes to sync sidebar state
const handleStorageChange = () => {
const stored = localStorage.getItem('sidebar-collapsed');
if (stored !== null) {
sidebarCollapsed = stored === 'true';
}
};
window.addEventListener('storage', handleStorageChange);
return () => window.removeEventListener('storage', handleStorageChange);
}
loading = false;
});
</script>
<!-- Full screen background container -->
<div class="fixed inset-0 -z-10 bg-theme-background"></div>
<svelte:window onkeydown={handleKeydown} />
<!-- Floating Sidebar for authenticated users on desktop -->
<FloatingSidebar user={data.user} />
<!-- Mobile Sidebar (overlay) -->
<MobileSidebar user={data.user} open={mobileMenuOpen} onClose={() => (mobileMenuOpen = false)} />
<!-- Top Navigation Bar with Menu Button for mobile/tablet -->
{#if data.user}
<nav
class="bg-theme-surface/80 sticky top-0 z-30 border-b border-theme-border shadow-sm backdrop-blur-xl lg:hidden"
>
<div class="mx-auto max-w-7xl px-4 sm:px-6">
<div class="flex h-16 items-center justify-between">
<!-- Logo & Menu Button -->
<div class="flex items-center gap-3">
<button
onclick={() => (mobileMenuOpen = !mobileMenuOpen)}
class="rounded-lg p-2 text-theme-text transition-colors hover:bg-theme-surface-hover"
aria-label="Menu"
>
<svg class="h-6 w-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M4 6h16M4 12h16M4 18h16"
/>
</svg>
</button>
<a href="/" class="flex items-center space-x-2 transition-opacity hover:opacity-80">
<svg
class="h-8 w-8 text-theme-primary"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1"
/>
</svg>
<span class="text-xl font-bold text-theme-text">uload</span>
</a>
</div>
<!-- Notifications & Workspace Switcher -->
<div class="flex items-center gap-2">
<NotificationBell />
<WorkspaceSwitcher />
</div>
</div>
{#if 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-indigo-500 border-r-transparent"
></div>
<p class="text-gray-600 dark:text-gray-400">Laden...</p>
</div>
</nav>
{/if}
</div>
{:else}
<div class="flex min-h-screen flex-col">
<PillNavigation
items={navItems}
currentPath={$page.url.pathname}
appName="uload"
homeRoute="/"
onLogout={handleLogout}
onToggleTheme={handleToggleTheme}
{isDark}
{isSidebarMode}
onModeChange={handleModeChange}
{isCollapsed}
onCollapsedChange={handleCollapsedChange}
showThemeToggle={true}
primaryColor="#6366f1"
showAppSwitcher={true}
{appItems}
{userEmail}
settingsHref="/settings"
manaHref="/subscription"
profileHref="/profile"
allAppsHref="/apps"
>
{#snippet logo()}
<span class="text-xl">🔗</span>
<span class="pill-label font-bold">uload</span>
{/snippet}
</PillNavigation>
<!-- Main Content with responsive margin -->
<main
class="min-h-screen transition-all duration-300 {mounted && data.user
? sidebarCollapsed
? 'lg:pl-24'
: 'lg:pl-72'
: ''}"
>
{@render children?.()}
</main>
<main
class="main-content flex-1 transition-all duration-300 {isCollapsed
? ''
: isSidebarMode
? 'pl-[180px]'
: 'pt-20'}"
>
<div class="container mx-auto px-4 py-8">
{@render children?.()}
</div>
</main>
</div>
{/if}

View file

@ -0,0 +1,17 @@
<script lang="ts">
import { AppsPage } from '@manacore/shared-ui';
</script>
<svelte:head>
<title>Alle Apps - uload</title>
</svelte:head>
<div class="apps-page-wrapper">
<AppsPage currentAppId="uload" locale="de" title="Alle Apps" />
</div>
<style>
.apps-page-wrapper {
min-height: 100%;
}
</style>

View file

@ -5,10 +5,79 @@
import { authStore } from '$lib/stores/auth.svelte';
import { initWebSocket, cleanup, isConnected } from '$lib/stores/jobs';
import type { LayoutData } from './$types';
import { PillNavigation } from '@manacore/shared-ui';
import type { PillNavItem } from '@manacore/shared-ui';
import { getPillAppItems } from '@manacore/shared-branding';
let { children, data }: { children: any; data: LayoutData } = $props();
// App switcher items
const appItems = getPillAppItems('wisekeep');
// User email for dropdown
let userEmail = $derived(authStore.user?.email);
// Navigation items for Wisekeep
const navItems: PillNavItem[] = [
{ href: '/dashboard', label: 'Dashboard', icon: 'home' },
{ href: '/transcribe', label: 'Transcribe', icon: 'mic' },
{ href: '/transcripts', label: 'Transcripts', icon: 'document' },
{ href: '/playlists', label: 'Playlists', icon: 'list' },
{ href: '/settings', label: 'Settings', icon: 'settings' },
];
let isChecking = $state(true);
let isSidebarMode = $state(false);
let isCollapsed = $state(false);
let isDark = $state(false);
// Navigation shortcuts (Ctrl+1-5)
const navRoutes = ['/dashboard', '/transcribe', '/transcripts', '/playlists', '/settings'];
function handleKeydown(event: KeyboardEvent) {
const target = event.target as HTMLElement;
if (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.isContentEditable) {
return;
}
if ((event.ctrlKey || event.metaKey) && !event.shiftKey && !event.altKey) {
const num = parseInt(event.key);
if (num >= 1 && num <= 5) {
event.preventDefault();
const route = navRoutes[num - 1];
if (route) {
goto(route);
}
}
}
}
function handleModeChange(isSidebar: boolean) {
isSidebarMode = isSidebar;
if (typeof localStorage !== 'undefined') {
localStorage.setItem('wisekeep-nav-sidebar', String(isSidebar));
}
}
function handleCollapsedChange(collapsed: boolean) {
isCollapsed = collapsed;
if (typeof localStorage !== 'undefined') {
localStorage.setItem('wisekeep-nav-collapsed', String(collapsed));
}
}
function handleToggleTheme() {
isDark = !isDark;
document.documentElement.classList.toggle('dark', isDark);
if (typeof localStorage !== 'undefined') {
localStorage.setItem('wisekeep-dark-mode', String(isDark));
}
}
async function handleSignOut() {
await authStore.signOut();
goto('/login');
}
// Check auth on mount and redirect if not authenticated
onMount(async () => {
@ -27,6 +96,23 @@
shouldRedirect = true;
}
// Restore nav mode from localStorage
if (typeof localStorage !== 'undefined') {
const savedSidebar = localStorage.getItem('wisekeep-nav-sidebar');
if (savedSidebar === 'true') {
isSidebarMode = true;
}
const savedCollapsed = localStorage.getItem('wisekeep-nav-collapsed');
if (savedCollapsed === 'true') {
isCollapsed = true;
}
const savedDark = localStorage.getItem('wisekeep-dark-mode');
if (savedDark === 'true') {
isDark = true;
document.documentElement.classList.add('dark');
}
}
// Always set isChecking to false
isChecking = false;
@ -38,87 +124,59 @@
// Return cleanup function
return () => cleanup();
});
async function handleSignOut() {
await authStore.signOut();
goto('/login');
}
</script>
<svelte:window onkeydown={handleKeydown} />
{#if isChecking}
<!-- Loading state while checking auth -->
<div class="min-h-screen bg-gray-50 flex items-center justify-center">
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-purple-600"></div>
<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-purple-600 border-r-transparent"
></div>
<p class="text-gray-600 dark:text-gray-400">Laden...</p>
</div>
</div>
{:else}
<div class="min-h-screen flex flex-col">
<header class="bg-white shadow-sm border-b">
<div class="max-w-7xl mx-auto px-4 py-4 flex items-center justify-between">
<a href="/dashboard" class="text-xl font-bold text-purple-600">Wisekeep</a>
<nav class="flex items-center gap-6">
<a
href="/dashboard"
class="transition-colors {$page.url.pathname === '/dashboard'
? 'text-purple-600 font-medium'
: 'text-gray-600 hover:text-gray-900'}"
>
Dashboard
</a>
<a
href="/transcribe"
class="transition-colors {$page.url.pathname === '/transcribe'
? 'text-purple-600 font-medium'
: 'text-gray-600 hover:text-gray-900'}"
>
Transcribe
</a>
<a
href="/transcripts"
class="transition-colors {$page.url.pathname === '/transcripts'
? 'text-purple-600 font-medium'
: 'text-gray-600 hover:text-gray-900'}"
>
Transcripts
</a>
<a
href="/playlists"
class="transition-colors {$page.url.pathname === '/playlists'
? 'text-purple-600 font-medium'
: 'text-gray-600 hover:text-gray-900'}"
>
Playlists
</a>
</nav>
<div class="flex items-center gap-4">
<div class="flex items-center gap-2">
<span class="w-2 h-2 rounded-full {$isConnected ? 'bg-green-500' : 'bg-red-500'}"></span>
<span class="text-sm text-gray-500">
{$isConnected ? 'Connected' : 'Disconnected'}
</span>
</div>
{#if authStore.user}
<span class="text-sm text-gray-600 hidden sm:block">
{authStore.user.email}
</span>
{/if}
<button
onclick={handleSignOut}
class="px-3 py-2 text-sm font-medium text-gray-600 hover:bg-gray-100 rounded-lg transition-colors"
>
Abmelden
</button>
</div>
</div>
</header>
<div class="flex min-h-screen flex-col">
<PillNavigation
items={navItems}
currentPath={$page.url.pathname}
appName="Wisekeep"
homeRoute="/dashboard"
onLogout={handleSignOut}
onToggleTheme={handleToggleTheme}
{isDark}
{isSidebarMode}
onModeChange={handleModeChange}
{isCollapsed}
onCollapsedChange={handleCollapsedChange}
showThemeToggle={true}
primaryColor="#9333ea"
showAppSwitcher={true}
{appItems}
{userEmail}
settingsHref="/settings"
manaHref="/subscription"
profileHref="/profile"
allAppsHref="/apps"
>
{#snippet logo()}
<span class="text-xl">🧠</span>
<span class="pill-label font-bold">Wisekeep</span>
{/snippet}
</PillNavigation>
<main class="flex-1">
{@render children()}
<main
class="main-content flex-1 transition-all duration-300 {isCollapsed
? ''
: isSidebarMode
? 'pl-[180px]'
: 'pt-20'}"
>
<div class="container mx-auto px-4 py-8">
{@render children()}
</div>
</main>
<footer class="bg-gray-100 border-t py-4">
<div class="max-w-7xl mx-auto px-4 text-center text-sm text-gray-500">
Wisekeep - AI-powered wisdom extraction from video
</div>
</footer>
</div>
{/if}

View file

@ -0,0 +1,17 @@
<script lang="ts">
import { AppsPage } from '@manacore/shared-ui';
</script>
<svelte:head>
<title>Alle Apps - Wisekeep</title>
</svelte:head>
<div class="apps-page-wrapper">
<AppsPage currentAppId="wisekeep" locale="de" title="Alle Apps" />
</div>
<style>
.apps-page-wrapper {
min-height: 100%;
}
</style>