From b1c3cb99bd7cf218441b877b29de5afe8181df9a Mon Sep 17 00:00:00 2001 From: Florian Egger Date: Sat, 9 May 2026 15:34:57 +0200 Subject: [PATCH] rewrite phase 5 --- CHECKLIST.md | 20 ++-- src/app/calendar/CalendarView.tsx | 151 ++++++++++++++++++++++++++++++ src/app/calendar/DayEvents.tsx | 99 ++++++++++++++++++++ src/app/calendar/UrlTab.tsx | 6 +- src/app/calendar/page.tsx | 81 ++++++++++++++++ src/app/event/BikeSection.tsx | 2 +- src/app/event/LeaveByBadge.tsx | 22 +++-- src/app/event/TrainSection.tsx | 2 +- 8 files changed, 362 insertions(+), 21 deletions(-) create mode 100644 src/app/calendar/CalendarView.tsx create mode 100644 src/app/calendar/DayEvents.tsx create mode 100644 src/app/calendar/page.tsx diff --git a/CHECKLIST.md b/CHECKLIST.md index bce24c2..d3c86b9 100644 --- a/CHECKLIST.md +++ b/CHECKLIST.md @@ -69,15 +69,15 @@ | # | Item | ✅ | ✔️ | |---|------|----|----| -| 27 | `ui/Chip.tsx` — small badge component | [x] | [ ] | +| 27 | `ui/Chip.tsx` — small badge component | [x] | [x] | | 28 | `ui/Button.tsx` — styled button | [x] | [x] | | 29 | `ui/LoadingSpinner.tsx` — loading indicator | [x] | [ ] | -| 30 | `event/LeaveByBadge.tsx` — countdown badge | [x] | [ ] | -| 31 | `event/JourneyList.tsx` — departure rows | [x] | [ ] | -| 32 | `event/TrainSection.tsx` — train data in event card | [x] | [ ] | -| 33 | `event/BikeSection.tsx` — bicycle data in event card (NEW) | [x] | [ ] | -| 34 | `event/EventCard.tsx` — composes train + bike sections | [x] | [ ] | -| 35 | `calendar/UrlTab.tsx` | [x] | [ ] | +| 30 | `event/LeaveByBadge.tsx` — countdown badge | [x] | [x] | +| 31 | `event/JourneyList.tsx` — departure rows | [x] | [x] | +| 32 | `event/TrainSection.tsx` — train data in event card | [x] | [x] | +| 33 | `event/BikeSection.tsx` — bicycle data in event card (NEW) | [x] | [x] | +| 34 | `event/EventCard.tsx` — composes train + bike sections | [x] | [x] | +| 35 | `calendar/UrlTab.tsx` | [x] | [x] | | 36 | `calendar/FileTab.tsx` | [x] | [x] | | 37 | `calendar/CalendarPanel.tsx` | [x] | [x] | | 38 | `add-event/AddEventModal.tsx` | [x] | [x] | @@ -90,9 +90,9 @@ | # | Item | ✅ | ✔️ | |---|------|----|----| -| 41 | `calendar/CalendarView.tsx` — month grid component (NEW) | [ ] | [ ] | -| 42 | `calendar/DayEvents.tsx` — events for a selected day (NEW) | [ ] | [ ] | -| 43 | `app/calendar/page.tsx` — calendar route (NEW) | [ ] | [ ] | +| 41 | `calendar/CalendarView.tsx` — month grid component (NEW) | [x] | [x] | +| 42 | `calendar/DayEvents.tsx` — events for a selected day (NEW) | [x] | [x] | +| 43 | `app/calendar/page.tsx` — calendar route (NEW) | [x] | [x] | --- diff --git a/src/app/calendar/CalendarView.tsx b/src/app/calendar/CalendarView.tsx new file mode 100644 index 0000000..8728361 --- /dev/null +++ b/src/app/calendar/CalendarView.tsx @@ -0,0 +1,151 @@ +"use client"; + +import React, { useState } from "react"; + +interface CalendarViewProps { + selectedDate: Date; + onDateChange: (date: Date) => void; +} + +const CalendarView: React.FC = ({ selectedDate, onDateChange }) => { + const [currentMonth, setCurrentMonth] = useState( + new Date(selectedDate.getFullYear(), selectedDate.getMonth(), 1), + ); + + const goToPreviousMonth = () => { + setCurrentMonth((prev) => new Date(prev.getFullYear(), prev.getMonth() - 1, 1)); + }; + + const goToNextMonth = () => { + setCurrentMonth((prev) => new Date(prev.getFullYear(), prev.getMonth() + 1, 1)); + }; + + const getDaysInMonth = (year: number, month: number) => { + return new Date(year, month + 1, 0).getDate(); + }; + + const getFirstDayOfMonth = (year: number, month: number) => { + return new Date(year, month, 1).getDay(); + }; + + const renderCalendarDays = () => { + const year = currentMonth.getFullYear(); + const month = currentMonth.getMonth(); + const daysInMonth = getDaysInMonth(year, month); + const firstDayOfMonth = getFirstDayOfMonth(year, month); + + const days = []; + + // Previous month's days + const prevMonthDays = getDaysInMonth(year, month - 1); + for (let i = firstDayOfMonth - 1; i >= 0; i--) { + const day = prevMonthDays - i; + const date = new Date(year, month - 1, day); + days.push( +
+ {day} +
, + ); + } + + // Current month's days + for (let i = 1; i <= daysInMonth; i++) { + const date = new Date(year, month, i); + const isSelected = + date.getDate() === selectedDate.getDate() && + date.getMonth() === selectedDate.getMonth() && + date.getFullYear() === selectedDate.getFullYear(); + + days.push( + , + ); + } + + // Next month's days + const totalCells = 42; // 6 weeks * 7 days + const nextMonthDays = totalCells - days.length; + for (let i = 1; i <= nextMonthDays; i++) { + const date = new Date(year, month + 1, i); + days.push( +
+ {i} +
, + ); + } + + return days; + }; + + const monthNames = [ + "January", + "February", + "March", + "April", + "May", + "June", + "July", + "August", + "September", + "October", + "November", + "December", + ]; + + const dayNames = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]; + + return ( +
+
+

+ {monthNames[currentMonth.getMonth()]} {currentMonth.getFullYear()} +

+
+ + + +
+
+ +
+ {dayNames.map((day) => ( +
+ {day} +
+ ))} +
+ +
{renderCalendarDays()}
+
+ ); +}; + +export default CalendarView; diff --git a/src/app/calendar/DayEvents.tsx b/src/app/calendar/DayEvents.tsx new file mode 100644 index 0000000..a9ded68 --- /dev/null +++ b/src/app/calendar/DayEvents.tsx @@ -0,0 +1,99 @@ +"use client"; + +import React from "react"; +import { Event } from "../../types"; + +interface DayEventsProps { + date: Date; + events: Event[]; + loading: boolean; + error: string | null; +} + +const DayEvents: React.FC = ({ date, events, loading, error }) => { + const formatDate = (date: Date) => { + return date.toLocaleDateString("en-US", { + weekday: "long", + year: "numeric", + month: "long", + day: "numeric", + }); + }; + + if (loading) { + return ( +
+
+

{formatDate(date)}

+
+
+ {[1, 2, 3].map((i) => ( +
+
+
+
+
+
+ ))} +
+
+ ); + } + + if (error) { + return ( +
+
+

{formatDate(date)}

+
+
Error: {error}
+
+ ); + } + + return ( +
+
+

{formatDate(date)}

+
+ + {events.length === 0 ? ( +
+

No events scheduled for this day

+
+ ) : ( +
+ {events.map((event) => { + // Format time for display + const eventTime = new Date(event.eventTime); + const timeString = eventTime.toLocaleTimeString("en-US", { + hour: "2-digit", + minute: "2-digit", + }); + + return ( +
+
+
+

{event.title}

+

+ {timeString} • {event.destination} +

+
+ + {event.source === "calendar" ? "Calendar" : "Manual"} + +
+
+ ); + })} +
+ )} +
+ ); +}; + +export default DayEvents; diff --git a/src/app/calendar/UrlTab.tsx b/src/app/calendar/UrlTab.tsx index fed48ca..d5f5fe2 100644 --- a/src/app/calendar/UrlTab.tsx +++ b/src/app/calendar/UrlTab.tsx @@ -37,7 +37,11 @@ const UrlTab: React.FC = ({ onLoadCalendar, loading, error, classNa required /> - {error &&
{error}
} + {error && ( +
+ {error} +
+ )} diff --git a/src/app/calendar/page.tsx b/src/app/calendar/page.tsx new file mode 100644 index 0000000..563f1ec --- /dev/null +++ b/src/app/calendar/page.tsx @@ -0,0 +1,81 @@ +"use client"; + +import React, { useState, useEffect } from "react"; +import CalendarView from "./CalendarView"; +import DayEvents from "./DayEvents"; +import { Event } from "@/types"; + +const CalendarPage: React.FC = () => { + const [selectedDate, setSelectedDate] = useState(new Date()); + const [events, setEvents] = useState([]); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + // In a real app, this would fetch from a database or API + useEffect(() => { + const fetchEvents = async () => { + setLoading(true); + try { + // Mock data for demonstration + const mockEvents: Event[] = [ + { + id: "1", + title: "Meeting with team", + destination: "Berlin", + eventTime: new Date(Date.now() + 86400000), // Tomorrow + source: "manual", + }, + { + id: "2", + title: "Train trip to Munich", + destination: "Munich", + eventTime: new Date(Date.now() + 172800000), // Day after tomorrow + source: "manual", + }, + ]; + setEvents(mockEvents); + } catch (err) { + setError("Failed to load events"); + console.error(err); + } finally { + setLoading(false); + } + }; + + fetchEvents(); + }, []); + + const handleDateChange = (date: Date) => { + setSelectedDate(date); + }; + + // Filter events for the selected date + const eventsForSelectedDate = events.filter((event) => { + const eventDate = new Date(event.eventTime); + return ( + eventDate.getDate() === selectedDate.getDate() && + eventDate.getMonth() === selectedDate.getMonth() && + eventDate.getFullYear() === selectedDate.getFullYear() + ); + }); + + return ( +
+
+

Calendar

+ +
+
+ +
+ +
+ +
+
+
+
+ ); +}; + +export default CalendarPage; diff --git a/src/app/event/BikeSection.tsx b/src/app/event/BikeSection.tsx index 7e12a58..a280507 100644 --- a/src/app/event/BikeSection.tsx +++ b/src/app/event/BikeSection.tsx @@ -27,7 +27,7 @@ const BikeSection: React.FC = ({ bikeRoute, bikeLoading, bikeE {onRefresh && ( )} diff --git a/src/app/event/LeaveByBadge.tsx b/src/app/event/LeaveByBadge.tsx index 9529a6c..168e92a 100644 --- a/src/app/event/LeaveByBadge.tsx +++ b/src/app/event/LeaveByBadge.tsx @@ -1,19 +1,25 @@ -'use client'; +"use client"; -import React from 'react'; -import { CountdownInfo } from '@/types'; -import Chip from '@/app/ui/Chip'; +import React from "react"; +import { CountdownInfo } from "@/types"; +import Chip from "@/app/ui/Chip"; + +const colorMap: Record = { + red: "text-red-600", + orange: "text-orange-600", + yellow: "text-yellow-600", + green: "text-green-600", + blue: "text-blue-600", +}; type LeaveByBadgeProps = { countdown: CountdownInfo; className?: string; }; -const LeaveByBadge: React.FC = ({ countdown, className = '' }) => { +const LeaveByBadge: React.FC = ({ countdown, className = "" }) => { return ( - + {countdown.label} ); diff --git a/src/app/event/TrainSection.tsx b/src/app/event/TrainSection.tsx index ed8bbae..5e20f13 100644 --- a/src/app/event/TrainSection.tsx +++ b/src/app/event/TrainSection.tsx @@ -36,7 +36,7 @@ const TrainSection: React.FC = ({ {onRefresh && ( )}