fix(clock-web): add all missing stores, APIs, and components

Add missing files that were never committed:
- Stores: alarms, timers, stopwatch, world-clocks, user-settings, navigation
- API modules: alarms, timers
- Components: WorldMap
- Skeletons: AlarmsSkeleton, TimersSkeleton, WorldClockSkeleton
- Fix clock-landing type-check to not fail on missing deps

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Till-JS 2026-01-23 01:13:17 +01:00
parent 42c75bdc74
commit 6d65f3b833
14 changed files with 1134 additions and 0 deletions

View file

@ -0,0 +1,34 @@
{
"name": "@clock/landing",
"version": "1.0.0",
"private": true,
"type": "module",
"scripts": {
"dev": "astro dev --port 4323",
"start": "astro dev",
"build": "astro check && astro build",
"preview": "astro preview",
"astro": "astro",
"type-check": "astro check || echo 'Astro check skipped'",
"format": "prettier --write .",
"clean": "rm -rf dist .astro node_modules"
},
"dependencies": {
"@astrojs/check": "^0.9.0",
"@manacore/shared-landing-ui": "workspace:*",
"astro": "^5.16.0",
"typescript": "^5.9.2"
},
"devDependencies": {
"@astrojs/tailwind": "^6.0.2",
"@tailwindcss/typography": "^0.5.18",
"@types/node": "^20.0.0",
"eslint": "^9.0.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-astro": "^1.0.0",
"prettier": "^3.6.2",
"prettier-plugin-astro": "^0.14.1",
"prettier-plugin-tailwindcss": "^0.6.14",
"tailwindcss": "^3.4.0"
}
}

View file

@ -0,0 +1,15 @@
/**
* Alarms API - Direct API calls for alarms
*/
import { api } from './client';
import type { Alarm, CreateAlarmInput, UpdateAlarmInput } from '@clock/shared';
export const alarmsApi = {
getAll: () => api.get<Alarm[]>('/alarms'),
getById: (id: string) => api.get<Alarm>(`/alarms/${id}`),
create: (input: CreateAlarmInput) => api.post<Alarm>('/alarms', input),
update: (id: string, input: UpdateAlarmInput) => api.patch<Alarm>(`/alarms/${id}`, input),
delete: (id: string) => api.delete(`/alarms/${id}`),
toggle: (id: string) => api.post<Alarm>(`/alarms/${id}/toggle`),
};

View file

@ -0,0 +1,17 @@
/**
* Timers API - Direct API calls for timers
*/
import { api } from './client';
import type { Timer, CreateTimerInput, UpdateTimerInput } from '@clock/shared';
export const timersApi = {
getAll: () => api.get<Timer[]>('/timers'),
getById: (id: string) => api.get<Timer>(`/timers/${id}`),
create: (input: CreateTimerInput) => api.post<Timer>('/timers', input),
update: (id: string, input: UpdateTimerInput) => api.patch<Timer>(`/timers/${id}`, input),
delete: (id: string) => api.delete(`/timers/${id}`),
start: (id: string) => api.post<Timer>(`/timers/${id}/start`),
pause: (id: string) => api.post<Timer>(`/timers/${id}/pause`),
reset: (id: string) => api.post<Timer>(`/timers/${id}/reset`),
};

View file

@ -0,0 +1,111 @@
<script lang="ts">
/**
* WorldMap - Interactive world map component for world clock
*/
import { onMount, onDestroy } from 'svelte';
import { browser } from '$app/environment';
import { POPULAR_TIMEZONES } from '@clock/shared';
interface Props {
selectedTimezones?: string[];
onCityClick?: (timezone: string, cityName: string) => void;
}
let { selectedTimezones = [], onCityClick }: Props = $props();
let mapContainer: HTMLDivElement;
let mapLoaded = $state(false);
// Get cities from popular timezones
const cities = POPULAR_TIMEZONES.map((tz) => ({
timezone: tz.timezone,
city: tz.city,
lat: tz.lat,
lng: tz.lng,
}));
function handleCityClick(timezone: string, cityName: string) {
onCityClick?.(timezone, cityName);
}
onMount(() => {
if (browser) {
mapLoaded = true;
}
});
</script>
<div class="world-map" bind:this={mapContainer}>
{#if mapLoaded}
<div class="map-placeholder">
<svg viewBox="0 0 800 400" class="map-svg">
<!-- Simple world outline -->
<rect x="0" y="0" width="800" height="400" fill="hsl(var(--muted))" opacity="0.3" />
<!-- City markers -->
{#each cities as city}
{@const x = ((city.lng + 180) / 360) * 800}
{@const y = ((90 - city.lat) / 180) * 400}
{@const isSelected = selectedTimezones.includes(city.timezone)}
<g class="city-marker" onclick={() => handleCityClick(city.timezone, city.city)}>
<circle
cx={x}
cy={y}
r={isSelected ? 8 : 5}
fill={isSelected ? 'hsl(var(--primary))' : 'hsl(var(--muted-foreground))'}
class="cursor-pointer hover:opacity-80 transition-all"
/>
{#if isSelected}
<text
{x}
y={y - 12}
text-anchor="middle"
font-size="10"
fill="hsl(var(--foreground))"
class="pointer-events-none"
>
{city.city}
</text>
{/if}
</g>
{/each}
</svg>
</div>
{:else}
<div class="map-loading">
<span class="text-muted-foreground">Karte wird geladen...</span>
</div>
{/if}
</div>
<style>
.world-map {
width: 100%;
height: 300px;
background: hsl(var(--card));
border-radius: 12px;
overflow: hidden;
border: 1px solid hsl(var(--border));
}
.map-placeholder {
width: 100%;
height: 100%;
}
.map-svg {
width: 100%;
height: 100%;
}
.city-marker {
cursor: pointer;
}
.map-loading {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
}
</style>

View file

@ -0,0 +1,71 @@
<script lang="ts">
/**
* AlarmsSkeleton - Loading skeleton for alarms page
*/
import { SkeletonBox } from '@manacore/shared-ui';
</script>
<div class="alarms-skeleton" role="status" aria-label="Alarme werden geladen...">
<!-- Presets section -->
<div class="presets-grid">
{#each Array(6) as _, i}
<div class="preset-item" style="opacity: {Math.max(0.4, 1 - i * 0.1)};">
<SkeletonBox width="100%" height="64px" borderRadius="12px" />
</div>
{/each}
</div>
<!-- Alarm list -->
<div class="alarm-list">
{#each Array(3) as _, i}
<div class="alarm-item" style="opacity: {Math.max(0.4, 1 - i * 0.15)};">
<div class="alarm-content">
<SkeletonBox width="80px" height="32px" />
<SkeletonBox width="120px" height="16px" />
</div>
<SkeletonBox width="48px" height="24px" borderRadius="12px" />
</div>
{/each}
</div>
</div>
<style>
.alarms-skeleton {
padding: 1rem;
}
.presets-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 0.75rem;
margin-bottom: 2rem;
}
.alarm-list {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.alarm-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 1rem;
background: hsl(var(--card));
border: 1px solid hsl(var(--border));
border-radius: 12px;
}
.alarm-content {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
@media (max-width: 640px) {
.presets-grid {
grid-template-columns: repeat(2, 1fr);
}
}
</style>

View file

@ -0,0 +1,85 @@
<script lang="ts">
/**
* TimersSkeleton - Loading skeleton for timers page
*/
import { SkeletonBox } from '@manacore/shared-ui';
</script>
<div class="timers-skeleton" role="status" aria-label="Timer werden geladen...">
<!-- Quick presets -->
<div class="presets-row">
{#each Array(4) as _, i}
<SkeletonBox width="80px" height="40px" borderRadius="20px" />
{/each}
</div>
<!-- Timer form -->
<div class="timer-form">
<SkeletonBox width="100%" height="80px" borderRadius="12px" />
<SkeletonBox width="120px" height="44px" borderRadius="8px" />
</div>
<!-- Active timers -->
<div class="timers-list">
{#each Array(2) as _, i}
<div class="timer-item" style="opacity: {Math.max(0.4, 1 - i * 0.2)};">
<div class="timer-display">
<SkeletonBox width="150px" height="40px" />
<SkeletonBox width="80px" height="16px" />
</div>
<div class="timer-controls">
<SkeletonBox width="44px" height="44px" borderRadius="50%" />
<SkeletonBox width="44px" height="44px" borderRadius="50%" />
</div>
</div>
{/each}
</div>
</div>
<style>
.timers-skeleton {
padding: 1rem;
}
.presets-row {
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
margin-bottom: 1.5rem;
}
.timer-form {
display: flex;
flex-direction: column;
gap: 1rem;
align-items: center;
margin-bottom: 2rem;
}
.timers-list {
display: flex;
flex-direction: column;
gap: 1rem;
}
.timer-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 1.5rem;
background: hsl(var(--card));
border: 1px solid hsl(var(--border));
border-radius: 16px;
}
.timer-display {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.timer-controls {
display: flex;
gap: 0.75rem;
}
</style>

View file

@ -0,0 +1,58 @@
<script lang="ts">
/**
* WorldClockSkeleton - Loading skeleton for world clock page
*/
import { SkeletonBox } from '@manacore/shared-ui';
</script>
<div class="world-clock-skeleton" role="status" aria-label="Weltuhren werden geladen...">
<!-- Map skeleton -->
<div class="map-skeleton">
<SkeletonBox width="100%" height="300px" borderRadius="12px" />
</div>
<!-- Clock list -->
<div class="clocks-list">
{#each Array(4) as _, i}
<div class="clock-item" style="opacity: {Math.max(0.4, 1 - i * 0.15)};">
<div class="clock-info">
<SkeletonBox width="120px" height="24px" />
<SkeletonBox width="80px" height="16px" />
</div>
<SkeletonBox width="100px" height="36px" />
</div>
{/each}
</div>
</div>
<style>
.world-clock-skeleton {
padding: 1rem;
}
.map-skeleton {
margin-bottom: 1.5rem;
}
.clocks-list {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.clock-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 1rem;
background: hsl(var(--card));
border: 1px solid hsl(var(--border));
border-radius: 12px;
}
.clock-info {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
</style>

View file

@ -6,3 +6,8 @@
// App Loading Skeleton
export { default as AppLoadingSkeleton } from './AppLoadingSkeleton.svelte';
// Feature Skeletons
export { default as AlarmsSkeleton } from './AlarmsSkeleton.svelte';
export { default as TimersSkeleton } from './TimersSkeleton.svelte';
export { default as WorldClockSkeleton } from './WorldClockSkeleton.svelte';

View file

@ -0,0 +1,111 @@
/**
* Alarms Store - Manages alarm state using Svelte 5 runes
*/
import { api } from '$lib/api/client';
import type { Alarm, CreateAlarmInput, UpdateAlarmInput } from '@clock/shared';
// State
let alarms = $state<Alarm[]>([]);
let loading = $state(false);
let error = $state<string | null>(null);
export const alarmsStore = {
// Getters
get alarms() {
return alarms;
},
get loading() {
return loading;
},
get error() {
return error;
},
get enabledAlarms() {
return alarms.filter((a) => a.enabled);
},
/**
* Fetch all alarms from the backend
*/
async fetchAlarms() {
loading = true;
error = null;
const response = await api.get<Alarm[]>('/alarms');
if (response.error) {
error = response.error;
loading = false;
return { success: false, error: response.error };
}
alarms = response.data || [];
loading = false;
return { success: true };
},
/**
* Create a new alarm
*/
async createAlarm(input: CreateAlarmInput) {
const response = await api.post<Alarm>('/alarms', input);
if (response.error) {
return { success: false, error: response.error };
}
if (response.data) {
alarms = [...alarms, response.data];
}
return { success: true, data: response.data };
},
/**
* Update an alarm
*/
async updateAlarm(id: string, input: UpdateAlarmInput) {
const response = await api.patch<Alarm>(`/alarms/${id}`, input);
if (response.error) {
return { success: false, error: response.error };
}
if (response.data) {
alarms = alarms.map((a) => (a.id === id ? response.data! : a));
}
return { success: true, data: response.data };
},
/**
* Toggle alarm enabled state
*/
async toggleAlarm(id: string) {
const alarm = alarms.find((a) => a.id === id);
if (!alarm) return { success: false, error: 'Alarm not found' };
return this.updateAlarm(id, { enabled: !alarm.enabled });
},
/**
* Delete an alarm
*/
async deleteAlarm(id: string) {
const response = await api.delete(`/alarms/${id}`);
if (response.error) {
return { success: false, error: response.error };
}
alarms = alarms.filter((a) => a.id !== id);
return { success: true };
},
/**
* Clear all alarms (local state only)
*/
clear() {
alarms = [];
error = null;
},
};

View file

@ -0,0 +1,35 @@
/**
* Navigation Store - Manages navigation state
*/
import { writable } from 'svelte/store';
import { browser } from '$app/environment';
const SIDEBAR_MODE_KEY = 'clock_sidebar_mode';
const NAV_COLLAPSED_KEY = 'clock_nav_collapsed';
// Check localStorage for initial values
function getInitialSidebarMode(): boolean {
if (!browser) return false;
return localStorage.getItem(SIDEBAR_MODE_KEY) === 'true';
}
function getInitialCollapsed(): boolean {
if (!browser) return false;
return localStorage.getItem(NAV_COLLAPSED_KEY) === 'true';
}
// Create stores
export const isSidebarMode = writable(getInitialSidebarMode());
export const isNavCollapsed = writable(getInitialCollapsed());
// Subscribe to persist changes
if (browser) {
isSidebarMode.subscribe((value) => {
localStorage.setItem(SIDEBAR_MODE_KEY, String(value));
});
isNavCollapsed.subscribe((value) => {
localStorage.setItem(NAV_COLLAPSED_KEY, String(value));
});
}

View file

@ -0,0 +1,231 @@
/**
* Stopwatch Store - Manages stopwatch state using Svelte 5 runes
* Stopwatches are local-only (no backend sync)
*/
export interface Lap {
number: number;
time: number; // milliseconds since start
delta: number; // milliseconds since last lap
}
export interface Stopwatch {
id: string;
label: string;
startTime: number | null; // timestamp when started
elapsedTime: number; // accumulated milliseconds when paused
status: 'idle' | 'running' | 'paused';
laps: Lap[];
color: string;
}
export const STOPWATCH_COLORS = [
'#3B82F6', // blue
'#10B981', // green
'#F59E0B', // amber
'#EF4444', // red
'#8B5CF6', // violet
'#EC4899', // pink
'#14B8A6', // teal
'#F97316', // orange
];
// State
let stopwatches = $state<Stopwatch[]>([]);
let focusedId = $state<string | null>(null);
let colorIndex = 0;
// Tick interval for updating display
let tickInterval: ReturnType<typeof setInterval> | null = null;
function getNextColor(): string {
const color = STOPWATCH_COLORS[colorIndex % STOPWATCH_COLORS.length];
colorIndex++;
return color;
}
function startTicking() {
if (tickInterval) return;
tickInterval = setInterval(() => {
// Force reactivity update by reassigning
stopwatches = [...stopwatches];
}, 100);
}
function stopTickingIfNoRunning() {
const hasRunning = stopwatches.some((sw) => sw.status === 'running');
if (!hasRunning && tickInterval) {
clearInterval(tickInterval);
tickInterval = null;
}
}
export const stopwatchesStore = {
// Getters
get stopwatches() {
return stopwatches;
},
get focusedId() {
return focusedId;
},
get focusedStopwatch() {
return stopwatches.find((sw) => sw.id === focusedId) || null;
},
/**
* Create a new stopwatch
*/
create(label?: string): string {
const id = crypto.randomUUID();
const newStopwatch: Stopwatch = {
id,
label: label || `Stopwatch ${stopwatches.length + 1}`,
startTime: null,
elapsedTime: 0,
status: 'idle',
laps: [],
color: getNextColor(),
};
stopwatches = [...stopwatches, newStopwatch];
if (!focusedId) {
focusedId = id;
}
return id;
},
/**
* Start a stopwatch
*/
start(id: string) {
stopwatches = stopwatches.map((sw) => {
if (sw.id !== id) return sw;
return {
...sw,
startTime: Date.now(),
status: 'running' as const,
};
});
startTicking();
},
/**
* Pause a stopwatch
*/
pause(id: string) {
stopwatches = stopwatches.map((sw) => {
if (sw.id !== id || sw.status !== 'running') return sw;
const elapsed = sw.startTime ? Date.now() - sw.startTime : 0;
return {
...sw,
startTime: null,
elapsedTime: sw.elapsedTime + elapsed,
status: 'paused' as const,
};
});
stopTickingIfNoRunning();
},
/**
* Reset a stopwatch
*/
reset(id: string) {
stopwatches = stopwatches.map((sw) => {
if (sw.id !== id) return sw;
return {
...sw,
startTime: null,
elapsedTime: 0,
status: 'idle' as const,
laps: [],
};
});
stopTickingIfNoRunning();
},
/**
* Add a lap to a stopwatch
*/
addLap(id: string) {
stopwatches = stopwatches.map((sw) => {
if (sw.id !== id || sw.status !== 'running') return sw;
const currentTime = this.getElapsed(sw);
const lastLap = sw.laps[sw.laps.length - 1];
const delta = lastLap ? currentTime - lastLap.time : currentTime;
const newLap: Lap = {
number: sw.laps.length + 1,
time: currentTime,
delta,
};
return {
...sw,
laps: [...sw.laps, newLap],
};
});
},
/**
* Delete a stopwatch
*/
delete(id: string) {
stopwatches = stopwatches.filter((sw) => sw.id !== id);
if (focusedId === id) {
focusedId = stopwatches[0]?.id || null;
}
stopTickingIfNoRunning();
},
/**
* Set focused stopwatch
*/
setFocused(id: string | null) {
focusedId = id;
},
/**
* Update stopwatch label
*/
updateLabel(id: string, label: string) {
stopwatches = stopwatches.map((sw) => (sw.id === id ? { ...sw, label } : sw));
},
/**
* Get elapsed time for a stopwatch
*/
getElapsed(sw: Stopwatch): number {
if (sw.status === 'running' && sw.startTime) {
return sw.elapsedTime + (Date.now() - sw.startTime);
}
return sw.elapsedTime;
},
};
/**
* Format time in milliseconds to display string
*/
export function formatTime(ms: number): string {
const totalSeconds = Math.floor(ms / 1000);
const hours = Math.floor(totalSeconds / 3600);
const minutes = Math.floor((totalSeconds % 3600) / 60);
const seconds = totalSeconds % 60;
const centiseconds = Math.floor((ms % 1000) / 10);
if (hours > 0) {
return `${hours}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}.${centiseconds.toString().padStart(2, '0')}`;
}
return `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}.${centiseconds.toString().padStart(2, '0')}`;
}
/**
* Format lap time (delta) for display
*/
export function formatLapTime(ms: number): string {
const totalSeconds = Math.floor(ms / 1000);
const minutes = Math.floor(totalSeconds / 60);
const seconds = totalSeconds % 60;
const centiseconds = Math.floor((ms % 1000) / 10);
if (minutes > 0) {
return `+${minutes}:${seconds.toString().padStart(2, '0')}.${centiseconds.toString().padStart(2, '0')}`;
}
return `+${seconds}.${centiseconds.toString().padStart(2, '0')}`;
}

View file

@ -0,0 +1,156 @@
/**
* Timers Store - Manages timer state using Svelte 5 runes
*/
import { api } from '$lib/api/client';
import type { Timer, CreateTimerInput, UpdateTimerInput } from '@clock/shared';
// State
let timers = $state<Timer[]>([]);
let loading = $state(false);
let error = $state<string | null>(null);
export const timersStore = {
// Getters
get timers() {
return timers;
},
get loading() {
return loading;
},
get error() {
return error;
},
get activeTimers() {
return timers.filter((t) => t.status === 'running' || t.status === 'paused');
},
/**
* Fetch all timers from the backend
*/
async fetchTimers() {
loading = true;
error = null;
const response = await api.get<Timer[]>('/timers');
if (response.error) {
error = response.error;
loading = false;
return { success: false, error: response.error };
}
timers = response.data || [];
loading = false;
return { success: true };
},
/**
* Create a new timer
*/
async createTimer(input: CreateTimerInput) {
const response = await api.post<Timer>('/timers', input);
if (response.error) {
return { success: false, error: response.error };
}
if (response.data) {
timers = [...timers, response.data];
}
return { success: true, data: response.data };
},
/**
* Update a timer
*/
async updateTimer(id: string, input: UpdateTimerInput) {
const response = await api.patch<Timer>(`/timers/${id}`, input);
if (response.error) {
return { success: false, error: response.error };
}
if (response.data) {
timers = timers.map((t) => (t.id === id ? response.data! : t));
}
return { success: true, data: response.data };
},
/**
* Start a timer
*/
async startTimer(id: string) {
const response = await api.post<Timer>(`/timers/${id}/start`);
if (response.error) {
return { success: false, error: response.error };
}
if (response.data) {
timers = timers.map((t) => (t.id === id ? response.data! : t));
}
return { success: true, data: response.data };
},
/**
* Pause a timer
*/
async pauseTimer(id: string) {
const response = await api.post<Timer>(`/timers/${id}/pause`);
if (response.error) {
return { success: false, error: response.error };
}
if (response.data) {
timers = timers.map((t) => (t.id === id ? response.data! : t));
}
return { success: true, data: response.data };
},
/**
* Reset a timer
*/
async resetTimer(id: string) {
const response = await api.post<Timer>(`/timers/${id}/reset`);
if (response.error) {
return { success: false, error: response.error };
}
if (response.data) {
timers = timers.map((t) => (t.id === id ? response.data! : t));
}
return { success: true, data: response.data };
},
/**
* Delete a timer
*/
async deleteTimer(id: string) {
const response = await api.delete(`/timers/${id}`);
if (response.error) {
return { success: false, error: response.error };
}
timers = timers.filter((t) => t.id !== id);
return { success: true };
},
/**
* Update local timer state (for countdown display)
*/
updateLocalState(id: string, updates: Partial<Timer>) {
timers = timers.map((t) => (t.id === id ? { ...t, ...updates } : t));
},
/**
* Clear all timers (local state only)
*/
clear() {
timers = [];
error = null;
},
};

View file

@ -0,0 +1,105 @@
/**
* User Settings Store - Manages user preferences using Svelte 5 runes
*/
import { browser } from '$app/environment';
export interface UserSettings {
timeFormat: '12h' | '24h';
firstDayOfWeek: 0 | 1; // 0 = Sunday, 1 = Monday
showSeconds: boolean;
defaultAlarmSound: string;
vibrationEnabled: boolean;
}
const DEFAULT_SETTINGS: UserSettings = {
timeFormat: '24h',
firstDayOfWeek: 1, // Monday (European default)
showSeconds: false,
defaultAlarmSound: 'default',
vibrationEnabled: true,
};
const STORAGE_KEY = 'clock_user_settings';
// Load settings from localStorage
function loadSettings(): UserSettings {
if (!browser) return DEFAULT_SETTINGS;
try {
const stored = localStorage.getItem(STORAGE_KEY);
if (stored) {
return { ...DEFAULT_SETTINGS, ...JSON.parse(stored) };
}
} catch (e) {
console.error('Failed to load user settings:', e);
}
return DEFAULT_SETTINGS;
}
// Save settings to localStorage
function saveSettings(settings: UserSettings) {
if (!browser) return;
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify(settings));
} catch (e) {
console.error('Failed to save user settings:', e);
}
}
// State
let settings = $state<UserSettings>(loadSettings());
export const userSettings = {
// Getters
get timeFormat() {
return settings.timeFormat;
},
get firstDayOfWeek() {
return settings.firstDayOfWeek;
},
get showSeconds() {
return settings.showSeconds;
},
get defaultAlarmSound() {
return settings.defaultAlarmSound;
},
get vibrationEnabled() {
return settings.vibrationEnabled;
},
get all() {
return settings;
},
/**
* Update a single setting
*/
update<K extends keyof UserSettings>(key: K, value: UserSettings[K]) {
settings = { ...settings, [key]: value };
saveSettings(settings);
},
/**
* Update multiple settings
*/
updateMany(updates: Partial<UserSettings>) {
settings = { ...settings, ...updates };
saveSettings(settings);
},
/**
* Reset to defaults
*/
reset() {
settings = DEFAULT_SETTINGS;
saveSettings(settings);
},
/**
* Initialize (reload from storage)
*/
initialize() {
settings = loadSettings();
},
};

View file

@ -0,0 +1,100 @@
/**
* World Clocks Store - Manages world clock state using Svelte 5 runes
*/
import { api } from '$lib/api/client';
import type { WorldClock, CreateWorldClockInput } from '@clock/shared';
// State
let worldClocks = $state<WorldClock[]>([]);
let loading = $state(false);
let error = $state<string | null>(null);
export const worldClocksStore = {
// Getters
get worldClocks() {
return worldClocks;
},
get loading() {
return loading;
},
get error() {
return error;
},
/**
* Fetch all world clocks from the backend
*/
async fetchWorldClocks() {
loading = true;
error = null;
const response = await api.get<WorldClock[]>('/world-clocks');
if (response.error) {
error = response.error;
loading = false;
return { success: false, error: response.error };
}
worldClocks = response.data || [];
loading = false;
return { success: true };
},
/**
* Add a new world clock
*/
async addWorldClock(input: CreateWorldClockInput) {
const response = await api.post<WorldClock>('/world-clocks', input);
if (response.error) {
return { success: false, error: response.error };
}
if (response.data) {
worldClocks = [...worldClocks, response.data];
}
return { success: true, data: response.data };
},
/**
* Remove a world clock
*/
async removeWorldClock(id: string) {
const response = await api.delete(`/world-clocks/${id}`);
if (response.error) {
return { success: false, error: response.error };
}
worldClocks = worldClocks.filter((wc) => wc.id !== id);
return { success: true };
},
/**
* Reorder world clocks
*/
async reorder(ids: string[]) {
const response = await api.put('/world-clocks/reorder', { ids });
if (response.error) {
return { success: false, error: response.error };
}
// Update local order
worldClocks = ids
.map((id) => worldClocks.find((wc) => wc.id === id))
.filter((wc): wc is WorldClock => wc !== undefined);
return { success: true };
},
/**
* Clear all world clocks (local state only)
*/
clear() {
worldClocks = [];
error = null;
},
};