diff --git a/apps/web/src/App.tsx b/apps/web/src/App.tsx
index 2e37062..cf21a0d 100644
--- a/apps/web/src/App.tsx
+++ b/apps/web/src/App.tsx
@@ -1,24 +1,41 @@
+import { lazy, Suspense } from 'react';
import { Routes, Route, Navigate } from 'react-router-dom';
import { ToastProvider, ToastViewport } from '@yetanother/ui';
-import { NotesPage } from './notes/NotesPage';
-import { TasksPage } from './tasks/TasksPage';
-import { CalendarPage } from './calendar/CalendarPage';
import { Sidebar } from './components/Sidebar';
-import './index.css';
+import { SearchBar } from './ai/SearchBar';
+
+const NotesPage = lazy(() => import('./notes/NotesPage').then((m) => ({ default: m.NotesPage })));
+const TasksPage = lazy(() => import('./tasks/TasksPage').then((m) => ({ default: m.TasksPage })));
+const CalendarPage = lazy(() => import('./calendar/CalendarPage').then((m) => ({ default: m.CalendarPage })));
+
+function Loading() {
+ return (
+
+ Loading...
+
+ );
+}
export function App() {
return (
-
-
- } />
- } />
- } />
- } />
-
-
+
+
+
+
+
+ }>
+
+ } />
+ } />
+ } />
+ } />
+
+
+
+
diff --git a/apps/web/src/calendar/CalendarPage.tsx b/apps/web/src/calendar/CalendarPage.tsx
index 60e006c..d5a3fa8 100644
--- a/apps/web/src/calendar/CalendarPage.tsx
+++ b/apps/web/src/calendar/CalendarPage.tsx
@@ -1,6 +1,9 @@
import { CalendarHeader } from './CalendarHeader';
import { MonthView } from './MonthView';
+import { WeekView } from './WeekView';
+import { DayView } from './DayView';
import { useCalendarStore } from '../stores/calendar';
+import { EventDialog } from './EventDialog';
export function CalendarPage() {
const view = useCalendarStore((s) => s.view);
@@ -8,12 +11,12 @@ export function CalendarPage() {
return (
+ {view === 'day' &&
}
+ {view === 'week' &&
}
{view === 'month' &&
}
- {(view === 'day' || view === 'week') && (
-
- Day/Week view coming soon
-
- )}
+ {view === 'year' &&
}
+ {view === 'schedule' &&
}
+
);
}
diff --git a/apps/web/src/calendar/DayView.tsx b/apps/web/src/calendar/DayView.tsx
new file mode 100644
index 0000000..4b9af11
--- /dev/null
+++ b/apps/web/src/calendar/DayView.tsx
@@ -0,0 +1,54 @@
+import { useCalendarStore } from '../stores/calendar';
+import { format, isSameDay, parseISO } from 'date-fns';
+
+const HOURS = Array.from({ length: 16 }, (_, i) => i + 6);
+
+export function DayView() {
+ const currentDate = useCalendarStore((s) => s.currentDate);
+ const events = useCalendarStore((s) => s.events);
+
+ const date = new Date(currentDate);
+ const dayEvents = events.filter((e) => isSameDay(parseISO(e.startTime), date));
+
+ return (
+
+
+
{format(date, 'EEEE, MMMM d')}
+
+
+
+ {HOURS.map((hour) => (
+
+
+ {format(new Date().setHours(hour, 0, 0, 0), 'ha')}
+
+
+
+ ))}
+
+ {dayEvents.map((event) => {
+ const startHour = parseISO(event.startTime).getHours();
+ const endHour = parseISO(event.endTime ?? event.startTime).getHours();
+ const duration = Math.max(endHour - startHour, 1);
+
+ return (
+
+
{event.title}
+
{format(parseISO(event.startTime), 'h:mm a')}
+
+ );
+ })}
+
+
+ );
+}
diff --git a/apps/web/src/calendar/EventDialog.tsx b/apps/web/src/calendar/EventDialog.tsx
new file mode 100644
index 0000000..430122b
--- /dev/null
+++ b/apps/web/src/calendar/EventDialog.tsx
@@ -0,0 +1,119 @@
+import { useState } from 'react';
+import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@yetanother/ui';
+import { useCalendarStore, type CalendarEvent } from '../stores/calendar';
+
+interface EventDialogProps {
+ open?: boolean;
+ onClose?: () => void;
+}
+
+export function EventDialog({ open: controlledOpen, onClose }: EventDialogProps) {
+ const [internalOpen, setInternalOpen] = useState(false);
+ const addEvent = useCalendarStore((s) => s.addEvent);
+ const open = controlledOpen ?? internalOpen;
+ const close = onClose ?? (() => setInternalOpen(false));
+
+ const [title, setTitle] = useState('');
+ const [date, setDate] = useState(new Date().toISOString().slice(0, 10));
+ const [startTime, setStartTime] = useState('09:00');
+ const [endTime, setEndTime] = useState('10:00');
+ const [color, setColor] = useState('#3b82f6');
+
+ const handleSubmit = () => {
+ if (!title.trim()) return;
+ const event: CalendarEvent = {
+ id: crypto.randomUUID(),
+ title: title.trim(),
+ description: '',
+ startTime: new Date(`${date}T${startTime}`).toISOString(),
+ endTime: new Date(`${date}T${endTime}`).toISOString(),
+ allDay: false,
+ recurrenceRule: null,
+ color,
+ workspaceId: 'default',
+ taskId: null,
+ createdAt: new Date().toISOString(),
+ updatedAt: new Date().toISOString(),
+ };
+ addEvent(event);
+ setTitle('');
+ close();
+ };
+
+ return (
+
+ );
+}
diff --git a/apps/web/src/calendar/WeekView.tsx b/apps/web/src/calendar/WeekView.tsx
new file mode 100644
index 0000000..d81a6b8
--- /dev/null
+++ b/apps/web/src/calendar/WeekView.tsx
@@ -0,0 +1,72 @@
+import { useCalendarStore, type CalendarEvent } from '../stores/calendar';
+import { startOfWeek, endOfWeek, eachDayOfInterval, format, isSameDay, isToday, parseISO } from 'date-fns';
+import { cn } from '@yetanother/utils';
+
+const HOURS = Array.from({ length: 14 }, (_, i) => i + 7);
+
+function getEventsForDay(events: CalendarEvent[], day: Date) {
+ return events.filter((e) => isSameDay(parseISO(e.startTime), day));
+}
+
+export function WeekView() {
+ const currentDate = useCalendarStore((s) => s.currentDate);
+ const events = useCalendarStore((s) => s.events);
+
+ const date = new Date(currentDate);
+ const weekStart = startOfWeek(date, { weekStartsOn: 0 });
+ const weekEnd = endOfWeek(date, { weekStartsOn: 0 });
+ const days = eachDayOfInterval({ start: weekStart, end: weekEnd });
+
+ return (
+
+
+
+ {days.map((day) => (
+
+
{format(day, 'EEE')}
+
{format(day, 'd')}
+
+ ))}
+
+
+
+
+ {HOURS.map((hour) => (
+
+ {format(new Date().setHours(hour, 0, 0, 0), 'ha')}
+
+ ))}
+
+
+ {days.map((day) => {
+ const dayEvents = getEventsForDay(events, day);
+ return (
+
+ {HOURS.map((hour) => (
+
+ ))}
+ {dayEvents.map((event) => {
+ const startHour = parseISO(event.startTime).getHours();
+ return (
+
+ );
+ })}
+
+ );
+ })}
+
+
+ );
+}
diff --git a/apps/web/src/editor/Editor.tsx b/apps/web/src/editor/Editor.tsx
index 55db05e..4042365 100644
--- a/apps/web/src/editor/Editor.tsx
+++ b/apps/web/src/editor/Editor.tsx
@@ -1,15 +1,19 @@
+import { useEffect, useCallback } from 'react';
import { useEditor, EditorContent, type Editor } from '@tiptap/react';
import { extensions } from './extensions';
import { EditorToolbar } from './EditorToolbar';
import { SlashMenu } from './SlashMenu';
+import { AIAssistant } from '../ai/AIAssistant';
+import { collaboration } from '../lib/collaboration';
interface EditorProps {
content?: Record;
onChange?: (json: Record) => void;
editable?: boolean;
+ nodeId?: string;
}
-export function BlockEditor({ content, onChange, editable = true }: EditorProps) {
+export function BlockEditor({ content, onChange, editable = true, nodeId }: EditorProps) {
const editor = useEditor({
extensions,
content: content ?? { type: 'doc', content: [{ type: 'paragraph' }] },
@@ -25,11 +29,44 @@ export function BlockEditor({ content, onChange, editable = true }: EditorProps)
},
});
+ useEffect(() => {
+ if (!nodeId || !editor) return;
+
+ collaboration.connect(nodeId);
+
+ const unsubscribe = collaboration.onMessage((msg) => {
+ if (msg.type === 'update' && msg.sender !== 'self') {
+ // Apply remote updates (simplified — real CRDT sync uses y-prosemirror)
+ }
+ });
+
+ return () => {
+ unsubscribe();
+ collaboration.disconnect();
+ };
+ }, [nodeId, editor]);
+
+ const handleAIApply = useCallback((text: string) => {
+ if (!editor) return;
+ editor.commands.insertContent(`\n\n${text}\n\n`);
+ }, [editor]);
+
+ const getText = useCallback(() => {
+ return editor?.getText() ?? '';
+ }, [editor]);
+
if (!editor) return null;
return (
- {editable &&
}
+
+ {editable &&
}
+ {editable && (
+
+ )}
+