mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 19:41:09 +02:00
✨ feat: add global start page setting across all apps
- Add GeneralSettings types (startPages, weekStartsOn, soundsEnabled) - Create app-routes.ts with available routes for 12 apps - Extend UserSettingsStore with general settings support - Update GlobalSettingsSection with start page selector UI - Add start page redirect logic to all app layouts: - Clock, Calendar, Todo, Zitare, Picture - Manadeck, Presi, Chat, Manacore - Create user-settings stores for Clock and Todo apps 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
5b3c5ff4fb
commit
bbe540c3f1
16 changed files with 602 additions and 22 deletions
|
|
@ -148,6 +148,12 @@
|
|||
if (authStore.isAuthenticated) {
|
||||
await calendarsStore.fetchCalendars();
|
||||
await 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
|
||||
|
|
@ -225,7 +231,10 @@
|
|||
class:sidebar-mode={isSidebarMode && !isCollapsed}
|
||||
class:floating-mode={!isSidebarMode && !isCollapsed}
|
||||
>
|
||||
<div class="content-wrapper" class:calendar-expanded={settingsStore.sidebarCollapsed && $page.url.pathname === '/'}>
|
||||
<div
|
||||
class="content-wrapper"
|
||||
class:calendar-expanded={settingsStore.sidebarCollapsed && $page.url.pathname === '/'}
|
||||
>
|
||||
{@render children()}
|
||||
</div>
|
||||
</main>
|
||||
|
|
|
|||
|
|
@ -160,6 +160,12 @@
|
|||
// Load user settings
|
||||
await userSettings.load();
|
||||
|
||||
// Redirect to start page if on /chat and a custom start page is set
|
||||
const currentPath = window.location.pathname;
|
||||
if (currentPath === '/chat' && userSettings.startPage && userSettings.startPage !== '/chat') {
|
||||
goto(userSettings.startPage, { replaceState: true });
|
||||
}
|
||||
|
||||
isChecking = false;
|
||||
});
|
||||
</script>
|
||||
|
|
|
|||
19
apps/clock/apps/web/src/lib/stores/user-settings.svelte.ts
Normal file
19
apps/clock/apps/web/src/lib/stores/user-settings.svelte.ts
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
/**
|
||||
* User Settings Store for Clock
|
||||
*
|
||||
* This store syncs settings with mana-core-auth and provides:
|
||||
* - Global settings that apply to all apps
|
||||
* - Per-app overrides for customization
|
||||
* - localStorage caching for offline support
|
||||
*/
|
||||
|
||||
import { createUserSettingsStore } from '@manacore/shared-theme';
|
||||
import { authStore } from './auth.svelte';
|
||||
|
||||
const MANA_AUTH_URL = 'http://localhost:3001';
|
||||
|
||||
export const userSettings = createUserSettingsStore({
|
||||
appId: 'clock',
|
||||
authUrl: MANA_AUTH_URL,
|
||||
getAccessToken: () => authStore.getAccessToken(),
|
||||
});
|
||||
|
|
@ -1,8 +1,10 @@
|
|||
<script lang="ts">
|
||||
import { _ } from 'svelte-i18n';
|
||||
import { theme } from '$lib/stores/theme.svelte';
|
||||
import { userSettings } from '$lib/stores/user-settings.svelte';
|
||||
import { setLocale, supportedLocales } from '$lib/i18n';
|
||||
import { THEME_DEFINITIONS } from '@manacore/shared-theme';
|
||||
import { GlobalSettingsSection } from '@manacore/shared-ui';
|
||||
|
||||
// Settings state
|
||||
let clockFormat = $state<'24h' | '12h'>('24h');
|
||||
|
|
@ -29,6 +31,11 @@
|
|||
es: 'Español',
|
||||
it: 'Italiano',
|
||||
};
|
||||
|
||||
// Translation function for GlobalSettingsSection
|
||||
function translate(key: string): string {
|
||||
return $_?.(key) ?? key;
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="mx-auto max-w-2xl space-y-6">
|
||||
|
|
@ -160,4 +167,17 @@
|
|||
Töne können für einzelne Wecker und Timer in deren Einstellungen angepasst werden.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Global Settings Section -->
|
||||
<GlobalSettingsSection
|
||||
{userSettings}
|
||||
appId="clock"
|
||||
showNavigation={false}
|
||||
showTheme={false}
|
||||
showLanguage={false}
|
||||
showGeneral={true}
|
||||
title="Globale Einstellungen"
|
||||
description="Diese Einstellungen gelten für alle Mana Apps"
|
||||
t={translate}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -148,6 +148,16 @@
|
|||
// Load user settings from server
|
||||
if (authStore.isAuthenticated) {
|
||||
await userSettings.load();
|
||||
|
||||
// Redirect to start page if on /dashboard and a custom start page is set
|
||||
const currentPath = window.location.pathname;
|
||||
if (
|
||||
currentPath === '/dashboard' &&
|
||||
userSettings.startPage &&
|
||||
userSettings.startPage !== '/dashboard'
|
||||
) {
|
||||
goto(userSettings.startPage, { replaceState: true });
|
||||
}
|
||||
}
|
||||
|
||||
loading = false;
|
||||
|
|
|
|||
|
|
@ -139,6 +139,12 @@
|
|||
// Load user settings
|
||||
await userSettings.load();
|
||||
|
||||
// Redirect to start page if on root and a custom start page is set
|
||||
const currentPath = window.location.pathname;
|
||||
if (currentPath === '/decks' && userSettings.startPage && userSettings.startPage !== '/decks') {
|
||||
goto(userSettings.startPage, { replaceState: true });
|
||||
}
|
||||
|
||||
// Initialize sidebar mode from localStorage
|
||||
const savedSidebar = localStorage.getItem('manadeck-nav-sidebar');
|
||||
if (savedSidebar === 'true') {
|
||||
|
|
|
|||
|
|
@ -68,7 +68,22 @@
|
|||
// Load user settings when authenticated
|
||||
$effect(() => {
|
||||
if (authStore.initialized && authStore.user) {
|
||||
userSettings.load();
|
||||
userSettings.load().then(() => {
|
||||
// Redirect to start page if on /app and a custom start page is set
|
||||
const currentPath = window.location.pathname;
|
||||
if (
|
||||
currentPath === '/app' &&
|
||||
userSettings.startPage &&
|
||||
userSettings.startPage !== '/' &&
|
||||
userSettings.startPage !== '/app'
|
||||
) {
|
||||
// Prepend /app if the start page doesn't include it
|
||||
const targetPath = userSettings.startPage.startsWith('/app')
|
||||
? userSettings.startPage
|
||||
: `/app${userSettings.startPage}`;
|
||||
goto(targetPath, { replaceState: true });
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -136,6 +136,12 @@
|
|||
// Load user settings
|
||||
await 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 theme
|
||||
const cleanup = theme.initialize();
|
||||
|
||||
|
|
|
|||
10
apps/todo/apps/web/src/lib/stores/user-settings.svelte.ts
Normal file
10
apps/todo/apps/web/src/lib/stores/user-settings.svelte.ts
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
import { createUserSettingsStore } from '@manacore/shared-theme';
|
||||
import { authStore } from './auth.svelte';
|
||||
|
||||
const MANA_AUTH_URL = import.meta.env.PUBLIC_MANA_CORE_AUTH_URL || 'http://localhost:3001';
|
||||
|
||||
export const userSettings = createUserSettingsStore({
|
||||
appId: 'todo',
|
||||
authUrl: MANA_AUTH_URL,
|
||||
getAccessToken: () => authStore.getAccessToken(),
|
||||
});
|
||||
|
|
@ -6,6 +6,7 @@
|
|||
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';
|
||||
|
|
@ -137,7 +138,17 @@
|
|||
|
||||
// Load data if authenticated
|
||||
if (authStore.isAuthenticated) {
|
||||
await Promise.all([projectsStore.fetchProjects(), labelsStore.fetchLabels()]);
|
||||
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
|
||||
|
|
@ -183,7 +194,7 @@
|
|||
onModeChange={handleModeChange}
|
||||
{isCollapsed}
|
||||
onCollapsedChange={handleCollapsedChange}
|
||||
desktopPosition="bottom"
|
||||
desktopPosition={userSettings.nav.desktopPosition}
|
||||
showThemeToggle={true}
|
||||
showThemeVariants={true}
|
||||
{themeVariantItems}
|
||||
|
|
|
|||
|
|
@ -145,6 +145,12 @@
|
|||
// Load user settings if authenticated
|
||||
if (authStore.isAuthenticated) {
|
||||
await 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
|
||||
|
|
|
|||
201
packages/shared-theme/src/app-routes.ts
Normal file
201
packages/shared-theme/src/app-routes.ts
Normal file
|
|
@ -0,0 +1,201 @@
|
|||
/**
|
||||
* App Routes Configuration
|
||||
*
|
||||
* Defines available start pages for each app in the ecosystem.
|
||||
* Used by the start page selector in global settings.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Route definition with i18n label
|
||||
*/
|
||||
export interface AppRoute {
|
||||
/** Route path (e.g., '/stopwatch') */
|
||||
path: string;
|
||||
/** i18n key for the label (e.g., 'nav.stopwatch') */
|
||||
labelKey: string;
|
||||
/** Optional icon name */
|
||||
icon?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* App route configuration
|
||||
*/
|
||||
export interface AppRouteConfig {
|
||||
/** App identifier */
|
||||
appId: string;
|
||||
/** Default start route (used when no preference set) */
|
||||
defaultRoute: string;
|
||||
/** Available routes that can be set as start page */
|
||||
availableRoutes: AppRoute[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Route configurations for all apps
|
||||
*/
|
||||
export const APP_ROUTES: Record<string, AppRouteConfig> = {
|
||||
clock: {
|
||||
appId: 'clock',
|
||||
defaultRoute: '/',
|
||||
availableRoutes: [
|
||||
{ path: '/', labelKey: 'nav.dashboard', icon: 'home' },
|
||||
{ path: '/alarms', labelKey: 'nav.alarms', icon: 'alarm' },
|
||||
{ path: '/timers', labelKey: 'nav.timers', icon: 'timer' },
|
||||
{ path: '/stopwatch', labelKey: 'nav.stopwatch', icon: 'stopwatch' },
|
||||
{ path: '/pomodoro', labelKey: 'nav.pomodoro', icon: 'target' },
|
||||
{ path: '/world-clock', labelKey: 'nav.worldClock', icon: 'globe' },
|
||||
{ path: '/life', labelKey: 'nav.lifeClock', icon: 'heart' },
|
||||
],
|
||||
},
|
||||
|
||||
calendar: {
|
||||
appId: 'calendar',
|
||||
defaultRoute: '/',
|
||||
availableRoutes: [
|
||||
{ path: '/', labelKey: 'nav.month', icon: 'calendar' },
|
||||
{ path: '/agenda', labelKey: 'nav.agenda', icon: 'list' },
|
||||
],
|
||||
},
|
||||
|
||||
contacts: {
|
||||
appId: 'contacts',
|
||||
defaultRoute: '/',
|
||||
availableRoutes: [
|
||||
{ path: '/', labelKey: 'nav.contacts', icon: 'users' },
|
||||
{ path: '/groups', labelKey: 'nav.groups', icon: 'folder' },
|
||||
{ path: '/favorites', labelKey: 'nav.favorites', icon: 'star' },
|
||||
],
|
||||
},
|
||||
|
||||
mail: {
|
||||
appId: 'mail',
|
||||
defaultRoute: '/',
|
||||
availableRoutes: [
|
||||
{ path: '/', labelKey: 'nav.inbox', icon: 'inbox' },
|
||||
{ path: '/sent', labelKey: 'nav.sent', icon: 'send' },
|
||||
{ path: '/drafts', labelKey: 'nav.drafts', icon: 'file' },
|
||||
{ path: '/starred', labelKey: 'nav.starred', icon: 'star' },
|
||||
],
|
||||
},
|
||||
|
||||
todo: {
|
||||
appId: 'todo',
|
||||
defaultRoute: '/',
|
||||
availableRoutes: [
|
||||
{ path: '/', labelKey: 'nav.all', icon: 'list' },
|
||||
{ path: '/today', labelKey: 'nav.today', icon: 'calendar' },
|
||||
{ path: '/upcoming', labelKey: 'nav.upcoming', icon: 'clock' },
|
||||
{ path: '/completed', labelKey: 'nav.completed', icon: 'check' },
|
||||
],
|
||||
},
|
||||
|
||||
storage: {
|
||||
appId: 'storage',
|
||||
defaultRoute: '/',
|
||||
availableRoutes: [
|
||||
{ path: '/', labelKey: 'nav.home', icon: 'home' },
|
||||
{ path: '/files', labelKey: 'nav.files', icon: 'folder' },
|
||||
{ path: '/favorites', labelKey: 'nav.favorites', icon: 'star' },
|
||||
{ path: '/shared', labelKey: 'nav.shared', icon: 'share' },
|
||||
],
|
||||
},
|
||||
|
||||
chat: {
|
||||
appId: 'chat',
|
||||
defaultRoute: '/chat',
|
||||
availableRoutes: [
|
||||
{ path: '/chat', labelKey: 'nav.chat', icon: 'message' },
|
||||
{ path: '/spaces', labelKey: 'nav.spaces', icon: 'folder' },
|
||||
{ path: '/templates', labelKey: 'nav.templates', icon: 'file' },
|
||||
{ path: '/documents', labelKey: 'nav.documents', icon: 'document' },
|
||||
],
|
||||
},
|
||||
|
||||
picture: {
|
||||
appId: 'picture',
|
||||
defaultRoute: '/app/gallery',
|
||||
availableRoutes: [
|
||||
{ path: '/app/gallery', labelKey: 'nav.gallery', icon: 'image' },
|
||||
{ path: '/app/generate', labelKey: 'nav.generate', icon: 'sparkle' },
|
||||
{ path: '/app/board', labelKey: 'nav.board', icon: 'grid' },
|
||||
{ path: '/app/explore', labelKey: 'nav.explore', icon: 'compass' },
|
||||
],
|
||||
},
|
||||
|
||||
manadeck: {
|
||||
appId: 'manadeck',
|
||||
defaultRoute: '/decks',
|
||||
availableRoutes: [
|
||||
{ path: '/decks', labelKey: 'nav.decks', icon: 'layers' },
|
||||
{ path: '/explore', labelKey: 'nav.explore', icon: 'compass' },
|
||||
{ path: '/progress', labelKey: 'nav.progress', icon: 'trending' },
|
||||
],
|
||||
},
|
||||
|
||||
zitare: {
|
||||
appId: 'zitare',
|
||||
defaultRoute: '/',
|
||||
availableRoutes: [
|
||||
{ path: '/', labelKey: 'nav.home', icon: 'home' },
|
||||
{ path: '/quotes', labelKey: 'nav.quotes', icon: 'quote' },
|
||||
{ path: '/favorites', labelKey: 'nav.favorites', icon: 'star' },
|
||||
{ path: '/authors', labelKey: 'nav.authors', icon: 'users' },
|
||||
{ path: '/lists', labelKey: 'nav.lists', icon: 'list' },
|
||||
],
|
||||
},
|
||||
|
||||
presi: {
|
||||
appId: 'presi',
|
||||
defaultRoute: '/',
|
||||
availableRoutes: [{ path: '/', labelKey: 'nav.home', icon: 'home' }],
|
||||
},
|
||||
|
||||
manacore: {
|
||||
appId: 'manacore',
|
||||
defaultRoute: '/',
|
||||
availableRoutes: [{ path: '/', labelKey: 'nav.dashboard', icon: 'home' }],
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the start page for a specific app
|
||||
* @param appId The app identifier
|
||||
* @param startPages User's start page preferences
|
||||
* @returns The start page path (user preference or app default)
|
||||
*/
|
||||
export function getStartPage(appId: string, startPages: Record<string, string> = {}): string {
|
||||
const config = APP_ROUTES[appId];
|
||||
if (!config) {
|
||||
return '/';
|
||||
}
|
||||
|
||||
// Check if user has a preference for this app
|
||||
const userPreference = startPages[appId];
|
||||
if (userPreference) {
|
||||
// Validate that the route is available
|
||||
const isValid = config.availableRoutes.some((r) => r.path === userPreference);
|
||||
if (isValid) {
|
||||
return userPreference;
|
||||
}
|
||||
}
|
||||
|
||||
// Return app default
|
||||
return config.defaultRoute;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get available routes for a specific app
|
||||
* @param appId The app identifier
|
||||
* @returns Array of available routes or empty array if app not found
|
||||
*/
|
||||
export function getAvailableRoutes(appId: string): AppRoute[] {
|
||||
return APP_ROUTES[appId]?.availableRoutes ?? [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get default route for a specific app
|
||||
* @param appId The app identifier
|
||||
* @returns The default route path or '/' if app not found
|
||||
*/
|
||||
export function getDefaultRoute(appId: string): string {
|
||||
return APP_ROUTES[appId]?.defaultRoute ?? '/';
|
||||
}
|
||||
|
|
@ -24,10 +24,14 @@ export type {
|
|||
UserSettingsResponse,
|
||||
UserSettingsStore,
|
||||
UserSettingsStoreConfig,
|
||||
// General Settings Types
|
||||
StartPageConfig,
|
||||
WeekStartDay,
|
||||
GeneralSettings,
|
||||
} from './types';
|
||||
|
||||
// User Settings Constants
|
||||
export { DEFAULT_GLOBAL_SETTINGS } from './types';
|
||||
export { DEFAULT_GLOBAL_SETTINGS, DEFAULT_GENERAL_SETTINGS } from './types';
|
||||
|
||||
// Constants
|
||||
export {
|
||||
|
|
@ -89,3 +93,7 @@ export {
|
|||
loadA11yFromStorage,
|
||||
saveA11yToStorage,
|
||||
} from './a11y-utils';
|
||||
|
||||
// App Routes
|
||||
export type { AppRoute, AppRouteConfig } from './app-routes';
|
||||
export { APP_ROUTES, getStartPage, getAvailableRoutes, getDefaultRoute } from './app-routes';
|
||||
|
|
|
|||
|
|
@ -223,6 +223,29 @@ export interface NavSettings {
|
|||
sidebarCollapsed: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Start page configuration per app
|
||||
* Keys are app IDs, values are route paths
|
||||
*/
|
||||
export type StartPageConfig = Record<string, string>;
|
||||
|
||||
/**
|
||||
* Day of week for calendar/week starts
|
||||
*/
|
||||
export type WeekStartDay = 'monday' | 'sunday';
|
||||
|
||||
/**
|
||||
* General settings (global preferences)
|
||||
*/
|
||||
export interface GeneralSettings {
|
||||
/** Start page per app (e.g., { clock: '/stopwatch', calendar: '/week' }) */
|
||||
startPages: StartPageConfig;
|
||||
/** First day of week */
|
||||
weekStartsOn: WeekStartDay;
|
||||
/** Master toggle for all app sounds */
|
||||
soundsEnabled: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Theme settings (synced to server)
|
||||
*/
|
||||
|
|
@ -240,6 +263,8 @@ export interface GlobalSettings {
|
|||
nav: NavSettings;
|
||||
theme: ThemeSettings;
|
||||
locale: string;
|
||||
/** General preferences (start pages, sounds, etc.) */
|
||||
general: GeneralSettings;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -258,6 +283,15 @@ export interface UserSettingsResponse {
|
|||
appOverrides: Record<string, AppOverride>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Default general settings
|
||||
*/
|
||||
export const DEFAULT_GENERAL_SETTINGS: GeneralSettings = {
|
||||
startPages: {}, // Empty = use app defaults
|
||||
weekStartsOn: 'monday',
|
||||
soundsEnabled: true,
|
||||
};
|
||||
|
||||
/**
|
||||
* Default global settings
|
||||
*/
|
||||
|
|
@ -265,6 +299,7 @@ export const DEFAULT_GLOBAL_SETTINGS: GlobalSettings = {
|
|||
nav: { desktopPosition: 'top', sidebarCollapsed: false },
|
||||
theme: { mode: 'system', colorScheme: 'ocean' },
|
||||
locale: 'de',
|
||||
general: DEFAULT_GENERAL_SETTINGS,
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
@ -277,6 +312,10 @@ export interface UserSettingsStore {
|
|||
readonly theme: ThemeSettings;
|
||||
/** Current locale */
|
||||
readonly locale: string;
|
||||
/** Resolved general settings */
|
||||
readonly general: GeneralSettings;
|
||||
/** Start page for current app (resolved from settings or default) */
|
||||
readonly startPage: string;
|
||||
/** Raw global settings */
|
||||
readonly globalSettings: GlobalSettings;
|
||||
/** Whether current app has an override */
|
||||
|
|
@ -294,6 +333,10 @@ export interface UserSettingsStore {
|
|||
updateAppOverride: (settings: AppOverride) => Promise<void>;
|
||||
/** Remove app override (revert to global) */
|
||||
removeAppOverride: () => Promise<void>;
|
||||
/** Set start page for a specific app */
|
||||
setStartPage: (appId: string, path: string) => Promise<void>;
|
||||
/** Update general settings */
|
||||
updateGeneral: (settings: Partial<GeneralSettings>) => Promise<void>;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -6,9 +6,11 @@ import type {
|
|||
NavSettings,
|
||||
ThemeSettings,
|
||||
UserSettingsResponse,
|
||||
GeneralSettings,
|
||||
} from './types';
|
||||
import { DEFAULT_GLOBAL_SETTINGS } from './types';
|
||||
import { DEFAULT_GLOBAL_SETTINGS, DEFAULT_GENERAL_SETTINGS } from './types';
|
||||
import { isBrowser } from './utils';
|
||||
import { getStartPage as getStartPageFromConfig } from './app-routes';
|
||||
|
||||
const STORAGE_KEY_PREFIX = 'manacore-user-settings';
|
||||
|
||||
|
|
@ -66,6 +68,15 @@ export function createUserSettingsStore(config: UserSettingsStoreConfig): UserSe
|
|||
// Derived: whether this app has an override
|
||||
const hasAppOverride = $derived(!!appOverrides[appId]);
|
||||
|
||||
// Derived: resolved general settings (always from global)
|
||||
const general = $derived<GeneralSettings>({
|
||||
...DEFAULT_GENERAL_SETTINGS,
|
||||
...globalSettings.general,
|
||||
});
|
||||
|
||||
// Derived: start page for current app
|
||||
const startPage = $derived(getStartPageFromConfig(appId, general.startPages));
|
||||
|
||||
/**
|
||||
* Save current settings to localStorage (for offline fallback)
|
||||
*/
|
||||
|
|
@ -111,11 +122,7 @@ export function createUserSettingsStore(config: UserSettingsStoreConfig): UserSe
|
|||
/**
|
||||
* Make an API request to the settings endpoint
|
||||
*/
|
||||
async function apiRequest<T>(
|
||||
method: string,
|
||||
path: string,
|
||||
body?: object
|
||||
): Promise<T | null> {
|
||||
async function apiRequest<T>(method: string, path: string, body?: object): Promise<T | null> {
|
||||
const token = await getAccessToken();
|
||||
if (!token) {
|
||||
console.warn('No access token available for settings API');
|
||||
|
|
@ -176,6 +183,14 @@ export function createUserSettingsStore(config: UserSettingsStoreConfig): UserSe
|
|||
nav: { ...globalSettings.nav, ...settings.nav },
|
||||
theme: { ...globalSettings.theme, ...settings.theme },
|
||||
locale: settings.locale ?? globalSettings.locale,
|
||||
general: {
|
||||
...globalSettings.general,
|
||||
...settings.general,
|
||||
startPages: {
|
||||
...globalSettings.general?.startPages,
|
||||
...settings.general?.startPages,
|
||||
},
|
||||
},
|
||||
};
|
||||
saveToStorage();
|
||||
|
||||
|
|
@ -238,6 +253,35 @@ export function createUserSettingsStore(config: UserSettingsStoreConfig): UserSe
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update start page for a specific app
|
||||
*/
|
||||
async function setStartPage(targetAppId: string, path: string): Promise<void> {
|
||||
await updateGlobal({
|
||||
general: {
|
||||
startPages: {
|
||||
[targetAppId]: path,
|
||||
},
|
||||
},
|
||||
} as Partial<GlobalSettings>);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update general settings
|
||||
*/
|
||||
async function updateGeneral(settings: Partial<GeneralSettings>): Promise<void> {
|
||||
await updateGlobal({
|
||||
general: {
|
||||
...globalSettings.general,
|
||||
...settings,
|
||||
startPages: {
|
||||
...globalSettings.general?.startPages,
|
||||
...settings.startPages,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove app override (revert to global settings)
|
||||
*/
|
||||
|
|
@ -280,6 +324,12 @@ export function createUserSettingsStore(config: UserSettingsStoreConfig): UserSe
|
|||
get locale() {
|
||||
return locale;
|
||||
},
|
||||
get general() {
|
||||
return general;
|
||||
},
|
||||
get startPage() {
|
||||
return startPage;
|
||||
},
|
||||
get globalSettings() {
|
||||
return globalSettings;
|
||||
},
|
||||
|
|
@ -297,5 +347,7 @@ export function createUserSettingsStore(config: UserSettingsStoreConfig): UserSe
|
|||
updateGlobal,
|
||||
updateAppOverride,
|
||||
removeAppOverride,
|
||||
setStartPage,
|
||||
updateGeneral,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,32 +1,54 @@
|
|||
<script lang="ts">
|
||||
import type { UserSettingsStore, NavPosition, ThemeMode } from '@manacore/shared-theme';
|
||||
import type {
|
||||
UserSettingsStore,
|
||||
NavPosition,
|
||||
ThemeMode,
|
||||
WeekStartDay,
|
||||
} from '@manacore/shared-theme';
|
||||
import { getAvailableRoutes, getDefaultRoute } from '@manacore/shared-theme';
|
||||
import SettingsSection from './SettingsSection.svelte';
|
||||
import SettingsCard from './SettingsCard.svelte';
|
||||
|
||||
interface Props {
|
||||
/** User settings store instance */
|
||||
userSettings: UserSettingsStore;
|
||||
/** App ID for start page selection */
|
||||
appId?: string;
|
||||
/** Whether to show navigation settings */
|
||||
showNavigation?: boolean;
|
||||
/** Whether to show theme settings */
|
||||
showTheme?: boolean;
|
||||
/** Whether to show language settings */
|
||||
showLanguage?: boolean;
|
||||
/** Whether to show general settings (start page, sounds, week start) */
|
||||
showGeneral?: boolean;
|
||||
/** Section title */
|
||||
title?: string;
|
||||
/** Section description */
|
||||
description?: string;
|
||||
/** Translation function (optional, falls back to German) */
|
||||
t?: (key: string) => string;
|
||||
}
|
||||
|
||||
let {
|
||||
userSettings,
|
||||
appId,
|
||||
showNavigation = true,
|
||||
showTheme = true,
|
||||
showLanguage = true,
|
||||
showGeneral = true,
|
||||
title = 'App-Einstellungen',
|
||||
description = 'Diese Einstellungen gelten für alle Mana Apps',
|
||||
t = (key: string) => key,
|
||||
}: Props = $props();
|
||||
|
||||
// Available routes for start page selection
|
||||
const availableRoutes = $derived(appId ? getAvailableRoutes(appId) : []);
|
||||
const defaultRoute = $derived(appId ? getDefaultRoute(appId) : '/');
|
||||
const currentStartPage = $derived(
|
||||
appId ? userSettings.general?.startPages?.[appId] || defaultRoute : '/'
|
||||
);
|
||||
|
||||
// Navigation position handler
|
||||
async function handleNavPositionChange(position: NavPosition) {
|
||||
await userSettings.updateGlobal({
|
||||
|
|
@ -60,6 +82,24 @@
|
|||
await userSettings.updateGlobal({ locale });
|
||||
}
|
||||
|
||||
// Start page handler
|
||||
async function handleStartPageChange(e: Event) {
|
||||
const target = e.target as HTMLSelectElement;
|
||||
if (appId) {
|
||||
await userSettings.setStartPage(appId, target.value);
|
||||
}
|
||||
}
|
||||
|
||||
// Week start handler
|
||||
async function handleWeekStartChange(day: WeekStartDay) {
|
||||
await userSettings.updateGeneral({ weekStartsOn: day });
|
||||
}
|
||||
|
||||
// Sounds handler
|
||||
async function handleSoundsChange(enabled: boolean) {
|
||||
await userSettings.updateGeneral({ soundsEnabled: enabled });
|
||||
}
|
||||
|
||||
const colorSchemes = [
|
||||
{ id: 'ocean', label: 'Ozean', color: 'bg-blue-500' },
|
||||
{ id: 'nature', label: 'Natur', color: 'bg-green-500' },
|
||||
|
|
@ -102,7 +142,9 @@
|
|||
{#if showNavigation}
|
||||
<!-- Navigation Settings -->
|
||||
<div class="space-y-4">
|
||||
<h3 class="text-xs font-semibold text-[hsl(var(--muted-foreground))] uppercase tracking-wider">
|
||||
<h3
|
||||
class="text-xs font-semibold text-[hsl(var(--muted-foreground))] uppercase tracking-wider"
|
||||
>
|
||||
Navigation
|
||||
</h3>
|
||||
|
||||
|
|
@ -135,10 +177,14 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between py-2 border-t border-[hsl(var(--border))]">
|
||||
<div
|
||||
class="flex items-center justify-between py-2 border-t border-[hsl(var(--border))]"
|
||||
>
|
||||
<div>
|
||||
<p class="font-medium text-[hsl(var(--foreground))]">Sidebar eingeklappt</p>
|
||||
<p class="text-sm text-[hsl(var(--muted-foreground))]">Standard-Zustand der Sidebar</p>
|
||||
<p class="text-sm text-[hsl(var(--muted-foreground))]">
|
||||
Standard-Zustand der Sidebar
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
class="relative inline-flex h-6 w-11 items-center rounded-full transition-colors {userSettings
|
||||
|
|
@ -161,15 +207,21 @@
|
|||
|
||||
{#if showTheme}
|
||||
<!-- Theme Settings -->
|
||||
<div class="space-y-4 {showNavigation ? 'pt-4 border-t border-[hsl(var(--border))]' : ''}">
|
||||
<h3 class="text-xs font-semibold text-[hsl(var(--muted-foreground))] uppercase tracking-wider">
|
||||
<div
|
||||
class="space-y-4 {showNavigation ? 'pt-4 border-t border-[hsl(var(--border))]' : ''}"
|
||||
>
|
||||
<h3
|
||||
class="text-xs font-semibold text-[hsl(var(--muted-foreground))] uppercase tracking-wider"
|
||||
>
|
||||
Erscheinungsbild
|
||||
</h3>
|
||||
|
||||
<div class="flex items-center justify-between py-2">
|
||||
<div>
|
||||
<p class="font-medium text-[hsl(var(--foreground))]">Farbmodus</p>
|
||||
<p class="text-sm text-[hsl(var(--muted-foreground))]">Hell, Dunkel oder automatisch</p>
|
||||
<p class="text-sm text-[hsl(var(--muted-foreground))]">
|
||||
Hell, Dunkel oder automatisch
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex gap-1">
|
||||
{#each ['light', 'dark', 'system'] as mode}
|
||||
|
|
@ -186,10 +238,14 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between py-2 border-t border-[hsl(var(--border))]">
|
||||
<div
|
||||
class="flex items-center justify-between py-2 border-t border-[hsl(var(--border))]"
|
||||
>
|
||||
<div>
|
||||
<p class="font-medium text-[hsl(var(--foreground))]">Farbschema</p>
|
||||
<p class="text-sm text-[hsl(var(--muted-foreground))]">Akzentfarbe der Benutzeroberfläche</p>
|
||||
<p class="text-sm text-[hsl(var(--muted-foreground))]">
|
||||
Akzentfarbe der Benutzeroberfläche
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
{#each colorSchemes as scheme}
|
||||
|
|
@ -214,14 +270,18 @@
|
|||
? 'pt-4 border-t border-[hsl(var(--border))]'
|
||||
: ''}"
|
||||
>
|
||||
<h3 class="text-xs font-semibold text-[hsl(var(--muted-foreground))] uppercase tracking-wider">
|
||||
<h3
|
||||
class="text-xs font-semibold text-[hsl(var(--muted-foreground))] uppercase tracking-wider"
|
||||
>
|
||||
Sprache
|
||||
</h3>
|
||||
|
||||
<div class="flex items-center justify-between py-2">
|
||||
<div>
|
||||
<p class="font-medium text-[hsl(var(--foreground))]">Anzeigesprache</p>
|
||||
<p class="text-sm text-[hsl(var(--muted-foreground))]">Sprache der Benutzeroberfläche</p>
|
||||
<p class="text-sm text-[hsl(var(--muted-foreground))]">
|
||||
Sprache der Benutzeroberfläche
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex gap-1">
|
||||
{#each languages as lang}
|
||||
|
|
@ -239,6 +299,104 @@
|
|||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if showGeneral}
|
||||
<!-- General Settings -->
|
||||
<div
|
||||
class="space-y-4 {showLanguage || showTheme || showNavigation
|
||||
? 'pt-4 border-t border-[hsl(var(--border))]'
|
||||
: ''}"
|
||||
>
|
||||
<h3
|
||||
class="text-xs font-semibold text-[hsl(var(--muted-foreground))] uppercase tracking-wider"
|
||||
>
|
||||
Allgemein
|
||||
</h3>
|
||||
|
||||
{#if appId && availableRoutes.length > 1}
|
||||
<!-- Start Page Selector -->
|
||||
<div class="flex items-center justify-between py-2">
|
||||
<div>
|
||||
<p class="font-medium text-[hsl(var(--foreground))]">Startseite</p>
|
||||
<p class="text-sm text-[hsl(var(--muted-foreground))]">
|
||||
Welche Seite beim Öffnen der App angezeigt wird
|
||||
</p>
|
||||
</div>
|
||||
<select
|
||||
class="px-3 py-1.5 text-sm font-medium rounded-lg bg-[hsl(var(--muted))] text-[hsl(var(--foreground))] border-none cursor-pointer appearance-none pr-8 bg-[url('data:image/svg+xml;charset=US-ASCII,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2224%22%20height%3D%2224%22%20viewBox%3D%220%200%2024%2024%22%20fill%3D%22none%22%20stroke%3D%22%23666%22%20stroke-width%3D%222%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%3E%3Cpolyline%20points%3D%226%209%2012%2015%2018%209%22%3E%3C%2Fpolyline%3E%3C%2Fsvg%3E')] bg-no-repeat bg-[right_0.5rem_center] bg-[length:1rem]"
|
||||
value={currentStartPage}
|
||||
onchange={handleStartPageChange}
|
||||
>
|
||||
{#each availableRoutes as route}
|
||||
<option value={route.path}>
|
||||
{t(route.labelKey)}
|
||||
</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Week Start Day -->
|
||||
<div
|
||||
class="flex items-center justify-between py-2 {appId && availableRoutes.length > 1
|
||||
? 'border-t border-[hsl(var(--border))]'
|
||||
: ''}"
|
||||
>
|
||||
<div>
|
||||
<p class="font-medium text-[hsl(var(--foreground))]">Wochenstart</p>
|
||||
<p class="text-sm text-[hsl(var(--muted-foreground))]">
|
||||
Erster Tag der Woche in Kalendern
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<button
|
||||
class="px-3 py-1.5 text-sm font-medium rounded-lg transition-colors {userSettings
|
||||
.general?.weekStartsOn === 'monday'
|
||||
? 'bg-[hsl(var(--primary))] text-[hsl(var(--primary-foreground))]'
|
||||
: 'bg-[hsl(var(--muted))] hover:bg-[hsl(var(--muted))]/80 text-[hsl(var(--foreground))]'}"
|
||||
onclick={() => handleWeekStartChange('monday')}
|
||||
>
|
||||
Montag
|
||||
</button>
|
||||
<button
|
||||
class="px-3 py-1.5 text-sm font-medium rounded-lg transition-colors {userSettings
|
||||
.general?.weekStartsOn === 'sunday'
|
||||
? 'bg-[hsl(var(--primary))] text-[hsl(var(--primary-foreground))]'
|
||||
: 'bg-[hsl(var(--muted))] hover:bg-[hsl(var(--muted))]/80 text-[hsl(var(--foreground))]'}"
|
||||
onclick={() => handleWeekStartChange('sunday')}
|
||||
>
|
||||
Sonntag
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sounds Toggle -->
|
||||
<div
|
||||
class="flex items-center justify-between py-2 border-t border-[hsl(var(--border))]"
|
||||
>
|
||||
<div>
|
||||
<p class="font-medium text-[hsl(var(--foreground))]">Sounds</p>
|
||||
<p class="text-sm text-[hsl(var(--muted-foreground))]">
|
||||
Sound-Effekte in allen Apps
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
class="relative inline-flex h-6 w-11 items-center rounded-full transition-colors {(userSettings
|
||||
.general?.soundsEnabled ?? true)
|
||||
? 'bg-[hsl(var(--primary))]'
|
||||
: 'bg-gray-200 dark:bg-gray-700'}"
|
||||
onclick={() => handleSoundsChange(!(userSettings.general?.soundsEnabled ?? true))}
|
||||
>
|
||||
<span
|
||||
class="inline-block h-4 w-4 transform rounded-full bg-white transition-transform {(userSettings
|
||||
.general?.soundsEnabled ?? true)
|
||||
? 'translate-x-6'
|
||||
: 'translate-x-1'}"
|
||||
></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if userSettings.syncing}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue