rewrite phase 6
This commit is contained in:
+10
-10
@@ -14,7 +14,7 @@
|
||||
## Phase 1 — Scaffold Next.js Project (~30 min)
|
||||
|
||||
| # | Item | ✅ | ✔️ |
|
||||
|---|------|----|----|
|
||||
|---|---|----|-|
|
||||
| 1 | Initialize Next.js with App Router, TypeScript, Tailwind CSS | [x] | [x] |
|
||||
| 2 | Set up `tsconfig.json` with strict mode | [x] | [x] |
|
||||
| 3 | Create `.env.example` with `PORT`, `HAFAS_URL`, `NOMINATIM_URL`, `OSRM_URL` | [x] | [x] |
|
||||
@@ -27,7 +27,7 @@
|
||||
## Phase 2 — Types + Library Layer (~2 hours)
|
||||
|
||||
| # | Item | ✅ | ✔️ |
|
||||
|---|------|----|----|
|
||||
|---|---|----|-|
|
||||
| 7 | Define TypeScript types in `src/types/index.ts` | [x] | [x] |
|
||||
| 8 | Port `lib/hafas-client.ts` — HAFAS API client functions with proper types | [x] | [x] |
|
||||
| 9 | Port `lib/calendar-utils.ts` — `extractEvents` + `cleanLocation` | [x] | [x] |
|
||||
@@ -40,7 +40,7 @@
|
||||
## Phase 3 — API Routes (~45 min)
|
||||
|
||||
| # | Item | ✅ | ✔️ |
|
||||
|---|------|----|----|
|
||||
|---|---|----|-|
|
||||
| 13 | `src/app/api/hafas/route.ts` — POST handler, same logic as Express | [x] | [x] |
|
||||
| 14 | `src/app/api/calendar/route.ts` — GET handler for remote ICS | [x] | [x] |
|
||||
| 15 | `src/app/api/calendar/parse/route.ts` — POST handler for ICS body | [x] | [x] |
|
||||
@@ -53,7 +53,7 @@
|
||||
## Phase 4 — Custom Hooks (~2.5 hours)
|
||||
|
||||
| # | Item | ✅ | ✔️ |
|
||||
|---|------|----|----|
|
||||
|---|---|----|-|
|
||||
| 19 | `useServerHealth.ts` — polls `/api/health` every 30s | [x] | [x] |
|
||||
| 20 | `useClock.ts` — interval that updates `now` every 10s | [x] | [x] |
|
||||
| 21 | `useGeolocation.ts` — wraps `navigator.geolocation` | [x] | [x] |
|
||||
@@ -68,10 +68,10 @@
|
||||
## Phase 5 — UI Components (~3.5 hours)
|
||||
|
||||
| # | Item | ✅ | ✔️ |
|
||||
|---|------|----|----|
|
||||
|---|---|----|-|
|
||||
| 27 | `ui/Chip.tsx` — small badge component | [x] | [x] |
|
||||
| 28 | `ui/Button.tsx` — styled button | [x] | [x] |
|
||||
| 29 | `ui/LoadingSpinner.tsx` — loading indicator | [x] | [ ] |
|
||||
| 29 | `ui/LoadingSpinner.tsx` — loading indicator | [x] | [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] |
|
||||
@@ -89,7 +89,7 @@
|
||||
## Phase 6 — Calendar Page (~1 hour)
|
||||
|
||||
| # | Item | ✅ | ✔️ |
|
||||
|---|------|----|----|
|
||||
|---|---|----|-|
|
||||
| 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] |
|
||||
@@ -99,7 +99,7 @@
|
||||
## Phase 7 — Tests (~2 hours)
|
||||
|
||||
| # | Item | ✅ | ✔️ |
|
||||
|---|------|----|----|
|
||||
|---|---|----|-|
|
||||
| 44 | Migrate `server/__tests__/*.test.js` → `src/app/api/__tests__/*.test.ts` | [ ] | [ ] |
|
||||
| 45 | Add unit tests for `calendar-utils.ts`, `countdown-utils.ts` in `src/lib/__tests__/` | [ ] | [ ] |
|
||||
| 46 | Add API tests for `geocode` and `bike-route` in `src/app/api/__tests__/` (NEW) | [ ] | [ ] |
|
||||
@@ -110,7 +110,7 @@
|
||||
## Phase 8 — Cleanup (~30 min)
|
||||
|
||||
| # | Item | ✅ | ✔️ |
|
||||
|---|------|----|----|
|
||||
|---|---|----|-|
|
||||
| 48 | Delete old `server/` directory | [ ] | [ ] |
|
||||
| 49 | Delete old `oebb-planner-app/` directory | [ ] | [ ] |
|
||||
| 50 | Delete `oebb-planner.jsx` | [ ] | [ ] |
|
||||
@@ -121,4 +121,4 @@
|
||||
|
||||
**✅ Implemented** — Code exists and is in the repo.
|
||||
**✔️ Reviewed** — Code has been reviewed for correctness against the plan.
|
||||
`[ ]` pending required · `[x]` done · `[~]` optional/deferred (never blocks phase advancement)
|
||||
`[ ]` pending required · `[x]` done · `[~]` optional/deferred (never blocks phase advancement)
|
||||
Generated
+11
@@ -8,6 +8,7 @@
|
||||
"name": "oebb_planner",
|
||||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"date-fns": "^4.1.0",
|
||||
"next": "16.2.6",
|
||||
"node-ical": "^0.18.0",
|
||||
"react": "19.2.4",
|
||||
@@ -3751,6 +3752,16 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/date-fns": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz",
|
||||
"integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/kossnocorp"
|
||||
}
|
||||
},
|
||||
"node_modules/debug": {
|
||||
"version": "4.4.3",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
"test:watch": "vitest"
|
||||
},
|
||||
"dependencies": {
|
||||
"date-fns": "^4.1.0",
|
||||
"next": "16.2.6",
|
||||
"node-ical": "^0.18.0",
|
||||
"react": "19.2.4",
|
||||
|
||||
+112
-127
@@ -1,149 +1,134 @@
|
||||
"use client";
|
||||
|
||||
import React, { useState } from "react";
|
||||
import { format, startOfMonth, endOfMonth, eachDayOfInterval, isSameMonth, isToday } from "date-fns";
|
||||
import { Event } from "@/types";
|
||||
|
||||
interface CalendarViewProps {
|
||||
type CalendarViewProps = {
|
||||
events: Event[];
|
||||
onDateSelect: (date: Date) => void;
|
||||
selectedDate: Date;
|
||||
onDateChange: (date: Date) => void;
|
||||
}
|
||||
className?: string;
|
||||
};
|
||||
|
||||
const CalendarView: React.FC<CalendarViewProps> = ({ selectedDate, onDateChange }) => {
|
||||
const [currentMonth, setCurrentMonth] = useState<Date>(
|
||||
new Date(selectedDate.getFullYear(), selectedDate.getMonth(), 1),
|
||||
);
|
||||
const CalendarView: React.FC<CalendarViewProps> = ({ events, onDateSelect, selectedDate, className = "" }) => {
|
||||
const [currentDate, setCurrentDate] = useState<Date>(selectedDate);
|
||||
|
||||
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(
|
||||
const renderHeader = () => {
|
||||
return (
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<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"
|
||||
}`}
|
||||
onClick={() => setCurrentDate(new Date(currentDate.getFullYear(), currentDate.getMonth() - 1, 1))}
|
||||
className="p-2 rounded-full hover:bg-gray-100 dark:hover:bg-gray-700"
|
||||
>
|
||||
{i}
|
||||
</button>,
|
||||
);
|
||||
}
|
||||
←
|
||||
</button>
|
||||
<h2 className="text-xl font-semibold text-gray-900 dark:text-white">{format(currentDate, "MMMM yyyy")}</h2>
|
||||
<button
|
||||
onClick={() => setCurrentDate(new Date(currentDate.getFullYear(), currentDate.getMonth() + 1, 1))}
|
||||
className="p-2 rounded-full hover:bg-gray-100 dark:hover:bg-gray-700"
|
||||
>
|
||||
→
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// 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}
|
||||
const renderDays = () => {
|
||||
const headers = [];
|
||||
const daysOfWeek = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
|
||||
|
||||
for (let i = 0; i < 7; i++) {
|
||||
headers.push(
|
||||
<div key={i} className="text-center font-medium text-gray-600 dark:text-gray-300 py-2">
|
||||
{daysOfWeek[i]}
|
||||
</div>,
|
||||
);
|
||||
}
|
||||
|
||||
return days;
|
||||
return <div className="grid grid-cols-7 gap-1 mb-1">{headers}</div>;
|
||||
};
|
||||
|
||||
const monthNames = [
|
||||
"January",
|
||||
"February",
|
||||
"March",
|
||||
"April",
|
||||
"May",
|
||||
"June",
|
||||
"July",
|
||||
"August",
|
||||
"September",
|
||||
"October",
|
||||
"November",
|
||||
"December",
|
||||
];
|
||||
const renderCells = () => {
|
||||
const monthStart = startOfMonth(currentDate);
|
||||
const monthEnd = endOfMonth(monthStart);
|
||||
const startDate = startOfMonth(monthStart);
|
||||
const endDate = endOfMonth(monthEnd);
|
||||
|
||||
const dayNames = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
|
||||
const days = eachDayOfInterval({ start: startDate, end: endDate });
|
||||
|
||||
const cells: React.ReactNode[] = [];
|
||||
|
||||
days.forEach((day, index) => {
|
||||
const dayEvents = events.filter((event) => {
|
||||
const eventDate = new Date(event.eventTime);
|
||||
return (
|
||||
eventDate.getDate() === day.getDate() &&
|
||||
eventDate.getMonth() === day.getMonth() &&
|
||||
eventDate.getFullYear() === day.getFullYear()
|
||||
);
|
||||
});
|
||||
|
||||
const isCurrentMonth = isSameMonth(day, monthStart);
|
||||
const isTodayDate = isToday(day);
|
||||
|
||||
cells.push(
|
||||
<div
|
||||
key={index}
|
||||
onClick={() => onDateSelect(day)}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
className={`
|
||||
min-h-24 p-2 border border-gray-200 dark:border-gray-700 rounded-lg cursor-pointer
|
||||
${!isCurrentMonth ? "bg-gray-50 dark:bg-gray-800 text-gray-400 dark:text-gray-500" : "bg-white dark:bg-gray-900"}
|
||||
${isTodayDate ? "ring-2 ring-blue-500" : ""}
|
||||
hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors
|
||||
`}
|
||||
>
|
||||
<div className="text-right">
|
||||
<span
|
||||
className={`inline-flex items-center justify-center w-6 h-6 rounded-full text-sm ${
|
||||
isTodayDate ? "bg-blue-500 text-white" : ""
|
||||
}`}
|
||||
>
|
||||
{format(day, "d")}
|
||||
</span>
|
||||
</div>
|
||||
<div className="mt-1 space-y-1 max-h-20 overflow-y-auto">
|
||||
{dayEvents.slice(0, 3).map((event) => (
|
||||
<div
|
||||
key={event.id}
|
||||
className="text-xs truncate bg-blue-100 dark:bg-blue-900 text-blue-800 dark:text-blue-200 px-2 py-1 rounded"
|
||||
>
|
||||
{event.title}
|
||||
</div>
|
||||
))}
|
||||
{dayEvents.length > 3 && (
|
||||
<div className="text-xs text-gray-500 dark:text-gray-400">+{dayEvents.length - 3} more</div>
|
||||
)}
|
||||
</div>
|
||||
</div>,
|
||||
);
|
||||
});
|
||||
|
||||
// Group cells into rows of 7
|
||||
const rows = [];
|
||||
for (let i = 0; i < cells.length; i += 7) {
|
||||
rows.push(
|
||||
<div key={i} className="grid grid-cols-7 gap-1 mb-1">
|
||||
{cells.slice(i, i + 7)}
|
||||
</div>,
|
||||
);
|
||||
}
|
||||
|
||||
return <div>{rows}</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-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 className={`bg-white dark:bg-gray-800 rounded-lg shadow-sm p-4 ${className}`}>
|
||||
{renderHeader()}
|
||||
{renderDays()}
|
||||
{renderCells()}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,97 +1,48 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import { Event } from "../../types";
|
||||
import { format } from "date-fns";
|
||||
import { Event, TripDataEntry } from "@/types";
|
||||
import EventCard from "@/app/event/EventCard";
|
||||
|
||||
interface DayEventsProps {
|
||||
date: Date;
|
||||
type DayEventsProps = {
|
||||
events: Event[];
|
||||
loading: boolean;
|
||||
error: string | null;
|
||||
}
|
||||
date: Date;
|
||||
tripData?: Record<string, TripDataEntry>; // Map of event id to trip data
|
||||
className?: string;
|
||||
};
|
||||
|
||||
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) {
|
||||
const DayEvents: React.FC<DayEventsProps> = ({ events, date, tripData = {}, className = "" }) => {
|
||||
const filteredEvents = events.filter((event) => {
|
||||
// Direct Date comparison instead of parseISO
|
||||
const eventDate = new Date(event.eventTime);
|
||||
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>
|
||||
eventDate.getDate() === date.getDate() &&
|
||||
eventDate.getMonth() === date.getMonth() &&
|
||||
eventDate.getFullYear() === date.getFullYear()
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
if (error) {
|
||||
if (filteredEvents.length === 0) {
|
||||
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 className={`text-center py-8 text-gray-500 dark:text-gray-400 ${className}`}>
|
||||
<p>No events scheduled for {format(date, "MMMM d, yyyy")}</p>
|
||||
</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 className={`space-y-4 ${className}`}>
|
||||
<h3 className="text-lg font-medium text-gray-900 dark:text-white">Events for {format(date, "MMMM d, yyyy")}</h3>
|
||||
<div className="space-y-3">
|
||||
{filteredEvents.map((event) => (
|
||||
<EventCard
|
||||
key={event.id}
|
||||
event={event}
|
||||
tripData={tripData[event.id] || { journeys: [], destName: "", demo: false, loading: false }}
|
||||
/>
|
||||
))}
|
||||
</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>
|
||||
);
|
||||
};
|
||||
|
||||
+41
-65
@@ -1,81 +1,57 @@
|
||||
"use client";
|
||||
|
||||
import React, { useState, useEffect } from "react";
|
||||
import React from "react";
|
||||
import { Event } from "@/types";
|
||||
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);
|
||||
export default function CalendarPage() {
|
||||
// Mock events for demonstration
|
||||
const events: Event[] = [
|
||||
{
|
||||
id: "1",
|
||||
title: "Meeting with team",
|
||||
destination: "Berlin",
|
||||
eventTime: new Date(new Date().setDate(new Date().getDate() + 1)), // Tomorrow
|
||||
source: "manual",
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
title: "Train trip to Munich",
|
||||
destination: "Munich",
|
||||
eventTime: new Date(new Date().setDate(new Date().getDate() + 2)), // Day after tomorrow
|
||||
source: "manual",
|
||||
},
|
||||
{
|
||||
id: "3",
|
||||
title: "Conference in Vienna",
|
||||
destination: "Vienna",
|
||||
eventTime: new Date(new Date().setDate(new Date().getDate() + 3)), // In 3 days
|
||||
source: "calendar",
|
||||
},
|
||||
];
|
||||
|
||||
// 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);
|
||||
}
|
||||
};
|
||||
const [selectedDate, setSelectedDate] = React.useState<Date>(new Date());
|
||||
|
||||
fetchEvents();
|
||||
}, []);
|
||||
|
||||
const handleDateChange = (date: Date) => {
|
||||
const handleDateSelect = (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="max-w-6xl mx-auto p-4">
|
||||
<div className="mb-6">
|
||||
<h1 className="text-3xl font-bold text-gray-900 dark:text-white">Calendar</h1>
|
||||
<p className="text-gray-600 dark:text-gray-400">View and manage your events</p>
|
||||
</div>
|
||||
|
||||
<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 className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
<div className="lg:col-span-2">
|
||||
<CalendarView events={events} onDateSelect={handleDateSelect} selectedDate={selectedDate} />
|
||||
</div>
|
||||
<div>
|
||||
<DayEvents events={events} date={selectedDate} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CalendarPage;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user