diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 9ae7b6f..bfc04bb 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -40,7 +40,6 @@ const CalculatorWidget = React.lazy(() => import('./components/CalculatorWidget. const TaskComposerPanel = React.lazy(() => import('./components/TaskComposerPanel.jsx')); const FriendsPanel = React.lazy(() => import('./components/FriendsPanel.jsx')); const Calendar = React.lazy(() => import('./components/Calendar.jsx')); -const CalendarSummaryCard = React.lazy(() => import('./components/CalendarSummaryCard.jsx')); const DynamicColorPlugin = registerPlugin('DynamicColor'); @@ -1642,11 +1641,6 @@ function App() { - - }> - setShowCalendar(true)} /> - - }> @@ -1748,18 +1742,11 @@ function App() { /> -
- - }> - - - - - }> - setShowCalendar(true)} /> - - -
+ + }> + + +
diff --git a/frontend/src/components/Calendar.jsx b/frontend/src/components/Calendar.jsx index 49fb50b..c0d2a79 100644 --- a/frontend/src/components/Calendar.jsx +++ b/frontend/src/components/Calendar.jsx @@ -20,7 +20,7 @@ const COLOR_PRESETS = [ { name: 'Pink', value: '#ec4899' } ]; -export const getDDayText = (targetDate) => { +const getDDayText = (targetDate) => { if (!targetDate) return ''; const today = new Date(); today.setHours(0, 0, 0, 0); @@ -32,7 +32,7 @@ export const getDDayText = (targetDate) => { return `D+${Math.abs(diff)}`; }; -export const formatDateStr = (d) => { +const formatDateStr = (d) => { const yr = d.getFullYear(); const mo = String(d.getMonth() + 1).padStart(2, '0'); const dy = String(d.getDate()).padStart(2, '0'); @@ -97,7 +97,7 @@ const getRruleLabel = (rule) => { // Splits a stored timestamp into an editable date ('YYYY-MM-DD') + time ('HH:MM'). // Handles naive values ('2026-09-01', '2026-09-01T17:15') and full Google ISO // values ('2026-09-01T17:15:00+05:30') — keeps the wall-clock time as-is. -export const splitDateTime = (value) => { +const splitDateTime = (value) => { if (!value) return { date: '', time: '' }; const s = String(value); const m = s.match(/^(\d{4}-\d{2}-\d{2})(?:T(\d{2}):(\d{2})(?::\d{2})?.*)?$/); @@ -106,7 +106,7 @@ export const splitDateTime = (value) => { }; // "5:15 PM" style wall-clock display for the detail panel. -export const getEventTime = (ev) => { +const getEventTime = (ev) => { if (!ev || ev.is_all_day || !ev.event_date) return ''; const { time } = splitDateTime(ev.event_date); if (!time) return ''; @@ -313,7 +313,7 @@ const CalendarCell = memo(({ dateStr, day, currentMonth, isToday, isSelected, da // Does this event occupy the given calendar day (YYYY-MM-DD)? // Handles single/multi-day spans AND recurring events (DAILY/WEEKLY/MONTHLY/YEARLY // with optional INTERVAL). For recurring events the optional end_date caps the series. -export const isEventOnDate = (ev, dateStr) => { +const isEventOnDate = (ev, dateStr) => { const startStr = ev.event_date ? String(ev.event_date).slice(0, 10) : ''; if (!startStr) return false; // A single occurrence was "deleted" via the exclude endpoint — skip that date. @@ -761,12 +761,8 @@ const Calendar = () => { const res = await apiFetch('/api/calendar/events'); if (res && res.ok) { const data = await res.json(); - if (Array.isArray(data)) { - setEvents(data); - window.dispatchEvent(new CustomEvent('snowball-calendar-updated', { detail: data })); - } else { - setFetchError(true); - } + if (Array.isArray(data)) setEvents(data); + else setFetchError(true); } else { setFetchError(true); } diff --git a/frontend/src/components/CalendarSummaryCard.jsx b/frontend/src/components/CalendarSummaryCard.jsx deleted file mode 100644 index 2f73354..0000000 --- a/frontend/src/components/CalendarSummaryCard.jsx +++ /dev/null @@ -1,446 +0,0 @@ -import React, { useState, useEffect, useCallback, useMemo } from 'react'; -import { - CalendarDays, ChevronDown, ChevronUp, Clock, - Repeat, Plus, ExternalLink, Timer, Sparkles -} from 'lucide-react'; -import { apiFetch } from '../utils/apiClient.js'; -import { getTagColor, loadTagColors, parseTags, getReadableTextColor } from '../utils/tagColors.js'; -import { - getDDayText, formatDateStr, splitDateTime, - getEventTime, isEventOnDate -} from './Calendar.jsx'; - -const CalendarSummaryCard = ({ onOpenCalendar }) => { - const [isExpanded, setIsExpanded] = useState(() => { - const saved = localStorage.getItem('snowball_calendar_summary_expanded'); - return saved !== null ? JSON.parse(saved) : true; - }); - - const [events, setEvents] = useState([]); - const [loading, setLoading] = useState(true); - const [tagColors, setTagColors] = useState(() => loadTagColors()); - - useEffect(() => { - localStorage.setItem('snowball_calendar_summary_expanded', JSON.stringify(isExpanded)); - }, [isExpanded]); - - const fetchEvents = useCallback(async () => { - try { - const res = await apiFetch('/api/calendar/events'); - if (res && res.ok) { - const data = await res.json(); - if (Array.isArray(data)) setEvents(data); - } - } catch (err) { - console.error('Failed to fetch calendar events in summary card:', err); - } finally { - setLoading(false); - } - }, []); - - useEffect(() => { - fetchEvents(); - - const handleCalendarUpdated = (e) => { - if (e?.detail && Array.isArray(e.detail)) { - setEvents(e.detail); - } else { - fetchEvents(); - } - }; - - const handleTagColorsChanged = () => { - setTagColors(loadTagColors()); - }; - - window.addEventListener('snowball-calendar-updated', handleCalendarUpdated); - window.addEventListener('snowball-tag-colors-changed', handleTagColorsChanged); - - return () => { - window.removeEventListener('snowball-calendar-updated', handleCalendarUpdated); - window.removeEventListener('snowball-tag-colors-changed', handleTagColorsChanged); - }; - }, [fetchEvents]); - - const today = useMemo(() => new Date(), []); - const todayStr = useMemo(() => formatDateStr(today), [today]); - - // Compute today's scheduled events (handling recurring + multi-day spans) - const todayEvents = useMemo(() => { - const list = events.filter((ev) => isEventOnDate(ev, todayStr)); - return list.sort((a, b) => { - if (a.is_all_day && !b.is_all_day) return -1; - if (!a.is_all_day && b.is_all_day) return 1; - const timeA = splitDateTime(a.event_date).time || '00:00'; - const timeB = splitDateTime(b.event_date).time || '00:00'; - return timeA.localeCompare(timeB); - }); - }, [events, todayStr]); - - // Compute upcoming D-Day countdowns - const ddayList = useMemo(() => { - const todayStart = new Date(today); - todayStart.setHours(0, 0, 0, 0); - - return events - .filter((e) => e.is_dday && e.dday_target_date) - .map((e) => { - const target = new Date(e.dday_target_date); - target.setHours(0, 0, 0, 0); - const diff = Math.ceil((target - todayStart) / (1000 * 60 * 60 * 24)); - return { - ...e, - diff, - dText: getDDayText(e.dday_target_date) - }; - }) - .sort((a, b) => { - // Future/today first (diff >= 0), ascending by distance - if (a.diff >= 0 && b.diff < 0) return -1; - if (a.diff < 0 && b.diff >= 0) return 1; - if (a.diff >= 0 && b.diff >= 0) return a.diff - b.diff; - // Past D-Days descending - return b.diff - a.diff; - }); - }, [events, today]); - - const nearestDDay = ddayList.length > 0 ? ddayList[0] : null; - - const formattedDate = useMemo(() => { - return today.toLocaleDateString(undefined, { - weekday: 'short', - month: 'short', - day: 'numeric' - }); - }, [today]); - - return ( -
- {/* Header with Title, D-Day counter pill, and Collapse trigger */} -
setIsExpanded(!isExpanded)} - style={{ - padding: '0.75rem 1rem', - background: 'var(--bg-secondary)', - display: 'flex', - justifyContent: 'space-between', - alignItems: 'center', - cursor: 'pointer', - borderBottom: isExpanded ? '1px solid var(--border-color)' : 'none', - gap: '0.5rem' - }} - > -
- -

- Schedule -

-
- -
- {nearestDDay && ( -
{ - e.stopPropagation(); - if (onOpenCalendar) onOpenCalendar(); - }} - title={`D-Day Target: ${nearestDDay.title} (${nearestDDay.dday_target_date})`} - style={{ - display: 'flex', - alignItems: 'center', - gap: '0.35rem', - padding: '2px 8px', - borderRadius: '999px', - fontSize: '0.72rem', - fontWeight: '700', - cursor: 'pointer', - background: - nearestDDay.diff === 0 - ? 'rgba(239, 68, 68, 0.15)' - : 'rgba(var(--accent-rgb), 0.12)', - border: - nearestDDay.diff === 0 - ? '1px solid rgba(239, 68, 68, 0.35)' - : '1px solid rgba(var(--accent-rgb), 0.25)', - color: nearestDDay.diff === 0 ? '#ef4444' : 'var(--accent-color)', - transition: 'all 0.15s ease' - }} - > - - {nearestDDay.dText} - | - - {nearestDDay.title} - -
- )} - -
- {isExpanded ? : } -
-
-
- - {/* Expanded Body */} - {isExpanded && ( -
- {/* Sub-bar: Overview date and Google sync status */} -
-
- - {todayEvents.length} event{todayEvents.length === 1 ? '' : 's'} today - - • - {formattedDate} -
-
- - {/* Today's Events Timeline */} -
- {loading ? ( -
- Loading schedule... -
- ) : todayEvents.length === 0 ? ( -
- - - No events scheduled for today - -
- ) : ( - todayEvents.map((ev) => { - const parsedTags = parseTags(ev.tags); - const eventTime = getEventTime(ev); - const isRecurring = /FREQ=/.test(String(ev.recurrence_rule || '')); - - return ( -
{ - if (onOpenCalendar) onOpenCalendar(); - }} - style={{ - display: 'flex', - alignItems: 'flex-start', - gap: '0.65rem', - padding: '0.5rem 0.65rem', - borderRadius: 'calc(var(--radius) - 2px)', - background: 'var(--bg-primary)', - border: '1px solid var(--border-color)', - cursor: 'pointer', - transition: 'border-color 0.15s ease' - }} - onMouseEnter={(e) => { - e.currentTarget.style.borderColor = 'var(--accent-color)'; - }} - onMouseLeave={(e) => { - e.currentTarget.style.borderColor = 'var(--border-color)'; - }} - > - {/* Time badge */} -
- {ev.is_all_day ? 'All Day' : eventTime || 'Scheduled'} -
- - {/* Event Details */} -
-
- - {ev.title} - - - {isRecurring && ( - - )} -
- - {/* Tag badges */} - {parsedTags.length > 0 && ( -
- {parsedTags.map((tag) => { - const bg = getTagColor(tag, tagColors); - const fg = getReadableTextColor(bg); - return ( - - {tag} - - ); - })} -
- )} -
-
- ); - }) - )} -
- - {/* Footer Quick Action Buttons */} -
- - - -
-
- )} -
- ); -}; - -export default CalendarSummaryCard;