diff options
Diffstat (limited to 'src/frontend/app/components')
19 files changed, 433 insertions, 12 deletions
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 ( + <div className="app-shell"> + <Header + className="app-shell__header" + title={title} + onMenuClick={() => setIsDrawerOpen(true)} + /> + <Drawer isOpen={isDrawerOpen} onClose={() => setIsDrawerOpen(false)} /> + <div className="app-shell__body"> + <aside className="app-shell__sidebar"> + <NavBar /> + </aside> + <main className="app-shell__main"> + <Outlet /> + </main> + </div> + <footer className="app-shell__bottom-nav"> + <NavBar /> + </footer> + </div> + ); +}; + +export const AppShell: React.FC = () => { + return ( + <PageTitleProvider> + <AppShellContent /> + </PageTitleProvider> + ); +}; 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<DrawerProps> = ({ isOpen, onClose }) => { + const { t } = useTranslation(); + const location = useLocation(); + + // Close drawer when route changes + useEffect(() => { + onClose(); + }, [location.pathname]); + + return ( + <> + <div + className={`drawer-overlay ${isOpen ? "open" : ""}`} + onClick={onClose} + aria-hidden="true" + /> + <div className={`drawer ${isOpen ? "open" : ""}`}> + <div className="drawer__header"> + <h2 className="drawer__title">Menu</h2> + <button className="drawer__close-btn" onClick={onClose}> + <X size={24} /> + </button> + </div> + <nav className="drawer__nav"> + <Link to="/favourites" className="drawer__link"> + <Star size={20} /> + <span>{t("navbar.favourites", "Favoritos")}</span> + </Link> + <Link to="/settings" className="drawer__link"> + <Settings size={20} /> + <span>{t("navbar.settings", "Ajustes")}</span> + </Link> + <Link to="/about" className="drawer__link"> + <Info size={20} /> + <span>{t("about.title", "Acerca de")}</span> + </Link> + </nav> + </div> + </> + ); +}; diff --git a/src/frontend/app/components/layout/Header.css b/src/frontend/app/components/layout/Header.css new file mode 100644 index 0000000..c95226f --- /dev/null +++ b/src/frontend/app/components/layout/Header.css @@ -0,0 +1,41 @@ +.app-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 0.5rem 1rem; + background-color: var(--background-color); + border-bottom: 1px solid var(--border-color); + height: 60px; + box-sizing: border-box; + width: 100%; +} + +.app-header__left { + display: flex; + align-items: center; + gap: 1rem; +} + +.app-header__menu-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%; + transition: background-color 0.2s; +} + +.app-header__menu-btn:hover { + background-color: rgba(0, 0, 0, 0.05); +} + +.app-header__title { + font-size: 1.25rem; + font-weight: 600; + margin: 0; + color: var(--text-color); +} diff --git a/src/frontend/app/components/layout/Header.tsx b/src/frontend/app/components/layout/Header.tsx new file mode 100644 index 0000000..2bdd764 --- /dev/null +++ b/src/frontend/app/components/layout/Header.tsx @@ -0,0 +1,32 @@ +import { Menu } from "lucide-react"; +import React from "react"; +import "./Header.css"; + +interface HeaderProps { + title?: string; + onMenuClick?: () => void; + className?: string; +} + +export const Header: React.FC<HeaderProps> = ({ + title = "Busurbano", + onMenuClick, + className = "", +}) => { + return ( + <header className={`app-header ${className}`}> + <div className="app-header__left"> + <h1 className="app-header__title">{title}</h1> + </div> + <div className="app-header__right"> + <button + className="app-header__menu-btn" + onClick={onMenuClick} + aria-label="Menu" + > + <Menu size={24} /> + </button> + </div> + </header> + ); +}; diff --git a/src/frontend/app/components/ui/Button.css b/src/frontend/app/components/ui/Button.css new file mode 100644 index 0000000..bf02a7c --- /dev/null +++ b/src/frontend/app/components/ui/Button.css @@ -0,0 +1,39 @@ +.ui-button { + display: inline-flex; + align-items: center; + gap: 0.5rem; + padding: 0.75rem 1.5rem; + border-radius: 0.5rem; + font-size: 0.9rem; + font-weight: 500; + cursor: pointer; + transition: background-color 0.2s ease, transform 0.1s ease; + border: none; +} + +.ui-button:active { + transform: translateY(1px); +} + +.ui-button--primary { + background: var(--button-background-color); + color: white; +} + +.ui-button--primary:hover { + background: var(--button-hover-background-color); +} + +.ui-button--secondary { + background: var(--border-color); + color: var(--text-color); +} + +.ui-button--secondary:hover { + background: #e0e0e0; +} + +.ui-button__icon { + display: flex; + align-items: center; +} diff --git a/src/frontend/app/components/ui/Button.tsx b/src/frontend/app/components/ui/Button.tsx new file mode 100644 index 0000000..18a15b2 --- /dev/null +++ b/src/frontend/app/components/ui/Button.tsx @@ -0,0 +1,25 @@ +import React from "react"; +import "./Button.css"; + +interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> { + variant?: "primary" | "secondary" | "outline"; + icon?: React.ReactNode; +} + +export const Button: React.FC<ButtonProps> = ({ + children, + variant = "primary", + icon, + className = "", + ...props +}) => { + return ( + <button + className={`ui-button ui-button--${variant} ${className}`} + {...props} + > + {icon && <span className="ui-button__icon">{icon}</span>} + {children} + </button> + ); +}; diff --git a/src/frontend/app/components/ui/PageContainer.css b/src/frontend/app/components/ui/PageContainer.css new file mode 100644 index 0000000..8a86035 --- /dev/null +++ b/src/frontend/app/components/ui/PageContainer.css @@ -0,0 +1,20 @@ +.page-container { + max-width: 100%; + padding: 0 16px; + background-color: var(--background-color); + color: var(--text-color); +} + +@media (min-width: 768px) { + .page-container { + width: 90%; + max-width: 768px; + margin: 0 auto; + } +} + +@media (min-width: 1024px) { + .page-container { + max-width: 1024px; + } +} diff --git a/src/frontend/app/components/ui/PageContainer.tsx b/src/frontend/app/components/ui/PageContainer.tsx new file mode 100644 index 0000000..4c9684a --- /dev/null +++ b/src/frontend/app/components/ui/PageContainer.tsx @@ -0,0 +1,14 @@ +import React from "react"; +import "./PageContainer.css"; + +interface PageContainerProps { + children: React.ReactNode; + className?: string; +} + +export const PageContainer: React.FC<PageContainerProps> = ({ + children, + className = "", +}) => { + return <div className={`page-container ${className}`}>{children}</div>; +}; |
