CalendarDebuggerTab.tsx24.5 KBView on GitHub
import {
  Calendar,
  Check,
  ChevronDown,
  ChevronRight,
  Copy,
  Eye,
  EyeOff,
  Search,
  Link,
} from 'lucide-react';
import type { CalendarListEntry } from '@/modules/calendar/types/calendar-types';
import type { CalendarEventWithCalendarId } from '@/modules/calendar/store/calendarSlice';
import { AnimatePresence, motion } from 'motion/react';
import React, { useMemo, useState } from 'react';
import { useCedarStore } from '@/modules/store';
import { cn } from '@/styles/stylingUtils';
import { JsonTreeView } from './JsonTreeView';

interface CalendarTabProps {
  calendars: CalendarListEntry[];
  calendarEvents: CalendarEventWithCalendarId[];
  selectedCalendarEventId: string | null;
  calendarEventToConversation: Record<string, string>;
  onCopy: (text: string, id: string) => void;
  copiedId: string | null;
}

export const CalendarDebuggerTab: React.FC<CalendarTabProps> = ({
  calendars,
  calendarEvents,
  selectedCalendarEventId,
  calendarEventToConversation,
  onCopy,
  copiedId,
}) => {
  const currentDate = useCedarStore((state) => state.currentDate);
  const visibleCalendarIds = useCedarStore((state) => state.visibleCalendarIds);
  const isLoadingCalendars = useCedarStore((state) => state.isLoadingCalendars);

  const [expandedCalendars, setExpandedCalendars] = useState<Set<string>>(new Set());
  const [expandedEvents, setExpandedEvents] = useState<Set<string>>(new Set());
  const [searchQuery, setSearchQuery] = useState('');
  const [showCalendarsSection, setShowCalendarsSection] = useState(true);
  const [showEventsSection, setShowEventsSection] = useState(true);
  const [showMappingsSection, setShowMappingsSection] = useState(true);

  const safeStringify = (obj: unknown, indent = 2): string => {
    try {
      return JSON.stringify(obj, null, indent);
    } catch (error) {
      return `[Error serializing: ${error instanceof Error ? error.message : 'Unknown'}]`;
    }
  };

  const formatDate = (date: Date | string) => {
    const d = typeof date === 'string' ? new Date(date) : date;
    return d.toLocaleString('en-US', {
      year: 'numeric',
      month: 'short',
      day: 'numeric',
      hour: '2-digit',
      minute: '2-digit',
    });
  };

  const filteredCalendars = useMemo(() => {
    if (!searchQuery) return calendars;
    const query = searchQuery.toLowerCase();
    return calendars.filter((calendar) => {
      if (calendar.id?.toLowerCase().includes(query)) return true;
      if (calendar.summary?.toLowerCase().includes(query)) return true;
      return false;
    });
  }, [calendars, searchQuery]);

  const filteredEvents = useMemo(() => {
    if (!searchQuery) return calendarEvents;
    const query = searchQuery.toLowerCase();
    return calendarEvents.filter((event) => {
      if (event.id?.toLowerCase().includes(query)) return true;
      if (event.summary?.toLowerCase().includes(query)) return true;
      if (event.calendarId?.toLowerCase().includes(query)) return true;
      return false;
    });
  }, [calendarEvents, searchQuery]);

  const filteredEventMappings = useMemo(() => {
    const mappingEntries = Object.entries(calendarEventToConversation);
    if (!searchQuery) return mappingEntries;

    const filteredEventIds = new Set(filteredEvents.map((event) => event.id).filter(Boolean));
    return mappingEntries.filter(([eventId]) => filteredEventIds.has(eventId));
  }, [calendarEventToConversation, filteredEvents, searchQuery]);

  const toggleCalendar = (calendarId: string) => {
    setExpandedCalendars((prev) => {
      const next = new Set(prev);
      if (next.has(calendarId)) {
        next.delete(calendarId);
      } else {
        next.add(calendarId);
      }
      return next;
    });
  };

  const toggleEvent = (eventId: string) => {
    setExpandedEvents((prev) => {
      const next = new Set(prev);
      if (next.has(eventId)) {
        next.delete(eventId);
      } else {
        next.add(eventId);
      }
      return next;
    });
  };

  const visibleCalendarIdsArray = Array.from(visibleCalendarIds);

  return (
    <div className="flex h-full flex-col">
      {/* State Overview Section */}
      <div className="border-b border-gray-200 bg-emerald-50/50 p-3 dark:border-gray-700 dark:bg-emerald-950/20">
        <div className="flex items-center justify-between">
          <div className="flex items-center gap-2">
            <Calendar className="h-4 w-4 text-emerald-600 dark:text-emerald-400" />
            <span className="font-medium text-emerald-900 dark:text-emerald-100">
              Calendar State Overview
            </span>
          </div>
          <button
            onClick={() =>
            onCopy(
              safeStringify({
                currentDate,
                calendarsCount: calendars.length,
                visibleCalendarIds: visibleCalendarIdsArray,
                isLoadingCalendars,
                eventsCount: calendarEvents.length,
                selectedCalendarEventId,
                mappingsCount: Object.keys(calendarEventToConversation).length,
                calendarEventToConversation,
              }),
              'calendarDebugState',
            )
            }
            className="rounded p-0.5 transition-colors hover:bg-emerald-200 dark:hover:bg-emerald-800"
          >
            {copiedId === 'calendarDebugState' ? (
              <Check className="h-3 w-3 text-green-600" />
            ) : (
              <Copy className="h-3 w-3" />
            )}
          </button>
        </div>
        <div className="mt-2 grid grid-cols-2 gap-2 text-xs">
          <div className="rounded bg-white p-2 dark:bg-gray-800">
            <span className="text-gray-500 dark:text-gray-400">Current Date:</span>
            <div className="font-mono">{formatDate(currentDate)}</div>
          </div>
          <div className="rounded bg-white p-2 dark:bg-gray-800">
            <span className="text-gray-500 dark:text-gray-400">Loading:</span>
            <div
              className={cn('font-mono', isLoadingCalendars ? 'text-yellow-600' : 'text-green-600')}
            >
              {isLoadingCalendars ? 'Yes' : 'No'}
            </div>
          </div>
          <div className="rounded bg-white p-2 dark:bg-gray-800">
            <span className="text-gray-500 dark:text-gray-400">Calendars:</span>
            <div className="font-mono">
              {calendars.length} total, {visibleCalendarIds.size} visible
            </div>
          </div>
          <div className="rounded bg-white p-2 dark:bg-gray-800">
            <span className="text-gray-500 dark:text-gray-400">Events:</span>
            <div className="font-mono">{calendarEvents.length}</div>
          </div>
          <div className="rounded bg-white p-2 dark:bg-gray-800">
            <span className="text-gray-500 dark:text-gray-400">Selected Event:</span>
            <div className="truncate font-mono text-xs">
              {selectedCalendarEventId || (
                <span className="text-gray-400">None</span>
              )}
            </div>
          </div>
          <div className="rounded bg-white p-2 dark:bg-gray-800">
            <span className="text-gray-500 dark:text-gray-400">Event → Conv Mappings:</span>
            <div className="font-mono">{Object.keys(calendarEventToConversation).length}</div>
          </div>
        </div>
      </div>

      {/* Event → Conversation Mappings Section */}
      {filteredEventMappings.length > 0 && (
        <div className="border-b border-gray-200 bg-blue-50/50 p-3 dark:border-gray-700 dark:bg-blue-950/20">
          <div
            className="flex cursor-pointer items-center justify-between"
            onClick={() => setShowMappingsSection(!showMappingsSection)}
          >
            <div className="flex items-center gap-2">
              <Link className="h-4 w-4 text-blue-600 dark:text-blue-400" />
              <span className="font-medium text-blue-900 dark:text-blue-100">
                Event → Conversation Mappings ({filteredEventMappings.length})
              </span>
            </div>
            {showMappingsSection ? (
              <ChevronDown className="h-3 w-3" />
            ) : (
              <ChevronRight className="h-3 w-3" />
            )}
          </div>

          <AnimatePresence>
            {showMappingsSection && (
              <motion.div
                initial={{ height: 0, opacity: 0 }}
                animate={{ height: 'auto', opacity: 1 }}
                exit={{ height: 0, opacity: 0 }}
                transition={{ duration: 0.2 }}
                className="overflow-hidden"
              >
                <div className="mt-2 max-h-48 space-y-1 overflow-y-auto rounded bg-white p-2 dark:bg-gray-800">
                  {filteredEventMappings.map(([eventId, conversationId]) => {
                    const event = calendarEvents.find((e) => e.id === eventId);
                    const isSelected = selectedCalendarEventId === eventId;

                    return (
                      <div
                        key=[redacted]
                        className={cn(
                          'flex items-center justify-between rounded border p-1.5 text-xs',
                          isSelected
                            ? 'border-blue-500 bg-blue-50 dark:border-blue-400 dark:bg-blue-950'
                            : 'border-gray-200 dark:border-gray-700',
                        )}
                      >
                        <div className="flex flex-1 flex-col gap-0.5">
                          <div className="flex items-center gap-1">
                            {isSelected && (
                              <span className="rounded bg-blue-500 px-1 py-0.5 text-xs font-bold text-white">
                                SELECTED
                              </span>
                            )}
                            <span className="font-medium">
                              {event?.summary || 'Unknown Event'}
                            </span>
                          </div>
                          <div className="flex items-center gap-2 font-mono text-xs text-gray-500 dark:text-gray-400">
                            <span className="truncate">{eventId}</span>
                            <span>→</span>
                            <span className="truncate">{conversationId}</span>
                          </div>
                        </div>
                        <button
                          onClick={(e) => {
                            e.stopPropagation();
                            onCopy(`${eventId} → ${conversationId}`, `mapping-${eventId}`);
                          }}
                          className="rounded p-0.5 hover:bg-gray-200 dark:hover:bg-gray-700"
                        >
                          {copiedId === `mapping-${eventId}` ? (
                            <Check className="h-3 w-3 text-green-600" />
                          ) : (
                            <Copy className="h-3 w-3" />
                          )}
                        </button>
                      </div>
                    );
                  })}
                </div>
              </motion.div>
            )}
          </AnimatePresence>
        </div>
      )}

      {/* Search Bar */}
      <div className="border-b border-gray-200 p-2 dark:border-gray-700">
        <div className="relative">
          <Search className="absolute left-2 top-1/2 h-3 w-3 -translate-y-1/2 text-gray-400" />
          <input
            type="text"
            placeholder="Search calendars and events..."
            value={searchQuery}
            onChange={(e) => setSearchQuery(e.target.value)}
            className="w-full rounded border border-gray-300 bg-white py-1 pl-7 pr-2 text-xs placeholder:text-gray-400 focus:border-blue-500 focus:outline-none dark:border-gray-600 dark:bg-gray-800 dark:text-white"
          />
        </div>
      </div>

      {/* Content */}
      <div className="flex-1 space-y-3 overflow-y-auto p-2">
        {/* Calendars Section */}
        <div>
          <div
            className="mb-2 flex cursor-pointer items-center justify-between text-xs font-semibold text-gray-600 dark:text-gray-400"
            onClick={() => setShowCalendarsSection(!showCalendarsSection)}
          >
            <span>Calendars ({filteredCalendars.length})</span>
            {showCalendarsSection ? (
              <ChevronDown className="h-3 w-3" />
            ) : (
              <ChevronRight className="h-3 w-3" />
            )}
          </div>

          <AnimatePresence>
            {showCalendarsSection && (
              <motion.div
                initial={{ height: 0, opacity: 0 }}
                animate={{ height: 'auto', opacity: 1 }}
                exit={{ height: 0, opacity: 0 }}
                transition={{ duration: 0.2 }}
                className="overflow-hidden"
              >
                {filteredCalendars.length === 0 ? (
                  <div className="py-4 text-center text-xs text-gray-500">
                    {searchQuery ? 'No calendars match' : 'No calendars loaded'}
                  </div>
                ) : (
                  <div className="space-y-2">
                    {filteredCalendars.map((calendar) => {
                      const calendarId = calendar.id || '';
                      const isExpanded = expandedCalendars.has(calendarId);
                      const isVisible = visibleCalendarIds.has(calendarId);

                      return (
                        <div
                          key=[redacted]
                          className="rounded-lg border border-gray-200 bg-gray-50 dark:border-gray-700 dark:bg-gray-950"
                        >
                          <div
                            className={cn(
                              'flex cursor-pointer items-center justify-between p-2 hover:bg-gray-100 dark:hover:bg-gray-900/80',
                              isExpanded ? 'rounded-t-lg' : 'rounded-lg',
                            )}
                            onClick={() => toggleCalendar(calendarId)}
                          >
                            <div className="flex flex-1 flex-col gap-1">
                              <div className="flex items-center gap-2">
                                {isVisible ? (
                                  <Eye className="h-3 w-3 text-green-600" />
                                ) : (
                                  <EyeOff className="h-3 w-3 text-gray-400" />
                                )}
                                <span className="text-xs font-medium">
                                  {calendar.summary || 'Unnamed Calendar'}
                                </span>
                                {calendar.primary && (
                                  <span className="rounded bg-blue-100 px-1.5 py-0.5 text-xs text-blue-700 dark:bg-blue-900 dark:text-blue-300">
                                    Primary
                                  </span>
                                )}
                                {calendar.backgroundColor && (
                                  <div
                                    className="h-3 w-3 rounded-full border border-gray-300"
                                    style={{ backgroundColor: calendar.backgroundColor }}
                                  />
                                )}
                              </div>
                              <span className="ml-5 font-mono text-xs text-gray-500 dark:text-gray-400">
                                {calendarId}
                              </span>
                            </div>
                            <div className="flex items-center gap-1">
                              <button
                                onClick={(e) => {
                                  e.stopPropagation();
                                  onCopy(safeStringify(calendar), calendarId);
                                }}
                                className="rounded p-0.5 hover:bg-gray-200 dark:hover:bg-gray-700"
                              >
                                {copiedId === calendarId ? (
                                  <Check className="h-3 w-3 text-green-600" />
                                ) : (
                                  <Copy className="h-3 w-3" />
                                )}
                              </button>
                              {isExpanded ? (
                                <ChevronDown className="h-3 w-3" />
                              ) : (
                                <ChevronRight className="h-3 w-3" />
                              )}
                            </div>
                          </div>
                          <AnimatePresence>
                            {isExpanded && (
                              <motion.div
                                initial={{ height: 0, opacity: 0 }}
                                animate={{ height: 'auto', opacity: 1 }}
                                exit={{ height: 0, opacity: 0 }}
                                transition={{ duration: 0.2 }}
                                className="overflow-hidden"
                              >
                                <div
                                  className="rounded-b-lg p-2 pt-0"
                                  onClick={(e) => e.stopPropagation()}
                                >
                                  <JsonTreeView data={calendar} onCopy={onCopy} copiedId={copiedId} defaultExpandDepth={2} className="max-h-64" />
                                </div>
                              </motion.div>
                            )}
                          </AnimatePresence>
                        </div>
                      );
                    })}
                  </div>
                )}
              </motion.div>
            )}
          </AnimatePresence>
        </div>

        {/* Events Section */}
        <div>
          <div
            className="mb-2 flex cursor-pointer items-center justify-between text-xs font-semibold text-gray-600 dark:text-gray-400"
            onClick={() => setShowEventsSection(!showEventsSection)}
          >
            <span>Events ({filteredEvents.length})</span>
            {showEventsSection ? (
              <ChevronDown className="h-3 w-3" />
            ) : (
              <ChevronRight className="h-3 w-3" />
            )}
          </div>

          <AnimatePresence>
            {showEventsSection && (
              <motion.div
                initial={{ height: 0, opacity: 0 }}
                animate={{ height: 'auto', opacity: 1 }}
                exit={{ height: 0, opacity: 0 }}
                transition={{ duration: 0.2 }}
                className="overflow-hidden"
              >
                {filteredEvents.length === 0 ? (
                  <div className="py-4 text-center text-xs text-gray-500">
                    {searchQuery ? 'No events match' : 'No events loaded'}
                  </div>
                ) : (
                  <div className="space-y-2">
                    {filteredEvents.map((event, index) => {
                      const eventId = event.id || `event-${index}`;
                      const isExpanded = expandedEvents.has(eventId);

                      return (
                        <div
                          key=[redacted]
                          className="rounded-lg border border-gray-200 bg-gray-50 dark:border-gray-700 dark:bg-gray-950"
                        >
                          <div
                            className={cn(
                              'flex cursor-pointer items-center justify-between p-2 hover:bg-gray-100 dark:hover:bg-gray-900/80',
                              isExpanded ? 'rounded-t-lg' : 'rounded-lg',
                            )}
                            onClick={() => toggleEvent(eventId)}
                          >
                            <div className="flex flex-1 flex-col gap-1">
                              <div className="flex items-center gap-2">
                                <span className="text-xs font-medium">
                                  {event.summary || 'Untitled Event'}
                                </span>
                                {event.status && (
                                  <span
                                    className={cn(
                                      'rounded px-1.5 py-0.5 text-xs',
                                      event.status === 'confirmed'
                                        ? 'bg-green-100 text-green-700 dark:bg-green-900 dark:text-green-300'
                                        : event.status === 'tentative'
                                          ? 'bg-yellow-100 text-yellow-700 dark:bg-yellow-900 dark:text-yellow-300'
                                          : 'bg-gray-100 text-gray-700 dark:bg-gray-800 dark:text-gray-300',
                                    )}
                                  >
                                    {event.status}
                                  </span>
                                )}
                              </div>
                              <div className="flex items-center gap-2 text-xs text-gray-500 dark:text-gray-400">
                                <span className="font-mono">{eventId}</span>
                                {event.calendarId && (
                                  <span className="rounded bg-emerald-100 px-1.5 py-0.5 text-emerald-700 dark:bg-emerald-900 dark:text-emerald-300">
                                    {event.calendarId}
                                  </span>
                                )}
                              </div>
                              {(event.start || event.end) && (
                                <div className="text-xs text-gray-500 dark:text-gray-400">
                                  {event.start?.dateTime && formatDate(event.start.dateTime)}
                                  {event.start?.date}
                                  {event.end && ' → '}
                                  {event.end?.dateTime && formatDate(event.end.dateTime)}
                                  {event.end?.date}
                                </div>
                              )}
                            </div>
                            <div className="flex items-center gap-1">
                              <button
                                onClick={(e) => {
                                  e.stopPropagation();
                                  onCopy(safeStringify(event), eventId);
                                }}
                                className="rounded p-0.5 hover:bg-gray-200 dark:hover:bg-gray-700"
                              >
                                {copiedId === eventId ? (
                                  <Check className="h-3 w-3 text-green-600" />
                                ) : (
                                  <Copy className="h-3 w-3" />
                                )}
                              </button>
                              {isExpanded ? (
                                <ChevronDown className="h-3 w-3" />
                              ) : (
                                <ChevronRight className="h-3 w-3" />
                              )}
                            </div>
                          </div>
                          <AnimatePresence>
                            {isExpanded && (
                              <motion.div
                                initial={{ height: 0, opacity: 0 }}
                                animate={{ height: 'auto', opacity: 1 }}
                                exit={{ height: 0, opacity: 0 }}
                                transition={{ duration: 0.2 }}
                                className="overflow-hidden"
                              >
                                <div
                                  className="rounded-b-lg p-2 pt-0"
                                  onClick={(e) => e.stopPropagation()}
                                >
                                  <JsonTreeView data={event} onCopy={onCopy} copiedId={copiedId} defaultExpandDepth={2} className="max-h-64" />
                                </div>
                              </motion.div>
                            )}
                          </AnimatePresence>
                        </div>
                      );
                    })}
                  </div>
                )}
              </motion.div>
            )}
          </AnimatePresence>
        </div>
      </div>

      {/* Footer */}
      <div className="border-t border-gray-200 p-2 text-center text-xs text-gray-500 dark:border-gray-700 dark:text-gray-400">
        {searchQuery
          ? `${filteredCalendars.length} calendars, ${filteredEvents.length} events`
          : `${calendars.length} calendars, ${calendarEvents.length} events`}
      </div>
    </div>
  );
};