diff --git a/apps-archived/clock/apps/landing/.gitignore b/apps-archived/clock/apps/landing/.gitignore
new file mode 100644
index 000000000..163286eca
--- /dev/null
+++ b/apps-archived/clock/apps/landing/.gitignore
@@ -0,0 +1,27 @@
+# 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
diff --git a/apps-archived/clock/apps/landing/.prettierrc b/apps-archived/clock/apps/landing/.prettierrc
new file mode 100644
index 000000000..09cfb7d9d
--- /dev/null
+++ b/apps-archived/clock/apps/landing/.prettierrc
@@ -0,0 +1,15 @@
+{
+ "useTabs": true,
+ "singleQuote": true,
+ "trailingComma": "none",
+ "printWidth": 100,
+ "plugins": ["prettier-plugin-astro", "prettier-plugin-tailwindcss"],
+ "overrides": [
+ {
+ "files": "*.astro",
+ "options": {
+ "parser": "astro"
+ }
+ }
+ ]
+}
diff --git a/apps-archived/clock/apps/landing/astro.config.mjs b/apps-archived/clock/apps/landing/astro.config.mjs
new file mode 100644
index 000000000..83c9c822a
--- /dev/null
+++ b/apps-archived/clock/apps/landing/astro.config.mjs
@@ -0,0 +1,19 @@
+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',
+ },
+ },
+ },
+});
diff --git a/apps-archived/clock/apps/landing/package.json b/apps-archived/clock/apps/landing/package.json
new file mode 100644
index 000000000..623930a53
--- /dev/null
+++ b/apps-archived/clock/apps/landing/package.json
@@ -0,0 +1,34 @@
+{
+ "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"
+ }
+}
diff --git a/apps-archived/clock/apps/landing/public/favicon.svg b/apps-archived/clock/apps/landing/public/favicon.svg
new file mode 100644
index 000000000..c5fd432f3
--- /dev/null
+++ b/apps-archived/clock/apps/landing/public/favicon.svg
@@ -0,0 +1,3 @@
+
+
+
diff --git a/apps-archived/clock/apps/landing/src/components/CTA.astro b/apps-archived/clock/apps/landing/src/components/CTA.astro
new file mode 100644
index 000000000..2cdc8a166
--- /dev/null
+++ b/apps-archived/clock/apps/landing/src/components/CTA.astro
@@ -0,0 +1,60 @@
+---
+// Call to Action section
+---
+
+
+
+
+
+
+
+
+
+ Bereit, produktiver zu werden?
+
+
+ Starte kostenlos und erlebe, wie einfach Zeitmanagement sein kann. Keine Kreditkarte
+ erforderlich.
+
+
+
+
+
+
+
+
+
+
+
Kostenlos starten
+
+
+
+
+
+
Keine Kreditkarte
+
+
+
+
+
+
Jederzeit kundbar
+
+
+
+
+
diff --git a/apps-archived/clock/apps/landing/src/components/Features.astro b/apps-archived/clock/apps/landing/src/components/Features.astro
new file mode 100644
index 000000000..77e7cf355
--- /dev/null
+++ b/apps-archived/clock/apps/landing/src/components/Features.astro
@@ -0,0 +1,82 @@
+---
+// Features section for Clock landing page
+
+const features = [
+ {
+ icon: `
+
+ `,
+ title: 'Pomodoro Timer',
+ description:
+ 'Arbeite in fokussierten 25-Minuten-Intervallen mit automatischen Pausen. Steigere deine Konzentration.',
+ },
+ {
+ icon: `
+
+ `,
+ title: 'Zeiterfassung',
+ description:
+ 'Tracke deine Arbeitszeit auf Projekte und Aufgaben. Detaillierte Berichte und Statistiken.',
+ },
+ {
+ icon: `
+
+ `,
+ title: 'Focus Sessions',
+ description:
+ 'Starte dedizierte Focus-Sessions und eliminiere Ablenkungen. Perfekt fur Deep Work.',
+ },
+ {
+ icon: `
+
+ `,
+ title: 'Erinnerungen',
+ description: 'Sanfte Erinnerungen fur Pausen und Arbeitsende. Schutze deine Work-Life-Balance.',
+ },
+ {
+ icon: `
+
+ `,
+ title: 'Detaillierte Reports',
+ description:
+ 'Wochentliche und monatliche Ubersichten. Exportiere deine Daten als CSV oder PDF.',
+ },
+ {
+ icon: `
+
+ `,
+ title: 'Alle Gerate',
+ description: 'Web-App, iOS und Android. Deine Zeit wird uber alle Gerate synchronisiert.',
+ },
+];
+---
+
+
+
+
+
+
+ Funktionen
+
+
Produktiver arbeiten
+
+ Clock bietet alle Tools, die du fur effektives Zeitmanagement brauchst.
+
+
+
+
+
+ {
+ features.map((feature) => (
+
+
+
+
+
{feature.title}
+
{feature.description}
+
+ ))
+ }
+
+
+
diff --git a/apps-archived/clock/apps/landing/src/components/Footer.astro b/apps-archived/clock/apps/landing/src/components/Footer.astro
new file mode 100644
index 000000000..603a48db8
--- /dev/null
+++ b/apps-archived/clock/apps/landing/src/components/Footer.astro
@@ -0,0 +1,108 @@
+---
+// 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' },
+ ],
+};
+---
+
+
+
+
+
+
+
+
Time Tracking & Focus fur bessere Produktivitat.
+
+
+
+
+
Produkt
+
+ {
+ links.product.map((link) => (
+
+
+ {link.name}
+
+
+ ))
+ }
+
+
+
+
+
Rechtliches
+
+ {
+ links.legal.map((link) => (
+
+
+ {link.name}
+
+
+ ))
+ }
+
+
+
+
+
Support
+
+ {
+ links.support.map((link) => (
+
+
+ {link.name}
+
+
+ ))
+ }
+
+
+
+
+
+
+
+ © {currentYear} Clock. Alle Rechte vorbehalten.
+
+
+ Ein Manacore Produkt
+
+
+
+
diff --git a/apps-archived/clock/apps/landing/src/components/Hero.astro b/apps-archived/clock/apps/landing/src/components/Hero.astro
new file mode 100644
index 000000000..6f59d48de
--- /dev/null
+++ b/apps-archived/clock/apps/landing/src/components/Hero.astro
@@ -0,0 +1,108 @@
+---
+// Hero section for Clock landing page
+---
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Time Tracking & Focus
+
+
+
+
+ Nutze deine Zeit
+ effektiv
+
+
+
+
+ Pomodoro-Timer, Zeiterfassung und Focus-Sessions - steigere deine Produktivitat und behalte
+ den Uberblick uber deine Arbeitszeit.
+
+
+
+
+
+
+
+
+ {
+ [1, 2, 3, 4, 5].map((i) => (
+
+ ))
+ }
+
+
+ 300+ Nutzer tracken ihre Zeit
+
+
+
+
+
+
+
+
+
+
+
+
+
+
25:00
+
+ Start
+ Pause
+ Reset
+
+
+ Focus
+ Short Break
+ Long Break
+
+
+
+
+
+
+
diff --git a/apps-archived/clock/apps/landing/src/layouts/Layout.astro b/apps-archived/clock/apps/landing/src/layouts/Layout.astro
new file mode 100644
index 000000000..07eb3c096
--- /dev/null
+++ b/apps-archived/clock/apps/landing/src/layouts/Layout.astro
@@ -0,0 +1,58 @@
+---
+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;
+---
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {
+ import.meta.env.PUBLIC_UMAMI_WEBSITE_ID && (
+
+ )
+ }
+
+ {title}
+
+
+
+
+
+
diff --git a/apps-archived/clock/apps/landing/src/pages/index.astro b/apps-archived/clock/apps/landing/src/pages/index.astro
new file mode 100644
index 000000000..5c7c7d38c
--- /dev/null
+++ b/apps-archived/clock/apps/landing/src/pages/index.astro
@@ -0,0 +1,158 @@
+---
+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: '#',
+ },
+ },
+];
+---
+
+
+
+
+
+
+
+
+
+ Preise
+
+
Einfache, transparente Preise
+
Starte kostenlos, upgrade wenn du mehr brauchst
+
+
+
+ {
+ pricingPlans.map((plan) => (
+
+ {plan.badge && (
+
+ {plan.badge}
+
+ )}
+
{plan.name}
+
{plan.description}
+
+ {plan.price}€
+ {plan.period}
+
+
+ {plan.features.map((feature) => (
+
+ {feature.included ? (
+
+
+
+ ) : (
+
+
+
+ )}
+ {feature.text}
+
+ ))}
+
+
+ {plan.cta.text}
+
+
+ ))
+ }
+
+
+
+
+
+
+
diff --git a/apps-archived/clock/apps/landing/src/styles/global.css b/apps-archived/clock/apps/landing/src/styles/global.css
new file mode 100644
index 000000000..a70d2c0f4
--- /dev/null
+++ b/apps-archived/clock/apps/landing/src/styles/global.css
@@ -0,0 +1,78 @@
+@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;
+}
diff --git a/apps-archived/clock/apps/landing/tailwind.config.mjs b/apps-archived/clock/apps/landing/tailwind.config.mjs
new file mode 100644
index 000000000..44e6cb8b0
--- /dev/null
+++ b/apps-archived/clock/apps/landing/tailwind.config.mjs
@@ -0,0 +1,52 @@
+/** @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')],
+};
diff --git a/apps-archived/clock/apps/landing/tsconfig.json b/apps-archived/clock/apps/landing/tsconfig.json
new file mode 100644
index 000000000..4b0f22d55
--- /dev/null
+++ b/apps-archived/clock/apps/landing/tsconfig.json
@@ -0,0 +1,10 @@
+{
+ "extends": "astro/tsconfigs/strict",
+ "compilerOptions": {
+ "baseUrl": ".",
+ "paths": {
+ "@components/*": ["src/components/*"],
+ "@layouts/*": ["src/layouts/*"]
+ }
+ }
+}
diff --git a/apps-archived/clock/apps/landing/wrangler.toml b/apps-archived/clock/apps/landing/wrangler.toml
new file mode 100644
index 000000000..411e74aa9
--- /dev/null
+++ b/apps-archived/clock/apps/landing/wrangler.toml
@@ -0,0 +1,7 @@
+# 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"
diff --git a/apps-archived/clock/apps/web/Dockerfile b/apps-archived/clock/apps/web/Dockerfile
new file mode 100644
index 000000000..98ea7cd3c
--- /dev/null
+++ b/apps-archived/clock/apps/web/Dockerfile
@@ -0,0 +1,32 @@
+# 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"]
diff --git a/apps-archived/clock/apps/web/package.json b/apps-archived/clock/apps/web/package.json
new file mode 100644
index 000000000..19a69de89
--- /dev/null
+++ b/apps-archived/clock/apps/web/package.json
@@ -0,0 +1,66 @@
+{
+ "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"
+}
diff --git a/apps-archived/clock/apps/web/src/app.css b/apps-archived/clock/apps/web/src/app.css
new file mode 100644
index 000000000..c29749613
--- /dev/null
+++ b/apps-archived/clock/apps/web/src/app.css
@@ -0,0 +1,10 @@
+@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";
diff --git a/apps-archived/clock/apps/web/src/app.d.ts b/apps-archived/clock/apps/web/src/app.d.ts
new file mode 100644
index 000000000..c269fca6f
--- /dev/null
+++ b/apps-archived/clock/apps/web/src/app.d.ts
@@ -0,0 +1,2 @@
+declare const __BUILD_HASH__: string;
+declare const __BUILD_TIME__: string;
diff --git a/apps-archived/clock/apps/web/src/app.html b/apps-archived/clock/apps/web/src/app.html
new file mode 100644
index 000000000..77a5ff52c
--- /dev/null
+++ b/apps-archived/clock/apps/web/src/app.html
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+ %sveltekit.head%
+
+
+ %sveltekit.body%
+
+
diff --git a/apps-archived/clock/apps/web/src/hooks.client.ts b/apps-archived/clock/apps/web/src/hooks.client.ts
new file mode 100644
index 000000000..8f23fa0cb
--- /dev/null
+++ b/apps-archived/clock/apps/web/src/hooks.client.ts
@@ -0,0 +1,12 @@
+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);
+};
diff --git a/apps-archived/clock/apps/web/src/hooks.server.ts b/apps-archived/clock/apps/web/src/hooks.server.ts
new file mode 100644
index 000000000..3f4c150e0
--- /dev/null
+++ b/apps-archived/clock/apps/web/src/hooks.server.ts
@@ -0,0 +1,28 @@
+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 = ``;
+ return injectUmamiAnalytics(html.replace('', `${envScript}`));
+ },
+ });
+
+ setSecurityHeaders(response, {
+ connectSrc: [PUBLIC_MANA_CORE_AUTH_URL_CLIENT, PUBLIC_BACKEND_URL_CLIENT],
+ });
+
+ return response;
+};
diff --git a/apps-archived/clock/apps/web/src/lib/api/alarms.ts b/apps-archived/clock/apps/web/src/lib/api/alarms.ts
new file mode 100644
index 000000000..a08041c97
--- /dev/null
+++ b/apps-archived/clock/apps/web/src/lib/api/alarms.ts
@@ -0,0 +1,15 @@
+/**
+ * Alarms API - Direct API calls for alarms
+ */
+
+import { api } from './client';
+import type { Alarm, CreateAlarmInput, UpdateAlarmInput } from '@clock/shared';
+
+export const alarmsApi = {
+ getAll: () => api.get('/alarms'),
+ getById: (id: string) => api.get(`/alarms/${id}`),
+ create: (input: CreateAlarmInput) => api.post('/alarms', input),
+ update: (id: string, input: UpdateAlarmInput) => api.patch(`/alarms/${id}`, input),
+ delete: (id: string) => api.delete(`/alarms/${id}`),
+ toggle: (id: string) => api.post(`/alarms/${id}/toggle`),
+};
diff --git a/apps-archived/clock/apps/web/src/lib/api/client.ts b/apps-archived/clock/apps/web/src/lib/api/client.ts
new file mode 100644
index 000000000..798c1b214
--- /dev/null
+++ b/apps-archived/clock/apps/web/src/lib/api/client.ts
@@ -0,0 +1,26 @@
+/**
+ * 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 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 };
diff --git a/apps-archived/clock/apps/web/src/lib/api/timers.ts b/apps-archived/clock/apps/web/src/lib/api/timers.ts
new file mode 100644
index 000000000..d375ce198
--- /dev/null
+++ b/apps-archived/clock/apps/web/src/lib/api/timers.ts
@@ -0,0 +1,17 @@
+/**
+ * Timers API - Direct API calls for timers
+ */
+
+import { api } from './client';
+import type { Timer, CreateTimerInput, UpdateTimerInput } from '@clock/shared';
+
+export const timersApi = {
+ getAll: () => api.get('/timers'),
+ getById: (id: string) => api.get(`/timers/${id}`),
+ create: (input: CreateTimerInput) => api.post('/timers', input),
+ update: (id: string, input: UpdateTimerInput) => api.patch(`/timers/${id}`, input),
+ delete: (id: string) => api.delete(`/timers/${id}`),
+ start: (id: string) => api.post(`/timers/${id}/start`),
+ pause: (id: string) => api.post(`/timers/${id}/pause`),
+ reset: (id: string) => api.post(`/timers/${id}/reset`),
+};
diff --git a/apps-archived/clock/apps/web/src/lib/components/AuthGateModal.svelte b/apps-archived/clock/apps/web/src/lib/components/AuthGateModal.svelte
new file mode 100644
index 000000000..6f3c17552
--- /dev/null
+++ b/apps-archived/clock/apps/web/src/lib/components/AuthGateModal.svelte
@@ -0,0 +1,226 @@
+
+
+{#if open}
+
+
+
e.stopPropagation()}>
+
+
+
+
{currentMessage.description}
+
+ {#if itemCount > 0}
+
+
+
+
+
+
+ Du hast {itemCount}
+ {itemCount === 1 ? 'Element' : 'Elemente'} in deiner Session. Diese werden nach der Anmeldung
+ in deinen Account übertragen.
+
+ {/if}
+
+
+
+ Später
+ Anmelden
+ Registrieren
+
+
+
+{/if}
+
+
diff --git a/apps-archived/clock/apps/web/src/lib/components/WorldMap.svelte b/apps-archived/clock/apps/web/src/lib/components/WorldMap.svelte
new file mode 100644
index 000000000..dfdd221dc
--- /dev/null
+++ b/apps-archived/clock/apps/web/src/lib/components/WorldMap.svelte
@@ -0,0 +1,111 @@
+
+
+
+ {#if mapLoaded}
+
+
+
+
+
+
+ {#each cities as city}
+ {@const x = ((city.lng + 180) / 360) * 800}
+ {@const y = ((90 - city.lat) / 180) * 400}
+ {@const isSelected = selectedTimezones.includes(city.timezone)}
+ handleCityClick(city.timezone, city.city)}>
+
+ {#if isSelected}
+
+ {city.city}
+
+ {/if}
+
+ {/each}
+
+
+ {:else}
+
+ Karte wird geladen...
+
+ {/if}
+
+
+
diff --git a/apps-archived/clock/apps/web/src/lib/components/life-clock/CircularProgress.svelte b/apps-archived/clock/apps/web/src/lib/components/life-clock/CircularProgress.svelte
new file mode 100644
index 000000000..8327c6928
--- /dev/null
+++ b/apps-archived/clock/apps/web/src/lib/components/life-clock/CircularProgress.svelte
@@ -0,0 +1,204 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ {#each Array(8) as _, i}
+ {@const angle = (i / 8) * 360 - 90}
+ {@const markerRadius = radius + strokeWidth / 2 + 8}
+ {@const x = size / 2 + markerRadius * Math.cos((angle * Math.PI) / 180)}
+ {@const y = size / 2 + markerRadius * Math.sin((angle * Math.PI) / 180)}
+
+ {i * 10}
+
+ {/each}
+
+
+
+
+ {percentage.toFixed(1)}%
+ gelebt
+
+
+
+
+
+
+ {daysLived.toLocaleString('de-DE')}
+ Tage gelebt
+
+
+
+ {remainingDays.toLocaleString('de-DE')}
+ Tage verbleibend
+
+
+
Basierend auf {lifeExpectancyYears} Jahren Lebenserwartung
+
+
+
+
diff --git a/apps-archived/clock/apps/web/src/lib/components/skeletons/AlarmsSkeleton.svelte b/apps-archived/clock/apps/web/src/lib/components/skeletons/AlarmsSkeleton.svelte
new file mode 100644
index 000000000..0c3ccd6c4
--- /dev/null
+++ b/apps-archived/clock/apps/web/src/lib/components/skeletons/AlarmsSkeleton.svelte
@@ -0,0 +1,71 @@
+
+
+
+
+
+ {#each Array(6) as _, i}
+
+
+
+ {/each}
+
+
+
+
+ {#each Array(3) as _, i}
+
+ {/each}
+
+
+
+
diff --git a/apps-archived/clock/apps/web/src/lib/components/skeletons/AppLoadingSkeleton.svelte b/apps-archived/clock/apps/web/src/lib/components/skeletons/AppLoadingSkeleton.svelte
new file mode 100644
index 000000000..21f6f848a
--- /dev/null
+++ b/apps-archived/clock/apps/web/src/lib/components/skeletons/AppLoadingSkeleton.svelte
@@ -0,0 +1,90 @@
+
+
+
+
+
diff --git a/apps-archived/clock/apps/web/src/lib/components/skeletons/TimersSkeleton.svelte b/apps-archived/clock/apps/web/src/lib/components/skeletons/TimersSkeleton.svelte
new file mode 100644
index 000000000..abf9d75ea
--- /dev/null
+++ b/apps-archived/clock/apps/web/src/lib/components/skeletons/TimersSkeleton.svelte
@@ -0,0 +1,85 @@
+
+
+
+
+
+ {#each Array(4) as _, i}
+
+ {/each}
+
+
+
+
+
+
+
+
+
+
+ {#each Array(2) as _, i}
+
+ {/each}
+
+
+
+
diff --git a/apps-archived/clock/apps/web/src/lib/components/skeletons/WorldClockSkeleton.svelte b/apps-archived/clock/apps/web/src/lib/components/skeletons/WorldClockSkeleton.svelte
new file mode 100644
index 000000000..8c87cc6ad
--- /dev/null
+++ b/apps-archived/clock/apps/web/src/lib/components/skeletons/WorldClockSkeleton.svelte
@@ -0,0 +1,58 @@
+
+
+
+
+
+
+
+
+
+
+ {#each Array(4) as _, i}
+
+ {/each}
+
+
+
+
diff --git a/apps-archived/clock/apps/web/src/lib/components/skeletons/index.ts b/apps-archived/clock/apps/web/src/lib/components/skeletons/index.ts
new file mode 100644
index 000000000..1fdb0e411
--- /dev/null
+++ b/apps-archived/clock/apps/web/src/lib/components/skeletons/index.ts
@@ -0,0 +1,13 @@
+/**
+ * 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';
diff --git a/apps-archived/clock/apps/web/src/lib/content/help/index.test.ts b/apps-archived/clock/apps/web/src/lib/content/help/index.test.ts
new file mode 100644
index 000000000..a1e31e35e
--- /dev/null
+++ b/apps-archived/clock/apps/web/src/lib/content/help/index.test.ts
@@ -0,0 +1,47 @@
+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);
+ });
+});
diff --git a/apps-archived/clock/apps/web/src/lib/content/help/index.ts b/apps-archived/clock/apps/web/src/lib/content/help/index.ts
new file mode 100644
index 000000000..4d0c24838
--- /dev/null
+++ b/apps-archived/clock/apps/web/src/lib/content/help/index.ts
@@ -0,0 +1,215 @@
+/**
+ * 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
+ ? 'Du kannst Wecker auf verschiedene Arten erstellen:
Schnellwecker : Drücke A oder klicke auf das + Symbol im Wecker-TabUhrzeit wählen : Stelle Stunde und Minute ein und wähle die gewünschten WochentageLabel : Gib deinem Wecker einen Namen, z.B. "Morgenroutine"Klingelton : Wähle aus verschiedenen Tönen oder nutze einen sanften Weckton '
+ : 'You can create alarms in several ways:
Quick alarm : Press A or click the + icon in the Alarms tabSet time : Choose hour and minute and select the desired weekdaysLabel : Give your alarm a name, e.g. "Morning routine"Ringtone : Choose from various sounds or use a gentle wake-up tone ',
+ 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
+ ? 'Clock bietet zwei Zeitmesser:
Timer : Stelle eine Countdown-Zeit ein und starte ihn. Du kannst mehrere Timer gleichzeitig laufen lassen. Drücke T für einen neuen Timer.Stoppuhr : Messe verstrichene Zeit mit Rundenzeiten. Starte, pausiere und setze zurück.Beide laufen auch im Hintergrund weiter und benachrichtigen dich, wenn die Zeit abgelaufen ist.
'
+ : 'Clock offers two time measurement tools:
Timer : Set a countdown duration and start it. You can run multiple timers simultaneously. Press T for a new timer.Stopwatch : Measure elapsed time with lap splits. Start, pause, and reset.Both continue running in the background and notify you when time is up.
',
+ 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
+ ? 'Die Pomodoro-Technik ist eine Zeitmanagement-Methode:
Arbeite 25 Minuten konzentriert (ein "Pomodoro") Mache eine 5-Minuten-Pause Nach 4 Pomodoros: 15-30 Minuten längere Pause In Clock kannst du die Intervalle anpassen, deinen Fortschritt verfolgen und Statistiken über deine Produktivität einsehen.
'
+ : 'The Pomodoro technique is a time management method:
Work for 25 minutes with focus (one "Pomodoro") Take a 5-minute break After 4 Pomodoros: take a 15-30 minute longer break In Clock you can customize the intervals, track your progress, and view statistics about your productivity.
',
+ 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
+ ? 'Die Life Clock ist eine einzigartige Visualisierung deiner Lebenszeit:
Gib dein Geburtsdatum und deine geschätzte Lebenserwartung ein Sieh, wie viel deiner Zeit bereits vergangen ist und wie viel noch vor dir liegt Verschiedene Darstellungen: Wochen, Monate oder Jahre als Raster Die Life Clock soll dich motivieren, deine Zeit bewusst zu nutzen — keine Angst, sondern Inspiration .
'
+ : 'The Life Clock is a unique visualization of your lifetime:
Enter your birth date and estimated life expectancy See how much of your time has passed and how much lies ahead Various display modes: weeks, months, or years as a grid The Life Clock is meant to motivate you to use your time mindfully — not fear, but inspiration .
',
+ 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: [
+ 'Lokale Speicherung : Wecker und Timer werden lokal auf deinem Gerät gespeichert',
+ ],
+ extraBulletsEN: [
+ 'Local storage : 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
+ ? 'Unser Support-Team hilft dir bei allen Fragen rund um Clock.
'
+ : 'Our support team is here to help you with any questions about Clock.
',
+ 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',
+ },
+ };
+}
diff --git a/apps-archived/clock/apps/web/src/lib/data/guest-seed.ts b/apps-archived/clock/apps/web/src/lib/data/guest-seed.ts
new file mode 100644
index 000000000..6739c7dc3
--- /dev/null
+++ b/apps-archived/clock/apps/web/src/lib/data/guest-seed.ts
@@ -0,0 +1,36 @@
+/**
+ * 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,
+ },
+];
diff --git a/apps-archived/clock/apps/web/src/lib/data/local-store.ts b/apps-archived/clock/apps/web/src/lib/data/local-store.ts
new file mode 100644
index 000000000..728302506
--- /dev/null
+++ b/apps-archived/clock/apps/web/src/lib/data/local-store.ts
@@ -0,0 +1,69 @@
+/**
+ * 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('alarms');
+export const timerCollection = clockStore.collection('timers');
+export const worldClockCollection = clockStore.collection('worldClocks');
diff --git a/apps-archived/clock/apps/web/src/lib/data/queries.ts b/apps-archived/clock/apps/web/src/lib/data/queries.ts
new file mode 100644
index 000000000..88e9a4c61
--- /dev/null
+++ b/apps-archived/clock/apps/web/src/lib/data/queries.ts
@@ -0,0 +1,106 @@
+/**
+ * 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);
+}
diff --git a/apps-archived/clock/apps/web/src/lib/i18n/index.ts b/apps-archived/clock/apps/web/src/lib/i18n/index.ts
new file mode 100644
index 000000000..e6c3a0023
--- /dev/null
+++ b/apps-archived/clock/apps/web/src/lib/i18n/index.ts
@@ -0,0 +1,49 @@
+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 };
diff --git a/apps-archived/clock/apps/web/src/lib/i18n/locales/de.json b/apps-archived/clock/apps/web/src/lib/i18n/locales/de.json
new file mode 100644
index 000000000..fc35180f9
--- /dev/null
+++ b/apps-archived/clock/apps/web/src/lib/i18n/locales/de.json
@@ -0,0 +1,23 @@
+{
+ "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"
+ }
+}
diff --git a/apps-archived/clock/apps/web/src/lib/i18n/locales/en.json b/apps-archived/clock/apps/web/src/lib/i18n/locales/en.json
new file mode 100644
index 000000000..f6f978137
--- /dev/null
+++ b/apps-archived/clock/apps/web/src/lib/i18n/locales/en.json
@@ -0,0 +1,23 @@
+{
+ "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"
+ }
+}
diff --git a/apps-archived/clock/apps/web/src/lib/stores/alarms.svelte.ts b/apps-archived/clock/apps/web/src/lib/stores/alarms.svelte.ts
new file mode 100644
index 000000000..e35201385
--- /dev/null
+++ b/apps-archived/clock/apps/web/src/lib/stores/alarms.svelte.ts
@@ -0,0 +1,97 @@
+/**
+ * Alarms Store — Mutation-Only Service
+ *
+ * All reads are handled by useLiveQuery() hooks in queries.ts.
+ * This store only provides write operations (create, update, delete, toggle).
+ * IndexedDB writes automatically trigger UI updates via Dexie liveQuery.
+ */
+
+import { alarmCollection, type LocalAlarm } from '$lib/data/local-store';
+import { toAlarm } from '$lib/data/queries';
+import type { CreateAlarmInput, UpdateAlarmInput, Alarm } from '@clock/shared';
+
+let error = $state(null);
+
+export const alarmsStore = {
+ get error() {
+ return error;
+ },
+
+ /**
+ * Create a new alarm -- writes to IndexedDB instantly.
+ */
+ async createAlarm(input: CreateAlarmInput) {
+ error = null;
+ try {
+ const newLocal: LocalAlarm = {
+ id: crypto.randomUUID(),
+ label: input.label ?? null,
+ time: input.time,
+ enabled: input.enabled ?? true,
+ repeatDays: input.repeatDays ?? null,
+ snoozeMinutes: input.snoozeMinutes ?? null,
+ sound: input.sound ?? null,
+ vibrate: input.vibrate ?? null,
+ };
+
+ const inserted = await alarmCollection.insert(newLocal);
+ return { success: true, data: toAlarm(inserted) };
+ } catch (e) {
+ error = e instanceof Error ? e.message : 'Failed to create alarm';
+ console.error('Failed to create alarm:', e);
+ return { success: false, error: error };
+ }
+ },
+
+ /**
+ * Update an alarm -- writes to IndexedDB instantly.
+ */
+ async updateAlarm(id: string, input: UpdateAlarmInput) {
+ error = null;
+ try {
+ const updateData: Partial = {};
+ if (input.label !== undefined) updateData.label = input.label ?? null;
+ if (input.time !== undefined) updateData.time = input.time;
+ if (input.enabled !== undefined) updateData.enabled = input.enabled;
+ if (input.repeatDays !== undefined) updateData.repeatDays = input.repeatDays ?? null;
+ if (input.snoozeMinutes !== undefined) updateData.snoozeMinutes = input.snoozeMinutes ?? null;
+ if (input.sound !== undefined) updateData.sound = input.sound ?? null;
+ if (input.vibrate !== undefined) updateData.vibrate = input.vibrate ?? null;
+
+ const updated = await alarmCollection.update(id, updateData);
+ if (updated) {
+ return { success: true, data: toAlarm(updated) };
+ }
+ return { success: false, error: 'Alarm not found' };
+ } catch (e) {
+ error = e instanceof Error ? e.message : 'Failed to update alarm';
+ console.error('Failed to update alarm:', e);
+ return { success: false, error: error };
+ }
+ },
+
+ /**
+ * Toggle alarm enabled state.
+ */
+ async toggleAlarm(id: string, currentAlarms: Alarm[]) {
+ const alarm = currentAlarms.find((a) => a.id === id);
+ if (!alarm) return { success: false, error: 'Alarm not found' };
+
+ return this.updateAlarm(id, { enabled: !alarm.enabled });
+ },
+
+ /**
+ * Delete an alarm -- removes from IndexedDB instantly.
+ */
+ async deleteAlarm(id: string) {
+ error = null;
+ try {
+ await alarmCollection.delete(id);
+ return { success: true };
+ } catch (e) {
+ error = e instanceof Error ? e.message : 'Failed to delete alarm';
+ console.error('Failed to delete alarm:', e);
+ return { success: false, error: error };
+ }
+ },
+};
diff --git a/apps-archived/clock/apps/web/src/lib/stores/app-onboarding.svelte.ts b/apps-archived/clock/apps/web/src/lib/stores/app-onboarding.svelte.ts
new file mode 100644
index 000000000..faf855557
--- /dev/null
+++ b/apps-archived/clock/apps/web/src/lib/stores/app-onboarding.svelte.ts
@@ -0,0 +1,82 @@
+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 () => {},
+});
diff --git a/apps-archived/clock/apps/web/src/lib/stores/auth.svelte.ts b/apps-archived/clock/apps/web/src/lib/stores/auth.svelte.ts
new file mode 100644
index 000000000..5f0483975
--- /dev/null
+++ b/apps-archived/clock/apps/web/src/lib/stores/auth.svelte.ts
@@ -0,0 +1,9 @@
+/**
+ * Auth Store — uses centralized Mana auth factory.
+ */
+
+import { createManaAuthStore } from '@manacore/shared-auth-stores';
+
+export const authStore = createManaAuthStore({
+ devBackendPort: 3017,
+});
diff --git a/apps-archived/clock/apps/web/src/lib/stores/navigation.ts b/apps-archived/clock/apps/web/src/lib/stores/navigation.ts
new file mode 100644
index 000000000..08ea3eeec
--- /dev/null
+++ b/apps-archived/clock/apps/web/src/lib/stores/navigation.ts
@@ -0,0 +1,5 @@
+import { createSimpleNavigationStores } from '@manacore/shared-stores';
+
+export const { isNavCollapsed } = createSimpleNavigationStores({
+ storageKey: 'clock',
+});
diff --git a/apps-archived/clock/apps/web/src/lib/stores/session-alarms.svelte.ts b/apps-archived/clock/apps/web/src/lib/stores/session-alarms.svelte.ts
new file mode 100644
index 000000000..04e334e34
--- /dev/null
+++ b/apps-archived/clock/apps/web/src/lib/stores/session-alarms.svelte.ts
@@ -0,0 +1,150 @@
+/**
+ * Session Alarms Store - Manages alarms in sessionStorage for guest users
+ * 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';
+
+const STORAGE_KEY = 'clock-session-alarms';
+
+// State
+let alarms = $state([]);
+
+// Generate session ID
+function generateSessionId(): string {
+ return `session_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
+}
+
+// Load from sessionStorage
+function loadFromStorage(): void {
+ if (typeof window === 'undefined') return;
+
+ try {
+ const stored = sessionStorage.getItem(STORAGE_KEY);
+ if (stored) {
+ alarms = JSON.parse(stored);
+ }
+ } catch (e) {
+ console.error('Failed to load session alarms:', e);
+ }
+}
+
+// Save to sessionStorage
+function saveToStorage(): void {
+ if (typeof window === 'undefined') return;
+
+ try {
+ sessionStorage.setItem(STORAGE_KEY, JSON.stringify(alarms));
+ } catch (e) {
+ console.error('Failed to save session alarms:', e);
+ }
+}
+
+// Initialize on load
+if (typeof window !== 'undefined') {
+ loadFromStorage();
+}
+
+export const sessionAlarmsStore = {
+ // Getters
+ get alarms() {
+ return alarms;
+ },
+ get enabledAlarms() {
+ return alarms.filter((a) => a.enabled);
+ },
+
+ /**
+ * Create a new session alarm
+ */
+ createAlarm(input: CreateAlarmInput): Alarm {
+ const now = new Date().toISOString();
+ const alarm: Alarm = {
+ id: generateSessionId(),
+ userId: 'guest',
+ label: input.label || null,
+ time: input.time,
+ enabled: input.enabled ?? true,
+ repeatDays: input.repeatDays || null,
+ snoozeMinutes: input.snoozeMinutes || null,
+ sound: input.sound || null,
+ vibrate: input.vibrate ?? null,
+ createdAt: now,
+ updatedAt: now,
+ };
+
+ alarms = [...alarms, alarm];
+ saveToStorage();
+
+ return alarm;
+ },
+
+ /**
+ * Update a session alarm
+ */
+ updateAlarm(id: string, input: UpdateAlarmInput): Alarm | null {
+ const index = alarms.findIndex((a) => a.id === id);
+ if (index === -1) return null;
+
+ const updated: Alarm = {
+ ...alarms[index],
+ ...input,
+ updatedAt: new Date().toISOString(),
+ };
+
+ alarms = alarms.map((a) => (a.id === id ? updated : a));
+ saveToStorage();
+
+ return updated;
+ },
+
+ /**
+ * Toggle alarm enabled state
+ */
+ toggleAlarm(id: string): Alarm | null {
+ const alarm = alarms.find((a) => a.id === id);
+ if (!alarm) return null;
+
+ return this.updateAlarm(id, { enabled: !alarm.enabled });
+ },
+
+ /**
+ * Delete a session alarm
+ */
+ deleteAlarm(id: string): void {
+ alarms = alarms.filter((a) => a.id !== id);
+ saveToStorage();
+ },
+
+ /**
+ * Check if ID is a session alarm
+ */
+ isSessionAlarm(id: string): boolean {
+ return id.startsWith('session_');
+ },
+
+ /**
+ * Get all alarms for migration
+ */
+ getAllAlarms(): Alarm[] {
+ return [...alarms];
+ },
+
+ /**
+ * Clear all session data
+ */
+ clear(): void {
+ alarms = [];
+ if (typeof window !== 'undefined') {
+ sessionStorage.removeItem(STORAGE_KEY);
+ }
+ },
+
+ /**
+ * Get count of session alarms
+ */
+ get count(): number {
+ return alarms.length;
+ },
+};
diff --git a/apps-archived/clock/apps/web/src/lib/stores/session-timers.svelte.ts b/apps-archived/clock/apps/web/src/lib/stores/session-timers.svelte.ts
new file mode 100644
index 000000000..d2953a383
--- /dev/null
+++ b/apps-archived/clock/apps/web/src/lib/stores/session-timers.svelte.ts
@@ -0,0 +1,214 @@
+/**
+ * Session Timers Store - Manages timers in sessionStorage for guest users
+ * 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';
+
+const STORAGE_KEY = 'clock-session-timers';
+
+// State
+let timers = $state([]);
+
+// Generate session ID
+function generateSessionId(): string {
+ return `session_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
+}
+
+// Load from sessionStorage
+function loadFromStorage(): void {
+ if (typeof window === 'undefined') return;
+
+ try {
+ const stored = sessionStorage.getItem(STORAGE_KEY);
+ if (stored) {
+ timers = JSON.parse(stored);
+ }
+ } catch (e) {
+ console.error('Failed to load session timers:', e);
+ }
+}
+
+// Save to sessionStorage
+function saveToStorage(): void {
+ if (typeof window === 'undefined') return;
+
+ try {
+ sessionStorage.setItem(STORAGE_KEY, JSON.stringify(timers));
+ } catch (e) {
+ console.error('Failed to save session timers:', e);
+ }
+}
+
+// Initialize on load
+if (typeof window !== 'undefined') {
+ loadFromStorage();
+}
+
+export const sessionTimersStore = {
+ // Getters
+ get timers() {
+ return timers;
+ },
+ get activeTimers() {
+ return timers.filter((t) => t.status === 'running' || t.status === 'paused');
+ },
+
+ /**
+ * Create a new session timer
+ */
+ createTimer(input: CreateTimerInput): Timer {
+ const now = new Date().toISOString();
+ const timer: Timer = {
+ id: generateSessionId(),
+ userId: 'guest',
+ label: input.label || null,
+ durationSeconds: input.durationSeconds,
+ remainingSeconds: input.durationSeconds,
+ status: 'idle' as TimerStatus,
+ startedAt: null,
+ pausedAt: null,
+ sound: input.sound || null,
+ createdAt: now,
+ updatedAt: now,
+ };
+
+ timers = [...timers, timer];
+ saveToStorage();
+
+ return timer;
+ },
+
+ /**
+ * Update a session timer
+ */
+ updateTimer(id: string, input: UpdateTimerInput): Timer | null {
+ const index = timers.findIndex((t) => t.id === id);
+ if (index === -1) return null;
+
+ const updated: Timer = {
+ ...timers[index],
+ ...input,
+ updatedAt: new Date().toISOString(),
+ };
+
+ timers = timers.map((t) => (t.id === id ? updated : t));
+ saveToStorage();
+
+ return updated;
+ },
+
+ /**
+ * Start a timer
+ */
+ startTimer(id: string): Timer | null {
+ const timer = timers.find((t) => t.id === id);
+ if (!timer) return null;
+
+ const now = new Date().toISOString();
+ const updated: Timer = {
+ ...timer,
+ status: 'running',
+ startedAt: now,
+ pausedAt: null,
+ updatedAt: now,
+ };
+
+ timers = timers.map((t) => (t.id === id ? updated : t));
+ saveToStorage();
+
+ return updated;
+ },
+
+ /**
+ * Pause a timer
+ */
+ pauseTimer(id: string): Timer | null {
+ const timer = timers.find((t) => t.id === id);
+ if (!timer) return null;
+
+ const now = new Date().toISOString();
+ const updated: Timer = {
+ ...timer,
+ status: 'paused',
+ pausedAt: now,
+ updatedAt: now,
+ };
+
+ timers = timers.map((t) => (t.id === id ? updated : t));
+ saveToStorage();
+
+ return updated;
+ },
+
+ /**
+ * Reset a timer
+ */
+ resetTimer(id: string): Timer | null {
+ const timer = timers.find((t) => t.id === id);
+ if (!timer) return null;
+
+ const now = new Date().toISOString();
+ const updated: Timer = {
+ ...timer,
+ status: 'idle',
+ remainingSeconds: timer.durationSeconds,
+ startedAt: null,
+ pausedAt: null,
+ updatedAt: now,
+ };
+
+ timers = timers.map((t) => (t.id === id ? updated : t));
+ saveToStorage();
+
+ return updated;
+ },
+
+ /**
+ * Update local timer state (for countdown display)
+ */
+ updateLocalState(id: string, updates: Partial): void {
+ timers = timers.map((t) => (t.id === id ? { ...t, ...updates } : t));
+ saveToStorage();
+ },
+
+ /**
+ * Delete a session timer
+ */
+ deleteTimer(id: string): void {
+ timers = timers.filter((t) => t.id !== id);
+ saveToStorage();
+ },
+
+ /**
+ * Check if ID is a session timer
+ */
+ isSessionTimer(id: string): boolean {
+ return id.startsWith('session_');
+ },
+
+ /**
+ * Get all timers for migration
+ */
+ getAllTimers(): Timer[] {
+ return [...timers];
+ },
+
+ /**
+ * Clear all session data
+ */
+ clear(): void {
+ timers = [];
+ if (typeof window !== 'undefined') {
+ sessionStorage.removeItem(STORAGE_KEY);
+ }
+ },
+
+ /**
+ * Get count of session timers
+ */
+ get count(): number {
+ return timers.length;
+ },
+};
diff --git a/apps-archived/clock/apps/web/src/lib/stores/stopwatch.svelte.ts b/apps-archived/clock/apps/web/src/lib/stores/stopwatch.svelte.ts
new file mode 100644
index 000000000..5573dfbc8
--- /dev/null
+++ b/apps-archived/clock/apps/web/src/lib/stores/stopwatch.svelte.ts
@@ -0,0 +1,231 @@
+/**
+ * Stopwatch Store - Manages stopwatch state using Svelte 5 runes
+ * Stopwatches are local-only (no backend sync)
+ */
+
+export interface Lap {
+ number: number;
+ time: number; // milliseconds since start
+ delta: number; // milliseconds since last lap
+}
+
+export interface Stopwatch {
+ id: string;
+ label: string;
+ startTime: number | null; // timestamp when started
+ elapsedTime: number; // accumulated milliseconds when paused
+ status: 'idle' | 'running' | 'paused';
+ laps: Lap[];
+ color: string;
+}
+
+export const STOPWATCH_COLORS = [
+ '#3B82F6', // blue
+ '#10B981', // green
+ '#F59E0B', // amber
+ '#EF4444', // red
+ '#8B5CF6', // violet
+ '#EC4899', // pink
+ '#14B8A6', // teal
+ '#F97316', // orange
+];
+
+// State
+let stopwatches = $state([]);
+let focusedId = $state(null);
+let colorIndex = 0;
+
+// Tick interval for updating display
+let tickInterval: ReturnType | null = null;
+
+function getNextColor(): string {
+ const color = STOPWATCH_COLORS[colorIndex % STOPWATCH_COLORS.length];
+ colorIndex++;
+ return color;
+}
+
+function startTicking() {
+ if (tickInterval) return;
+ tickInterval = setInterval(() => {
+ // Force reactivity update by reassigning
+ stopwatches = [...stopwatches];
+ }, 100);
+}
+
+function stopTickingIfNoRunning() {
+ const hasRunning = stopwatches.some((sw) => sw.status === 'running');
+ if (!hasRunning && tickInterval) {
+ clearInterval(tickInterval);
+ tickInterval = null;
+ }
+}
+
+export const stopwatchesStore = {
+ // Getters
+ get stopwatches() {
+ return stopwatches;
+ },
+ get focusedId() {
+ return focusedId;
+ },
+ get focusedStopwatch() {
+ return stopwatches.find((sw) => sw.id === focusedId) || null;
+ },
+
+ /**
+ * Create a new stopwatch
+ */
+ create(label?: string): string {
+ const id = crypto.randomUUID();
+ const newStopwatch: Stopwatch = {
+ id,
+ label: label || `Stopwatch ${stopwatches.length + 1}`,
+ startTime: null,
+ elapsedTime: 0,
+ status: 'idle',
+ laps: [],
+ color: getNextColor(),
+ };
+ stopwatches = [...stopwatches, newStopwatch];
+ if (!focusedId) {
+ focusedId = id;
+ }
+ return id;
+ },
+
+ /**
+ * Start a stopwatch
+ */
+ start(id: string) {
+ stopwatches = stopwatches.map((sw) => {
+ if (sw.id !== id) return sw;
+ return {
+ ...sw,
+ startTime: Date.now(),
+ status: 'running' as const,
+ };
+ });
+ startTicking();
+ },
+
+ /**
+ * Pause a stopwatch
+ */
+ pause(id: string) {
+ stopwatches = stopwatches.map((sw) => {
+ if (sw.id !== id || sw.status !== 'running') return sw;
+ const elapsed = sw.startTime ? Date.now() - sw.startTime : 0;
+ return {
+ ...sw,
+ startTime: null,
+ elapsedTime: sw.elapsedTime + elapsed,
+ status: 'paused' as const,
+ };
+ });
+ stopTickingIfNoRunning();
+ },
+
+ /**
+ * Reset a stopwatch
+ */
+ reset(id: string) {
+ stopwatches = stopwatches.map((sw) => {
+ if (sw.id !== id) return sw;
+ return {
+ ...sw,
+ startTime: null,
+ elapsedTime: 0,
+ status: 'idle' as const,
+ laps: [],
+ };
+ });
+ stopTickingIfNoRunning();
+ },
+
+ /**
+ * Add a lap to a stopwatch
+ */
+ addLap(id: string) {
+ stopwatches = stopwatches.map((sw) => {
+ if (sw.id !== id || sw.status !== 'running') return sw;
+ const currentTime = this.getElapsed(sw);
+ const lastLap = sw.laps[sw.laps.length - 1];
+ const delta = lastLap ? currentTime - lastLap.time : currentTime;
+ const newLap: Lap = {
+ number: sw.laps.length + 1,
+ time: currentTime,
+ delta,
+ };
+ return {
+ ...sw,
+ laps: [...sw.laps, newLap],
+ };
+ });
+ },
+
+ /**
+ * Delete a stopwatch
+ */
+ delete(id: string) {
+ stopwatches = stopwatches.filter((sw) => sw.id !== id);
+ if (focusedId === id) {
+ focusedId = stopwatches[0]?.id || null;
+ }
+ stopTickingIfNoRunning();
+ },
+
+ /**
+ * Set focused stopwatch
+ */
+ setFocused(id: string | null) {
+ focusedId = id;
+ },
+
+ /**
+ * Update stopwatch label
+ */
+ updateLabel(id: string, label: string) {
+ stopwatches = stopwatches.map((sw) => (sw.id === id ? { ...sw, label } : sw));
+ },
+
+ /**
+ * Get elapsed time for a stopwatch
+ */
+ getElapsed(sw: Stopwatch): number {
+ if (sw.status === 'running' && sw.startTime) {
+ return sw.elapsedTime + (Date.now() - sw.startTime);
+ }
+ return sw.elapsedTime;
+ },
+};
+
+/**
+ * Format time in milliseconds to display string
+ */
+export function formatTime(ms: number): string {
+ const totalSeconds = Math.floor(ms / 1000);
+ const hours = Math.floor(totalSeconds / 3600);
+ const minutes = Math.floor((totalSeconds % 3600) / 60);
+ const seconds = totalSeconds % 60;
+ const centiseconds = Math.floor((ms % 1000) / 10);
+
+ if (hours > 0) {
+ return `${hours}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}.${centiseconds.toString().padStart(2, '0')}`;
+ }
+ return `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}.${centiseconds.toString().padStart(2, '0')}`;
+}
+
+/**
+ * Format lap time (delta) for display
+ */
+export function formatLapTime(ms: number): string {
+ const totalSeconds = Math.floor(ms / 1000);
+ const minutes = Math.floor(totalSeconds / 60);
+ const seconds = totalSeconds % 60;
+ const centiseconds = Math.floor((ms % 1000) / 10);
+
+ if (minutes > 0) {
+ return `+${minutes}:${seconds.toString().padStart(2, '0')}.${centiseconds.toString().padStart(2, '0')}`;
+ }
+ return `+${seconds}.${centiseconds.toString().padStart(2, '0')}`;
+}
diff --git a/apps-archived/clock/apps/web/src/lib/stores/tags.svelte.ts b/apps-archived/clock/apps/web/src/lib/stores/tags.svelte.ts
new file mode 100644
index 000000000..074782375
--- /dev/null
+++ b/apps-archived/clock/apps/web/src/lib/stores/tags.svelte.ts
@@ -0,0 +1,13 @@
+/**
+ * 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';
diff --git a/apps-archived/clock/apps/web/src/lib/stores/theme.svelte.ts b/apps-archived/clock/apps/web/src/lib/stores/theme.svelte.ts
new file mode 100644
index 000000000..5784cfeec
--- /dev/null
+++ b/apps-archived/clock/apps/web/src/lib/stores/theme.svelte.ts
@@ -0,0 +1,7 @@
+import { createThemeStore } from '@manacore/shared-theme';
+
+// Create theme store with Clock's styling
+export const theme = createThemeStore({
+ appId: 'clock',
+ defaultVariant: 'lume',
+});
diff --git a/apps-archived/clock/apps/web/src/lib/stores/timers.svelte.ts b/apps-archived/clock/apps/web/src/lib/stores/timers.svelte.ts
new file mode 100644
index 000000000..0c5e3441e
--- /dev/null
+++ b/apps-archived/clock/apps/web/src/lib/stores/timers.svelte.ts
@@ -0,0 +1,191 @@
+/**
+ * Timers Store — Mutation-Only Service
+ *
+ * All reads are handled by useLiveQuery() hooks in queries.ts.
+ * This store only provides write operations (create, update, delete, start, pause, reset).
+ * IndexedDB writes automatically trigger UI updates via Dexie liveQuery.
+ */
+
+import { timerCollection, type LocalTimer } from '$lib/data/local-store';
+import { toTimer } from '$lib/data/queries';
+import type { CreateTimerInput, UpdateTimerInput } from '@clock/shared';
+import { ClockEvents } from '@manacore/shared-utils/analytics';
+
+let error = $state(null);
+
+export const timersStore = {
+ get error() {
+ return error;
+ },
+
+ /**
+ * Create a new timer -- writes to IndexedDB instantly.
+ */
+ async createTimer(input: CreateTimerInput) {
+ error = null;
+ try {
+ const newLocal: LocalTimer = {
+ id: crypto.randomUUID(),
+ label: input.label ?? null,
+ durationSeconds: input.durationSeconds,
+ remainingSeconds: null,
+ status: 'idle',
+ startedAt: null,
+ pausedAt: null,
+ sound: input.sound ?? null,
+ };
+
+ const inserted = await timerCollection.insert(newLocal);
+ return { success: true, data: toTimer(inserted) };
+ } catch (e) {
+ error = e instanceof Error ? e.message : 'Failed to create timer';
+ console.error('Failed to create timer:', e);
+ return { success: false, error: error };
+ }
+ },
+
+ /**
+ * Update a timer -- writes to IndexedDB instantly.
+ */
+ async updateTimer(id: string, input: UpdateTimerInput) {
+ error = null;
+ try {
+ const updateData: Partial = {};
+ 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;
+
+ const updated = await timerCollection.update(id, updateData);
+ if (updated) {
+ return { success: true, data: toTimer(updated) };
+ }
+ return { success: false, error: 'Timer not found' };
+ } catch (e) {
+ error = e instanceof Error ? e.message : 'Failed to update timer';
+ console.error('Failed to update timer:', e);
+ return { success: false, error: error };
+ }
+ },
+
+ /**
+ * Start a timer -- sets status to running with current timestamp.
+ */
+ async startTimer(id: string) {
+ error = null;
+ try {
+ const existing = await timerCollection.get(id);
+ if (!existing) return { success: false, error: 'Timer not found' };
+
+ const updateData: Partial = {
+ status: 'running',
+ startedAt: new Date().toISOString(),
+ pausedAt: null,
+ };
+
+ // If resuming from pause, keep remaining seconds
+ if (existing.status !== 'paused') {
+ updateData.remainingSeconds = existing.durationSeconds;
+ }
+
+ const updated = await timerCollection.update(id, updateData);
+ if (updated) {
+ const updatedTimer = toTimer(updated);
+ ClockEvents.timerStarted(
+ (updatedTimer as any).type as 'pomodoro' | 'stopwatch' | 'countdown'
+ );
+ return { success: true, data: updatedTimer };
+ }
+ return { success: false, error: 'Timer not found' };
+ } catch (e) {
+ error = e instanceof Error ? e.message : 'Failed to start timer';
+ console.error('Failed to start timer:', e);
+ return { success: false, error: error };
+ }
+ },
+
+ /**
+ * Pause a timer -- calculates remaining seconds and saves.
+ */
+ async pauseTimer(id: string) {
+ error = null;
+ try {
+ const existing = await timerCollection.get(id);
+ if (!existing) return { success: false, error: 'Timer not found' };
+
+ // Calculate remaining seconds
+ let remaining = existing.remainingSeconds ?? existing.durationSeconds;
+ if (existing.startedAt) {
+ const elapsed = (Date.now() - new Date(existing.startedAt).getTime()) / 1000;
+ remaining = Math.max(0, remaining - elapsed);
+ }
+
+ const updateData: Partial = {
+ status: 'paused',
+ pausedAt: new Date().toISOString(),
+ remainingSeconds: Math.round(remaining),
+ startedAt: null,
+ };
+
+ const updated = await timerCollection.update(id, updateData);
+ if (updated) {
+ return { success: true, data: toTimer(updated) };
+ }
+ return { success: false, error: 'Timer not found' };
+ } catch (e) {
+ error = e instanceof Error ? e.message : 'Failed to pause timer';
+ console.error('Failed to pause timer:', e);
+ return { success: false, error: error };
+ }
+ },
+
+ /**
+ * Reset a timer -- back to idle with full duration.
+ */
+ async resetTimer(id: string) {
+ error = null;
+ try {
+ const updateData: Partial = {
+ status: 'idle',
+ remainingSeconds: null,
+ startedAt: null,
+ pausedAt: null,
+ };
+
+ const updated = await timerCollection.update(id, updateData);
+ if (updated) {
+ return { success: true, data: toTimer(updated) };
+ }
+ return { success: false, error: 'Timer not found' };
+ } catch (e) {
+ error = e instanceof Error ? e.message : 'Failed to reset timer';
+ console.error('Failed to reset timer:', e);
+ return { success: false, error: error };
+ }
+ },
+
+ /**
+ * Delete a timer -- removes from IndexedDB instantly.
+ */
+ async deleteTimer(id: string) {
+ error = null;
+ try {
+ await timerCollection.delete(id);
+ return { success: true };
+ } catch (e) {
+ error = e instanceof Error ? e.message : 'Failed to delete timer';
+ console.error('Failed to delete timer:', e);
+ return { success: false, error: error };
+ }
+ },
+
+ /**
+ * Update remaining seconds in IndexedDB (for countdown display).
+ */
+ async updateLocalTimer(id: string, remainingSeconds: number) {
+ try {
+ await timerCollection.update(id, { remainingSeconds });
+ } catch (e) {
+ console.error('Failed to update local timer:', e);
+ }
+ },
+};
diff --git a/apps-archived/clock/apps/web/src/lib/stores/user-settings.svelte.ts b/apps-archived/clock/apps/web/src/lib/stores/user-settings.svelte.ts
new file mode 100644
index 000000000..d28b67ea6
--- /dev/null
+++ b/apps-archived/clock/apps/web/src/lib/stores/user-settings.svelte.ts
@@ -0,0 +1,28 @@
+/**
+ * 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(),
+});
diff --git a/apps-archived/clock/apps/web/src/lib/stores/world-clocks.svelte.ts b/apps-archived/clock/apps/web/src/lib/stores/world-clocks.svelte.ts
new file mode 100644
index 000000000..252468d9a
--- /dev/null
+++ b/apps-archived/clock/apps/web/src/lib/stores/world-clocks.svelte.ts
@@ -0,0 +1,74 @@
+/**
+ * World Clocks Store — Mutation-Only Service
+ *
+ * All reads are handled by useLiveQuery() hooks in queries.ts.
+ * This store only provides write operations (add, remove, reorder).
+ * IndexedDB writes automatically trigger UI updates via Dexie liveQuery.
+ */
+
+import { worldClockCollection, type LocalWorldClock } from '$lib/data/local-store';
+import type { CreateWorldClockInput, WorldClock } from '@clock/shared';
+
+let error = $state(null);
+
+export const worldClocksStore = {
+ get error() {
+ return error;
+ },
+
+ /**
+ * Add a new world clock -- writes to IndexedDB instantly.
+ */
+ async addWorldClock(input: CreateWorldClockInput, currentCount: number = 0) {
+ error = null;
+ try {
+ const newLocal: LocalWorldClock = {
+ id: crypto.randomUUID(),
+ timezone: input.timezone,
+ cityName: input.cityName,
+ sortOrder: currentCount,
+ };
+
+ await worldClockCollection.insert(newLocal);
+ return { success: true };
+ } catch (e) {
+ error = e instanceof Error ? e.message : 'Failed to add world clock';
+ console.error('Failed to add world clock:', e);
+ return { success: false, error: error };
+ }
+ },
+
+ /**
+ * Remove a world clock -- removes from IndexedDB instantly.
+ */
+ async removeWorldClock(id: string) {
+ error = null;
+ try {
+ await worldClockCollection.delete(id);
+ return { success: true };
+ } catch (e) {
+ error = e instanceof Error ? e.message : 'Failed to remove world clock';
+ console.error('Failed to remove world clock:', e);
+ return { success: false, error: error };
+ }
+ },
+
+ /**
+ * Reorder world clocks -- updates sortOrder in IndexedDB.
+ */
+ async reorder(ids: string[]) {
+ error = null;
+ try {
+ for (let i = 0; i < ids.length; i++) {
+ await worldClockCollection.update(ids[i], {
+ sortOrder: i,
+ } as Partial);
+ }
+ return { success: true };
+ } catch (e) {
+ error = e instanceof Error ? e.message : 'Failed to reorder world clocks';
+ console.error('Failed to reorder world clocks:', e);
+ return { success: false, error: error };
+ }
+ },
+};
diff --git a/apps-archived/clock/apps/web/src/lib/version.ts b/apps-archived/clock/apps/web/src/lib/version.ts
new file mode 100644
index 000000000..d63b4cfef
--- /dev/null
+++ b/apps-archived/clock/apps/web/src/lib/version.ts
@@ -0,0 +1,4 @@
+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';
diff --git a/apps-archived/clock/apps/web/src/routes/(app)/+layout.svelte b/apps-archived/clock/apps/web/src/routes/(app)/+layout.svelte
new file mode 100644
index 000000000..79c8a9c38
--- /dev/null
+++ b/apps-archived/clock/apps/web/src/routes/(app)/+layout.svelte
@@ -0,0 +1,465 @@
+
+
+
+
+
+
+
+
+
+ {#if isTagStripVisible}
+
({
+ id: t.id,
+ name: t.name,
+ color: t.color || '#3b82f6',
+ }))}
+ selectedIds={[]}
+ onToggle={() => {}}
+ onClear={() => {}}
+ managementHref="/tags"
+ />
+ {/if}
+
+
+
+ {@render children()}
+
+
+
+
+ (commandBarOpen = false)}
+ onSearch={handleCommandBarSearch}
+ onSelect={handleCommandBarSelect}
+ quickActions={commandBarQuickActions}
+ placeholder="Schnellzugriff..."
+ emptyText="Keine Ergebnisse"
+ searchingText="Suche..."
+ />
+
+
+
+ {#if clockOnboarding.shouldShow}
+
+ {/if}
+
+
+ (showGuestWelcome = false)}
+ onLogin={() => goto('/login')}
+ onRegister={() => goto('/register')}
+ locale={($locale || 'de') === 'de' ? 'de' : 'en'}
+ />
+
+ {#if authStore.isAuthenticated}
+
+ {/if}
+
+
+
+
diff --git a/apps-archived/clock/apps/web/src/routes/(app)/+page.svelte b/apps-archived/clock/apps/web/src/routes/(app)/+page.svelte
new file mode 100644
index 000000000..74d5d5739
--- /dev/null
+++ b/apps-archived/clock/apps/web/src/routes/(app)/+page.svelte
@@ -0,0 +1,115 @@
+
+
+
+ Clock - Dashboard
+
+
+{#if isLoading}
+
+{:else}
+
+
+ Clock
+ Dein Zeit-Management Hub
+
+
+
+
+
+
+
+
+
+
+ {new Date().toLocaleTimeString('de-DE', {
+ hour: '2-digit',
+ minute: '2-digit',
+ second: '2-digit',
+ })}
+
+
+ {new Date().toLocaleDateString('de-DE', {
+ weekday: 'long',
+ year: 'numeric',
+ month: 'long',
+ day: 'numeric',
+ })}
+
+
+
+
+
+
+
+
+{/if}
+
+
diff --git a/apps-archived/clock/apps/web/src/routes/(app)/alarms/+page.svelte b/apps-archived/clock/apps/web/src/routes/(app)/alarms/+page.svelte
new file mode 100644
index 000000000..eb35d0c02
--- /dev/null
+++ b/apps-archived/clock/apps/web/src/routes/(app)/alarms/+page.svelte
@@ -0,0 +1,323 @@
+
+
+
+
+
+
+
+
+ {#if showOptions}
+
+ {#each dayNames as day, i}
+ toggleNewDay(i)}
+ >
+ {day}
+
+ {/each}
+
+ {/if}
+
+
+
+ {#each DEFAULT_ALARM_PRESETS as preset}
+ {@const existingAlarm = findAlarmForPreset(preset.time)}
+ {@const isActive = existingAlarm?.enabled ?? false}
+
togglePreset(preset.time, preset.label)}
+ onkeydown={(e) => e.key === 'Enter' && togglePreset(preset.time, preset.label)}
+ >
+
+ {preset.time}
+
+
+ {existingAlarm?.label || preset.label}
+
+
+ {/each}
+
+
+
+ {#if allAlarms.value.filter((a) => !DEFAULT_ALARM_PRESETS.some((p) => p.time === a.time.slice(0, 5))).length > 0}
+ {@const customAlarms = allAlarms.value.filter(
+ (a) => !DEFAULT_ALARM_PRESETS.some((p) => p.time === a.time.slice(0, 5))
+ )}
+
+
+ {$_('alarm.custom')}
+
+
+ {#each customAlarms as alarm (alarm.id)}
+
handleToggle(alarm.id)}
+ onkeydown={(e) => e.key === 'Enter' && handleToggle(alarm.id)}
+ >
+
+ {alarm.time.slice(0, 5)}
+
+
+ {alarm.label || getRepeatText(alarm.repeatDays)}
+
+
+ {/each}
+
+
+ {/if}
+
+
+ {#if showEditModal}
+
+
+
{$_('alarm.edit')}
+
+
+
+
+ {/if}
+
diff --git a/apps-archived/clock/apps/web/src/routes/(app)/feedback/+page.svelte b/apps-archived/clock/apps/web/src/routes/(app)/feedback/+page.svelte
new file mode 100644
index 000000000..83db9757e
--- /dev/null
+++ b/apps-archived/clock/apps/web/src/routes/(app)/feedback/+page.svelte
@@ -0,0 +1,32 @@
+
+
+
diff --git a/apps-archived/clock/apps/web/src/routes/(app)/help/+page.svelte b/apps-archived/clock/apps/web/src/routes/(app)/help/+page.svelte
new file mode 100644
index 000000000..535f3fa9d
--- /dev/null
+++ b/apps-archived/clock/apps/web/src/routes/(app)/help/+page.svelte
@@ -0,0 +1,32 @@
+
+
+
+ {translations.title} | Clock
+
+
+ goto('/')}
+ showGettingStarted={false}
+ showChangelog={false}
+ defaultSection="faq"
+/>
diff --git a/apps-archived/clock/apps/web/src/routes/(app)/mana/+page.svelte b/apps-archived/clock/apps/web/src/routes/(app)/mana/+page.svelte
new file mode 100644
index 000000000..55f06f19a
--- /dev/null
+++ b/apps-archived/clock/apps/web/src/routes/(app)/mana/+page.svelte
@@ -0,0 +1,6 @@
+
+
+
diff --git a/apps-archived/clock/apps/web/src/routes/(app)/profile/+page.svelte b/apps-archived/clock/apps/web/src/routes/(app)/profile/+page.svelte
new file mode 100644
index 000000000..fd83a7c3d
--- /dev/null
+++ b/apps-archived/clock/apps/web/src/routes/(app)/profile/+page.svelte
@@ -0,0 +1,6 @@
+
+
+
diff --git a/apps-archived/clock/apps/web/src/routes/(app)/settings/+page.svelte b/apps-archived/clock/apps/web/src/routes/(app)/settings/+page.svelte
new file mode 100644
index 000000000..a0ac42575
--- /dev/null
+++ b/apps-archived/clock/apps/web/src/routes/(app)/settings/+page.svelte
@@ -0,0 +1,108 @@
+
+
+
+
{$_('settings.title')}
+
+
+
+
+
+
+
{$_('settings.clockFormat')}
+
+
+
Zeitformat
+
+ setClockFormat('24h')}
+ >
+ {$_('settings.format24h')}
+
+ setClockFormat('12h')}
+ >
+ {$_('settings.format12h')}
+
+
+
+
+
+
+
+
{$_('settings.notifications')}
+
+ Benachrichtigungen werden für Wecker, Timer und Pomodoro-Sitzungen verwendet.
+
+
+
{
+ if ('Notification' in window) {
+ const permission = await Notification.requestPermission();
+ if (permission === 'granted') {
+ new Notification('Clock', {
+ body: 'Benachrichtigungen sind jetzt aktiviert!',
+ });
+ }
+ }
+ }}
+ >
+ Benachrichtigungen aktivieren
+
+
+
+
+
+
{$_('settings.sounds')}
+
+ Töne können für einzelne Wecker und Timer in deren Einstellungen angepasst werden.
+
+
+
+
v{APP_VERSION}
+
diff --git a/apps-archived/clock/apps/web/src/routes/(app)/stopwatch/+page.svelte b/apps-archived/clock/apps/web/src/routes/(app)/stopwatch/+page.svelte
new file mode 100644
index 000000000..cdb4c68b9
--- /dev/null
+++ b/apps-archived/clock/apps/web/src/routes/(app)/stopwatch/+page.svelte
@@ -0,0 +1,466 @@
+
+
+
+
+
+
+
+
+ {$_('stopwatch.new')}
+
+
+
+{#if stopwatchesStore.stopwatches.length === 0}
+
+
+
+
+
+
{$_('stopwatch.noStopwatches')}
+
{$_('stopwatch.noStopwatchesDescription')}
+
+
+
+
+ {$_('stopwatch.startFirst')}
+
+
+{:else}
+
+
+ {#if focused}
+ {@const bestLap = stopwatchesStore.getBestLap(focused.id)}
+ {@const worstLap = stopwatchesStore.getWorstLap(focused.id)}
+
+
+
+
+
+ {#if editingLabelId === focused.id}
+
+ {:else}
+
startEditLabel(focused)}
+ >
+ {focused.label}
+
+ {/if}
+
+
stopwatchesStore.delete(focused.id)}
+ >
+
+
+
+
+
+
+
+
+
+ {formatTime(focused.elapsedTime)}
+
+ {#if focused.laps.length > 0}
+
+ {focused.laps.length}
+ {$_('stopwatch.laps')}
+
+ {/if}
+
+
+
+
+ {#if focused.isRunning}
+
stopwatchesStore.pause(focused.id)}
+ >
+
+
+
+ {$_('stopwatch.stop')}
+
+
stopwatchesStore.lap(focused.id)}>
+
+
+
+ {$_('stopwatch.lap')}
+
+ {:else if focused.elapsedTime > 0}
+
stopwatchesStore.start(focused.id)}
+ >
+
+
+
+ {$_('stopwatch.continue')}
+
+
stopwatchesStore.reset(focused.id)}
+ >
+
+
+
+ {$_('stopwatch.reset')}
+
+ {:else}
+
stopwatchesStore.start(focused.id)}
+ >
+
+
+
+ {$_('stopwatch.start')}
+
+ {/if}
+
+
+
+ {#if focused.laps.length > 0}
+
+
+ {$_('stopwatch.laps')} ({focused.laps.length})
+
+
+
+ {$_('stopwatch.total')}
+
+ {formatTime(focused.elapsedTime)}
+
+
+
+ {/if}
+
+ {/if}
+
+
+ {#if otherStopwatches.length > 0}
+
+
+ {$_('stopwatch.otherStopwatches')} ({otherStopwatches.length})
+
+
+ {#each otherStopwatches as sw (sw.id)}
+
handleFocus(sw.id)}
+ onkeydown={(e) => e.key === 'Enter' && handleFocus(sw.id)}
+ role="button"
+ tabindex="0"
+ >
+
+
+
+
{
+ e.stopPropagation();
+ stopwatchesStore.delete(sw.id);
+ }}
+ >
+
+
+
+
+
+
+
+
+ {formatTime(sw.elapsedTime)}
+
+
+
+
+ {sw.label}
+
+
+
+
+ {#if sw.isRunning}
+
{
+ e.stopPropagation();
+ stopwatchesStore.pause(sw.id);
+ }}
+ >
+ {$_('stopwatch.stop')}
+
+ {:else}
+
{
+ e.stopPropagation();
+ stopwatchesStore.start(sw.id);
+ }}
+ >
+ {sw.elapsedTime > 0 ? $_('stopwatch.continue') : $_('stopwatch.start')}
+
+ {/if}
+ {#if sw.elapsedTime > 0 && !sw.isRunning}
+
{
+ e.stopPropagation();
+ stopwatchesStore.reset(sw.id);
+ }}
+ >
+
+
+
+
+ {/if}
+
+
+
+ {#if sw.laps.length > 0}
+
+ {sw.laps.length}
+
+ {/if}
+
+ {/each}
+
+
+ {/if}
+
+{/if}
+
+
diff --git a/apps-archived/clock/apps/web/src/routes/(app)/tags/+page.svelte b/apps-archived/clock/apps/web/src/routes/(app)/tags/+page.svelte
new file mode 100644
index 000000000..9ec94b7e2
--- /dev/null
+++ b/apps-archived/clock/apps/web/src/routes/(app)/tags/+page.svelte
@@ -0,0 +1,45 @@
+
+
+
+ Tags | Clock
+
+
+
+
Tags verwalten
+
+ Tags sind app-übergreifend — Änderungen gelten in allen ManaCore-Apps.
+
+
+ {#if tagsCtx.value.length === 0}
+
Keine Tags vorhanden.
+ {:else}
+
+ {#each tagsCtx.value as tag}
+
+
+ {tag.name}
+
+ {/each}
+
+ {/if}
+
+
+
diff --git a/apps-archived/clock/apps/web/src/routes/(app)/themes/+page.svelte b/apps-archived/clock/apps/web/src/routes/(app)/themes/+page.svelte
new file mode 100644
index 000000000..ffad20085
--- /dev/null
+++ b/apps-archived/clock/apps/web/src/routes/(app)/themes/+page.svelte
@@ -0,0 +1,30 @@
+
+
+
+
Alle Themes
+
+
+ {#each THEME_VARIANTS as variant}
+ {@const def = THEME_DEFINITIONS[variant]}
+
theme.setVariant(variant)}
+ >
+
+
{def.icon}
+
+
{def.label}
+
{def.description}
+
+
+ {#if theme.variant === variant}
+ ✓ Aktiv
+ {/if}
+
+ {/each}
+
+
diff --git a/apps-archived/clock/apps/web/src/routes/(app)/timers/+page.svelte b/apps-archived/clock/apps/web/src/routes/(app)/timers/+page.svelte
new file mode 100644
index 000000000..cda333ef2
--- /dev/null
+++ b/apps-archived/clock/apps/web/src/routes/(app)/timers/+page.svelte
@@ -0,0 +1,292 @@
+
+
+
+
+
+
+
+
+
+
+ {#each QUICK_TIMER_PRESETS as preset}
+ setPreset(preset.seconds)}>
+ {preset.label}
+
+ {/each}
+
+
+ {#if allTimers.length > 0}
+
+
+
+ Aktiv ({allTimers.length})
+
+
+ {#each allTimers as timer (timer.id)}
+ {@const isLocal = isLocalTimer(timer)}
+
+
+
+ {getTimerDisplay(timer)}
+
+
{
+ e.stopPropagation();
+ handleDelete(timer.id, isLocal);
+ }}
+ >
+
+
+
+
+
+
{timer.label}
+
+
+ {#if timer.status === 'running'}
+ handlePause(timer.id, isLocal)}
+ >
+ Pause
+
+ {:else}
+ handleStart(timer.id, isLocal)}
+ >
+ {timer.status === 'finished' ? 'Neu' : 'Start'}
+
+ {/if}
+ handleReset(timer.id, isLocal)}
+ >
+ Reset
+
+
+
+ {/each}
+
+
+ {/if}
+
diff --git a/apps-archived/clock/apps/web/src/routes/(app)/world-clock/+page.svelte b/apps-archived/clock/apps/web/src/routes/(app)/world-clock/+page.svelte
new file mode 100644
index 000000000..7793e6b85
--- /dev/null
+++ b/apps-archived/clock/apps/web/src/routes/(app)/world-clock/+page.svelte
@@ -0,0 +1,368 @@
+
+
+
+ {#snippet actions()}
+
+ (showMap = !showMap)}
+ title={showMap ? 'Karte ausblenden' : 'Karte anzeigen'}
+ >
+
+
+
+ + {$_('worldClock.add')}
+
+
+ {/snippet}
+
+
+
+
+ {#if showMap}
+
+
+
+
+
+ Klicke auf eine Stadt um sie hinzuzufügen
+
+
+ {/if}
+
+
+ {#if allWorldClocks.value.length === 0}
+
+
{$_('worldClock.noClocks')}
+
+ {$_('worldClock.add')}
+
+
+ {:else}
+
+ {#each allWorldClocks.value as clock (clock.id)}
+ {@const isDay = isDaytime(clock.timezone)}
+
+
+
removeCity(clock.id)}
+ >
+
+
+
+
+
+
+
+ {isDay ? 'Tag' : 'Nacht'}
+ {clock.cityName}
+
+
+
+
+ {getTimeForTimezone(clock.timezone)}
+
+
+
+
+
+ {getDateForTimezone(clock.timezone)}
+
+
+ {getOffsetText(clock.timezone)}
+
+
+
+ {/each}
+
+ {/if}
+
+
+ {#if showAddModal}
+
+
+
+
{$_('worldClock.add')}
+
+
+
+
+
+
+
+
+
+
+
+
+ {#each filteredTimezones as tz}
+ {@const alreadyAdded = allWorldClocks.value.some((wc) => wc.timezone === tz.timezone)}
+
addCity(tz.timezone, tz.city)}
+ >
+
+
{tz.city}
+
{tz.timezone}
+
+
+
{getTimeForTimezone(tz.timezone)}
+
{tz.region}
+
+
+ {/each}
+
+ {#if filteredTimezones.length === 0}
+
+ Keine Ergebnisse für "{searchQuery}"
+
+ {/if}
+
+
+
+ {/if}
+
+
+
diff --git a/apps-archived/clock/apps/web/src/routes/(auth)/forgot-password/+page.svelte b/apps-archived/clock/apps/web/src/routes/(auth)/forgot-password/+page.svelte
new file mode 100644
index 000000000..4403962d9
--- /dev/null
+++ b/apps-archived/clock/apps/web/src/routes/(auth)/forgot-password/+page.svelte
@@ -0,0 +1,35 @@
+
+
+
diff --git a/apps-archived/clock/apps/web/src/routes/(auth)/login/+page.svelte b/apps-archived/clock/apps/web/src/routes/(auth)/login/+page.svelte
new file mode 100644
index 000000000..83a406630
--- /dev/null
+++ b/apps-archived/clock/apps/web/src/routes/(auth)/login/+page.svelte
@@ -0,0 +1,73 @@
+
+
+
+ {translations.title} | Clock
+
+
+ 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}
+/>
diff --git a/apps-archived/clock/apps/web/src/routes/(auth)/register/+page.svelte b/apps-archived/clock/apps/web/src/routes/(auth)/register/+page.svelte
new file mode 100644
index 000000000..2af0a135b
--- /dev/null
+++ b/apps-archived/clock/apps/web/src/routes/(auth)/register/+page.svelte
@@ -0,0 +1,52 @@
+
+
+
+ {translations.title} | Clock
+
+
+
diff --git a/apps-archived/clock/apps/web/src/routes/(auth)/reset-password/+page.svelte b/apps-archived/clock/apps/web/src/routes/(auth)/reset-password/+page.svelte
new file mode 100644
index 000000000..54d42004f
--- /dev/null
+++ b/apps-archived/clock/apps/web/src/routes/(auth)/reset-password/+page.svelte
@@ -0,0 +1,174 @@
+
+
+
+ Reset Password - Clock
+
+
+
+
+
+
+
+
+
Reset Password
+
+ {#if success}Password reset successfully
+ {:else if hasToken}Enter your new password
+ {:else}Invalid or missing token{/if}
+
+
+
+ {#if success}
+
+
+
✅
+
+ Your password has been reset successfully. You will be redirected to the login page
+ shortly.
+
+
+ Go to login
+
+
+
+ {:else if hasToken}
+
+ {:else}
+
+ {/if}
+
+
+
diff --git a/apps-archived/clock/apps/web/src/routes/+layout.svelte b/apps-archived/clock/apps/web/src/routes/+layout.svelte
new file mode 100644
index 000000000..489b4af31
--- /dev/null
+++ b/apps-archived/clock/apps/web/src/routes/+layout.svelte
@@ -0,0 +1,47 @@
+
+
+
+
+{#if $isLocaleLoading || loading}
+
+{:else}
+
+ {@render children()}
+
+{/if}
diff --git a/apps-archived/clock/apps/web/src/routes/+layout.ts b/apps-archived/clock/apps/web/src/routes/+layout.ts
new file mode 100644
index 000000000..ad6cddb06
--- /dev/null
+++ b/apps-archived/clock/apps/web/src/routes/+layout.ts
@@ -0,0 +1,2 @@
+// Disable SSR — all data is local-first (IndexedDB + mana-sync)
+export const ssr = false;
diff --git a/apps-archived/clock/apps/web/src/routes/health/+server.ts b/apps-archived/clock/apps/web/src/routes/health/+server.ts
new file mode 100644
index 000000000..91a4d2956
--- /dev/null
+++ b/apps-archived/clock/apps/web/src/routes/health/+server.ts
@@ -0,0 +1,10 @@
+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(),
+ });
+};
diff --git a/apps-archived/clock/apps/web/src/routes/offline/+page.svelte b/apps-archived/clock/apps/web/src/routes/offline/+page.svelte
new file mode 100644
index 000000000..688302729
--- /dev/null
+++ b/apps-archived/clock/apps/web/src/routes/offline/+page.svelte
@@ -0,0 +1,9 @@
+
+
+
diff --git a/apps-archived/clock/apps/web/src/routes/offline/+page.ts b/apps-archived/clock/apps/web/src/routes/offline/+page.ts
new file mode 100644
index 000000000..189f71e2e
--- /dev/null
+++ b/apps-archived/clock/apps/web/src/routes/offline/+page.ts
@@ -0,0 +1 @@
+export const prerender = true;
diff --git a/apps-archived/clock/apps/web/svelte.config.js b/apps-archived/clock/apps/web/svelte.config.js
new file mode 100644
index 000000000..f290ef5a6
--- /dev/null
+++ b/apps-archived/clock/apps/web/svelte.config.js
@@ -0,0 +1,21 @@
+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;
diff --git a/apps-archived/clock/apps/web/tsconfig.json b/apps-archived/clock/apps/web/tsconfig.json
new file mode 100644
index 000000000..a8f10c8e3
--- /dev/null
+++ b/apps-archived/clock/apps/web/tsconfig.json
@@ -0,0 +1,14 @@
+{
+ "extends": "./.svelte-kit/tsconfig.json",
+ "compilerOptions": {
+ "allowJs": true,
+ "checkJs": true,
+ "esModuleInterop": true,
+ "forceConsistentCasingInFileNames": true,
+ "resolveJsonModule": true,
+ "skipLibCheck": true,
+ "sourceMap": true,
+ "strict": true,
+ "moduleResolution": "bundler"
+ }
+}
diff --git a/apps-archived/clock/apps/web/vite.config.ts b/apps-archived/clock/apps/web/vite.config.ts
new file mode 100644
index 000000000..b82ef0e7e
--- /dev/null
+++ b/apps-archived/clock/apps/web/vite.config.ts
@@ -0,0 +1,33 @@
+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(),
+ },
+});
diff --git a/apps-archived/clock/packages/shared/package.json b/apps-archived/clock/packages/shared/package.json
new file mode 100644
index 000000000..8b9961090
--- /dev/null
+++ b/apps-archived/clock/packages/shared/package.json
@@ -0,0 +1,19 @@
+{
+ "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"
+ }
+}
diff --git a/apps-archived/clock/packages/shared/src/constants/index.ts b/apps-archived/clock/packages/shared/src/constants/index.ts
new file mode 100644
index 000000000..205c25895
--- /dev/null
+++ b/apps-archived/clock/packages/shared/src/constants/index.ts
@@ -0,0 +1,151 @@
+// Popular timezones with city names and coordinates for map display
+export const POPULAR_TIMEZONES = [
+ {
+ timezone: 'America/New_York',
+ city: 'New York',
+ region: 'Americas',
+ lat: 40.7128,
+ lng: -74.006,
+ },
+ {
+ timezone: 'America/Los_Angeles',
+ city: 'Los Angeles',
+ region: 'Americas',
+ lat: 34.0522,
+ lng: -118.2437,
+ },
+ { timezone: 'America/Chicago', city: 'Chicago', region: 'Americas', lat: 41.8781, lng: -87.6298 },
+ { timezone: 'America/Toronto', city: 'Toronto', region: 'Americas', lat: 43.6532, lng: -79.3832 },
+ {
+ timezone: 'America/Sao_Paulo',
+ city: 'São Paulo',
+ region: 'Americas',
+ lat: -23.5505,
+ lng: -46.6333,
+ },
+ {
+ timezone: 'America/Mexico_City',
+ city: 'Mexico City',
+ region: 'Americas',
+ lat: 19.4326,
+ lng: -99.1332,
+ },
+ {
+ timezone: 'America/Buenos_Aires',
+ city: 'Buenos Aires',
+ region: 'Americas',
+ lat: -34.6037,
+ lng: -58.3816,
+ },
+ {
+ timezone: 'America/Vancouver',
+ city: 'Vancouver',
+ region: 'Americas',
+ lat: 49.2827,
+ lng: -123.1207,
+ },
+ { timezone: 'Europe/London', city: 'London', region: 'Europe', lat: 51.5074, lng: -0.1278 },
+ { timezone: 'Europe/Paris', city: 'Paris', region: 'Europe', lat: 48.8566, lng: 2.3522 },
+ { timezone: 'Europe/Berlin', city: 'Berlin', region: 'Europe', lat: 52.52, lng: 13.405 },
+ { timezone: 'Europe/Rome', city: 'Rome', region: 'Europe', lat: 41.9028, lng: 12.4964 },
+ { timezone: 'Europe/Madrid', city: 'Madrid', region: 'Europe', lat: 40.4168, lng: -3.7038 },
+ { timezone: 'Europe/Amsterdam', city: 'Amsterdam', region: 'Europe', lat: 52.3676, lng: 4.9041 },
+ { timezone: 'Europe/Vienna', city: 'Vienna', region: 'Europe', lat: 48.2082, lng: 16.3738 },
+ { timezone: 'Europe/Zurich', city: 'Zurich', region: 'Europe', lat: 47.3769, lng: 8.5417 },
+ { timezone: 'Europe/Moscow', city: 'Moscow', region: 'Europe', lat: 55.7558, lng: 37.6173 },
+ { timezone: 'Europe/Stockholm', city: 'Stockholm', region: 'Europe', lat: 59.3293, lng: 18.0686 },
+ { timezone: 'Europe/Istanbul', city: 'Istanbul', region: 'Europe', lat: 41.0082, lng: 28.9784 },
+ { timezone: 'Asia/Tokyo', city: 'Tokyo', region: 'Asia', lat: 35.6762, lng: 139.6503 },
+ { timezone: 'Asia/Shanghai', city: 'Shanghai', region: 'Asia', lat: 31.2304, lng: 121.4737 },
+ { timezone: 'Asia/Hong_Kong', city: 'Hong Kong', region: 'Asia', lat: 22.3193, lng: 114.1694 },
+ { timezone: 'Asia/Singapore', city: 'Singapore', region: 'Asia', lat: 1.3521, lng: 103.8198 },
+ { timezone: 'Asia/Seoul', city: 'Seoul', region: 'Asia', lat: 37.5665, lng: 126.978 },
+ { timezone: 'Asia/Mumbai', city: 'Mumbai', region: 'Asia', lat: 19.076, lng: 72.8777 },
+ { timezone: 'Asia/Dubai', city: 'Dubai', region: 'Asia', lat: 25.2048, lng: 55.2708 },
+ { timezone: 'Asia/Bangkok', city: 'Bangkok', region: 'Asia', lat: 13.7563, lng: 100.5018 },
+ { timezone: 'Asia/Jakarta', city: 'Jakarta', region: 'Asia', lat: -6.2088, lng: 106.8456 },
+ { timezone: 'Australia/Sydney', city: 'Sydney', region: 'Oceania', lat: -33.8688, lng: 151.2093 },
+ {
+ timezone: 'Australia/Melbourne',
+ city: 'Melbourne',
+ region: 'Oceania',
+ lat: -37.8136,
+ lng: 144.9631,
+ },
+ {
+ timezone: 'Pacific/Auckland',
+ city: 'Auckland',
+ region: 'Oceania',
+ lat: -36.8485,
+ lng: 174.7633,
+ },
+ { timezone: 'Africa/Cairo', city: 'Cairo', region: 'Africa', lat: 30.0444, lng: 31.2357 },
+ {
+ timezone: 'Africa/Johannesburg',
+ city: 'Johannesburg',
+ region: 'Africa',
+ lat: -26.2041,
+ lng: 28.0473,
+ },
+ { timezone: 'Africa/Lagos', city: 'Lagos', region: 'Africa', lat: 6.5244, lng: 3.3792 },
+] as const;
+
+// Available alarm sounds
+export const ALARM_SOUNDS = [
+ { id: 'default', name: 'Default', nameDE: 'Standard' },
+ { id: 'gentle', name: 'Gentle', nameDE: 'Sanft' },
+ { id: 'classic', name: 'Classic', nameDE: 'Klassisch' },
+ { id: 'digital', name: 'Digital', nameDE: 'Digital' },
+ { id: 'nature', name: 'Nature', nameDE: 'Natur' },
+ { id: 'chime', name: 'Chime', nameDE: 'Glockenspiel' },
+] as const;
+
+// 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;
diff --git a/apps-archived/clock/packages/shared/src/index.ts b/apps-archived/clock/packages/shared/src/index.ts
new file mode 100644
index 000000000..33c8572a1
--- /dev/null
+++ b/apps-archived/clock/packages/shared/src/index.ts
@@ -0,0 +1,2 @@
+export * from './types';
+export * from './constants';
diff --git a/apps-archived/clock/packages/shared/src/types/alarm.ts b/apps-archived/clock/packages/shared/src/types/alarm.ts
new file mode 100644
index 000000000..212e10059
--- /dev/null
+++ b/apps-archived/clock/packages/shared/src/types/alarm.ts
@@ -0,0 +1,55 @@
+export interface Alarm {
+ id: string;
+ userId: string;
+ label: string | null;
+ time: string; // HH:MM:SS format
+ enabled: boolean;
+ repeatDays: number[] | null; // [0-6] where 0 = Sunday
+ snoozeMinutes: number | null;
+ sound: string | null;
+ vibrate: boolean | null;
+ createdAt: string;
+ updatedAt: string;
+}
+
+export interface CreateAlarmInput {
+ label?: string;
+ time: string;
+ enabled?: boolean;
+ repeatDays?: number[];
+ snoozeMinutes?: number;
+ sound?: string;
+ vibrate?: boolean;
+}
+
+export interface UpdateAlarmInput {
+ label?: string;
+ time?: string;
+ enabled?: boolean;
+ repeatDays?: number[];
+ snoozeMinutes?: number;
+ sound?: string;
+ vibrate?: boolean;
+}
+
+export type RepeatDay = 0 | 1 | 2 | 3 | 4 | 5 | 6;
+
+export const REPEAT_DAY_LABELS = {
+ 0: 'Sun',
+ 1: 'Mon',
+ 2: 'Tue',
+ 3: 'Wed',
+ 4: 'Thu',
+ 5: 'Fri',
+ 6: 'Sat',
+} as const;
+
+export const REPEAT_DAY_LABELS_DE = {
+ 0: 'So',
+ 1: 'Mo',
+ 2: 'Di',
+ 3: 'Mi',
+ 4: 'Do',
+ 5: 'Fr',
+ 6: 'Sa',
+} as const;
diff --git a/apps-archived/clock/packages/shared/src/types/index.ts b/apps-archived/clock/packages/shared/src/types/index.ts
new file mode 100644
index 000000000..1bd20351b
--- /dev/null
+++ b/apps-archived/clock/packages/shared/src/types/index.ts
@@ -0,0 +1,4 @@
+export * from './alarm';
+export * from './timer';
+export * from './world-clock';
+export * from './preset';
diff --git a/apps-archived/clock/packages/shared/src/types/preset.ts b/apps-archived/clock/packages/shared/src/types/preset.ts
new file mode 100644
index 000000000..948adf37e
--- /dev/null
+++ b/apps-archived/clock/packages/shared/src/types/preset.ts
@@ -0,0 +1,42 @@
+export type PresetType = 'timer' | 'pomodoro';
+
+export interface PresetSettings {
+ // For pomodoro presets
+ workDuration?: number;
+ breakDuration?: number;
+ longBreakDuration?: number;
+ sessionsBeforeLongBreak?: number;
+ // For timer presets
+ sound?: string;
+}
+
+export interface Preset {
+ id: string;
+ userId: string;
+ type: PresetType;
+ name: string;
+ durationSeconds: number;
+ settings: PresetSettings | null;
+ createdAt: string;
+}
+
+export interface CreatePresetInput {
+ type: PresetType;
+ name: string;
+ durationSeconds: number;
+ settings?: PresetSettings;
+}
+
+export interface UpdatePresetInput {
+ name?: string;
+ durationSeconds?: number;
+ settings?: PresetSettings;
+}
+
+// Default pomodoro settings
+export const DEFAULT_POMODORO_SETTINGS: PresetSettings = {
+ workDuration: 25 * 60, // 25 minutes
+ breakDuration: 5 * 60, // 5 minutes
+ longBreakDuration: 15 * 60, // 15 minutes
+ sessionsBeforeLongBreak: 4,
+};
diff --git a/apps-archived/clock/packages/shared/src/types/timer.ts b/apps-archived/clock/packages/shared/src/types/timer.ts
new file mode 100644
index 000000000..221bb56ef
--- /dev/null
+++ b/apps-archived/clock/packages/shared/src/types/timer.ts
@@ -0,0 +1,49 @@
+export type TimerStatus = 'idle' | 'running' | 'paused' | 'finished';
+
+export interface Timer {
+ id: string;
+ userId: string;
+ label: string | null;
+ durationSeconds: number;
+ remainingSeconds: number | null;
+ status: TimerStatus;
+ startedAt: string | null;
+ pausedAt: string | null;
+ sound: string | null;
+ createdAt: string;
+ updatedAt: string;
+}
+
+export interface CreateTimerInput {
+ label?: string;
+ durationSeconds: number;
+ sound?: string;
+}
+
+export interface UpdateTimerInput {
+ label?: string;
+ durationSeconds?: number;
+ sound?: string;
+}
+
+export function formatDuration(seconds: number): string {
+ const hours = Math.floor(seconds / 3600);
+ const minutes = Math.floor((seconds % 3600) / 60);
+ const secs = seconds % 60;
+
+ if (hours > 0) {
+ return `${hours}:${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
+ }
+ return `${minutes}:${secs.toString().padStart(2, '0')}`;
+}
+
+export function parseDuration(formatted: string): number {
+ const parts = formatted.split(':').map(Number);
+ if (parts.length === 3) {
+ return parts[0] * 3600 + parts[1] * 60 + parts[2];
+ }
+ if (parts.length === 2) {
+ return parts[0] * 60 + parts[1];
+ }
+ return parts[0];
+}
diff --git a/apps-archived/clock/packages/shared/src/types/world-clock.ts b/apps-archived/clock/packages/shared/src/types/world-clock.ts
new file mode 100644
index 000000000..7a2ae55a7
--- /dev/null
+++ b/apps-archived/clock/packages/shared/src/types/world-clock.ts
@@ -0,0 +1,18 @@
+export interface WorldClock {
+ id: string;
+ userId: string;
+ timezone: string; // IANA timezone e.g. 'America/New_York'
+ cityName: string;
+ sortOrder: number;
+ createdAt: string;
+}
+
+export interface CreateWorldClockInput {
+ timezone: string;
+ cityName: string;
+}
+
+export interface TimezoneInfo {
+ timezone: string;
+ city: string;
+}
diff --git a/apps-archived/clock/packages/shared/tsconfig.json b/apps-archived/clock/packages/shared/tsconfig.json
new file mode 100644
index 000000000..b17efe376
--- /dev/null
+++ b/apps-archived/clock/packages/shared/tsconfig.json
@@ -0,0 +1,14 @@
+{
+ "compilerOptions": {
+ "target": "ES2021",
+ "module": "ESNext",
+ "moduleResolution": "bundler",
+ "strict": true,
+ "skipLibCheck": true,
+ "declaration": true,
+ "outDir": "./dist",
+ "rootDir": "./src"
+ },
+ "include": ["src/**/*"],
+ "exclude": ["node_modules", "dist"]
+}