rewrite phase 5
This commit is contained in:
+10
-10
@@ -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] |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -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"
|
||||
>
|
||||
<
|
||||
</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"
|
||||
>
|
||||
>
|
||||
</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;
|
||||
@@ -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;
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -36,7 +36,7 @@ const TrainSection: React.FC<TrainSectionProps> = ({
|
||||
</div>
|
||||
{onRefresh && (
|
||||
<Button variant="secondary" size="sm" onClick={onRefresh}>
|
||||
{loading ? <>Refresh</> : <>Refresh</>}
|
||||
Refresh
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user