Repository navigation
Calendar functionality #654
Description
Based on the codebase context, the calendar system in QCX consists of several interconnected components. Here are all the calendar implementations:
Core Components
CalendarNotepad Component
The main UI component CalendarNotepad provides a date-based note-taking interface with location tagging capabilities 1 . It features:
- Date navigation with a 7-day range selector
- Note creation via
⌘+Enterkeyboard shortcut - Location tagging using current map position
- Fly-to navigation when clicking location icons
- Integration with
TimezoneClockcomponent
Server Actions
The calendar system uses two server actions in lib/actions/calendar.ts 2 :
getNotes(date, chatId)- Fetches notes for a specific date and user, optionally filtered by chat sessionsaveNote(noteData)- Inserts or updates notes, and creates a chat message for AI context when a note has achatId
Database Schema
The calendarNotes table is defined in lib/db/schema.ts 3 , storing:
- User and chat associations
- Date, content, and timestamps
- Location tags (GeoJSON), user tags, and map feature IDs
Type Definitions
Type definitions in lib/types/index.ts define the CalendarNote and NewCalendarNote interfaces 4 .
Integration Points
State Management
The CalendarToggleProvider in components/calendar-toggle-context.tsx manages calendar visibility state 5 .
UI Integration
- Header: Desktop calendar toggle button in
components/header.tsx6 - Mobile: Mobile calendar button in
components/mobile-icons-bar.tsx7 - Chat Layout: Renders
CalendarNotepadwhen calendar is open in both mobile and desktop layouts incomponents/chat.tsx8 9
Notes
The calendar system is tightly integrated with the map context, allowing users to tag notes with geographic locations and navigate to those locations. Notes can be associated with specific chat sessions to provide AI context.
Wiki pages you might want to explore:
Citations
File: components/calendar-notepad.tsx (L18-180)
export function CalendarNotepad({ chatId }: CalendarNotepadProps) {
const { mapData, setMapData } = useMapData()
const [selectedDate, setSelectedDate] = useState(new Date())
const [notes, setNotes] = useState<CalendarNote[]>([])
const [noteContent, setNoteContent] = useState("")
const [dateOffset, setDateOffset] = useState(0)
const [taggedLocation, setTaggedLocation] = useState<any | null>(null)
useEffect(() => {
const fetchNotes = async () => {
const fetchedNotes = await getNotes(selectedDate, chatId ?? null)
setNotes(fetchedNotes)
}
fetchNotes()
}, [selectedDate, chatId])
const generateDateRange = (offset: number) => {
const dates = []
const today = new Date()
for (let i = 0; i < 7; i++) {
const date = new Date(today)
date.setDate(today.getDate() + offset + i)
dates.push(date)
}
return dates
}
const dateRange = generateDateRange(dateOffset)
const isSameDay = (date1: Date, date2: Date) => {
return (
date1.getDate() === date2.getDate() &&
date1.getMonth() === date2.getMonth() &&
date1.getFullYear() === date2.getFullYear()
)
}
const handleAddNote = async (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) {
if (!noteContent.trim()) return
const newNote: NewCalendarNote = {
date: selectedDate,
content: noteContent,
chatId: chatId ?? null,
userId: '', // This will be set on the server
locationTags: taggedLocation,
userTags: null,
mapFeatureId: null,
}
const savedNote = await saveNote(newNote)
if (savedNote) {
setNotes([savedNote, ...notes])
setNoteContent("")
setTaggedLocation(null)
}
}
}
const handleTagLocation = () => {
if (mapData.targetPosition) {
setTaggedLocation({
type: 'Point',
coordinates: mapData.targetPosition
});
setNoteContent(prev => `${prev} #location`);
}
};
const handleFlyTo = (location: any) => {
if (location && location.coordinates) {
setMapData(prev => ({ ...prev, targetPosition: location.coordinates }));
}
};
return (
<div data-testid="calendar-notepad" className="bg-card text-card-foreground shadow-lg rounded-lg p-4 max-w-2xl mx-auto my-4 border">
<div className="flex items-center justify-between mb-4">
<button
onClick={() => setDateOffset(dateOffset - 7)}
className="p-2 text-muted-foreground hover:text-foreground"
>
<ChevronLeft className="h-5 w-5" />
</button>
<div className="flex space-x-2 overflow-x-auto">
{dateRange.map((date) => (
<button
key={date.toISOString()}
onClick={() => setSelectedDate(date)}
className={cn(
"flex flex-col items-center p-2 rounded-md transition-colors",
isSameDay(date, selectedDate)
? "bg-primary text-primary-foreground"
: "hover:bg-accent"
)}
>
<span className="text-sm font-medium">
{date.toLocaleDateString(undefined, { day: "numeric" })}
</span>
<span className="text-xs text-muted-foreground">
{date.toLocaleDateString(undefined, { month: "short" })}
</span>
</button>
))}
</div>
<button
onClick={() => setDateOffset(dateOffset + 7)}
className="p-2 text-muted-foreground hover:text-foreground"
>
<ChevronRight className="h-5 w-5" />
</button>
</div>
<div className="mb-4">
<div className="relative">
<textarea
value={noteContent}
onChange={(e) => setNoteContent(e.target.value)}
onKeyDown={handleAddNote}
placeholder="Add note... (⌘+Enter to save, @mention, #location)"
className="w-full p-2 bg-input rounded-md border focus:ring-ring focus:ring-2 focus:outline-none pr-10"
rows={3}
/>
<button
onClick={handleTagLocation}
className="absolute right-2 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
>
<MapPin className="h-5 w-5" />
</button>
</div>
</div>
<div className="space-y-4">
{notes.length > 0 ? (
notes.map((note) => (
<div key={note.id} className="p-3 bg-muted rounded-md">
<div className="flex justify-between items-start">
<div>
<p className="text-xs text-muted-foreground mb-1">
{new Date(note.createdAt).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
</p>
<p className="text-sm whitespace-pre-wrap break-words">{note.content}</p>
</div>
{note.locationTags && (
<button onClick={() => handleFlyTo(note.locationTags)} className="text-muted-foreground hover:text-foreground ml-2">
<MapPin className="h-5 w-5" />
</button>
)}
</div>
</div>
))
) : (
<p className="text-center text-muted-foreground text-sm py-4">
No notes for this day.
</p>
)}
</div>
<TimezoneClock />
</div>
)
}File: lib/actions/calendar.ts (L1-112)
'use server'
import { and, desc, eq, isNull, sql } from 'drizzle-orm'
import { db } from '@/lib/db'
import { calendarNotes } from '@/lib/db/schema'
import { getCurrentUserIdOnServer } from '@/lib/auth/get-current-user'
import type { CalendarNote, NewCalendarNote } from '@/lib/types'
import { createMessage, NewMessage } from './chat-db'
/**
* Retrieves notes for a specific date and chat session.
* @param date - The date to fetch notes for.
* @param chatId - The ID of the chat session.
* @returns A promise that resolves to an array of notes.
*/
export async function getNotes(date: Date, chatId: string | null): Promise<CalendarNote[]> {
const userId = await getCurrentUserIdOnServer()
if (!userId) {
console.error('getNotes: User not authenticated')
return []
}
// Normalize date to the start of the day for consistent querying
const startDate = new Date(date)
startDate.setHours(0, 0, 0, 0)
const endDate = new Date(date)
endDate.setHours(23, 59, 59, 999)
try {
const whereConditions = [
eq(calendarNotes.userId, userId),
and(
sql`${calendarNotes.date} >= ${startDate}`,
sql`${calendarNotes.date} <= ${endDate}`
)
];
if (chatId) {
whereConditions.push(eq(calendarNotes.chatId, chatId));
} else {
whereConditions.push(isNull(calendarNotes.chatId));
}
const notes = await db
.select()
.from(calendarNotes)
.where(and(...whereConditions))
.orderBy(desc(calendarNotes.createdAt))
.execute()
return notes;
} catch (error) {
console.error('Error fetching notes:', error)
return []
}
}
/**
* Saves a new note or updates an existing one.
* @param noteData - The note data to save.
* @returns A promise that resolves to the saved note or null if an error occurs.
*/
export async function saveNote(noteData: NewCalendarNote | CalendarNote): Promise<CalendarNote | null> {
const userId = await getCurrentUserIdOnServer();
if (!userId) {
console.error('saveNote: User not authenticated');
return null;
}
if ('id' in noteData) {
// Update existing note
try {
const [updatedNote] = await db
.update(calendarNotes)
.set({ ...noteData, updatedAt: new Date() })
.where(and(eq(calendarNotes.id, noteData.id), eq(calendarNotes.userId, userId)))
.returning();
return updatedNote;
} catch (error) {
console.error('Error updating note:', error);
return null;
}
} else {
// Create new note
try {
const [newNote] = await db
.insert(calendarNotes)
.values({ ...noteData, userId })
.returning();
if (newNote && newNote.chatId) {
const calendarContextMessage: NewMessage = {
chatId: newNote.chatId,
userId: userId,
role: 'data',
content: JSON.stringify({
type: 'calendar_note',
note: newNote,
}),
};
await createMessage(calendarContextMessage);
}
return newNote;
} catch (error) {
console.error('Error creating note:', error);
return null;
}
}
}File: lib/db/schema.ts (L92-103)
export const calendarNotes = pgTable('calendar_notes', {
id: uuid('id').primaryKey().defaultRandom(),
userId: uuid('user_id').notNull().references(() => users.id, { onDelete: 'cascade' }),
chatId: uuid('chat_id').references(() => chats.id, { onDelete: 'cascade' }),
date: timestamp('date', { withTimezone: true }).notNull(),
content: text('content').notNull(),
locationTags: jsonb('location_tags'),
userTags: text('user_tags').array(),
mapFeatureId: text('map_feature_id'),
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(),
});File: lib/types/index.ts (L79-92)
export type CalendarNote = {
id: string;
userId: string;
chatId: string | null;
date: Date;
content: string;
locationTags: any | null;
userTags: string[] | null;
mapFeatureId: string | null;
createdAt: Date;
updatedAt: Date;
};
export type NewCalendarNote = Omit<CalendarNote, 'id' | 'createdAt' | 'updatedAt'>;File: app/layout.tsx (L98-98)
<CalendarToggleProvider>File: components/header.tsx (L74-76)
<Button variant="ghost" size="icon" onClick={toggleCalendar} title="Open Calendar" data-testid="calendar-toggle">
<CalendarDays className="h-[1.2rem] w-[1.2rem]" />
</Button>File: components/mobile-icons-bar.tsx (L57-59)
<Button variant="ghost" size="icon" onClick={toggleCalendar} title="Open Calendar" data-testid="mobile-calendar-button">
<CalendarDays className="h-[1.2rem] w-[1.2rem] transition-all rotate-0 scale-100" />
</Button>File: components/chat.tsx (L159-161)
{isCalendarOpen ? (
<CalendarNotepad chatId={id} />
) : (File: components/chat.tsx (L191-193)
{isCalendarOpen ? (
<CalendarNotepad chatId={id} />
) : (
Activity
ngoiyaeric commentedon Jun 8, 2026
@coderabbitai plan to improve the functionality for geospatial context. Such as using the calendar icon at a location for rendezvous.
coderabbitai commentedon Jun 8, 2026
Coding Plan
Summary
0001_add_calendar_notesin the Drizzle journal with a corresponding snapshot fileuser_id,date,(user_id, date), andchat_idcolumns to support the query patterns ingetNotesImplementation Steps
Phase 1: Fix Migration Tracking
The
0001_add_calendar_notes.sqlmigration exists but is not registered in the Drizzle migration journal, which could cause deployment issues where the migration is re-run or skipped unexpectedly.Register Calendar Notes Migration in Journal
Update the migration journal to track the existing calendar_notes migration and ensure Drizzle's migrator recognizes it as applied.
0001_add_calendar_notestodrizzle/migrations/meta/_journal.jsonwith appropriate timestamp and hashdrizzle/migrations/meta/0001_snapshot.jsoncapturing the schema state after the calendar_notes table addition0000_sweet_metal_masterentry structure🤖 Prompt for AI agents
Phase 2: Add Database Indexes for Performance
The
calendar_notestable lacks indexes on frequently queried columns, which will cause performance degradation at scale since everygetNotescall performs a full table scan.Task 1: Create Index Migration
Add a new Drizzle migration to create indexes on the calendar_notes table for the primary query patterns.
drizzle/migrations/(e.g.,0002_calendar_notes_indexes.sql)user_idcolumn for user-scoped queriesdatecolumn for date-range filtering(user_id, date)to optimize the canonical query pattern used bygetNoteschat_idfor chat-scoped note retrievalTask 2: Update Schema with Index Definitions
Add index definitions to the Drizzle schema so future migrations stay in sync.
lib/db/schema.tsto include index definitions on thecalendarNotestable using Drizzle's index syntaxTask 3: Register Index Migration in Journal
Track the new index migration properly in the Drizzle migration system.
drizzle/migrations/meta/_journal.jsondrizzle/migrations/meta/🤖 Prompt for AI agents
Research
The calendar system is built around
CalendarNotepad(components/calendar-notepad.tsx), which provides a date-based note-taking interface with location tagging via map integration. Server actions inlib/actions/calendar.tshandle note CRUD operations using Drizzle ORM against thecalendar_notestable defined inlib/db/schema.ts. Visibility is managed byCalendarToggleProvider(components/calendar-toggle-context.tsx), with toggle buttons in both the desktop header and mobile icons bar. The system supports chat association for AI context by injectingrole: 'data'messages when notes are linked to a chat session.🚀 Next Steps
🤖 All AI agent prompts combined
💡 Iterate on the plan with:
`@coderabbitai` <feedback>💬 Have feedback or questions? Drop into our discord!
ngoiyaeric commentedon Jun 8, 2026
@coderabbitai the system code for the calendar function is not working right now. I need more pre-database utility like notes on locations and reminders with notifications.
coderabbitai commentedon Jun 8, 2026
🧩 Analysis chain
🏁 Script executed:
Length of output: 1142
🏁 Script executed:
Length of output: 20333
🏁 Script executed:
Length of output: 443
🏁 Script executed:
Length of output: 4749
🏁 Script executed:
Length of output: 427
🏁 Script executed:
Length of output: 5720
🏁 Script executed:
Length of output: 4134
Now I have everything. Here is the complete, implementation-ready plan:
Task: Fix broken calendar + add pre-DB local state, location notes & reminders
- Phase 1 — Register
- Phase 2 — New migration
- Phase 3 — Update
- Phase 4 — Update
- Phase 5 — Create
- Phase 6 — Create
- Phase 7 — Create
- Phase 8 — Update
- Phase 9 — Update
- Phase 10 — Update
statement-breakpoint0001_add_calendar_notesin Drizzle journal + create snapshot0002_add_calendar_reminders.sql+ journal + snapshotlib/types/index.ts— add reminder fields toCalendarNotelib/db/schema.ts— add reminder columns + indexeslib/hooks/use-local-notes.ts— localStorage pre-DB layerlib/hooks/use-reminders.ts— browser Notification API schedulercomponents/reminder-notification-provider.tsx— permission + pollinglib/actions/calendar.ts— addgetUpcomingReminders+markReminderTriggeredcomponents/calendar-notepad.tsx— fix"use client"dup, add reminder picker, location display, optimistic saves,initialLocationpropapp/layout.tsx— add<ReminderNotificationProvider>ALTER TABLE "calendar_notes" ADD COLUMN IF NOT EXISTS "reminder_triggered" boolean DEFAULT false NOT NULL;
--> statement-breakpoint
CREATE INDEX IF NOT EXISTS "calendar_notes_user_id_idx" ON "calendar_notes" ("user_id");
--> statement-breakpoint
CREATE INDEX IF NOT EXISTS "calendar_notes_date_idx" ON "calendar_notes" ("date");
--> statement-breakpoint
CREATE INDEX IF NOT EXISTS "calendar_notes_user_date_idx" ON "calendar_notes" ("user_id", "date");
--> statement-breakpoint
CREATE INDEX IF NOT EXISTS "calendar_notes_reminder_time_idx" ON "calendar_notes" ("reminder_time") WHERE reminder_time IS NOT NULL AND reminder_triggered = false;
Create
drizzle/migrations/meta/0002_snapshot.json— copy structure from0001_snapshot.json, use a new UUID for"id", set"prevId"to the UUID used in0001_snapshot.json, and addreminder_time(timestamp with time zone, nullable) andreminder_triggered(boolean, notNull, default false) to thecalendar_notescolumns, plus add the four index entries undercalendar_notes.indexes.-->
💡 For code changes across multiple files, use
@coderabbitaiplan in an issue comment