aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorAriel Costas Guerrero <ariel@costas.dev>2025-11-19 22:34:07 +0100
committerAriel Costas Guerrero <ariel@costas.dev>2025-11-19 22:34:20 +0100
commit747c579b15c54dc5dbc50482d3361761853e007a (patch)
tree13587e5825bd5353fe75f4129c0746f28bba4cea /src
parentd51169f6411b68a226d76d2d39826904de484929 (diff)
feat: Refactor layout and styles for StopList and related components; add ThemeColorManager for dynamic theme support
Diffstat (limited to 'src')
-rw-r--r--src/frontend/app/components/ServiceAlerts.css5
-rw-r--r--src/frontend/app/components/ServiceAlerts.tsx2
-rw-r--r--src/frontend/app/components/StopGallery.css51
-rw-r--r--src/frontend/app/components/StopGallery.tsx42
-rw-r--r--src/frontend/app/components/StopItem.tsx15
-rw-r--r--src/frontend/app/components/ThemeColorManager.tsx20
-rw-r--r--src/frontend/app/components/layout/AppShell.tsx2
-rw-r--r--src/frontend/app/components/layout/Header.css1
-rw-r--r--src/frontend/app/contexts/SettingsContext.tsx2
-rw-r--r--src/frontend/app/root.css23
-rw-r--r--src/frontend/app/root.tsx14
-rw-r--r--src/frontend/app/routes/home.css86
-rw-r--r--src/frontend/app/routes/home.tsx35
13 files changed, 183 insertions, 115 deletions
diff --git a/src/frontend/app/components/ServiceAlerts.css b/src/frontend/app/components/ServiceAlerts.css
index 7c271f9..c0f67f5 100644
--- a/src/frontend/app/components/ServiceAlerts.css
+++ b/src/frontend/app/components/ServiceAlerts.css
@@ -1,6 +1,9 @@
/* Service Alerts Container */
.service-alerts-container {
- margin-bottom: 1.5rem;
+ margin: 0;
+ display: flex;
+ flex-direction: column;
+ gap: 0.75rem;
}
.service-alert {
diff --git a/src/frontend/app/components/ServiceAlerts.tsx b/src/frontend/app/components/ServiceAlerts.tsx
index eba8a92..a6a1ee8 100644
--- a/src/frontend/app/components/ServiceAlerts.tsx
+++ b/src/frontend/app/components/ServiceAlerts.tsx
@@ -6,7 +6,7 @@ const ServiceAlerts: React.FC = () => {
const { t } = useTranslation();
return (
- <div className="service-alerts-container">
+ <div className="service-alerts-container stoplist-section">
<h2 className="page-subtitle">{t("stoplist.service_alerts")}</h2>
<div className="service-alert info">
<div className="alert-icon">ℹ️</div>
diff --git a/src/frontend/app/components/StopGallery.css b/src/frontend/app/components/StopGallery.css
index bc9b955..070a01f 100644
--- a/src/frontend/app/components/StopGallery.css
+++ b/src/frontend/app/components/StopGallery.css
@@ -1,13 +1,16 @@
/* Gallery Container */
.gallery-container {
- margin-bottom: 1.5rem;
+ margin: 0;
+ display: flex;
+ flex-direction: column;
+ gap: 0.75rem;
}
.gallery-header {
display: flex;
align-items: center;
justify-content: space-between;
- margin-bottom: 0.75rem;
+ margin-bottom: 0.5rem;
}
.gallery-counter {
@@ -24,6 +27,15 @@
font-weight: 600;
}
+/* Empty State */
+.gallery-empty-state {
+ text-align: center;
+}
+
+.gallery-empty-state .message {
+ font-size: 0.85rem;
+}
+
/* Scroll Container */
.gallery-scroll-container {
overflow-x: auto;
@@ -32,7 +44,6 @@
scroll-snap-type: x mandatory;
scrollbar-width: none; /* Firefox */
-ms-overflow-style: none; /* IE and Edge */
- padding: 0 1rem;
}
.gallery-scroll-container::-webkit-scrollbar {
@@ -48,14 +59,15 @@
/* Gallery Item */
.gallery-item {
- flex: 0 0 100%;
+ flex: 0 0 90%;
+ max-width: 320px;
scroll-snap-align: start;
scroll-snap-stop: always;
}
.gallery-item-link {
display: block;
- padding: 1rem;
+ padding: 0.75rem;
background-color: var(
--card-background-color,
var(--message-background-color)
@@ -64,7 +76,7 @@
border-radius: 12px;
text-decoration: none;
color: var(--text-color);
- min-height: 120px;
+ min-height: 100px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
@@ -72,24 +84,24 @@
display: flex;
align-items: center;
gap: 0.5rem;
- margin-bottom: 0.5rem;
+ margin-bottom: 0.25rem;
}
.gallery-item-header .favourite-icon {
color: var(--star-color);
- font-size: 1rem;
+ font-size: 0.9rem;
}
.gallery-item-code {
- font-size: 0.85rem;
+ font-size: 0.8rem;
color: var(--subtitle-color);
font-weight: 500;
}
.gallery-item-name {
- font-size: 1rem;
+ font-size: 0.95rem;
font-weight: 600;
- margin-bottom: 0.75rem;
+ margin-bottom: 0.5rem;
line-height: 1.3;
display: -webkit-box;
/* Standard property for compatibility */
@@ -97,7 +109,7 @@
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
- min-height: 2.6em;
+ min-height: 2.5em;
}
.gallery-item-lines {
@@ -117,23 +129,22 @@
}
/* Gallery Indicators */
-.gallery-indicators {
+.gallery-dots {
display: flex;
justify-content: center;
- gap: 0.5rem;
- margin-top: 1rem;
- padding: 0.5rem 0;
+ gap: 0.35rem;
+ margin-top: 0.25rem;
}
-.gallery-indicator {
- width: 8px;
- height: 8px;
+.dot {
+ width: 6px;
+ height: 6px;
border-radius: 50%;
background-color: var(--border-color);
transition: background-color 0.2s ease-in-out;
}
-.gallery-indicator.active {
+.dot.active {
background-color: var(--button-background-color);
}
diff --git a/src/frontend/app/components/StopGallery.tsx b/src/frontend/app/components/StopGallery.tsx
index 18d0725..500ea20 100644
--- a/src/frontend/app/components/StopGallery.tsx
+++ b/src/frontend/app/components/StopGallery.tsx
@@ -1,7 +1,7 @@
-import React, { useRef, useState, useEffect } from "react";
+import React, { useEffect, useRef, useState } from "react";
import { type Stop } from "../data/StopDataProvider";
-import StopGalleryItem from "./StopGalleryItem";
import "./StopGallery.css";
+import StopGalleryItem from "./StopGalleryItem";
interface StopGalleryProps {
stops: Stop[];
@@ -19,7 +19,9 @@ const StopGallery: React.FC<StopGalleryProps> = ({
useEffect(() => {
const element = scrollRef.current;
- if (!element) return;
+ if (!element || stops.length === 0) {
+ return;
+ }
const handleScroll = () => {
const scrollLeft = element.scrollLeft;
@@ -34,9 +36,11 @@ const StopGallery: React.FC<StopGalleryProps> = ({
if (stops.length === 0 && emptyMessage) {
return (
- <div className="gallery-container">
- <h2 className="page-subtitle">{title}</h2>
- <p className="message">{emptyMessage}</p>
+ <div className="gallery-container stoplist-section">
+ <h3 className="page-subtitle">{title}</h3>
+ <div className="gallery-empty-state">
+ <p className="message">{emptyMessage}</p>
+ </div>
</div>
);
}
@@ -46,29 +50,27 @@ const StopGallery: React.FC<StopGalleryProps> = ({
}
return (
- <div className="gallery-container">
+ <div className="gallery-container stoplist-section">
<div className="gallery-header">
- <h2 className="page-subtitle">{title}</h2>
+ <h3 className="page-subtitle">{title}</h3>
+ <span className="gallery-counter">{stops.length}</span>
</div>
- <div className="gallery-scroll-container" ref={scrollRef}>
+ <div ref={scrollRef} className="gallery-scroll-container">
<div className="gallery-track">
{stops.map((stop) => (
<StopGalleryItem key={stop.stopId} stop={stop} />
))}
</div>
</div>
-
- {stops.length > 1 && (
- <div className="gallery-indicators">
- {stops.map((_, index) => (
- <div
- key={index}
- className={`gallery-indicator ${index === activeIndex ? "active" : ""}`}
- />
- ))}
- </div>
- )}
+ <div className="gallery-dots">
+ {stops.map((_, index) => (
+ <span
+ key={index}
+ className={`dot ${index === activeIndex ? "active" : ""}`}
+ ></span>
+ ))}
+ </div>
</div>
);
};
diff --git a/src/frontend/app/components/StopItem.tsx b/src/frontend/app/components/StopItem.tsx
index ae51df8..de51576 100644
--- a/src/frontend/app/components/StopItem.tsx
+++ b/src/frontend/app/components/StopItem.tsx
@@ -1,8 +1,8 @@
import React from "react";
import { Link } from "react-router";
+import { useApp } from "../AppContext";
import StopDataProvider, { type Stop } from "../data/StopDataProvider";
import LineIcon from "./LineIcon";
-import { useApp } from "../AppContext";
interface StopItemProps {
stop: Stop;
@@ -14,9 +14,16 @@ const StopItem: React.FC<StopItemProps> = ({ stop }) => {
return (
<li className="list-item">
<Link className="list-item-link" to={`/estimates/${stop.stopId}`}>
- {stop.favourite && <span className="favourite-icon">★</span>} (
- {stop.stopId}) {StopDataProvider.getDisplayName(region, stop)}
- <div className="line-icons">
+ <div style={{ display: "flex", justifyContent: "space-between", alignItems: "baseline" }}>
+ <span style={{ fontWeight: 600 }}>
+ {stop.favourite && <span className="favourite-icon">★</span>}
+ {StopDataProvider.getDisplayName(region, stop)}
+ </span>
+ <span style={{ fontSize: "0.85em", color: "var(--subtitle-color)", marginLeft: "0.5rem" }}>
+ ({stop.stopId})
+ </span>
+ </div>
+ <div className="line-icons" style={{ marginTop: "0.25rem" }}>
{stop.lines?.map((line) => (
<LineIcon key={line} line={line} region={region} />
))}
diff --git a/src/frontend/app/components/ThemeColorManager.tsx b/src/frontend/app/components/ThemeColorManager.tsx
new file mode 100644
index 0000000..c138dc9
--- /dev/null
+++ b/src/frontend/app/components/ThemeColorManager.tsx
@@ -0,0 +1,20 @@
+import { useEffect } from "react";
+import { useSettings } from "../contexts/SettingsContext";
+
+export const ThemeColorManager = () => {
+ const { resolvedTheme } = useSettings();
+
+ useEffect(() => {
+ const color = resolvedTheme === "dark" ? "#121212" : "#ffffff";
+
+ let meta = document.querySelector('meta[name="theme-color"]');
+ if (!meta) {
+ meta = document.createElement('meta');
+ meta.setAttribute('name', 'theme-color');
+ document.head.appendChild(meta);
+ }
+ meta.setAttribute('content', color);
+ }, [resolvedTheme]);
+
+ return null;
+};
diff --git a/src/frontend/app/components/layout/AppShell.tsx b/src/frontend/app/components/layout/AppShell.tsx
index d0c0121..e0559ac 100644
--- a/src/frontend/app/components/layout/AppShell.tsx
+++ b/src/frontend/app/components/layout/AppShell.tsx
@@ -2,6 +2,7 @@ import React, { useState } from "react";
import { Outlet } from "react-router";
import { PageTitleProvider, usePageTitleContext } from "~/contexts/PageTitleContext";
import NavBar from "../NavBar";
+import { ThemeColorManager } from "../ThemeColorManager";
import "./AppShell.css";
import { Drawer } from "./Drawer";
import { Header } from "./Header";
@@ -12,6 +13,7 @@ const AppShellContent: React.FC = () => {
return (
<div className="app-shell">
+ <ThemeColorManager />
<Header
className="app-shell__header"
title={title}
diff --git a/src/frontend/app/components/layout/Header.css b/src/frontend/app/components/layout/Header.css
index c95226f..4ff492e 100644
--- a/src/frontend/app/components/layout/Header.css
+++ b/src/frontend/app/components/layout/Header.css
@@ -4,7 +4,6 @@
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%;
diff --git a/src/frontend/app/contexts/SettingsContext.tsx b/src/frontend/app/contexts/SettingsContext.tsx
index ed20fcb..a273008 100644
--- a/src/frontend/app/contexts/SettingsContext.tsx
+++ b/src/frontend/app/contexts/SettingsContext.tsx
@@ -30,6 +30,7 @@ interface SettingsContextProps {
region: RegionId;
setRegion: (region: RegionId) => void;
+ resolvedTheme: "light" | "dark";
}
const SettingsContext = createContext<SettingsContextProps | undefined>(
@@ -182,6 +183,7 @@ export const SettingsProvider = ({ children }: { children: ReactNode }) => {
setMapPositionMode,
region,
setRegion,
+ resolvedTheme,
}}
>
{children}
diff --git a/src/frontend/app/root.css b/src/frontend/app/root.css
index 6c3dd99..f87fdc3 100644
--- a/src/frontend/app/root.css
+++ b/src/frontend/app/root.css
@@ -182,6 +182,29 @@ body {
color: var(--text-color);
}
+.page-title {
+ font-size: 1.25rem;
+ font-weight: 600;
+ color: var(--text-color);
+ margin: 0;
+}
+
+.page-subtitle {
+ font-size: 1rem;
+ font-weight: 500;
+ color: var(--subtitle-color);
+ margin: 0 0 0.5rem 0;
+}
+
+.message {
+ background-color: var(--message-background-color);
+ padding: 1rem;
+ border-radius: 0.5rem;
+ margin: 0;
+ color: var(--text-color);
+ font-size: 0.9rem;
+}
+
@media (min-width: 768px) {
.page-container {
width: 90%;
diff --git a/src/frontend/app/root.tsx b/src/frontend/app/root.tsx
index 8f0c916..7bf07a0 100644
--- a/src/frontend/app/root.tsx
+++ b/src/frontend/app/root.tsx
@@ -1,9 +1,9 @@
import {
- isRouteErrorResponse,
- Links,
- Meta,
- Scripts,
- ScrollRestoration
+ isRouteErrorResponse,
+ Links,
+ Meta,
+ Scripts,
+ ScrollRestoration
} from "react-router";
import "@fontsource-variable/roboto";
@@ -37,12 +37,10 @@ export function Layout({ children }: { children: React.ReactNode }) {
<meta name="mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
-
<link rel="icon" type="image/jpg" href="/logo-512.jpg" />
<link rel="icon" href="/favicon.ico" sizes="64x64" />
<link rel="apple-touch-icon" href="/logo-512.jpg" sizes="512x512" />
- <meta name="theme-color" content="#007bff" />
-
+ <meta name="theme-color" content="#ffffff" />
<link rel="canonical" href="https://busurbano.costas.dev/" />
<meta
diff --git a/src/frontend/app/routes/home.css b/src/frontend/app/routes/home.css
index 3d5ba3a..b935518 100644
--- a/src/frontend/app/routes/home.css
+++ b/src/frontend/app/routes/home.css
@@ -1,33 +1,59 @@
/* Common page styles */
-.page-title {
- font-size: 1.4rem;
- margin-bottom: 1rem;
- font-weight: 600;
+.stoplist-page {
+ display: flex;
+ flex-direction: column;
+ gap: 1.5rem;
+ padding: 1rem 0 2rem;
+}
+
+.stoplist-section {
+ width: 100%;
+ padding: 0 1rem;
+ box-sizing: border-box;
+}
+
+.search-container {
+ display: flex;
+ flex-direction: column;
+ gap: 0.5rem;
+}
+
+.search-bar {
+ width: 100%;
+ padding: 0.75rem 1rem;
+ font-size: 1rem;
+ border: 1px solid var(--border-color);
+ border-radius: 0.75rem;
+ background-color: var(--card-background, var(--background-color));
color: var(--text-color);
+ transition: border-color 0.2s ease, box-shadow 0.2s ease;
}
-.page-subtitle {
- font-size: 1.4rem;
- margin-top: 1.5rem;
- margin-bottom: 0.75rem;
- font-weight: 500;
+.search-bar::placeholder {
color: var(--subtitle-color);
+ opacity: 0.8;
+}
+
+.search-bar:focus {
+ outline: none;
+ border-color: var(--button-background-color);
+ box-shadow: 0 0 0 2px rgba(0, 0, 0, 0.05);
}
/* Form styles */
.search-form {
- margin-bottom: 1.5rem;
+ margin: 0;
}
.form-group {
- margin-bottom: 1rem;
+ margin: 0;
display: flex;
flex-direction: column;
}
.form-label {
- font-size: 0.9rem;
- margin-bottom: 0.25rem;
+ font-size: 0.85rem;
+ margin-bottom: 0.5rem;
font-weight: 500;
}
@@ -38,41 +64,25 @@
border-radius: 8px;
}
-.form-button {
- display: inline-flex;
- align-items: center;
- justify-content: center;
- gap: 1rem;
-
- padding: 0.75rem 1rem;
- background-color: var(--button-background-color);
- color: white;
- border: none;
- border-radius: 8px;
- font-size: 1rem;
- font-weight: 500;
- cursor: pointer;
- width: 100%;
- margin-top: 0.5rem;
-}
-
-.form-button:hover {
- background-color: var(--button-hover-background-color);
-}
-
/* List styles */
.list-container {
- margin-bottom: 1.5rem;
+ margin: 0;
+ display: flex;
+ flex-direction: column;
+ gap: 0.75rem;
}
.list {
list-style: none;
padding: 0;
margin: 0;
+ display: flex;
+ flex-direction: column;
+ gap: 0.75rem;
}
.list-item {
- padding: 1rem;
+ padding: 0.75rem;
border-bottom: 1px solid var(--border-color);
}
@@ -80,7 +90,7 @@
display: block;
color: var(--text-color);
text-decoration: none;
- font-size: 1.1rem; /* Increased font size for stop name */
+ font-size: 1rem; /* Reduced font size */
}
.list-item-link:hover {
diff --git a/src/frontend/app/routes/home.tsx b/src/frontend/app/routes/home.tsx
index 8a1e3b3..ca6a20b 100644
--- a/src/frontend/app/routes/home.tsx
+++ b/src/frontend/app/routes/home.tsx
@@ -2,10 +2,8 @@
import Fuse from "fuse.js";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
-import { REGIONS } from "~/config/RegionConfig";
import { usePageTitle } from "~/contexts/PageTitleContext";
import { useApp } from "../AppContext";
-import ServiceAlerts from "../components/ServiceAlerts";
import StopGallery from "../components/StopGallery";
import StopItem from "../components/StopItem";
import StopItemSkeleton from "../components/StopItemSkeleton";
@@ -249,26 +247,19 @@ export default function StopList() {
};
return (
- <div className="page-container stoplist-page">
- <h1 className="page-title">BusUrbano - {REGIONS[region].name}</h1>
-
- <form className="search-form">
- <div className="form-group">
- <label className="form-label" htmlFor="stopName">
- {t("stoplist.search_label", "Buscar paradas")}
- </label>
- <input
- className="form-input"
- type="text"
- placeholder={randomPlaceholder}
- id="stopName"
- onChange={handleStopSearch}
- />
- </div>
- </form>
+ <div className="stoplist-page">
+ <div className="stoplist-section search-container">
+ <h3 className="page-subtitle">{t("stoplist.search_label", "Buscar paradas")}</h3>
+ <input
+ type="search"
+ placeholder={randomPlaceholder}
+ onChange={handleStopSearch}
+ className="search-bar"
+ />
+ </div>
{searchResults && searchResults.length > 0 && (
- <div className="list-container">
+ <div className="stoplist-section list-container">
<h2 className="page-subtitle">
{t("stoplist.search_results", "Resultados de la búsqueda")}
</h2>
@@ -295,9 +286,9 @@ export default function StopList() {
/>
)}
- <ServiceAlerts />
+ {/*<ServiceAlerts />*/}
- <div className="list-container">
+ <div className="stoplist-section list-container">
<h2 className="page-subtitle">
{userLocation
? t("stoplist.nearby_stops", "Nearby stops")