From d6a1c9fd8b45e74baf92fa67f058ae3e9144e12e Mon Sep 17 00:00:00 2001 From: Till JS Date: Sun, 12 Apr 2026 18:41:06 +0200 Subject: [PATCH] feat(drink): add beverage tracking module with inline editing New module for tracking all beverages (water, coffee, tea, juice, alcohol, etc.) with daily progress bar, quick-tap presets, and inline editing of quantity/date/time. Includes: module config, types, collections with guest seed (5 presets), queries, store, ListView with context menus, route, app-registry registration, Dexie schema v7, encryption registry, shared-branding icon/app entry. Also extends docs/future/MODULE_IDEAS.md with additional module ideas. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../apps/web/src/lib/app-registry/apps.ts | 10 + .../apps/web/src/lib/data/crypto/registry.ts | 7 + apps/mana/apps/web/src/lib/data/database.ts | 12 + .../apps/web/src/lib/data/module-registry.ts | 2 + .../apps/web/src/lib/data/seed-registry.ts | 2 + .../web/src/lib/modules/drink/ListView.svelte | 819 ++++++++++++++++++ .../web/src/lib/modules/drink/collections.ts | 71 ++ .../apps/web/src/lib/modules/drink/index.ts | 40 + .../src/lib/modules/drink/module.config.ts | 6 + .../apps/web/src/lib/modules/drink/queries.ts | 125 +++ .../lib/modules/drink/stores/drink.svelte.ts | 147 ++++ .../apps/web/src/lib/modules/drink/types.ts | 128 +++ .../web/src/routes/(app)/drink/+layout.svelte | 15 + .../web/src/routes/(app)/drink/+page.svelte | 9 + docs/future/MODULE_IDEAS.md | 31 + packages/shared-branding/src/app-icons.ts | 5 + packages/shared-branding/src/mana-apps.ts | 17 + 17 files changed, 1446 insertions(+) create mode 100644 apps/mana/apps/web/src/lib/modules/drink/ListView.svelte create mode 100644 apps/mana/apps/web/src/lib/modules/drink/collections.ts create mode 100644 apps/mana/apps/web/src/lib/modules/drink/index.ts create mode 100644 apps/mana/apps/web/src/lib/modules/drink/module.config.ts create mode 100644 apps/mana/apps/web/src/lib/modules/drink/queries.ts create mode 100644 apps/mana/apps/web/src/lib/modules/drink/stores/drink.svelte.ts create mode 100644 apps/mana/apps/web/src/lib/modules/drink/types.ts create mode 100644 apps/mana/apps/web/src/routes/(app)/drink/+layout.svelte create mode 100644 apps/mana/apps/web/src/routes/(app)/drink/+page.svelte diff --git a/apps/mana/apps/web/src/lib/app-registry/apps.ts b/apps/mana/apps/web/src/lib/app-registry/apps.ts index da8a9c01b..d8427bccf 100644 --- a/apps/mana/apps/web/src/lib/app-registry/apps.ts +++ b/apps/mana/apps/web/src/lib/app-registry/apps.ts @@ -831,3 +831,13 @@ registerApp({ return first.id; }, }); + +registerApp({ + id: 'drink', + name: 'Drink', + color: '#3b82f6', + icon: Drop, + views: { + list: { load: () => import('$lib/modules/drink/ListView.svelte') }, + }, +}); diff --git a/apps/mana/apps/web/src/lib/data/crypto/registry.ts b/apps/mana/apps/web/src/lib/data/crypto/registry.ts index 346457d5e..622dbdca1 100644 --- a/apps/mana/apps/web/src/lib/data/crypto/registry.ts +++ b/apps/mana/apps/web/src/lib/data/crypto/registry.ts @@ -403,6 +403,13 @@ export const ENCRYPTION_REGISTRY: Record = { guides: { enabled: true, fields: ['title', 'description'] }, sections: { enabled: true, fields: ['title', 'content'] }, steps: { enabled: true, fields: ['title', 'content'] }, + + // ─── Drink ─────────────────────────────────────────────── + // User-typed content (drink names, notes) → encrypted. + // Structural fields (date, time, drinkType, quantityMl, presetId) → + // plaintext for indexing and daily aggregation queries. + drinkEntries: { enabled: true, fields: ['name', 'note'] }, + drinkPresets: { enabled: true, fields: ['name'] }, }; /** diff --git a/apps/mana/apps/web/src/lib/data/database.ts b/apps/mana/apps/web/src/lib/data/database.ts index 1d67853fe..a782eece5 100644 --- a/apps/mana/apps/web/src/lib/data/database.ts +++ b/apps/mana/apps/web/src/lib/data/database.ts @@ -380,6 +380,18 @@ db.version(6).stores({ firsts: 'id, status, category, date, priority, isPinned, isArchived', }); +// Schema version 7 — adds the Drink module (beverage tracking). +// Additive only; no prior tables touched. +// +// Index strategy: +// - drinkEntries indexes [date+time] for the daily timeline view +// (range scan on date, sorted by time within a day). +// - drinkPresets indexes `order` for the preset-picker sort. +db.version(7).stores({ + drinkEntries: 'id, date, drinkType, presetId, [date+time]', + drinkPresets: 'id, order, drinkType, isArchived', +}); + // ─── Sync Routing ────────────────────────────────────────── // SYNC_APP_MAP, TABLE_TO_SYNC_NAME, TABLE_TO_APP, SYNC_NAME_TO_TABLE, // toSyncName() and fromSyncName() are now derived from per-module diff --git a/apps/mana/apps/web/src/lib/data/module-registry.ts b/apps/mana/apps/web/src/lib/data/module-registry.ts index f9cfdefc5..ca2e5f3fc 100644 --- a/apps/mana/apps/web/src/lib/data/module-registry.ts +++ b/apps/mana/apps/web/src/lib/data/module-registry.ts @@ -88,6 +88,7 @@ import { whoModuleConfig } from '$lib/modules/who/module.config'; import { newsModuleConfig } from '$lib/modules/news/module.config'; import { bodyModuleConfig } from '$lib/modules/body/module.config'; import { firstsModuleConfig } from '$lib/modules/firsts/module.config'; +import { drinkModuleConfig } from '$lib/modules/drink/module.config'; export const MODULE_CONFIGS: readonly ModuleConfig[] = [ manaCoreConfig, @@ -131,6 +132,7 @@ export const MODULE_CONFIGS: readonly ModuleConfig[] = [ newsModuleConfig, bodyModuleConfig, firstsModuleConfig, + drinkModuleConfig, ]; // ─── Derived Maps ────────────────────────────────────────── diff --git a/apps/mana/apps/web/src/lib/data/seed-registry.ts b/apps/mana/apps/web/src/lib/data/seed-registry.ts index 216067a71..cba89fa4c 100644 --- a/apps/mana/apps/web/src/lib/data/seed-registry.ts +++ b/apps/mana/apps/web/src/lib/data/seed-registry.ts @@ -28,6 +28,7 @@ import { TODO_GUEST_SEED } from '$lib/modules/todo/collections'; import { NOTES_GUEST_SEED } from '$lib/modules/notes/collections'; import { TIMES_GUEST_SEED } from '$lib/modules/times/collections'; import { PLANTA_GUEST_SEED } from '$lib/modules/planta/collections'; +import { DRINK_GUEST_SEED } from '$lib/modules/drink/collections'; /** * Flat list of { tableName, rows } entries. Only modules with non-empty @@ -60,6 +61,7 @@ register(TODO_GUEST_SEED); register(NOTES_GUEST_SEED); register(TIMES_GUEST_SEED); register(PLANTA_GUEST_SEED); +register(DRINK_GUEST_SEED); /** * Seed all module guest data into empty tables. Idempotent: tables diff --git a/apps/mana/apps/web/src/lib/modules/drink/ListView.svelte b/apps/mana/apps/web/src/lib/modules/drink/ListView.svelte new file mode 100644 index 000000000..6f955f49e --- /dev/null +++ b/apps/mana/apps/web/src/lib/modules/drink/ListView.svelte @@ -0,0 +1,819 @@ + + + +
+ +
+
+ Heute + + {formatMl(todayTotalMl)} + / {formatMl(DEFAULT_DAILY_GOAL_ML)} + +
+
+
+
+
+ + +
+ {#each activePresets as preset (preset.id)} + + {/each} + + {#if !showCreate} + + {/if} +
+ + + {#if showCreate} + +
+
+ + + +
+ {#if showIconPicker} +
+ { + newIcon = i; + showIconPicker = false; + }} + size="sm" + /> +
+ {/if} +
+ +
+ {#each QUICK_QUANTITIES as q} + + {/each} +
+
+
+ + +
+
+ {/if} + + + {#if todayEntries.length > 0} +
+
Verlauf
+ {#each todayEntries as entry (entry.id)} + {#if editingId === entry.id} + +
+ + {entry.name} + + ml + + + + +
+ {:else} + + {/if} + {/each} +
+ {/if} + + + + + {#if activePresets.length === 0 && !showCreate} +
+

Noch keine Getränke-Presets.

+ +
+ {/if} +
+ + diff --git a/apps/mana/apps/web/src/lib/modules/drink/collections.ts b/apps/mana/apps/web/src/lib/modules/drink/collections.ts new file mode 100644 index 000000000..985929b95 --- /dev/null +++ b/apps/mana/apps/web/src/lib/modules/drink/collections.ts @@ -0,0 +1,71 @@ +/** + * Drink module — collection accessors and guest seed data. + * + * Tables: drinkEntries, drinkPresets + */ + +import { db } from '$lib/data/database'; +import type { LocalDrinkEntry, LocalDrinkPreset } from './types'; + +// ─── Collection Accessors ────────────────────────────────── + +export const drinkEntryTable = db.table('drinkEntries'); +export const drinkPresetTable = db.table('drinkPresets'); + +// ─── Guest Seed ──────────────────────────────────────────── + +export const DRINK_GUEST_SEED = { + drinkEntries: [] satisfies LocalDrinkEntry[], + drinkPresets: [ + { + id: 'drink-preset-water', + name: 'Wasser', + icon: 'drop', + color: '#3b82f6', + drinkType: 'water', + defaultQuantityMl: 250, + order: 0, + isArchived: false, + }, + { + id: 'drink-preset-coffee', + name: 'Kaffee', + icon: 'coffee', + color: '#92400e', + drinkType: 'coffee', + defaultQuantityMl: 200, + order: 1, + isArchived: false, + }, + { + id: 'drink-preset-tea', + name: 'Tee', + icon: 'coffee', + color: '#65a30d', + drinkType: 'tea', + defaultQuantityMl: 250, + order: 2, + isArchived: false, + }, + { + id: 'drink-preset-juice', + name: 'Saft', + icon: 'orange-slice', + color: '#f97316', + drinkType: 'juice', + defaultQuantityMl: 200, + order: 3, + isArchived: false, + }, + { + id: 'drink-preset-beer', + name: 'Bier', + icon: 'beer-stein', + color: '#f59e0b', + drinkType: 'beer', + defaultQuantityMl: 330, + order: 4, + isArchived: false, + }, + ] satisfies LocalDrinkPreset[], +}; diff --git a/apps/mana/apps/web/src/lib/modules/drink/index.ts b/apps/mana/apps/web/src/lib/modules/drink/index.ts new file mode 100644 index 000000000..5a5b5fcd7 --- /dev/null +++ b/apps/mana/apps/web/src/lib/modules/drink/index.ts @@ -0,0 +1,40 @@ +/** + * Drink module — barrel exports. + */ + +// ─── Stores ────────────────────────────────────────────── +export { drinkStore } from './stores/drink.svelte'; + +// ─── Queries ───────────────────────────────────────────── +export { + useAllDrinkEntries, + useAllDrinkPresets, + toDrinkEntry, + toDrinkPreset, + todayStr, + nowTime, + getEntriesForDate, + getTotalMlForDate, + getTotalMlByType, + groupEntriesByDate, + getActivePresets, + formatMl, +} from './queries'; + +// ─── Collections ───────────────────────────────────────── +export { drinkEntryTable, drinkPresetTable, DRINK_GUEST_SEED } from './collections'; + +// ─── Types ─────────────────────────────────────────────── +export { + DRINK_TYPE_LABELS, + DRINK_TYPE_ICONS, + DRINK_TYPE_COLORS, + DEFAULT_DAILY_GOAL_ML, +} from './types'; +export type { + LocalDrinkEntry, + LocalDrinkPreset, + DrinkEntry, + DrinkPreset, + DrinkType, +} from './types'; diff --git a/apps/mana/apps/web/src/lib/modules/drink/module.config.ts b/apps/mana/apps/web/src/lib/modules/drink/module.config.ts new file mode 100644 index 000000000..9bbc52d7d --- /dev/null +++ b/apps/mana/apps/web/src/lib/modules/drink/module.config.ts @@ -0,0 +1,6 @@ +import type { ModuleConfig } from '$lib/data/module-registry'; + +export const drinkModuleConfig: ModuleConfig = { + appId: 'drink', + tables: [{ name: 'drinkEntries' }, { name: 'drinkPresets' }], +}; diff --git a/apps/mana/apps/web/src/lib/modules/drink/queries.ts b/apps/mana/apps/web/src/lib/modules/drink/queries.ts new file mode 100644 index 000000000..59e0aa9e3 --- /dev/null +++ b/apps/mana/apps/web/src/lib/modules/drink/queries.ts @@ -0,0 +1,125 @@ +/** + * Reactive Queries & Pure Helpers for Drink module. + */ + +import { useLiveQueryWithDefault } from '@mana/local-store/svelte'; +import { decryptRecords } from '$lib/data/crypto'; +import { db } from '$lib/data/database'; +import type { LocalDrinkEntry, LocalDrinkPreset, DrinkEntry, DrinkPreset } from './types'; + +// ─── Type Converters ────────────────────────────────────── + +export function toDrinkEntry(local: LocalDrinkEntry): DrinkEntry { + const now = new Date().toISOString(); + return { + id: local.id, + name: local.name, + drinkType: local.drinkType, + quantityMl: local.quantityMl, + date: local.date, + time: local.time, + note: local.note ?? null, + presetId: local.presetId ?? null, + createdAt: local.createdAt ?? now, + updatedAt: local.updatedAt ?? now, + }; +} + +export function toDrinkPreset(local: LocalDrinkPreset): DrinkPreset { + const now = new Date().toISOString(); + return { + id: local.id, + name: local.name, + icon: local.icon, + color: local.color, + drinkType: local.drinkType, + defaultQuantityMl: local.defaultQuantityMl, + order: local.order, + isArchived: local.isArchived, + createdAt: local.createdAt ?? now, + updatedAt: local.updatedAt ?? now, + }; +} + +// ─── Live Queries ───────────────────────────────────────── + +export function useAllDrinkEntries() { + return useLiveQueryWithDefault(async () => { + const locals = await db + .table('drinkEntries') + .orderBy('date') + .reverse() + .toArray(); + const visible = locals.filter((e) => !e.deletedAt); + const decrypted = await decryptRecords('drinkEntries', visible); + return decrypted.map(toDrinkEntry); + }, [] as DrinkEntry[]); +} + +export function useAllDrinkPresets() { + return useLiveQueryWithDefault(async () => { + const locals = await db.table('drinkPresets').orderBy('order').toArray(); + const visible = locals.filter((p) => !p.deletedAt); + const decrypted = await decryptRecords('drinkPresets', visible); + return decrypted.map(toDrinkPreset); + }, [] as DrinkPreset[]); +} + +// ─── Pure Helpers ───────────────────────────────────────── + +/** Get today's date string (YYYY-MM-DD) */ +export function todayStr(): string { + return new Date().toISOString().split('T')[0]; +} + +/** Current time as HH:mm */ +export function nowTime(): string { + const d = new Date(); + return `${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}`; +} + +/** Filter entries for a specific date */ +export function getEntriesForDate(entries: DrinkEntry[], date: string): DrinkEntry[] { + return entries.filter((e) => e.date === date).sort((a, b) => b.time.localeCompare(a.time)); +} + +/** Total ml for a given date */ +export function getTotalMlForDate(entries: DrinkEntry[], date: string): number { + return entries.filter((e) => e.date === date).reduce((sum, e) => sum + e.quantityMl, 0); +} + +/** Total ml for a given date and drink type */ +export function getTotalMlByType(entries: DrinkEntry[], date: string, drinkType: string): number { + return entries + .filter((e) => e.date === date && e.drinkType === drinkType) + .reduce((sum, e) => sum + e.quantityMl, 0); +} + +/** Group entries by date (most recent first) */ +export function groupEntriesByDate(entries: DrinkEntry[]): Map { + const groups = new Map(); + for (const entry of entries) { + const existing = groups.get(entry.date) || []; + existing.push(entry); + groups.set(entry.date, existing); + } + // Sort entries within each group by time descending + for (const [, group] of groups) { + group.sort((a, b) => b.time.localeCompare(a.time)); + } + return groups; +} + +/** Get active (non-archived) presets sorted by order */ +export function getActivePresets(presets: DrinkPreset[]): DrinkPreset[] { + return presets.filter((p) => !p.isArchived).sort((a, b) => a.order - b.order); +} + +/** Format ml as a readable string */ +export function formatMl(ml: number): string { + if (ml >= 1000) { + const liters = ml / 1000; + return `${liters % 1 === 0 ? liters.toFixed(0) : liters.toFixed(1)} L`; + } + return `${ml} ml`; +} diff --git a/apps/mana/apps/web/src/lib/modules/drink/stores/drink.svelte.ts b/apps/mana/apps/web/src/lib/modules/drink/stores/drink.svelte.ts new file mode 100644 index 000000000..0cab7cfdc --- /dev/null +++ b/apps/mana/apps/web/src/lib/modules/drink/stores/drink.svelte.ts @@ -0,0 +1,147 @@ +/** + * Drink Store — Mutation-Only Service + * + * All reads are handled by liveQuery hooks in queries.ts. + */ + +import { encryptRecord } from '$lib/data/crypto'; +import { drinkEntryTable, drinkPresetTable } from '../collections'; +import { toDrinkEntry, toDrinkPreset, todayStr, nowTime } from '../queries'; +import type { LocalDrinkEntry, LocalDrinkPreset, DrinkType } from '../types'; + +export const drinkStore = { + // ─── Entries ────────────────────────────────────────────── + + async logDrink(input: { + name: string; + drinkType: DrinkType; + quantityMl: number; + date?: string; + time?: string; + note?: string | null; + presetId?: string | null; + }) { + const newLocal: LocalDrinkEntry = { + id: crypto.randomUUID(), + name: input.name, + drinkType: input.drinkType, + quantityMl: input.quantityMl, + date: input.date ?? todayStr(), + time: input.time ?? nowTime(), + note: input.note ?? null, + presetId: input.presetId ?? null, + }; + const snapshot = toDrinkEntry({ ...newLocal }); + await encryptRecord('drinkEntries', newLocal); + await drinkEntryTable.add(newLocal); + return snapshot; + }, + + /** Quick-log from a preset (one tap) */ + async logFromPreset(presetId: string) { + const preset = await drinkPresetTable.get(presetId); + if (!preset) return null; + return this.logDrink({ + name: preset.name, + drinkType: preset.drinkType, + quantityMl: preset.defaultQuantityMl, + presetId: preset.id, + }); + }, + + async updateEntry( + id: string, + patch: Partial< + Pick + > + ) { + const wrapped = { ...patch } as Record; + await encryptRecord('drinkEntries', wrapped); + await drinkEntryTable.update(id, { + ...wrapped, + updatedAt: new Date().toISOString(), + }); + }, + + async deleteEntry(id: string) { + await drinkEntryTable.update(id, { + deletedAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + }); + }, + + async undoLastEntry() { + const all = await drinkEntryTable.toArray(); + const active = all + .filter((e) => !e.deletedAt) + .sort((a, b) => (b.createdAt ?? '').localeCompare(a.createdAt ?? '')); + if (active.length > 0) { + await drinkEntryTable.update(active[0].id, { + deletedAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + }); + } + }, + + // ─── Presets ────────────────────────────────────────────── + + async createPreset(input: { + name: string; + icon: string; + color: string; + drinkType: DrinkType; + defaultQuantityMl: number; + }) { + const existing = await drinkPresetTable.toArray(); + const count = existing.filter((p) => !p.deletedAt).length; + + const newLocal: LocalDrinkPreset = { + id: crypto.randomUUID(), + name: input.name, + icon: input.icon, + color: input.color, + drinkType: input.drinkType, + defaultQuantityMl: input.defaultQuantityMl, + order: count, + isArchived: false, + }; + const snapshot = toDrinkPreset({ ...newLocal }); + await encryptRecord('drinkPresets', newLocal); + await drinkPresetTable.add(newLocal); + return snapshot; + }, + + async updatePreset( + id: string, + patch: Partial< + Pick< + LocalDrinkPreset, + 'name' | 'icon' | 'color' | 'drinkType' | 'defaultQuantityMl' | 'isArchived' | 'order' + > + > + ) { + const wrapped = { ...patch } as Record; + await encryptRecord('drinkPresets', wrapped); + await drinkPresetTable.update(id, { + ...wrapped, + updatedAt: new Date().toISOString(), + }); + }, + + async deletePreset(id: string) { + await drinkPresetTable.update(id, { + deletedAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + }); + }, + + async reorderPresets(presetIds: string[]) { + const now = new Date().toISOString(); + for (let i = 0; i < presetIds.length; i++) { + await drinkPresetTable.update(presetIds[i], { + order: i, + updatedAt: now, + }); + } + }, +}; diff --git a/apps/mana/apps/web/src/lib/modules/drink/types.ts b/apps/mana/apps/web/src/lib/modules/drink/types.ts new file mode 100644 index 000000000..3225fd98d --- /dev/null +++ b/apps/mana/apps/web/src/lib/modules/drink/types.ts @@ -0,0 +1,128 @@ +/** + * Drink module types. + * + * DrinkEntry = a single logged drink with quantity and type + * DrinkPreset = a saved favourite drink for quick-add + */ + +import type { BaseRecord } from '@mana/local-store'; + +// ─── Drink Types ───────────────────────────────────────── + +export type DrinkType = + | 'water' + | 'coffee' + | 'tea' + | 'juice' + | 'soda' + | 'smoothie' + | 'milk' + | 'beer' + | 'wine' + | 'cocktail' + | 'spirit' + | 'energy' + | 'other'; + +// ─── Local Record Types (Dexie) ────────────────────────── + +export interface LocalDrinkEntry extends BaseRecord { + name: string; + drinkType: DrinkType; + quantityMl: number; + date: string; // YYYY-MM-DD + time: string; // HH:mm + note: string | null; + presetId: string | null; +} + +export interface LocalDrinkPreset extends BaseRecord { + name: string; + icon: string; + color: string; + drinkType: DrinkType; + defaultQuantityMl: number; + order: number; + isArchived: boolean; +} + +// ─── Domain Types ──────────────────────────────────────── + +export interface DrinkEntry { + id: string; + name: string; + drinkType: DrinkType; + quantityMl: number; + date: string; + time: string; + note: string | null; + presetId: string | null; + createdAt: string; + updatedAt: string; +} + +export interface DrinkPreset { + id: string; + name: string; + icon: string; + color: string; + drinkType: DrinkType; + defaultQuantityMl: number; + order: number; + isArchived: boolean; + createdAt: string; + updatedAt: string; +} + +// ─── Constants ─────────────────────────────────────────── + +export const DRINK_TYPE_LABELS: Record = { + water: { de: 'Wasser', en: 'Water' }, + coffee: { de: 'Kaffee', en: 'Coffee' }, + tea: { de: 'Tee', en: 'Tea' }, + juice: { de: 'Saft', en: 'Juice' }, + soda: { de: 'Limonade', en: 'Soda' }, + smoothie: { de: 'Smoothie', en: 'Smoothie' }, + milk: { de: 'Milch', en: 'Milk' }, + beer: { de: 'Bier', en: 'Beer' }, + wine: { de: 'Wein', en: 'Wine' }, + cocktail: { de: 'Cocktail', en: 'Cocktail' }, + spirit: { de: 'Spirituose', en: 'Spirit' }, + energy: { de: 'Energy Drink', en: 'Energy Drink' }, + other: { de: 'Sonstiges', en: 'Other' }, +}; + +export const DRINK_TYPE_ICONS: Record = { + water: 'drop', + coffee: 'coffee', + tea: 'coffee', // Phosphor doesn't have a teacup, use coffee + juice: 'orange-slice', + soda: 'beer-bottle', + smoothie: 'blender', + milk: 'cow', + beer: 'beer-stein', + wine: 'wine', + cocktail: 'martini', + spirit: 'martini', + energy: 'lightning', + other: 'drop', +}; + +export const DRINK_TYPE_COLORS: Record = { + water: '#3b82f6', + coffee: '#92400e', + tea: '#65a30d', + juice: '#f97316', + soda: '#ef4444', + smoothie: '#a855f7', + milk: '#e5e7eb', + beer: '#f59e0b', + wine: '#881337', + cocktail: '#ec4899', + spirit: '#6366f1', + energy: '#22d3ee', + other: '#6b7280', +}; + +/** Default daily goal in ml */ +export const DEFAULT_DAILY_GOAL_ML = 2500; diff --git a/apps/mana/apps/web/src/routes/(app)/drink/+layout.svelte b/apps/mana/apps/web/src/routes/(app)/drink/+layout.svelte new file mode 100644 index 000000000..2b776d749 --- /dev/null +++ b/apps/mana/apps/web/src/routes/(app)/drink/+layout.svelte @@ -0,0 +1,15 @@ + + +{@render children()} diff --git a/apps/mana/apps/web/src/routes/(app)/drink/+page.svelte b/apps/mana/apps/web/src/routes/(app)/drink/+page.svelte new file mode 100644 index 000000000..d7325c1e7 --- /dev/null +++ b/apps/mana/apps/web/src/routes/(app)/drink/+page.svelte @@ -0,0 +1,9 @@ + + + + Drink - Mana + + + diff --git a/docs/future/MODULE_IDEAS.md b/docs/future/MODULE_IDEAS.md index 5496d990b..76886a754 100644 --- a/docs/future/MODULE_IDEAS.md +++ b/docs/future/MODULE_IDEAS.md @@ -106,6 +106,37 @@ recommendation. - **pets** — Vet appointments, vaccinations, feeding, weight - **plants-care** — Extension of `planta`: watering plan, fertilizing, repotting +## Health & Body (additional) + +- **drink** — ✅ **Built.** Getränke-Tracker für alle Getränke (Wasser, Kaffee, Tee, Saft, Alkohol etc.). Tages-/Wochenziele, Favoriten, Verlauf. Verknüpfung mit `nutriphi` und `body`. +- **breathe** — Atemübungen & Meditation-Timer mit geführten Mustern (Box Breathing, 4-7-8). Sessions-Log verknüpft mit `moodlit`. +- **fasting** — Intervallfasten-Timer (16:8, 5:2 etc.), verknüpft mit `nutriphi` und `body`. + +## Knowledge & Productivity (additional) + +- **readlog** — Lese-Fortschritt tracken (Seiten/Tag, aktuelles Kapitel). Leichter als `library`, fokussiert auf Dranbleiben. +- **snippets** — Code-Schnipsel-Bibliothek mit Syntax-Highlighting, Tags, Suche. +- **flashbacks** — "On this day"-Aggregator über alle Module. Journal, Fotos, Moods, Todos von vor 1/2/5 Jahren. +- **teach** — Feynman-Methode: Konzepte in eigenen Worten erklären, Lücken erkennen. Verknüpft mit `cards` und `skilltree`. + +## Alltag & Organisation (additional) + +- **routines** — Morgen-/Abend-Routinen als geordnete Checklisten mit Timer pro Schritt. Ergänzt `habits`. +- **documents** *(ZK)* — Persönliches Dokumenten-Management: Pass, Ausweis, Versicherungen, Verträge. Ablaufdaten mit Erinnerungen. +- **addresses** — Adressen-Sammlung über `contacts` hinaus: Ärzte, Handwerker, Restaurants mit Bewertungen. + +## Social & Fun (additional) + +- **challenges** — Gemeinsame Challenges mit Freunden (30 Tage Sport, Bücher lesen). Leaderboard, Beweise per Foto. +- **mixtapes** — Kuratierte Playlists/Musikempfehlungen für Freunde, verknüpft mit `music`. + +## Meta & System + +- **dashboard** — Konfigurierbares Dashboard mit Widgets aus allen Modulen. Tages-Überblick: Wetter, Termine, Habits, Mood, Todos. +- **review** — Wöchentliche/monatliche/jährliche Reviews: automatisch Daten aus allen Modulen aggregieren, Trends zeigen, Reflexionsfragen. +- **export** — Daten-Export pro Modul (JSON, CSV, PDF). DSGVO-konform, "deine Daten gehören dir". +- **integrations** — Webhook/API-Anbindungen für externe Dienste (Spotify, Strava, Toggl). Feeds Daten in bestehende Module. + --- ## Next steps diff --git a/packages/shared-branding/src/app-icons.ts b/packages/shared-branding/src/app-icons.ts index f254bec69..94dcc87af 100644 --- a/packages/shared-branding/src/app-icons.ts +++ b/packages/shared-branding/src/app-icons.ts @@ -161,6 +161,11 @@ export const APP_ICONS = { // Warm amber→rose gradient to evoke excitement and novelty. `` ), + drink: svgToDataUrl( + // Water drop + glass — represents beverage tracking. + // Blue→cyan gradient for the hydration theme. + `` + ), who: svgToDataUrl( // Theatre mask silhouette in front of a question mark — references // the "guess who's behind the disguise" mechanic. Purple gradient. diff --git a/packages/shared-branding/src/mana-apps.ts b/packages/shared-branding/src/mana-apps.ts index b87f4b59f..12bb5a019 100644 --- a/packages/shared-branding/src/mana-apps.ts +++ b/packages/shared-branding/src/mana-apps.ts @@ -751,6 +751,23 @@ export const MANA_APPS: ManaApp[] = [ status: 'beta', requiredTier: 'guest', }, + { + id: 'drink', + name: 'Drink', + description: { + de: 'Getränke-Tracker', + en: 'Beverage Tracker', + }, + longDescription: { + de: 'Tracke alle Getränke — Wasser, Kaffee, Tee, Saft, Alkohol und mehr. Mit Tageszielen, Favoriten und Verlauf.', + en: 'Track all beverages — water, coffee, tea, juice, alcohol, and more. With daily goals, favourites, and history.', + }, + icon: APP_ICONS.drink, + color: '#3b82f6', + comingSoon: false, + status: 'development', + requiredTier: 'guest', + }, { id: 'who', name: 'Who',