diff options
| author | Ariel Costas Guerrero <ariel@costas.dev> | 2025-11-30 23:27:33 +0100 |
|---|---|---|
| committer | Ariel Costas Guerrero <ariel@costas.dev> | 2025-11-30 23:29:45 +0100 |
| commit | a477dda9dc4291ab25fffe2525acf44177154c86 (patch) | |
| tree | 8fc73d9b288b0a5d6597a9e79bfb6d1a9eb45d73 /src/frontend/app | |
| parent | 688a4359d345c32c05d5d43c484b487de344ac97 (diff) | |
Remake home and settings pages using Tailwind styles
Diffstat (limited to 'src/frontend/app')
| -rw-r--r-- | src/frontend/app/components/LineIcon.css | 3 | ||||
| -rw-r--r-- | src/frontend/app/components/StopGallery.tsx | 36 | ||||
| -rw-r--r-- | src/frontend/app/components/StopGalleryItem.tsx | 38 | ||||
| -rw-r--r-- | src/frontend/app/components/StopItem.tsx | 17 | ||||
| -rw-r--r-- | src/frontend/app/root.css | 1 | ||||
| -rw-r--r-- | src/frontend/app/routes/home.css | 320 | ||||
| -rw-r--r-- | src/frontend/app/routes/home.tsx | 67 | ||||
| -rw-r--r-- | src/frontend/app/routes/settings.css | 283 | ||||
| -rw-r--r-- | src/frontend/app/routes/settings.tsx | 145 | ||||
| -rw-r--r-- | src/frontend/app/tailwind-full.css | 3 |
10 files changed, 190 insertions, 723 deletions
diff --git a/src/frontend/app/components/LineIcon.css b/src/frontend/app/components/LineIcon.css index c6dffd8..6363c85 100644 --- a/src/frontend/app/components/LineIcon.css +++ b/src/frontend/app/components/LineIcon.css @@ -13,6 +13,7 @@ --line-l5b: hsl(204, 100%, 54%); --line-l5b-text: hsl(0, 0%, 100%); --line-l6: hsl(330, 60%, 50%); + --line-l6-text: hsl(0, 0%, 100%); --line-l7: hsl(120, 60%, 70%); --line-l9b: hsl(36, 83%, 75%); --line-l10: hsl(30, 80%, 20%); @@ -40,6 +41,7 @@ --line-l23-text: hsl(0, 0%, 100%); --line-l24: hsl(0, 0%, 75%); --line-l25: hsl(34, 95%, 35%); + --line-l25-text: hsl(0, 0%, 100%); --line-l27: hsl(30, 60%, 30%); --line-l27-text: hsl(0, 0%, 100%); --line-l28: hsl(230, 98%, 84%); @@ -59,6 +61,7 @@ --line-h3-text: hsl(0, 0%, 100%); --line-lzd: hsl(220, 60%, 50%); --line-n1: hsl(0, 51%, 53%); + --line-n1-text: hsl(0, 0%, 100%); --line-n4: hsl(300, 33%, 30%); --line-n4-text: hsl(0, 0%, 100%); --line-psa1: hsl(120, 100%, 30%); diff --git a/src/frontend/app/components/StopGallery.tsx b/src/frontend/app/components/StopGallery.tsx index 500ea20..c1d9780 100644 --- a/src/frontend/app/components/StopGallery.tsx +++ b/src/frontend/app/components/StopGallery.tsx @@ -1,6 +1,5 @@ import React, { useEffect, useRef, useState } from "react"; import { type Stop } from "../data/StopDataProvider"; -import "./StopGallery.css"; import StopGalleryItem from "./StopGalleryItem"; interface StopGalleryProps { @@ -36,10 +35,12 @@ const StopGallery: React.FC<StopGalleryProps> = ({ if (stops.length === 0 && emptyMessage) { return ( - <div className="gallery-container stoplist-section"> - <h3 className="page-subtitle">{title}</h3> - <div className="gallery-empty-state"> - <p className="message">{emptyMessage}</p> + <div className="w-full px-4 flex flex-col gap-2"> + <h3 className="text-lg font-semibold text-gray-900 dark:text-gray-100">{title}</h3> + <div className="text-center"> + <p className="text-sm px-4 py-3 bg-gray-100 dark:bg-gray-800 rounded-lg"> + {emptyMessage} + </p> </div> </div> ); @@ -50,24 +51,31 @@ const StopGallery: React.FC<StopGalleryProps> = ({ } return ( - <div className="gallery-container stoplist-section"> - <div className="gallery-header"> - <h3 className="page-subtitle">{title}</h3> - <span className="gallery-counter">{stops.length}</span> - </div> + <div className="w-full px-4 flex flex-col gap-2"> + <h3 className="text-lg font-semibold text-gray-900 dark:text-gray-100">{title}</h3> - <div ref={scrollRef} className="gallery-scroll-container"> - <div className="gallery-track"> + <div + ref={scrollRef} + className="overflow-x-auto overflow-y-hidden snap-x snap-mandatory scrollbar-hide pb-2" + style={{ + WebkitOverflowScrolling: 'touch', + scrollbarWidth: 'none', + msOverflowStyle: 'none' + }} + > + <div className="flex gap-3"> {stops.map((stop) => ( <StopGalleryItem key={stop.stopId} stop={stop} /> ))} </div> </div> - <div className="gallery-dots"> + <div className="flex justify-center gap-1.5 mt-1"> {stops.map((_, index) => ( <span key={index} - className={`dot ${index === activeIndex ? "active" : ""}`} + className={`w-1.5 h-1.5 rounded-full transition-colors duration-200 ${ + index === activeIndex ? "bg-blue-600" : "bg-gray-300 dark:bg-gray-700" + }`} ></span> ))} </div> diff --git a/src/frontend/app/components/StopGalleryItem.tsx b/src/frontend/app/components/StopGalleryItem.tsx index 72a13e5..6c80362 100644 --- a/src/frontend/app/components/StopGalleryItem.tsx +++ b/src/frontend/app/components/StopGalleryItem.tsx @@ -9,21 +9,43 @@ interface StopGalleryItemProps { const StopGalleryItem: React.FC<StopGalleryItemProps> = ({ stop }) => { return ( - <div className="gallery-item"> - <Link className="gallery-item-link" to={`/stops/${stop.stopId}`}> - <div className="gallery-item-header"> - {stop.favourite && <span className="favourite-icon">★</span>} - <span className="gallery-item-code">({stop.stopId})</span> + <div className="flex-[0_0_90%] max-w-80 snap-start snap-always md:flex-[0_0_320px] lg:flex-[0_0_340px]"> + <Link + className=" + block p-3 min-h-[100px] + bg-gradient-to-br from-white to-gray-50 dark:from-gray-800 dark:to-gray-800/80 + border-2 border-gray-200 dark:border-gray-700 rounded-xl + no-underline text-gray-900 dark:text-gray-100 + hover:border-blue-400 dark:hover:border-blue-600 hover:shadow-sm + transition-all duration-200 + " + to={`/stops/${stop.stopId}`} + > + <div className="flex items-center gap-2 mb-1"> + {stop.favourite && <span className="text-yellow-500 text-base">★</span>} + <span className="text-xs text-gray-600 dark:text-gray-400 font-medium"> + ({stop.stopId}) + </span> </div> - <div className="gallery-item-name"> + <div + className="text-[0.95rem] font-semibold mb-2 leading-snug line-clamp-2 min-h-[2.5em]" + style={{ + display: '-webkit-box', + WebkitLineClamp: 2, + WebkitBoxOrient: 'vertical', + overflow: 'hidden' + }} + > {StopDataProvider.getDisplayName(stop)} </div> - <div className="gallery-item-lines"> + <div className="flex flex-wrap gap-1 items-center"> {stop.lines?.slice(0, 5).map((line) => ( <LineIcon key={line} line={line} /> ))} {stop.lines && stop.lines.length > 5 && ( - <span className="more-lines">+{stop.lines.length - 5}</span> + <span className="text-xs text-gray-600 dark:text-gray-400 font-medium px-1.5 py-0.5 bg-gray-200 dark:bg-gray-700 rounded"> + +{stop.lines.length - 5} + </span> )} </div> </Link> diff --git a/src/frontend/app/components/StopItem.tsx b/src/frontend/app/components/StopItem.tsx index 7875b59..9679b05 100644 --- a/src/frontend/app/components/StopItem.tsx +++ b/src/frontend/app/components/StopItem.tsx @@ -9,18 +9,21 @@ interface StopItemProps { const StopItem: React.FC<StopItemProps> = ({ stop }) => { return ( - <li className="list-item"> - <Link className="list-item-link" to={`/stops/${stop.stopId}`}> - <div style={{ display: "flex", justifyContent: "space-between", alignItems: "baseline" }}> - <span style={{ fontWeight: 600 }}> - {stop.favourite && <span className="favourite-icon">★</span>} + <li className="pb-3 border-b border-gray-200 dark:border-gray-700 md:border md:border-gray-300 dark:md:border-gray-700 md:rounded-lg md:p-3 md:pb-3"> + <Link + className="block text-gray-900 dark:text-gray-100 no-underline hover:text-blue-600 dark:hover:text-blue-400 transition-colors duration-200" + to={`/stops/${stop.stopId}`} + > + <div className="flex justify-between items-baseline"> + <span className="font-semibold"> + {stop.favourite && <span className="text-yellow-500 mr-1">★</span>} {StopDataProvider.getDisplayName(stop)} </span> - <span style={{ fontSize: "0.85em", color: "var(--subtitle-color)", marginLeft: "0.5rem" }}> + <span className="text-sm text-gray-600 dark:text-gray-400 ml-2"> ({stop.stopId}) </span> </div> - <div className="line-icons" style={{ marginTop: "0.25rem" }}> + <div className="flex flex-wrap gap-1 mt-1"> {stop.lines?.map((line) => ( <LineIcon key={line} line={line} /> ))} diff --git a/src/frontend/app/root.css b/src/frontend/app/root.css index 8c3ef47..5365d53 100644 --- a/src/frontend/app/root.css +++ b/src/frontend/app/root.css @@ -45,7 +45,6 @@ --error-message-color: #7f8c8d; color-scheme: light; - font-family: "Roboto Variable", Roboto, Arial, sans-serif; } [data-theme="dark"] { diff --git a/src/frontend/app/routes/home.css b/src/frontend/app/routes/home.css deleted file mode 100644 index b935518..0000000 --- a/src/frontend/app/routes/home.css +++ /dev/null @@ -1,320 +0,0 @@ -/* Common page styles */ -.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; -} - -.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: 0; -} - -.form-group { - margin: 0; - display: flex; - flex-direction: column; -} - -.form-label { - font-size: 0.85rem; - margin-bottom: 0.5rem; - font-weight: 500; -} - -.form-input { - padding: 0.75rem; - font-size: 1rem; - border: 1px solid var(--border-color); - border-radius: 8px; -} - -/* List styles */ -.list-container { - 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: 0.75rem; - border-bottom: 1px solid var(--border-color); -} - -.list-item-link { - display: block; - color: var(--text-color); - text-decoration: none; - font-size: 1rem; /* Reduced font size */ -} - -.list-item-link:hover { - color: var(--button-background-color); -} - -.list-item-link:hover .line-icon { - color: var(--text-color); -} - -.distance-info { - font-size: 0.9rem; - color: var(--subtitle-color); -} - -/* Message styles */ -.message { - padding: 1rem; - background-color: var(--message-background-color); - border-radius: 8px; - margin-bottom: 1rem; -} - -/* About page specific styles */ -.about-page { - text-align: center; - padding: 1rem; -} - -.about-version { - color: var(--subtitle-color); - font-size: 0.9rem; - margin-top: 2rem; -} - -.about-description { - margin-top: 1rem; - line-height: 1.6; -} - -/* Map page specific styles */ -.map-container { - height: calc(100dvh - 140px); - margin: -16px; - margin-bottom: 1rem; - position: relative; -} - -/* Fullscreen map styles */ -.fullscreen-container { - position: absolute; - top: 0; - left: 0; - width: 100vw; - height: 100dvh; - padding: 0; - margin: 0; - max-width: none; - overflow: hidden; -} - -.fullscreen-map { - width: 100%; - height: 100%; -} - -.fullscreen-loading { - display: flex; - justify-content: center; - align-items: center; - height: 100dvh; - width: 100vw; - font-size: 1.8rem; - font-weight: 600; - color: var(--text-color); -} - -/* Map marker and popup styles */ -.stop-marker { - box-shadow: 0 0 5px rgba(0, 0, 0, 0.3); - transition: all 0.2s ease-in-out; -} - -.stop-marker:hover { - transform: scale(1.2); -} - -.maplibregl-popup { - max-width: 250px; -} - -.maplibregl-popup-content { - padding: 12px; - border-radius: 8px; - box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); -} - -.popup-line-icons { - display: flex; - flex-wrap: wrap; - margin: 6px 0; - gap: 5px; -} - -.popup-line { - display: inline-block; - background-color: var(--button-background-color); - color: white; - padding: 2px 6px; - margin-right: 4px; - border-radius: 4px; - font-size: 0.8rem; - font-weight: 500; -} - -.popup-link { - display: block; - margin-top: 8px; - color: var(--button-background-color); - text-decoration: none; - font-weight: 500; -} - -.popup-link:hover { - text-decoration: underline; -} - -/* Estimates page specific styles */ -.estimates-header { - display: flex; - align-items: center; - margin-bottom: 1rem; -} - -.estimates-stop-id { - font-size: 1rem; - color: var(--subtitle-color); - margin-left: 0.5rem; -} - -.estimates-arrival { - color: #28a745; - font-weight: 500; -} - -.estimates-delayed { - color: #dc3545; -} - -.button-group { - display: flex; - gap: 1rem; - margin-bottom: 1.5rem; - flex-wrap: wrap; -} - -.button { - 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; - text-align: center; - text-decoration: none; - display: inline-block; -} - -.button:hover { - background-color: var(--button-hover-background-color); -} - -.button:disabled { - background-color: var(--button-disabled-background-color); - cursor: not-allowed; -} - -.star-icon { - margin-right: 0.5rem; - color: #ccc; - fill: none; -} - -.star-icon.active { - color: var(--star-color); /* Yellow color for active star */ - fill: var(--star-color); -} - -/* Tablet and larger breakpoint */ -@media (min-width: 768px) { - .search-form { - display: flex; - align-items: flex-end; - gap: 1rem; - } - - .form-group { - flex: 1; - margin-bottom: 0; - } - - .form-button { - width: auto; - margin-top: 0; - } - - .list { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); - gap: 1rem; - } - - .list-item { - border: 1px solid var(--border-color); - border-radius: 8px; - margin-bottom: 0; - } -} - -/* Desktop breakpoint */ -@media (min-width: 1024px) { - .list { - grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); - } -} diff --git a/src/frontend/app/routes/home.tsx b/src/frontend/app/routes/home.tsx index 7d8338f..cb640c3 100644 --- a/src/frontend/app/routes/home.tsx +++ b/src/frontend/app/routes/home.tsx @@ -2,22 +2,18 @@ import Fuse from "fuse.js"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; import { usePageTitle } from "~/contexts/PageTitleContext"; -import { useApp } from "../AppContext"; import StopGallery from "../components/StopGallery"; import StopItem from "../components/StopItem"; import StopItemSkeleton from "../components/StopItemSkeleton"; import StopDataProvider, { type Stop } from "../data/StopDataProvider"; -import "./home.css"; +import "../tailwind-full.css"; export default function StopList() { const { t } = useTranslation(); usePageTitle(t("navbar.stops", "Paradas")); - const { region } = useApp(); const [data, setData] = useState<Stop[] | null>(null); const [loading, setLoading] = useState(true); const [searchResults, setSearchResults] = useState<Stop[] | null>(null); - const [favouriteIds, setFavouriteIds] = useState<number[]>([]); - const [recentIds, setRecentIds] = useState<number[]>([]); const [favouriteStops, setFavouriteStops] = useState<Stop[]>([]); const [recentStops, setRecentStops] = useState<Stop[]>([]); const [userLocation, setUserLocation] = useState<{ @@ -158,12 +154,6 @@ export default function StopList() { .map(({ stop }) => stop); }, [data, userLocation]); - // Load favourite and recent IDs immediately from localStorage - useEffect(() => { - setFavouriteIds(StopDataProvider.getFavouriteIds()); - setRecentIds(StopDataProvider.getRecent()); - }, [region]); - // Load stops from network const loadStops = useCallback(async () => { try { @@ -196,7 +186,7 @@ export default function StopList() { } finally { setLoading(false); } - }, [region]); + }, []); useEffect(() => { loadStops(); @@ -246,23 +236,35 @@ export default function StopList() { }; return ( - <div className="stoplist-page"> - <div className="stoplist-section search-container"> - <h3 className="page-subtitle">{t("stoplist.search_label", "Buscar paradas")}</h3> + <div className="flex flex-col gap-4 py-4 pb-8"> + {/* Search Section */} + <div className="w-full px-4"> + <h3 className="text-lg font-semibold mb-2 text-gray-900 dark:text-gray-100"> + {t("stoplist.search_label", "Buscar paradas")} + </h3> <input type="search" placeholder={randomPlaceholder} onChange={handleStopSearch} - className="search-bar" + className=" + w-full px-4 py-3 text-base + border border-gray-300 dark:border-gray-700 rounded-xl + bg-white dark:bg-gray-800 + text-gray-900 dark:text-gray-100 + placeholder:text-gray-500 dark:placeholder:text-gray-400 placeholder:opacity-80 + focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent + transition-all duration-200 + " /> </div> + {/* Search Results */} {searchResults && searchResults.length > 0 && ( - <div className="stoplist-section list-container"> - <h2 className="page-subtitle"> + <div className="w-full px-4 flex flex-col gap-2"> + <h2 className="text-lg font-semibold text-gray-900 dark:text-gray-100"> {t("stoplist.search_results", "Resultados de la búsqueda")} </h2> - <ul className="list"> + <ul className="list-none p-0 m-0 flex flex-col gap-2 md:grid md:grid-cols-[repeat(auto-fill,minmax(300px,1fr))] lg:grid-cols-[repeat(auto-fill,minmax(320px,1fr))]"> {searchResults.map((stop: Stop) => ( <StopItem key={stop.stopId} stop={stop} /> ))} @@ -270,6 +272,7 @@ export default function StopList() { </div> )} + {/* Favourites Gallery */} {!loading && ( <StopGallery stops={favouriteStops.sort((a, b) => a.stopId - b.stopId)} @@ -278,7 +281,8 @@ export default function StopList() { /> )} - {!loading && ( + {/* Recent Stops Gallery - only show if no favourites */} + {!loading && favouriteStops.length === 0 && ( <StopGallery stops={recentStops.slice(0, 5)} title={t("stoplist.recents")} @@ -287,14 +291,23 @@ export default function StopList() { {/*<ServiceAlerts />*/} - <div className="stoplist-section list-container"> - <h2 className="page-subtitle"> - {userLocation - ? t("stoplist.nearby_stops", "Nearby stops") - : t("stoplist.all_stops", "Paradas")} - </h2> + {/* All Stops / Nearby Stops */} + <div className="w-full px-4 flex flex-col gap-2"> + <div className="flex items-center gap-2"> + {userLocation && ( + <svg className="w-5 h-5 text-blue-600 dark:text-blue-400" fill="none" viewBox="0 0 24 24" stroke="currentColor"> + <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z" /> + <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 11a3 3 0 11-6 0 3 3 0 016 0z" /> + </svg> + )} + <h2 className="text-lg font-semibold text-gray-900 dark:text-gray-100"> + {userLocation + ? t("stoplist.nearby_stops", "Nearby stops") + : t("stoplist.all_stops", "Paradas")} + </h2> + </div> - <ul className="list"> + <ul className="list-none p-0 m-0 flex flex-col gap-2 md:grid md:grid-cols-[repeat(auto-fill,minmax(300px,1fr))] lg:grid-cols-[repeat(auto-fill,minmax(320px,1fr))]"> {loading && ( <> {Array.from({ length: 6 }, (_, index) => ( diff --git a/src/frontend/app/routes/settings.css b/src/frontend/app/routes/settings.css deleted file mode 100644 index 02708a7..0000000 --- a/src/frontend/app/routes/settings.css +++ /dev/null @@ -1,283 +0,0 @@ -/* About page specific styles */ -.about-page { - text-align: center; - padding: 1rem; -} - -.about-version { - color: var(--subtitle-color); - font-size: 0.9rem; - margin-top: 2rem; -} - -.about-description { - margin-top: 1rem; - line-height: 1.6; -} - -.settings-section { - margin-bottom: 2em; - padding: 1rem; - border: 1px solid var(--border-color); - border-radius: 8px; - background-color: var(--message-background-color); - text-align: left; -} - -.settings-section h2 { - margin-bottom: 1em; -} - -.settings-content { - display: flex; - flex-direction: column; - align-items: flex-start; - margin-bottom: 1em; -} - -.settings-content-inline { - display: flex; - flex-direction: column; - align-items: stretch; - margin-bottom: 1em; -} - -.settings-section .form-button { - margin-bottom: 1em; - padding: 0.75rem 1.5rem; - font-size: 1.1rem; -} - -.settings-section .form-select-inline { - margin-left: 0.5em; - padding: 0.5rem; - font-size: 1rem; - border-radius: 8px; -} - -.settings-section .form-label-inline { - font-weight: 500; -} - -.settings-section .form-label { - display: block; - margin-bottom: 0.5em; - font-weight: 500; -} - -.settings-section .form-description { - margin-top: 0.5em; - font-size: 0.9rem; - color: var(--subtitle-color); -} - -.settings-section .form-details { - margin-top: 0.5em; - font-size: 0.9rem; - color: var(--subtitle-color); - border: 1px solid var(--border-color); - border-radius: 8px; - padding: 0.5rem; -} - -.settings-section .form-details summary { - cursor: pointer; - font-weight: 500; -} - -.settings-section .form-details p { - margin-top: 0.5em; -} - -.settings-section p { - margin-top: 0.5em; -} - -/* Update controls styles */ -.update-controls { - display: flex; - gap: 1rem; - margin-bottom: 1rem; - flex-wrap: wrap; -} - -.update-button, -.clear-cache-button { - display: inline-flex; - align-items: center; - gap: 0.5rem; - padding: 0.75rem 1rem; - border: none; - border-radius: 8px; - font-size: 0.9rem; - font-weight: 500; - cursor: pointer; - transition: all 0.2s ease; - text-decoration: none; -} - -.update-button { - background-color: var(--button-background-color); - color: white; -} - -.update-button:hover:not(:disabled) { - background-color: var(--button-hover-background-color); -} - -.update-button:disabled { - background-color: var(--button-disabled-background-color); - cursor: not-allowed; -} - -.clear-cache-button { - background-color: #6c757d; - color: white; -} - -.clear-cache-button:hover { - background-color: #5a6268; -} - -.reset-pwa-button { - background-color: #dc3545; - color: white; - font-weight: bold; -} - -.reset-pwa-button:hover { - background-color: #c82333; -} - -.update-message { - padding: 0.75rem; - border-radius: 6px; - font-size: 0.9rem; - margin-bottom: 1rem; -} - -.update-message.success { - background-color: #d4edda; - color: #155724; - border: 1px solid #c3e6cb; -} - -.update-message.error { - background-color: #f8d7da; - color: #721c24; - border: 1px solid #f5c6cb; -} - -.update-help-text { - font-size: 0.85rem; - color: var(--subtitle-color); - line-height: 1.4; - margin: 0; -} - -.spinning { - animation: spin 1s linear infinite; -} - -@keyframes spin { - from { - transform: rotate(0deg); - } - to { - transform: rotate(360deg); - } -} - -/* Modal styles */ -.modal-overlay { - position: fixed; - top: 0; - left: 0; - right: 0; - bottom: 0; - background-color: rgba(0, 0, 0, 0.5); - display: flex; - align-items: center; - justify-content: center; - z-index: 1000; - padding: 1rem; -} - -.modal-content { - background-color: var(--message-background-color); - padding: 2rem; - border-radius: 12px; - max-width: 500px; - width: 100%; - box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); -} - -.modal-content h2 { - margin-top: 0; - margin-bottom: 1rem; - color: var(--text-color); -} - -.modal-content p { - margin-bottom: 1.5rem; - line-height: 1.6; - color: var(--text-color); -} - -.modal-buttons { - display: flex; - gap: 1rem; - justify-content: flex-end; -} - -.modal-button { - padding: 0.75rem 1.5rem; - border: none; - border-radius: 8px; - font-size: 1rem; - font-weight: 500; - cursor: pointer; - transition: all 0.2s ease; -} - -.modal-button-cancel { - background-color: #6c757d; - color: white; -} - -.modal-button-cancel:hover { - background-color: #5a6268; -} - -.modal-button-confirm { - background-color: var(--button-background-color); - color: white; -} - -.modal-button-confirm:hover { - background-color: var(--button-hover-background-color); -} - -@media (max-width: 768px) { - .update-controls { - flex-direction: column; - } - - .update-button, - .clear-cache-button { - justify-content: center; - } - - .modal-content { - padding: 1.5rem; - } - - .modal-buttons { - flex-direction: column; - gap: 0.5rem; - } - - .modal-button { - width: 100%; - } -} diff --git a/src/frontend/app/routes/settings.tsx b/src/frontend/app/routes/settings.tsx index faad5a6..9b4625f 100644 --- a/src/frontend/app/routes/settings.tsx +++ b/src/frontend/app/routes/settings.tsx @@ -1,8 +1,8 @@ import { Computer, Moon, Sun } from "lucide-react"; import { useTranslation } from "react-i18next"; import { usePageTitle } from "~/contexts/PageTitleContext"; -import { type Theme, useApp } from "../AppContext"; -import "./settings.css"; +import { useApp, type Theme } from "../AppContext"; +import '../tailwind-full.css'; export default function Settings() { const { t, i18n } = useTranslation(); @@ -14,72 +14,91 @@ export default function Settings() { setMapPositionMode } = useApp(); - return ( - <div className="page-container"> - <section className="settings-section"> - <h2>{t("about.settings")}</h2> - - <div className="settings-content-inline"> - <label htmlFor="theme" className="form-label-inline"> - {t("about.theme")} - </label> + const THEMES = [ + { value: "light" as Theme, label: t("about.theme_light", "Claro"), icon: Sun }, + { value: "dark" as Theme, label: t("about.theme_dark", "Oscuro"), icon: Moon }, + { value: "system" as Theme, label: t("about.theme_system", "Sistema"), icon: Computer }, + ]; - <div className="flex"> - <button onClick={() => setTheme("light")}> - <Sun /> - </button> - <button onClick={() => setTheme("dark")}> - <Moon /> - </button> - <button onClick={() => setTheme("system")}> - <Computer /> + return ( + <div className="max-w-2xl mx-auto px-4 sm:px-6 lg:px-8 py-8"> + {/* Theme Selection */} + <section className="mb-8"> + <h2 className="text-xl font-semibold mb-4 text-gray-900 dark:text-gray-100"> + {t("about.theme", "Tema")} + </h2> + <div className="grid grid-cols-3 gap-3 sm:gap-4"> + {THEMES.map(({ value, label, icon: Icon }) => ( + <button + key={value} + onClick={() => setTheme(value)} + className={` + p-4 sm:p-6 flex flex-col items-center justify-center gap-2 + rounded-lg border-2 transition-all duration-200 + hover:bg-gray-50 dark:hover:bg-gray-800 + focus:outline-none focus:ring focus:ring-blue-500 dark:focus:ring-offset-gray-900 + ${value === theme + ? "border-blue-600 bg-blue-50 dark:bg-blue-900/20 text-blue-700 dark:text-blue-400 font-semibold" + : "border-gray-300 dark:border-gray-700 text-gray-700 dark:text-gray-300" + } + `} + > + <Icon className="w-6 h-6" /> + <span className="text-sm sm:text-base">{label}</span> </button> - </div> - - <select - id="theme" - className="form-select-inline" - value={theme} - onChange={(e) => setTheme(e.target.value as Theme)} - > - <option value="light">{t("about.theme_light")}</option> - <option value="dark">{t("about.theme_dark")}</option> - <option value="system">{t("about.theme_system")}</option> - </select> + ))} </div> + </section> - <div className="settings-content-inline"> - <label htmlFor="mapPositionMode" className="form-label-inline"> - {t("about.map_position_mode")} - </label> - <select - id="mapPositionMode" - className="form-select-inline" - value={mapPositionMode} - onChange={(e) => - setMapPositionMode(e.target.value as "gps" | "last") - } - > - <option value="gps">{t("about.map_position_gps")}</option> - <option value="last">{t("about.map_position_last")}</option> - </select> - </div> - <div className="settings-content-inline"> - <label htmlFor="language" className="form-label-inline"> - {t("about.language", "Idioma")}: - </label> - <select - id="language" - className="form-select-inline" - value={i18n.language} - onChange={(e) => i18n.changeLanguage(e.target.value)} - > - <option value="es-ES">Español</option> - <option value="gl-ES">Galego</option> - <option value="en-GB">English</option> - </select> - </div> + {/* Map Position Mode */} + <section className="mb-8"> + <label + htmlFor="mapPositionMode" + className="block text-lg font-medium text-gray-900 dark:text-gray-100 mb-3" + > + {t("about.map_position_mode")} + </label> + <select + id="mapPositionMode" + className=" + w-full px-4 py-3 rounded-lg border border-gray-300 dark:border-gray-700 + bg-white dark:bg-gray-800 + text-gray-900 dark:text-gray-100 + focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent + transition-colors duration-200 + " + value={mapPositionMode} + onChange={(e) => setMapPositionMode(e.target.value as "gps" | "last")} + > + <option value="gps">{t("about.map_position_gps")}</option> + <option value="last">{t("about.map_position_last")}</option> + </select> + </section> + {/* Language Selection */} + <section> + <label + htmlFor="language" + className="block text-lg font-medium text-gray-900 dark:text-gray-100 mb-3" + > + {t("about.language", "Idioma")} + </label> + <select + id="language" + className=" + w-full px-4 py-3 rounded-lg border border-gray-300 dark:border-gray-700 + bg-white dark:bg-gray-800 + text-gray-900 dark:text-gray-100 + focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent + transition-colors duration-200 + " + value={i18n.language} + onChange={(e) => i18n.changeLanguage(e.target.value)} + > + <option value="es-ES">Español</option> + <option value="gl-ES">Galego</option> + <option value="en-GB">English</option> + </select> </section> </div> ); diff --git a/src/frontend/app/tailwind-full.css b/src/frontend/app/tailwind-full.css new file mode 100644 index 0000000..1767d61 --- /dev/null +++ b/src/frontend/app/tailwind-full.css @@ -0,0 +1,3 @@ +@import "tailwindcss"; + +@custom-variant dark (&:where([data-theme=dark], [data-theme=dark] *)); |
