From 73e3fdbbed4e8cdff869dc342d2a0b1d26410df1 Mon Sep 17 00:00:00 2001 From: Till JS Date: Tue, 14 Apr 2026 19:50:13 +0200 Subject: [PATCH] feat(meditate): add meditation module with presets, sessions, breathing UI Local-first module with meditatePresets/Sessions/Settings tables, hub ListView with stats + recent sessions, and SessionPlayer with BreathingCircle + MoodPicker. Route at /meditate. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../src/lib/modules/meditate/ListView.svelte | 205 +++++++ .../src/lib/modules/meditate/collections.ts | 23 + .../components/BreathingCircle.svelte | 181 ++++++ .../meditate/components/MoodPicker.svelte | 86 +++ .../meditate/components/PresetCard.svelte | 125 ++++ .../meditate/components/SessionCard.svelte | 110 ++++ .../meditate/components/SessionPlayer.svelte | 560 ++++++++++++++++++ .../meditate/components/StatsOverview.svelte | 80 +++ .../lib/modules/meditate/default-presets.ts | 83 +++ .../web/src/lib/modules/meditate/index.ts | 58 ++ .../src/lib/modules/meditate/module.config.ts | 6 + .../web/src/lib/modules/meditate/queries.ts | 184 ++++++ .../web/src/lib/modules/meditate/types.ts | 143 +++++ .../src/routes/(app)/meditate/+layout.svelte | 13 + .../src/routes/(app)/meditate/+page.svelte | 149 +++++ .../(app)/meditate/history/+page.svelte | 155 +++++ 16 files changed, 2161 insertions(+) create mode 100644 apps/mana/apps/web/src/lib/modules/meditate/ListView.svelte create mode 100644 apps/mana/apps/web/src/lib/modules/meditate/collections.ts create mode 100644 apps/mana/apps/web/src/lib/modules/meditate/components/BreathingCircle.svelte create mode 100644 apps/mana/apps/web/src/lib/modules/meditate/components/MoodPicker.svelte create mode 100644 apps/mana/apps/web/src/lib/modules/meditate/components/PresetCard.svelte create mode 100644 apps/mana/apps/web/src/lib/modules/meditate/components/SessionCard.svelte create mode 100644 apps/mana/apps/web/src/lib/modules/meditate/components/SessionPlayer.svelte create mode 100644 apps/mana/apps/web/src/lib/modules/meditate/components/StatsOverview.svelte create mode 100644 apps/mana/apps/web/src/lib/modules/meditate/default-presets.ts create mode 100644 apps/mana/apps/web/src/lib/modules/meditate/index.ts create mode 100644 apps/mana/apps/web/src/lib/modules/meditate/module.config.ts create mode 100644 apps/mana/apps/web/src/lib/modules/meditate/queries.ts create mode 100644 apps/mana/apps/web/src/lib/modules/meditate/types.ts create mode 100644 apps/mana/apps/web/src/routes/(app)/meditate/+layout.svelte create mode 100644 apps/mana/apps/web/src/routes/(app)/meditate/+page.svelte create mode 100644 apps/mana/apps/web/src/routes/(app)/meditate/history/+page.svelte diff --git a/apps/mana/apps/web/src/lib/modules/meditate/ListView.svelte b/apps/mana/apps/web/src/lib/modules/meditate/ListView.svelte new file mode 100644 index 000000000..343eb7762 --- /dev/null +++ b/apps/mana/apps/web/src/lib/modules/meditate/ListView.svelte @@ -0,0 +1,205 @@ + + + +{#if activePreset} + +{/if} + +
+ + {#if sessions.length > 0} +
+
+ {todayMinutes} + min heute +
+ {#if streak > 0} +
+ {streak} + {streak === 1 ? 'Tag' : 'Tage'} Streak +
+ {/if} +
+ {/if} + + +
+ {#each presets as preset (preset.id)} + + {/each} +
+ + + {#if todaySessions.length > 0} +
Heute
+
+ {#each todaySessions as session (session.id)} + {@const preset = presets.find((p) => p.id === session.presetId)} +
+ {preset?.name ?? CATEGORY_LABELS[session.category].de} + {formatDuration(session.durationSec)} +
+ {/each} +
+ {/if} +
+ + diff --git a/apps/mana/apps/web/src/lib/modules/meditate/collections.ts b/apps/mana/apps/web/src/lib/modules/meditate/collections.ts new file mode 100644 index 000000000..d9a449891 --- /dev/null +++ b/apps/mana/apps/web/src/lib/modules/meditate/collections.ts @@ -0,0 +1,23 @@ +/** + * Meditate module β€” collection accessors and guest seed data. + * + * Tables: meditatePresets, meditateSessions, meditateSettings. + */ + +import { db } from '$lib/data/database'; +import type { LocalMeditatePreset, LocalMeditateSession, LocalMeditateSettings } from './types'; +import { DEFAULT_PRESETS } from './default-presets'; + +// ─── Collection Accessors ─────────────────────────────────── + +export const meditatePresetTable = db.table('meditatePresets'); +export const meditateSessionTable = db.table('meditateSessions'); +export const meditateSettingsTable = db.table('meditateSettings'); + +// ─── Guest Seed ───────────────────────────────────────────── + +export const MEDITATE_GUEST_SEED = { + meditatePresets: DEFAULT_PRESETS as unknown as Record[], + meditateSessions: [] as Record[], + meditateSettings: [] as Record[], +}; diff --git a/apps/mana/apps/web/src/lib/modules/meditate/components/BreathingCircle.svelte b/apps/mana/apps/web/src/lib/modules/meditate/components/BreathingCircle.svelte new file mode 100644 index 000000000..7a20743a6 --- /dev/null +++ b/apps/mana/apps/web/src/lib/modules/meditate/components/BreathingCircle.svelte @@ -0,0 +1,181 @@ + + + +
+
+
{phaseLabel}
+ {#if phaseDuration > 0} +
{phaseCountdown}
+ {/if} +
+
+
+
+ + diff --git a/apps/mana/apps/web/src/lib/modules/meditate/components/MoodPicker.svelte b/apps/mana/apps/web/src/lib/modules/meditate/components/MoodPicker.svelte new file mode 100644 index 000000000..6f9cbca6a --- /dev/null +++ b/apps/mana/apps/web/src/lib/modules/meditate/components/MoodPicker.svelte @@ -0,0 +1,86 @@ + + + +
+ {label} +
+ {#each moods as mood} + + {/each} +
+
+ + diff --git a/apps/mana/apps/web/src/lib/modules/meditate/components/PresetCard.svelte b/apps/mana/apps/web/src/lib/modules/meditate/components/PresetCard.svelte new file mode 100644 index 000000000..19673fbb8 --- /dev/null +++ b/apps/mana/apps/web/src/lib/modules/meditate/components/PresetCard.svelte @@ -0,0 +1,125 @@ + + + + + + diff --git a/apps/mana/apps/web/src/lib/modules/meditate/components/SessionCard.svelte b/apps/mana/apps/web/src/lib/modules/meditate/components/SessionCard.svelte new file mode 100644 index 000000000..f2863bf38 --- /dev/null +++ b/apps/mana/apps/web/src/lib/modules/meditate/components/SessionCard.svelte @@ -0,0 +1,110 @@ + + + +
+
+
{name}
+
+ {date} + Β· + {duration} + {#if !session.completed} + Β· + abgebrochen + {/if} +
+
+ {#if session.moodBefore || session.moodAfter} +
+ {#if session.moodBefore} + {moodEmojis[session.moodBefore]} + {/if} + {#if session.moodBefore && session.moodAfter} + β†’ + {/if} + {#if session.moodAfter} + {moodEmojis[session.moodAfter]} + {/if} +
+ {/if} +
+ + diff --git a/apps/mana/apps/web/src/lib/modules/meditate/components/SessionPlayer.svelte b/apps/mana/apps/web/src/lib/modules/meditate/components/SessionPlayer.svelte new file mode 100644 index 000000000..e622f5845 --- /dev/null +++ b/apps/mana/apps/web/src/lib/modules/meditate/components/SessionPlayer.svelte @@ -0,0 +1,560 @@ + + + +
+ + + + {#if phase === 'mood_before'} + +
+

{preset.name}

+

Wie fΓΌhlst du dich gerade?

+
+ +
+ +
+ {:else if phase === 'countdown'} + +
+
{countdownValue}
+

Mach es dir bequem…

+
+ {:else if phase === 'active'} + +
+ {#if isBreathing && preset.breathPattern} + + {:else if isBodyScan} +
+
+ {currentBodyScanStep + 1} / {bodyScanStepCount} +
+

{bodyScanStepText}

+
+ {:else} + +
+ {/if} + +
{displayTime}
+ + +
+
+
+ + +
+ + +
+
+ {:else if phase === 'finishing'} + +
+

Session beenden?

+

+ Du hast {formatDuration(Math.round(totalDuration - timeRemaining))} meditiert. +

+
+ + +
+
+ {:else if phase === 'mood_after'} + +
+

Geschafft!

+

+ {formatDuration(Math.round(totalDuration - timeRemaining))} meditiert +

+
+ +
+
+ +
+ +
+ {/if} +
+ + diff --git a/apps/mana/apps/web/src/lib/modules/meditate/components/StatsOverview.svelte b/apps/mana/apps/web/src/lib/modules/meditate/components/StatsOverview.svelte new file mode 100644 index 000000000..b829d31c5 --- /dev/null +++ b/apps/mana/apps/web/src/lib/modules/meditate/components/StatsOverview.svelte @@ -0,0 +1,80 @@ + + + +
+
+ {streak} + {streak === 1 ? 'Tag' : 'Tage'} Streak +
+
+ {weekMinutes} + Min. diese Woche +
+
+ {weekSessions} + Sessions +
+
+ {totalMinutes} + Min. gesamt +
+
+ + diff --git a/apps/mana/apps/web/src/lib/modules/meditate/default-presets.ts b/apps/mana/apps/web/src/lib/modules/meditate/default-presets.ts new file mode 100644 index 000000000..ef3d6fed2 --- /dev/null +++ b/apps/mana/apps/web/src/lib/modules/meditate/default-presets.ts @@ -0,0 +1,83 @@ +/** + * Meditate module β€” seed presets for new users. + * + * Five presets covering the three categories: silence, breathing, bodyscan. + */ + +import type { LocalMeditatePreset } from './types'; + +export const DEFAULT_PRESETS: LocalMeditatePreset[] = [ + { + id: 'meditate-preset-silence', + name: 'Stille Meditation', + description: + 'Setze dich hin, schließe die Augen und beobachte deinen Atem ohne ihn zu steuern.', + category: 'silence', + breathPattern: null, + bodyScanSteps: null, + defaultDurationSec: 600, + isPreset: true, + isArchived: false, + order: 0, + }, + { + id: 'meditate-preset-box', + name: 'Box Breathing', + description: + 'GleichmÀßiges Atmen im 4-4-4-4-Rhythmus. Beruhigt das Nervensystem und schΓ€rft den Fokus.', + category: 'breathing', + breathPattern: { inhale: 4, hold1: 4, exhale: 4, hold2: 4 }, + bodyScanSteps: null, + defaultDurationSec: 300, + isPreset: true, + isArchived: false, + order: 1, + }, + { + id: 'meditate-preset-478', + name: '4-7-8 Entspannung', + description: + 'Tiefe Entspannung: 4s einatmen, 7s halten, 8s langsam ausatmen. Ideal zum Einschlafen.', + category: 'breathing', + breathPattern: { inhale: 4, hold1: 7, exhale: 8, hold2: 0 }, + bodyScanSteps: null, + defaultDurationSec: 300, + isPreset: true, + isArchived: false, + order: 2, + }, + { + id: 'meditate-preset-wimhof', + name: 'Wim Hof', + description: + 'Kraftvolles Ein- und Ausatmen in schnellem Rhythmus. Energetisierend und aktivierend.', + category: 'breathing', + breathPattern: { inhale: 2, hold1: 0, exhale: 2, hold2: 0 }, + bodyScanSteps: null, + defaultDurationSec: 300, + isPreset: true, + isArchived: false, + order: 3, + }, + { + id: 'meditate-preset-bodyscan', + name: 'Body Scan', + description: 'Wandere mit deiner Aufmerksamkeit durch den KΓΆrper, von den Füßen bis zum Kopf.', + category: 'bodyscan', + breathPattern: null, + bodyScanSteps: [ + 'Füße β€” SpΓΌre den Kontakt zum Boden. Lass die Spannung los.', + 'Unterschenkel & Knie β€” Lass die Muskeln weich werden.', + 'Oberschenkel & HΓΌfte β€” SpΓΌre die Schwere. Lass los.', + 'Bauch & unterer RΓΌcken β€” Atme in den Bauch. Lass ihn weich werden.', + 'Brust & oberer RΓΌcken β€” SpΓΌre den Atem. Lass die Schultern sinken.', + 'HΓ€nde & Arme β€” Lass die Finger entspannen. SpΓΌre die WΓ€rme.', + 'Nacken & Schultern β€” LΓΆse alle Anspannung. Lass den Kopf schwer werden.', + 'Gesicht & Kopf β€” Stirn entspannen, Kiefer lockern, Augen ruhen lassen.', + ], + defaultDurationSec: 600, + isPreset: true, + isArchived: false, + order: 4, + }, +]; diff --git a/apps/mana/apps/web/src/lib/modules/meditate/index.ts b/apps/mana/apps/web/src/lib/modules/meditate/index.ts new file mode 100644 index 000000000..5eee8b1df --- /dev/null +++ b/apps/mana/apps/web/src/lib/modules/meditate/index.ts @@ -0,0 +1,58 @@ +/** + * Meditate module β€” barrel exports. + */ + +// ─── Stores ────────────────────────────────────────────── +export { meditateStore } from './stores/meditate.svelte'; + +// ─── Queries ───────────────────────────────────────────── +export { + useAllPresets, + useAllSessions, + useSettings, + toMeditatePreset, + toMeditateSession, + toMeditateSettings, + todayDateStr, + getSessionsForDate, + getTodaySessions, + getTodayMinutes, + getWeekSessionCount, + getWeekMinutes, + getCurrentStreak, + getTotalSessions, + getTotalMinutes, + formatDuration, + formatDurationLong, + getDefaultSettings, +} from './queries'; + +// ─── Collections ───────────────────────────────────────── +export { + meditatePresetTable, + meditateSessionTable, + meditateSettingsTable, + MEDITATE_GUEST_SEED, +} from './collections'; + +// ─── Types ─────────────────────────────────────────────── +export { + MEDITATE_CATEGORIES, + CATEGORY_LABELS, + BELL_SOUND_LABELS, + BREATH_PHASE_LABELS, + DEFAULT_SETTINGS, +} from './types'; +export type { + MeditateCategory, + BellSound, + BackgroundTheme, + BreathPattern, + BreathPhase, + LocalMeditatePreset, + LocalMeditateSession, + LocalMeditateSettings, + MeditatePreset, + MeditateSession, + MeditateSettings, +} from './types'; diff --git a/apps/mana/apps/web/src/lib/modules/meditate/module.config.ts b/apps/mana/apps/web/src/lib/modules/meditate/module.config.ts new file mode 100644 index 000000000..d938d17a3 --- /dev/null +++ b/apps/mana/apps/web/src/lib/modules/meditate/module.config.ts @@ -0,0 +1,6 @@ +import type { ModuleConfig } from '$lib/data/module-registry'; + +export const meditateModuleConfig: ModuleConfig = { + appId: 'meditate', + tables: [{ name: 'meditatePresets' }, { name: 'meditateSessions' }, { name: 'meditateSettings' }], +}; diff --git a/apps/mana/apps/web/src/lib/modules/meditate/queries.ts b/apps/mana/apps/web/src/lib/modules/meditate/queries.ts new file mode 100644 index 000000000..b8ca1841c --- /dev/null +++ b/apps/mana/apps/web/src/lib/modules/meditate/queries.ts @@ -0,0 +1,184 @@ +/** + * Reactive Queries & Pure Helpers for the Meditate module. + * + * Read-side only β€” mutations live in stores/meditate.svelte.ts. + */ + +import { useLiveQueryWithDefault } from '@mana/local-store/svelte'; +import { decryptRecords } from '$lib/data/crypto'; +import { db } from '$lib/data/database'; +import type { + LocalMeditatePreset, + LocalMeditateSession, + LocalMeditateSettings, + MeditatePreset, + MeditateSession, + MeditateSettings, +} from './types'; +import { DEFAULT_SETTINGS } from './types'; + +// ─── Type Converters ──────────────────────────────────────── + +export function toMeditatePreset(local: LocalMeditatePreset): MeditatePreset { + const now = new Date().toISOString(); + return { + id: local.id, + name: local.name, + description: local.description ?? '', + category: local.category, + breathPattern: local.breathPattern ?? null, + bodyScanSteps: local.bodyScanSteps ?? null, + defaultDurationSec: local.defaultDurationSec, + isPreset: local.isPreset, + isArchived: local.isArchived, + order: local.order, + createdAt: local.createdAt ?? now, + updatedAt: local.updatedAt ?? now, + }; +} + +export function toMeditateSession(local: LocalMeditateSession): MeditateSession { + return { + id: local.id, + presetId: local.presetId ?? null, + category: local.category, + startedAt: local.startedAt, + durationSec: local.durationSec, + completed: local.completed, + moodBefore: local.moodBefore ?? null, + moodAfter: local.moodAfter ?? null, + notes: local.notes ?? null, + createdAt: local.createdAt ?? new Date().toISOString(), + }; +} + +export function toMeditateSettings(local: LocalMeditateSettings): MeditateSettings { + return { + id: local.id, + bellSound: local.bellSound, + intervalBell: local.intervalBell, + intervalSeconds: local.intervalSeconds, + showBreathGuide: local.showBreathGuide, + backgroundTheme: local.backgroundTheme, + }; +} + +// ─── Live Queries ─────────────────────────────────────────── + +export function useAllPresets() { + return useLiveQueryWithDefault(async () => { + const locals = await db + .table('meditatePresets') + .orderBy('order') + .toArray(); + const visible = locals.filter((p) => !p.deletedAt && !p.isArchived); + const decrypted = await decryptRecords('meditatePresets', visible); + return decrypted.map(toMeditatePreset); + }, [] as MeditatePreset[]); +} + +export function useAllSessions() { + return useLiveQueryWithDefault(async () => { + const locals = await db + .table('meditateSessions') + .orderBy('startedAt') + .reverse() + .toArray(); + const visible = locals.filter((s) => !s.deletedAt); + const decrypted = await decryptRecords('meditateSessions', visible); + return decrypted.map(toMeditateSession); + }, [] as MeditateSession[]); +} + +export function useSettings() { + return useLiveQueryWithDefault( + async () => { + const locals = await db.table('meditateSettings').toArray(); + if (locals.length === 0) return null; + return toMeditateSettings(locals[0]); + }, + null as MeditateSettings | null + ); +} + +// ─── Pure Helpers (for $derived) ──────────────────────────── + +export function todayDateStr(): string { + return new Date().toISOString().split('T')[0]; +} + +export function getSessionsForDate(sessions: MeditateSession[], date: string): MeditateSession[] { + return sessions.filter((s) => s.startedAt.split('T')[0] === date); +} + +export function getTodaySessions(sessions: MeditateSession[]): MeditateSession[] { + return getSessionsForDate(sessions, todayDateStr()); +} + +export function getTodayMinutes(sessions: MeditateSession[]): number { + return Math.round(getTodaySessions(sessions).reduce((sum, s) => sum + s.durationSec, 0) / 60); +} + +export function getWeekSessionCount(sessions: MeditateSession[]): number { + const now = new Date(); + const weekAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000).toISOString(); + return sessions.filter((s) => s.startedAt >= weekAgo).length; +} + +export function getWeekMinutes(sessions: MeditateSession[]): number { + const now = new Date(); + const weekAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000).toISOString(); + return Math.round( + sessions.filter((s) => s.startedAt >= weekAgo).reduce((sum, s) => sum + s.durationSec, 0) / 60 + ); +} + +export function getCurrentStreak(sessions: MeditateSession[]): number { + if (sessions.length === 0) return 0; + + const uniqueDays = new Set(sessions.map((s) => s.startedAt.split('T')[0])); + const today = todayDateStr(); + let streak = 0; + let d = new Date(today); + + // If no session today, start checking from yesterday + if (!uniqueDays.has(today)) { + d.setDate(d.getDate() - 1); + } + + while (uniqueDays.has(d.toISOString().split('T')[0])) { + streak++; + d.setDate(d.getDate() - 1); + } + + return streak; +} + +export function getTotalSessions(sessions: MeditateSession[]): number { + return sessions.filter((s) => s.completed).length; +} + +export function getTotalMinutes(sessions: MeditateSession[]): number { + return Math.round(sessions.reduce((sum, s) => sum + s.durationSec, 0) / 60); +} + +export function formatDuration(seconds: number): string { + const mins = Math.floor(seconds / 60); + const secs = seconds % 60; + if (mins === 0) return `${secs}s`; + if (secs === 0) return `${mins} min`; + return `${mins}:${String(secs).padStart(2, '0')}`; +} + +export function formatDurationLong(seconds: number): string { + const mins = Math.floor(seconds / 60); + if (mins === 1) return '1 Minute'; + return `${mins} Minuten`; +} + +export function getDefaultSettings(): MeditateSettings { + return { + id: 'settings', + ...DEFAULT_SETTINGS, + }; +} diff --git a/apps/mana/apps/web/src/lib/modules/meditate/types.ts b/apps/mana/apps/web/src/lib/modules/meditate/types.ts new file mode 100644 index 000000000..9e9850870 --- /dev/null +++ b/apps/mana/apps/web/src/lib/modules/meditate/types.ts @@ -0,0 +1,143 @@ +/** + * Meditate module types β€” meditation timer, breathing exercises, body scans. + * + * Tables: + * meditatePresets β€” predefined & custom meditation/breathing templates + * meditateSessions β€” completed meditation sessions + * meditateSettings β€” per-user preferences (bell sound, theme, etc.) + */ + +import type { BaseRecord } from '@mana/local-store'; + +// ─── Enums / unions ───────────────────────────────────────── + +export type MeditateCategory = 'silence' | 'breathing' | 'bodyscan'; + +export type BellSound = 'gong' | 'bowl' | 'bell' | 'none'; + +export type BackgroundTheme = 'minimal' | 'gradient' | 'dark'; + +// ─── Embedded Types ───────────────────────────────────────── + +/** Breathing pattern β€” all values in seconds. */ +export interface BreathPattern { + inhale: number; + hold1: number; + exhale: number; + hold2: number; +} + +/** Which phase of a breath cycle we're in. */ +export type BreathPhase = 'inhale' | 'hold1' | 'exhale' | 'hold2'; + +// ─── Local Record Types (Dexie) ───────────────────────────── + +export interface LocalMeditatePreset extends BaseRecord { + name: string; + description: string; + category: MeditateCategory; + /** null for silence and bodyscan presets. */ + breathPattern: BreathPattern | null; + /** Text steps for body scan (e.g. "Feet", "Legs", …). null for other categories. */ + bodyScanSteps: string[] | null; + defaultDurationSec: number; + /** Built-in seed vs. user-created. */ + isPreset: boolean; + isArchived: boolean; + order: number; +} + +export interface LocalMeditateSession extends BaseRecord { + presetId: string | null; + /** Denormalized for stats queries without join. */ + category: MeditateCategory; + startedAt: string; + durationSec: number; + completed: boolean; + moodBefore: number | null; + moodAfter: number | null; + notes: string | null; +} + +export interface LocalMeditateSettings extends BaseRecord { + bellSound: BellSound; + intervalBell: boolean; + intervalSeconds: number; + showBreathGuide: boolean; + backgroundTheme: BackgroundTheme; +} + +// ─── Domain Types (UI-facing) ─────────────────────────────── + +export interface MeditatePreset { + id: string; + name: string; + description: string; + category: MeditateCategory; + breathPattern: BreathPattern | null; + bodyScanSteps: string[] | null; + defaultDurationSec: number; + isPreset: boolean; + isArchived: boolean; + order: number; + createdAt: string; + updatedAt: string; +} + +export interface MeditateSession { + id: string; + presetId: string | null; + category: MeditateCategory; + startedAt: string; + durationSec: number; + completed: boolean; + moodBefore: number | null; + moodAfter: number | null; + notes: string | null; + createdAt: string; +} + +export interface MeditateSettings { + id: string; + bellSound: BellSound; + intervalBell: boolean; + intervalSeconds: number; + showBreathGuide: boolean; + backgroundTheme: BackgroundTheme; +} + +// ─── Constants ────────────────────────────────────────────── + +export const MEDITATE_CATEGORIES: readonly MeditateCategory[] = [ + 'silence', + 'breathing', + 'bodyscan', +] as const; + +export const CATEGORY_LABELS: Record = { + silence: { de: 'Stille', en: 'Silence' }, + breathing: { de: 'AtemΓΌbung', en: 'Breathing' }, + bodyscan: { de: 'Body Scan', en: 'Body Scan' }, +}; + +export const BELL_SOUND_LABELS: Record = { + gong: { de: 'Gong', en: 'Gong' }, + bowl: { de: 'Klangschale', en: 'Singing Bowl' }, + bell: { de: 'Glocke', en: 'Bell' }, + none: { de: 'Aus', en: 'Off' }, +}; + +export const BREATH_PHASE_LABELS: Record = { + inhale: { de: 'Einatmen', en: 'Inhale' }, + hold1: { de: 'Halten', en: 'Hold' }, + exhale: { de: 'Ausatmen', en: 'Exhale' }, + hold2: { de: 'Halten', en: 'Hold' }, +}; + +export const DEFAULT_SETTINGS: Omit = { + bellSound: 'gong', + intervalBell: false, + intervalSeconds: 300, + showBreathGuide: true, + backgroundTheme: 'minimal', +}; diff --git a/apps/mana/apps/web/src/routes/(app)/meditate/+layout.svelte b/apps/mana/apps/web/src/routes/(app)/meditate/+layout.svelte new file mode 100644 index 000000000..4dd9b8e86 --- /dev/null +++ b/apps/mana/apps/web/src/routes/(app)/meditate/+layout.svelte @@ -0,0 +1,13 @@ + + +{@render children()} diff --git a/apps/mana/apps/web/src/routes/(app)/meditate/+page.svelte b/apps/mana/apps/web/src/routes/(app)/meditate/+page.svelte new file mode 100644 index 000000000..3605f03c4 --- /dev/null +++ b/apps/mana/apps/web/src/routes/(app)/meditate/+page.svelte @@ -0,0 +1,149 @@ + + + + + Meditate - Mana + + +{#if activePreset} + +{/if} + +
+ + + + {#if sessions.length > 0} +
+ +
+ {/if} + + +
+
+

Meditationen

+
+
+ {#each presets as preset (preset.id)} + + {/each} +
+
+ + + {#if recentSessions.length > 0} +
+
+

Letzte Sessions

+ Alle β†’ +
+
+ {#each recentSessions as session (session.id)} + + {/each} +
+
+ {/if} +
+ + diff --git a/apps/mana/apps/web/src/routes/(app)/meditate/history/+page.svelte b/apps/mana/apps/web/src/routes/(app)/meditate/history/+page.svelte new file mode 100644 index 000000000..cf38316dc --- /dev/null +++ b/apps/mana/apps/web/src/routes/(app)/meditate/history/+page.svelte @@ -0,0 +1,155 @@ + + + + + Meditation History - Mana + + +
+ + + +
+ + {#each Object.entries(CATEGORY_LABELS) as [key, label]} + + {/each} +
+ + + {#if filtered.length === 0} +
+

Noch keine Sessions.

+
+ {:else} +
+ {#each filtered as session (session.id)} + + {/each} +
+ {/if} +
+ +