aboutsummaryrefslogtreecommitdiff
path: root/src/frontend/app/components/layout
diff options
context:
space:
mode:
authorAriel Costas Guerrero <ariel@costas.dev>2025-11-19 15:04:55 +0100
committerAriel Costas Guerrero <ariel@costas.dev>2025-11-19 15:05:34 +0100
commitd51169f6411b68a226d76d2d39826904de484929 (patch)
tree4d8a403dfcc5b17671a92b8cc1e5d71d20ed9537 /src/frontend/app/components/layout
parentd434204860fc0409ad6343e815d0057b97ce3573 (diff)
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.
Diffstat (limited to 'src/frontend/app/components/layout')
-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
6 files changed, 323 insertions, 0 deletions
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>
+ );
+};