Hacktoberfest 2026:メンテナが10月に向けて印を付けた、オープンで初心者向けの issue。 Hacktoberfest の issue を見る

Calendar functionality

オープン
#654 コメント 4 件 リアクション 0 件 担当者 0 名 GitHub で見る

まだ誰も着手していません。

評価

難易度
5/5
見積もり時間
1週間以上
初心者へのやさしさ
30/100
issue の種類
機能追加
明瞭さ
説明が足りない
活発さ
静か
技術スタック
react, typescript

調査の方向性

この issue では、components/calendar-notepad.tsx、lib/actions/calendar.ts、lib/db/schema.ts、lib/types/index.ts、および関連する統合ファイルにある既存のカレンダーコードについて説明されていますが、何を変更すべきかは示されていません。まず、意図している機能または不具合と、その受け入れ条件を明確にしてください。具体的なテストや Definition of Done は提示されていません。

索引モデルが issue の本文から書いたものです。

説明

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 ⌘+Enter keyboard shortcut
  • Location tagging using current map position
  • Fly-to navigation when clicking location icons
  • Integration with TimezoneClock component
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 session
  • saveNote(noteData) - Inserts or updates notes, and creates a chat message for AI context when a note has a chatId
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.tsx 6
  • Mobile: Mobile calendar button in components/mobile-icons-bar.tsx 7
  • Chat Layout: Renders CalendarNotepad when calendar is open in both mobile and desktop layouts in components/chat.tsx 8 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} />
        ) : (
主要言語
TypeScript
スター
19
フォーク
7
平均マージ
59分
マージ済み PR(30日)
16

コントリビューションガイド

このリポジトリのコントリビューションガイドは索引されていません

はじめの一歩

  1. issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
  2. 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
  3. リポジトリをフォークし、ブランチを切って変更します。
  4. issue 番号を参照したプルリクエストを送ります。

QueueLab/QCX のほかの issue

QueueLab/QCX の issue をすべて見る

似ている issue

TypeScript の issue をもっと見る

新しい issue をメールで受け取る

初心者向けの GitHub issue を短くまとめたダイジェスト。