mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 19:01:08 +02:00
feat(timeblocks): integrate guides, places, cards modules
Extend the unified TimeBlock system to 3 more modules. New TimeBlockTypes: guide, visit, study New SourceModules: guides, places, cards - guides: startRun() creates 'guide' block, completeRun() stamps endDate - places: recordVisit() + auto-visit tracking create 'visit' point-events - cards: add startStudySession/endStudySession with live 'study' blocks - Update analytics colors/labels, calendar filters, dashboard widgets Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
6ee1df390e
commit
29ad31c4ed
12 changed files with 154 additions and 12 deletions
|
|
@ -26,6 +26,9 @@
|
|||
Moon,
|
||||
GraduationCap,
|
||||
FlowerLotus,
|
||||
Compass,
|
||||
MapPin,
|
||||
BookOpen,
|
||||
} from '@mana/shared-icons';
|
||||
import { getIconComponent } from '@mana/shared-icons';
|
||||
import { formatDistanceToNow } from 'date-fns';
|
||||
|
|
@ -57,6 +60,9 @@
|
|||
sleep: Moon,
|
||||
practice: GraduationCap,
|
||||
cycle: FlowerLotus,
|
||||
guide: Compass,
|
||||
visit: MapPin,
|
||||
study: BookOpen,
|
||||
};
|
||||
|
||||
function timeAgo(iso: string): string {
|
||||
|
|
|
|||
|
|
@ -25,6 +25,9 @@
|
|||
Moon,
|
||||
GraduationCap,
|
||||
FlowerLotus,
|
||||
Compass,
|
||||
MapPin,
|
||||
BookOpen,
|
||||
} from '@mana/shared-icons';
|
||||
import { getIconComponent } from '@mana/shared-icons';
|
||||
import { format } from 'date-fns';
|
||||
|
|
@ -72,6 +75,9 @@
|
|||
sleep: { icon: Moon, label: 'Schlaf' },
|
||||
practice: { icon: GraduationCap, label: 'Übung' },
|
||||
cycle: { icon: FlowerLotus, label: 'Zyklus' },
|
||||
guide: { icon: Compass, label: 'Guide' },
|
||||
visit: { icon: MapPin, label: 'Besuch' },
|
||||
study: { icon: BookOpen, label: 'Lernen' },
|
||||
};
|
||||
|
||||
function formatBlockTime(block: TimeBlock): string {
|
||||
|
|
|
|||
|
|
@ -30,6 +30,9 @@ const TYPE_COLORS: Record<TimeBlockType, string> = {
|
|||
sleep: '#6366f1',
|
||||
practice: '#f97316',
|
||||
cycle: '#ec4899',
|
||||
guide: '#14b8a6',
|
||||
visit: '#a855f7',
|
||||
study: '#0ea5e9',
|
||||
};
|
||||
|
||||
const TYPE_LABELS: Record<TimeBlockType, string> = {
|
||||
|
|
@ -44,6 +47,9 @@ const TYPE_LABELS: Record<TimeBlockType, string> = {
|
|||
sleep: 'Schlaf',
|
||||
practice: 'Übung',
|
||||
cycle: 'Zyklus',
|
||||
guide: 'Guides',
|
||||
visit: 'Besuche',
|
||||
study: 'Lernen',
|
||||
};
|
||||
|
||||
function blockDuration(b: TimeBlock): number {
|
||||
|
|
|
|||
|
|
@ -23,7 +23,10 @@ export type TimeBlockType =
|
|||
| 'watering'
|
||||
| 'sleep'
|
||||
| 'practice'
|
||||
| 'cycle';
|
||||
| 'cycle'
|
||||
| 'guide'
|
||||
| 'visit'
|
||||
| 'study';
|
||||
|
||||
export type TimeBlockSourceModule =
|
||||
| 'calendar'
|
||||
|
|
@ -35,7 +38,10 @@ export type TimeBlockSourceModule =
|
|||
| 'planta'
|
||||
| 'dreams'
|
||||
| 'skilltree'
|
||||
| 'cycles';
|
||||
| 'cycles'
|
||||
| 'guides'
|
||||
| 'places'
|
||||
| 'cards';
|
||||
|
||||
// ─── Local Record Types (Dexie) ──────────────────────────
|
||||
|
||||
|
|
|
|||
|
|
@ -17,6 +17,9 @@
|
|||
Moon,
|
||||
GraduationCap,
|
||||
FlowerLotus,
|
||||
Compass,
|
||||
MapPin,
|
||||
BookOpen,
|
||||
} from '@mana/shared-icons';
|
||||
import { db } from '$lib/data/database';
|
||||
import { decryptRecords } from '$lib/data/crypto';
|
||||
|
|
@ -44,6 +47,9 @@
|
|||
{ type: 'sleep', label: 'Schlaf', icon: Moon },
|
||||
{ type: 'practice', label: 'Übung', icon: GraduationCap },
|
||||
{ type: 'cycle', label: 'Zyklus', icon: FlowerLotus },
|
||||
{ type: 'guide', label: 'Guides', icon: Compass },
|
||||
{ type: 'visit', label: 'Besuche', icon: MapPin },
|
||||
{ type: 'study', label: 'Lernen', icon: BookOpen },
|
||||
];
|
||||
|
||||
let allActive = $derived(
|
||||
|
|
|
|||
|
|
@ -36,6 +36,9 @@ let visibleBlockTypes = $state<Set<TimeBlockType>>(
|
|||
'sleep',
|
||||
'practice',
|
||||
'cycle',
|
||||
'guide',
|
||||
'visit',
|
||||
'study',
|
||||
])
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -9,7 +9,8 @@ import { CardsEvents } from '@mana/shared-utils/analytics';
|
|||
import { db } from '$lib/data/database';
|
||||
import { cardDeckTable, cardTable } from '../collections';
|
||||
import { toDeck } from '../queries';
|
||||
import { encryptRecord } from '$lib/data/crypto';
|
||||
import { encryptRecord, decryptRecord } from '$lib/data/crypto';
|
||||
import { createBlock, updateBlock } from '$lib/data/time-blocks/service';
|
||||
import type { LocalDeck } from '../types';
|
||||
import type { Deck, CreateDeckInput, UpdateDeckInput } from '../types';
|
||||
|
||||
|
|
@ -86,6 +87,54 @@ export const deckStore = {
|
|||
}
|
||||
},
|
||||
|
||||
async startStudySession(deckId: string): Promise<string | null> {
|
||||
const deck = await cardDeckTable.get(deckId);
|
||||
if (!deck) return null;
|
||||
|
||||
// Don't start a second session if one is already active
|
||||
if (deck.activeStudyBlockId) return deck.activeStudyBlockId;
|
||||
|
||||
const decrypted = await decryptRecord('cardDecks', { ...deck });
|
||||
const deckName = decrypted?.name ?? 'Deck';
|
||||
const now = new Date().toISOString();
|
||||
|
||||
const timeBlockId = await createBlock({
|
||||
startDate: now,
|
||||
endDate: null,
|
||||
isLive: true,
|
||||
kind: 'logged',
|
||||
type: 'study',
|
||||
sourceModule: 'cards',
|
||||
sourceId: deckId,
|
||||
title: `${deckName} lernen`,
|
||||
color: '#0ea5e9',
|
||||
});
|
||||
|
||||
await cardDeckTable.update(deckId, {
|
||||
activeStudyBlockId: timeBlockId,
|
||||
lastStudied: now,
|
||||
updatedAt: now,
|
||||
});
|
||||
|
||||
return timeBlockId;
|
||||
},
|
||||
|
||||
async endStudySession(deckId: string): Promise<void> {
|
||||
const deck = await cardDeckTable.get(deckId);
|
||||
if (!deck?.activeStudyBlockId) return;
|
||||
|
||||
const now = new Date().toISOString();
|
||||
await updateBlock(deck.activeStudyBlockId, {
|
||||
endDate: now,
|
||||
isLive: false,
|
||||
});
|
||||
|
||||
await cardDeckTable.update(deckId, {
|
||||
activeStudyBlockId: null,
|
||||
updatedAt: now,
|
||||
});
|
||||
},
|
||||
|
||||
clearError() {
|
||||
error = null;
|
||||
},
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ export interface LocalDeck extends BaseRecord {
|
|||
cardCount: number;
|
||||
lastStudied?: string | null;
|
||||
isPublic: boolean;
|
||||
activeStudyBlockId?: string | null;
|
||||
}
|
||||
|
||||
export interface LocalCard extends BaseRecord {
|
||||
|
|
|
|||
|
|
@ -8,7 +8,8 @@
|
|||
|
||||
import { guideTable, sectionTable, stepTable, runTable } from '../collections';
|
||||
import { toGuide, toSection, toStep, toRun } from '../queries';
|
||||
import { encryptRecord } from '$lib/data/crypto';
|
||||
import { encryptRecord, decryptRecord } from '$lib/data/crypto';
|
||||
import { createBlock, updateBlock, deleteBlock } from '$lib/data/time-blocks/service';
|
||||
import type {
|
||||
LocalGuide,
|
||||
LocalSection,
|
||||
|
|
@ -150,12 +151,32 @@ export const guidesStore = {
|
|||
// ─── Runs (Progress Tracking) ────────────────────────
|
||||
|
||||
async startRun(guideId: string): Promise<Run> {
|
||||
// Resolve guide title for the TimeBlock
|
||||
const guide = await guideTable.get(guideId);
|
||||
const decryptedGuide = guide ? await decryptRecord('guides', { ...guide }) : null;
|
||||
const guideTitle = decryptedGuide?.title ?? 'Guide';
|
||||
|
||||
const runId = crypto.randomUUID();
|
||||
const now = new Date().toISOString();
|
||||
|
||||
const timeBlockId = await createBlock({
|
||||
startDate: now,
|
||||
endDate: null,
|
||||
kind: 'logged',
|
||||
type: 'guide',
|
||||
sourceModule: 'guides',
|
||||
sourceId: runId,
|
||||
title: guideTitle,
|
||||
color: '#14b8a6',
|
||||
});
|
||||
|
||||
const newLocal: LocalRun = {
|
||||
id: crypto.randomUUID(),
|
||||
id: runId,
|
||||
guideId,
|
||||
startedAt: new Date().toISOString(),
|
||||
startedAt: now,
|
||||
completedAt: null,
|
||||
completedStepIds: [],
|
||||
timeBlockId,
|
||||
};
|
||||
const snapshot = toRun({ ...newLocal });
|
||||
await runTable.add(newLocal);
|
||||
|
|
@ -182,14 +203,23 @@ export const guidesStore = {
|
|||
},
|
||||
|
||||
async completeRun(runId: string): Promise<void> {
|
||||
const now = new Date().toISOString();
|
||||
const run = await runTable.get(runId);
|
||||
await runTable.update(runId, {
|
||||
completedAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
completedAt: now,
|
||||
updatedAt: now,
|
||||
});
|
||||
if (run?.timeBlockId) {
|
||||
await updateBlock(run.timeBlockId, { endDate: now });
|
||||
}
|
||||
},
|
||||
|
||||
async deleteRun(id: string): Promise<void> {
|
||||
const run = await runTable.get(id);
|
||||
const now = new Date().toISOString();
|
||||
if (run?.timeBlockId) {
|
||||
await deleteBlock(run.timeBlockId);
|
||||
}
|
||||
await runTable.update(id, { deletedAt: now, updatedAt: now });
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -66,6 +66,7 @@ export interface LocalRun extends BaseRecord {
|
|||
startedAt: string;
|
||||
completedAt: string | null;
|
||||
completedStepIds: string[];
|
||||
timeBlockId?: string | null;
|
||||
}
|
||||
|
||||
// ─── Domain Types (UI-facing) ─────────────────────────────
|
||||
|
|
|
|||
|
|
@ -5,7 +5,8 @@
|
|||
* This store only exposes mutations that write to IndexedDB.
|
||||
*/
|
||||
|
||||
import { encryptRecord } from '$lib/data/crypto';
|
||||
import { encryptRecord, decryptRecord } from '$lib/data/crypto';
|
||||
import { createBlock } from '$lib/data/time-blocks/service';
|
||||
import { placeTable } from '../collections';
|
||||
import { toPlace } from '../queries';
|
||||
import type { LocalPlace, Place, PlaceCategory } from '../types';
|
||||
|
|
@ -93,10 +94,25 @@ export const placesStore = {
|
|||
const local = await placeTable.get(id);
|
||||
if (!local) return;
|
||||
|
||||
const now = new Date().toISOString();
|
||||
const decrypted = await decryptRecord('places', { ...local });
|
||||
const placeName = decrypted?.name ?? 'Ort';
|
||||
|
||||
await placeTable.update(id, {
|
||||
visitCount: (local.visitCount ?? 0) + 1,
|
||||
lastVisitedAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
lastVisitedAt: now,
|
||||
updatedAt: now,
|
||||
});
|
||||
|
||||
await createBlock({
|
||||
startDate: now,
|
||||
endDate: now,
|
||||
kind: 'logged',
|
||||
type: 'visit',
|
||||
sourceModule: 'places',
|
||||
sourceId: id,
|
||||
title: placeName,
|
||||
color: '#a855f7',
|
||||
});
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
*/
|
||||
|
||||
import { decryptRecords, encryptRecord } from '$lib/data/crypto';
|
||||
import { createBlock } from '$lib/data/time-blocks/service';
|
||||
import { locationLogTable, placeTable } from '../collections';
|
||||
import { getDistanceKm, findNearestPlace, toPlace } from '../queries';
|
||||
import type { LocalLocationLog, LocalPlace } from '../types';
|
||||
|
|
@ -134,7 +135,7 @@ async function logPosition(pos: GeolocationPosition) {
|
|||
await encryptRecord('locationLogs', log);
|
||||
await locationLogTable.add(log);
|
||||
|
||||
// Update visit count on the matched place
|
||||
// Update visit count on the matched place + create TimeBlock
|
||||
if (nearest) {
|
||||
const local = await placeTable.get(nearest.id);
|
||||
if (local) {
|
||||
|
|
@ -143,6 +144,17 @@ async function logPosition(pos: GeolocationPosition) {
|
|||
lastVisitedAt: log.timestamp,
|
||||
updatedAt: new Date().toISOString(),
|
||||
});
|
||||
|
||||
await createBlock({
|
||||
startDate: log.timestamp,
|
||||
endDate: log.timestamp,
|
||||
kind: 'logged',
|
||||
type: 'visit',
|
||||
sourceModule: 'places',
|
||||
sourceId: nearest.id,
|
||||
title: nearest.name,
|
||||
color: '#a855f7',
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue