import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { ArrowLeft, CalendarDays, ChevronDown, Users } from 'lucide-react'; import { Document, JournalDay, User } from '../../lib/types'; import { shortDateLabel, todayISO } from '../../lib/journal'; import { DocumentEditor } from './DocumentEditor'; import { JournalCalendar } from './JournalCalendar'; const PAGE = 7; interface JournalViewProps { days: JournalDay[]; /** Days jumped to over the calendar (shown even while still empty) */ extraDates: string[]; activeDate: string; onActiveDateChange: (date: string) => void; /** Set by the calendar; the view scrolls there (nonce: the same day again) */ jumpRequest: { date: string; nonce: number } | null; /** Opens (creates on first use) the note of a day */ onOpenDay: (date: string) => Promise; onJump: (date: string) => void; getDoc: (id: string) => Document | undefined; currentUser?: User | null; availableFolders: string[]; onOpenShareModal: (doc: Document) => void; onDocMetadataChange: (updated: Document) => void; onDeleteDoc: (docId: string) => void; onSelectTag: (tag: string) => void; onWikiLinkClick?: (pageTitle: string) => void; onOpenDocBlock?: (docId: string, blockId?: string) => void; onOpenTask?: (taskId: string) => void; onBackMobile: () => void; /** A day was saved (its calendar dot may change) */ onDaySaved: () => void; } // The journal: one note per day below each other, today on top, older days load while scrolling. export const JournalView: React.FC = ({ days, extraDates, activeDate, onActiveDateChange, jumpRequest, onOpenDay, onJump, getDoc, currentUser, availableFolders, onOpenShareModal, onDocMetadataChange, onDeleteDoc, onSelectTag, onWikiLinkClick, onOpenDocBlock, onOpenTask, onBackMobile, onDaySaved, }) => { const today = todayISO(); const docIdByDate = useMemo(() => new Map(days.map((d) => [d.date, d.docId])), [days]); const entryDates = useMemo(() => new Set(days.filter((d) => !d.empty).map((d) => d.date)), [days]); // Today, every day with text and the days jumped to – newest first const dates = useMemo(() => { const set = new Set([today, ...extraDates]); days.forEach((d) => { if (!d.empty) set.add(d.date); }); return Array.from(set).sort((a, b) => b.localeCompare(a)); }, [days, extraDates, today]); const [limit, setLimit] = useState(PAGE); const shown = dates.slice(0, limit); const [showCalendar, setShowCalendar] = useState(false); // Days on screen without a note yet (today, a day jumped to) get one const opening = useRef(new Set()); useEffect(() => { shown.forEach((date) => { if (!docIdByDate.has(date) && !opening.current.has(date)) { opening.current.add(date); void onOpenDay(date).finally(() => opening.current.delete(date)); } }); }, [shown, docIdByDate, onOpenDay]); const scrollRef = useRef(null); const sectionRefs = useRef(new Map()); // Jump: make sure the day is rendered, then scroll to it once its note is there const pendingJump = useRef(null); const handledJump = useRef(0); useEffect(() => { if (!jumpRequest || handledJump.current === jumpRequest.nonce) return; const idx = dates.indexOf(jumpRequest.date); if (idx === -1) return; // the parent adds the day first handledJump.current = jumpRequest.nonce; if (idx >= limit) setLimit(idx + PAGE); pendingJump.current = jumpRequest.date; }, [jumpRequest, dates, limit]); useEffect(() => { const date = pendingJump.current; if (!date) return; const el = sectionRefs.current.get(date); if (el && docIdByDate.has(date)) { pendingJump.current = null; el.scrollIntoView({ block: 'start' }); } }); // The day at the top of the scroll area is the active one (calendar, share button) const onScroll = useCallback(() => { const root = scrollRef.current; if (!root) return; const top = root.getBoundingClientRect().top + 48; let current = shown[0]; for (const date of shown) { const el = sectionRefs.current.get(date); if (!el) continue; if (el.getBoundingClientRect().top <= top) current = date; else break; } if (current && current !== activeDate) onActiveDateChange(current); }, [shown, activeDate, onActiveDateChange]); // Load older days when the end comes into view const sentinelRef = useRef(null); useEffect(() => { const el = sentinelRef.current; const root = scrollRef.current; if (!el || !root || typeof IntersectionObserver === 'undefined' || limit >= dates.length) return; const observer = new IntersectionObserver( ([entry]) => { if (entry.isIntersecting) setLimit((l) => l + PAGE); }, { root, rootMargin: '400px 0px' } ); observer.observe(el); return () => observer.disconnect(); }, [limit, dates.length]); const activeDocId = docIdByDate.get(activeDate); const activeDoc = activeDocId ? getDoc(activeDocId) : undefined; const sharedCount = activeDoc?.sharedWith?.length ?? 0; return (
{/* One header for all days: the day in view and its sharing */}
{activeDate !== today && ( )}
{showCalendar && (
{ setShowCalendar(false); onJump(date); }} />
)}
{shown.map((date) => { const docId = docIdByDate.get(date); return (
{ if (el) sectionRefs.current.set(date, el); else sectionRefs.current.delete(date); }} className="border-b border-border last:border-b-0 min-h-[8rem]" > {docId ? ( { if (status === 'saved') onDaySaved(); }} /> ) : (
{shortDateLabel(date)} wird geöffnet…
)}
); })} {limit < dates.length &&
} {/* Room below the oldest day, so every day can be scrolled to the top */}
); };