refactor: consolidate Clock app into Times

Merge the standalone Clock app (alarms, countdown timers, stopwatch,
world clock, pomodoro) into the Times app as a unified time management
application.

Times standalone app:
- Add 3 new collections (alarms, countdownTimers, worldClocks) to timesStore
- Add Clock types and constants to @times/shared
- Add 6 new stores (alarms, countdown-timers, world-clocks, stopwatch, session-*)
- Add 5 new routes under /clock/* (dashboard, alarms, timers, stopwatch, world-clock)
- Extend layout with Clock context providers and navigation items
- Add clock.* i18n namespace (de/en)
- Add WorldMap and CircularProgress components

Manacore unified app:
- Merge clock module into times module (stores, queries, types, components)
- Move Clock DB tables under times appId (timeAlarms, timeCountdownTimers, timeWorldClocks)
- Update search provider, splitscreen registry, dashboard widgets
- Add redirects from /clock/* to /times/clock/*
- Remove @clock/shared dependency

Cleanup:
- Archive Clock app to apps-archived/clock/
- Remove dev:clock:* scripts from root package.json
- Remove Clock from mana-apps.ts, update Times description
- Update CLAUDE.md documentation

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Till JS 2026-04-02 13:04:07 +02:00
parent 35f4bd48de
commit e870270734
131 changed files with 1524 additions and 5969 deletions

View file

@ -44,7 +44,6 @@ For comprehensive guidelines on code patterns and conventions, see the `.claude/
| **storage** | Cloud file storage | Backend, Web |
| **mukke** | Music production | Backend, Web, Landing |
| **zitare** | Daily inspiration quotes | Web, Landing |
| **clock** | World clock, alarms, timers | Web, Landing |
| **presi** | Presentations | Mobile, Web, Landing |
| **questions** | Research assistant | Backend, Web |
| **context** | Document workspace | Backend, Mobile, Web |
@ -55,7 +54,7 @@ For comprehensive guidelines on code patterns and conventions, see the `.claude/
| **citycorners** | City guide for Konstanz | Web, Landing |
| **inventar** | Inventory management | Web |
| **traces** | City exploration | Backend, Mobile |
| **times** | Time tracking | Web |
| **times** | Time tracking, clocks, alarms, timers, stopwatch | Web |
| **uload** | URL shortener & link management | Server, Web, Landing |
| **news** | AI news reader & personal library | Server, Web, Landing |
| **wisekeep** | AI transcription & wisdom library | Server, Web, Landing |
@ -76,7 +75,11 @@ For comprehensive guidelines on code patterns and conventions, see the `.claude/
### Archived Projects (`apps-archived/`)
Currently empty. To archive a project, move it from `apps/` to `apps-archived/` (excluded from workspace).
Archived apps are excluded from the pnpm workspace.
| Project | Reason |
|---------|--------|
| **clock** | Consolidated into Times |
## Development Commands
@ -92,7 +95,7 @@ pnpm dev:chat:full # Start chat with auth + auto DB setup
pnpm dev:zitare:full # Start zitare with auth + auto DB setup
pnpm dev:contacts:full # Start contacts with auth + auto DB setup
pnpm dev:calendar:full # Start calendar with auth + auto DB setup
pnpm dev:clock:full # Start clock with auth + auto DB setup
pnpm dev:times:full # Start times with auth + auto DB setup
pnpm dev:todo:full # Start todo with auth + auto DB setup
pnpm dev:picture:full # Start picture with auth + auto DB setup
pnpm dev:uload:full # Start uload with auth + auto DB setup

View file

@ -1,27 +0,0 @@
# build output
dist/
# generated types
.astro/
# dependencies
node_modules/
# logs
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
# environment variables
.env
.env.production
# macOS
.DS_Store
# IDE
.idea/
.vscode/
*.swp
*.swo

View file

@ -1,15 +0,0 @@
{
"useTabs": true,
"singleQuote": true,
"trailingComma": "none",
"printWidth": 100,
"plugins": ["prettier-plugin-astro", "prettier-plugin-tailwindcss"],
"overrides": [
{
"files": "*.astro",
"options": {
"parser": "astro"
}
}
]
}

View file

@ -1,19 +0,0 @@
import { defineConfig } from 'astro/config';
import tailwind from '@astrojs/tailwind';
// https://astro.build/config
export default defineConfig({
integrations: [tailwind()],
output: 'static',
build: {
inlineStylesheets: 'auto',
},
vite: {
resolve: {
alias: {
'@components': '/src/components',
'@layouts': '/src/layouts',
},
},
},
});

View file

@ -1,34 +0,0 @@
{
"name": "@clock/landing",
"version": "0.2.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

@ -1,3 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="#f59e0b">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 6v6h4.5m4.5 0a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" />
</svg>

Before

Width:  |  Height:  |  Size: 227 B

View file

@ -1,60 +0,0 @@
---
// Call to Action section
---
<section class="relative overflow-hidden bg-dark-bg">
<!-- Background gradient -->
<div class="absolute inset-0 bg-gradient-to-r from-primary-950/30 via-dark-bg to-primary-950/30">
</div>
<div class="container relative">
<div class="mx-auto max-w-3xl text-center">
<h2 class="mb-6 text-3xl font-bold md:text-4xl lg:text-5xl">
Bereit, produktiver zu werden?
</h2>
<p class="mb-10 text-lg text-gray-400">
Starte kostenlos und erlebe, wie einfach Zeitmanagement sein kann. Keine Kreditkarte
erforderlich.
</p>
<div class="flex flex-col items-center justify-center gap-4 sm:flex-row">
<a href="#" class="btn btn-primary text-lg">
Jetzt kostenlos starten
<svg class="ml-2 h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M17 8l4 4m0 0l-4 4m4-4H3"></path>
</svg>
</a>
<a href="#features" class="btn btn-secondary"> Mehr erfahren </a>
</div>
<!-- Benefits list -->
<div class="mt-12 flex flex-wrap items-center justify-center gap-6 text-sm text-gray-500">
<div class="flex items-center gap-2">
<svg class="h-5 w-5 text-green-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"
></path>
</svg>
<span>Kostenlos starten</span>
</div>
<div class="flex items-center gap-2">
<svg class="h-5 w-5 text-green-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"
></path>
</svg>
<span>Keine Kreditkarte</span>
</div>
<div class="flex items-center gap-2">
<svg class="h-5 w-5 text-green-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"
></path>
</svg>
<span>Jederzeit kundbar</span>
</div>
</div>
</div>
</div>
</section>

View file

@ -1,82 +0,0 @@
---
// Features section for Clock landing page
const features = [
{
icon: `<svg class="h-6 w-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>`,
title: 'Pomodoro Timer',
description:
'Arbeite in fokussierten 25-Minuten-Intervallen mit automatischen Pausen. Steigere deine Konzentration.',
},
{
icon: `<svg class="h-6 w-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"></path>
</svg>`,
title: 'Zeiterfassung',
description:
'Tracke deine Arbeitszeit auf Projekte und Aufgaben. Detaillierte Berichte und Statistiken.',
},
{
icon: `<svg class="h-6 w-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"></path>
</svg>`,
title: 'Focus Sessions',
description:
'Starte dedizierte Focus-Sessions und eliminiere Ablenkungen. Perfekt fur Deep Work.',
},
{
icon: `<svg class="h-6 w-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9"></path>
</svg>`,
title: 'Erinnerungen',
description: 'Sanfte Erinnerungen fur Pausen und Arbeitsende. Schutze deine Work-Life-Balance.',
},
{
icon: `<svg class="h-6 w-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 17v-2m3 2v-4m3 4v-6m2 10H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
</svg>`,
title: 'Detaillierte Reports',
description:
'Wochentliche und monatliche Ubersichten. Exportiere deine Daten als CSV oder PDF.',
},
{
icon: `<svg class="h-6 w-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 18h.01M8 21h8a2 2 0 002-2V5a2 2 0 00-2-2H8a2 2 0 00-2 2v14a2 2 0 002 2z"></path>
</svg>`,
title: 'Alle Gerate',
description: 'Web-App, iOS und Android. Deine Zeit wird uber alle Gerate synchronisiert.',
},
];
---
<section id="features" class="bg-dark-surface">
<div class="container">
<!-- Section header -->
<div class="mx-auto mb-16 max-w-3xl text-center">
<span class="mb-4 inline-block text-sm font-medium uppercase tracking-wider text-primary-400">
Funktionen
</span>
<h2 class="mb-6 text-3xl font-bold md:text-4xl lg:text-5xl">Produktiver arbeiten</h2>
<p class="text-lg text-gray-400">
Clock bietet alle Tools, die du fur effektives Zeitmanagement brauchst.
</p>
</div>
<!-- Features grid -->
<div class="grid gap-8 md:grid-cols-2 lg:grid-cols-3">
{
features.map((feature) => (
<div class="group rounded-xl border border-dark-border bg-dark-card p-6 transition-all duration-300 hover:border-primary-500/50 hover:bg-dark-card/80">
<div class="mb-4 flex h-12 w-12 items-center justify-center rounded-lg bg-primary-500/10 text-primary-400 transition-colors group-hover:bg-primary-500/20">
<Fragment set:html={feature.icon} />
</div>
<h3 class="mb-3 text-xl font-semibold">{feature.title}</h3>
<p class="text-gray-400">{feature.description}</p>
</div>
))
}
</div>
</div>
</section>

View file

@ -1,108 +0,0 @@
---
// Footer component
const currentYear = new Date().getFullYear();
const links = {
product: [
{ name: 'Funktionen', href: '#features' },
{ name: 'Preise', href: '#pricing' },
{ name: 'Changelog', href: '/changelog' },
{ name: 'Roadmap', href: '/roadmap' },
],
legal: [
{ name: 'Impressum', href: '/impressum' },
{ name: 'Datenschutz', href: '/datenschutz' },
{ name: 'AGB', href: '/agb' },
],
support: [
{ name: 'FAQ', href: '/faq' },
{ name: 'Kontakt', href: '/kontakt' },
{ name: 'Status', href: '/status' },
],
};
---
<footer class="border-t border-dark-border bg-dark-bg py-12">
<div class="container">
<div class="grid gap-8 md:grid-cols-4">
<!-- Brand -->
<div class="md:col-span-1">
<div class="mb-4 flex items-center gap-2">
<svg
class="h-8 w-8 text-primary-500"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>
<span class="text-xl font-bold">Clock</span>
</div>
<p class="text-sm text-gray-500">Time Tracking & Focus fur bessere Produktivitat.</p>
</div>
<!-- Links -->
<div>
<h4 class="mb-4 font-semibold">Produkt</h4>
<ul class="space-y-2 text-sm text-gray-400">
{
links.product.map((link) => (
<li>
<a href={link.href} class="transition-colors hover:text-white">
{link.name}
</a>
</li>
))
}
</ul>
</div>
<div>
<h4 class="mb-4 font-semibold">Rechtliches</h4>
<ul class="space-y-2 text-sm text-gray-400">
{
links.legal.map((link) => (
<li>
<a href={link.href} class="transition-colors hover:text-white">
{link.name}
</a>
</li>
))
}
</ul>
</div>
<div>
<h4 class="mb-4 font-semibold">Support</h4>
<ul class="space-y-2 text-sm text-gray-400">
{
links.support.map((link) => (
<li>
<a href={link.href} class="transition-colors hover:text-white">
{link.name}
</a>
</li>
))
}
</ul>
</div>
</div>
<!-- Bottom bar -->
<div
class="mt-12 flex flex-col items-center justify-between gap-4 border-t border-dark-border pt-8 md:flex-row"
>
<p class="text-sm text-gray-500">
&copy; {currentYear} Clock. Alle Rechte vorbehalten.
</p>
<p class="text-sm text-gray-500">
Ein <a href="https://mana.how" class="text-primary-400 hover:underline">Manacore</a> Produkt
</p>
</div>
</div>
</footer>

View file

@ -1,108 +0,0 @@
---
// Hero section for Clock landing page
---
<section class="relative overflow-hidden py-20 md:py-32">
<!-- Background gradient -->
<div class="absolute inset-0 bg-gradient-to-b from-primary-950/30 via-dark-bg to-dark-bg"></div>
<!-- Grid pattern -->
<div class="absolute inset-0 bg-[url('/grid.svg')] bg-center opacity-10"></div>
<div class="container relative">
<div class="mx-auto max-w-4xl text-center">
<!-- Badge -->
<div
class="mb-8 inline-flex items-center gap-2 rounded-full border border-primary-500/30 bg-primary-500/10 px-4 py-2 text-sm text-primary-400"
>
<svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>
<span>Time Tracking & Focus</span>
</div>
<!-- Headline -->
<h1 class="mb-6 text-4xl font-bold leading-tight md:text-6xl lg:text-7xl">
Nutze deine Zeit
<span class="gradient-text">effektiv</span>
</h1>
<!-- Subheadline -->
<p class="mx-auto mb-10 max-w-2xl text-lg text-gray-400 md:text-xl">
Pomodoro-Timer, Zeiterfassung und Focus-Sessions - steigere deine Produktivitat und behalte
den Uberblick uber deine Arbeitszeit.
</p>
<!-- CTA Buttons -->
<div class="flex flex-col items-center justify-center gap-4 sm:flex-row">
<a href="#" class="btn btn-primary group text-lg">
Kostenlos starten
<svg
class="ml-2 h-5 w-5 transition-transform group-hover:translate-x-1"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M17 8l4 4m0 0l-4 4m4-4H3"></path>
</svg>
</a>
<a href="#features" class="btn btn-secondary"> Funktionen entdecken </a>
</div>
<!-- Social proof -->
<div class="mt-16 flex flex-col items-center gap-4">
<div class="flex -space-x-2">
{
[1, 2, 3, 4, 5].map((i) => (
<div class="h-10 w-10 rounded-full border-2 border-dark-bg bg-gradient-to-br from-primary-400 to-primary-600" />
))
}
</div>
<p class="text-sm text-gray-500">
<span class="font-semibold text-white">300+</span> Nutzer tracken ihre Zeit
</p>
</div>
</div>
<!-- Preview mockup -->
<div class="relative mx-auto mt-16 max-w-5xl">
<div
class="absolute -inset-4 rounded-2xl bg-gradient-to-r from-primary-500/20 via-transparent to-primary-500/20 blur-3xl"
>
</div>
<div class="relative rounded-xl border border-dark-border bg-dark-card p-2 shadow-2xl">
<div class="flex gap-2 px-4 py-3">
<div class="h-3 w-3 rounded-full bg-red-500"></div>
<div class="h-3 w-3 rounded-full bg-yellow-500"></div>
<div class="h-3 w-3 rounded-full bg-green-500"></div>
</div>
<div class="aspect-[16/9] overflow-hidden rounded-lg bg-dark-surface">
<!-- Timer preview placeholder -->
<div class="flex h-full flex-col items-center justify-center p-6">
<div class="mb-8 text-8xl font-bold text-primary-400">25:00</div>
<div class="mb-6 flex gap-4">
<button class="rounded-lg bg-primary-500 px-6 py-2 text-sm font-medium">Start</button>
<button class="rounded-lg bg-dark-card px-6 py-2 text-sm">Pause</button>
<button class="rounded-lg bg-dark-card px-6 py-2 text-sm">Reset</button>
</div>
<div class="flex gap-2">
<span class="rounded-full bg-primary-500/20 px-3 py-1 text-xs text-primary-400"
>Focus</span
>
<span class="rounded-full bg-dark-card px-3 py-1 text-xs">Short Break</span>
<span class="rounded-full bg-dark-card px-3 py-1 text-xs">Long Break</span>
</div>
</div>
</div>
</div>
</div>
</div>
</section>

View file

@ -1,58 +0,0 @@
---
import '../styles/global.css';
import Analytics from '@manacore/shared-landing-ui/atoms/Analytics.astro';
interface Props {
title?: string;
description?: string;
}
const {
title = 'Clock - Time Tracking & Focus',
description = 'Track your time, stay focused, and boost productivity. Pomodoro timer, time tracking, and focus sessions. Start free today.',
} = Astro.props;
---
<!doctype html>
<html lang="de" class="scroll-smooth">
<head>
<meta charset="UTF-8" />
<meta name="description" content={description} />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="generator" content={Astro.generator} />
<!-- SEO Meta Tags -->
<meta property="og:title" content={title} />
<meta property="og:description" content={description} />
<meta property="og:type" content="website" />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content={title} />
<meta name="twitter:description" content={description} />
<!-- Preconnect to Google Fonts -->
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap"
rel="stylesheet"
/>
<!-- Umami Analytics -->
{
import.meta.env.PUBLIC_UMAMI_WEBSITE_ID && (
<script
defer
src="https://stats.mana.how/script.js"
data-website-id={import.meta.env.PUBLIC_UMAMI_WEBSITE_ID}
/>
)
}
<title>{title}</title>
</head>
<body class="antialiased">
<slot />
<Analytics />
</body>
</html>

View file

@ -1,158 +0,0 @@
---
import Layout from '@layouts/Layout.astro';
import Hero from '@components/Hero.astro';
import Features from '@components/Features.astro';
import CTA from '@components/CTA.astro';
import Footer from '@components/Footer.astro';
// Pricing data
const pricingPlans = [
{
name: 'Free',
price: '0',
period: '/Monat',
description: 'Perfekt zum Ausprobieren',
features: [
{ text: 'Pomodoro Timer', included: true },
{ text: 'Basis-Zeiterfassung', included: true },
{ text: '7 Tage Historie', included: true },
{ text: 'Web-App Zugang', included: true },
{ text: 'Projekte & Tags', included: false },
{ text: 'Detaillierte Reports', included: false },
],
cta: {
text: 'Kostenlos starten',
href: '#',
},
},
{
name: 'Pro',
price: '3,99',
period: '/Monat',
description: 'Fur Freelancer & Profis',
features: [
{ text: 'Alles aus Free', included: true },
{ text: 'Unbegrenzte Historie', included: true },
{ text: 'Projekte & Tags', included: true },
{ text: 'Detaillierte Reports', included: true },
{ text: 'CSV/PDF Export', included: true },
{ text: 'Mobile Apps', included: true },
],
cta: {
text: 'Pro starten',
href: '#',
},
highlighted: true,
badge: 'Beliebt',
},
{
name: 'Team',
price: '7,99',
period: '/Nutzer/Monat',
description: 'Fur Teams & Agenturen',
features: [
{ text: 'Alles aus Pro', included: true },
{ text: 'Team-Dashboard', included: true },
{ text: 'Projekt-Budgets', included: true },
{ text: 'Kunden-Reports', included: true },
{ text: 'API-Zugang', included: true },
{ text: 'Priority Support', included: true },
],
cta: {
text: 'Team erstellen',
href: '#',
},
},
];
---
<Layout
title="Clock - Time Tracking & Focus"
description="Tracke deine Zeit, bleib fokussiert und steigere deine Produktivitat. Pomodoro Timer, Zeiterfassung und Focus Sessions. Kostenlos starten."
>
<Hero />
<Features />
<section id="pricing" class="bg-dark-bg">
<div class="container">
<div class="mx-auto mb-16 max-w-3xl text-center">
<span
class="mb-4 inline-block text-sm font-medium uppercase tracking-wider text-primary-400"
>
Preise
</span>
<h2 class="mb-6 text-3xl font-bold md:text-4xl">Einfache, transparente Preise</h2>
<p class="text-lg text-gray-400">Starte kostenlos, upgrade wenn du mehr brauchst</p>
</div>
<div class="mx-auto grid max-w-5xl gap-8 md:grid-cols-3">
{
pricingPlans.map((plan) => (
<div
class={`relative rounded-xl border p-6 ${plan.highlighted ? 'border-primary-500 bg-primary-500/10' : 'border-dark-border bg-dark-card'}`}
>
{plan.badge && (
<div class="absolute -top-3 left-1/2 -translate-x-1/2 rounded-full bg-primary-500 px-3 py-1 text-xs font-medium text-white">
{plan.badge}
</div>
)}
<h3 class="mb-2 text-xl font-semibold">{plan.name}</h3>
<p class="mb-4 text-sm text-gray-400">{plan.description}</p>
<div class="mb-6">
<span class="text-4xl font-bold">{plan.price}€</span>
<span class="text-gray-500">{plan.period}</span>
</div>
<ul class="mb-8 space-y-3">
{plan.features.map((feature) => (
<li
class={`flex items-center gap-2 text-sm ${feature.included ? 'text-white' : 'text-gray-600'}`}
>
{feature.included ? (
<svg
class="h-5 w-5 text-green-500"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M5 13l4 4L19 7"
/>
</svg>
) : (
<svg
class="h-5 w-5 text-gray-600"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M6 18L18 6M6 6l12 12"
/>
</svg>
)}
{feature.text}
</li>
))}
</ul>
<a
href={plan.cta.href}
class={`btn w-full ${plan.highlighted ? 'btn-primary' : 'btn-secondary'}`}
>
{plan.cta.text}
</a>
</div>
))
}
</div>
</div>
</section>
<CTA />
<Footer />
</Layout>

View file

@ -1,78 +0,0 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
:root {
--color-background-page: #0a0a0a;
--color-background-card: #1a1a1a;
--color-background-card-hover: #242424;
--color-text-primary: #ffffff;
--color-text-secondary: #d1d5db;
--color-text-muted: #9ca3af;
--color-border: #262626;
--color-border-hover: #3f3f3f;
--color-primary: #f59e0b;
--color-primary-hover: #d97706;
}
* {
box-sizing: border-box;
}
html {
scroll-behavior: smooth;
}
body {
@apply bg-dark-bg text-white;
margin: 0;
padding: 0;
overflow-x: hidden;
font-family: 'Inter', system-ui, -apple-system, sans-serif;
}
/* Custom scrollbar */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
@apply bg-dark-bg;
}
::-webkit-scrollbar-thumb {
@apply bg-dark-border rounded-full;
}
::-webkit-scrollbar-thumb:hover {
@apply bg-gray-600;
}
/* Section padding */
section {
@apply py-16 md:py-24;
}
/* Container */
.container {
@apply mx-auto max-w-7xl px-4 sm:px-6 lg:px-8;
}
/* Gradient text */
.gradient-text {
@apply bg-gradient-to-r from-primary-400 to-primary-600 bg-clip-text text-transparent;
}
/* Button styles */
.btn {
@apply inline-flex items-center justify-center rounded-lg px-6 py-3 font-medium transition-all duration-200;
}
.btn-primary {
@apply bg-primary-500 text-white hover:bg-primary-600;
}
.btn-secondary {
@apply border border-dark-border bg-dark-card text-white hover:bg-dark-surface;
}

View file

@ -1,52 +0,0 @@
/** @type {import('tailwindcss').Config} */
export default {
content: [
'./src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue}',
'../../packages/shared-landing-ui/src/**/*.{astro,html,js,jsx,ts,tsx}',
],
theme: {
extend: {
colors: {
// Clock app theme - amber/orange
primary: {
DEFAULT: '#f59e0b',
50: '#fffbeb',
100: '#fef3c7',
200: '#fde68a',
300: '#fcd34d',
400: '#fbbf24',
500: '#f59e0b',
600: '#d97706',
700: '#b45309',
800: '#92400e',
900: '#78350f',
950: '#451a03',
},
dark: {
bg: '#0a0a0a',
surface: '#111111',
card: '#1a1a1a',
border: '#262626',
},
background: {
page: 'var(--color-background-page, #0a0a0a)',
card: 'var(--color-background-card, #1a1a1a)',
'card-hover': 'var(--color-background-card-hover, #242424)',
},
text: {
primary: 'var(--color-text-primary, #ffffff)',
secondary: 'var(--color-text-secondary, #d1d5db)',
muted: 'var(--color-text-muted, #9ca3af)',
},
border: {
DEFAULT: 'var(--color-border, #262626)',
hover: 'var(--color-border-hover, #3f3f3f)',
},
},
fontFamily: {
sans: ['Inter', 'system-ui', '-apple-system', 'sans-serif'],
},
},
},
plugins: [require('@tailwindcss/typography')],
};

View file

@ -1,10 +0,0 @@
{
"extends": "astro/tsconfigs/strict",
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@components/*": ["src/components/*"],
"@layouts/*": ["src/layouts/*"]
}
}
}

View file

@ -1,7 +0,0 @@
# Cloudflare Pages configuration for Clock Landing
# Deployed via Wrangler CLI (Direct Upload)
# Custom domain: clocks.mana.how
name = "clocks-landing"
compatibility_date = "2024-12-01"
pages_build_output_dir = "dist"

View file

@ -1,32 +0,0 @@
# syntax=docker/dockerfile:1
FROM sveltekit-base:local AS builder
ARG PUBLIC_BACKEND_URL=http://mana-auth
ARG PUBLIC_MANA_CORE_AUTH_URL=http://mana-auth:3001
ENV PUBLIC_BACKEND_URL=$PUBLIC_BACKEND_URL
ENV PUBLIC_MANA_CORE_AUTH_URL=$PUBLIC_MANA_CORE_AUTH_URL
COPY apps/clock/packages/shared ./apps/clock/packages/shared
COPY apps/clock/apps/web ./apps/clock/apps/web
RUN --mount=type=cache,id=pnpm,target=/root/.local/share/pnpm/store \
pnpm install --no-frozen-lockfile --ignore-scripts
WORKDIR /app/apps/clock/apps/web
RUN pnpm exec svelte-kit sync
RUN NODE_OPTIONS="--max-old-space-size=4096" pnpm build
FROM node:20-alpine AS production
WORKDIR /app/apps/clock/apps/web
COPY --from=builder /app/node_modules/.pnpm /app/node_modules/.pnpm
COPY --from=builder /app/apps/clock/apps/web/node_modules ./node_modules
COPY --from=builder /app/apps/clock/apps/web/build ./build
COPY --from=builder /app/apps/clock/apps/web/package.json ./
EXPOSE 5013
ENV NODE_ENV=production PORT=5013 HOST=0.0.0.0
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
CMD wget --no-verbose --tries=1 --spider http://localhost:5013/health || exit 1
CMD ["node", "build"]

View file

@ -1,66 +0,0 @@
{
"name": "@clock/web",
"version": "0.2.0",
"private": true,
"scripts": {
"dev": "vite dev",
"build": "vite build",
"preview": "vite preview",
"prepare": "svelte-kit sync || echo ''",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
"lint": "eslint .",
"format": "prettier --write .",
"type-check": "svelte-kit sync && svelte-check --threshold error"
},
"devDependencies": {
"@manacore/shared-pwa": "workspace:*",
"@manacore/shared-vite-config": "workspace:*",
"@sveltejs/adapter-node": "^5.0.0",
"@sveltejs/kit": "^2.47.1",
"@sveltejs/vite-plugin-svelte": "^5.0.0",
"@tailwindcss/vite": "^4.1.7",
"@types/d3": "^7.4.3",
"@types/node": "^20.0.0",
"@types/topojson-client": "^3.1.5",
"@types/topojson-specification": "^1.0.5",
"@vite-pwa/sveltekit": "^1.1.0",
"prettier": "^3.1.1",
"prettier-plugin-svelte": "^3.1.2",
"svelte": "^5.41.0",
"svelte-check": "^4.3.3",
"tailwindcss": "^4.1.7",
"tslib": "^2.4.1",
"typescript": "^5.9.3",
"vite": "^6.0.0"
},
"dependencies": {
"@clock/shared": "workspace:*",
"@manacore/local-store": "workspace:*",
"@manacore/shared-api-client": "workspace:*",
"@manacore/shared-app-onboarding": "workspace:*",
"@manacore/shared-auth": "workspace:*",
"@manacore/shared-auth-stores": "workspace:*",
"@manacore/shared-auth-ui": "workspace:*",
"@manacore/shared-branding": "workspace:*",
"@manacore/shared-error-tracking": "workspace:*",
"@manacore/feedback": "workspace:*",
"@manacore/shared-i18n": "workspace:*",
"@manacore/help": "workspace:*",
"@manacore/shared-icons": "workspace:*",
"@manacore/shared-profile-ui": "workspace:*",
"@manacore/shared-stores": "workspace:*",
"@manacore/shared-tags": "workspace:*",
"@manacore/subscriptions": "workspace:*",
"@manacore/shared-tailwind": "workspace:*",
"@manacore/shared-theme": "workspace:*",
"@manacore/shared-theme-ui": "workspace:*",
"@manacore/shared-ui": "workspace:*",
"@manacore/shared-utils": "workspace:*",
"d3": "^7.9.0",
"svelte-dnd-action": "^0.9.68",
"svelte-i18n": "^4.0.1",
"topojson-client": "^3.1.0"
},
"type": "module"
}

View file

@ -1,10 +0,0 @@
@import "tailwindcss";
@import "@manacore/shared-tailwind/themes.css";
/* Scan shared packages for Tailwind classes */
@source "../../../../packages/shared-ui/src";
@source "../../../../packages/shared-auth-ui/src";
@source "../../../../packages/shared-branding/src";
@source "../../../../packages/shared-theme-ui/src";
@source "../../../../packages/shared-theme-ui/src/components";
@source "../../../../packages/shared-theme-ui/src/pages";

View file

@ -1,2 +0,0 @@
declare const __BUILD_HASH__: string;
declare const __BUILD_TIME__: string;

View file

@ -1,12 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
%sveltekit.head%
</head>
<body data-sveltekit-preload-data="hover">
<div style="display: contents">%sveltekit.body%</div>
</body>
</html>

View file

@ -1,12 +0,0 @@
import { initErrorTracking, handleSvelteError } from '@manacore/shared-error-tracking/browser';
import type { HandleClientError } from '@sveltejs/kit';
initErrorTracking({
serviceName: 'clock-web',
dsn: (window as any).__PUBLIC_GLITCHTIP_DSN__,
environment: import.meta.env.MODE,
});
export const handleError: HandleClientError = ({ error }) => {
handleSvelteError(error);
};

View file

@ -1,28 +0,0 @@
import type { Handle } from '@sveltejs/kit';
import { injectUmamiAnalytics } from '@manacore/shared-utils/analytics-server';
import { setSecurityHeaders } from '@manacore/shared-utils/security-headers';
const PUBLIC_MANA_CORE_AUTH_URL_CLIENT =
process.env.PUBLIC_MANA_CORE_AUTH_URL_CLIENT || process.env.PUBLIC_MANA_CORE_AUTH_URL || '';
const PUBLIC_BACKEND_URL_CLIENT =
process.env.PUBLIC_BACKEND_URL_CLIENT || process.env.PUBLIC_BACKEND_URL || '';
const PUBLIC_GLITCHTIP_DSN = process.env.PUBLIC_GLITCHTIP_DSN || '';
export const handle: Handle = async ({ event, resolve }) => {
const response = await resolve(event, {
transformPageChunk: ({ html }) => {
const envScript = `<script>
window.__PUBLIC_MANA_CORE_AUTH_URL__ = ${JSON.stringify(PUBLIC_MANA_CORE_AUTH_URL_CLIENT)};
window.__PUBLIC_BACKEND_URL__ = ${JSON.stringify(PUBLIC_BACKEND_URL_CLIENT)};
window.__PUBLIC_GLITCHTIP_DSN__ = ${JSON.stringify(PUBLIC_GLITCHTIP_DSN)};
</script>`;
return injectUmamiAnalytics(html.replace('<head>', `<head>${envScript}`));
},
});
setSecurityHeaders(response, {
connectSrc: [PUBLIC_MANA_CORE_AUTH_URL_CLIENT, PUBLIC_BACKEND_URL_CLIENT],
});
return response;
};

View file

@ -1,15 +0,0 @@
/**
* 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

@ -1,26 +0,0 @@
/**
* API Client for Clock backend
* Uses @manacore/shared-api-client for consistent error handling
*/
import { createApiClient, type ApiResult } from '@manacore/shared-api-client';
import { authStore } from '$lib/stores/auth.svelte';
const API_URL = 'http://localhost:3017';
/**
* Clock API client instance
* - Auto token handling via authStore.getValidToken()
* - Consistent ApiResult<T> response format
* - Automatic retry on server errors (configurable)
*/
export const api = createApiClient({
baseUrl: API_URL,
apiPrefix: '/api/v1',
getAuthToken: () => authStore.getValidToken(),
timeout: 30000,
debug: import.meta.env.DEV,
});
// Re-export types for convenience
export type { ApiResult };

View file

@ -1,17 +0,0 @@
/**
* 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

@ -1,226 +0,0 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { browser } from '$app/environment';
type Props = {
open: boolean;
action?: 'save' | 'sync' | 'feature';
itemCount?: number;
onClose: () => void;
};
let { open, action = 'save', itemCount = 0, onClose }: Props = $props();
// Messages based on action type
const messages = {
save: {
title: 'Daten speichern',
description: 'Melde dich an, um deine Wecker und Timer dauerhaft in der Cloud zu speichern.',
},
sync: {
title: 'Daten synchronisieren',
description: 'Melde dich an, um deine Wecker und Timer auf allen Geräten zu synchronisieren.',
},
feature: {
title: 'Funktion freischalten',
description: 'Diese Funktion ist nur für angemeldete Benutzer verfügbar.',
},
};
const currentMessage = $derived(messages[action] || messages.save);
function handleLogin() {
if (browser) {
sessionStorage.setItem('auth-return-url', window.location.pathname);
}
goto('/login');
}
function handleRegister() {
if (browser) {
sessionStorage.setItem('auth-return-url', window.location.pathname);
}
goto('/register');
}
</script>
{#if open}
<!-- svelte-ignore a11y_click_events_have_key_events a11y_no_static_element_interactions -->
<div class="modal-backdrop" onclick={onClose}>
<div class="modal-content" onclick={(e) => e.stopPropagation()}>
<div class="modal-header">
<h2>{currentMessage.title}</h2>
<button class="close-btn" onclick={onClose} aria-label="Schliessen">
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</svg>
</button>
</div>
<div class="modal-body">
<p>{currentMessage.description}</p>
{#if itemCount > 0}
<div class="migration-info">
<svg
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<circle cx="12" cy="12" r="10"></circle>
<line x1="12" y1="16" x2="12" y2="12"></line>
<line x1="12" y1="8" x2="12.01" y2="8"></line>
</svg>
<span
>Du hast {itemCount}
{itemCount === 1 ? 'Element' : 'Elemente'} in deiner Session. Diese werden nach der Anmeldung
in deinen Account übertragen.</span
>
</div>
{/if}
</div>
<div class="modal-actions">
<button class="btn btn-secondary" onclick={onClose}> Später </button>
<button class="btn btn-primary" onclick={handleLogin}> Anmelden </button>
<button class="btn btn-outline" onclick={handleRegister}> Registrieren </button>
</div>
</div>
</div>
{/if}
<style>
.modal-backdrop {
position: fixed;
inset: 0;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 100;
padding: 1rem;
}
.modal-content {
background-color: var(--color-surface-elevated-2);
border: 1px solid var(--color-border-strong);
border-radius: 0.75rem;
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5);
max-width: 28rem;
width: 100%;
padding: 1.5rem;
}
.modal-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 1rem;
}
.modal-header h2 {
font-size: 1.25rem;
font-weight: 600;
color: var(--color-foreground, #1f2937);
margin: 0;
}
.close-btn {
background: none;
border: none;
cursor: pointer;
padding: 0.25rem;
color: var(--color-muted-foreground, #6b7280);
border-radius: 0.375rem;
transition: color 0.15s;
}
.close-btn:hover {
color: var(--color-foreground, #1f2937);
}
.modal-body {
margin-bottom: 1.5rem;
}
.modal-body p {
color: var(--color-muted-foreground, #6b7280);
margin: 0 0 1rem 0;
line-height: 1.5;
}
.migration-info {
display: flex;
gap: 0.75rem;
padding: 0.75rem;
background-color: var(--color-primary-50, #fef3c7);
border-radius: 0.5rem;
font-size: 0.875rem;
color: var(--color-primary-700, #b45309);
}
.migration-info svg {
flex-shrink: 0;
margin-top: 0.125rem;
}
.modal-actions {
display: flex;
gap: 0.75rem;
flex-wrap: wrap;
}
.btn {
padding: 0.625rem 1rem;
border-radius: 0.5rem;
font-weight: 500;
font-size: 0.875rem;
cursor: pointer;
transition: all 0.15s;
border: 1px solid transparent;
}
.btn-primary {
background-color: var(--color-primary, #f59e0b);
color: white;
flex: 1;
}
.btn-primary:hover {
background-color: var(--color-primary-600, #d97706);
}
.btn-secondary {
background-color: var(--color-muted, #f3f4f6);
color: var(--color-muted-foreground, #6b7280);
}
.btn-secondary:hover {
background-color: var(--color-muted-200, #e5e7eb);
}
.btn-outline {
background-color: transparent;
border-color: var(--color-border, #e5e7eb);
color: var(--color-foreground, #1f2937);
}
.btn-outline:hover {
background-color: var(--color-muted, #f3f4f6);
}
</style>

View file

@ -1,71 +0,0 @@
<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

@ -1,90 +0,0 @@
<script lang="ts">
/**
* AppLoadingSkeleton - Full page loading skeleton for initial app load
* Shows a minimal skeleton layout while auth is being checked
*/
import { SkeletonBox } from '@manacore/shared-ui';
</script>
<div class="app-loading-skeleton" role="status" aria-label="App wird geladen...">
<!-- Header placeholder -->
<div class="header-skeleton">
<SkeletonBox width="120px" height="32px" borderRadius="8px" />
<div class="header-nav">
<SkeletonBox width="80px" height="32px" borderRadius="16px" />
<SkeletonBox width="80px" height="32px" borderRadius="16px" />
</div>
<SkeletonBox width="36px" height="36px" borderRadius="50%" />
</div>
<!-- Content placeholder - Clock specific -->
<div class="content-skeleton">
<!-- Clock display placeholder -->
<div class="clock-placeholder">
<SkeletonBox width="300px" height="300px" borderRadius="50%" />
</div>
<!-- Controls placeholder -->
<div class="controls-placeholder">
<SkeletonBox width="200px" height="48px" borderRadius="12px" />
</div>
</div>
</div>
<style>
.app-loading-skeleton {
min-height: 100vh;
background: hsl(var(--background));
}
.header-skeleton {
display: flex;
align-items: center;
justify-content: space-between;
padding: 1rem 2rem;
border-bottom: 1px solid hsl(var(--border));
}
.header-nav {
display: flex;
gap: 0.5rem;
}
.content-skeleton {
max-width: 80rem;
margin: 0 auto;
padding: 2rem;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: calc(100vh - 80px);
gap: 2rem;
}
.clock-placeholder {
display: flex;
align-items: center;
justify-content: center;
}
.controls-placeholder {
display: flex;
gap: 1rem;
}
@media (max-width: 768px) {
.header-nav {
display: none;
}
.header-skeleton {
padding: 1rem;
}
.content-skeleton {
padding: 1rem;
}
}
</style>

View file

@ -1,85 +0,0 @@
<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

@ -1,58 +0,0 @@
<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

@ -1,13 +0,0 @@
/**
* Clock App Skeleton Components
*
* App-specific skeleton loaders for loading states.
*/
// 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

@ -1,47 +0,0 @@
import { describe, it, expect } from 'vitest';
import { getClockHelpContent } from './index';
describe('Clock Help Content', () => {
it('returns valid German content', () => {
const content = getClockHelpContent('de');
expect(content.faq.length).toBeGreaterThan(0);
content.faq.forEach((faq) => {
expect(faq.id).toBeTruthy();
expect(faq.question).toBeTruthy();
expect(faq.answer).toBeTruthy();
});
expect(content.features).toBeDefined();
expect(content.contact).toBeDefined();
expect(content.contact.supportEmail).toBe('support@mana.how');
});
it('returns valid English content', () => {
const content = getClockHelpContent('en');
expect(content.faq.length).toBeGreaterThan(0);
content.faq.forEach((faq) => {
expect(faq.id).toBeTruthy();
expect(faq.question).toBeTruthy();
expect(faq.answer).toBeTruthy();
});
expect(content.features).toBeDefined();
expect(content.contact).toBeDefined();
});
it('returns same number of FAQ items for both languages', () => {
const de = getClockHelpContent('de');
const en = getClockHelpContent('en');
expect(de.faq.length).toBe(en.faq.length);
expect(de.features.length).toBe(en.features.length);
});
it('has unique FAQ IDs', () => {
const content = getClockHelpContent('de');
const ids = content.faq.map((f) => f.id);
expect(new Set(ids).size).toBe(ids.length);
});
});

View file

@ -1,215 +0,0 @@
/**
* Help content for Clock app
*/
import type { HelpContent } from '@manacore/help';
import { getPrivacyFAQs } from '@manacore/help';
export function getClockHelpContent(locale: string): HelpContent {
const isDE = locale === 'de';
return {
faq: [
{
id: 'faq-create-alarms',
question: isDE ? 'Wie erstelle ich Wecker?' : 'How do I create alarms?',
answer: isDE
? '<p>Du kannst Wecker auf verschiedene Arten erstellen:</p><ul><li><strong>Schnellwecker</strong>: Drücke <kbd>A</kbd> oder klicke auf das + Symbol im Wecker-Tab</li><li><strong>Uhrzeit wählen</strong>: Stelle Stunde und Minute ein und wähle die gewünschten Wochentage</li><li><strong>Label</strong>: Gib deinem Wecker einen Namen, z.B. "Morgenroutine"</li><li><strong>Klingelton</strong>: Wähle aus verschiedenen Tönen oder nutze einen sanften Weckton</li></ul>'
: '<p>You can create alarms in several ways:</p><ul><li><strong>Quick alarm</strong>: Press <kbd>A</kbd> or click the + icon in the Alarms tab</li><li><strong>Set time</strong>: Choose hour and minute and select the desired weekdays</li><li><strong>Label</strong>: Give your alarm a name, e.g. "Morning routine"</li><li><strong>Ringtone</strong>: Choose from various sounds or use a gentle wake-up tone</li></ul>',
category: 'features',
order: 1,
language: isDE ? 'de' : 'en',
tags: isDE ? ['wecker', 'erstellen', 'neu'] : ['alarm', 'create', 'new'],
},
{
id: 'faq-timers',
question: isDE
? 'Wie funktionieren Timer und Stoppuhr?'
: 'How do timers and the stopwatch work?',
answer: isDE
? '<p>Clock bietet zwei Zeitmesser:</p><ul><li><strong>Timer</strong>: Stelle eine Countdown-Zeit ein und starte ihn. Du kannst mehrere Timer gleichzeitig laufen lassen. Drücke <kbd>T</kbd> für einen neuen Timer.</li><li><strong>Stoppuhr</strong>: Messe verstrichene Zeit mit Rundenzeiten. Starte, pausiere und setze zurück.</li></ul><p>Beide laufen auch im Hintergrund weiter und benachrichtigen dich, wenn die Zeit abgelaufen ist.</p>'
: '<p>Clock offers two time measurement tools:</p><ul><li><strong>Timer</strong>: Set a countdown duration and start it. You can run multiple timers simultaneously. Press <kbd>T</kbd> for a new timer.</li><li><strong>Stopwatch</strong>: Measure elapsed time with lap splits. Start, pause, and reset.</li></ul><p>Both continue running in the background and notify you when time is up.</p>',
category: 'features',
order: 2,
language: isDE ? 'de' : 'en',
tags: isDE ? ['timer', 'stoppuhr', 'countdown'] : ['timer', 'stopwatch', 'countdown'],
},
{
id: 'faq-pomodoro',
question: isDE ? 'Was ist die Pomodoro-Technik?' : 'What is the Pomodoro technique?',
answer: isDE
? '<p>Die <strong>Pomodoro-Technik</strong> ist eine Zeitmanagement-Methode:</p><ol><li>Arbeite <strong>25 Minuten</strong> konzentriert (ein "Pomodoro")</li><li>Mache eine <strong>5-Minuten-Pause</strong></li><li>Nach 4 Pomodoros: <strong>15-30 Minuten</strong> längere Pause</li></ol><p>In Clock kannst du die Intervalle anpassen, deinen Fortschritt verfolgen und Statistiken über deine Produktivität einsehen.</p>'
: '<p>The <strong>Pomodoro technique</strong> is a time management method:</p><ol><li>Work for <strong>25 minutes</strong> with focus (one "Pomodoro")</li><li>Take a <strong>5-minute break</strong></li><li>After 4 Pomodoros: take a <strong>15-30 minute</strong> longer break</li></ol><p>In Clock you can customize the intervals, track your progress, and view statistics about your productivity.</p>',
category: 'features',
order: 3,
language: isDE ? 'de' : 'en',
tags: isDE
? ['pomodoro', 'produktivität', 'fokus', 'technik']
: ['pomodoro', 'productivity', 'focus', 'technique'],
},
{
id: 'faq-life-clock',
question: isDE ? 'Was ist die Life Clock?' : 'What is the Life Clock?',
answer: isDE
? '<p>Die <strong>Life Clock</strong> ist eine einzigartige Visualisierung deiner Lebenszeit:</p><ul><li>Gib dein Geburtsdatum und deine geschätzte Lebenserwartung ein</li><li>Sieh, wie viel deiner Zeit bereits vergangen ist und wie viel noch vor dir liegt</li><li>Verschiedene Darstellungen: Wochen, Monate oder Jahre als Raster</li></ul><p>Die Life Clock soll dich motivieren, deine Zeit bewusst zu nutzen — keine Angst, sondern <strong>Inspiration</strong>.</p>'
: '<p>The <strong>Life Clock</strong> is a unique visualization of your lifetime:</p><ul><li>Enter your birth date and estimated life expectancy</li><li>See how much of your time has passed and how much lies ahead</li><li>Various display modes: weeks, months, or years as a grid</li></ul><p>The Life Clock is meant to motivate you to use your time mindfully — not fear, but <strong>inspiration</strong>.</p>',
category: 'features',
order: 4,
language: isDE ? 'de' : 'en',
tags: isDE
? ['life-clock', 'lebenszeit', 'visualisierung']
: ['life-clock', 'lifetime', 'visualization'],
},
...getPrivacyFAQs(locale, {
dataTypeDE: 'Daten',
dataTypeEN: 'data',
extraBulletsDE: [
'<strong>Lokale Speicherung</strong>: Wecker und Timer werden lokal auf deinem Gerät gespeichert',
],
extraBulletsEN: [
'<strong>Local storage</strong>: Alarms and timers are stored locally on your device',
],
}),
],
features: [
{
id: 'feature-alarms',
title: isDE ? 'Wecker' : 'Alarms',
description: isDE
? 'Erstelle wiederkehrende und einmalige Wecker mit individuellen Tönen'
: 'Create recurring and one-time alarms with custom sounds',
icon: '⏰',
category: 'core',
highlights: isDE
? ['Wiederkehrende Wecker', 'Individuelle Töne', 'Schlummerfunktion', 'Labels']
: ['Recurring alarms', 'Custom sounds', 'Snooze function', 'Labels'],
content: '',
order: 1,
language: isDE ? 'de' : 'en',
},
{
id: 'feature-timers-stopwatch',
title: isDE ? 'Timer & Stoppuhr' : 'Timers & Stopwatch',
description: isDE
? 'Mehrere gleichzeitige Timer und eine Stoppuhr mit Rundenzeiten'
: 'Multiple simultaneous timers and a stopwatch with lap times',
icon: '⏱️',
category: 'core',
highlights: isDE
? ['Mehrere Timer', 'Rundenzeiten', 'Hintergrund-Benachrichtigung', 'Voreinstellungen']
: ['Multiple timers', 'Lap times', 'Background notifications', 'Presets'],
content: '',
order: 2,
language: isDE ? 'de' : 'en',
},
{
id: 'feature-pomodoro',
title: 'Pomodoro',
description: isDE
? 'Steigere deine Produktivität mit der Pomodoro-Technik und Statistiken'
: 'Boost your productivity with the Pomodoro technique and statistics',
icon: '🍅',
category: 'advanced',
highlights: isDE
? ['Anpassbare Intervalle', 'Sitzungs-Tracking', 'Statistiken', 'Benachrichtigungen']
: ['Customizable intervals', 'Session tracking', 'Statistics', 'Notifications'],
content: '',
order: 3,
language: isDE ? 'de' : 'en',
},
{
id: 'feature-world-clock',
title: isDE ? 'Weltzeituhr' : 'World Clock',
description: isDE
? 'Behalte die Uhrzeit in verschiedenen Zeitzonen im Blick'
: 'Keep track of the time across different time zones',
icon: '🌍',
category: 'core',
highlights: isDE
? ['Alle Zeitzonen', 'Zeitvergleich', 'Favoriten', 'Analoges Zifferblatt']
: ['All time zones', 'Time comparison', 'Favorites', 'Analog clock face'],
content: '',
order: 4,
language: isDE ? 'de' : 'en',
},
],
shortcuts: [
{
id: 'shortcuts-general',
category: 'general',
title: isDE ? 'Allgemein' : 'General',
language: isDE ? 'de' : 'en',
order: 1,
shortcuts: [
{
shortcut: 'Cmd/Ctrl + K',
action: isDE ? 'Kommandoleiste öffnen' : 'Open command bar',
},
{
shortcut: 'A',
action: isDE ? 'Neuer Wecker' : 'New alarm',
},
{
shortcut: 'T',
action: isDE ? 'Neuer Timer' : 'New timer',
},
],
},
{
id: 'shortcuts-navigation',
category: 'navigation',
title: 'Navigation',
language: isDE ? 'de' : 'en',
order: 2,
shortcuts: [
{
shortcut: 'Cmd/Ctrl + 1',
action: isDE ? 'Wecker öffnen' : 'Open Alarms',
},
{
shortcut: 'Cmd/Ctrl + 2',
action: isDE ? 'Timer öffnen' : 'Open Timers',
},
{
shortcut: 'Cmd/Ctrl + 3',
action: isDE ? 'Stoppuhr öffnen' : 'Open Stopwatch',
},
{
shortcut: 'Cmd/Ctrl + 4',
action: isDE ? 'Pomodoro öffnen' : 'Open Pomodoro',
},
{
shortcut: 'Cmd/Ctrl + 5',
action: isDE ? 'Weltzeituhr öffnen' : 'Open World Clock',
},
{
shortcut: 'Cmd/Ctrl + 6',
action: isDE ? 'Life Clock öffnen' : 'Open Life Clock',
},
{
shortcut: 'Cmd/Ctrl + 7',
action: isDE ? 'Statistiken öffnen' : 'Open Statistics',
},
{
shortcut: 'Cmd/Ctrl + 8',
action: isDE ? 'Einstellungen öffnen' : 'Open Settings',
},
],
},
],
gettingStarted: [],
changelog: [],
contact: {
id: 'contact-support',
title: isDE ? 'Support kontaktieren' : 'Contact Support',
content: isDE
? '<p>Unser Support-Team hilft dir bei allen Fragen rund um Clock.</p>'
: '<p>Our support team is here to help you with any questions about Clock.</p>',
language: isDE ? 'de' : 'en',
order: 1,
supportEmail: 'support@mana.how',
documentationUrl: 'https://mana.how/docs',
responseTime: isDE ? 'Normalerweise innerhalb von 24 Stunden' : 'Usually within 24 hours',
},
};
}

View file

@ -1,36 +0,0 @@
/**
* Guest seed data for the Clock app.
*
* These records are loaded into IndexedDB when a new guest visits the app.
* They provide sample alarms and world clocks to showcase the app.
*/
import type { LocalAlarm, LocalWorldClock } from './local-store';
export const guestAlarms: LocalAlarm[] = [
{
id: 'alarm-weekday-morning',
label: 'Wecker Wochentags',
time: '07:00',
enabled: true,
repeatDays: [1, 2, 3, 4, 5], // Mon-Fri
snoozeMinutes: 5,
sound: null,
vibrate: true,
},
];
export const guestWorldClocks: LocalWorldClock[] = [
{
id: 'wc-new-york',
timezone: 'America/New_York',
cityName: 'New York',
sortOrder: 0,
},
{
id: 'wc-tokyo',
timezone: 'Asia/Tokyo',
cityName: 'Tokio',
sortOrder: 1,
},
];

View file

@ -1,69 +0,0 @@
/**
* Clock App Local-First Data Layer
*
* Defines the IndexedDB database, collections, and guest seed data.
* This is the single source of truth for all Clock data.
*/
import { createLocalStore, type BaseRecord } from '@manacore/local-store';
import { guestAlarms, guestWorldClocks } from './guest-seed';
// ─── Types ──────────────────────────────────────────────────
export interface LocalAlarm extends BaseRecord {
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;
}
export interface LocalTimer extends BaseRecord {
label: string | null;
durationSeconds: number;
remainingSeconds: number | null;
status: 'idle' | 'running' | 'paused' | 'finished';
startedAt: string | null;
pausedAt: string | null;
sound: string | null;
}
export interface LocalWorldClock extends BaseRecord {
timezone: string; // IANA timezone e.g. 'America/New_York'
cityName: string;
sortOrder: number;
}
// ─── Store ──────────────────────────────────────────────────
const SYNC_SERVER_URL = import.meta.env.PUBLIC_SYNC_SERVER_URL || 'http://localhost:3050';
export const clockStore = createLocalStore({
appId: 'clock',
collections: [
{
name: 'alarms',
indexes: ['enabled', 'time'],
guestSeed: guestAlarms,
},
{
name: 'timers',
indexes: ['status'],
},
{
name: 'worldClocks',
indexes: ['sortOrder', 'timezone'],
guestSeed: guestWorldClocks,
},
],
sync: {
serverUrl: SYNC_SERVER_URL,
},
});
// Typed collection accessors
export const alarmCollection = clockStore.collection<LocalAlarm>('alarms');
export const timerCollection = clockStore.collection<LocalTimer>('timers');
export const worldClockCollection = clockStore.collection<LocalWorldClock>('worldClocks');

View file

@ -1,106 +0,0 @@
/**
* Reactive Queries & Pure Helpers for Clock
*
* Uses Dexie liveQuery to automatically re-render when IndexedDB changes
* (local writes, sync updates, other tabs). Components call these hooks
* at init time; no manual fetch/refresh needed.
*/
import { useLiveQueryWithDefault } from '@manacore/local-store/svelte';
import {
alarmCollection,
timerCollection,
worldClockCollection,
type LocalAlarm,
type LocalTimer,
type LocalWorldClock,
} from './local-store';
import type { Alarm, Timer, WorldClock } from '@clock/shared';
// ─── Type Converters ───────────────────────────────────────
export function toAlarm(local: LocalAlarm): Alarm {
return {
id: local.id,
userId: 'local',
label: local.label,
time: local.time,
enabled: local.enabled,
repeatDays: local.repeatDays,
snoozeMinutes: local.snoozeMinutes,
sound: local.sound,
vibrate: local.vibrate ?? null,
createdAt: local.createdAt ?? new Date().toISOString(),
updatedAt: local.updatedAt ?? new Date().toISOString(),
};
}
export function toTimer(local: LocalTimer): Timer {
return {
id: local.id,
userId: 'local',
label: local.label,
durationSeconds: local.durationSeconds,
remainingSeconds: local.remainingSeconds,
status: local.status,
startedAt: local.startedAt,
pausedAt: local.pausedAt,
sound: local.sound,
createdAt: local.createdAt ?? new Date().toISOString(),
updatedAt: local.updatedAt ?? new Date().toISOString(),
};
}
export function toWorldClock(local: LocalWorldClock): WorldClock {
return {
id: local.id,
userId: 'local',
timezone: local.timezone,
cityName: local.cityName,
sortOrder: local.sortOrder,
createdAt: local.createdAt ?? new Date().toISOString(),
};
}
// ─── Live Query Hooks (call during component init) ─────────
/** All alarms, auto-updates on any change. */
export function useAllAlarms() {
return useLiveQueryWithDefault(async () => {
const locals = await alarmCollection.getAll();
return locals.map(toAlarm);
}, [] as Alarm[]);
}
/** All timers, auto-updates on any change. */
export function useAllTimers() {
return useLiveQueryWithDefault(async () => {
const locals = await timerCollection.getAll();
return locals.map(toTimer);
}, [] as Timer[]);
}
/** All world clocks, sorted by sortOrder. Auto-updates on any change. */
export function useAllWorldClocks() {
return useLiveQueryWithDefault(async () => {
const locals = await worldClockCollection.getAll(undefined, {
sortBy: 'sortOrder',
sortDirection: 'asc',
});
return locals.map(toWorldClock);
}, [] as WorldClock[]);
}
// ─── Pure Filter Functions (for $derived) ──────────────────
export function filterEnabledAlarms(alarms: Alarm[]): Alarm[] {
return alarms.filter((a) => a.enabled);
}
export function filterActiveTimers(timers: Timer[]): Timer[] {
return timers.filter((t) => t.status === 'running' || t.status === 'paused');
}
export function sortWorldClocksByOrder(clocks: WorldClock[]): WorldClock[] {
return [...clocks].sort((a, b) => a.sortOrder - b.sortOrder);
}

View file

@ -1,49 +0,0 @@
import { browser } from '$app/environment';
import { init, register, locale, waitLocale } from 'svelte-i18n';
// List of supported locales
export const supportedLocales = ['de', 'en'] as const;
export type SupportedLocale = (typeof supportedLocales)[number];
// Default locale
const defaultLocale = 'de';
// Register all available locales
register('de', () => import('./locales/de.json'));
register('en', () => import('./locales/en.json'));
// Get initial locale from browser or localStorage
function getInitialLocale(): SupportedLocale {
if (browser) {
// Check localStorage first
const stored = localStorage.getItem('clock_locale');
if (stored && supportedLocales.includes(stored as SupportedLocale)) {
return stored as SupportedLocale;
}
// Fall back to browser language
const browserLang = navigator.language.split('-')[0];
if (supportedLocales.includes(browserLang as SupportedLocale)) {
return browserLang as SupportedLocale;
}
}
return defaultLocale;
}
// Initialize i18n at module scope (required for SSR)
init({
fallbackLocale: defaultLocale,
initialLocale: getInitialLocale(),
});
// Set locale and persist to localStorage
export function setLocale(newLocale: SupportedLocale) {
locale.set(newLocale);
if (browser) {
localStorage.setItem('clock_locale', newLocale);
}
}
// Wait for locale to be loaded (useful for SSR)
export { waitLocale };

View file

@ -1,23 +0,0 @@
{
"app": {
"name": "Clock"
},
"common": {
"back": "Zurück",
"cancel": "Abbrechen",
"loading": "Lade..."
},
"nav": {
"home": "Startseite",
"settings": "Einstellungen"
},
"clock": {
"title": "Life Clock",
"remaining": "Verbleibende Zeit",
"elapsed": "Vergangene Zeit"
},
"messages": {
"saved": "Gespeichert",
"error": "Ein Fehler ist aufgetreten"
}
}

View file

@ -1,23 +0,0 @@
{
"app": {
"name": "Clock"
},
"common": {
"back": "Back",
"cancel": "Cancel",
"loading": "Loading..."
},
"nav": {
"home": "Home",
"settings": "Settings"
},
"clock": {
"title": "Life Clock",
"remaining": "Time remaining",
"elapsed": "Time elapsed"
},
"messages": {
"saved": "Saved",
"error": "An error occurred"
}
}

View file

@ -1,82 +0,0 @@
import { createAppOnboardingStore, type AppOnboardingStep } from '@manacore/shared-app-onboarding';
import { userSettings } from './user-settings.svelte';
/**
* Clock-specific onboarding steps
*/
const clockOnboardingSteps: AppOnboardingStep[] = [
{
id: 'features',
type: 'info',
question: 'Willkommen bei Clock!',
description: 'Das kann Clock für dich tun:',
emoji: '🕐',
gradient: { from: 'blue-500', to: 'blue-700' },
bullets: [
'Flexible Timer & Stoppuhr',
'Pomodoro-Technik für produktives Arbeiten',
'Voreingestellte Timer-Dauern',
'Minimalistisches Design',
],
},
{
id: 'defaultTimer',
type: 'select',
question: 'Welche Timer-Dauer nutzt du am häufigsten?',
description: 'Du kannst Timer jederzeit individuell einstellen.',
emoji: '⏱️',
gradient: { from: 'blue-500', to: 'blue-700' },
options: [
{
id: '5',
label: '5 Minuten',
description: 'Für kurze Pausen',
emoji: '⚡',
},
{
id: '15',
label: '15 Minuten',
description: 'Für konzentrierte Einheiten',
emoji: '🎯',
},
{
id: '25',
label: '25 Minuten',
description: 'Pomodoro-Technik (Empfohlen)',
emoji: '🍅',
},
{
id: '45',
label: '45 Minuten',
description: 'Für längere Arbeitsphasen',
emoji: '🧘',
},
],
defaultValue: '25',
},
{
id: 'welcome',
type: 'info',
question: 'Deine Uhr ist bereit!',
description: 'Hier sind einige Tipps:',
emoji: '🎉',
gradient: { from: 'primary', to: 'primary/70' },
bullets: [
'Nutze die Stoppuhr für freie Zeitmessung',
'Stelle Wecker für wichtige Erinnerungen',
'Die Weltuhr zeigt mehrere Zeitzonen gleichzeitig',
'Drücke Cmd/Ctrl+K für die Schnellsuche',
],
},
];
/**
* Clock app onboarding store
*/
export const clockOnboarding = createAppOnboardingStore({
appId: 'clock',
steps: clockOnboardingSteps,
userSettings,
onComplete: async () => {},
onSkip: async () => {},
});

View file

@ -1,9 +0,0 @@
/**
* Auth Store uses centralized Mana auth factory.
*/
import { createManaAuthStore } from '@manacore/shared-auth-stores';
export const authStore = createManaAuthStore({
devBackendPort: 3017,
});

View file

@ -1,5 +0,0 @@
import { createSimpleNavigationStores } from '@manacore/shared-stores';
export const { isNavCollapsed } = createSimpleNavigationStores({
storageKey: 'clock',
});

View file

@ -1,13 +0,0 @@
/**
* Tag Store Local-First via Shared Tag Store
* Tags are stored in shared IndexedDB ('manacore-tags'), accessible across all apps.
* Use context ('tags') for reads, tagMutations for writes.
*/
export {
tagMutations,
useAllTags,
getTagById,
getTagsByIds,
getTagColor,
getTagsByGroup,
} from '@manacore/shared-stores';

View file

@ -1,7 +0,0 @@
import { createThemeStore } from '@manacore/shared-theme';
// Create theme store with Clock's styling
export const theme = createThemeStore({
appId: 'clock',
defaultVariant: 'lume',
});

View file

@ -1,28 +0,0 @@
/**
* 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 { browser } from '$app/environment';
import { createUserSettingsStore } from '@manacore/shared-theme';
import { authStore } from './auth.svelte';
// Get auth URL dynamically at runtime
function getAuthUrl(): string {
if (browser && typeof window !== 'undefined') {
const injectedUrl = (window as unknown as { __PUBLIC_MANA_CORE_AUTH_URL__?: string })
.__PUBLIC_MANA_CORE_AUTH_URL__;
if (injectedUrl) return injectedUrl;
}
return import.meta.env.DEV ? 'http://localhost:3001' : '';
}
export const userSettings = createUserSettingsStore({
appId: 'clock',
authUrl: getAuthUrl,
getAccessToken: () => authStore.getAccessToken(),
});

View file

@ -1,4 +0,0 @@
export const APP_VERSION = '0.2.0';
export const BUILD_TIME: string =
typeof __BUILD_TIME__ !== 'undefined' ? __BUILD_TIME__ : new Date().toISOString();
export const BUILD_HASH: string = typeof __BUILD_HASH__ !== 'undefined' ? __BUILD_HASH__ : 'dev';

View file

@ -1,465 +0,0 @@
<script lang="ts">
import { setContext } from 'svelte';
import { goto } from '$app/navigation';
import { page } from '$app/stores';
import { locale } from 'svelte-i18n';
import { PillNavigation, CommandBar, TagStrip } from '@manacore/shared-ui';
import { SyncIndicator } from '@manacore/shared-ui';
import type {
PillNavItem,
PillDropdownItem,
CommandBarItem,
QuickAction,
SpotlightAction,
} from '@manacore/shared-ui';
import { theme } from '$lib/stores/theme.svelte';
import { authStore } from '$lib/stores/auth.svelte';
import { userSettings } from '$lib/stores/user-settings.svelte';
import { useAllAlarms, useAllTimers, useAllWorldClocks } from '$lib/data/queries';
import {
THEME_DEFINITIONS,
DEFAULT_THEME_VARIANTS,
EXTENDED_THEME_VARIANTS,
} from '@manacore/shared-theme';
import type { ThemeVariant } from '@manacore/shared-theme';
import { filterHiddenNavItems } from '@manacore/shared-theme';
import { isNavCollapsed as collapsedStore } from '$lib/stores/navigation';
import { getLanguageDropdownItems, getCurrentLanguageLabel } from '@manacore/shared-i18n';
import { getPillAppItems, getManaApp } from '@manacore/shared-branding';
import { setLocale, supportedLocales } from '$lib/i18n';
import { clockOnboarding } from '$lib/stores/app-onboarding.svelte';
import { MiniOnboardingModal } from '@manacore/shared-app-onboarding';
import { SessionExpiredBanner, AuthGate, GuestWelcomeModal } from '@manacore/shared-auth-ui';
import { shouldShowGuestWelcome } from '@manacore/shared-auth-ui';
import { clockStore } from '$lib/data/local-store';
import {
tagLocalStore,
tagMutations,
useAllTags as useAllSharedTags,
} from '@manacore/shared-stores';
// Live queries — auto-update when IndexedDB changes (local writes, sync, other tabs)
const allAlarms = useAllAlarms();
const allTimers = useAllTimers();
const allWorldClocks = useAllWorldClocks();
const allTags = useAllSharedTags();
// Provide data to child components via Svelte context
setContext('alarms', allAlarms);
setContext('timers', allTimers);
setContext('worldClocks', allWorldClocks);
setContext('tags', allTags);
// Guest welcome modal state
let showGuestWelcome = $state(false);
function initGuestWelcome() {
if (!authStore.isAuthenticated && shouldShowGuestWelcome('clock')) {
showGuestWelcome = true;
}
}
// App switcher items
let appItems = $derived(getPillAppItems('clock', undefined, undefined, authStore.user?.tier));
let { children } = $props();
// CommandBar state
let commandBarOpen = $state(false);
// CommandBar quick actions
const commandBarQuickActions: QuickAction[] = [
{
id: 'alarm',
label: 'Neuen Wecker erstellen',
icon: 'bell',
href: '/alarms?new=true',
shortcut: 'A',
},
{
id: 'timer',
label: 'Neuen Timer starten',
icon: 'timer',
href: '/timers?new=true',
shortcut: 'T',
},
{ id: 'stopwatch', label: 'Stoppuhr', icon: 'stopwatch', href: '/stopwatch' },
{ id: 'pomodoro', label: 'Pomodoro starten', icon: 'target', href: '/pomodoro' },
{ id: 'worldclock', label: 'Weltzeituhr', icon: 'globe', href: '/world-clock' },
{ id: 'settings', label: 'Einstellungen', icon: 'settings', href: '/settings' },
];
// CommandBar search - search alarms and timers using live query data
async function handleCommandBarSearch(query: string): Promise<CommandBarItem[]> {
if (!query.trim()) return [];
const queryLower = query.toLowerCase();
const results: CommandBarItem[] = [];
// Search alarms (from live query)
const matchingAlarms = allAlarms.value
.filter((alarm) => alarm.label?.toLowerCase().includes(queryLower))
.slice(0, 5)
.map((alarm) => ({
id: `alarm-${alarm.id}`,
title: alarm.label || 'Wecker',
subtitle: `${alarm.time.slice(0, 5)} ${alarm.enabled ? '(aktiv)' : '(inaktiv)'}`,
}));
results.push(...matchingAlarms);
// Search timers (from live query)
const matchingTimers = allTimers.value
.filter((timer) => timer.label?.toLowerCase().includes(queryLower))
.slice(0, 5)
.map((timer) => {
const mins = Math.floor(timer.durationSeconds / 60);
const secs = timer.durationSeconds % 60;
return {
id: `timer-${timer.id}`,
title: timer.label || 'Timer',
subtitle: `${mins}:${secs.toString().padStart(2, '0')} ${timer.status === 'running' ? '(läuft)' : ''}`,
};
});
results.push(...matchingTimers);
return results.slice(0, 10);
}
function handleCommandBarSelect(item: CommandBarItem) {
if (item.id.startsWith('alarm-')) {
goto('/alarms');
} else if (item.id.startsWith('timer-')) {
goto('/timers');
}
}
let isCollapsed = $state(false);
// Use theme store's isDark directly
let isDark = $derived(theme.isDark);
// Get pinned themes from user settings (extended themes only)
let pinnedThemes = $derived<ThemeVariant[]>(
(userSettings.theme?.pinnedThemes || []).filter((t): t is ThemeVariant =>
EXTENDED_THEME_VARIANTS.includes(t as ThemeVariant)
)
);
// Visible themes in PillNav: default + pinned extended
let visibleThemes = $derived<ThemeVariant[]>([...DEFAULT_THEME_VARIANTS, ...pinnedThemes]);
// Theme variant dropdown items (with SSR fallback)
let themeVariantItems = $derived<PillDropdownItem[]>([
...visibleThemes.map((variant) => ({
id: variant,
label: THEME_DEFINITIONS[variant]?.label || variant,
icon: THEME_DEFINITIONS[variant]?.icon || '🎨',
onClick: () => theme.setVariant(variant),
active: (theme.variant || 'lume') === variant,
})),
{
id: 'all-themes',
label: 'Alle Themes',
icon: 'palette',
onClick: () => goto('/themes'),
active: false,
},
]);
// Current theme variant label (with SSR fallback)
let currentThemeVariantLabel = $derived(
THEME_DEFINITIONS[theme.variant]?.label || THEME_DEFINITIONS.lume?.label || 'Lume'
);
// Language selector items
let currentLocale = $derived($locale || 'de');
function handleLocaleChange(newLocale: string) {
setLocale(newLocale as any);
}
let languageItems = $derived(
getLanguageDropdownItems(supportedLocales, currentLocale, handleLocaleChange)
);
let currentLanguageLabel = $derived(getCurrentLanguageLabel(currentLocale));
// User email for user dropdown
// User email for user dropdown — empty string for guests so PillNav shows login button
let userEmail = $derived(authStore.isAuthenticated ? authStore.user?.email || 'Menü' : '');
// Spotlight actions for PillNavigation
const spotlightActions: SpotlightAction[] = [
{
id: 'new-alarm',
label: 'Neuer Alarm',
icon: 'plus',
category: 'Erstellen',
onExecute: () => goto('/alarms'),
},
{
id: 'new-timer',
label: 'Neuer Timer',
icon: 'plus',
category: 'Erstellen',
onExecute: () => goto('/timers'),
},
{
id: 'world-clock',
label: 'Weltuhren',
category: 'Navigation',
onExecute: () => goto('/world-clock'),
},
{
id: 'settings',
label: 'Einstellungen',
category: 'Navigation',
onExecute: () => goto('/settings'),
},
];
// TagStrip visibility
let isTagStripVisible = $state(false);
function handleTagStripToggle() {
isTagStripVisible = !isTagStripVisible;
}
// Base navigation items for Clock
const baseNavItems: PillNavItem[] = [
{ href: '/', label: 'Übersicht', icon: 'home' },
{ href: '/alarms', label: 'Wecker', icon: 'bell' },
{ href: '/timers', label: 'Timer', icon: 'timer' },
{ href: '/stopwatch', label: 'Stoppuhr', icon: 'stopwatch' },
{ href: '/pomodoro', label: 'Pomodoro', icon: 'target' },
{ href: '/world-clock', label: 'Weltzeituhr', icon: 'globe' },
{ href: '/life', label: 'Lebensuhr', icon: 'heart' },
{ href: '/settings', label: 'Einstellungen', icon: 'settings' },
{
href: '/',
label: 'Tags',
icon: 'tag',
onClick: handleTagStripToggle,
active: isTagStripVisible,
},
];
// Navigation items filtered by visibility settings
const navItems = $derived(
filterHiddenNavItems('clock', baseNavItems, userSettings.nav?.hiddenNavItems || {})
);
// Navigation shortcuts (Ctrl+1-9) - use base items for consistent shortcuts
const navRoutes = baseNavItems.map((item) => item.href);
function handleKeydown(event: KeyboardEvent) {
const target = event.target as HTMLElement;
// Cmd/Ctrl+K to open command bar (works even in inputs)
if ((event.ctrlKey || event.metaKey) && event.key === 'k') {
event.preventDefault();
commandBarOpen = true;
return;
}
if (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.isContentEditable) {
return;
}
if ((event.ctrlKey || event.metaKey) && !event.shiftKey && !event.altKey) {
const num = parseInt(event.key);
if (num >= 1 && num <= navRoutes.length) {
event.preventDefault();
const route = navRoutes[num - 1];
if (route) {
goto(route);
}
}
}
}
function handleCollapsedChange(collapsed: boolean) {
isCollapsed = collapsed;
collapsedStore.set(collapsed);
if (typeof localStorage !== 'undefined') {
localStorage.setItem('clock-nav-collapsed', String(collapsed));
}
}
function handleToggleTheme() {
theme.toggleMode();
}
function handleThemeModeChange(mode: 'light' | 'dark' | 'system') {
theme.setMode(mode);
}
async function handleLogout() {
await authStore.signOut();
goto('/login');
}
async function handleAuthReady() {
// Initialize local-first databases (opens IndexedDB, seeds guest data)
await Promise.all([clockStore.initialize(), tagLocalStore.initialize()]);
// If authenticated, start syncing to server
if (authStore.isAuthenticated) {
const getToken = () => authStore.getValidToken();
clockStore.startSync(getToken);
tagMutations.startSync(getToken);
}
// Initialize collapsed state from localStorage
const savedCollapsed = localStorage.getItem('clock-nav-collapsed');
if (savedCollapsed === 'true') {
isCollapsed = true;
collapsedStore.set(true);
}
// Show guest welcome modal on first visit
initGuestWelcome();
// Load user settings (requires auth)
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 });
}
}
</script>
<svelte:window onkeydown={handleKeydown} />
<AuthGate
{authStore}
{goto}
allowGuest={true}
onReady={handleAuthReady}
requiredTier={getManaApp('clock')?.requiredTier}
appName={getManaApp('clock')?.name}
>
<div class="layout-container">
<PillNavigation
items={navItems}
currentPath={$page.url.pathname}
appName="Clock"
homeRoute="/"
onToggleTheme={handleToggleTheme}
{isDark}
{isCollapsed}
onCollapsedChange={handleCollapsedChange}
showThemeToggle={true}
showThemeVariants={true}
{themeVariantItems}
{currentThemeVariantLabel}
themeMode={theme.mode}
onThemeModeChange={handleThemeModeChange}
showLanguageSwitcher={true}
{languageItems}
{currentLanguageLabel}
showLogout={authStore.isAuthenticated}
onLogout={handleLogout}
loginHref="/login"
primaryColor="#f59e0b"
showAppSwitcher={true}
{appItems}
{userEmail}
settingsHref="/settings"
manaHref="/mana"
profileHref="/profile"
themesHref="/themes"
helpHref="/help"
allAppsHref="/apps"
{spotlightActions}
/>
<!-- TagStrip (above PillNav, toggled via Tags pill) -->
{#if isTagStripVisible}
<TagStrip
tags={allTags.value.map((t) => ({
id: t.id,
name: t.name,
color: t.color || '#3b82f6',
}))}
selectedIds={[]}
onToggle={() => {}}
onClear={() => {}}
managementHref="/tags"
/>
{/if}
<main class="main-content bg-background">
<div class="content-wrapper">
{@render children()}
</div>
</main>
<!-- Global Command Bar (Cmd/K) -->
<CommandBar
bind:open={commandBarOpen}
onClose={() => (commandBarOpen = false)}
onSearch={handleCommandBarSearch}
onSelect={handleCommandBarSelect}
quickActions={commandBarQuickActions}
placeholder="Schnellzugriff..."
emptyText="Keine Ergebnisse"
searchingText="Suche..."
/>
</div>
<!-- Onboarding Modal -->
{#if clockOnboarding.shouldShow}
<MiniOnboardingModal store={clockOnboarding} appName="Uhr" appEmoji="⏰" />
{/if}
<!-- Guest Welcome Modal -->
<GuestWelcomeModal
appId="clock"
visible={showGuestWelcome}
onClose={() => (showGuestWelcome = false)}
onLogin={() => goto('/login')}
onRegister={() => goto('/register')}
locale={($locale || 'de') === 'de' ? 'de' : 'en'}
/>
{#if authStore.isAuthenticated}
<SessionExpiredBanner locale={$locale || 'de'} loginHref="/login" />
{/if}
<SyncIndicator />
</AuthGate>
<style>
.layout-container {
display: flex;
flex-direction: column;
min-height: 100vh;
}
.main-content {
position: relative;
z-index: 0;
padding-bottom: 100px;
}
.content-wrapper {
max-width: 100%;
margin-left: auto;
margin-right: auto;
padding: 1rem;
position: relative;
z-index: 0;
}
@media (min-width: 640px) {
.content-wrapper {
padding: 1.5rem;
}
}
@media (min-width: 1024px) {
.content-wrapper {
padding: 2rem;
}
}
</style>

View file

@ -1,115 +0,0 @@
<script lang="ts">
import { onMount } from 'svelte';
import { goto } from '$app/navigation';
import { Clock, Bell, Timer, Hourglass, Globe } from '@manacore/shared-icons';
import { authStore } from '$lib/stores/auth.svelte';
import { AppLoadingSkeleton } from '$lib/components/skeletons';
let isLoading = $state(true);
onMount(async () => {
// No auth redirect - guest mode allowed
isLoading = false;
});
const quickLinks = [
{
href: '/world-clock',
icon: Globe,
label: 'Weltzeituhr',
description: 'Zeitzonen im Blick',
color: 'bg-blue-500',
},
{
href: '/alarms',
icon: Bell,
label: 'Wecker',
description: 'Alarme verwalten',
color: 'bg-amber-500',
},
{
href: '/timers',
icon: Timer,
label: 'Timer',
description: 'Countdowns starten',
color: 'bg-green-500',
},
{
href: '/stopwatch',
icon: Hourglass,
label: 'Stoppuhr',
description: 'Zeit messen',
color: 'bg-purple-500',
},
];
</script>
<svelte:head>
<title>Clock - Dashboard</title>
</svelte:head>
{#if isLoading}
<AppLoadingSkeleton />
{:else}
<div class="dashboard">
<header class="mb-8">
<h1 class="text-2xl font-bold text-foreground">Clock</h1>
<p class="text-muted-foreground text-sm mt-1">Dein Zeit-Management Hub</p>
</header>
<!-- Current Time Display -->
<div class="current-time-card mb-8 p-6 rounded-xl bg-card border border-border">
<div class="flex items-center gap-4">
<div class="p-3 rounded-full bg-primary/10">
<Clock size={32} class="text-primary" />
</div>
<div>
<div class="text-4xl font-bold text-foreground tabular-nums">
{new Date().toLocaleTimeString('de-DE', {
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
})}
</div>
<div class="text-muted-foreground">
{new Date().toLocaleDateString('de-DE', {
weekday: 'long',
year: 'numeric',
month: 'long',
day: 'numeric',
})}
</div>
</div>
</div>
</div>
<!-- Quick Links Grid -->
<div class="grid grid-cols-2 md:grid-cols-4 gap-4">
{#each quickLinks as link}
<a
href={link.href}
class="quick-link p-4 rounded-xl bg-card border border-border hover:border-primary/50 transition-all hover:shadow-lg group"
>
<div class="flex flex-col items-center text-center gap-3">
<div
class="{link.color} p-3 rounded-full text-white group-hover:scale-110 transition-transform"
>
<link.icon size={24} />
</div>
<div>
<div class="font-medium text-foreground">{link.label}</div>
<div class="text-xs text-muted-foreground">{link.description}</div>
</div>
</div>
</a>
{/each}
</div>
</div>
{/if}
<style>
.dashboard {
max-width: 800px;
margin: 0 auto;
}
</style>

View file

@ -1,32 +0,0 @@
<script lang="ts">
import { browser } from '$app/environment';
import { FeedbackPage } from '@manacore/feedback';
import { createFeedbackService } from '@manacore/feedback';
import { authStore } from '$lib/stores/auth.svelte';
import '$lib/i18n';
// Get auth URL dynamically at runtime
function getAuthUrl(): string {
if (browser && typeof window !== 'undefined') {
const injectedUrl = (window as unknown as { __PUBLIC_MANA_CORE_AUTH_URL__?: string })
.__PUBLIC_MANA_CORE_AUTH_URL__;
return injectedUrl || 'http://localhost:3001';
}
return 'http://localhost:3001';
}
const feedbackService = createFeedbackService({
appName: 'clock',
apiUrl: getAuthUrl(),
});
async function handleSubmit(data: { type: string; message: string; email?: string }) {
const token = await authStore.getAccessToken();
return feedbackService.submit({
...data,
token: token || undefined,
});
}
</script>
<FeedbackPage appName="Clock" onSubmit={handleSubmit} userEmail={authStore.user?.email} />

View file

@ -1,32 +0,0 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { locale } from 'svelte-i18n';
import { HelpPage, getHelpTranslations } from '@manacore/help';
import { getClockHelpContent } from '$lib/content/help/index.js';
const content = $derived(getClockHelpContent($locale ?? 'de'));
const translations = $derived(
getHelpTranslations($locale ?? 'de', {
subtitle:
$locale === 'de'
? 'Finde Antworten und lerne Clock kennen'
: 'Find answers and learn how to use Clock',
})
);
</script>
<svelte:head>
<title>{translations.title} | Clock</title>
</svelte:head>
<HelpPage
{content}
appName="Clock"
appId="clock"
{translations}
showBackButton
onBack={() => goto('/')}
showGettingStarted={false}
showChangelog={false}
defaultSection="faq"
/>

View file

@ -1,6 +0,0 @@
<script lang="ts">
import { SubscriptionPage } from '@manacore/subscriptions';
import { authStore } from '$lib/stores/auth.svelte';
</script>
<SubscriptionPage user={authStore.user} appName="Clock" />

View file

@ -1,6 +0,0 @@
<script lang="ts">
import { ProfilePage } from '@manacore/shared-profile-ui';
import { authStore } from '$lib/stores/auth.svelte';
</script>
<ProfilePage user={authStore.user} appName="Clock" />

View file

@ -1,108 +0,0 @@
<script lang="ts">
import { onMount } from 'svelte';
import { _ } from 'svelte-i18n';
import { userSettings } from '$lib/stores/user-settings.svelte';
import { GlobalSettingsSection } from '@manacore/shared-ui';
import { APP_VERSION } from '$lib/version';
onMount(async () => {
await userSettings.load();
});
// Settings state
let clockFormat = $state<'24h' | '12h'>('24h');
// Load settings from localStorage
if (typeof localStorage !== 'undefined') {
const savedFormat = localStorage.getItem('clock-format');
if (savedFormat === '12h') {
clockFormat = '12h';
}
}
function setClockFormat(format: '24h' | '12h') {
clockFormat = format;
if (typeof localStorage !== 'undefined') {
localStorage.setItem('clock-format', format);
}
}
// Translation function for GlobalSettingsSection
function translate(key: string): string {
return $_?.(key) ?? key;
}
</script>
<div class="mx-auto max-w-2xl space-y-6">
<h1 class="text-2xl font-bold text-foreground">{$_('settings.title')}</h1>
<!-- Global Settings Section (synced across all apps) -->
<GlobalSettingsSection
{userSettings}
appId="clock"
title="App-Einstellungen"
description="Diese Einstellungen werden mit allen Mana Apps synchronisiert"
t={translate}
/>
<!-- Clock-specific Settings -->
<div class="card">
<h2 class="mb-4 text-lg font-semibold">{$_('settings.clockFormat')}</h2>
<div>
<label class="mb-2 block text-sm font-medium">Zeitformat</label>
<div class="flex gap-2">
<button
class="btn btn-sm"
class:btn-primary={clockFormat === '24h'}
class:btn-secondary={clockFormat !== '24h'}
onclick={() => setClockFormat('24h')}
>
{$_('settings.format24h')}
</button>
<button
class="btn btn-sm"
class:btn-primary={clockFormat === '12h'}
class:btn-secondary={clockFormat !== '12h'}
onclick={() => setClockFormat('12h')}
>
{$_('settings.format12h')}
</button>
</div>
</div>
</div>
<!-- Notifications Section -->
<div class="card">
<h2 class="mb-4 text-lg font-semibold">{$_('settings.notifications')}</h2>
<p class="text-sm text-muted-foreground">
Benachrichtigungen werden für Wecker, Timer und Pomodoro-Sitzungen verwendet.
</p>
<button
class="btn btn-secondary mt-4"
onclick={async () => {
if ('Notification' in window) {
const permission = await Notification.requestPermission();
if (permission === 'granted') {
new Notification('Clock', {
body: 'Benachrichtigungen sind jetzt aktiviert!',
});
}
}
}}
>
Benachrichtigungen aktivieren
</button>
</div>
<!-- Sounds Section -->
<div class="card">
<h2 class="mb-4 text-lg font-semibold">{$_('settings.sounds')}</h2>
<p class="text-sm text-muted-foreground">
Töne können für einzelne Wecker und Timer in deren Einstellungen angepasst werden.
</p>
</div>
<p class="mt-8 pb-4 text-center text-xs text-gray-400 dark:text-gray-600">v{APP_VERSION}</p>
</div>

View file

@ -1,45 +0,0 @@
<script lang="ts">
import { getContext } from 'svelte';
import { tagMutations } from '@manacore/shared-stores';
import type { Tag } from '@manacore/shared-tags';
// Live tags from layout context
const tagsCtx: { readonly value: Tag[] } = getContext('tags');
</script>
<svelte:head>
<title>Tags | Clock</title>
</svelte:head>
<div class="tags-page">
<h1>Tags verwalten</h1>
<p class="text-sm text-muted-foreground mb-4">
Tags sind app-übergreifend — Änderungen gelten in allen ManaCore-Apps.
</p>
{#if tagsCtx.value.length === 0}
<p>Keine Tags vorhanden.</p>
{:else}
<div class="grid gap-2">
{#each tagsCtx.value as tag}
<div class="flex items-center gap-2 p-2 rounded-lg bg-card">
<span class="w-3 h-3 rounded-full" style="background-color: {tag.color}"></span>
<span>{tag.name}</span>
</div>
{/each}
</div>
{/if}
</div>
<style>
.tags-page {
padding: 1.5rem;
max-width: 600px;
margin: 0 auto;
}
h1 {
font-size: 1.5rem;
font-weight: 700;
margin-bottom: 0.5rem;
}
</style>

View file

@ -1,30 +0,0 @@
<script lang="ts">
import { theme } from '$lib/stores/theme.svelte';
import { THEME_DEFINITIONS, THEME_VARIANTS } from '@manacore/shared-theme';
</script>
<div class="mx-auto max-w-4xl">
<h1 class="mb-6 text-2xl font-bold text-foreground">Alle Themes</h1>
<div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
{#each THEME_VARIANTS as variant}
{@const def = THEME_DEFINITIONS[variant]}
<button
class="card text-left transition-all hover:border-primary/50"
class:border-primary={theme.variant === variant}
onclick={() => theme.setVariant(variant)}
>
<div class="flex items-center gap-3">
<span class="text-3xl">{def.icon}</span>
<div>
<h3 class="font-semibold">{def.label}</h3>
<p class="text-sm text-muted-foreground">{def.description}</p>
</div>
</div>
{#if theme.variant === variant}
<div class="mt-3 text-sm text-primary">✓ Aktiv</div>
{/if}
</button>
{/each}
</div>
</div>

View file

@ -1,35 +0,0 @@
<script lang="ts">
import { ForgotPasswordPage } from '@manacore/shared-auth-ui';
import { authStore } from '$lib/stores/auth.svelte';
import '$lib/i18n';
let error = $state('');
let success = $state(false);
let loading = $state(false);
async function handleResetPassword(email: string) {
loading = true;
error = '';
success = false;
const result = await authStore.resetPassword(email);
if (result.success) {
success = true;
} else {
error = result.error || 'Passwort-Zurücksetzung fehlgeschlagen';
}
loading = false;
}
</script>
<ForgotPasswordPage
appName="Clock"
appLogo=""
{loading}
{error}
{success}
onSubmit={handleResetPassword}
loginHref="/login"
/>

View file

@ -1,73 +0,0 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { page } from '$app/stores';
import { browser } from '$app/environment';
import { locale } from 'svelte-i18n';
import { LoginPage } from '@manacore/shared-auth-ui';
import { getLoginTranslations } from '@manacore/shared-i18n';
import { ClockLogo } from '@manacore/shared-branding';
import { authStore } from '$lib/stores/auth.svelte';
import { APP_VERSION, BUILD_TIME } from '$lib/version';
import '$lib/i18n';
// Read verification status from query params (set after email verification)
const verified = $derived($page.url.searchParams.get('verified') === 'true');
const initialEmail = $derived($page.url.searchParams.get('email') || '');
// Get redirect URL from query params or sessionStorage (set by AuthGateModal in guest mode)
const redirectTo = $derived.by(() => {
const queryRedirect = $page.url.searchParams.get('redirectTo');
if (queryRedirect) return queryRedirect;
// Check sessionStorage for return URL (from guest mode)
if (browser) {
const sessionRedirect = sessionStorage.getItem('auth-return-url');
if (sessionRedirect) {
// Clear it after reading
sessionStorage.removeItem('auth-return-url');
return sessionRedirect;
}
}
return '/';
});
// Get translations based on current locale
const translations = $derived(getLoginTranslations($locale || 'de'));
async function handleSignIn(email: string, password: string) {
return authStore.signIn(email, password);
}
async function handleResendVerification(email: string) {
return authStore.resendVerificationEmail(email);
}
</script>
<svelte:head>
<title>{translations.title} | Clock</title>
</svelte:head>
<LoginPage
appName="Clock"
logo={ClockLogo}
primaryColor="#f59e0b"
onSignIn={handleSignIn}
onResendVerification={handleResendVerification}
passkeyAvailable={authStore.isPasskeyAvailable()}
onSignInWithPasskey={() => authStore.signInWithPasskey()}
onVerifyTwoFactor={(code, trust) => authStore.verifyTwoFactor(code, trust)}
onVerifyBackupCode={(code) => authStore.verifyBackupCode(code)}
onSendMagicLink={(email) => authStore.sendMagicLink(email)}
{goto}
successRedirect={redirectTo}
registerPath="/register"
forgotPasswordPath="/forgot-password"
lightBackground="#fffbeb"
darkBackground="#1c1917"
{translations}
{verified}
{initialEmail}
version={APP_VERSION}
buildTime={BUILD_TIME}
/>

View file

@ -1,52 +0,0 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { browser } from '$app/environment';
import { locale } from 'svelte-i18n';
import { RegisterPage } from '@manacore/shared-auth-ui';
import { getRegisterTranslations } from '@manacore/shared-i18n';
import { ClockLogo } from '@manacore/shared-branding';
import { authStore } from '$lib/stores/auth.svelte';
import '$lib/i18n';
// Get redirect URL from sessionStorage (set by AuthGateModal in guest mode)
const redirectTo = $derived.by(() => {
if (browser) {
const sessionRedirect = sessionStorage.getItem('auth-return-url');
if (sessionRedirect) {
// Clear it after reading
sessionStorage.removeItem('auth-return-url');
return sessionRedirect;
}
}
return '/';
});
// Get translations based on current locale
const translations = $derived(getRegisterTranslations($locale || 'de'));
async function handleSignUp(email: string, password: string) {
return authStore.signUp(email, password);
}
async function handleResendVerification(email: string) {
return authStore.resendVerificationEmail(email);
}
</script>
<svelte:head>
<title>{translations.title} | Clock</title>
</svelte:head>
<RegisterPage
appName="Clock"
logo={ClockLogo}
primaryColor="#f59e0b"
onSignUp={handleSignUp}
onResendVerification={handleResendVerification}
{goto}
successRedirect={redirectTo}
loginPath="/login"
lightBackground="#fffbeb"
darkBackground="#1c1917"
{translations}
/>

View file

@ -1,174 +0,0 @@
<script lang="ts">
import { onMount } from 'svelte';
import { page } from '$app/stores';
import { goto } from '$app/navigation';
import { ClockLogo } from '@manacore/shared-branding';
import { authStore } from '$lib/stores/auth.svelte';
let loading = $state(false);
let hasToken = $state(false);
let token = $state<string | null>(null);
let password = $state('');
let confirmPassword = $state('');
let error = $state<string | null>(null);
let success = $state(false);
onMount(() => {
token = $page.url.searchParams.get('token');
hasToken = !!token;
});
async function handleSubmit(e: Event) {
e.preventDefault();
error = null;
if (!token) {
error = 'Invalid or missing token';
return;
}
if (password !== confirmPassword) {
error = 'Passwords do not match';
return;
}
if (password.length < 8) {
error = 'Password must be at least 8 characters';
return;
}
loading = true;
try {
const result = await authStore.resetPasswordWithToken(token, password);
if (!result.success) {
error = result.error || 'Failed to reset password';
} else {
success = true;
setTimeout(() => goto('/login'), 3000);
}
} catch (err) {
error = err instanceof Error ? err.message : 'An error occurred';
} finally {
loading = false;
}
}
</script>
<svelte:head>
<title>Reset Password - Clock</title>
</svelte:head>
<div
class="flex min-h-screen flex-col bg-gradient-to-b from-amber-100 to-white dark:from-stone-900 dark:to-stone-800"
>
<header class="flex items-center justify-between p-4">
<a href="/" class="flex items-center gap-2">
<ClockLogo class="h-8 w-8" />
<span class="text-xl font-semibold" style="color: #f59e0b">Clock</span>
</a>
</header>
<main class="flex flex-1 items-center justify-center p-4">
<div class="w-full max-w-md">
<div class="text-center mb-8">
<h1 class="text-3xl font-bold text-gray-900 dark:text-white">Reset Password</h1>
<p class="mt-2 text-gray-600 dark:text-gray-400">
{#if success}Password reset successfully
{:else if hasToken}Enter your new password
{:else}Invalid or missing token{/if}
</p>
</div>
{#if success}
<div class="rounded-xl bg-white p-8 shadow-lg dark:bg-slate-800">
<div class="text-center">
<div class="mb-4 text-6xl">&#x2705;</div>
<p class="mb-6 text-gray-600 dark:text-gray-400">
Your password has been reset successfully. You will be redirected to the login page
shortly.
</p>
<a
href="/login"
class="inline-block rounded-lg px-6 py-3 font-medium text-white transition-colors"
style="background-color: #f59e0b"
>
Go to login
</a>
</div>
</div>
{:else if hasToken}
<div class="rounded-xl bg-white p-8 shadow-lg dark:bg-slate-800">
<form onsubmit={handleSubmit}>
{#if error}
<div
class="mb-4 rounded-lg bg-red-50 p-4 text-sm text-red-800 dark:bg-red-900/20 dark:text-red-400"
>
{error}
</div>
{/if}
<div class="space-y-4">
<div>
<label
for="password"
class="mb-2 block text-sm font-medium text-gray-900 dark:text-gray-100"
>New Password</label
>
<input
type="password"
id="password"
autocomplete="new-password"
placeholder="Enter new password"
required
minlength={8}
bind:value={password}
class="w-full rounded-lg border border-gray-300 px-4 py-3 text-gray-900 focus:outline-none focus:ring-2 dark:border-slate-600 dark:bg-slate-700 dark:text-white dark:placeholder-gray-400"
style="--tw-ring-color: #f59e0b80; border-color: #f59e0b"
/>
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">At least 8 characters</p>
</div>
<div>
<label
for="confirmPassword"
class="mb-2 block text-sm font-medium text-gray-900 dark:text-gray-100"
>Confirm Password</label
>
<input
type="password"
id="confirmPassword"
autocomplete="new-password"
placeholder="Confirm password"
required
minlength={8}
bind:value={confirmPassword}
class="w-full rounded-lg border border-gray-300 px-4 py-3 text-gray-900 focus:outline-none focus:ring-2 dark:border-slate-600 dark:bg-slate-700 dark:text-white dark:placeholder-gray-400"
/>
</div>
<button
type="submit"
disabled={loading}
class="w-full rounded-lg px-4 py-3 font-medium text-white transition-colors disabled:cursor-not-allowed disabled:opacity-50"
style="background-color: #f59e0b"
>
{loading ? 'Updating password...' : 'Update Password'}
</button>
</div>
</form>
</div>
{:else}
<div class="rounded-xl bg-white p-8 shadow-lg dark:bg-slate-800">
<div class="text-center">
<div class="mb-4 text-6xl">&#x26A0;&#xFE0F;</div>
<p class="mb-6 text-gray-600 dark:text-gray-400">
This password reset link is invalid or has expired.
</p>
<a
href="/forgot-password"
class="inline-block rounded-lg px-6 py-3 font-medium text-white transition-colors"
style="background-color: #f59e0b"
>
Request a new link
</a>
</div>
</div>
{/if}
</div>
</main>
</div>

View file

@ -1,47 +0,0 @@
<script lang="ts">
import '../app.css';
import { onMount } from 'svelte';
import { isLoading as isLocaleLoading } from 'svelte-i18n';
import { theme } from '$lib/stores/theme.svelte';
import { authStore } from '$lib/stores/auth.svelte';
import { waitLocale } from '$lib/i18n';
import { ToastContainer, setupGlobalErrorHandler } from '@manacore/shared-ui';
import { AppLoadingSkeleton } from '$lib/components/skeletons';
let { children } = $props();
let loading = $state(true);
onMount(() => {
// Setup global error handling
const cleanupErrorHandler = setupGlobalErrorHandler();
// Initialize async operations
const init = async () => {
// Wait for locale to be loaded
await waitLocale();
// Initialize theme
theme.initialize();
// Initialize auth
await authStore.initialize();
loading = false;
};
init();
return cleanupErrorHandler;
});
</script>
<ToastContainer />
{#if $isLocaleLoading || loading}
<AppLoadingSkeleton />
{:else}
<div class="min-h-screen bg-background text-foreground">
{@render children()}
</div>
{/if}

View file

@ -1,2 +0,0 @@
// Disable SSR — all data is local-first (IndexedDB + mana-sync)
export const ssr = false;

View file

@ -1,10 +0,0 @@
import { json } from '@sveltejs/kit';
import type { RequestHandler } from './$types';
export const GET: RequestHandler = async () => {
return json({
status: 'ok',
service: 'clock-web',
timestamp: new Date().toISOString(),
});
};

View file

@ -1,9 +0,0 @@
<script lang="ts">
import { OfflinePage } from '@manacore/shared-ui';
</script>
<OfflinePage
appName="Clock"
offlineMessage="Die Uhr funktioniert auch offline - Timer und Stoppuhr sind verfügbar."
accentColor="#0891b2"
/>

View file

@ -1 +0,0 @@
export const prerender = true;

View file

@ -1,21 +0,0 @@
import adapter from '@sveltejs/adapter-node';
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
/** @type {import('@sveltejs/kit').Config} */
const config = {
preprocess: vitePreprocess(),
kit: {
adapter: adapter({
out: 'build',
}),
prerender: {
handleHttpError: ({ path, message }) => {
if (path === '/favicon.png') return;
throw new Error(message);
},
},
},
};
export default config;

View file

@ -1,14 +0,0 @@
{
"extends": "./.svelte-kit/tsconfig.json",
"compilerOptions": {
"allowJs": true,
"checkJs": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"skipLibCheck": true,
"sourceMap": true,
"strict": true,
"moduleResolution": "bundler"
}
}

View file

@ -1,33 +0,0 @@
import { sveltekit } from '@sveltejs/kit/vite';
import { defineConfig } from 'vite';
import { SvelteKitPWA } from '@vite-pwa/sveltekit';
import { createPWAConfig } from '@manacore/shared-pwa';
import { MANACORE_SHARED_PACKAGES, getBuildDefines } from '@manacore/shared-vite-config';
export default defineConfig({
plugins: [
sveltekit(),
SvelteKitPWA(
createPWAConfig({
name: 'Clock - Uhr & Timer',
shortName: 'Clock',
description: 'Uhr, Timer und Stoppuhr',
themeColor: '#06b6d4',
preset: 'minimal',
})
),
],
server: {
port: 5187,
strictPort: true,
},
ssr: {
noExternal: [...MANACORE_SHARED_PACKAGES],
},
optimizeDeps: {
exclude: [...MANACORE_SHARED_PACKAGES],
},
define: {
...getBuildDefines(),
},
});

View file

@ -1,19 +0,0 @@
{
"name": "@clock/shared",
"version": "0.2.0",
"private": true,
"type": "module",
"main": "./src/index.ts",
"exports": {
".": "./src/index.ts",
"./types": "./src/types/index.ts",
"./constants": "./src/constants/index.ts"
},
"scripts": {
"type-check": "tsc --noEmit",
"lint": "eslint src"
},
"devDependencies": {
"typescript": "^5.7.2"
}
}

View file

@ -1,151 +0,0 @@
// 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;
// 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 (like iOS Clock app)
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;

View file

@ -1,2 +0,0 @@
export * from './types';
export * from './constants';

View file

@ -1,55 +0,0 @@
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;

View file

@ -1,4 +0,0 @@
export * from './alarm';
export * from './timer';
export * from './world-clock';
export * from './preset';

View file

@ -1,42 +0,0 @@
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,
};

View file

@ -1,49 +0,0 @@
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];
}

View file

@ -1,18 +0,0 @@
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;
}

View file

@ -1,14 +0,0 @@
{
"compilerOptions": {
"target": "ES2021",
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true,
"skipLibCheck": true,
"declaration": true,
"outDir": "./dist",
"rootDir": "./src"
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}

View file

@ -43,7 +43,6 @@
},
"dependencies": {
"@calc/shared": "workspace:*",
"@clock/shared": "workspace:*",
"@manacore/credits": "workspace:^",
"@manacore/feedback": "workspace:*",
"@manacore/help": "workspace:*",

View file

@ -30,7 +30,6 @@ const APP_SUBDOMAINS = new Set([
'todo',
'chat',
'calendar',
'clock',
'contacts',
'zitare',
'skilltree',

View file

@ -421,43 +421,7 @@ export const appConfigs: Record<string, AppConfig> = {
// ============================================
// UTILITY APPS
// ============================================
clock: {
name: 'clock',
displayName: 'ManaClock',
tagline: 'Uhren & Timer',
description: 'Weltzeituhr, Wecker, Timer und stilvolle Uhren-Widgets in einer App.',
logoEmoji: '⏰',
primaryColor: '#F59E0B',
accentColor: '#FBBF24',
features: [
{
icon: '🌍',
title: 'Weltzeituhr',
description: 'Zeitzonen weltweit im Blick',
color: '#0EA5E9',
},
{
icon: '⏰',
title: 'Wecker',
description: 'Flexible Alarm-Einstellungen',
color: '#F59E0B',
},
{
icon: '⏱️',
title: 'Timer & Stoppuhr',
description: 'Für alle Timing-Aufgaben',
color: '#10B981',
},
{
icon: '🍅',
title: 'Pomodoro',
description: 'Fokussiertes Arbeiten',
color: '#EF4444',
},
],
dashboardRoute: '/dashboard',
website: 'https://clock.mana.how',
},
// clock: consolidated into times
zitare: {
name: 'zitare',

View file

@ -58,11 +58,6 @@ db.version(1).stores({
zitareFavorites: 'id, quoteId',
zitareLists: 'id',
// ─── Clock (appId: 'clock') ───
alarms: 'id, enabled, time',
timers: 'id, status',
worldClocks: 'id, sortOrder, timezone',
// ─── Mukke (appId: 'mukke') ───
songs: 'id, artist, album, genre, favorite, title',
mukkePlaylists: 'id, name',
@ -111,6 +106,9 @@ db.version(1).stores({
timeTags: 'id, name, order',
timeTemplates: 'id, usageCount, lastUsedAt, projectId',
timeSettings: 'id',
timeAlarms: 'id, enabled, time',
timeCountdownTimers: 'id, status',
timeWorldClocks: 'id, sortOrder, timezone',
// ─── Context (appId: 'context') ───
contextSpaces: 'id, pinned, prefix',
@ -186,7 +184,6 @@ export const SYNC_APP_MAP: Record<string, string[]> = {
picture: ['images', 'boards', 'boardItems', 'pictureTags', 'imageTags'],
cards: ['cardDecks', 'cards'],
zitare: ['zitareFavorites', 'zitareLists'],
clock: ['alarms', 'timers', 'worldClocks'],
mukke: ['songs', 'mukkePlaylists', 'playlistSongs', 'mukkeProjects', 'markers'],
storage: ['files', 'storageFolders', 'storageTags', 'fileTags'],
presi: ['presiDecks', 'slides'],
@ -201,6 +198,9 @@ export const SYNC_APP_MAP: Record<string, string[]> = {
'timeTags',
'timeTemplates',
'timeSettings',
'timeAlarms',
'timeCountdownTimers',
'timeWorldClocks',
],
context: ['contextSpaces', 'documents'],
questions: ['qCollections', 'questions', 'answers'],
@ -273,6 +273,9 @@ export const TABLE_TO_SYNC_NAME: Record<string, string> = {
timeTags: 'tags',
timeTemplates: 'templates',
timeSettings: 'settings',
timeAlarms: 'alarms',
timeCountdownTimers: 'countdownTimers',
timeWorldClocks: 'worldClocks',
// context
contextSpaces: 'spaces',
// questions

View file

@ -1,126 +0,0 @@
<!--
Clock — Split-Screen AppView
World clocks and active timers.
-->
<script lang="ts">
import { liveQuery } from 'dexie';
import { db } from '$lib/data/database';
import type { LocalWorldClock, LocalTimer, LocalAlarm } from './types';
let worldClocks = $state<LocalWorldClock[]>([]);
let timers = $state<LocalTimer[]>([]);
let alarms = $state<LocalAlarm[]>([]);
let now = $state(new Date());
$effect(() => {
const interval = setInterval(() => {
now = new Date();
}, 1000);
return () => clearInterval(interval);
});
$effect(() => {
const sub = liveQuery(async () => {
return db
.table<LocalWorldClock>('worldClocks')
.orderBy('sortOrder')
.toArray()
.then((all) => all.filter((w) => !w.deletedAt));
}).subscribe((val) => {
worldClocks = val ?? [];
});
return () => sub.unsubscribe();
});
$effect(() => {
const sub = liveQuery(async () => {
return db
.table<LocalTimer>('timers')
.toArray()
.then((all) => all.filter((t) => !t.deletedAt));
}).subscribe((val) => {
timers = val ?? [];
});
return () => sub.unsubscribe();
});
$effect(() => {
const sub = liveQuery(async () => {
return db
.table<LocalAlarm>('alarms')
.toArray()
.then((all) => all.filter((a) => !a.deletedAt));
}).subscribe((val) => {
alarms = val ?? [];
});
return () => sub.unsubscribe();
});
function timeInZone(tz: string): string {
return now.toLocaleTimeString('de', { timeZone: tz, hour: '2-digit', minute: '2-digit' });
}
function formatDuration(seconds: number): string {
const m = Math.floor(seconds / 60);
const s = seconds % 60;
return `${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}`;
}
const activeTimers = $derived(
timers.filter((t) => t.status === 'running' || t.status === 'paused')
);
const enabledAlarms = $derived(alarms.filter((a) => a.enabled));
</script>
<div class="flex h-full flex-col gap-4 p-4">
<!-- Local time -->
<div class="text-center">
<p class="text-3xl font-light tracking-wider text-white/90">
{now.toLocaleTimeString('de', { hour: '2-digit', minute: '2-digit', second: '2-digit' })}
</p>
<p class="text-xs text-white/40">
{now.toLocaleDateString('de', { weekday: 'long', day: 'numeric', month: 'long' })}
</p>
</div>
<!-- World clocks -->
{#if worldClocks.length > 0}
<div>
<h3 class="mb-2 text-xs font-medium text-white/50">Weltuhr</h3>
{#each worldClocks as wc (wc.id)}
<div class="flex items-center justify-between py-1">
<span class="text-sm text-white/60">{wc.cityName}</span>
<span class="font-mono text-sm text-white/80">{timeInZone(wc.timezone)}</span>
</div>
{/each}
</div>
{/if}
<!-- Active timers -->
{#if activeTimers.length > 0}
<div>
<h3 class="mb-2 text-xs font-medium text-white/50">Timer</h3>
{#each activeTimers as timer (timer.id)}
<div class="flex items-center justify-between rounded-md bg-white/5 px-3 py-2">
<span class="text-sm text-white/60">{timer.label ?? 'Timer'}</span>
<span class="font-mono text-sm text-white/80">
{formatDuration(timer.remainingSeconds ?? timer.durationSeconds)}
</span>
</div>
{/each}
</div>
{/if}
<!-- Alarms summary -->
{#if enabledAlarms.length > 0}
<div>
<h3 class="mb-2 text-xs font-medium text-white/50">Wecker ({enabledAlarms.length})</h3>
{#each enabledAlarms.slice(0, 3) as alarm (alarm.id)}
<div class="flex items-center justify-between py-1">
<span class="text-sm text-white/60">{alarm.label ?? 'Wecker'}</span>
<span class="font-mono text-sm text-white/80">{alarm.time}</span>
</div>
{/each}
</div>
{/if}
</div>

View file

@ -1,43 +0,0 @@
/**
* Clock module collection accessors and guest seed data.
*/
import { db } from '$lib/data/database';
import type { LocalAlarm, LocalTimer, LocalWorldClock } from './types';
// ─── Collection Accessors ──────────────────────────────────
export const alarmTable = db.table<LocalAlarm>('alarms');
export const timerTable = db.table<LocalTimer>('timers');
export const worldClockTable = db.table<LocalWorldClock>('worldClocks');
// ─── Guest Seed ────────────────────────────────────────────
export const CLOCK_GUEST_SEED = {
alarms: [
{
id: 'alarm-weekday-morning',
label: 'Wecker Wochentags',
time: '07:00',
enabled: true,
repeatDays: [1, 2, 3, 4, 5], // Mon-Fri
snoozeMinutes: 5,
sound: null,
vibrate: true,
},
] satisfies LocalAlarm[],
worldClocks: [
{
id: 'wc-new-york',
timezone: 'America/New_York',
cityName: 'New York',
sortOrder: 0,
},
{
id: 'wc-tokyo',
timezone: 'Asia/Tokyo',
cityName: 'Tokio',
sortOrder: 1,
},
] satisfies LocalWorldClock[],
};

View file

@ -1,26 +0,0 @@
/**
* Clock module barrel exports.
*/
export { alarmsStore } from './stores/alarms.svelte';
export { timersStore } from './stores/timers.svelte';
export { worldClocksStore } from './stores/world-clocks.svelte';
export { stopwatchesStore, formatTime, formatLapTime } from './stores/stopwatch.svelte';
export { sessionAlarmsStore } from './stores/session-alarms.svelte';
export { sessionTimersStore } from './stores/session-timers.svelte';
export {
useAllAlarms,
useAllTimers,
useAllWorldClocks,
allAlarms$,
allTimers$,
allWorldClocks$,
toAlarm,
toTimer,
toWorldClock,
filterEnabledAlarms,
filterActiveTimers,
sortWorldClocksByOrder,
} from './queries';
export { alarmTable, timerTable, worldClockTable, CLOCK_GUEST_SEED } from './collections';
export type { LocalAlarm, LocalTimer, LocalWorldClock } from './types';

View file

@ -1,124 +0,0 @@
/**
* Reactive Queries & Pure Helpers for Clock module.
*
* Uses Dexie liveQuery to automatically re-render when IndexedDB changes
* (local writes, sync updates, other tabs). Components call these hooks
* at init time; no manual fetch/refresh needed.
*/
import { liveQuery } from 'dexie';
import { useLiveQueryWithDefault } from '@manacore/local-store/svelte';
import { db } from '$lib/data/database';
import type { LocalAlarm, LocalTimer, LocalWorldClock } from './types';
import type { Alarm, Timer, WorldClock } from '@clock/shared';
// ─── Type Converters ───────────────────────────────────────
export function toAlarm(local: LocalAlarm): Alarm {
return {
id: local.id,
userId: 'local',
label: local.label,
time: local.time,
enabled: local.enabled,
repeatDays: local.repeatDays,
snoozeMinutes: local.snoozeMinutes,
sound: local.sound,
vibrate: local.vibrate ?? null,
createdAt: local.createdAt ?? new Date().toISOString(),
updatedAt: local.updatedAt ?? new Date().toISOString(),
};
}
export function toTimer(local: LocalTimer): Timer {
return {
id: local.id,
userId: 'local',
label: local.label,
durationSeconds: local.durationSeconds,
remainingSeconds: local.remainingSeconds,
status: local.status,
startedAt: local.startedAt,
pausedAt: local.pausedAt,
sound: local.sound,
createdAt: local.createdAt ?? new Date().toISOString(),
updatedAt: local.updatedAt ?? new Date().toISOString(),
};
}
export function toWorldClock(local: LocalWorldClock): WorldClock {
return {
id: local.id,
userId: 'local',
timezone: local.timezone,
cityName: local.cityName,
sortOrder: local.sortOrder,
createdAt: local.createdAt ?? new Date().toISOString(),
};
}
// ─── Raw Observable Queries (for Svelte $ auto-subscribe) ──
/** All alarms as raw observable. */
export function allAlarms$() {
return liveQuery(async () => {
const locals = await db.table<LocalAlarm>('alarms').toArray();
return locals.filter((a) => !a.deletedAt).map(toAlarm);
});
}
/** All timers as raw observable. */
export function allTimers$() {
return liveQuery(async () => {
const locals = await db.table<LocalTimer>('timers').toArray();
return locals.filter((t) => !t.deletedAt).map(toTimer);
});
}
/** All world clocks as raw observable, sorted by sortOrder. */
export function allWorldClocks$() {
return liveQuery(async () => {
const locals = await db.table<LocalWorldClock>('worldClocks').orderBy('sortOrder').toArray();
return locals.filter((wc) => !wc.deletedAt).map(toWorldClock);
});
}
// ─── Svelte 5 Reactive Hooks (call during component init) ──
/** All alarms, auto-updates on any change. Returns { value, loading, error }. */
export function useAllAlarms() {
return useLiveQueryWithDefault(async () => {
const locals = await db.table<LocalAlarm>('alarms').toArray();
return locals.filter((a) => !a.deletedAt).map(toAlarm);
}, [] as Alarm[]);
}
/** All timers, auto-updates on any change. Returns { value, loading, error }. */
export function useAllTimers() {
return useLiveQueryWithDefault(async () => {
const locals = await db.table<LocalTimer>('timers').toArray();
return locals.filter((t) => !t.deletedAt).map(toTimer);
}, [] as Timer[]);
}
/** All world clocks, sorted by sortOrder. Returns { value, loading, error }. */
export function useAllWorldClocks() {
return useLiveQueryWithDefault(async () => {
const locals = await db.table<LocalWorldClock>('worldClocks').orderBy('sortOrder').toArray();
return locals.filter((wc) => !wc.deletedAt).map(toWorldClock);
}, [] as WorldClock[]);
}
// ─── Pure Filter Functions (for $derived) ──────────────────
export function filterEnabledAlarms(alarms: Alarm[]): Alarm[] {
return alarms.filter((a) => a.enabled);
}
export function filterActiveTimers(timers: Timer[]): Timer[] {
return timers.filter((t) => t.status === 'running' || t.status === 'paused');
}
export function sortWorldClocksByOrder(clocks: WorldClock[]): WorldClock[] {
return [...clocks].sort((a, b) => a.sortOrder - b.sortOrder);
}

View file

@ -1,31 +0,0 @@
/**
* Clock module types for the unified ManaCore app.
*/
import type { BaseRecord } from '@manacore/local-store';
export interface LocalAlarm extends BaseRecord {
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;
}
export interface LocalTimer extends BaseRecord {
label: string | null;
durationSeconds: number;
remainingSeconds: number | null;
status: 'idle' | 'running' | 'paused' | 'finished';
startedAt: string | null;
pausedAt: string | null;
sound: string | null;
}
export interface LocalWorldClock extends BaseRecord {
timezone: string; // IANA timezone e.g. 'America/New_York'
cityName: string;
sortOrder: number;
}

View file

@ -13,6 +13,9 @@ import type {
LocalTag,
LocalTemplate,
LocalSettings,
LocalAlarm,
LocalCountdownTimer,
LocalWorldClock,
} from './types';
// ─── Collection Accessors ──────────────────────────────────
@ -24,6 +27,11 @@ export const tagTable = db.table<LocalTag>('timeTags');
export const templateTable = db.table<LocalTemplate>('timeTemplates');
export const settingsTable = db.table<LocalSettings>('timeSettings');
// Clock collections (merged from clock module)
export const alarmTable = db.table<LocalAlarm>('timeAlarms');
export const countdownTimerTable = db.table<LocalCountdownTimer>('timeCountdownTimers');
export const worldClockTable = db.table<LocalWorldClock>('timeWorldClocks');
// ─── Guest Seed ────────────────────────────────────────────
const DEMO_CLIENT_ID = 'demo-client-acme';
@ -192,4 +200,31 @@ export const TIMES_GUEST_SEED = {
autoStopTimerHours: 0,
},
],
// Clock guest seed (merged from clock module)
timeAlarms: [
{
id: 'alarm-weekday-morning',
label: 'Wecker Wochentags',
time: '07:00',
enabled: true,
repeatDays: [1, 2, 3, 4, 5], // Mon-Fri
snoozeMinutes: 5,
sound: null,
vibrate: true,
},
] satisfies LocalAlarm[],
timeWorldClocks: [
{
id: 'wc-new-york',
timezone: 'America/New_York',
cityName: 'New York',
sortOrder: 0,
},
{
id: 'wc-tokyo',
timezone: 'Asia/Tokyo',
cityName: 'Tokio',
sortOrder: 1,
},
] satisfies LocalWorldClock[],
};

View file

@ -4,7 +4,7 @@
*/
import { onMount } from 'svelte';
import { browser } from '$app/environment';
import { POPULAR_TIMEZONES } from '@clock/shared';
import { POPULAR_TIMEZONES } from '../../types';
interface Props {
selectedTimezones?: string[];

View file

@ -2,8 +2,19 @@
* Times module barrel exports.
*/
// ─── Times Stores ─────────────────────────────────────────
export { timerStore } from './stores/timer.svelte';
export { viewStore } from './stores/view.svelte';
// ─── Clock Stores (merged from clock module) ──────────────
export { alarmsStore } from './stores/alarms.svelte';
export { countdownTimersStore } from './stores/countdown-timers.svelte';
export { worldClocksStore } from './stores/world-clocks.svelte';
export { stopwatchesStore, formatTime, formatLapTime } from './stores/stopwatch.svelte';
export { sessionAlarmsStore } from './stores/session-alarms.svelte';
export { sessionCountdownTimersStore } from './stores/session-countdown-timers.svelte';
// ─── Times Queries ────────────────────────────────────────
export {
useAllClients,
useAllProjects,
@ -34,6 +45,24 @@ export {
getClientById,
getProjectsByClient,
} from './queries';
// ─── Clock Queries (merged from clock module) ─────────────
export {
useAllAlarms,
useAllCountdownTimers,
useAllWorldClocks,
allAlarms$,
allCountdownTimers$,
allWorldClocks$,
toAlarm,
toCountdownTimer,
toWorldClock,
filterEnabledAlarms,
filterActiveCountdownTimers,
sortWorldClocksByOrder,
} from './queries';
// ─── Times Collections ───────────────────────────────────
export {
clientTable,
projectTable,
@ -43,8 +72,15 @@ export {
settingsTable,
TIMES_GUEST_SEED,
} from './collections';
// ─── Clock Collections (merged from clock module) ─────────
export { alarmTable, countdownTimerTable, worldClockTable } from './collections';
// ─── Utils ────────────────────────────────────────────────
export { roundDuration } from './utils/rounding';
export { exportEntriesToCSV } from './utils/export';
// ─── Times Types ──────────────────────────────────────────
export { PROJECT_COLORS } from './types';
export type {
LocalClient,
@ -68,3 +104,6 @@ export type {
ViewMode,
RoundingMethod,
} from './types';
// ─── Clock Types (merged from clock module) ───────────────
export type { LocalAlarm, LocalCountdownTimer, LocalWorldClock } from './types';

View file

@ -6,6 +6,7 @@
import { liveQuery } from 'dexie';
import { db } from '$lib/data/database';
import { useLiveQueryWithDefault } from '@manacore/local-store/svelte';
import type {
LocalClient,
LocalProject,
@ -13,6 +14,9 @@ import type {
LocalTag,
LocalTemplate,
LocalSettings,
LocalAlarm,
LocalCountdownTimer,
LocalWorldClock,
Client,
Project,
TimeEntry,
@ -22,6 +26,7 @@ import type {
FilterCriteria,
SortOption,
} from './types';
import type { Alarm, Timer, WorldClock } from './types';
// ─── Type Converters ───────────────────────────────────────
@ -127,6 +132,51 @@ export function toSettings(local: LocalSettings): TimesSettings {
};
}
// ─── Clock Type Converters (merged from clock module) ─────
export function toAlarm(local: LocalAlarm): Alarm {
return {
id: local.id,
userId: 'local',
label: local.label,
time: local.time,
enabled: local.enabled,
repeatDays: local.repeatDays,
snoozeMinutes: local.snoozeMinutes,
sound: local.sound,
vibrate: local.vibrate ?? null,
createdAt: local.createdAt ?? new Date().toISOString(),
updatedAt: local.updatedAt ?? new Date().toISOString(),
};
}
export function toCountdownTimer(local: LocalCountdownTimer): Timer {
return {
id: local.id,
userId: 'local',
label: local.label,
durationSeconds: local.durationSeconds,
remainingSeconds: local.remainingSeconds,
status: local.status,
startedAt: local.startedAt,
pausedAt: local.pausedAt,
sound: local.sound,
createdAt: local.createdAt ?? new Date().toISOString(),
updatedAt: local.updatedAt ?? new Date().toISOString(),
};
}
export function toWorldClock(local: LocalWorldClock): WorldClock {
return {
id: local.id,
userId: 'local',
timezone: local.timezone,
cityName: local.cityName,
sortOrder: local.sortOrder,
createdAt: local.createdAt ?? new Date().toISOString(),
};
}
// ─── Live Queries ──────────────────────────────────────────
export function useAllClients() {
@ -172,6 +222,78 @@ export function useSettings() {
});
}
// ─── Clock Raw Observable Queries ─────────────────────────
/** All alarms as raw observable. */
export function allAlarms$() {
return liveQuery(async () => {
const locals = await db.table<LocalAlarm>('timeAlarms').toArray();
return locals.filter((a) => !a.deletedAt).map(toAlarm);
});
}
/** All countdown timers as raw observable. */
export function allCountdownTimers$() {
return liveQuery(async () => {
const locals = await db.table<LocalCountdownTimer>('timeCountdownTimers').toArray();
return locals.filter((t) => !t.deletedAt).map(toCountdownTimer);
});
}
/** All world clocks as raw observable, sorted by sortOrder. */
export function allWorldClocks$() {
return liveQuery(async () => {
const locals = await db
.table<LocalWorldClock>('timeWorldClocks')
.orderBy('sortOrder')
.toArray();
return locals.filter((wc) => !wc.deletedAt).map(toWorldClock);
});
}
// ─── Clock Svelte 5 Reactive Hooks ────────────────────────
/** All alarms, auto-updates on any change. Returns { value, loading, error }. */
export function useAllAlarms() {
return useLiveQueryWithDefault(async () => {
const locals = await db.table<LocalAlarm>('timeAlarms').toArray();
return locals.filter((a) => !a.deletedAt).map(toAlarm);
}, [] as Alarm[]);
}
/** All countdown timers, auto-updates on any change. Returns { value, loading, error }. */
export function useAllCountdownTimers() {
return useLiveQueryWithDefault(async () => {
const locals = await db.table<LocalCountdownTimer>('timeCountdownTimers').toArray();
return locals.filter((t) => !t.deletedAt).map(toCountdownTimer);
}, [] as Timer[]);
}
/** All world clocks, sorted by sortOrder. Returns { value, loading, error }. */
export function useAllWorldClocks() {
return useLiveQueryWithDefault(async () => {
const locals = await db
.table<LocalWorldClock>('timeWorldClocks')
.orderBy('sortOrder')
.toArray();
return locals.filter((wc) => !wc.deletedAt).map(toWorldClock);
}, [] as WorldClock[]);
}
// ─── Clock Pure Filter Functions ──────────────────────────
export function filterEnabledAlarms(alarms: Alarm[]): Alarm[] {
return alarms.filter((a) => a.enabled);
}
export function filterActiveCountdownTimers(timers: Timer[]): Timer[] {
return timers.filter((t) => t.status === 'running' || t.status === 'paused');
}
export function sortWorldClocksByOrder(clocks: WorldClock[]): WorldClock[] {
return [...clocks].sort((a, b) => a.sortOrder - b.sortOrder);
}
// ─── Pure Helpers ──────────────────────────────────────────
/** Format duration in seconds to HH:MM:SS */

View file

@ -1,5 +1,5 @@
/**
* Alarms Store Mutation-Only Service
* Alarms Store Mutation-Only Service (merged from clock module)
*
* All reads are handled by liveQuery hooks in queries.ts.
* This store only provides write operations (create, update, delete, toggle).
@ -9,7 +9,7 @@
import { db } from '$lib/data/database';
import type { LocalAlarm } from '../types';
import { toAlarm } from '../queries';
import type { CreateAlarmInput, UpdateAlarmInput, Alarm } from '@clock/shared';
import type { CreateAlarmInput, UpdateAlarmInput, Alarm } from '../types';
let error = $state<string | null>(null);
@ -37,7 +37,7 @@ export const alarmsStore = {
updatedAt: new Date().toISOString(),
};
await db.table<LocalAlarm>('alarms').add(newLocal);
await db.table<LocalAlarm>('timeAlarms').add(newLocal);
return { success: true, data: toAlarm(newLocal) };
} catch (e) {
error = e instanceof Error ? e.message : 'Failed to create alarm';
@ -63,8 +63,8 @@ export const alarmsStore = {
if (input.sound !== undefined) updateData.sound = input.sound ?? null;
if (input.vibrate !== undefined) updateData.vibrate = input.vibrate ?? null;
await db.table('alarms').update(id, updateData);
const updated = await db.table<LocalAlarm>('alarms').get(id);
await db.table('timeAlarms').update(id, updateData);
const updated = await db.table<LocalAlarm>('timeAlarms').get(id);
if (updated) {
return { success: true, data: toAlarm(updated) };
}
@ -92,7 +92,7 @@ export const alarmsStore = {
async deleteAlarm(id: string) {
error = null;
try {
await db.table('alarms').update(id, {
await db.table('timeAlarms').update(id, {
deletedAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
});

View file

@ -1,5 +1,5 @@
/**
* Timers Store Mutation-Only Service
* Countdown Timers Store Mutation-Only Service (merged from clock module)
*
* All reads are handled by liveQuery hooks in queries.ts.
* This store only provides write operations (create, update, delete, start, pause, reset).
@ -7,25 +7,25 @@
*/
import { db } from '$lib/data/database';
import type { LocalTimer } from '../types';
import { toTimer } from '../queries';
import type { CreateTimerInput, UpdateTimerInput } from '@clock/shared';
import type { LocalCountdownTimer } from '../types';
import { toCountdownTimer } from '../queries';
import type { CreateTimerInput, UpdateTimerInput } from '../types';
import { ClockEvents } from '@manacore/shared-utils/analytics';
let error = $state<string | null>(null);
export const timersStore = {
export const countdownTimersStore = {
get error() {
return error;
},
/**
* Create a new timer -- writes to IndexedDB instantly.
* Create a new countdown timer -- writes to IndexedDB instantly.
*/
async createTimer(input: CreateTimerInput) {
error = null;
try {
const newLocal: LocalTimer = {
const newLocal: LocalCountdownTimer = {
id: crypto.randomUUID(),
label: input.label ?? null,
durationSeconds: input.durationSeconds,
@ -38,8 +38,8 @@ export const timersStore = {
updatedAt: new Date().toISOString(),
};
await db.table<LocalTimer>('timers').add(newLocal);
return { success: true, data: toTimer(newLocal) };
await db.table<LocalCountdownTimer>('timeCountdownTimers').add(newLocal);
return { success: true, data: toCountdownTimer(newLocal) };
} catch (e) {
error = e instanceof Error ? e.message : 'Failed to create timer';
console.error('Failed to create timer:', e);
@ -48,22 +48,22 @@ export const timersStore = {
},
/**
* Update a timer -- writes to IndexedDB instantly.
* Update a countdown timer -- writes to IndexedDB instantly.
*/
async updateTimer(id: string, input: UpdateTimerInput) {
error = null;
try {
const updateData: Partial<LocalTimer> = {
const updateData: Partial<LocalCountdownTimer> = {
updatedAt: new Date().toISOString(),
};
if (input.label !== undefined) updateData.label = input.label ?? null;
if (input.durationSeconds !== undefined) updateData.durationSeconds = input.durationSeconds;
if (input.sound !== undefined) updateData.sound = input.sound ?? null;
await db.table('timers').update(id, updateData);
const updated = await db.table<LocalTimer>('timers').get(id);
await db.table('timeCountdownTimers').update(id, updateData);
const updated = await db.table<LocalCountdownTimer>('timeCountdownTimers').get(id);
if (updated) {
return { success: true, data: toTimer(updated) };
return { success: true, data: toCountdownTimer(updated) };
}
return { success: false, error: 'Timer not found' };
} catch (e) {
@ -74,15 +74,15 @@ export const timersStore = {
},
/**
* Start a timer -- sets status to running with current timestamp.
* Start a countdown timer -- sets status to running with current timestamp.
*/
async startTimer(id: string) {
error = null;
try {
const existing = await db.table<LocalTimer>('timers').get(id);
const existing = await db.table<LocalCountdownTimer>('timeCountdownTimers').get(id);
if (!existing) return { success: false, error: 'Timer not found' };
const updateData: Partial<LocalTimer> = {
const updateData: Partial<LocalCountdownTimer> = {
status: 'running',
startedAt: new Date().toISOString(),
pausedAt: null,
@ -94,10 +94,10 @@ export const timersStore = {
updateData.remainingSeconds = existing.durationSeconds;
}
await db.table('timers').update(id, updateData);
const updated = await db.table<LocalTimer>('timers').get(id);
await db.table('timeCountdownTimers').update(id, updateData);
const updated = await db.table<LocalCountdownTimer>('timeCountdownTimers').get(id);
if (updated) {
const updatedTimer = toTimer(updated);
const updatedTimer = toCountdownTimer(updated);
ClockEvents.timerStarted(
(updatedTimer as any).type as 'pomodoro' | 'stopwatch' | 'countdown'
);
@ -112,12 +112,12 @@ export const timersStore = {
},
/**
* Pause a timer -- calculates remaining seconds and saves.
* Pause a countdown timer -- calculates remaining seconds and saves.
*/
async pauseTimer(id: string) {
error = null;
try {
const existing = await db.table<LocalTimer>('timers').get(id);
const existing = await db.table<LocalCountdownTimer>('timeCountdownTimers').get(id);
if (!existing) return { success: false, error: 'Timer not found' };
// Calculate remaining seconds
@ -127,7 +127,7 @@ export const timersStore = {
remaining = Math.max(0, remaining - elapsed);
}
const updateData: Partial<LocalTimer> = {
const updateData: Partial<LocalCountdownTimer> = {
status: 'paused',
pausedAt: new Date().toISOString(),
remainingSeconds: Math.round(remaining),
@ -135,10 +135,10 @@ export const timersStore = {
updatedAt: new Date().toISOString(),
};
await db.table('timers').update(id, updateData);
const updated = await db.table<LocalTimer>('timers').get(id);
await db.table('timeCountdownTimers').update(id, updateData);
const updated = await db.table<LocalCountdownTimer>('timeCountdownTimers').get(id);
if (updated) {
return { success: true, data: toTimer(updated) };
return { success: true, data: toCountdownTimer(updated) };
}
return { success: false, error: 'Timer not found' };
} catch (e) {
@ -149,12 +149,12 @@ export const timersStore = {
},
/**
* Reset a timer -- back to idle with full duration.
* Reset a countdown timer -- back to idle with full duration.
*/
async resetTimer(id: string) {
error = null;
try {
const updateData: Partial<LocalTimer> = {
const updateData: Partial<LocalCountdownTimer> = {
status: 'idle',
remainingSeconds: null,
startedAt: null,
@ -162,10 +162,10 @@ export const timersStore = {
updatedAt: new Date().toISOString(),
};
await db.table('timers').update(id, updateData);
const updated = await db.table<LocalTimer>('timers').get(id);
await db.table('timeCountdownTimers').update(id, updateData);
const updated = await db.table<LocalCountdownTimer>('timeCountdownTimers').get(id);
if (updated) {
return { success: true, data: toTimer(updated) };
return { success: true, data: toCountdownTimer(updated) };
}
return { success: false, error: 'Timer not found' };
} catch (e) {
@ -176,12 +176,12 @@ export const timersStore = {
},
/**
* Delete a timer -- soft-deletes from IndexedDB instantly.
* Delete a countdown timer -- soft-deletes from IndexedDB instantly.
*/
async deleteTimer(id: string) {
error = null;
try {
await db.table('timers').update(id, {
await db.table('timeCountdownTimers').update(id, {
deletedAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
});
@ -198,7 +198,7 @@ export const timersStore = {
*/
async updateLocalTimer(id: string, remainingSeconds: number) {
try {
await db.table('timers').update(id, { remainingSeconds });
await db.table('timeCountdownTimers').update(id, { remainingSeconds });
} catch (e) {
console.error('Failed to update local timer:', e);
}

View file

@ -1,10 +1,10 @@
/**
* Session Alarms Store - Manages alarms in sessionStorage for guest users
* Session Alarms Store - Manages alarms in sessionStorage for guest users (merged from clock module)
* This allows users to try the app without signing in.
* Data is stored in sessionStorage (lost when tab closes).
*/
import type { Alarm, CreateAlarmInput, UpdateAlarmInput } from '@clock/shared';
import type { Alarm, CreateAlarmInput, UpdateAlarmInput } from '../types';
const STORAGE_KEY = 'clock-session-alarms';

View file

@ -1,10 +1,11 @@
/**
* Session Timers Store - Manages timers in sessionStorage for guest users
* Session Countdown Timers Store - Manages countdown timers in sessionStorage for guest users
* (merged from clock module)
* This allows users to try the app without signing in.
* Data is stored in sessionStorage (lost when tab closes).
*/
import type { Timer, CreateTimerInput, UpdateTimerInput, TimerStatus } from '@clock/shared';
import type { Timer, CreateTimerInput, UpdateTimerInput, TimerStatus } from '../types';
const STORAGE_KEY = 'clock-session-timers';
@ -46,7 +47,7 @@ if (typeof window !== 'undefined') {
loadFromStorage();
}
export const sessionTimersStore = {
export const sessionCountdownTimersStore = {
// Getters
get timers() {
return timers;
@ -56,7 +57,7 @@ export const sessionTimersStore = {
},
/**
* Create a new session timer
* Create a new session countdown timer
*/
createTimer(input: CreateTimerInput): Timer {
const now = new Date().toISOString();
@ -81,7 +82,7 @@ export const sessionTimersStore = {
},
/**
* Update a session timer
* Update a session countdown timer
*/
updateTimer(id: string, input: UpdateTimerInput): Timer | null {
const index = timers.findIndex((t) => t.id === id);
@ -100,7 +101,7 @@ export const sessionTimersStore = {
},
/**
* Start a timer
* Start a countdown timer
*/
startTimer(id: string): Timer | null {
const timer = timers.find((t) => t.id === id);
@ -122,7 +123,7 @@ export const sessionTimersStore = {
},
/**
* Pause a timer
* Pause a countdown timer
*/
pauseTimer(id: string): Timer | null {
const timer = timers.find((t) => t.id === id);
@ -143,7 +144,7 @@ export const sessionTimersStore = {
},
/**
* Reset a timer
* Reset a countdown timer
*/
resetTimer(id: string): Timer | null {
const timer = timers.find((t) => t.id === id);
@ -174,7 +175,7 @@ export const sessionTimersStore = {
},
/**
* Delete a session timer
* Delete a session countdown timer
*/
deleteTimer(id: string): void {
timers = timers.filter((t) => t.id !== id);
@ -206,7 +207,7 @@ export const sessionTimersStore = {
},
/**
* Get count of session timers
* Get count of session countdown timers
*/
get count(): number {
return timers.length;

View file

@ -1,5 +1,5 @@
/**
* Stopwatch Store - Manages stopwatch state using Svelte 5 runes
* Stopwatch Store - Manages stopwatch state using Svelte 5 runes (merged from clock module)
* Stopwatches are local-only (no backend sync)
*/

View file

@ -1,5 +1,5 @@
/**
* World Clocks Store Mutation-Only Service
* World Clocks Store Mutation-Only Service (merged from clock module)
*
* All reads are handled by liveQuery hooks in queries.ts.
* This store only provides write operations (add, remove, reorder).
@ -8,7 +8,7 @@
import { db } from '$lib/data/database';
import type { LocalWorldClock } from '../types';
import type { CreateWorldClockInput } from '@clock/shared';
import type { CreateWorldClockInput } from '../types';
let error = $state<string | null>(null);
@ -32,7 +32,7 @@ export const worldClocksStore = {
updatedAt: new Date().toISOString(),
};
await db.table<LocalWorldClock>('worldClocks').add(newLocal);
await db.table<LocalWorldClock>('timeWorldClocks').add(newLocal);
return { success: true };
} catch (e) {
error = e instanceof Error ? e.message : 'Failed to add world clock';
@ -47,7 +47,7 @@ export const worldClocksStore = {
async removeWorldClock(id: string) {
error = null;
try {
await db.table('worldClocks').update(id, {
await db.table('timeWorldClocks').update(id, {
deletedAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
});
@ -67,7 +67,7 @@ export const worldClocksStore = {
try {
const now = new Date().toISOString();
for (let i = 0; i < ids.length; i++) {
await db.table('worldClocks').update(ids[i], {
await db.table('timeWorldClocks').update(ids[i], {
sortOrder: i,
updatedAt: now,
});

View file

@ -224,7 +224,224 @@ export interface LocalSettings extends BaseRecord {
autoStopTimerHours: number;
}
// ─── Constants ────────────────────────────────────────────
// ─── Clock Types (merged from clock module) ─────────────
export interface LocalAlarm extends BaseRecord {
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;
}
export interface LocalCountdownTimer extends BaseRecord {
label: string | null;
durationSeconds: number;
remainingSeconds: number | null;
status: 'idle' | 'running' | 'paused' | 'finished';
startedAt: string | null;
pausedAt: string | null;
sound: string | null;
}
export interface LocalWorldClock extends BaseRecord {
timezone: string; // IANA timezone e.g. 'America/New_York'
cityName: string;
sortOrder: number;
}
// ─── API-Level Clock Types (inlined from archived @clock/shared) ───
export type TimerStatus = 'idle' | 'running' | 'paused' | 'finished';
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 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 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;
}
// ─── Clock Constants (inlined from archived @clock/shared) ───
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;
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;
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;
// ─── Times Constants ─────────────────────────────────────────
export const PROJECT_COLORS: string[] = [
'#ef4444',

View file

@ -3,21 +3,21 @@ import { getManaApp } from '@manacore/shared-branding';
import { scoreRecord } from '../scoring';
import type { SearchProvider, SearchResult, SearchOptions } from '../types';
const app = getManaApp('clock');
const app = getManaApp('times');
export const clockSearchProvider: SearchProvider = {
appId: 'clock',
appName: 'Clock',
appId: 'times',
appName: 'Times',
appIcon: app?.icon,
appColor: app?.color,
searchableTypes: ['alarm', 'timer', 'worldClock'],
searchableTypes: ['alarm', 'countdownTimer', 'worldClock'],
async search(query: string, options?: SearchOptions): Promise<SearchResult[]> {
const limit = options?.limit ?? 5;
const results: SearchResult[] = [];
// Search alarms by label
const alarms = await db.table('alarms').toArray();
const alarms = await db.table('timeAlarms').toArray();
for (const alarm of alarms) {
if (alarm.deletedAt || !alarm.label) continue;
const { score, matchedField } = scoreRecord(
@ -28,20 +28,20 @@ export const clockSearchProvider: SearchProvider = {
results.push({
id: alarm.id,
type: 'alarm',
appId: 'clock',
appId: 'times',
title: alarm.label,
subtitle: 'Alarm',
appIcon: app?.icon,
appColor: app?.color,
href: '/clock',
href: '/times/clock/alarms',
score,
matchedField,
});
}
}
// Search timers by label
const timers = await db.table('timers').toArray();
// Search countdown timers by label
const timers = await db.table('timeCountdownTimers').toArray();
for (const timer of timers) {
if (timer.deletedAt || !timer.label) continue;
const { score, matchedField } = scoreRecord(
@ -51,13 +51,13 @@ export const clockSearchProvider: SearchProvider = {
if (score > 0) {
results.push({
id: timer.id,
type: 'timer',
appId: 'clock',
type: 'countdownTimer',
appId: 'times',
title: timer.label,
subtitle: 'Timer',
appIcon: app?.icon,
appColor: app?.color,
href: '/clock',
href: '/times/clock/timers',
score,
matchedField,
});
@ -65,7 +65,7 @@ export const clockSearchProvider: SearchProvider = {
}
// Search world clocks by city name
const worldClocks = await db.table('worldClocks').toArray();
const worldClocks = await db.table('timeWorldClocks').toArray();
for (const wc of worldClocks) {
if (wc.deletedAt) continue;
const { score, matchedField } = scoreRecord(
@ -76,12 +76,12 @@ export const clockSearchProvider: SearchProvider = {
results.push({
id: wc.id,
type: 'worldClock',
appId: 'clock',
appId: 'times',
title: wc.cityName,
subtitle: 'Weltzeit',
appIcon: app?.icon,
appColor: app?.color,
href: '/clock',
href: '/times/clock/world-clock',
score,
matchedField,
});

View file

@ -13,7 +13,6 @@ const APP_COMPONENTS = {
picture: () => import('$lib/modules/picture/AppView.svelte'),
cards: () => import('$lib/modules/cards/AppView.svelte'),
zitare: () => import('$lib/modules/zitare/AppView.svelte'),
clock: () => import('$lib/modules/clock/AppView.svelte'),
mukke: () => import('$lib/modules/mukke/AppView.svelte'),
storage: () => import('$lib/modules/storage/AppView.svelte'),
presi: () => import('$lib/modules/presi/AppView.svelte'),
@ -46,7 +45,6 @@ export const SPLIT_APP_LABELS: Record<SplitAppId, string> = {
picture: 'Picture',
cards: 'Cards',
zitare: 'Zitare',
clock: 'Uhr',
mukke: 'Mukke',
storage: 'Storage',
presi: 'Presi',
@ -54,7 +52,7 @@ export const SPLIT_APP_LABELS: Record<SplitAppId, string> = {
photos: 'Fotos',
skilltree: 'SkillTree',
citycorners: 'CityCorners',
times: 'Times',
times: 'Times & Clock',
context: 'Context',
questions: 'Questions',
nutriphi: 'NutriPhi',

Some files were not shown because too many files have changed in this diff Show more