diff options
| author | Ariel Costas Guerrero <ariel@costas.dev> | 2025-11-19 15:04:55 +0100 |
|---|---|---|
| committer | Ariel Costas Guerrero <ariel@costas.dev> | 2025-11-19 15:05:34 +0100 |
| commit | d51169f6411b68a226d76d2d39826904de484929 (patch) | |
| tree | 4d8a403dfcc5b17671a92b8cc1e5d71d20ed9537 /src/frontend/app/components/layout | |
| parent | d434204860fc0409ad6343e815d0057b97ce3573 (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.css | 63 | ||||
| -rw-r--r-- | src/frontend/app/components/layout/AppShell.tsx | 42 | ||||
| -rw-r--r-- | src/frontend/app/components/layout/Drawer.css | 93 | ||||
| -rw-r--r-- | src/frontend/app/components/layout/Drawer.tsx | 52 | ||||
| -rw-r--r-- | src/frontend/app/components/layout/Header.css | 41 | ||||
| -rw-r--r-- | src/frontend/app/components/layout/Header.tsx | 32 |
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> + ); +}; |
