mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 21:01:08 +02:00
feat(dreams): scaffold Traumtagebuch module
Adds a new Dreams module to the unified Mana app for capturing dream journal entries with mood, lucid status, recurring symbols, and timeline insights. Founder-tier gated for now. - Dexie schema v5 with dreams, dreamSymbols, dreamTags - Mutation store with auto symbol counting on create/update/delete - ListView with quick capture, inline editor, mood picker, lucid toggle, monthly grouping, insights ribbon, context menu - Workbench registration with note → dream drop transform - New 'dream' DragType, dreams app icon, mana-apps catalog entry Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
b9fdf0802f
commit
8e71096a61
11 changed files with 1287 additions and 4 deletions
|
|
@ -259,6 +259,48 @@ registerApp({
|
|||
},
|
||||
});
|
||||
|
||||
registerApp({
|
||||
id: 'dreams',
|
||||
name: 'Dreams',
|
||||
color: '#6366F1',
|
||||
views: {
|
||||
list: { load: () => import('$lib/modules/dreams/ListView.svelte') },
|
||||
},
|
||||
contextMenuActions: [
|
||||
{
|
||||
id: 'new-dream',
|
||||
label: 'Neuer Traum',
|
||||
icon: Plus,
|
||||
action: () =>
|
||||
window.dispatchEvent(
|
||||
new CustomEvent('mana:quick-action', { detail: { app: 'dreams', action: 'new' } })
|
||||
),
|
||||
},
|
||||
],
|
||||
collection: 'dreams',
|
||||
paramKey: 'dreamId',
|
||||
dragType: 'dream',
|
||||
acceptsDropFrom: ['note'],
|
||||
transformIncoming: {
|
||||
note: (source) => ({
|
||||
title: source.title as string,
|
||||
content: (source.content as string) ?? '',
|
||||
}),
|
||||
},
|
||||
getDisplayData: (item) => ({
|
||||
title: (item.title as string) || 'Traum',
|
||||
subtitle: (item.dreamDate as string) ?? undefined,
|
||||
}),
|
||||
createItem: async (data) => {
|
||||
const { dreamsStore } = await import('$lib/modules/dreams/stores/dreams.svelte');
|
||||
const dream = await dreamsStore.createDream({
|
||||
title: (data.title as string) ?? null,
|
||||
content: (data.content as string) ?? '',
|
||||
});
|
||||
return dream.id;
|
||||
},
|
||||
});
|
||||
|
||||
registerApp({
|
||||
id: 'finance',
|
||||
name: 'Finance',
|
||||
|
|
|
|||
|
|
@ -405,6 +405,25 @@ db.version(4).stores({
|
|||
'id, startDate, kind, type, sourceModule, sourceId, parentBlockId, [sourceModule+sourceId], [type+startDate], [kind+startDate], [parentBlockId+recurrenceDate]',
|
||||
});
|
||||
|
||||
// ─── Version 5: Dreams (Traumtagebuch) ────────────────────────
|
||||
// Adds dreams, dreamSymbols, dreamTags tables.
|
||||
|
||||
db.version(5).stores({
|
||||
dreams: 'id, dreamDate, mood, isLucid, isPinned, isArchived, updatedAt',
|
||||
dreamSymbols: 'id, name, count, updatedAt',
|
||||
dreamTags: 'id, dreamId, tagId, [dreamId+tagId]',
|
||||
});
|
||||
|
||||
// ─── Version 6: Events (Social gatherings) ────────────────────
|
||||
// Distinct from calendar's `events` table — these are gatherings with guests/RSVPs.
|
||||
// Main table is `socialEvents` to avoid collision with calendar.events.
|
||||
|
||||
db.version(6).stores({
|
||||
socialEvents: 'id, status, timeBlockId, hostContactId, isPublished, [status+createdAt]',
|
||||
eventGuests: 'id, eventId, contactId, rsvpStatus, [eventId+rsvpStatus], [eventId+contactId]',
|
||||
eventInvitations: 'id, eventId, guestId, channel, [eventId+guestId]',
|
||||
});
|
||||
|
||||
// ─── Sync App Map ──────────────────────────────────────────
|
||||
// Maps each table to its appId for sync routing.
|
||||
// The SyncEngine uses this to group pending changes and push to /sync/{appId}.
|
||||
|
|
@ -447,6 +466,8 @@ export const SYNC_APP_MAP: Record<string, string[]> = {
|
|||
guides: ['guides', 'sections', 'steps', 'guideCollections', 'runs', 'guideTags'],
|
||||
habits: ['habits', 'habitLogs'],
|
||||
notes: ['notes', 'noteTags'],
|
||||
dreams: ['dreams', 'dreamSymbols', 'dreamTags'],
|
||||
events: ['socialEvents', 'eventGuests', 'eventInvitations'],
|
||||
finance: ['transactions', 'financeCategories', 'budgets'],
|
||||
places: ['places', 'locationLogs', 'placeTags'],
|
||||
tags: ['globalTags', 'tagGroups'],
|
||||
|
|
@ -518,6 +539,8 @@ export const TABLE_TO_SYNC_NAME: Record<string, string> = {
|
|||
guideCollections: 'collections',
|
||||
// finance
|
||||
financeCategories: 'categories',
|
||||
// events (social gatherings)
|
||||
socialEvents: 'events',
|
||||
// shared: tags
|
||||
globalTags: 'tags',
|
||||
tagGroups: 'tagGroups',
|
||||
|
|
@ -550,11 +573,52 @@ export function fromSyncName(appId: string, syncCollection: string): string {
|
|||
// Automatically records pending changes for every write to sync-relevant tables.
|
||||
// This means module stores (taskTable.add(), etc.) don't need manual trackChange() calls.
|
||||
|
||||
let _applyingServerChanges = false;
|
||||
/**
|
||||
* Tables that are currently having server changes applied. Hooks for tables
|
||||
* in this set skip pending-change tracking (sync loop guard) — but writes to
|
||||
* OTHER tables continue tracking normally, so a user typing into chat while
|
||||
* todo is syncing no longer silently drops the chat write.
|
||||
*
|
||||
* Replaces a single global boolean that previously caused a cross-app race:
|
||||
* one app's apply could swallow another app's writes for the duration.
|
||||
*/
|
||||
const _applyingTables = new Set<string>();
|
||||
|
||||
/** Set to true while applying server changes to prevent sync loops. */
|
||||
/**
|
||||
* Marks one or more tables as "currently applying server changes".
|
||||
* Returned dispose function MUST be called (use try/finally) to clear them.
|
||||
*/
|
||||
export function beginApplyingTables(tables: Iterable<string>): () => void {
|
||||
const added: string[] = [];
|
||||
for (const t of tables) {
|
||||
if (!_applyingTables.has(t)) {
|
||||
_applyingTables.add(t);
|
||||
added.push(t);
|
||||
}
|
||||
}
|
||||
return () => {
|
||||
for (const t of added) _applyingTables.delete(t);
|
||||
};
|
||||
}
|
||||
|
||||
/** True when a write to `tableName` should bypass the pending-change hook. */
|
||||
export function isApplyingTable(tableName: string): boolean {
|
||||
return _applyingTables.has(tableName);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Legacy single-flag API kept temporarily for any external
|
||||
* caller. Prefer `beginApplyingTables` so per-table races stay impossible.
|
||||
* When `v === true` it marks every sync-tracked table; `false` clears them.
|
||||
*/
|
||||
export function setApplyingServerChanges(v: boolean): void {
|
||||
_applyingServerChanges = v;
|
||||
if (v) {
|
||||
for (const tables of Object.values(SYNC_APP_MAP)) {
|
||||
for (const t of tables) _applyingTables.add(t);
|
||||
}
|
||||
} else {
|
||||
_applyingTables.clear();
|
||||
}
|
||||
}
|
||||
|
||||
const pendingChangesTable = db.table('_pendingChanges');
|
||||
|
|
|
|||
624
apps/mana/apps/web/src/lib/modules/dreams/ListView.svelte
Normal file
624
apps/mana/apps/web/src/lib/modules/dreams/ListView.svelte
Normal file
|
|
@ -0,0 +1,624 @@
|
|||
<!--
|
||||
Dreams — Workbench ListView
|
||||
Quick capture, inline edit, mood + lucid + symbol affordances.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import {
|
||||
computeInsights,
|
||||
formatDreamDate,
|
||||
groupByMonth,
|
||||
searchDreams,
|
||||
useAllDreams,
|
||||
} from './queries';
|
||||
import { dreamsStore } from './stores/dreams.svelte';
|
||||
import { MOOD_COLORS, MOOD_LABELS, type Dream, type DreamMood } from './types';
|
||||
import type { ViewProps } from '$lib/app-registry';
|
||||
import { ContextMenu, type ContextMenuItem } from '@mana/shared-ui';
|
||||
import { PencilSimple, PushPin, Trash } from '@mana/shared-icons';
|
||||
|
||||
let { navigate, goBack, params }: ViewProps = $props();
|
||||
|
||||
let dreams$ = useAllDreams();
|
||||
let dreams = $derived(dreams$.value);
|
||||
|
||||
let searchQuery = $state('');
|
||||
let editingId = $state<string | null>(null);
|
||||
let editTitle = $state('');
|
||||
let editContent = $state('');
|
||||
let editSymbols = $state('');
|
||||
let editMood = $state<DreamMood | null>(null);
|
||||
let editIsLucid = $state(false);
|
||||
let newTitle = $state('');
|
||||
|
||||
let filtered = $derived(searchDreams(dreams, searchQuery));
|
||||
let grouped = $derived(groupByMonth(filtered));
|
||||
let insights = $derived(computeInsights(dreams));
|
||||
|
||||
async function handleQuickCreate(e: KeyboardEvent) {
|
||||
if (e.key !== 'Enter' || !newTitle.trim()) return;
|
||||
e.preventDefault();
|
||||
const dream = await dreamsStore.createDream({ title: newTitle.trim() });
|
||||
newTitle = '';
|
||||
startEdit(dream);
|
||||
}
|
||||
|
||||
function startEdit(dream: Dream) {
|
||||
if (editingId && editingId !== dream.id) saveEdit();
|
||||
editingId = dream.id;
|
||||
editTitle = dream.title ?? '';
|
||||
editContent = dream.content;
|
||||
editSymbols = (dream.symbols ?? []).join(', ');
|
||||
editMood = dream.mood;
|
||||
editIsLucid = dream.isLucid;
|
||||
}
|
||||
|
||||
async function saveEdit() {
|
||||
if (!editingId) return;
|
||||
const symbols = editSymbols
|
||||
.split(',')
|
||||
.map((s) => s.trim())
|
||||
.filter(Boolean);
|
||||
await dreamsStore.updateDream(editingId, {
|
||||
title: editTitle.trim() || null,
|
||||
content: editContent,
|
||||
symbols,
|
||||
mood: editMood,
|
||||
isLucid: editIsLucid,
|
||||
});
|
||||
editingId = null;
|
||||
}
|
||||
|
||||
async function handleDelete(id: string) {
|
||||
await dreamsStore.deleteDream(id);
|
||||
if (editingId === id) editingId = null;
|
||||
}
|
||||
|
||||
// Context menu
|
||||
let ctxMenu = $state<{ visible: boolean; x: number; y: number; dream: Dream | null }>({
|
||||
visible: false,
|
||||
x: 0,
|
||||
y: 0,
|
||||
dream: null,
|
||||
});
|
||||
|
||||
function handleItemContextMenu(e: MouseEvent, dream: Dream) {
|
||||
e.preventDefault();
|
||||
ctxMenu = { visible: true, x: e.clientX, y: e.clientY, dream };
|
||||
}
|
||||
|
||||
let ctxMenuItems = $derived<ContextMenuItem[]>(
|
||||
ctxMenu.dream
|
||||
? [
|
||||
{
|
||||
id: 'edit',
|
||||
label: 'Bearbeiten',
|
||||
icon: PencilSimple,
|
||||
action: () => {
|
||||
if (ctxMenu.dream) startEdit(ctxMenu.dream);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'pin',
|
||||
label: ctxMenu.dream.isPinned ? 'Lösen' : 'Pinnen',
|
||||
icon: PushPin,
|
||||
action: () => {
|
||||
if (ctxMenu.dream) dreamsStore.togglePin(ctxMenu.dream.id);
|
||||
},
|
||||
},
|
||||
{ id: 'div', label: '', type: 'divider' as const },
|
||||
{
|
||||
id: 'delete',
|
||||
label: 'Löschen',
|
||||
icon: Trash,
|
||||
variant: 'danger' as const,
|
||||
action: () => {
|
||||
if (ctxMenu.dream) handleDelete(ctxMenu.dream.id);
|
||||
},
|
||||
},
|
||||
]
|
||||
: []
|
||||
);
|
||||
|
||||
const MOODS: DreamMood[] = ['angenehm', 'neutral', 'unangenehm', 'albtraum'];
|
||||
</script>
|
||||
|
||||
<div class="app-view">
|
||||
<!-- Quick create -->
|
||||
<form onsubmit={(e) => e.preventDefault()} class="quick-add">
|
||||
<span class="add-icon">🌙</span>
|
||||
<input
|
||||
class="add-input"
|
||||
type="text"
|
||||
placeholder="Was hast du geträumt? (Enter)"
|
||||
bind:value={newTitle}
|
||||
onkeydown={handleQuickCreate}
|
||||
/>
|
||||
</form>
|
||||
|
||||
<!-- Insights ribbon -->
|
||||
{#if insights.total > 0}
|
||||
<div class="insights">
|
||||
<span class="ins-stat">{insights.total} Träume</span>
|
||||
{#if insights.lucidCount > 0}
|
||||
<span class="ins-stat">✨ {insights.lucidCount} Klarträume</span>
|
||||
{/if}
|
||||
{#each insights.topSymbols as sym}
|
||||
<span class="ins-symbol">{sym.name} · {sym.count}</span>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Search -->
|
||||
{#if dreams.length > 5}
|
||||
<input
|
||||
class="search-input"
|
||||
type="text"
|
||||
placeholder="Träume durchsuchen..."
|
||||
bind:value={searchQuery}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<!-- Dream list -->
|
||||
<div class="dream-list">
|
||||
{#each grouped as group (group.label)}
|
||||
<div class="month-label">{group.label}</div>
|
||||
{#each group.dreams as dream (dream.id)}
|
||||
{#if editingId === dream.id}
|
||||
<!-- Inline editor -->
|
||||
<div
|
||||
class="dream-item editing"
|
||||
onkeydown={(e) => {
|
||||
if (e.key === 'Escape') saveEdit();
|
||||
}}
|
||||
>
|
||||
<input
|
||||
class="ed-title"
|
||||
type="text"
|
||||
bind:value={editTitle}
|
||||
placeholder="Titel (optional)..."
|
||||
autofocus
|
||||
/>
|
||||
<textarea
|
||||
class="ed-content"
|
||||
bind:value={editContent}
|
||||
placeholder="Erzähl mir den Traum..."
|
||||
rows="5"
|
||||
></textarea>
|
||||
<input
|
||||
class="ed-symbols"
|
||||
type="text"
|
||||
bind:value={editSymbols}
|
||||
placeholder="Symbole (Komma-getrennt): Wasser, Fliegen, Tür"
|
||||
/>
|
||||
|
||||
<div class="ed-row">
|
||||
<div class="mood-picker">
|
||||
{#each MOODS as mood}
|
||||
<button
|
||||
class="mood-btn"
|
||||
class:active={editMood === mood}
|
||||
style="--mood-color: {MOOD_COLORS[mood]}"
|
||||
onclick={() => (editMood = editMood === mood ? null : mood)}
|
||||
title={MOOD_LABELS[mood]}
|
||||
>
|
||||
<span class="mood-dot"></span>
|
||||
{MOOD_LABELS[mood]}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
<label class="lucid-toggle">
|
||||
<input type="checkbox" bind:checked={editIsLucid} />
|
||||
✨ Klartraum
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="ed-actions">
|
||||
<button class="ed-btn danger" onclick={() => handleDelete(dream.id)}>Löschen</button>
|
||||
<button class="ed-btn primary" onclick={saveEdit}>Fertig</button>
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<!-- Dream row -->
|
||||
<button
|
||||
class="dream-item"
|
||||
onclick={() => startEdit(dream)}
|
||||
oncontextmenu={(e) => handleItemContextMenu(e, dream)}
|
||||
>
|
||||
{#if dream.mood}
|
||||
<span class="mood-dot-row" style="background: {MOOD_COLORS[dream.mood]}"></span>
|
||||
{:else}
|
||||
<span class="mood-dot-row empty"></span>
|
||||
{/if}
|
||||
|
||||
<div class="dream-content">
|
||||
<div class="dream-top">
|
||||
<span class="dream-title">{dream.title || 'Traum ohne Titel'}</span>
|
||||
{#if dream.isLucid}<span class="badge lucid">✨</span>{/if}
|
||||
{#if dream.isPinned}<span class="badge">📌</span>{/if}
|
||||
{#if dream.isPrivate}<span class="badge">🔒</span>{/if}
|
||||
</div>
|
||||
{#if dream.content}
|
||||
<p class="dream-preview">{dream.content.split('\n')[0]}</p>
|
||||
{/if}
|
||||
<div class="dream-meta">
|
||||
<span>{formatDreamDate(dream.dreamDate)}</span>
|
||||
{#if dream.symbols.length > 0}
|
||||
<span class="dot">·</span>
|
||||
<span class="symbols">{dream.symbols.slice(0, 3).join(' · ')}</span>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
{/if}
|
||||
{/each}
|
||||
{/each}
|
||||
|
||||
{#if filtered.length === 0 && dreams.length > 0}
|
||||
<p class="empty">Keine Treffer</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if dreams.length === 0}
|
||||
<p class="empty">Tippe oben, um deinen ersten Traum festzuhalten.</p>
|
||||
{/if}
|
||||
|
||||
<ContextMenu
|
||||
visible={ctxMenu.visible}
|
||||
x={ctxMenu.x}
|
||||
y={ctxMenu.y}
|
||||
items={ctxMenuItems}
|
||||
onClose={() => (ctxMenu = { ...ctxMenu, visible: false, dream: null })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.app-view {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.625rem;
|
||||
padding: 1rem;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* ── Quick Add ─────────────────────────────── */
|
||||
.quick-add {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.375rem 0.5rem;
|
||||
border-radius: 0.375rem;
|
||||
border: 1px solid rgba(0, 0, 0, 0.08);
|
||||
background: transparent;
|
||||
}
|
||||
:global(.dark) .quick-add {
|
||||
border-color: rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
.add-icon {
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
.add-input {
|
||||
flex: 1;
|
||||
border: none;
|
||||
background: transparent;
|
||||
outline: none;
|
||||
font-size: 0.8125rem;
|
||||
color: #374151;
|
||||
}
|
||||
.add-input::placeholder {
|
||||
color: #c0bfba;
|
||||
}
|
||||
:global(.dark) .add-input {
|
||||
color: #f3f4f6;
|
||||
}
|
||||
:global(.dark) .add-input::placeholder {
|
||||
color: #4b5563;
|
||||
}
|
||||
|
||||
/* ── Insights ──────────────────────────────── */
|
||||
.insights {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
padding: 0.375rem 0.25rem;
|
||||
font-size: 0.6875rem;
|
||||
color: #9ca3af;
|
||||
}
|
||||
.ins-stat {
|
||||
font-weight: 500;
|
||||
}
|
||||
.ins-symbol {
|
||||
padding: 0.125rem 0.5rem;
|
||||
border-radius: 9999px;
|
||||
background: rgba(99, 102, 241, 0.08);
|
||||
color: #6366f1;
|
||||
}
|
||||
|
||||
/* ── Search ────────────────────────────────── */
|
||||
.search-input {
|
||||
padding: 0.3rem 0.5rem;
|
||||
border-radius: 0.375rem;
|
||||
border: 1px solid rgba(0, 0, 0, 0.08);
|
||||
background: transparent;
|
||||
font-size: 0.75rem;
|
||||
color: #374151;
|
||||
outline: none;
|
||||
}
|
||||
.search-input:focus {
|
||||
border-color: #6366f1;
|
||||
}
|
||||
:global(.dark) .search-input {
|
||||
border-color: rgba(255, 255, 255, 0.08);
|
||||
color: #f3f4f6;
|
||||
}
|
||||
|
||||
/* ── Dream List ────────────────────────────── */
|
||||
.dream-list {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.month-label {
|
||||
font-size: 0.625rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
color: #c0bfba;
|
||||
padding: 0.75rem 0.25rem 0.25rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
:global(.dark) .month-label {
|
||||
color: #4b5563;
|
||||
}
|
||||
|
||||
.dream-item {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 0.5rem;
|
||||
width: 100%;
|
||||
padding: 0.5rem 0.25rem;
|
||||
border: none;
|
||||
background: transparent;
|
||||
text-align: left;
|
||||
border-radius: 0.25rem;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s;
|
||||
}
|
||||
.dream-item:hover {
|
||||
background: rgba(0, 0, 0, 0.03);
|
||||
}
|
||||
:global(.dark) .dream-item:hover {
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
}
|
||||
|
||||
.mood-dot-row {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 9999px;
|
||||
flex-shrink: 0;
|
||||
margin-top: 0.4375rem;
|
||||
}
|
||||
.mood-dot-row.empty {
|
||||
background: rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
:global(.dark) .mood-dot-row.empty {
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
|
||||
.dream-content {
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.dream-top {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.375rem;
|
||||
}
|
||||
|
||||
.dream-title {
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 500;
|
||||
color: #374151;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
:global(.dark) .dream-title {
|
||||
color: #e5e7eb;
|
||||
}
|
||||
|
||||
.badge {
|
||||
font-size: 0.625rem;
|
||||
}
|
||||
|
||||
.dream-preview {
|
||||
font-size: 0.6875rem;
|
||||
color: #9ca3af;
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.dream-meta {
|
||||
display: flex;
|
||||
gap: 0.25rem;
|
||||
font-size: 0.625rem;
|
||||
color: #c0bfba;
|
||||
margin-top: 0.125rem;
|
||||
}
|
||||
.dream-meta .symbols {
|
||||
color: #6366f1;
|
||||
}
|
||||
.dream-meta .dot {
|
||||
opacity: 0.5;
|
||||
}
|
||||
:global(.dark) .dream-meta {
|
||||
color: #4b5563;
|
||||
}
|
||||
|
||||
/* ── Inline Editor ─────────────────────────── */
|
||||
.dream-item.editing {
|
||||
cursor: default;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
padding: 0.625rem;
|
||||
border: 1px solid rgba(99, 102, 241, 0.3);
|
||||
border-radius: 0.375rem;
|
||||
background: rgba(99, 102, 241, 0.03);
|
||||
}
|
||||
.dream-item.editing:hover {
|
||||
background: rgba(99, 102, 241, 0.03);
|
||||
}
|
||||
:global(.dark) .dream-item.editing {
|
||||
border-color: rgba(99, 102, 241, 0.4);
|
||||
background: rgba(99, 102, 241, 0.06);
|
||||
}
|
||||
|
||||
.ed-title {
|
||||
width: 100%;
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: #374151;
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 600;
|
||||
padding: 0;
|
||||
outline: none;
|
||||
}
|
||||
:global(.dark) .ed-title {
|
||||
color: #f3f4f6;
|
||||
}
|
||||
|
||||
.ed-content {
|
||||
width: 100%;
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: #374151;
|
||||
font-size: 0.75rem;
|
||||
padding: 0;
|
||||
outline: none;
|
||||
resize: vertical;
|
||||
min-height: 4rem;
|
||||
font-family: inherit;
|
||||
line-height: 1.5;
|
||||
}
|
||||
:global(.dark) .ed-content {
|
||||
color: #e5e7eb;
|
||||
}
|
||||
|
||||
.ed-symbols {
|
||||
width: 100%;
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-top: 1px dashed rgba(0, 0, 0, 0.08);
|
||||
padding: 0.25rem 0 0;
|
||||
font-size: 0.6875rem;
|
||||
color: #6366f1;
|
||||
outline: none;
|
||||
}
|
||||
.ed-symbols::placeholder {
|
||||
color: #c0bfba;
|
||||
}
|
||||
:global(.dark) .ed-symbols {
|
||||
border-top-color: rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
|
||||
.ed-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.mood-picker {
|
||||
display: flex;
|
||||
gap: 0.25rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.mood-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
padding: 0.125rem 0.5rem;
|
||||
border-radius: 9999px;
|
||||
border: 1px solid rgba(0, 0, 0, 0.08);
|
||||
background: transparent;
|
||||
font-size: 0.625rem;
|
||||
color: #9ca3af;
|
||||
cursor: pointer;
|
||||
}
|
||||
.mood-btn .mood-dot {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 9999px;
|
||||
background: var(--mood-color);
|
||||
}
|
||||
.mood-btn.active {
|
||||
border-color: var(--mood-color);
|
||||
color: var(--mood-color);
|
||||
background: color-mix(in srgb, var(--mood-color) 10%, transparent);
|
||||
}
|
||||
|
||||
.lucid-toggle {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
font-size: 0.6875rem;
|
||||
color: #9ca3af;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.ed-actions {
|
||||
display: flex;
|
||||
gap: 0.25rem;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.ed-btn {
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 0.25rem;
|
||||
font-size: 0.6875rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: #9ca3af;
|
||||
transition: all 0.15s;
|
||||
}
|
||||
.ed-btn:hover {
|
||||
background: rgba(0, 0, 0, 0.04);
|
||||
color: #374151;
|
||||
}
|
||||
:global(.dark) .ed-btn:hover {
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
color: #e5e7eb;
|
||||
}
|
||||
.ed-btn.primary {
|
||||
background: #6366f1;
|
||||
color: white;
|
||||
}
|
||||
.ed-btn.primary:hover {
|
||||
background: #5558e6;
|
||||
}
|
||||
.ed-btn.danger:hover {
|
||||
color: #ef4444;
|
||||
background: rgba(239, 68, 68, 0.08);
|
||||
}
|
||||
|
||||
.empty {
|
||||
padding: 2rem 0;
|
||||
text-align: center;
|
||||
font-size: 0.8125rem;
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.app-view {
|
||||
padding: 0.75rem;
|
||||
}
|
||||
.dream-item {
|
||||
min-height: 44px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
88
apps/mana/apps/web/src/lib/modules/dreams/collections.ts
Normal file
88
apps/mana/apps/web/src/lib/modules/dreams/collections.ts
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
/**
|
||||
* Dreams module — collection accessors and guest seed data.
|
||||
*/
|
||||
|
||||
import { db } from '$lib/data/database';
|
||||
import type { LocalDream, LocalDreamSymbol, LocalDreamTag } from './types';
|
||||
|
||||
// ─── Collection Accessors ──────────────────────────────────
|
||||
|
||||
export const dreamTable = db.table<LocalDream>('dreams');
|
||||
export const dreamSymbolTable = db.table<LocalDreamSymbol>('dreamSymbols');
|
||||
export const dreamTagTable = db.table<LocalDreamTag>('dreamTags');
|
||||
|
||||
// ─── Guest Seed ────────────────────────────────────────────
|
||||
|
||||
const today = new Date().toISOString().slice(0, 10);
|
||||
const yesterday = new Date(Date.now() - 86_400_000).toISOString().slice(0, 10);
|
||||
|
||||
export const DREAMS_GUEST_SEED = {
|
||||
dreams: [
|
||||
{
|
||||
id: 'dream-welcome',
|
||||
title: 'Willkommen im Traumtagebuch',
|
||||
content:
|
||||
'Notiere deine Träume direkt nach dem Aufwachen. Je früher du sie festhältst, desto mehr Details bleiben dir.\n\n**Tipps:**\n- Stichworte reichen — Sätze bilden sich später.\n- Stimmung und Klarheit helfen bei Mustern über Wochen.\n- Pinne wiederkehrende Träume.',
|
||||
dreamDate: today,
|
||||
mood: 'angenehm',
|
||||
clarity: 4,
|
||||
isLucid: false,
|
||||
isRecurring: false,
|
||||
sleepQuality: null,
|
||||
bedtime: null,
|
||||
wakeTime: null,
|
||||
location: null,
|
||||
people: [],
|
||||
emotions: ['Ruhe', 'Neugier'],
|
||||
symbols: [],
|
||||
audioPath: null,
|
||||
transcript: null,
|
||||
interpretation: null,
|
||||
aiInterpretation: null,
|
||||
isPrivate: false,
|
||||
isPinned: true,
|
||||
isArchived: false,
|
||||
},
|
||||
{
|
||||
id: 'dream-fliegen',
|
||||
title: 'Über der Stadt geflogen',
|
||||
content:
|
||||
'Ich konnte mich aus dem Bett heben und über die Stadt fliegen. Alles war weich und leuchtete in goldenem Licht.',
|
||||
dreamDate: yesterday,
|
||||
mood: 'angenehm',
|
||||
clarity: 5,
|
||||
isLucid: true,
|
||||
isRecurring: false,
|
||||
sleepQuality: 4,
|
||||
bedtime: '23:30',
|
||||
wakeTime: '07:15',
|
||||
location: 'Über einer fremden Stadt',
|
||||
people: [],
|
||||
emotions: ['Freiheit', 'Staunen'],
|
||||
symbols: ['Fliegen', 'Licht'],
|
||||
audioPath: null,
|
||||
transcript: null,
|
||||
interpretation: 'Gefühl von Kontrolle und Leichtigkeit nach einer entspannten Woche.',
|
||||
aiInterpretation: null,
|
||||
isPrivate: false,
|
||||
isPinned: false,
|
||||
isArchived: false,
|
||||
},
|
||||
] satisfies LocalDream[],
|
||||
dreamSymbols: [
|
||||
{
|
||||
id: 'dream-symbol-fliegen',
|
||||
name: 'Fliegen',
|
||||
meaning: 'Freiheit, Loslösung, Kontrolle',
|
||||
color: '#6366f1',
|
||||
count: 1,
|
||||
},
|
||||
{
|
||||
id: 'dream-symbol-licht',
|
||||
name: 'Licht',
|
||||
meaning: 'Klarheit, Bewusstsein',
|
||||
color: '#f59e0b',
|
||||
count: 1,
|
||||
},
|
||||
] satisfies LocalDreamSymbol[],
|
||||
};
|
||||
34
apps/mana/apps/web/src/lib/modules/dreams/index.ts
Normal file
34
apps/mana/apps/web/src/lib/modules/dreams/index.ts
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
/**
|
||||
* Dreams module — barrel exports.
|
||||
*/
|
||||
|
||||
// ─── Stores ──────────────────────────────────────────────
|
||||
export { dreamsStore } from './stores/dreams.svelte';
|
||||
|
||||
// ─── Queries ─────────────────────────────────────────────
|
||||
export {
|
||||
useAllDreams,
|
||||
useAllDreamSymbols,
|
||||
toDream,
|
||||
toDreamSymbol,
|
||||
searchDreams,
|
||||
groupByMonth,
|
||||
formatDreamDate,
|
||||
computeInsights,
|
||||
} from './queries';
|
||||
|
||||
// ─── Collections ─────────────────────────────────────────
|
||||
export { dreamTable, dreamSymbolTable, dreamTagTable, DREAMS_GUEST_SEED } from './collections';
|
||||
|
||||
// ─── Types ───────────────────────────────────────────────
|
||||
export { MOOD_COLORS, MOOD_LABELS } from './types';
|
||||
export type {
|
||||
LocalDream,
|
||||
LocalDreamSymbol,
|
||||
LocalDreamTag,
|
||||
Dream,
|
||||
DreamSymbol,
|
||||
DreamMood,
|
||||
DreamClarity,
|
||||
SleepQuality,
|
||||
} from './types';
|
||||
142
apps/mana/apps/web/src/lib/modules/dreams/queries.ts
Normal file
142
apps/mana/apps/web/src/lib/modules/dreams/queries.ts
Normal file
|
|
@ -0,0 +1,142 @@
|
|||
/**
|
||||
* Reactive Queries & Pure Helpers for Dreams module.
|
||||
*/
|
||||
|
||||
import { useLiveQueryWithDefault } from '@mana/local-store/svelte';
|
||||
import { db } from '$lib/data/database';
|
||||
import type { Dream, DreamSymbol, LocalDream, LocalDreamSymbol } from './types';
|
||||
|
||||
// ─── Type Converters ───────────────────────────────────────
|
||||
|
||||
export function toDream(local: LocalDream): Dream {
|
||||
return {
|
||||
id: local.id,
|
||||
title: local.title,
|
||||
content: local.content,
|
||||
dreamDate: local.dreamDate,
|
||||
mood: local.mood,
|
||||
clarity: local.clarity,
|
||||
isLucid: local.isLucid,
|
||||
isRecurring: local.isRecurring,
|
||||
sleepQuality: local.sleepQuality,
|
||||
bedtime: local.bedtime,
|
||||
wakeTime: local.wakeTime,
|
||||
location: local.location,
|
||||
people: local.people ?? [],
|
||||
emotions: local.emotions ?? [],
|
||||
symbols: local.symbols ?? [],
|
||||
audioPath: local.audioPath,
|
||||
transcript: local.transcript,
|
||||
interpretation: local.interpretation,
|
||||
aiInterpretation: local.aiInterpretation,
|
||||
isPrivate: local.isPrivate,
|
||||
isPinned: local.isPinned,
|
||||
isArchived: local.isArchived,
|
||||
createdAt: local.createdAt ?? new Date().toISOString(),
|
||||
updatedAt: local.updatedAt ?? new Date().toISOString(),
|
||||
};
|
||||
}
|
||||
|
||||
export function toDreamSymbol(local: LocalDreamSymbol): DreamSymbol {
|
||||
return {
|
||||
id: local.id,
|
||||
name: local.name,
|
||||
meaning: local.meaning,
|
||||
color: local.color,
|
||||
count: local.count ?? 0,
|
||||
createdAt: local.createdAt ?? new Date().toISOString(),
|
||||
updatedAt: local.updatedAt ?? new Date().toISOString(),
|
||||
};
|
||||
}
|
||||
|
||||
// ─── Live Queries ──────────────────────────────────────────
|
||||
|
||||
export function useAllDreams() {
|
||||
return useLiveQueryWithDefault(async () => {
|
||||
const locals = await db.table<LocalDream>('dreams').toArray();
|
||||
return locals
|
||||
.filter((d) => !d.deletedAt && !d.isArchived)
|
||||
.map(toDream)
|
||||
.sort((a, b) => {
|
||||
if (a.isPinned !== b.isPinned) return a.isPinned ? -1 : 1;
|
||||
return b.dreamDate.localeCompare(a.dreamDate);
|
||||
});
|
||||
}, [] as Dream[]);
|
||||
}
|
||||
|
||||
export function useAllDreamSymbols() {
|
||||
return useLiveQueryWithDefault(async () => {
|
||||
const locals = await db.table<LocalDreamSymbol>('dreamSymbols').toArray();
|
||||
return locals
|
||||
.filter((s) => !s.deletedAt)
|
||||
.map(toDreamSymbol)
|
||||
.sort((a, b) => b.count - a.count);
|
||||
}, [] as DreamSymbol[]);
|
||||
}
|
||||
|
||||
// ─── Pure Helpers ──────────────────────────────────────────
|
||||
|
||||
/** Search dreams by title, content, location, symbols and emotions. */
|
||||
export function searchDreams(dreams: Dream[], query: string): Dream[] {
|
||||
if (!query.trim()) return dreams;
|
||||
const q = query.toLowerCase();
|
||||
return dreams.filter((d) => {
|
||||
const haystack = [
|
||||
d.title,
|
||||
d.content,
|
||||
d.location,
|
||||
d.interpretation,
|
||||
...(d.symbols ?? []),
|
||||
...(d.emotions ?? []),
|
||||
...(d.people ?? []),
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' ')
|
||||
.toLowerCase();
|
||||
return haystack.includes(q);
|
||||
});
|
||||
}
|
||||
|
||||
/** Group dreams by month label (e.g. "April 2026"). */
|
||||
export function groupByMonth(dreams: Dream[]): Array<{ label: string; dreams: Dream[] }> {
|
||||
const groups = new Map<string, Dream[]>();
|
||||
for (const d of dreams) {
|
||||
const date = new Date(d.dreamDate);
|
||||
const label = date.toLocaleDateString('de-DE', { month: 'long', year: 'numeric' });
|
||||
if (!groups.has(label)) groups.set(label, []);
|
||||
groups.get(label)!.push(d);
|
||||
}
|
||||
return Array.from(groups, ([label, dreams]) => ({ label, dreams }));
|
||||
}
|
||||
|
||||
/** Format the dream date relative to today. */
|
||||
export function formatDreamDate(iso: string): string {
|
||||
const date = new Date(iso);
|
||||
const today = new Date();
|
||||
const diffDays = Math.floor((today.getTime() - date.getTime()) / 86_400_000);
|
||||
if (diffDays === 0) return 'Heute Nacht';
|
||||
if (diffDays === 1) return 'Gestern Nacht';
|
||||
if (diffDays < 7) return `vor ${diffDays} Tagen`;
|
||||
return date.toLocaleDateString('de-DE', { day: 'numeric', month: 'short', year: 'numeric' });
|
||||
}
|
||||
|
||||
/** Compute insights snapshot from dreams collection. */
|
||||
export function computeInsights(dreams: Dream[]) {
|
||||
const total = dreams.length;
|
||||
const lucidCount = dreams.filter((d) => d.isLucid).length;
|
||||
const symbolCounts = new Map<string, number>();
|
||||
for (const d of dreams) {
|
||||
for (const sym of d.symbols ?? []) {
|
||||
symbolCounts.set(sym, (symbolCounts.get(sym) ?? 0) + 1);
|
||||
}
|
||||
}
|
||||
const topSymbols = Array.from(symbolCounts, ([name, count]) => ({ name, count }))
|
||||
.sort((a, b) => b.count - a.count)
|
||||
.slice(0, 5);
|
||||
return {
|
||||
total,
|
||||
lucidCount,
|
||||
lucidRate: total ? lucidCount / total : 0,
|
||||
topSymbols,
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,165 @@
|
|||
/**
|
||||
* Dreams Store — Mutation-Only Service
|
||||
*/
|
||||
|
||||
import { dreamSymbolTable, dreamTable } from '../collections';
|
||||
import { toDream } from '../queries';
|
||||
import type { DreamClarity, DreamMood, LocalDream, SleepQuality } from '../types';
|
||||
|
||||
function todayIsoDate(): string {
|
||||
return new Date().toISOString().slice(0, 10);
|
||||
}
|
||||
|
||||
export const dreamsStore = {
|
||||
async createDream(data: {
|
||||
title?: string | null;
|
||||
content?: string;
|
||||
dreamDate?: string;
|
||||
mood?: DreamMood | null;
|
||||
clarity?: DreamClarity | null;
|
||||
isLucid?: boolean;
|
||||
symbols?: string[];
|
||||
emotions?: string[];
|
||||
}) {
|
||||
const newLocal: LocalDream = {
|
||||
id: crypto.randomUUID(),
|
||||
title: data.title ?? null,
|
||||
content: data.content ?? '',
|
||||
dreamDate: data.dreamDate ?? todayIsoDate(),
|
||||
mood: data.mood ?? null,
|
||||
clarity: data.clarity ?? null,
|
||||
isLucid: data.isLucid ?? false,
|
||||
isRecurring: false,
|
||||
sleepQuality: null,
|
||||
bedtime: null,
|
||||
wakeTime: null,
|
||||
location: null,
|
||||
people: [],
|
||||
emotions: data.emotions ?? [],
|
||||
symbols: data.symbols ?? [],
|
||||
audioPath: null,
|
||||
transcript: null,
|
||||
interpretation: null,
|
||||
aiInterpretation: null,
|
||||
isPrivate: false,
|
||||
isPinned: false,
|
||||
isArchived: false,
|
||||
};
|
||||
|
||||
await dreamTable.add(newLocal);
|
||||
await this.touchSymbols(newLocal.symbols, +1);
|
||||
return toDream(newLocal);
|
||||
},
|
||||
|
||||
async updateDream(
|
||||
id: string,
|
||||
data: Partial<
|
||||
Pick<
|
||||
LocalDream,
|
||||
| 'title'
|
||||
| 'content'
|
||||
| 'dreamDate'
|
||||
| 'mood'
|
||||
| 'clarity'
|
||||
| 'isLucid'
|
||||
| 'isRecurring'
|
||||
| 'sleepQuality'
|
||||
| 'bedtime'
|
||||
| 'wakeTime'
|
||||
| 'location'
|
||||
| 'people'
|
||||
| 'emotions'
|
||||
| 'symbols'
|
||||
| 'interpretation'
|
||||
| 'aiInterpretation'
|
||||
| 'isPrivate'
|
||||
| 'isPinned'
|
||||
| 'isArchived'
|
||||
>
|
||||
>
|
||||
) {
|
||||
if (data.symbols) {
|
||||
const existing = await dreamTable.get(id);
|
||||
if (existing) {
|
||||
const oldSet = new Set(existing.symbols ?? []);
|
||||
const newSet = new Set(data.symbols);
|
||||
const added = [...newSet].filter((s) => !oldSet.has(s));
|
||||
const removed = [...oldSet].filter((s) => !newSet.has(s));
|
||||
if (added.length) await this.touchSymbols(added, +1);
|
||||
if (removed.length) await this.touchSymbols(removed, -1);
|
||||
}
|
||||
}
|
||||
|
||||
await dreamTable.update(id, {
|
||||
...data,
|
||||
updatedAt: new Date().toISOString(),
|
||||
});
|
||||
},
|
||||
|
||||
async deleteDream(id: string) {
|
||||
const existing = await dreamTable.get(id);
|
||||
if (existing?.symbols?.length) {
|
||||
await this.touchSymbols(existing.symbols, -1);
|
||||
}
|
||||
await dreamTable.update(id, {
|
||||
deletedAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
});
|
||||
},
|
||||
|
||||
async togglePin(id: string) {
|
||||
const dream = await dreamTable.get(id);
|
||||
if (!dream) return;
|
||||
await dreamTable.update(id, {
|
||||
isPinned: !dream.isPinned,
|
||||
updatedAt: new Date().toISOString(),
|
||||
});
|
||||
},
|
||||
|
||||
async toggleLucid(id: string) {
|
||||
const dream = await dreamTable.get(id);
|
||||
if (!dream) return;
|
||||
await dreamTable.update(id, {
|
||||
isLucid: !dream.isLucid,
|
||||
updatedAt: new Date().toISOString(),
|
||||
});
|
||||
},
|
||||
|
||||
async setMood(id: string, mood: DreamMood | null) {
|
||||
await dreamTable.update(id, {
|
||||
mood,
|
||||
updatedAt: new Date().toISOString(),
|
||||
});
|
||||
},
|
||||
|
||||
async setSleepQuality(id: string, quality: SleepQuality | null) {
|
||||
await dreamTable.update(id, {
|
||||
sleepQuality: quality,
|
||||
updatedAt: new Date().toISOString(),
|
||||
});
|
||||
},
|
||||
|
||||
/** Increment or decrement counts for the given symbol names. Creates symbols on demand. */
|
||||
async touchSymbols(names: string[], delta: number) {
|
||||
for (const name of names) {
|
||||
const trimmed = name.trim();
|
||||
if (!trimmed) continue;
|
||||
const existing = await dreamSymbolTable.where('name').equals(trimmed).first();
|
||||
if (existing) {
|
||||
const next = Math.max(0, (existing.count ?? 0) + delta);
|
||||
await dreamSymbolTable.update(existing.id, {
|
||||
count: next,
|
||||
updatedAt: new Date().toISOString(),
|
||||
});
|
||||
} else if (delta > 0) {
|
||||
await dreamSymbolTable.add({
|
||||
id: crypto.randomUUID(),
|
||||
name: trimmed,
|
||||
meaning: null,
|
||||
color: null,
|
||||
count: delta,
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
102
apps/mana/apps/web/src/lib/modules/dreams/types.ts
Normal file
102
apps/mana/apps/web/src/lib/modules/dreams/types.ts
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
/**
|
||||
* Dreams module types — Traumtagebuch.
|
||||
*/
|
||||
|
||||
import type { BaseRecord } from '@mana/local-store';
|
||||
|
||||
export type DreamMood = 'angenehm' | 'neutral' | 'unangenehm' | 'albtraum';
|
||||
export type DreamClarity = 1 | 2 | 3 | 4 | 5;
|
||||
export type SleepQuality = 1 | 2 | 3 | 4 | 5;
|
||||
|
||||
// ─── Local Record Types (Dexie) ───────────────────────────
|
||||
|
||||
export interface LocalDream extends BaseRecord {
|
||||
title: string | null;
|
||||
content: string;
|
||||
dreamDate: string; // ISO date (YYYY-MM-DD) — die Nacht, in der geträumt wurde
|
||||
mood: DreamMood | null;
|
||||
clarity: DreamClarity | null;
|
||||
isLucid: boolean;
|
||||
isRecurring: boolean;
|
||||
sleepQuality: SleepQuality | null;
|
||||
bedtime: string | null; // ISO time (HH:mm)
|
||||
wakeTime: string | null; // ISO time (HH:mm)
|
||||
location: string | null;
|
||||
people: string[];
|
||||
emotions: string[];
|
||||
symbols: string[];
|
||||
audioPath: string | null;
|
||||
transcript: string | null;
|
||||
interpretation: string | null;
|
||||
aiInterpretation: string | null;
|
||||
isPrivate: boolean;
|
||||
isPinned: boolean;
|
||||
isArchived: boolean;
|
||||
}
|
||||
|
||||
export interface LocalDreamSymbol extends BaseRecord {
|
||||
name: string;
|
||||
meaning: string | null;
|
||||
color: string | null;
|
||||
count: number;
|
||||
}
|
||||
|
||||
export interface LocalDreamTag extends BaseRecord {
|
||||
dreamId: string;
|
||||
tagId: string;
|
||||
}
|
||||
|
||||
// ─── Domain Types ─────────────────────────────────────────
|
||||
|
||||
export interface Dream {
|
||||
id: string;
|
||||
title: string | null;
|
||||
content: string;
|
||||
dreamDate: string;
|
||||
mood: DreamMood | null;
|
||||
clarity: DreamClarity | null;
|
||||
isLucid: boolean;
|
||||
isRecurring: boolean;
|
||||
sleepQuality: SleepQuality | null;
|
||||
bedtime: string | null;
|
||||
wakeTime: string | null;
|
||||
location: string | null;
|
||||
people: string[];
|
||||
emotions: string[];
|
||||
symbols: string[];
|
||||
audioPath: string | null;
|
||||
transcript: string | null;
|
||||
interpretation: string | null;
|
||||
aiInterpretation: string | null;
|
||||
isPrivate: boolean;
|
||||
isPinned: boolean;
|
||||
isArchived: boolean;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface DreamSymbol {
|
||||
id: string;
|
||||
name: string;
|
||||
meaning: string | null;
|
||||
color: string | null;
|
||||
count: number;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
// ─── Constants ────────────────────────────────────────────
|
||||
|
||||
export const MOOD_COLORS: Record<DreamMood, string> = {
|
||||
angenehm: '#22c55e',
|
||||
neutral: '#9ca3af',
|
||||
unangenehm: '#f59e0b',
|
||||
albtraum: '#ef4444',
|
||||
};
|
||||
|
||||
export const MOOD_LABELS: Record<DreamMood, string> = {
|
||||
angenehm: 'Angenehm',
|
||||
neutral: 'Neutral',
|
||||
unangenehm: 'Unangenehm',
|
||||
albtraum: 'Albtraum',
|
||||
};
|
||||
|
|
@ -133,6 +133,9 @@ export const APP_ICONS = {
|
|||
notes: svgToDataUrl(
|
||||
`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><defs><linearGradient id="nt" x1="0%" y1="0%" x2="100%" y2="100%"><stop offset="0%" style="stop-color:#f59e0b"/><stop offset="100%" style="stop-color:#d97706"/></linearGradient></defs><rect width="100" height="100" rx="22" fill="url(#nt)"/><rect x="28" y="22" width="44" height="56" rx="4" stroke="white" stroke-width="4" fill="none"/><path d="M38 36h24M38 46h24M38 56h16" stroke="white" stroke-width="3" stroke-linecap="round"/></svg>`
|
||||
),
|
||||
dreams: svgToDataUrl(
|
||||
`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><defs><linearGradient id="dr" x1="0%" y1="0%" x2="100%" y2="100%"><stop offset="0%" style="stop-color:#6366f1"/><stop offset="100%" style="stop-color:#312e81"/></linearGradient></defs><rect width="100" height="100" rx="22" fill="url(#dr)"/><path d="M62 30a22 22 0 1 0 18 34 18 18 0 0 1-18-34z" fill="white"/><circle cx="32" cy="38" r="1.6" fill="white"/><circle cx="26" cy="58" r="1.2" fill="white" fill-opacity="0.8"/><circle cx="40" cy="68" r="1.4" fill="white" fill-opacity="0.7"/><circle cx="22" cy="46" r="1" fill="white" fill-opacity="0.6"/></svg>`
|
||||
),
|
||||
finance: svgToDataUrl(
|
||||
`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><defs><linearGradient id="fn" x1="0%" y1="0%" x2="100%" y2="100%"><stop offset="0%" style="stop-color:#22c55e"/><stop offset="100%" style="stop-color:#16a34a"/></linearGradient></defs><rect width="100" height="100" rx="22" fill="url(#fn)"/><circle cx="50" cy="50" r="22" stroke="white" stroke-width="4" fill="none"/><path d="M50 34v32M42 42c0-4 3.5-6 8-6s8 2 8 6-3.5 5-8 5-8 2-8 6 3.5 6 8 6 8-2 8-6" stroke="white" stroke-width="3" stroke-linecap="round" fill="none"/></svg>`
|
||||
),
|
||||
|
|
|
|||
|
|
@ -615,6 +615,23 @@ export const MANA_APPS: ManaApp[] = [
|
|||
status: 'development',
|
||||
requiredTier: 'founder',
|
||||
},
|
||||
{
|
||||
id: 'dreams',
|
||||
name: 'Dreams',
|
||||
description: {
|
||||
de: 'Traumtagebuch',
|
||||
en: 'Dream Journal',
|
||||
},
|
||||
longDescription: {
|
||||
de: 'Halte deine Träume fest, bevor sie verblassen. Stimmung, Klartraum-Status, wiederkehrende Symbole und Insights über die Zeit.',
|
||||
en: 'Capture your dreams before they fade. Mood, lucid status, recurring symbols, and insights over time.',
|
||||
},
|
||||
icon: APP_ICONS.dreams,
|
||||
color: '#6366f1',
|
||||
comingSoon: false,
|
||||
status: 'development',
|
||||
requiredTier: 'founder',
|
||||
},
|
||||
{
|
||||
id: 'finance',
|
||||
name: 'Finance',
|
||||
|
|
@ -777,6 +794,7 @@ export const APP_URLS: Record<AppIconId, { dev: string; prod: string }> = {
|
|||
guides: { dev: 'http://localhost:5173/guides', prod: 'https://mana.how/guides' },
|
||||
habits: { dev: 'http://localhost:5173/habits', prod: 'https://mana.how/habits' },
|
||||
notes: { dev: 'http://localhost:5173/notes', prod: 'https://mana.how/notes' },
|
||||
dreams: { dev: 'http://localhost:5173/dreams', prod: 'https://mana.how/dreams' },
|
||||
finance: { dev: 'http://localhost:5173/finance', prod: 'https://mana.how/finance' },
|
||||
places: { dev: 'http://localhost:5173/places', prod: 'https://mana.how/places' },
|
||||
wisekeep: { dev: 'http://localhost:5173/wisekeep', prod: 'https://mana.how/wisekeep' },
|
||||
|
|
|
|||
|
|
@ -23,7 +23,8 @@ export type DragType =
|
|||
| 'habit'
|
||||
| 'note'
|
||||
| 'transaction'
|
||||
| 'place';
|
||||
| 'place'
|
||||
| 'dream';
|
||||
|
||||
export interface DragPayload<T = Record<string, unknown>> {
|
||||
type: DragType;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue