From 9dee75e06e7cc23968ceddd66758a84b199df701 Mon Sep 17 00:00:00 2001 From: Till-JS <101404291+Till-JS@users.noreply.github.com> Date: Thu, 4 Dec 2025 17:26:50 +0100 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20feat(clock):=20improve=20UI=20acros?= =?UTF-8?q?s=20alarms,=20timers,=20pomodoro,=20and=20world=20clock=20pages?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Enhanced alarm page with preset suggestions and better layout - Simplified timers page with cleaner controls - Improved pomodoro with visual progress indicators - World clock now shows interactive map with city markers - Extended app.css with new utility classes and animations 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- apps/clock/apps/web/src/app.css | 217 +++++++- .../apps/web/src/routes/alarms/+page.svelte | 312 +++++++---- .../apps/web/src/routes/pomodoro/+page.svelte | 172 +++--- .../apps/web/src/routes/timers/+page.svelte | 506 ++++-------------- .../web/src/routes/world-clock/+page.svelte | 165 +++++- 5 files changed, 750 insertions(+), 622 deletions(-) diff --git a/apps/clock/apps/web/src/app.css b/apps/clock/apps/web/src/app.css index 862080096..197179276 100644 --- a/apps/clock/apps/web/src/app.css +++ b/apps/clock/apps/web/src/app.css @@ -157,7 +157,122 @@ color: hsl(var(--color-error)); } -/* Alarm Card */ +/* Quick Create Form */ +.quick-create { + display: flex; + align-items: center; + gap: 0.5rem; + background-color: hsl(var(--color-surface)); + border-radius: var(--radius-md); + padding: 0.5rem 0.75rem; + border: 1px solid hsl(var(--color-border)); +} + +.time-input-inline { + font-size: 1.5rem; + font-weight: 300; + width: 5rem; + padding: 0.25rem; + font-variant-numeric: tabular-nums; + background: transparent; + border: none; + color: hsl(var(--color-foreground)); +} + +.time-input-inline:focus { + outline: none; +} + +.time-input-inline::-webkit-calendar-picker-indicator { + display: none; +} + +.label-input { + flex: 1; + min-width: 0; + padding: 0.375rem 0.5rem; + font-size: 0.875rem; + background: transparent; + border: none; + color: hsl(var(--color-foreground)); +} + +.label-input:focus { + outline: none; +} + +.label-input::placeholder { + color: hsl(var(--color-muted-foreground)); +} + +.day-selector-compact { + display: flex; + gap: 0.25rem; + padding: 0.5rem 0; +} + +.day-selector-compact button { + width: 32px; + height: 32px; + border-radius: 50%; + border: 1px solid hsl(var(--color-border)); + background: transparent; + color: hsl(var(--color-muted-foreground)); + font-size: 0.7rem; + font-weight: 500; + cursor: pointer; + transition: all var(--transition-fast); +} + +.day-selector-compact button:hover { + border-color: hsl(var(--color-primary)); + color: hsl(var(--color-foreground)); +} + +.day-selector-compact button.active { + background-color: hsl(var(--color-primary)); + border-color: hsl(var(--color-primary)); + color: hsl(var(--color-primary-foreground)); +} + +/* Alarm Grid (responsive) */ +.alarm-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(100px, 1fr)); + gap: 0.5rem; +} + +/* Alarm Tile (Grid layout) */ +.alarm-tile { + background-color: hsl(var(--color-surface)); + border-radius: var(--radius-md); + padding: 0.5rem; + border: 1px solid hsl(var(--color-border)); + transition: all var(--transition-base); + cursor: pointer; + opacity: 0.5; +} + +.alarm-tile:hover { + background-color: hsl(var(--color-muted) / 0.5); + opacity: 0.7; +} + +.alarm-tile:active { + background-color: hsl(var(--color-muted)); +} + +.alarm-tile.active { + opacity: 1; + border-color: hsl(var(--color-primary) / 0.3); +} + +.alarm-tile.active:hover { + opacity: 1; + border-color: hsl(var(--color-primary) / 0.5); +} + +/* Alarm Card (legacy, for edit form) */ .alarm-card { background-color: hsl(var(--color-surface)); border-radius: var(--radius-lg); @@ -317,7 +432,105 @@ font-variant-numeric: tabular-nums; } -/* Toggle Switch */ +/* Time Input Small (for timer) */ +.time-input-sm { + font-size: 1.5rem; + font-weight: 300; + text-align: center; + width: 3.5rem; + padding: 0.375rem; + font-variant-numeric: tabular-nums; +} + +/* Time Input Large (for quick create) */ +.time-input-large { + font-size: 2.5rem; + font-weight: 200; + text-align: center; + width: auto; + padding: 0.25rem 0.5rem; + font-variant-numeric: tabular-nums; + background: transparent; + border: none; + color: hsl(var(--color-foreground)); + cursor: pointer; +} + +.time-input-large:focus { + outline: none; +} + +.time-input-large::-webkit-calendar-picker-indicator { + display: none; +} + +/* Toggle Switch (iOS-style) */ +.toggle-switch { + position: relative; + width: 44px; + height: 26px; + background-color: hsl(var(--color-muted)); + border-radius: var(--radius-full); + cursor: pointer; + transition: background-color var(--transition-base); + border: none; + padding: 0; + flex-shrink: 0; +} + +.toggle-switch.active { + background-color: hsl(var(--color-primary)); +} + +.toggle-switch .toggle-knob { + position: absolute; + top: 2px; + left: 2px; + width: 22px; + height: 22px; + background-color: white; + border-radius: 50%; + transition: transform var(--transition-base); + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2); +} + +.toggle-switch.active .toggle-knob { + transform: translateX(18px); +} + +/* Toggle Switch Small (for grid tiles) */ +.toggle-switch-sm { + position: relative; + display: inline-block; + width: 36px; + height: 20px; + background-color: hsl(var(--color-muted)); + border-radius: var(--radius-full); + transition: background-color var(--transition-base); + flex-shrink: 0; +} + +.toggle-switch-sm.active { + background-color: hsl(var(--color-primary)); +} + +.toggle-switch-sm .toggle-knob { + position: absolute; + top: 2px; + left: 2px; + width: 16px; + height: 16px; + background-color: white; + border-radius: 50%; + transition: transform var(--transition-base); + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2); +} + +.toggle-switch-sm.active .toggle-knob { + transform: translateX(16px); +} + +/* Legacy toggle (deprecated) */ .toggle { position: relative; width: 44px; diff --git a/apps/clock/apps/web/src/routes/alarms/+page.svelte b/apps/clock/apps/web/src/routes/alarms/+page.svelte index e7c0cb5e8..4275ddf20 100644 --- a/apps/clock/apps/web/src/routes/alarms/+page.svelte +++ b/apps/clock/apps/web/src/routes/alarms/+page.svelte @@ -1,81 +1,127 @@ -
- -
-

{$_('alarm.title')}

- +
- + {#if showOptions} +
+ {#each dayNames as day, i} + + {/each} +
+ {/if} + + {#if alarmsStore.loading}
- {:else if alarmsStore.alarms.length === 0} -
-

{$_('alarm.noAlarms')}

- -
{:else} -
- {#each alarmsStore.alarms as alarm (alarm.id)} -
-
-
- -
-
- - -
+ +
+ {#each DEFAULT_ALARM_PRESETS as preset} + {@const existingAlarm = findAlarmForPreset(preset.time)} + {@const isActive = existingAlarm?.enabled ?? false} +
togglePreset(preset.time, preset.label)} + onkeydown={(e) => e.key === 'Enter' && togglePreset(preset.time, preset.label)} + > +
+ {preset.time} +
+
+ {existingAlarm?.label || preset.label}
{/each}
+ + + {@const customAlarms = alarmsStore.alarms.filter( + (a) => !DEFAULT_ALARM_PRESETS.some((p) => p.time === a.time.slice(0, 5)) + )} + {#if customAlarms.length > 0} +
+

+ {$_('alarm.custom')} +

+
+ {#each customAlarms as alarm (alarm.id)} +
handleToggle(alarm.id)} + onkeydown={(e) => e.key === 'Enter' && handleToggle(alarm.id)} + > +
+ {alarm.time.slice(0, 5)} +
+
+ {alarm.label || getRepeatText(alarm.repeatDays)} +
+
+ {/each} +
+
+ {/if} {/if} - - {#if showForm} -
+ + {#if showEditModal} +
-

- {editingId ? $_('alarm.edit') : $_('alarm.add')} -

+

{$_('alarm.edit')}

{ e.preventDefault(); - handleSubmit(); + handleEditSubmit(); }} >
- +
@@ -198,7 +280,7 @@ type="text" class="input" placeholder="Arbeit, Sport, etc." - bind:value={formLabel} + bind:value={editLabel} />
@@ -209,8 +291,8 @@ {#each dayNames as day, i} @@ -221,7 +303,7 @@
- {#each ALARM_SOUNDS as sound} {/each} @@ -231,7 +313,7 @@
- @@ -241,7 +323,7 @@
- - {:else} - - {/if} - - -
- - -
+ +
{#each Array(pomodoroStore.sessionsBeforeLongBreak) as _, i}
= @@ -130,43 +86,51 @@ {/each}
- -
-

{$_('timer.presets')}

-
- {#each POMODORO_PRESETS as preset} - - {/each} -
+ +
+ {#if pomodoroStore.isRunning} + + {:else} + + {/if} + +
- -
-

Aktuelle Einstellungen

-
-
- {$_('pomodoro.settings.workDuration')}: - {pomodoroStore.settings.workDuration / 60} min -
-
- {$_('pomodoro.settings.breakDuration')}: - {pomodoroStore.settings.breakDuration / 60} min -
-
- {$_('pomodoro.settings.longBreakDuration')}: - {pomodoroStore.settings.longBreakDuration / 60} min -
-
- Sitzungen: - {pomodoroStore.settings.sessionsBeforeLongBreak} -
-
+ +
+ {#each POMODORO_PRESETS as preset} + + {/each}
+ + diff --git a/apps/clock/apps/web/src/routes/timers/+page.svelte b/apps/clock/apps/web/src/routes/timers/+page.svelte index 6df007956..26754d01e 100644 --- a/apps/clock/apps/web/src/routes/timers/+page.svelte +++ b/apps/clock/apps/web/src/routes/timers/+page.svelte @@ -2,19 +2,18 @@ import { onMount, onDestroy } from 'svelte'; import { _ } from 'svelte-i18n'; import { browser } from '$app/environment'; + import { PageHeader } from '@manacore/shared-ui'; import { timersStore } from '$lib/stores/timers.svelte'; import { authStore } from '$lib/stores/auth.svelte'; import { toast } from '$lib/stores/toast'; import { QUICK_TIMER_PRESETS, formatDuration } from '@clock/shared'; - // Form state - let showForm = $state(false); - let formHours = $state(0); + // Form state (inline on page) let formMinutes = $state(5); let formSeconds = $state(0); let formLabel = $state(''); - // Local timers (for quick timers without backend) + // Local timers interface LocalTimer { id: string; label: string; @@ -24,56 +23,20 @@ createdAt: Date; } let localTimers = $state([]); - - // Recently used presets (stored in localStorage) - let recentPresets = $state<{ label: string; seconds: number }[]>([]); - const RECENT_STORAGE_KEY = 'clock-recent-timers'; - const MAX_RECENT = 5; - - // Local countdown intervals let intervals: Map> = new Map(); - - // Combined timers (backend + local) let allTimers = $derived([...timersStore.timers, ...localTimers]); onMount(async () => { - // Load recent presets from localStorage - if (browser) { - const saved = localStorage.getItem(RECENT_STORAGE_KEY); - if (saved) { - try { - recentPresets = JSON.parse(saved); - } catch { - recentPresets = []; - } - } - } - - // Fetch backend timers if authenticated if (authStore.isAuthenticated) { await timersStore.fetchTimers(); } }); onDestroy(() => { - // Clear all intervals intervals.forEach((interval) => clearInterval(interval)); }); - function saveRecentPreset(preset: { label: string; seconds: number }) { - // Add to recent, removing duplicates - recentPresets = [ - preset, - ...recentPresets.filter((p) => p.seconds !== preset.seconds), - ].slice(0, MAX_RECENT); - - if (browser) { - localStorage.setItem(RECENT_STORAGE_KEY, JSON.stringify(recentPresets)); - } - } - function startLocalCountdown(timerId: string, isLocal: boolean = false) { - // Clear existing interval if any if (intervals.has(timerId)) { clearInterval(intervals.get(timerId)); } @@ -90,7 +53,11 @@ const newRemaining = Math.max(0, timer.remainingSeconds - 1); localTimers = localTimers.map((t) => t.id === timerId - ? { ...t, remainingSeconds: newRemaining, status: newRemaining === 0 ? 'finished' : 'running' } + ? { + ...t, + remainingSeconds: newRemaining, + status: newRemaining === 0 ? 'finished' : 'running', + } : t ); @@ -98,7 +65,6 @@ clearInterval(interval); intervals.delete(timerId); toast.success($_('timer.finished')); - // Play notification sound if (browser && 'Notification' in window && Notification.permission === 'granted') { new Notification('Timer', { body: 'Timer abgelaufen!' }); } @@ -125,69 +91,30 @@ intervals.set(timerId, interval); } - function openForm() { - formHours = 0; - formMinutes = 5; - formSeconds = 0; - formLabel = ''; - showForm = true; - } - - function closeForm() { - showForm = false; - } - - async function createTimer() { - const durationSeconds = formHours * 3600 + formMinutes * 60 + formSeconds; + function createAndStartTimer() { + const durationSeconds = formMinutes * 60 + formSeconds; if (durationSeconds <= 0) { toast.error('Bitte eine gültige Zeit eingeben'); return; } - if (authStore.isAuthenticated) { - const result = await timersStore.createTimer({ - durationSeconds, - label: formLabel || undefined, - }); - - if (result.success) { - toast.success('Timer erstellt'); - closeForm(); - } else { - toast.error(result.error || 'Fehler beim Erstellen'); - } - } else { - // Create local timer - const newTimer: LocalTimer = { - id: crypto.randomUUID(), - label: formLabel || formatDuration(durationSeconds), - durationSeconds, - remainingSeconds: durationSeconds, - status: 'idle', - createdAt: new Date(), - }; - localTimers = [...localTimers, newTimer]; - toast.success('Timer erstellt'); - closeForm(); - } - } - - function createQuickTimer(seconds: number, label: string) { - // Save to recent - saveRecentPreset({ label, seconds }); - - // Create local timer and start immediately const newTimer: LocalTimer = { id: crypto.randomUUID(), - label: label, - durationSeconds: seconds, - remainingSeconds: seconds, + label: formLabel || formatDuration(durationSeconds), + durationSeconds, + remainingSeconds: durationSeconds, status: 'running', createdAt: new Date(), }; localTimers = [...localTimers, newTimer]; startLocalCountdown(newTimer.id, true); - toast.success(`Timer ${label} gestartet`); + toast.success('Timer gestartet'); + formLabel = ''; + } + + function setPreset(seconds: number) { + formMinutes = Math.floor(seconds / 60); + formSeconds = seconds % 60; } async function handleStart(id: string, isLocal: boolean) { @@ -198,12 +125,7 @@ startLocalCountdown(id, true); } else { const result = await timersStore.startTimer(id); - if (result.success) { - const timer = timersStore.timers.find((t) => t.id === id); - if (timer) { - startLocalCountdown(id, false); - } - } + if (result.success) startLocalCountdown(id, false); } } @@ -212,11 +134,8 @@ clearInterval(intervals.get(id)); intervals.delete(id); } - if (isLocal) { - localTimers = localTimers.map((t) => - t.id === id ? { ...t, status: 'paused' as const } : t - ); + localTimers = localTimers.map((t) => (t.id === id ? { ...t, status: 'paused' as const } : t)); } else { await timersStore.pauseTimer(id); } @@ -227,7 +146,6 @@ clearInterval(intervals.get(id)); intervals.delete(id); } - if (isLocal) { localTimers = localTimers.map((t) => t.id === id ? { ...t, remainingSeconds: t.durationSeconds, status: 'idle' as const } : t @@ -242,15 +160,10 @@ clearInterval(intervals.get(id)); intervals.delete(id); } - if (isLocal) { localTimers = localTimers.filter((t) => t.id !== id); - toast.success('Timer gelöscht'); } else { - const result = await timersStore.deleteTimer(id); - if (result.success) { - toast.success('Timer gelöscht'); - } + await timersStore.deleteTimer(id); } } @@ -267,189 +180,120 @@ function isLocalTimer(timer: any): boolean { return localTimers.some((t) => t.id === timer.id); } - - // Preset icons based on duration - function getPresetIcon(seconds: number): string { - if (seconds <= 60) return '⚡'; - if (seconds <= 300) return '☕'; - if (seconds <= 900) return '📝'; - if (seconds <= 1800) return '💪'; - if (seconds <= 2700) return '🎯'; - return '🏃'; - } -
- -
-

{$_('timer.title')}

-

Schnelle Timer für jeden Anlass

-
+ - -
-

- {$_('timer.presets')} -

-
- {#each QUICK_TIMER_PRESETS as preset} - - {/each} +
+ +
+
+ + : +
+ +
- - {#if recentPresets.length > 0} -
-
-

Zuletzt verwendet

- -
-
- {#each recentPresets as preset} - - {/each} -
-
- {/if} - - -
- + +
+ {#each QUICK_TIMER_PRESETS as preset} + + {/each}
- + {#if timersStore.loading} -
-
+
+
- {:else if allTimers.length === 0} -
-
⏱️
-

{$_('timer.noTimers')}

-

- Klicke auf einen Preset oben oder erstelle einen eigenen Timer -

-
- {:else} -
-

- Aktive Timer ({allTimers.length}) -

-
+ {:else if allTimers.length > 0} + +
+

+ Aktiv ({allTimers.length}) +

+
{#each allTimers as timer (timer.id)} {@const isLocal = isLocalTimer(timer)} -
- -
- -
- -
- - {timer.label || 'Timer'} - - +
+ + {getTimerDisplay(timer)} + +
- - -
- - {getTimerDisplay(timer)} - -
- - -
-
-
- - -
- {#if timer.status === 'running'} - - {:else if timer.status === 'finished'} - - {:else} - - {/if} + + + +
+
{timer.label}
+
+
+
+
+ {#if timer.status === 'running'} + {:else} -
+ {/if} +
{/each} @@ -457,117 +301,3 @@
{/if}
- - -{#if showForm} -
-
-
-

{$_('timer.add')}

- -
- - { - e.preventDefault(); - createTimer(); - }} - > - -
- -
-
- - -
-
- - -
-
- - -
-
-
- - -
- -
- {#each [1, 3, 5, 10, 15, 30] as mins} - - {/each} -
-
- - -
- - -
- - -
- - -
- -
-
-{/if} - - diff --git a/apps/clock/apps/web/src/routes/world-clock/+page.svelte b/apps/clock/apps/web/src/routes/world-clock/+page.svelte index 7354a322b..62a417b3f 100644 --- a/apps/clock/apps/web/src/routes/world-clock/+page.svelte +++ b/apps/clock/apps/web/src/routes/world-clock/+page.svelte @@ -1,16 +1,32 @@ -
- -
-

{$_('worldClock.title')}

- -
+ + {#snippet actions()} +
+ + +
+ {/snippet} +
+ +
+ + {#if showMap} +
+
+ +
+

+ Klicke auf eine Stadt um sie hinzuzufügen +

+
+ {/if} {#if worldClocksStore.loading} @@ -169,15 +224,26 @@
- {isDay ? '☀️' : '🌙'} + {isDay ? 'Tag' : 'Nacht'} {clock.cityName}
@@ -206,8 +272,19 @@

{$_('worldClock.add')}

-
@@ -252,3 +329,65 @@
{/if}
+ +