mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 19:41:09 +02:00
refactor(mail): 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
ebec369a57
commit
2f7450b5af
11 changed files with 319 additions and 222 deletions
4
apps/mail/apps/web/src/lib/stores/navigation.ts
Normal file
4
apps/mail/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/mail/apps/web/src/lib/stores/theme.ts
Normal file
6
apps/mail/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: 'mail',
|
||||
variants: ['default', 'ocean', 'blue', 'purple', 'green', 'orange'],
|
||||
});
|
||||
51
apps/mail/apps/web/src/lib/stores/user-settings.svelte.ts
Normal file
51
apps/mail/apps/web/src/lib/stores/user-settings.svelte.ts
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
/**
|
||||
* User Settings Store for Mail
|
||||
* Manages user preferences and settings
|
||||
*/
|
||||
|
||||
interface UserSettings {
|
||||
nav: {
|
||||
desktopPosition: 'left' | 'center' | 'right';
|
||||
};
|
||||
}
|
||||
|
||||
const defaultSettings: UserSettings = {
|
||||
nav: {
|
||||
desktopPosition: 'center',
|
||||
},
|
||||
};
|
||||
|
||||
let settings = $state<UserSettings>({ ...defaultSettings });
|
||||
let isLoaded = $state(false);
|
||||
|
||||
export const userSettings = {
|
||||
get nav() {
|
||||
return settings.nav;
|
||||
},
|
||||
get isLoaded() {
|
||||
return isLoaded;
|
||||
},
|
||||
|
||||
async load() {
|
||||
if (typeof window === 'undefined') return;
|
||||
|
||||
// Load from localStorage
|
||||
const saved = localStorage.getItem('mail-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('mail-user-settings', JSON.stringify(settings));
|
||||
}
|
||||
},
|
||||
};
|
||||
249
apps/mail/apps/web/src/routes/(app)/+layout.svelte
Normal file
249
apps/mail/apps/web/src/routes/(app)/+layout.svelte
Normal file
|
|
@ -0,0 +1,249 @@
|
|||
<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 } from '$lib/stores/auth.svelte';
|
||||
import { accountsStore } from '$lib/stores/accounts.svelte';
|
||||
import { foldersStore } from '$lib/stores/folders.svelte';
|
||||
import { theme } from '$lib/stores/theme';
|
||||
import {
|
||||
isSidebarMode as sidebarModeStore,
|
||||
isNavCollapsed as collapsedStore,
|
||||
} from '$lib/stores/navigation';
|
||||
import { userSettings } from '$lib/stores/user-settings.svelte';
|
||||
|
||||
let { children } = $props();
|
||||
|
||||
let isSidebarMode = $state(false);
|
||||
let isCollapsed = $state(false);
|
||||
|
||||
// App switcher items
|
||||
const appItems = getPillAppItems('mail');
|
||||
|
||||
// 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 Mail
|
||||
const navItems: PillNavItem[] = [
|
||||
{ href: '/', label: 'Inbox', icon: 'inbox' },
|
||||
{ href: '/sent', label: 'Gesendet', icon: 'send' },
|
||||
{ href: '/drafts', label: 'Entwürfe', icon: 'file' },
|
||||
{ href: '/starred', label: 'Markiert', icon: 'star' },
|
||||
{ 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('mail-nav-sidebar', String(isSidebar));
|
||||
}
|
||||
}
|
||||
|
||||
function handleCollapsedChange(collapsed: boolean) {
|
||||
isCollapsed = collapsed;
|
||||
collapsedStore.set(collapsed);
|
||||
if (typeof localStorage !== 'undefined') {
|
||||
localStorage.setItem('mail-nav-collapsed', String(collapsed));
|
||||
}
|
||||
}
|
||||
|
||||
function handleToggleTheme() {
|
||||
theme.toggleMode();
|
||||
}
|
||||
|
||||
function handleThemeModeChange(mode: 'light' | 'dark' | 'system') {
|
||||
theme.setMode(mode);
|
||||
}
|
||||
|
||||
async function handleLogout() {
|
||||
await authStore.signOut();
|
||||
goto('/login');
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
// Redirect to login if not authenticated
|
||||
if (!authStore.isAuthenticated) {
|
||||
goto('/login');
|
||||
return;
|
||||
}
|
||||
|
||||
// Load user settings
|
||||
await userSettings.load();
|
||||
|
||||
// Load data
|
||||
await accountsStore.fetchAccounts();
|
||||
if (accountsStore.selectedAccountId) {
|
||||
await foldersStore.fetchFolders(accountsStore.selectedAccountId);
|
||||
}
|
||||
|
||||
// Initialize sidebar mode from localStorage
|
||||
const savedSidebar = localStorage.getItem('mail-nav-sidebar');
|
||||
if (savedSidebar === 'true') {
|
||||
isSidebarMode = true;
|
||||
sidebarModeStore.set(true);
|
||||
}
|
||||
|
||||
// Initialize collapsed state from localStorage
|
||||
const savedCollapsed = localStorage.getItem('mail-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="Mail"
|
||||
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="#6366f1"
|
||||
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: 100%;
|
||||
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>
|
||||
|
|
@ -1,249 +1,36 @@
|
|||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import { page } from '$app/stores';
|
||||
import { onMount } from 'svelte';
|
||||
import { PillNavigation } from '@manacore/shared-ui';
|
||||
import type { PillNavItem, PillDropdownItem } from '@manacore/shared-ui';
|
||||
import { THEME_DEFINITIONS, type ThemeVariant } from '@manacore/shared-theme';
|
||||
import { getPillAppItems } from '@manacore/shared-branding';
|
||||
import { authStore } from '$lib/stores/auth.svelte';
|
||||
import { accountsStore } from '$lib/stores/accounts.svelte';
|
||||
import { foldersStore } from '$lib/stores/folders.svelte';
|
||||
import '$lib/i18n';
|
||||
import '../app.css';
|
||||
import '$lib/i18n';
|
||||
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 isDark = $state(false);
|
||||
let themeMode = $state<'light' | 'dark' | 'system'>('system');
|
||||
let themeVariant = $state<ThemeVariant>('ocean');
|
||||
let isSidebarMode = $state(false);
|
||||
let isCollapsed = $state(false);
|
||||
|
||||
// App switcher items
|
||||
const appItems = getPillAppItems('mail');
|
||||
|
||||
// Theme variant dropdown items
|
||||
let themeVariantItems = $derived<PillDropdownItem[]>(
|
||||
Object.entries(THEME_DEFINITIONS).map(([key, def]) => ({
|
||||
id: key,
|
||||
label: def.label,
|
||||
icon: def.icon,
|
||||
onClick: () => setThemeVariant(key as ThemeVariant),
|
||||
active: themeVariant === key,
|
||||
}))
|
||||
);
|
||||
|
||||
let currentThemeVariantLabel = $derived(THEME_DEFINITIONS[themeVariant]?.label || 'Ocean');
|
||||
|
||||
// User email for user dropdown
|
||||
let userEmail = $derived(authStore.user?.email || 'Menu');
|
||||
|
||||
// Check if current route is an auth route
|
||||
let isAuthRoute = $derived(
|
||||
$page.url.pathname.startsWith('/login') ||
|
||||
$page.url.pathname.startsWith('/register') ||
|
||||
$page.url.pathname.startsWith('/forgot-password')
|
||||
);
|
||||
|
||||
// Navigation items for Mail
|
||||
const navItems: PillNavItem[] = [
|
||||
{ href: '/', label: 'Inbox', icon: 'inbox' },
|
||||
{ href: '/sent', label: 'Sent', icon: 'send' },
|
||||
{ href: '/drafts', label: 'Drafts', icon: 'file' },
|
||||
{ href: '/starred', label: 'Starred', icon: 'star' },
|
||||
{ href: '/settings', label: 'Settings', icon: 'settings' },
|
||||
];
|
||||
|
||||
function handleModeChange(isSidebar: boolean) {
|
||||
isSidebarMode = isSidebar;
|
||||
if (typeof localStorage !== 'undefined') {
|
||||
localStorage.setItem('mail-nav-sidebar', String(isSidebar));
|
||||
}
|
||||
}
|
||||
|
||||
function handleCollapsedChange(collapsed: boolean) {
|
||||
isCollapsed = collapsed;
|
||||
if (typeof localStorage !== 'undefined') {
|
||||
localStorage.setItem('mail-nav-collapsed', String(collapsed));
|
||||
}
|
||||
}
|
||||
|
||||
function handleToggleTheme() {
|
||||
isDark = !isDark;
|
||||
if (typeof document !== 'undefined') {
|
||||
document.documentElement.classList.toggle('dark', isDark);
|
||||
localStorage.setItem('mail-theme-dark', String(isDark));
|
||||
}
|
||||
}
|
||||
|
||||
function handleThemeModeChange(mode: 'light' | 'dark' | 'system') {
|
||||
themeMode = mode;
|
||||
if (typeof localStorage !== 'undefined') {
|
||||
localStorage.setItem('mail-theme-mode', mode);
|
||||
}
|
||||
updateTheme();
|
||||
}
|
||||
|
||||
function setThemeVariant(variant: ThemeVariant) {
|
||||
themeVariant = variant;
|
||||
if (typeof document !== 'undefined') {
|
||||
document.documentElement.setAttribute('data-theme', variant);
|
||||
localStorage.setItem('mail-theme-variant', variant);
|
||||
}
|
||||
}
|
||||
|
||||
function updateTheme() {
|
||||
if (typeof window === 'undefined') return;
|
||||
|
||||
let shouldBeDark = false;
|
||||
if (themeMode === 'dark') {
|
||||
shouldBeDark = true;
|
||||
} else if (themeMode === 'system') {
|
||||
shouldBeDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
}
|
||||
|
||||
isDark = shouldBeDark;
|
||||
document.documentElement.classList.toggle('dark', isDark);
|
||||
}
|
||||
|
||||
async function handleLogout() {
|
||||
await authStore.signOut();
|
||||
goto('/login');
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
// Initialize theme
|
||||
const savedMode = localStorage.getItem('mail-theme-mode') as 'light' | 'dark' | 'system' | null;
|
||||
if (savedMode) themeMode = savedMode;
|
||||
|
||||
const savedVariant = localStorage.getItem('mail-theme-variant') as ThemeVariant | null;
|
||||
if (savedVariant && savedVariant in THEME_DEFINITIONS) {
|
||||
themeVariant = savedVariant;
|
||||
document.documentElement.setAttribute('data-theme', savedVariant);
|
||||
}
|
||||
|
||||
updateTheme();
|
||||
theme.initialize();
|
||||
|
||||
// Initialize auth
|
||||
await authStore.initialize();
|
||||
|
||||
// Load data if authenticated
|
||||
if (authStore.isAuthenticated) {
|
||||
await accountsStore.fetchAccounts();
|
||||
if (accountsStore.selectedAccountId) {
|
||||
await foldersStore.fetchFolders(accountsStore.selectedAccountId);
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize sidebar mode from localStorage
|
||||
const savedSidebar = localStorage.getItem('mail-nav-sidebar');
|
||||
if (savedSidebar === 'true') {
|
||||
isSidebarMode = true;
|
||||
}
|
||||
|
||||
const savedCollapsed = localStorage.getItem('mail-nav-collapsed');
|
||||
if (savedCollapsed === 'true') {
|
||||
isCollapsed = true;
|
||||
}
|
||||
|
||||
loading = false;
|
||||
});
|
||||
</script>
|
||||
|
||||
{#if isAuthRoute}
|
||||
{@render children()}
|
||||
{:else if loading}
|
||||
{#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">Loading...</p>
|
||||
<p class="text-muted-foreground">Laden...</p>
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="layout-container">
|
||||
<PillNavigation
|
||||
items={navItems}
|
||||
currentPath={$page.url.pathname}
|
||||
appName="Mail"
|
||||
homeRoute="/"
|
||||
onToggleTheme={handleToggleTheme}
|
||||
{isDark}
|
||||
{isSidebarMode}
|
||||
onModeChange={handleModeChange}
|
||||
{isCollapsed}
|
||||
onCollapsedChange={handleCollapsedChange}
|
||||
showThemeToggle={true}
|
||||
showThemeVariants={true}
|
||||
{themeVariantItems}
|
||||
{currentThemeVariantLabel}
|
||||
{themeMode}
|
||||
onThemeModeChange={handleThemeModeChange}
|
||||
showLogout={authStore.isAuthenticated}
|
||||
onLogout={handleLogout}
|
||||
loginHref="/login"
|
||||
primaryColor="#6366f1"
|
||||
showAppSwitcher={true}
|
||||
{appItems}
|
||||
{userEmail}
|
||||
settingsHref="/settings"
|
||||
profileHref="/profile"
|
||||
/>
|
||||
|
||||
<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: 100%;
|
||||
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>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue