feat(workspace): journal with one note per day and a calendar
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 2m6s
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 2m6s
- journal_days (migration 8) maps a person's day to an ordinary note;
GET /journal lists the days, POST /journal/{date} opens or creates one
- the journal sits on top of the notes sidebar: days below each other
(today first, older ones load while scrolling), a calendar with month
and year picker follows the day in view and jumps to any day
- the header shares the day in view; journal notes stay out of the folder
tree; can be switched off per device under Darstellung
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
261579f0f2
commit
9008d7c915
19 files changed
+864
-19
No files matched your search
@@ -0,0 +1,16 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { monthGrid, toISODate, parseISODate } from '../journal';
|
||||
|
||||
describe('journal dates', () => {
|
||||
it('builds a Monday-first month grid', () => {
|
||||
// October 2026 starts on a Thursday
|
||||
const weeks = monthGrid(2026, 9);
|
||||
expect(weeks[0]).toEqual([null, null, null, '2026-10-01', '2026-10-02', '2026-10-03', '2026-10-04']);
|
||||
expect(weeks.flat().filter(Boolean)).toHaveLength(31);
|
||||
expect(weeks.every((w) => w.length === 7)).toBe(true);
|
||||
});
|
||||
|
||||
it('round-trips local dates', () => {
|
||||
expect(toISODate(parseISODate('2026-03-01'))).toBe('2026-03-01');
|
||||
});
|
||||
});
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
DashboardStats,
|
||||
Document,
|
||||
TrashedDocument,
|
||||
JournalDay,
|
||||
BackupFile,
|
||||
DataSection,
|
||||
ApiKeyScope,
|
||||
@@ -723,6 +724,17 @@ export const api = {
|
||||
return request<TrashedDocument[]>('/api/v1/documents/trash');
|
||||
},
|
||||
|
||||
/** Days of the own journal (newest first) with their notes. */
|
||||
async getJournal(): Promise<JournalDay[]> {
|
||||
const res = await request<{ days: JournalDay[] }>('/api/v1/journal');
|
||||
return res.days || [];
|
||||
},
|
||||
|
||||
/** The note of a journal day (YYYY-MM-DD); created on first use. */
|
||||
async openJournalDay(date: string): Promise<Document> {
|
||||
return request<Document>(`/api/v1/journal/${date}`, { method: 'POST' });
|
||||
},
|
||||
|
||||
/** Creates a note from a bullet's children; the editor then replaces the branch by `link` itself. */
|
||||
async extractBranch(docId: string, blockId: string, clientId: string): Promise<{ document: Document; link: string }> {
|
||||
return request<{ document: Document; link: string }>(`/api/v1/documents/${docId}/extract`, {
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
// Journal helpers: dates are local calendar days as YYYY-MM-DD (the server stores them as given).
|
||||
|
||||
export const JOURNAL_ENABLED_KEY = 'mindsift_journal_enabled';
|
||||
|
||||
export function isJournalEnabled(): boolean {
|
||||
try {
|
||||
return localStorage.getItem(JOURNAL_ENABLED_KEY) !== 'false';
|
||||
} catch {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
export function setJournalEnabled(on: boolean) {
|
||||
try {
|
||||
localStorage.setItem(JOURNAL_ENABLED_KEY, on ? 'true' : 'false');
|
||||
} catch {
|
||||
// storage blocked: the setting simply does not stick
|
||||
}
|
||||
}
|
||||
|
||||
export function toISODate(d: Date): string {
|
||||
const m = String(d.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(d.getDate()).padStart(2, '0');
|
||||
return `${d.getFullYear()}-${m}-${day}`;
|
||||
}
|
||||
|
||||
export function todayISO(): string {
|
||||
return toISODate(new Date());
|
||||
}
|
||||
|
||||
export function parseISODate(date: string): Date {
|
||||
const [y, m, d] = date.split('-').map(Number);
|
||||
return new Date(y, (m || 1) - 1, d || 1);
|
||||
}
|
||||
|
||||
export const MONTHS = [
|
||||
'Januar', 'Februar', 'März', 'April', 'Mai', 'Juni',
|
||||
'Juli', 'August', 'September', 'Oktober', 'November', 'Dezember',
|
||||
];
|
||||
|
||||
/** "Fr, 30. Okt. 2026" for compact labels */
|
||||
export function shortDateLabel(date: string): string {
|
||||
return parseISODate(date).toLocaleDateString('de-DE', {
|
||||
weekday: 'short',
|
||||
day: 'numeric',
|
||||
month: 'short',
|
||||
year: 'numeric',
|
||||
});
|
||||
}
|
||||
|
||||
/** Weeks of a month for a Monday-first grid; days outside the month are null. */
|
||||
export function monthGrid(year: number, month: number): (string | null)[][] {
|
||||
const first = new Date(year, month, 1);
|
||||
const offset = (first.getDay() + 6) % 7; // Monday = 0
|
||||
const daysInMonth = new Date(year, month + 1, 0).getDate();
|
||||
const cells: (string | null)[] = Array(offset).fill(null);
|
||||
for (let d = 1; d <= daysInMonth; d++) cells.push(toISODate(new Date(year, month, d)));
|
||||
while (cells.length % 7 !== 0) cells.push(null);
|
||||
const weeks: (string | null)[][] = [];
|
||||
for (let i = 0; i < cells.length; i += 7) weeks.push(cells.slice(i, i + 7));
|
||||
return weeks;
|
||||
}
|
||||
@@ -322,6 +322,13 @@ export interface Backlink {
|
||||
}
|
||||
|
||||
/** A note in the trash (GET /documents/trash). */
|
||||
/** A day of the own journal; `empty` while its note has no text yet. */
|
||||
export interface JournalDay {
|
||||
date: string; // YYYY-MM-DD
|
||||
docId: string;
|
||||
empty: boolean;
|
||||
}
|
||||
|
||||
export interface TrashedDocument {
|
||||
id: string;
|
||||
title: string;
|
||||
|
||||
Reference in new issue
Block a user