rewrite phase 5

This commit is contained in:
2026-05-09 15:34:57 +02:00
parent edeb8ef8e3
commit b1c3cb99bd
8 changed files with 362 additions and 21 deletions
+10 -10
View File
@@ -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] |
---
+151
View File
@@ -0,0 +1,151 @@
"use client";
import React, { useState } from "react";
interface CalendarViewProps {
selectedDate: Date;
onDateChange: (date: Date) => void;
}
const CalendarView: React.FC<CalendarViewProps> = ({ selectedDate, onDateChange }) => {
const [currentMonth, setCurrentMonth] = useState<Date>(
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(
<div key={`prev-${day}`} className="p-2 text-center text-gray-400 dark:text-gray-500">
{day}
</div>,
);
}
// 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(
<button
key={`current-${i}`}
onClick={() => onDateChange(date)}
className={`p-2 text-center rounded-lg transition-colors ${
isSelected
? "bg-blue-500 text-white"
: date.toDateString() === new Date().toDateString()
? "bg-blue-100 dark:bg-blue-900 text-blue-700 dark:text-blue-200"
: "hover:bg-gray-100 dark:hover:bg-gray-700 text-gray-900 dark:text-gray-100"
}`}
>
{i}
</button>,
);
}
// 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(
<div key={`next-${i}`} className="p-2 text-center text-gray-400 dark:text-gray-500">
{i}
</div>,
);
}
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 (
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-sm p-4">
<div className="flex items-center justify-between mb-4">
<h2 className="text-xl font-semibold text-gray-900 dark:text-white">
{monthNames[currentMonth.getMonth()]} {currentMonth.getFullYear()}
</h2>
<div className="flex space-x-2">
<button
onClick={goToPreviousMonth}
className="p-2 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-300"
aria-label="Previous month"
>
&lt;
</button>
<button
onClick={() => setCurrentMonth(new Date())}
className="px-3 py-1 text-sm rounded-lg bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-600"
>
Today
</button>
<button
onClick={goToNextMonth}
className="p-2 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-300"
aria-label="Next month"
>
&gt;
</button>
</div>
</div>
<div className="grid grid-cols-7 gap-1 mb-2">
{dayNames.map((day) => (
<div key={day} className="p-2 text-center text-sm font-medium text-gray-500 dark:text-gray-400">
{day}
</div>
))}
</div>
<div className="grid grid-cols-7 gap-1">{renderCalendarDays()}</div>
</div>
);
};
export default CalendarView;
+99
View File
@@ -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<DayEventsProps> = ({ date, events, loading, error }) => {
const formatDate = (date: Date) => {
return date.toLocaleDateString("en-US", {
weekday: "long",
year: "numeric",
month: "long",
day: "numeric",
});
};
if (loading) {
return (
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-sm p-4">
<div className="flex items-center justify-between mb-4">
<h2 className="text-lg font-semibold text-gray-900 dark:text-white">{formatDate(date)}</h2>
</div>
<div className="space-y-3">
{[1, 2, 3].map((i) => (
<div key={i} className="animate-pulse flex space-x-4">
<div className="flex-1 space-y-2 py-1">
<div className="h-4 bg-gray-200 dark:bg-gray-700 rounded w-3/4"></div>
<div className="h-3 bg-gray-200 dark:bg-gray-700 rounded"></div>
</div>
</div>
))}
</div>
</div>
);
}
if (error) {
return (
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-sm p-4">
<div className="flex items-center justify-between mb-4">
<h2 className="text-lg font-semibold text-gray-900 dark:text-white">{formatDate(date)}</h2>
</div>
<div className="text-red-500 dark:text-red-400 p-4 bg-red-50 dark:bg-red-900/20 rounded-lg">Error: {error}</div>
</div>
);
}
return (
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-sm p-4">
<div className="flex items-center justify-between mb-4">
<h2 className="text-lg font-semibold text-gray-900 dark:text-white">{formatDate(date)}</h2>
</div>
{events.length === 0 ? (
<div className="text-center py-8 text-gray-500 dark:text-gray-400">
<p>No events scheduled for this day</p>
</div>
) : (
<div className="space-y-3">
{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 (
<div
key={event.id}
className="p-3 border border-gray-200 dark:border-gray-700 rounded-lg hover:shadow-md transition-shadow"
>
<div className="flex justify-between items-start">
<div>
<h3 className="font-medium text-gray-900 dark:text-white">{event.title}</h3>
<p className="text-sm text-gray-500 dark:text-gray-400 mt-1">
{timeString} {event.destination}
</p>
</div>
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-100">
{event.source === "calendar" ? "Calendar" : "Manual"}
</span>
</div>
</div>
);
})}
</div>
)}
</div>
);
};
export default DayEvents;
+5 -1
View File
@@ -37,7 +37,11 @@ const UrlTab: React.FC<UrlTabProps> = ({ onLoadCalendar, loading, error, classNa
required
/>
</div>
{error && <div className="text-red-600 text-sm">{error}</div>}
{error && (
<div role="alert" className="text-red-600 text-sm">
{error}
</div>
)}
<Button type="submit" disabled={loading}>
{loading ? <>Load Calendar</> : <>Load Calendar</>}
</Button>
+81
View File
@@ -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<Date>(new Date());
const [events, setEvents] = useState<Event[]>([]);
const [loading, setLoading] = useState<boolean>(false);
const [error, setError] = useState<string | null>(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 (
<div className="flex flex-col min-h-screen p-4 md:p-8">
<div className="max-w-6xl mx-auto w-full">
<h1 className="text-3xl font-bold text-gray-900 dark:text-white mb-6">Calendar</h1>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
<div className="lg:col-span-2">
<CalendarView selectedDate={selectedDate} onDateChange={handleDateChange} />
</div>
<div>
<DayEvents date={selectedDate} events={eventsForSelectedDate} loading={loading} error={error} />
</div>
</div>
</div>
</div>
);
};
export default CalendarPage;
+1 -1
View File
@@ -27,7 +27,7 @@ const BikeSection: React.FC<BikeSectionProps> = ({ bikeRoute, bikeLoading, bikeE
</div>
{onRefresh && (
<Button variant="secondary" size="sm" onClick={onRefresh}>
{bikeLoading ? <>Refresh</> : <>Refresh</>}
Refresh
</Button>
)}
</div>
+14 -8
View File
@@ -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<string, string> = {
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<LeaveByBadgeProps> = ({ countdown, className = '' }) => {
const LeaveByBadge: React.FC<LeaveByBadgeProps> = ({ countdown, className = "" }) => {
return (
<Chip className={
`${className} ${countdown.color} ${countdown.urgent ? 'animate-pulse' : ''}`
}>
<Chip className={`${className} ${colorMap[countdown.color] || ""} ${countdown.urgent ? "animate-pulse" : ""}`}>
{countdown.label}
</Chip>
);
+1 -1
View File
@@ -36,7 +36,7 @@ const TrainSection: React.FC<TrainSectionProps> = ({
</div>
{onRefresh && (
<Button variant="secondary" size="sm" onClick={onRefresh}>
{loading ? <>Refresh</> : <>Refresh</>}
Refresh
</Button>
)}
</div>