mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-20 08:53:39 +02:00
refactor(finance): reorganize routes into (app) layout group
Move all authenticated routes into (app) layout group for better code organization and layout management. Add missing stores. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
4a41b45efb
commit
ebec369a57
14 changed files with 346 additions and 173 deletions
|
|
@ -5,3 +5,6 @@ export { transactionsStore } from './transactions.svelte';
|
|||
export { budgetsStore } from './budgets.svelte';
|
||||
export { dashboardStore } from './dashboard.svelte';
|
||||
export { settingsStore } from './settings.svelte';
|
||||
export { theme } from './theme';
|
||||
export { isSidebarMode, isNavCollapsed } from './navigation';
|
||||
export { userSettings } from './user-settings.svelte';
|
||||
|
|
|
|||
4
apps/finance/apps/web/src/lib/stores/navigation.ts
Normal file
4
apps/finance/apps/web/src/lib/stores/navigation.ts
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
import { writable } from 'svelte/store';
|
||||
|
||||
export const isSidebarMode = writable(false);
|
||||
export const isNavCollapsed = writable(false);
|
||||
6
apps/finance/apps/web/src/lib/stores/theme.ts
Normal file
6
apps/finance/apps/web/src/lib/stores/theme.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
import { createTheme, type ThemeStore } from '@manacore/shared-theme';
|
||||
|
||||
export const theme: ThemeStore = createTheme({
|
||||
storagePrefix: 'finance',
|
||||
variants: ['default', 'blue', 'green', 'purple', 'orange', 'pink'],
|
||||
});
|
||||
66
apps/finance/apps/web/src/lib/stores/user-settings.svelte.ts
Normal file
66
apps/finance/apps/web/src/lib/stores/user-settings.svelte.ts
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
/**
|
||||
* User Settings Store for Finance
|
||||
* Manages user preferences and settings
|
||||
*/
|
||||
|
||||
interface UserSettings {
|
||||
currency: string;
|
||||
locale: string;
|
||||
dateFormat: string;
|
||||
nav: {
|
||||
desktopPosition: 'left' | 'center' | 'right';
|
||||
};
|
||||
}
|
||||
|
||||
const defaultSettings: UserSettings = {
|
||||
currency: 'EUR',
|
||||
locale: 'de',
|
||||
dateFormat: 'dd.MM.yyyy',
|
||||
nav: {
|
||||
desktopPosition: 'center',
|
||||
},
|
||||
};
|
||||
|
||||
let settings = $state<UserSettings>({ ...defaultSettings });
|
||||
let isLoaded = $state(false);
|
||||
|
||||
export const userSettings = {
|
||||
get currency() {
|
||||
return settings.currency;
|
||||
},
|
||||
get locale() {
|
||||
return settings.locale;
|
||||
},
|
||||
get dateFormat() {
|
||||
return settings.dateFormat;
|
||||
},
|
||||
get nav() {
|
||||
return settings.nav;
|
||||
},
|
||||
get isLoaded() {
|
||||
return isLoaded;
|
||||
},
|
||||
|
||||
async load() {
|
||||
if (typeof window === 'undefined') return;
|
||||
|
||||
// Load from localStorage
|
||||
const saved = localStorage.getItem('finance-user-settings');
|
||||
if (saved) {
|
||||
try {
|
||||
const parsed = JSON.parse(saved);
|
||||
settings = { ...defaultSettings, ...parsed };
|
||||
} catch {
|
||||
// Ignore parse errors
|
||||
}
|
||||
}
|
||||
isLoaded = true;
|
||||
},
|
||||
|
||||
update(updates: Partial<UserSettings>) {
|
||||
settings = { ...settings, ...updates };
|
||||
if (typeof localStorage !== 'undefined') {
|
||||
localStorage.setItem('finance-user-settings', JSON.stringify(settings));
|
||||
}
|
||||
},
|
||||
};
|
||||
246
apps/finance/apps/web/src/routes/(app)/+layout.svelte
Normal file
246
apps/finance/apps/web/src/routes/(app)/+layout.svelte
Normal file
|
|
@ -0,0 +1,246 @@
|
|||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import { page } from '$app/stores';
|
||||
import { onMount } from 'svelte';
|
||||
import { locale } from 'svelte-i18n';
|
||||
import { PillNavigation } from '@manacore/shared-ui';
|
||||
import type { PillNavItem, PillDropdownItem } from '@manacore/shared-ui';
|
||||
import { THEME_DEFINITIONS } from '@manacore/shared-theme';
|
||||
import { getLanguageDropdownItems, getCurrentLanguageLabel } from '@manacore/shared-i18n';
|
||||
import { getPillAppItems } from '@manacore/shared-branding';
|
||||
import { setLocale, supportedLocales } from '$lib/i18n';
|
||||
import {
|
||||
authStore,
|
||||
theme,
|
||||
isSidebarMode as sidebarModeStore,
|
||||
isNavCollapsed as collapsedStore,
|
||||
userSettings,
|
||||
} from '$lib/stores';
|
||||
|
||||
// App switcher items
|
||||
const appItems = getPillAppItems('finance');
|
||||
|
||||
let { children } = $props();
|
||||
|
||||
let isSidebarMode = $state(false);
|
||||
let isCollapsed = $state(false);
|
||||
|
||||
// Use theme store's isDark directly
|
||||
let isDark = $derived(theme.isDark);
|
||||
|
||||
// Theme variant dropdown items
|
||||
let themeVariantItems = $derived<PillDropdownItem[]>([
|
||||
...theme.variants.map((variant) => ({
|
||||
id: variant,
|
||||
label: THEME_DEFINITIONS[variant].label,
|
||||
icon: THEME_DEFINITIONS[variant].icon,
|
||||
onClick: () => theme.setVariant(variant),
|
||||
active: theme.variant === variant,
|
||||
})),
|
||||
{
|
||||
id: 'all-themes',
|
||||
label: 'Alle Themes',
|
||||
icon: 'palette',
|
||||
onClick: () => goto('/themes'),
|
||||
active: false,
|
||||
},
|
||||
]);
|
||||
|
||||
// Current theme variant label
|
||||
let currentThemeVariantLabel = $derived(THEME_DEFINITIONS[theme.variant].label);
|
||||
|
||||
// Language selector items
|
||||
let currentLocale = $derived($locale || 'de');
|
||||
function handleLocaleChange(newLocale: string) {
|
||||
setLocale(newLocale as any);
|
||||
}
|
||||
let languageItems = $derived(
|
||||
getLanguageDropdownItems(supportedLocales, currentLocale, handleLocaleChange)
|
||||
);
|
||||
let currentLanguageLabel = $derived(getCurrentLanguageLabel(currentLocale));
|
||||
|
||||
// User email for user dropdown
|
||||
let userEmail = $derived(authStore.user?.email || 'Menü');
|
||||
|
||||
// Navigation items for Finance
|
||||
const navItems: PillNavItem[] = [
|
||||
{ href: '/', label: 'Dashboard', icon: 'home' },
|
||||
{ href: '/transactions', label: 'Transaktionen', icon: 'list' },
|
||||
{ href: '/accounts', label: 'Konten', icon: 'wallet' },
|
||||
{ href: '/categories', label: 'Kategorien', icon: 'tag' },
|
||||
{ href: '/budgets', label: 'Budgets', icon: 'pie-chart' },
|
||||
{ href: '/reports', label: 'Berichte', icon: 'bar-chart' },
|
||||
{ href: '/feedback', label: 'Feedback', icon: 'chat' },
|
||||
];
|
||||
|
||||
// Navigation shortcuts (Ctrl+1-7)
|
||||
const navRoutes = navItems.map((item) => item.href);
|
||||
|
||||
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 <= navRoutes.length) {
|
||||
event.preventDefault();
|
||||
const route = navRoutes[num - 1];
|
||||
if (route) {
|
||||
goto(route);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handleModeChange(isSidebar: boolean) {
|
||||
isSidebarMode = isSidebar;
|
||||
sidebarModeStore.set(isSidebar);
|
||||
if (typeof localStorage !== 'undefined') {
|
||||
localStorage.setItem('finance-nav-sidebar', String(isSidebar));
|
||||
}
|
||||
}
|
||||
|
||||
function handleCollapsedChange(collapsed: boolean) {
|
||||
isCollapsed = collapsed;
|
||||
collapsedStore.set(collapsed);
|
||||
if (typeof localStorage !== 'undefined') {
|
||||
localStorage.setItem('finance-nav-collapsed', String(collapsed));
|
||||
}
|
||||
}
|
||||
|
||||
function handleToggleTheme() {
|
||||
theme.toggleMode();
|
||||
}
|
||||
|
||||
function handleThemeModeChange(mode: 'light' | 'dark' | 'system') {
|
||||
theme.setMode(mode);
|
||||
}
|
||||
|
||||
async function handleLogout() {
|
||||
authStore.logout();
|
||||
goto('/login');
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
// Redirect to login if not authenticated
|
||||
if (!authStore.isAuthenticated) {
|
||||
goto('/login');
|
||||
return;
|
||||
}
|
||||
|
||||
// Load user settings
|
||||
await userSettings.load();
|
||||
|
||||
// Initialize sidebar mode from localStorage
|
||||
const savedSidebar = localStorage.getItem('finance-nav-sidebar');
|
||||
if (savedSidebar === 'true') {
|
||||
isSidebarMode = true;
|
||||
sidebarModeStore.set(true);
|
||||
}
|
||||
|
||||
// Initialize collapsed state from localStorage
|
||||
const savedCollapsed = localStorage.getItem('finance-nav-collapsed');
|
||||
if (savedCollapsed === 'true') {
|
||||
isCollapsed = true;
|
||||
collapsedStore.set(true);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<svelte:window onkeydown={handleKeydown} />
|
||||
|
||||
<!-- Navigation Layout -->
|
||||
<div class="layout-container">
|
||||
<!-- Floating/Sidebar Pill Navigation -->
|
||||
<PillNavigation
|
||||
items={navItems}
|
||||
currentPath={$page.url.pathname}
|
||||
appName="Finance"
|
||||
homeRoute="/"
|
||||
onToggleTheme={handleToggleTheme}
|
||||
{isDark}
|
||||
{isSidebarMode}
|
||||
onModeChange={handleModeChange}
|
||||
{isCollapsed}
|
||||
onCollapsedChange={handleCollapsedChange}
|
||||
desktopPosition={userSettings.nav.desktopPosition}
|
||||
showThemeToggle={true}
|
||||
showThemeVariants={true}
|
||||
{themeVariantItems}
|
||||
{currentThemeVariantLabel}
|
||||
themeMode={theme.mode}
|
||||
onThemeModeChange={handleThemeModeChange}
|
||||
showLanguageSwitcher={true}
|
||||
{languageItems}
|
||||
{currentLanguageLabel}
|
||||
showLogout={authStore.isAuthenticated}
|
||||
onLogout={handleLogout}
|
||||
loginHref="/login"
|
||||
primaryColor="#10b981"
|
||||
showAppSwitcher={true}
|
||||
{appItems}
|
||||
{userEmail}
|
||||
settingsHref="/settings"
|
||||
manaHref="/mana"
|
||||
profileHref="/profile"
|
||||
allAppsHref="/apps"
|
||||
/>
|
||||
|
||||
<!-- Main Content with dynamic padding based on nav mode -->
|
||||
<main
|
||||
class="main-content bg-background"
|
||||
class:sidebar-mode={isSidebarMode && !isCollapsed}
|
||||
class:floating-mode={!isSidebarMode && !isCollapsed}
|
||||
>
|
||||
<div class="content-wrapper">
|
||||
{@render children()}
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.layout-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.main-content {
|
||||
flex: 1;
|
||||
transition: all 300ms ease;
|
||||
}
|
||||
|
||||
/* Floating nav mode - add top padding for fixed nav */
|
||||
.main-content.floating-mode {
|
||||
padding-top: 100px;
|
||||
}
|
||||
|
||||
/* Sidebar mode - add left padding for sidebar nav */
|
||||
.main-content.sidebar-mode {
|
||||
padding-left: 180px;
|
||||
}
|
||||
|
||||
.content-wrapper {
|
||||
max-width: 80rem;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
padding: 2rem 1rem;
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
.content-wrapper {
|
||||
padding-left: 1.5rem;
|
||||
padding-right: 1.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
.content-wrapper {
|
||||
padding-left: 2rem;
|
||||
padding-right: 2rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -2,186 +2,34 @@
|
|||
import '../app.css';
|
||||
import '$lib/i18n';
|
||||
import { onMount } from 'svelte';
|
||||
import { page } from '$app/stores';
|
||||
import { authStore } from '$lib/stores';
|
||||
import AppSlider from '$lib/components/AppSlider.svelte';
|
||||
import LanguageSelector from '$lib/components/LanguageSelector.svelte';
|
||||
import { authStore, theme } from '$lib/stores';
|
||||
|
||||
let { children } = $props();
|
||||
|
||||
let isAppSliderOpen = $state(false);
|
||||
let isDark = $state(false);
|
||||
let loading = $state(true);
|
||||
|
||||
const navItems = [
|
||||
{ href: '/', label: 'Dashboard', icon: 'home' },
|
||||
{ href: '/transactions', label: 'Transaktionen', icon: 'list' },
|
||||
{ href: '/accounts', label: 'Konten', icon: 'wallet' },
|
||||
{ href: '/categories', label: 'Kategorien', icon: 'tag' },
|
||||
{ href: '/budgets', label: 'Budgets', icon: 'pie-chart' },
|
||||
{ href: '/reports', label: 'Berichte', icon: 'bar-chart' },
|
||||
{ href: '/settings', label: 'Einstellungen', icon: 'settings' },
|
||||
];
|
||||
onMount(async () => {
|
||||
// Initialize theme
|
||||
theme.initialize();
|
||||
|
||||
onMount(() => {
|
||||
authStore.init();
|
||||
// Check for dark mode preference
|
||||
isDark =
|
||||
document.documentElement.classList.contains('dark') ||
|
||||
(!localStorage.getItem('theme') && window.matchMedia('(prefers-color-scheme: dark)').matches);
|
||||
if (isDark) {
|
||||
document.documentElement.classList.add('dark');
|
||||
}
|
||||
// Initialize auth
|
||||
await authStore.init();
|
||||
|
||||
loading = false;
|
||||
});
|
||||
|
||||
function toggleTheme() {
|
||||
isDark = !isDark;
|
||||
if (isDark) {
|
||||
document.documentElement.classList.add('dark');
|
||||
localStorage.setItem('theme', 'dark');
|
||||
} else {
|
||||
document.documentElement.classList.remove('dark');
|
||||
localStorage.setItem('theme', 'light');
|
||||
}
|
||||
}
|
||||
|
||||
function isActive(href: string) {
|
||||
if (href === '/') {
|
||||
return $page.url.pathname === '/';
|
||||
}
|
||||
return $page.url.pathname.startsWith(href);
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="min-h-screen bg-background text-foreground">
|
||||
<!-- Header -->
|
||||
<header class="sticky top-0 z-50 border-b border-border bg-card">
|
||||
<div class="container mx-auto flex h-16 items-center justify-between px-4">
|
||||
<div class="flex items-center gap-4">
|
||||
<button
|
||||
onclick={() => (isAppSliderOpen = true)}
|
||||
class="flex items-center gap-2 rounded-lg p-2 hover:bg-accent"
|
||||
aria-label="Open app menu"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<rect x="3" y="3" width="7" height="7" />
|
||||
<rect x="14" y="3" width="7" height="7" />
|
||||
<rect x="14" y="14" width="7" height="7" />
|
||||
<rect x="3" y="14" width="7" height="7" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<a href="/" class="flex items-center gap-2 font-semibold">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
class="text-primary"
|
||||
>
|
||||
<line x1="12" y1="1" x2="12" y2="23" />
|
||||
<path d="M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6" />
|
||||
</svg>
|
||||
<span>Finance</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<nav class="hidden md:flex items-center gap-1">
|
||||
{#each navItems as item}
|
||||
<a
|
||||
href={item.href}
|
||||
class="px-3 py-2 rounded-md text-sm font-medium transition-colors {isActive(item.href)
|
||||
? 'bg-primary text-primary-foreground'
|
||||
: 'text-muted-foreground hover:bg-accent hover:text-foreground'}"
|
||||
>
|
||||
{item.label}
|
||||
</a>
|
||||
{/each}
|
||||
</nav>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<LanguageSelector />
|
||||
<button
|
||||
onclick={toggleTheme}
|
||||
class="rounded-lg p-2 hover:bg-accent"
|
||||
aria-label="Toggle theme"
|
||||
>
|
||||
{#if isDark}
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<circle cx="12" cy="12" r="5"></circle>
|
||||
<line x1="12" y1="1" x2="12" y2="3"></line>
|
||||
<line x1="12" y1="21" x2="12" y2="23"></line>
|
||||
<line x1="4.22" y1="4.22" x2="5.64" y2="5.64"></line>
|
||||
<line x1="18.36" y1="18.36" x2="19.78" y2="19.78"></line>
|
||||
<line x1="1" y1="12" x2="3" y2="12"></line>
|
||||
<line x1="21" y1="12" x2="23" y2="12"></line>
|
||||
<line x1="4.22" y1="19.78" x2="5.64" y2="18.36"></line>
|
||||
<line x1="18.36" y1="5.64" x2="19.78" y2="4.22"></line>
|
||||
</svg>
|
||||
{:else}
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path>
|
||||
</svg>
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
{#if loading}
|
||||
<div class="flex min-h-screen items-center justify-center bg-background">
|
||||
<div class="text-center">
|
||||
<div
|
||||
class="mb-4 inline-block h-12 w-12 animate-spin rounded-full border-4 border-solid border-primary border-r-transparent"
|
||||
></div>
|
||||
<p class="text-muted-foreground">Laden...</p>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Mobile Navigation -->
|
||||
<nav class="md:hidden fixed bottom-0 left-0 right-0 z-50 border-t border-border bg-card">
|
||||
<div class="flex justify-around py-2">
|
||||
{#each navItems.slice(0, 5) as item}
|
||||
<a
|
||||
href={item.href}
|
||||
class="flex flex-col items-center p-2 text-xs {isActive(item.href)
|
||||
? 'text-primary'
|
||||
: 'text-muted-foreground'}"
|
||||
>
|
||||
<span class="mb-1">{item.label}</span>
|
||||
</a>
|
||||
{/each}
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- Main Content -->
|
||||
<main class="container mx-auto px-4 py-6 pb-20 md:pb-6">
|
||||
</div>
|
||||
{:else}
|
||||
<div class="min-h-screen bg-background text-foreground">
|
||||
{@render children()}
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<AppSlider bind:isOpen={isAppSliderOpen} />
|
||||
</div>
|
||||
{/if}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue