rewrite phase 5
This commit is contained in:
+10
-10
@@ -69,15 +69,15 @@
|
|||||||
|
|
||||||
| # | Item | ✅ | ✔️ |
|
| # | 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] |
|
| 28 | `ui/Button.tsx` — styled button | [x] | [x] |
|
||||||
| 29 | `ui/LoadingSpinner.tsx` — loading indicator | [x] | [ ] |
|
| 29 | `ui/LoadingSpinner.tsx` — loading indicator | [x] | [ ] |
|
||||||
| 30 | `event/LeaveByBadge.tsx` — countdown badge | [x] | [ ] |
|
| 30 | `event/LeaveByBadge.tsx` — countdown badge | [x] | [x] |
|
||||||
| 31 | `event/JourneyList.tsx` — departure rows | [x] | [ ] |
|
| 31 | `event/JourneyList.tsx` — departure rows | [x] | [x] |
|
||||||
| 32 | `event/TrainSection.tsx` — train data in event card | [x] | [ ] |
|
| 32 | `event/TrainSection.tsx` — train data in event card | [x] | [x] |
|
||||||
| 33 | `event/BikeSection.tsx` — bicycle data in event card (NEW) | [x] | [ ] |
|
| 33 | `event/BikeSection.tsx` — bicycle data in event card (NEW) | [x] | [x] |
|
||||||
| 34 | `event/EventCard.tsx` — composes train + bike sections | [x] | [ ] |
|
| 34 | `event/EventCard.tsx` — composes train + bike sections | [x] | [x] |
|
||||||
| 35 | `calendar/UrlTab.tsx` | [x] | [ ] |
|
| 35 | `calendar/UrlTab.tsx` | [x] | [x] |
|
||||||
| 36 | `calendar/FileTab.tsx` | [x] | [x] |
|
| 36 | `calendar/FileTab.tsx` | [x] | [x] |
|
||||||
| 37 | `calendar/CalendarPanel.tsx` | [x] | [x] |
|
| 37 | `calendar/CalendarPanel.tsx` | [x] | [x] |
|
||||||
| 38 | `add-event/AddEventModal.tsx` | [x] | [x] |
|
| 38 | `add-event/AddEventModal.tsx` | [x] | [x] |
|
||||||
@@ -90,9 +90,9 @@
|
|||||||
|
|
||||||
| # | Item | ✅ | ✔️ |
|
| # | Item | ✅ | ✔️ |
|
||||||
|---|------|----|----|
|
|---|------|----|----|
|
||||||
| 41 | `calendar/CalendarView.tsx` — month grid component (NEW) | [ ] | [ ] |
|
| 41 | `calendar/CalendarView.tsx` — month grid component (NEW) | [x] | [x] |
|
||||||
| 42 | `calendar/DayEvents.tsx` — events for a selected day (NEW) | [ ] | [ ] |
|
| 42 | `calendar/DayEvents.tsx` — events for a selected day (NEW) | [x] | [x] |
|
||||||
| 43 | `app/calendar/page.tsx` — calendar route (NEW) | [ ] | [ ] |
|
| 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
|
required
|
||||||
/>
|
/>
|
||||||
</div>
|
</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}>
|
<Button type="submit" disabled={loading}>
|
||||||
{loading ? <>Load Calendar</> : <>Load Calendar</>}
|
{loading ? <>Load Calendar</> : <>Load Calendar</>}
|
||||||
</Button>
|
</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>
|
</div>
|
||||||
{onRefresh && (
|
{onRefresh && (
|
||||||
<Button variant="secondary" size="sm" onClick={onRefresh}>
|
<Button variant="secondary" size="sm" onClick={onRefresh}>
|
||||||
{bikeLoading ? <>Refresh</> : <>Refresh</>}
|
Refresh
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,19 +1,25 @@
|
|||||||
'use client';
|
"use client";
|
||||||
|
|
||||||
import React from 'react';
|
import React from "react";
|
||||||
import { CountdownInfo } from '@/types';
|
import { CountdownInfo } from "@/types";
|
||||||
import Chip from '@/app/ui/Chip';
|
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 = {
|
type LeaveByBadgeProps = {
|
||||||
countdown: CountdownInfo;
|
countdown: CountdownInfo;
|
||||||
className?: string;
|
className?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const LeaveByBadge: React.FC<LeaveByBadgeProps> = ({ countdown, className = '' }) => {
|
const LeaveByBadge: React.FC<LeaveByBadgeProps> = ({ countdown, className = "" }) => {
|
||||||
return (
|
return (
|
||||||
<Chip className={
|
<Chip className={`${className} ${colorMap[countdown.color] || ""} ${countdown.urgent ? "animate-pulse" : ""}`}>
|
||||||
`${className} ${countdown.color} ${countdown.urgent ? 'animate-pulse' : ''}`
|
|
||||||
}>
|
|
||||||
{countdown.label}
|
{countdown.label}
|
||||||
</Chip>
|
</Chip>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ const TrainSection: React.FC<TrainSectionProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
{onRefresh && (
|
{onRefresh && (
|
||||||
<Button variant="secondary" size="sm" onClick={onRefresh}>
|
<Button variant="secondary" size="sm" onClick={onRefresh}>
|
||||||
{loading ? <>Refresh</> : <>Refresh</>}
|
Refresh
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user