mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-18 08:49:39 +02:00
✨ feat(clock): add complete Clock app with backend, web, and landing
Features: - World clock with timezone support and drag & drop sorting - Alarms with repeat days, snooze, and custom sounds - Multiple timers with start/pause/reset controls - Stopwatch with lap times (local only) - Pomodoro timer with customizable intervals - Analog and digital clock widgets - i18n support (DE, EN, FR, ES, IT) Stack: - Backend: NestJS 10, Drizzle ORM, PostgreSQL (port 3017) - Web: SvelteKit 2.x, Svelte 5 runes, Tailwind CSS 4 (port 5186) - Landing: Astro 5.x with animated clock hero (port 4323) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
110c6779a8
commit
2ef457ea23
104 changed files with 7517 additions and 2 deletions
77
apps/clock/packages/shared/src/constants/index.ts
Normal file
77
apps/clock/packages/shared/src/constants/index.ts
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
// Popular timezones with city names
|
||||
export const POPULAR_TIMEZONES = [
|
||||
{ timezone: 'America/New_York', city: 'New York', region: 'Americas' },
|
||||
{ timezone: 'America/Los_Angeles', city: 'Los Angeles', region: 'Americas' },
|
||||
{ timezone: 'America/Chicago', city: 'Chicago', region: 'Americas' },
|
||||
{ timezone: 'America/Toronto', city: 'Toronto', region: 'Americas' },
|
||||
{ timezone: 'America/Sao_Paulo', city: 'São Paulo', region: 'Americas' },
|
||||
{ timezone: 'Europe/London', city: 'London', region: 'Europe' },
|
||||
{ timezone: 'Europe/Paris', city: 'Paris', region: 'Europe' },
|
||||
{ timezone: 'Europe/Berlin', city: 'Berlin', region: 'Europe' },
|
||||
{ timezone: 'Europe/Rome', city: 'Rome', region: 'Europe' },
|
||||
{ timezone: 'Europe/Madrid', city: 'Madrid', region: 'Europe' },
|
||||
{ timezone: 'Europe/Amsterdam', city: 'Amsterdam', region: 'Europe' },
|
||||
{ timezone: 'Europe/Vienna', city: 'Vienna', region: 'Europe' },
|
||||
{ timezone: 'Europe/Zurich', city: 'Zurich', region: 'Europe' },
|
||||
{ timezone: 'Europe/Moscow', city: 'Moscow', region: 'Europe' },
|
||||
{ timezone: 'Asia/Tokyo', city: 'Tokyo', region: 'Asia' },
|
||||
{ timezone: 'Asia/Shanghai', city: 'Shanghai', region: 'Asia' },
|
||||
{ timezone: 'Asia/Hong_Kong', city: 'Hong Kong', region: 'Asia' },
|
||||
{ timezone: 'Asia/Singapore', city: 'Singapore', region: 'Asia' },
|
||||
{ timezone: 'Asia/Seoul', city: 'Seoul', region: 'Asia' },
|
||||
{ timezone: 'Asia/Mumbai', city: 'Mumbai', region: 'Asia' },
|
||||
{ timezone: 'Asia/Dubai', city: 'Dubai', region: 'Asia' },
|
||||
{ timezone: 'Australia/Sydney', city: 'Sydney', region: 'Oceania' },
|
||||
{ timezone: 'Australia/Melbourne', city: 'Melbourne', region: 'Oceania' },
|
||||
{ timezone: 'Pacific/Auckland', city: 'Auckland', region: 'Oceania' },
|
||||
] 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;
|
||||
|
||||
// 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;
|
||||
|
||||
// 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;
|
||||
2
apps/clock/packages/shared/src/index.ts
Normal file
2
apps/clock/packages/shared/src/index.ts
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
export * from './types';
|
||||
export * from './constants';
|
||||
55
apps/clock/packages/shared/src/types/alarm.ts
Normal file
55
apps/clock/packages/shared/src/types/alarm.ts
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
export interface Alarm {
|
||||
id: string;
|
||||
userId: string;
|
||||
label: string | null;
|
||||
time: string; // HH:MM:SS 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;
|
||||
4
apps/clock/packages/shared/src/types/index.ts
Normal file
4
apps/clock/packages/shared/src/types/index.ts
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
export * from './alarm';
|
||||
export * from './timer';
|
||||
export * from './world-clock';
|
||||
export * from './preset';
|
||||
42
apps/clock/packages/shared/src/types/preset.ts
Normal file
42
apps/clock/packages/shared/src/types/preset.ts
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
export type PresetType = 'timer' | 'pomodoro';
|
||||
|
||||
export interface PresetSettings {
|
||||
// For pomodoro presets
|
||||
workDuration?: number;
|
||||
breakDuration?: number;
|
||||
longBreakDuration?: number;
|
||||
sessionsBeforeLongBreak?: number;
|
||||
// For timer presets
|
||||
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;
|
||||
}
|
||||
|
||||
// Default pomodoro settings
|
||||
export const DEFAULT_POMODORO_SETTINGS: PresetSettings = {
|
||||
workDuration: 25 * 60, // 25 minutes
|
||||
breakDuration: 5 * 60, // 5 minutes
|
||||
longBreakDuration: 15 * 60, // 15 minutes
|
||||
sessionsBeforeLongBreak: 4,
|
||||
};
|
||||
49
apps/clock/packages/shared/src/types/timer.ts
Normal file
49
apps/clock/packages/shared/src/types/timer.ts
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
export type TimerStatus = 'idle' | 'running' | 'paused' | 'finished';
|
||||
|
||||
export interface Timer {
|
||||
id: string;
|
||||
userId: string;
|
||||
label: string | null;
|
||||
durationSeconds: number;
|
||||
remainingSeconds: number | null;
|
||||
status: TimerStatus;
|
||||
startedAt: string | null;
|
||||
pausedAt: string | null;
|
||||
sound: string | null;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface CreateTimerInput {
|
||||
label?: string;
|
||||
durationSeconds: number;
|
||||
sound?: string;
|
||||
}
|
||||
|
||||
export interface UpdateTimerInput {
|
||||
label?: string;
|
||||
durationSeconds?: number;
|
||||
sound?: string;
|
||||
}
|
||||
|
||||
export function formatDuration(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 parseDuration(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];
|
||||
}
|
||||
18
apps/clock/packages/shared/src/types/world-clock.ts
Normal file
18
apps/clock/packages/shared/src/types/world-clock.ts
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
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;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue