diff --git a/apps/times/packages/shared/src/constants/clock.ts b/apps/times/packages/shared/src/constants/clock.ts new file mode 100644 index 000000000..8235095c0 --- /dev/null +++ b/apps/times/packages/shared/src/constants/clock.ts @@ -0,0 +1,151 @@ +// Popular timezones with city names and coordinates for map display +export const POPULAR_TIMEZONES = [ + { + timezone: 'America/New_York', + city: 'New York', + region: 'Americas', + lat: 40.7128, + lng: -74.006, + }, + { + timezone: 'America/Los_Angeles', + city: 'Los Angeles', + region: 'Americas', + lat: 34.0522, + lng: -118.2437, + }, + { timezone: 'America/Chicago', city: 'Chicago', region: 'Americas', lat: 41.8781, lng: -87.6298 }, + { timezone: 'America/Toronto', city: 'Toronto', region: 'Americas', lat: 43.6532, lng: -79.3832 }, + { + timezone: 'America/Sao_Paulo', + city: 'São Paulo', + region: 'Americas', + lat: -23.5505, + lng: -46.6333, + }, + { + timezone: 'America/Mexico_City', + city: 'Mexico City', + region: 'Americas', + lat: 19.4326, + lng: -99.1332, + }, + { + timezone: 'America/Buenos_Aires', + city: 'Buenos Aires', + region: 'Americas', + lat: -34.6037, + lng: -58.3816, + }, + { + timezone: 'America/Vancouver', + city: 'Vancouver', + region: 'Americas', + lat: 49.2827, + lng: -123.1207, + }, + { timezone: 'Europe/London', city: 'London', region: 'Europe', lat: 51.5074, lng: -0.1278 }, + { timezone: 'Europe/Paris', city: 'Paris', region: 'Europe', lat: 48.8566, lng: 2.3522 }, + { timezone: 'Europe/Berlin', city: 'Berlin', region: 'Europe', lat: 52.52, lng: 13.405 }, + { timezone: 'Europe/Rome', city: 'Rome', region: 'Europe', lat: 41.9028, lng: 12.4964 }, + { timezone: 'Europe/Madrid', city: 'Madrid', region: 'Europe', lat: 40.4168, lng: -3.7038 }, + { timezone: 'Europe/Amsterdam', city: 'Amsterdam', region: 'Europe', lat: 52.3676, lng: 4.9041 }, + { timezone: 'Europe/Vienna', city: 'Vienna', region: 'Europe', lat: 48.2082, lng: 16.3738 }, + { timezone: 'Europe/Zurich', city: 'Zurich', region: 'Europe', lat: 47.3769, lng: 8.5417 }, + { timezone: 'Europe/Moscow', city: 'Moscow', region: 'Europe', lat: 55.7558, lng: 37.6173 }, + { timezone: 'Europe/Stockholm', city: 'Stockholm', region: 'Europe', lat: 59.3293, lng: 18.0686 }, + { timezone: 'Europe/Istanbul', city: 'Istanbul', region: 'Europe', lat: 41.0082, lng: 28.9784 }, + { timezone: 'Asia/Tokyo', city: 'Tokyo', region: 'Asia', lat: 35.6762, lng: 139.6503 }, + { timezone: 'Asia/Shanghai', city: 'Shanghai', region: 'Asia', lat: 31.2304, lng: 121.4737 }, + { timezone: 'Asia/Hong_Kong', city: 'Hong Kong', region: 'Asia', lat: 22.3193, lng: 114.1694 }, + { timezone: 'Asia/Singapore', city: 'Singapore', region: 'Asia', lat: 1.3521, lng: 103.8198 }, + { timezone: 'Asia/Seoul', city: 'Seoul', region: 'Asia', lat: 37.5665, lng: 126.978 }, + { timezone: 'Asia/Mumbai', city: 'Mumbai', region: 'Asia', lat: 19.076, lng: 72.8777 }, + { timezone: 'Asia/Dubai', city: 'Dubai', region: 'Asia', lat: 25.2048, lng: 55.2708 }, + { timezone: 'Asia/Bangkok', city: 'Bangkok', region: 'Asia', lat: 13.7563, lng: 100.5018 }, + { timezone: 'Asia/Jakarta', city: 'Jakarta', region: 'Asia', lat: -6.2088, lng: 106.8456 }, + { timezone: 'Australia/Sydney', city: 'Sydney', region: 'Oceania', lat: -33.8688, lng: 151.2093 }, + { + timezone: 'Australia/Melbourne', + city: 'Melbourne', + region: 'Oceania', + lat: -37.8136, + lng: 144.9631, + }, + { + timezone: 'Pacific/Auckland', + city: 'Auckland', + region: 'Oceania', + lat: -36.8485, + lng: 174.7633, + }, + { timezone: 'Africa/Cairo', city: 'Cairo', region: 'Africa', lat: 30.0444, lng: 31.2357 }, + { + timezone: 'Africa/Johannesburg', + city: 'Johannesburg', + region: 'Africa', + lat: -26.2041, + lng: 28.0473, + }, + { timezone: 'Africa/Lagos', city: 'Lagos', region: 'Africa', lat: 6.5244, lng: 3.3792 }, +] as const; + +// Available alarm sounds +export const ALARM_SOUNDS = [ + { id: 'default', name: 'Default', nameDE: 'Standard' }, + { id: 'gentle', name: 'Gentle', nameDE: 'Sanft' }, + { id: 'classic', name: 'Classic', nameDE: 'Klassisch' }, + { id: 'digital', name: 'Digital', nameDE: 'Digital' }, + { id: 'nature', name: 'Nature', nameDE: 'Natur' }, + { id: 'chime', name: 'Chime', nameDE: 'Glockenspiel' }, +] as const; + +// Countdown timer presets +export const QUICK_TIMER_PRESETS = [ + { label: '1 min', seconds: 60 }, + { label: '3 min', seconds: 180 }, + { label: '5 min', seconds: 300 }, + { label: '10 min', seconds: 600 }, + { label: '15 min', seconds: 900 }, + { label: '30 min', seconds: 1800 }, + { label: '45 min', seconds: 2700 }, + { label: '1 hour', seconds: 3600 }, +] as const; + +// Default alarm presets +export const DEFAULT_ALARM_PRESETS = [ + { time: '06:00', label: 'Früh aufstehen', labelEN: 'Wake up early' }, + { time: '07:00', label: 'Aufwachen', labelEN: 'Wake up' }, + { time: '08:00', label: 'Morgen', labelEN: 'Morning' }, + { time: '12:00', label: 'Mittag', labelEN: 'Noon' }, + { time: '18:00', label: 'Feierabend', labelEN: 'End of work' }, + { time: '22:00', label: 'Schlafenszeit', labelEN: 'Bedtime' }, +] as const; + +// Pomodoro presets +export const POMODORO_PRESETS = [ + { + name: 'Classic Pomodoro', + nameDE: 'Klassischer Pomodoro', + workDuration: 25 * 60, + breakDuration: 5 * 60, + longBreakDuration: 15 * 60, + sessionsBeforeLongBreak: 4, + }, + { + name: 'Short Focus', + nameDE: 'Kurzer Fokus', + workDuration: 15 * 60, + breakDuration: 3 * 60, + longBreakDuration: 10 * 60, + sessionsBeforeLongBreak: 4, + }, + { + name: 'Deep Work', + nameDE: 'Tiefes Arbeiten', + workDuration: 50 * 60, + breakDuration: 10 * 60, + longBreakDuration: 30 * 60, + sessionsBeforeLongBreak: 3, + }, +] as const; diff --git a/apps/times/packages/shared/src/constants/index.ts b/apps/times/packages/shared/src/constants/index.ts index 62052c39c..4ace56ae0 100644 --- a/apps/times/packages/shared/src/constants/index.ts +++ b/apps/times/packages/shared/src/constants/index.ts @@ -38,3 +38,7 @@ export const DEFAULT_SETTINGS = { timerReminderMinutes: 0, autoStopTimerHours: 0, }; + +// ─── Clock Constants ──────────────────────────────────── + +export * from './clock'; diff --git a/apps/times/packages/shared/src/types/clock.ts b/apps/times/packages/shared/src/types/clock.ts new file mode 100644 index 000000000..4fb77bd79 --- /dev/null +++ b/apps/times/packages/shared/src/types/clock.ts @@ -0,0 +1,172 @@ +// ─── Alarm ────────────────────────────────────────────── + +export interface Alarm { + id: string; + userId: string; + label: string | null; + time: string; // HH:mm format + enabled: boolean; + repeatDays: number[] | null; // [0-6] where 0 = Sunday + snoozeMinutes: number | null; + sound: string | null; + vibrate: boolean | null; + createdAt: string; + updatedAt: string; +} + +export interface CreateAlarmInput { + label?: string; + time: string; + enabled?: boolean; + repeatDays?: number[]; + snoozeMinutes?: number; + sound?: string; + vibrate?: boolean; +} + +export interface UpdateAlarmInput { + label?: string; + time?: string; + enabled?: boolean; + repeatDays?: number[]; + snoozeMinutes?: number; + sound?: string; + vibrate?: boolean; +} + +export type RepeatDay = 0 | 1 | 2 | 3 | 4 | 5 | 6; + +export const REPEAT_DAY_LABELS = { + 0: 'Sun', + 1: 'Mon', + 2: 'Tue', + 3: 'Wed', + 4: 'Thu', + 5: 'Fri', + 6: 'Sat', +} as const; + +export const REPEAT_DAY_LABELS_DE = { + 0: 'So', + 1: 'Mo', + 2: 'Di', + 3: 'Mi', + 4: 'Do', + 5: 'Fr', + 6: 'Sa', +} as const; + +// ─── Countdown Timer ──────────────────────────────────── + +export type CountdownTimerStatus = 'idle' | 'running' | 'paused' | 'finished'; + +export interface CountdownTimer { + id: string; + userId: string; + label: string | null; + durationSeconds: number; + remainingSeconds: number | null; + status: CountdownTimerStatus; + startedAt: string | null; + pausedAt: string | null; + sound: string | null; + createdAt: string; + updatedAt: string; +} + +export interface CreateCountdownTimerInput { + label?: string; + durationSeconds: number; + sound?: string; +} + +export interface UpdateCountdownTimerInput { + label?: string; + durationSeconds?: number; + sound?: string; +} + +export function formatCountdownDuration(seconds: number): string { + const hours = Math.floor(seconds / 3600); + const minutes = Math.floor((seconds % 3600) / 60); + const secs = seconds % 60; + + if (hours > 0) { + return `${hours}:${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`; + } + return `${minutes}:${secs.toString().padStart(2, '0')}`; +} + +export function parseCountdownDuration(formatted: string): number { + const parts = formatted.split(':').map(Number); + if (parts.length === 3) { + return parts[0] * 3600 + parts[1] * 60 + parts[2]; + } + if (parts.length === 2) { + return parts[0] * 60 + parts[1]; + } + return parts[0]; +} + +// ─── World Clock ──────────────────────────────────────── + +export interface WorldClock { + id: string; + userId: string; + timezone: string; // IANA timezone e.g. 'America/New_York' + cityName: string; + sortOrder: number; + createdAt: string; +} + +export interface CreateWorldClockInput { + timezone: string; + cityName: string; +} + +export interface TimezoneInfo { + timezone: string; + city: string; +} + +// ─── Preset ───────────────────────────────────────────── + +export type PresetType = 'timer' | 'pomodoro'; + +export interface PresetSettings { + workDuration?: number; + breakDuration?: number; + longBreakDuration?: number; + sessionsBeforeLongBreak?: number; + sound?: string; +} + +export interface Preset { + id: string; + userId: string; + type: PresetType; + name: string; + durationSeconds: number; + settings: PresetSettings | null; + createdAt: string; +} + +export interface CreatePresetInput { + type: PresetType; + name: string; + durationSeconds: number; + settings?: PresetSettings; +} + +export interface UpdatePresetInput { + name?: string; + durationSeconds?: number; + settings?: PresetSettings; +} + +export const DEFAULT_POMODORO_SETTINGS: PresetSettings = { + workDuration: 25 * 60, + breakDuration: 5 * 60, + longBreakDuration: 15 * 60, + sessionsBeforeLongBreak: 4, +}; diff --git a/apps/times/packages/shared/src/types/index.ts b/apps/times/packages/shared/src/types/index.ts index f81df2d8a..8c251d1c3 100644 --- a/apps/times/packages/shared/src/types/index.ts +++ b/apps/times/packages/shared/src/types/index.ts @@ -152,3 +152,7 @@ export interface SavedFilter { criteria: FilterCriteria; createdAt: string; } + +// ─── Clock Types ──────────────────────────────────────── + +export * from './clock'; diff --git a/apps/todo/apps/web/src/lib/components/pages/PageEditBar.svelte b/apps/todo/apps/web/src/lib/components/pages/PageEditBar.svelte new file mode 100644 index 000000000..3a1e9acc9 --- /dev/null +++ b/apps/todo/apps/web/src/lib/components/pages/PageEditBar.svelte @@ -0,0 +1,396 @@ + + +
+ +
+ {#each ICONS as icon (icon.id)} + + {/each} +
+ + + + + + {#if showFilters} +
+ +
+ Priorität +
+ {#each PRIORITIES as p (p.id)} + + {/each} +
+
+ + +
+ Zeitraum +
+ {#each DATE_RANGES as dr (dr.id)} + + {/each} +
+
+ + +
+ +
+
+ {/if} + + +
+
+ {#if !isFirst && onMoveLeft} + + {/if} + {#if !isLast && onMoveRight} + + {/if} +
+ +
+
+ + diff --git a/apps/todo/apps/web/src/lib/components/pages/PagePicker.svelte b/apps/todo/apps/web/src/lib/components/pages/PagePicker.svelte index c086fed5b..c4d7d40c8 100644 --- a/apps/todo/apps/web/src/lib/components/pages/PagePicker.svelte +++ b/apps/todo/apps/web/src/lib/components/pages/PagePicker.svelte @@ -9,6 +9,7 @@ Calendar, TagSimple, X, + Plus, } from '@manacore/shared-icons'; export interface PageOption { @@ -22,10 +23,11 @@ interface Props { onSelect: (pageId: string) => void; onClose: () => void; + onCreateCustom?: () => void; activePageIds?: string[]; } - let { onSelect, onClose, activePageIds = [] }: Props = $props(); + let { onSelect, onClose, onCreateCustom, activePageIds = [] }: Props = $props(); const PAGE_OPTIONS: PageOption[] = [ { @@ -113,7 +115,23 @@ {/each} - {#if availableOptions.length === 0} + {#if availableOptions.length > 0 && onCreateCustom} +
+ {/if} + + {#if onCreateCustom} + + {/if} + + {#if availableOptions.length === 0 && !onCreateCustom}

Alle Seiten sind bereits geöffnet

@@ -237,6 +255,15 @@ background: color-mix(in srgb, currentColor 10%, transparent); } + .custom-icon { + color: var(--color-primary, #8b5cf6); + background: color-mix(in srgb, var(--color-primary, #8b5cf6) 10%, transparent); + } + + .custom-option { + margin-top: 0.25rem; + } + .option-text { display: flex; flex-direction: column; diff --git a/apps/todo/apps/web/src/lib/components/pages/TodoPage.svelte b/apps/todo/apps/web/src/lib/components/pages/TodoPage.svelte index 8c1e86868..0baab80d3 100644 --- a/apps/todo/apps/web/src/lib/components/pages/TodoPage.svelte +++ b/apps/todo/apps/web/src/lib/components/pages/TodoPage.svelte @@ -1,31 +1,72 @@ -
+
+ + {#if editMode && isCustom && customPageConfig && onUpdateConfig && onDelete} + + {/if} +
e.preventDefault()}> - {#if pageId === 'completed'} + {#if pageId === 'completed' || showCompleted} {#each filteredTasks as task (task.id)}
{/if} -
- - - - { - if (e.key === 'Enter') handleInlineCreate(); - }} - /> -
+ {#if !editMode} +
+ + + + { + if (e.key === 'Enter') handleInlineCreate(); + }} + /> +
+ {/if} {/if}
@@ -318,6 +461,17 @@ 0 0 0 1px rgba(255, 255, 255, 0.06); } + .todo-page.editing { + box-shadow: + 0 2px 12px rgba(139, 92, 246, 0.12), + 0 0 0 2px rgba(139, 92, 246, 0.3); + } + :global(.dark) .todo-page.editing { + box-shadow: + 0 2px 12px rgba(139, 92, 246, 0.2), + 0 0 0 2px rgba(139, 92, 246, 0.4); + } + .todo-page.maximized { position: fixed; inset: 0; @@ -408,6 +562,12 @@ gap: 0.5rem; } + .header-icon { + flex-shrink: 0; + display: flex; + align-items: center; + } + .color-dot { width: 0.625rem; height: 0.625rem; @@ -470,6 +630,15 @@ color: #f3f4f6; } + .delete-preset:hover { + color: #ef4444; + background: rgba(239, 68, 68, 0.08); + } + :global(.dark) .delete-preset:hover { + color: #ef4444; + background: rgba(239, 68, 68, 0.15); + } + .page-body { flex: 1; padding: 0.75rem 1rem; diff --git a/apps/todo/apps/web/src/lib/stores/minimized-pages.svelte.ts b/apps/todo/apps/web/src/lib/stores/minimized-pages.svelte.ts index 387ac1fed..8793ecd40 100644 --- a/apps/todo/apps/web/src/lib/stores/minimized-pages.svelte.ts +++ b/apps/todo/apps/web/src/lib/stores/minimized-pages.svelte.ts @@ -5,6 +5,7 @@ * Page registers callbacks (restore/remove/togglePicker) and syncs its openPages. */ import type { MinimizedPage } from '@manacore/shared-ui'; +import type { PageConfig } from '$lib/stores/settings.svelte'; export const PAGE_META: Record = { todo: { title: 'To Do', color: '#6B7280' }, @@ -17,13 +18,43 @@ export const PAGE_META: Record = { 'no-date': { title: 'Ohne Datum', color: '#6B7280' }, }; +/** Icon-to-color mapping for custom pages */ +const ICON_COLORS: Record = { + warning: '#EF4444', + calendar: '#3B82F6', + 'calendar-dots': '#8B5CF6', + check: '#22C55E', + star: '#F59E0B', + lightning: '#F97316', + clock: '#6B7280', + fire: '#EF4444', + leaf: '#22C55E', + heart: '#EC4899', +}; + +/** Get display meta for a page, supporting both presets and custom pages */ +export function getPageMeta( + pageId: string, + customPages: PageConfig[] +): { title: string; color: string } { + if (PAGE_META[pageId]) return PAGE_META[pageId]; + const custom = customPages.find((p) => p.id === pageId); + if (custom) { + return { + title: custom.label, + color: ICON_COLORS[custom.icon ?? 'star'] ?? '#8B5CF6', + }; + } + return { title: pageId, color: '#6B7280' }; +} + export interface MinimizedPagesContext { /** Reactive list of minimized pages (read by layout for rendering) */ readonly pages: MinimizedPage[]; /** Whether there are any minimized pages */ readonly hasPages: boolean; /** Sync open pages state from page component */ - sync(openPages: { id: string; minimized: boolean }[]): void; + sync(openPages: { id: string; minimized: boolean }[], customPages?: PageConfig[]): void; /** Clear all pages (called on page unmount) */ clear(): void; /** Restore a minimized page — delegates to registered callback */ @@ -59,11 +90,11 @@ export function createMinimizedPagesContext(): MinimizedPagesContext { get hasPages() { return pages.length > 0; }, - sync(openPages) { + sync(openPages, customPages: PageConfig[] = []) { pages = openPages .filter((p) => p.minimized) .map((p) => { - const meta = PAGE_META[p.id] ?? { title: p.id, color: '#6B7280' }; + const meta = getPageMeta(p.id, customPages); return { id: p.id, title: meta.title, color: meta.color }; }); },