refactor(todo): reorganize routes into (app) layout group

Move all authenticated routes into (app) layout group for better
code organization and layout management. Add navigation store.

🤖 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-05 04:20:16 +01:00
parent 4a6d946f33
commit 862560f376
9 changed files with 264 additions and 244 deletions

View file

@ -0,0 +1,4 @@
import { writable } from 'svelte/store';
export const isSidebarMode = writable(false);
export const isNavCollapsed = writable(false);

View file

@ -0,0 +1,254 @@
<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 { authStore } from '$lib/stores/auth.svelte';
import { userSettings } from '$lib/stores/user-settings.svelte';
import { projectsStore } from '$lib/stores/projects.svelte';
import { labelsStore } from '$lib/stores/labels.svelte';
import { theme } from '$lib/stores/theme';
import {
isSidebarMode as sidebarModeStore,
isNavCollapsed as collapsedStore,
} from '$lib/stores/navigation';
import { THEME_DEFINITIONS } from '@manacore/shared-theme';
import { getLanguageDropdownItems, getCurrentLanguageLabel } from '@manacore/shared-i18n';
import { getPillAppItems } from '@manacore/shared-branding';
// App switcher items
const appItems = getPillAppItems('todo');
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) {
locale.set(newLocale);
}
let languageItems = $derived(
getLanguageDropdownItems(['de', 'en'], currentLocale, handleLocaleChange)
);
let currentLanguageLabel = $derived(getCurrentLanguageLabel(currentLocale));
// User email for user dropdown
let userEmail = $derived(authStore.user?.email || 'Menü');
// Navigation items for Todo
const navItems: PillNavItem[] = [
{ href: '/', label: 'Inbox', icon: 'inbox' },
{ href: '/today', label: 'Heute', icon: 'calendar-day' },
{ href: '/upcoming', label: 'Demnächst', icon: 'calendar' },
{ href: '/completed', label: 'Erledigt', icon: 'check-circle' },
{ href: '/settings', label: 'Einstellungen', icon: 'settings' },
{ href: '/feedback', label: 'Feedback', icon: 'chat' },
];
// Navigation shortcuts (Ctrl+1-6)
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('todo-nav-sidebar', String(isSidebar));
}
}
function handleCollapsedChange(collapsed: boolean) {
isCollapsed = collapsed;
collapsedStore.set(collapsed);
if (typeof localStorage !== 'undefined') {
localStorage.setItem('todo-nav-collapsed', String(collapsed));
}
}
function handleToggleTheme() {
theme.toggleMode();
}
function handleThemeModeChange(mode: 'light' | 'dark' | 'system') {
theme.setMode(mode);
}
async function handleLogout() {
await authStore.signOut();
projectsStore.clear();
labelsStore.clear();
goto('/login');
}
onMount(async () => {
// Redirect to login if not authenticated
if (!authStore.isAuthenticated) {
goto('/login');
return;
}
// Load data
await Promise.all([
projectsStore.fetchProjects(),
labelsStore.fetchLabels(),
userSettings.load(),
]);
// Redirect to start page if on root and a custom start page is set
const currentPath = window.location.pathname;
if (currentPath === '/' && userSettings.startPage && userSettings.startPage !== '/') {
goto(userSettings.startPage, { replaceState: true });
}
// Initialize sidebar mode from localStorage
const savedSidebar = localStorage.getItem('todo-nav-sidebar');
if (savedSidebar === 'true') {
isSidebarMode = true;
sidebarModeStore.set(true);
}
// Initialize collapsed state from localStorage
const savedCollapsed = localStorage.getItem('todo-nav-collapsed');
if (savedCollapsed === 'true') {
isCollapsed = true;
collapsedStore.set(true);
}
});
</script>
<svelte:window onkeydown={handleKeydown} />
<div class="layout-container">
<PillNavigation
items={navItems}
currentPath={$page.url.pathname}
appName="Todo"
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="#8b5cf6"
showAppSwitcher={true}
{appItems}
{userEmail}
settingsHref="/settings"
manaHref="/mana"
profileHref="/profile"
allAppsHref="/apps"
/>
<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 {
transition: all 300ms ease;
position: relative;
z-index: 0;
}
.main-content.floating-mode {
padding-top: 70px;
}
.main-content.sidebar-mode {
padding-left: 180px;
}
.content-wrapper {
max-width: 900px;
margin-left: auto;
margin-right: auto;
padding: 1rem;
position: relative;
z-index: 0;
}
@media (min-width: 640px) {
.content-wrapper {
padding: 1.5rem;
}
}
@media (min-width: 1024px) {
.content-wrapper {
padding: 2rem;
}
}
</style>

View file

@ -1,133 +1,12 @@
<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 { authStore } from '$lib/stores/auth.svelte';
import { userSettings } from '$lib/stores/user-settings.svelte';
import { projectsStore } from '$lib/stores/projects.svelte';
import { labelsStore } from '$lib/stores/labels.svelte';
import { theme } from '$lib/stores/theme';
import { THEME_DEFINITIONS } from '@manacore/shared-theme';
import { getLanguageDropdownItems, getCurrentLanguageLabel } from '@manacore/shared-i18n';
import { getPillAppItems } from '@manacore/shared-branding';
import '../app.css';
// App switcher items
const appItems = getPillAppItems('todo');
import { onMount } from 'svelte';
import { theme } from '$lib/stores/theme';
import { authStore } from '$lib/stores/auth.svelte';
let { children } = $props();
let loading = $state(true);
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) {
locale.set(newLocale);
}
let languageItems = $derived(
getLanguageDropdownItems(['de', 'en'], currentLocale, handleLocaleChange)
);
let currentLanguageLabel = $derived(getCurrentLanguageLabel(currentLocale));
// User email for user dropdown
let userEmail = $derived(authStore.user?.email || 'Menü');
// Check if current route is an auth route (no navigation needed)
let isAuthRoute = $derived(
$page.url.pathname.startsWith('/login') ||
$page.url.pathname.startsWith('/register') ||
$page.url.pathname.startsWith('/forgot-password')
);
// Navigation items for Todo
const navItems: PillNavItem[] = [
{ href: '/', label: 'Inbox', icon: 'inbox' },
{ href: '/today', label: 'Heute', icon: 'calendar-day' },
{ href: '/upcoming', label: 'Demnächst', icon: 'calendar' },
{ href: '/completed', label: 'Erledigt', icon: 'check-circle' },
{ href: '/settings', label: 'Einstellungen', icon: 'settings' },
{ href: '/feedback', label: 'Feedback', icon: 'chat' },
];
// Navigation shortcuts (Ctrl+1-5)
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;
if (typeof localStorage !== 'undefined') {
localStorage.setItem('todo-nav-sidebar', String(isSidebar));
}
}
function handleCollapsedChange(collapsed: boolean) {
isCollapsed = collapsed;
if (typeof localStorage !== 'undefined') {
localStorage.setItem('todo-nav-collapsed', String(collapsed));
}
}
function handleToggleTheme() {
theme.toggleMode();
}
function handleThemeModeChange(mode: 'light' | 'dark' | 'system') {
theme.setMode(mode);
}
async function handleLogout() {
await authStore.signOut();
projectsStore.clear();
labelsStore.clear();
goto('/login');
}
onMount(async () => {
// Initialize theme
@ -136,43 +15,11 @@
// Initialize auth
await authStore.initialize();
// Load data if authenticated
if (authStore.isAuthenticated) {
await Promise.all([
projectsStore.fetchProjects(),
labelsStore.fetchLabels(),
userSettings.load(),
]);
// Redirect to start page if on root and a custom start page is set
const currentPath = window.location.pathname;
if (currentPath === '/' && userSettings.startPage && userSettings.startPage !== '/') {
goto(userSettings.startPage, { replaceState: true });
}
}
// Initialize sidebar mode from localStorage
const savedSidebar = localStorage.getItem('todo-nav-sidebar');
if (savedSidebar === 'true') {
isSidebarMode = true;
}
// Initialize collapsed state from localStorage
const savedCollapsed = localStorage.getItem('todo-nav-collapsed');
if (savedCollapsed === 'true') {
isCollapsed = true;
}
loading = false;
});
</script>
<svelte:window onkeydown={handleKeydown} />
{#if isAuthRoute}
<!-- Auth routes: no navigation, just render content -->
{@render children()}
{:else if loading}
{#if loading}
<div class="flex min-h-screen items-center justify-center bg-background">
<div class="text-center">
<div
@ -182,92 +29,7 @@
</div>
</div>
{:else}
<div class="layout-container">
<PillNavigation
items={navItems}
currentPath={$page.url.pathname}
appName="Todo"
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="#8b5cf6"
showAppSwitcher={true}
{appItems}
{userEmail}
settingsHref="/settings"
manaHref="/mana"
profileHref="/profile"
allAppsHref="/apps"
/>
<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 class="min-h-screen bg-background text-foreground">
{@render children()}
</div>
{/if}
<style>
.layout-container {
display: flex;
flex-direction: column;
min-height: 100vh;
}
.main-content {
transition: all 300ms ease;
position: relative;
z-index: 0;
}
.main-content.floating-mode {
padding-top: 70px;
}
.main-content.sidebar-mode {
padding-left: 180px;
}
.content-wrapper {
max-width: 900px;
margin-left: auto;
margin-right: auto;
padding: 1rem;
position: relative;
z-index: 0;
}
@media (min-width: 640px) {
.content-wrapper {
padding: 1.5rem;
}
}
@media (min-width: 1024px) {
.content-wrapper {
padding: 2rem;
}
}
</style>