aboutsummaryrefslogtreecommitdiff
path: root/src/frontend/app/components
diff options
context:
space:
mode:
Diffstat (limited to 'src/frontend/app/components')
-rw-r--r--src/frontend/app/components/GroupedTable.tsx2
-rw-r--r--src/frontend/app/components/LineIcon.tsx2
-rw-r--r--src/frontend/app/components/RegionSelector.tsx2
-rw-r--r--src/frontend/app/components/RegularTable.tsx2
-rw-r--r--src/frontend/app/components/StopMapSheet.tsx8
-rw-r--r--src/frontend/app/components/StopSheet.tsx2
-rw-r--r--src/frontend/app/components/Stops/ConsolidatedCirculationCard.tsx2
-rw-r--r--src/frontend/app/components/Stops/ConsolidatedCirculationList.css2
-rw-r--r--src/frontend/app/components/Stops/ConsolidatedCirculationList.tsx2
-rw-r--r--src/frontend/app/components/layout/AppShell.css63
-rw-r--r--src/frontend/app/components/layout/AppShell.tsx42
-rw-r--r--src/frontend/app/components/layout/Drawer.css93
-rw-r--r--src/frontend/app/components/layout/Drawer.tsx52
-rw-r--r--src/frontend/app/components/layout/Header.css41
-rw-r--r--src/frontend/app/components/layout/Header.tsx32
-rw-r--r--src/frontend/app/components/ui/Button.css39
-rw-r--r--src/frontend/app/components/ui/Button.tsx25
-rw-r--r--src/frontend/app/components/ui/PageContainer.css20
-rw-r--r--src/frontend/app/components/ui/PageContainer.tsx14
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>;
+};