Calendar functionality
Los mantenedores suelen responder en 1 día
Nadie ha tomado este issue todavía.
Evaluación
- Dificultad
- 5/5
- Tiempo estimado
- Más de una semana
- Aptitud para principiantes
- 30/100
- Tipo de issue
- Nueva funcionalidad
- Claridad
- Necesita aclaración
- Estado de actividad
- Tranquilo
- Stack tecnológico
- react, typescript
- Área
- databases, full-stack
Línea de trabajo
El issue describe código de calendario existente en components/calendar-notepad.tsx, lib/actions/calendar.ts, lib/db/schema.ts, lib/types/index.ts y archivos de integración relacionados, pero no indica qué debería cambiar. Empieza por aclarar la funcionalidad o el defecto previsto y sus criterios de aceptación; no se proporciona ninguna prueba específica ni una definición de terminado.
Escrito por el modelo de indexación a partir del texto del issue.
Descripción
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} />
) : (
- Lenguaje dominante
- TypeScript
- Estrellas
- 19
- Forks
- 7
- Merge medio
- 24 min
- PR fusionados (30 d)
- 9
Preparar el entorno
- Incluye un Dockerfile o un archivo de Docker Compose
- Sin plantilla de pull request
- Sin guía de contribución
Primeros pasos
- Lee el issue completo y luego la guía de contribución del proyecto.
- Comenta en el issue que vas a ocuparte — evita que dos personas hagan lo mismo.
- Haz un fork del repositorio y trabaja en una rama.
- Abre un pull request que haga referencia al número del issue.
Más de QueueLab/QCX
-
SettingsAbierto
Dificultad 2/5 1-3 horas Aptitud para principiantes 64/100
QueueLab/QCX#734 · 3 comentarios ·
Los mantenedores suelen responder en 1 día
-
Dificultad 2/5 1-3 horas Aptitud para principiantes 68/100
QueueLab/QCX#646 · 2 comentarios ·
Los mantenedores suelen responder en 1 día
-
Update Gemini 3.1 ProAbierto
Dificultad 2/5 1-3 horas Aptitud para principiantes 62/100
QueueLab/QCX#583 · 2 comentarios ·
Los mantenedores suelen responder en 1 día
-
CollaborationAbierto
Dificultad 5/5 Más de una semana Aptitud para principiantes 30/100
QueueLab/QCX#791 · 8 comentarios ·
Los mantenedores suelen responder en 1 día
-
Image preview Gen UIAbierto
Dificultad 3/5 1-2 días Aptitud para principiantes 35/100
QueueLab/QCX#790 · 2 comentarios ·
Los mantenedores suelen responder en 1 día
Todos los issues de QueueLab/QCX
Issues similares
-
Dificultad 2/5 1-3 horas Aptitud para principiantes 85/100
Comfy-Org/ComfyUI_frontend#20346 ·
Los mantenedores suelen responder en 1 día
-
Dificultad 1/5 Menos de una hora Aptitud para principiantes 90/100
decentralized-identity/didwebvh-ts#203 ·
Los mantenedores suelen responder en 1 día
-
community first-timers-only good first issue hacktoberfest help wanted low hanging fruit up-for-grabs
Dificultad 2/5 1-3 horas Aptitud para principiantes 65/100
lingdojo/kana-dojo#31791 · 1 comentario · 5 reacciones ·
Los mantenedores suelen responder en 1 día
-
Telegram webhook: line breaks lost since switch to rich messagesPosiblemente ocupada @Kshot3000 la tomó hoy. Abierto
Dificultad 2/5 1-3 horas Aptitud para principiantes 82/100
Los mantenedores suelen responder en 1 día
-
github_actions security
Dificultad 2/5 1-3 horas Aptitud para principiantes 75/100
Los mantenedores suelen responder en 1 día