From 6cbb3bb544c34a9469c7b652d971f29fbed5c225 Mon Sep 17 00:00:00 2001 From: bot-hermes Date: Wed, 29 Jul 2026 19:23:56 +0000 Subject: [PATCH 1/4] feat: add daily notes page with date navigation and sidebar link --- apps/web/app/(dashboard)/daily-notes/page.tsx | 205 ++++++++++++++++++ apps/web/components/sidebar.tsx | 2 + 2 files changed, 207 insertions(+) create mode 100644 apps/web/app/(dashboard)/daily-notes/page.tsx diff --git a/apps/web/app/(dashboard)/daily-notes/page.tsx b/apps/web/app/(dashboard)/daily-notes/page.tsx new file mode 100644 index 0000000..b7e7b06 --- /dev/null +++ b/apps/web/app/(dashboard)/daily-notes/page.tsx @@ -0,0 +1,205 @@ +'use client'; + +import { useState, useEffect, useCallback, Suspense } from 'react'; +import { BookOpen, Plus, ChevronLeft, ChevronRight, Loader2 } from 'lucide-react'; +import dynamic from 'next/dynamic'; +import { format, addDays, subDays } from 'date-fns'; +import { toast } from 'sonner'; +import { Button } from '@/components/ui/button'; +import { Card } from '@/components/ui/card'; + +const NoteEditor = dynamic( + () => import('@/components/notes/note-editor').then((m) => m.NoteEditor), + { + ssr: false, + loading: () => ( +
+
Loading editor...
+
+ ), + } +); + +interface Note { + id: string; + title: string; + content: string | null; + createdAt: string; + updatedAt: string; +} + +export default function DailyNotesPage() { + const [currentDate, setCurrentDate] = useState(new Date()); + const [note, setNote] = useState(null); + const [loading, setLoading] = useState(true); + const [creating, setCreating] = useState(false); + const [saveStatus, setSaveStatus] = useState<'Saved' | 'Saving' | 'Failed'>('Saved'); + + const dateStr = format(currentDate, 'yyyy-MM-dd'); + const displayDate = format(currentDate, 'EEEE, MMMM d, yyyy'); + + const fetchDailyNote = useCallback(async () => { + setLoading(true); + setNote(null); + try { + const res = await fetch(`/api/notes/daily?date=${dateStr}`); + if (!res.ok) throw new Error('Failed to fetch daily note'); + const data = await res.json(); + if (data.note) { + setNote(data.note); + } + } catch (err) { + console.error('Failed to fetch daily note:', err); + } finally { + setLoading(false); + } + }, [dateStr]); + + useEffect(() => { + fetchDailyNote(); + }, [fetchDailyNote]); + + async function handleCreateDailyNote() { + setCreating(true); + try { + const res = await fetch('/api/notes/daily', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ date: dateStr }), + }); + if (!res.ok) { + const text = await res.text(); + console.error('Failed to create daily note', text); + toast.error('Failed to create daily note'); + return; + } + const createdNote = await res.json(); + setNote(createdNote); + toast.success('Daily note created'); + } catch (err) { + console.error('Failed to create daily note:', err); + toast.error('Failed to create daily note'); + } finally { + setCreating(false); + } + } + + function navigate(delta: number) { + const next = delta > 0 ? addDays(currentDate, 1) : subDays(currentDate, 1); + setCurrentDate(next); + } + + function goToToday() { + setCurrentDate(new Date()); + } + + async function handleSave(content: string) { + if (!note) return; + setSaveStatus('Saving'); + try { + const res = await fetch(`/api/notes/${note.id}`, { + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ content }), + }); + if (!res.ok) throw new Error('Failed to save'); + setSaveStatus('Saved'); + } catch (err) { + console.error('Failed to save note:', err); + setSaveStatus('Failed'); + toast.error('Failed to save note'); + } + } + + const isToday = dateStr === format(new Date(), 'yyyy-MM-dd'); + + return ( +
+
+
+

Daily Notes

+

+ {displayDate} +

+
+
+ + {!isToday && ( + + )} + +
+
+ + + {loading ? ( +
+ +
+ ) : note ? ( +
+
+
+

{note.title}

+ + {saveStatus} + +
+
+
+ +
+ Loading editor... +
+
+ } + > + { + setNote({ ...note, content }); + handleSave(content); + }} + /> + +
+
+ ) : ( +
+ +
+

No daily note yet

+

+ {isToday + ? 'Create your daily note to track what you accomplished today.' + : 'No daily note exists for this date.'} +

+
+ +
+ )} + + + ); +} diff --git a/apps/web/components/sidebar.tsx b/apps/web/components/sidebar.tsx index 85079ed..1c23967 100644 --- a/apps/web/components/sidebar.tsx +++ b/apps/web/components/sidebar.tsx @@ -10,6 +10,7 @@ import { Flame, FolderKanban, NotebookPen, + BookOpen, Share2, CalendarDays, Search, @@ -41,6 +42,7 @@ const navItems = [ { href: '/habits', label: 'Habits', icon: Flame }, { href: '/projects', label: 'Projects', icon: FolderKanban }, { href: '/notes', label: 'Notes', icon: NotebookPen }, + { href: '/daily-notes', label: 'Daily Note', icon: BookOpen }, { href: '/graph', label: 'Graph', icon: Share2 }, { href: '/calendar', label: 'Calendar', icon: CalendarDays }, { href: '/search', label: 'Search', icon: Search }, From d951348372cc65f1bbdfc8badc04e827ab3bcc2d Mon Sep 17 00:00:00 2001 From: bot-hermes Date: Wed, 29 Jul 2026 19:25:37 +0000 Subject: [PATCH 2/4] feat: add canvas freeform board page with drag-and-drop cards and sidebar link --- apps/web/app/(dashboard)/canvas/page.tsx | 523 +++++++++++++++++++++++ apps/web/components/sidebar.tsx | 2 + 2 files changed, 525 insertions(+) create mode 100644 apps/web/app/(dashboard)/canvas/page.tsx diff --git a/apps/web/app/(dashboard)/canvas/page.tsx b/apps/web/app/(dashboard)/canvas/page.tsx new file mode 100644 index 0000000..7207141 --- /dev/null +++ b/apps/web/app/(dashboard)/canvas/page.tsx @@ -0,0 +1,523 @@ +'use client'; + +import { useState, useEffect, useCallback, useRef } from 'react'; +import { LayoutGrid, Plus, Trash2, GripVertical, X, ZoomIn, ZoomOut, Maximize2 } from 'lucide-react'; +import { toast } from 'sonner'; +import { Button } from '@/components/ui/button'; +import { Card } from '@/components/ui/card'; +import { Input } from '@/components/ui/input'; +import { Textarea } from '@/components/ui/textarea'; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogTrigger, +} from '@/components/ui/dialog'; + +interface CanvasCard { + id: string; + canvas_id: string; + type: 'note' | 'task' | 'image' | 'entity'; + entity_id?: string; + title?: string; + content?: string; + x: number; + y: number; + width: number; + height: number; + rotation: number; + color?: string; + z_index: number; + created: string; + updated: string; +} + +interface CanvasConnection { + id: string; + source_card_id: string; + target_card_id: string; + label?: string; + style: 'solid' | 'dashed' | 'dotted'; +} + +interface Canvas { + id: string; + name: string; + description?: string; + mode: 'freeform' | 'graph'; + domain: string; + tags: string[]; + cards: CanvasCard[]; + connections: CanvasConnection[]; + viewport?: { x: number; y: number; zoom: number }; + background?: string; + created: string; + updated: string; +} + +function CanvasBoard({ canvas, onBack }: { canvas: Canvas; onBack: () => void }) { + const [cards, setCards] = useState(canvas.cards || []); + const [dragging, setDragging] = useState(null); + const [dragOffset, setDragOffset] = useState({ x: 0, y: 0 }); + const [viewport, setViewport] = useState(canvas.viewport || { x: 0, y: 0, zoom: 1 }); + const [editingCard, setEditingCard] = useState(null); + const [editTitle, setEditTitle] = useState(''); + const [editContent, setEditContent] = useState(''); + const boardRef = useRef(null); + + const handlePointerDown = useCallback( + (e: React.PointerEvent, cardId: string) => { + e.preventDefault(); + const card = cards.find((c) => c.id === cardId); + if (!card) return; + setDragging(cardId); + setDragOffset({ + x: e.clientX - card.x * viewport.zoom, + y: e.clientY - card.y * viewport.zoom, + }); + (e.target as HTMLElement).setPointerCapture(e.pointerId); + }, + [cards, viewport.zoom] + ); + + const handlePointerMove = useCallback( + (e: React.PointerEvent) => { + if (!dragging) return; + const newX = (e.clientX - dragOffset.x) / viewport.zoom; + const newY = (e.clientY - dragOffset.y) / viewport.zoom; + setCards((prev) => + prev.map((c) => (c.id === dragging ? { ...c, x: Math.max(0, newX), y: Math.max(0, newY) } : c)) + ); + }, + [dragging, dragOffset, viewport.zoom] + ); + + const handlePointerUp = useCallback(() => { + setDragging(null); + }, []); + + async function saveCardPosition(card: CanvasCard) { + try { + await fetch(`/api/canvases/${canvas.id}`, { + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + cards: cards.map((c) => + c.id === card.id + ? { ...c, x: card.x, y: card.y } + : c + ), + }), + }); + } catch (err) { + console.error('Failed to save card position:', err); + } + } + + async function addCard() { + const newCard: CanvasCard = { + id: crypto.randomUUID(), + canvas_id: canvas.id, + type: 'note', + title: 'New note', + content: '', + x: 50 + Math.random() * 200, + y: 50 + Math.random() * 200, + width: 200, + height: 150, + rotation: 0, + z_index: cards.length, + created: new Date().toISOString(), + updated: new Date().toISOString(), + }; + const updatedCards = [...cards, newCard]; + setCards(updatedCards); + try { + await fetch(`/api/canvases/${canvas.id}`, { + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ cards: updatedCards }), + }); + toast.success('Card added'); + } catch (err) { + console.error('Failed to add card:', err); + toast.error('Failed to add card'); + } + } + + async function deleteCard(cardId: string) { + const updatedCards = cards.filter((c) => c.id !== cardId); + setCards(updatedCards); + try { + await fetch(`/api/canvases/${canvas.id}`, { + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ cards: updatedCards }), + }); + toast.success('Card removed'); + } catch (err) { + console.error('Failed to delete card:', err); + toast.error('Failed to delete card'); + } + } + + async function saveCardEdit() { + if (!editingCard) return; + const updatedCards = cards.map((c) => + c.id === editingCard.id + ? { ...c, title: editTitle, content: editContent } + : c + ); + setCards(updatedCards); + setEditingCard(null); + try { + await fetch(`/api/canvases/${canvas.id}`, { + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ cards: updatedCards }), + }); + } catch (err) { + console.error('Failed to save card:', err); + } + } + + function openEdit(card: CanvasCard) { + setEditingCard(card); + setEditTitle(card.title || ''); + setEditContent(card.content || ''); + } + + return ( +
+ {/* Toolbar */} +
+
+ +

{canvas.name}

+
+
+ + + {Math.round(viewport.zoom * 100)}% + + + + +
+
+ + {/* Board */} +
+
+ {/* Connections */} + + {canvas.connections.map((conn) => { + const source = cards.find((c) => c.id === conn.source_card_id); + const target = cards.find((c) => c.id === conn.target_card_id); + if (!source || !target) return null; + return ( + + ); + })} + + + {/* Cards */} + {cards.map((card) => ( +
+ {/* Drag handle */} +
handlePointerDown(e, card.id)} + style={{ touchAction: 'none' }} + > + + + {card.title || 'Untitled'} + + + +
+ {/* Content */} +
+ {card.content || 'No content'} +
+
+ ))} + + {cards.length === 0 && ( +
+ +

No cards yet

+ +
+ )} +
+
+ + {/* Edit dialog */} + !open && setEditingCard(null)}> + + + Edit card + +
+
+ + setEditTitle(e.target.value)} + placeholder="Card title" + /> +
+
+ +