From d51169f6411b68a226d76d2d39826904de484929 Mon Sep 17 00:00:00 2001 From: Ariel Costas Guerrero Date: Wed, 19 Nov 2025 15:04:55 +0100 Subject: feat: Add About and Favourites pages, update routing and context management - Added new routes for About and Favourites pages. - Implemented About page with version information and credits. - Created Favourites page with a placeholder message for empty favourites. - Refactored RegionConfig import paths for consistency. - Introduced PageTitleContext to manage page titles dynamically. - Updated various components to utilize the new context for setting page titles. - Enhanced AppShell layout with a responsive Drawer for navigation. - Added CSS styles for new components and pages. - Integrated commit hash display in the About page for version tracking. --- src/frontend/app/components/GroupedTable.tsx | 2 +- src/frontend/app/components/LineIcon.tsx | 2 +- src/frontend/app/components/RegionSelector.tsx | 2 +- src/frontend/app/components/RegularTable.tsx | 2 +- src/frontend/app/components/StopMapSheet.tsx | 8 +- src/frontend/app/components/StopSheet.tsx | 2 +- .../Stops/ConsolidatedCirculationCard.tsx | 2 +- .../Stops/ConsolidatedCirculationList.css | 2 +- .../Stops/ConsolidatedCirculationList.tsx | 2 +- src/frontend/app/components/layout/AppShell.css | 63 +++++++++++++++ src/frontend/app/components/layout/AppShell.tsx | 42 ++++++++++ src/frontend/app/components/layout/Drawer.css | 93 ++++++++++++++++++++++ src/frontend/app/components/layout/Drawer.tsx | 52 ++++++++++++ src/frontend/app/components/layout/Header.css | 41 ++++++++++ src/frontend/app/components/layout/Header.tsx | 32 ++++++++ src/frontend/app/components/ui/Button.css | 39 +++++++++ src/frontend/app/components/ui/Button.tsx | 25 ++++++ src/frontend/app/components/ui/PageContainer.css | 20 +++++ src/frontend/app/components/ui/PageContainer.tsx | 14 ++++ 19 files changed, 433 insertions(+), 12 deletions(-) create mode 100644 src/frontend/app/components/layout/AppShell.css create mode 100644 src/frontend/app/components/layout/AppShell.tsx create mode 100644 src/frontend/app/components/layout/Drawer.css create mode 100644 src/frontend/app/components/layout/Drawer.tsx create mode 100644 src/frontend/app/components/layout/Header.css create mode 100644 src/frontend/app/components/layout/Header.tsx create mode 100644 src/frontend/app/components/ui/Button.css create mode 100644 src/frontend/app/components/ui/Button.tsx create mode 100644 src/frontend/app/components/ui/PageContainer.css create mode 100644 src/frontend/app/components/ui/PageContainer.tsx (limited to 'src/frontend/app/components') diff --git a/src/frontend/app/components/GroupedTable.tsx b/src/frontend/app/components/GroupedTable.tsx index 3a799a7..175899a 100644 --- a/src/frontend/app/components/GroupedTable.tsx +++ b/src/frontend/app/components/GroupedTable.tsx @@ -1,6 +1,6 @@ +import { type RegionConfig } from "../config/RegionConfig"; import { type Estimate } from "../routes/estimates-$id"; import LineIcon from "./LineIcon"; -import { type RegionConfig } from "../data/RegionConfig"; interface GroupedTable { data: Estimate[]; diff --git a/src/frontend/app/components/LineIcon.tsx b/src/frontend/app/components/LineIcon.tsx index 8e9a4bd..3ad9293 100644 --- a/src/frontend/app/components/LineIcon.tsx +++ b/src/frontend/app/components/LineIcon.tsx @@ -1,6 +1,6 @@ import React, { useMemo } from "react"; +import { type RegionId } from "../config/RegionConfig"; import "./LineIcon.css"; -import { type RegionId } from "../data/RegionConfig"; interface LineIconProps { line: string; diff --git a/src/frontend/app/components/RegionSelector.tsx b/src/frontend/app/components/RegionSelector.tsx index 6c9fe8b..124b574 100644 --- a/src/frontend/app/components/RegionSelector.tsx +++ b/src/frontend/app/components/RegionSelector.tsx @@ -1,5 +1,5 @@ import { useApp } from "../AppContext"; -import { getAvailableRegions } from "../data/RegionConfig"; +import { getAvailableRegions } from "../config/RegionConfig"; import "./RegionSelector.css"; export function RegionSelector() { diff --git a/src/frontend/app/components/RegularTable.tsx b/src/frontend/app/components/RegularTable.tsx index 868332f..a738d03 100644 --- a/src/frontend/app/components/RegularTable.tsx +++ b/src/frontend/app/components/RegularTable.tsx @@ -1,7 +1,7 @@ import { useTranslation } from "react-i18next"; +import { type RegionConfig } from "../config/RegionConfig"; import { type Estimate } from "../routes/estimates-$id"; import LineIcon from "./LineIcon"; -import { type RegionConfig } from "../data/RegionConfig"; interface RegularTableProps { data: Estimate[]; diff --git a/src/frontend/app/components/StopMapSheet.tsx b/src/frontend/app/components/StopMapSheet.tsx index 8bdcfa9..e1f0bf7 100644 --- a/src/frontend/app/components/StopMapSheet.tsx +++ b/src/frontend/app/components/StopMapSheet.tsx @@ -1,13 +1,13 @@ import maplibregl from "maplibre-gl"; import React, { useEffect, useMemo, useRef, useState } from "react"; import Map, { - AttributionControl, - Marker, - type MapRef, + AttributionControl, + Marker, + type MapRef, } from "react-map-gl/maplibre"; import { useApp } from "~/AppContext"; +import type { RegionId } from "~/config/RegionConfig"; import { getLineColor } from "~/data/LineColors"; -import type { RegionId } from "~/data/RegionConfig"; import type { Stop } from "~/data/StopDataProvider"; import { loadStyle } from "~/maps/styleloader"; import "./StopMapSheet.css"; diff --git a/src/frontend/app/components/StopSheet.tsx b/src/frontend/app/components/StopSheet.tsx index 2a28d36..8749fe8 100644 --- a/src/frontend/app/components/StopSheet.tsx +++ b/src/frontend/app/components/StopSheet.tsx @@ -5,7 +5,7 @@ import { Sheet } from "react-modal-sheet"; import { Link } from "react-router"; import type { Stop } from "~/data/StopDataProvider"; import { useApp } from "../AppContext"; -import { type RegionId, getRegionConfig } from "../data/RegionConfig"; +import { type RegionId, getRegionConfig } from "../config/RegionConfig"; import { type ConsolidatedCirculation } from "../routes/stops-$id"; import { ErrorDisplay } from "./ErrorDisplay"; import LineIcon from "./LineIcon"; diff --git a/src/frontend/app/components/Stops/ConsolidatedCirculationCard.tsx b/src/frontend/app/components/Stops/ConsolidatedCirculationCard.tsx index 8c3e922..9733d89 100644 --- a/src/frontend/app/components/Stops/ConsolidatedCirculationCard.tsx +++ b/src/frontend/app/components/Stops/ConsolidatedCirculationCard.tsx @@ -1,7 +1,7 @@ import { Clock } from "lucide-react"; import { useTranslation } from "react-i18next"; +import { type RegionConfig } from "~/config/RegionConfig"; import LineIcon from "~components/LineIcon"; -import { type RegionConfig } from "~data/RegionConfig"; import { type ConsolidatedCirculation } from "~routes/stops-$id"; import "./ConsolidatedCirculationList.css"; diff --git a/src/frontend/app/components/Stops/ConsolidatedCirculationList.css b/src/frontend/app/components/Stops/ConsolidatedCirculationList.css index 939f40d..b79fc73 100644 --- a/src/frontend/app/components/Stops/ConsolidatedCirculationList.css +++ b/src/frontend/app/components/Stops/ConsolidatedCirculationList.css @@ -128,7 +128,7 @@ @media (max-width: 480px) { .consolidated-circulation-card .card-header { gap: 0.5rem; - padding: 0.75rem 0.875rem; + padding: 0.5rem 0.65rem; } .consolidated-circulation-card .arrival-time { diff --git a/src/frontend/app/components/Stops/ConsolidatedCirculationList.tsx b/src/frontend/app/components/Stops/ConsolidatedCirculationList.tsx index d95ee03..047dfd4 100644 --- a/src/frontend/app/components/Stops/ConsolidatedCirculationList.tsx +++ b/src/frontend/app/components/Stops/ConsolidatedCirculationList.tsx @@ -1,5 +1,5 @@ import { useTranslation } from "react-i18next"; -import { type RegionConfig } from "~data/RegionConfig"; +import { type RegionConfig } from "~/config/RegionConfig"; import { type ConsolidatedCirculation } from "~routes/stops-$id"; import { ConsolidatedCirculationCard } from "./ConsolidatedCirculationCard"; diff --git a/src/frontend/app/components/layout/AppShell.css b/src/frontend/app/components/layout/AppShell.css new file mode 100644 index 0000000..89a4d1f --- /dev/null +++ b/src/frontend/app/components/layout/AppShell.css @@ -0,0 +1,63 @@ +.app-shell { + display: flex; + flex-direction: column; + height: 100dvh; + width: 100%; + overflow: hidden; + background-color: var(--background-color); +} + +.app-shell__header { + flex-shrink: 0; + z-index: 10; +} + +.app-shell__body { + display: flex; + flex: 1; + overflow: hidden; + position: relative; +} + +.app-shell__sidebar { + display: none; /* Hidden on mobile */ + width: 80px; + border-right: 1px solid var(--border-color); + background: var(--background-color); + flex-shrink: 0; + z-index: 5; +} + +.app-shell__main { + flex: 1; + overflow: auto; + overflow-x: hidden; + position: relative; +} + +.app-shell__bottom-nav { + flex-shrink: 0; + display: block; /* Visible on mobile */ + z-index: 10; +} + +/* Desktop styles */ +@media (min-width: 768px) { + .app-shell__sidebar { + display: block; + } + + .app-shell__bottom-nav { + display: none; + } + + /* Override NavBar styles for sidebar */ + .app-shell__sidebar .navigation-bar { + flex-direction: column; + height: 100%; + justify-content: flex-start; + padding-top: 1rem; + gap: 1rem; + border-top: none; + } +} diff --git a/src/frontend/app/components/layout/AppShell.tsx b/src/frontend/app/components/layout/AppShell.tsx new file mode 100644 index 0000000..d0c0121 --- /dev/null +++ b/src/frontend/app/components/layout/AppShell.tsx @@ -0,0 +1,42 @@ +import React, { useState } from "react"; +import { Outlet } from "react-router"; +import { PageTitleProvider, usePageTitleContext } from "~/contexts/PageTitleContext"; +import NavBar from "../NavBar"; +import "./AppShell.css"; +import { Drawer } from "./Drawer"; +import { Header } from "./Header"; + +const AppShellContent: React.FC = () => { + const { title } = usePageTitleContext(); + const [isDrawerOpen, setIsDrawerOpen] = useState(false); + + return ( +
+
setIsDrawerOpen(true)} + /> + setIsDrawerOpen(false)} /> +
+ +
+ +
+
+
+ +
+
+ ); +}; + +export const AppShell: React.FC = () => { + return ( + + + + ); +}; diff --git a/src/frontend/app/components/layout/Drawer.css b/src/frontend/app/components/layout/Drawer.css new file mode 100644 index 0000000..27ccce6 --- /dev/null +++ b/src/frontend/app/components/layout/Drawer.css @@ -0,0 +1,93 @@ +.drawer-overlay { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.5); + z-index: 99; + opacity: 0; + visibility: hidden; + transition: opacity 0.3s ease, visibility 0.3s ease; +} + +.drawer-overlay.open { + opacity: 1; + visibility: visible; +} + +.drawer { + position: fixed; + top: 0; + right: 0; + width: 280px; + height: 100%; + background-color: var(--background-color); + z-index: 100; + transform: translateX(100%); + transition: transform 0.3s ease; + box-shadow: -2px 0 8px rgba(0, 0, 0, 0.1); + display: flex; + flex-direction: column; +} + +.drawer.open { + transform: translateX(0); +} + +.drawer__header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 0.5rem 1rem; + border-bottom: 1px solid var(--border-color); + height: 60px; + box-sizing: border-box; +} + +.drawer__title { + margin: 0; + font-size: 1.25rem; + font-weight: 600; +} + +.drawer__close-btn { + background: none; + border: none; + cursor: pointer; + color: var(--text-color); + padding: 0.5rem; + display: flex; + align-items: center; + justify-content: center; + border-radius: 50%; +} + +.drawer__close-btn:hover { + background-color: rgba(0, 0, 0, 0.05); +} + +.drawer__nav { + padding: 1rem 0; + display: flex; + flex-direction: column; +} + +.drawer__link { + display: flex; + align-items: center; + gap: 1rem; + padding: 0.75rem 1.5rem; + text-decoration: none; + color: var(--text-color); + font-size: 1rem; + transition: background-color 0.2s; +} + +.drawer__link:hover { + background-color: rgba(0, 0, 0, 0.05); +} + +.drawer__link svg { + color: var(--subtitle-color); +} diff --git a/src/frontend/app/components/layout/Drawer.tsx b/src/frontend/app/components/layout/Drawer.tsx new file mode 100644 index 0000000..55aa3a0 --- /dev/null +++ b/src/frontend/app/components/layout/Drawer.tsx @@ -0,0 +1,52 @@ +import { Info, Settings, Star, X } from "lucide-react"; +import React, { useEffect } from "react"; +import { useTranslation } from "react-i18next"; +import { Link, useLocation } from "react-router"; +import "./Drawer.css"; + +interface DrawerProps { + isOpen: boolean; + onClose: () => void; +} + +export const Drawer: React.FC = ({ isOpen, onClose }) => { + const { t } = useTranslation(); + const location = useLocation(); + + // Close drawer when route changes + useEffect(() => { + onClose(); + }, [location.pathname]); + + return ( + <> +