From 3aa6eee0f54dec3e4f92be2ad335a04145ac4db8 Mon Sep 17 00:00:00 2001 From: Ariel Costas Guerrero <94913521+arielcostas@users.noreply.github.com> Date: Mon, 3 Mar 2025 18:54:35 +0100 Subject: Improve the UI --- src/App.tsx | 34 ------ src/Layout.css | 44 ++++++++ src/Layout.tsx | 58 ++++++++++ src/assets/react.svg | 1 - src/components/LineIcon.css | 191 +++++++++++++++++++++++++++++++++ src/components/LineIcon.tsx | 17 +++ src/components/StopItem.tsx | 23 ++++ src/data/StopDataProvider.ts | 5 +- src/main.tsx | 42 ++++++-- src/pages/Estimates.tsx | 137 ++++++++++++++++++++++++ src/pages/Home.tsx | 99 ----------------- src/pages/Map.tsx | 76 ++++++++++++++ src/pages/Stop.tsx | 131 ----------------------- src/pages/StopList.tsx | 102 ++++++++++++++++++ src/styles/Estimates.css | 28 +++++ src/styles/Pages.css | 245 +++++++++++++++++++++++++++++++++++++++++++ 16 files changed, 956 insertions(+), 277 deletions(-) delete mode 100644 src/App.tsx create mode 100644 src/Layout.css create mode 100644 src/Layout.tsx delete mode 100644 src/assets/react.svg create mode 100644 src/components/LineIcon.css create mode 100644 src/components/LineIcon.tsx create mode 100644 src/components/StopItem.tsx create mode 100644 src/pages/Estimates.tsx delete mode 100644 src/pages/Home.tsx create mode 100644 src/pages/Map.tsx delete mode 100644 src/pages/Stop.tsx create mode 100644 src/pages/StopList.tsx create mode 100644 src/styles/Estimates.css create mode 100644 src/styles/Pages.css (limited to 'src') diff --git a/src/App.tsx b/src/App.tsx deleted file mode 100644 index dde37c8..0000000 --- a/src/App.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import { useState } from 'react' -import reactLogo from './assets/react.svg' -import viteLogo from '/vite.svg' - -function App() { - const [count, setCount] = useState(0) - - return ( - <> -
- - Vite logo - - - React logo - -
-

Vite + React

-
- -

- Edit src/App.tsx and save to test HMR -

-
-

- Click on the Vite and React logos to learn more -

- - ) -} - -export default App diff --git a/src/Layout.css b/src/Layout.css new file mode 100644 index 0000000..52b0262 --- /dev/null +++ b/src/Layout.css @@ -0,0 +1,44 @@ +.app-container { + display: flex; + flex-direction: column; + height: 100vh; + width: 100%; + overflow: hidden; +} + +.main-content { + flex: 1; + overflow: auto; + padding: 16px; + padding-bottom: 70px; /* Extra padding to ensure content isn't hidden behind navbar */ +} + +.nav-bar { + position: fixed; + bottom: 0; + left: 0; + right: 0; + background-color: #ffffff; + display: flex; + justify-content: space-around; + align-items: center; + height: 60px; + border-top: 1px solid #e0e0e0; + box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.1); +} + +.nav-item { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 8px; + color: #616161; + text-decoration: none; + width: 33.3%; + font-size: 12px; +} + +.nav-item.active { + color: #007bff; +} \ No newline at end of file diff --git a/src/Layout.tsx b/src/Layout.tsx new file mode 100644 index 0000000..5aaafbf --- /dev/null +++ b/src/Layout.tsx @@ -0,0 +1,58 @@ +import { ReactNode } from 'react'; +import { Link, useLocation } from 'react-router'; +import { MapPin, Map, Info } from 'lucide-react'; +import './Layout.css'; + +interface LayoutProps { + children: ReactNode; +} + +export function Layout({ children }: LayoutProps) { + const location = useLocation(); + + const navItems = [ + { + name: 'Stops', + icon: MapPin, + path: '/stops' + }, + { + name: 'Maps', + icon: Map, + path: '/map' + }, + { + name: 'About', + icon: Info, + path: '/about' + } + ]; + + return ( +
+ {/* Main content area */} +
+ {children} +
+ + {/* Android style bottom navigation bar */} + +
+ ); +} \ No newline at end of file diff --git a/src/assets/react.svg b/src/assets/react.svg deleted file mode 100644 index 6c87de9..0000000 --- a/src/assets/react.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/components/LineIcon.css b/src/components/LineIcon.css new file mode 100644 index 0000000..8c9f7ee --- /dev/null +++ b/src/components/LineIcon.css @@ -0,0 +1,191 @@ +.line-icon { + display: inline-block; + padding: 0.25rem 0.5rem; + margin-right: 0.5rem; + border-bottom: 3px solid; + font-size: 0.9rem; + font-weight: 600; + text-transform: uppercase; + color: inherit; + /* Prevent color change on hover */ +} + +.line-c1 { + border-color: rgb(237, 71, 19); +} + +.line-c3d { + border-color: rgb(255, 204, 0); +} + +.line-c3i { + border-color: rgb(255, 204, 0); +} + +.line-l4a { + border-color: rgb(0, 153, 0); +} + +.line-l4c { + border-color: rgb(0, 153, 0); +} + +.line-l5a { + border-color: rgb(0, 176, 240); +} + +.line-l5b { + border-color: rgb(0, 176, 240); +} + +.line-l6 { + border-color: rgb(204, 51, 153); +} + +.line-l7 { + border-color: rgb(150, 220, 153); +} + +.line-l9b { + border-color: rgb(244, 202, 140); +} + +.line-l10 { + border-color: rgb(153, 51, 0); +} + +.line-l11 { + border-color: rgb(226, 0, 38); +} + +.line-l12a { + border-color: rgb(106, 150, 190); +} + +.line-l12b { + border-color: rgb(106, 150, 190); +} + +.line-l13 { + border-color: rgb(0, 176, 240); +} + +.line-l14 { + border-color: rgb(129, 142, 126); +} + +.line-l15a { + border-color: rgb(216, 168, 206); +} + +.line-l15b { + border-color: rgb(216, 168, 206); +} + +.line-l15c { + border-color: rgb(216, 168, 168); +} + +.line-l16 { + border-color: rgb(129, 142, 126); +} + +.line-l17 { + border-color: rgb(214, 245, 31); +} + +.line-l18a { + border-color: rgb(212, 80, 168); +} + +.line-l18b { + border-color: rgb(0, 0, 0); +} + +.line-l18h { + border-color: rgb(0, 0, 0); +} + +.line-l23 { + border-color: rgb(0, 70, 210); +} + +.line-l24 { + border-color: rgb(191, 191, 191); +} + +.line-l25 { + border-color: rgb(172, 100, 4); +} + +.line-l27 { + border-color: rgb(112, 74, 42); +} + +.line-l28 { + border-color: rgb(176, 189, 254); +} + +.line-l29 { + border-color: rgb(248, 184, 90); +} + +.line-l31 { + border-color: rgb(255, 255, 0); +} + +.line-a { + border-color: rgb(119, 41, 143); +} + +.line-h { + border-color: rgb(0, 96, 168); +} + +.line-h1 { + border-color: rgb(0, 96, 168); +} + +.line-h2 { + border-color: rgb(0, 96, 168); +} + +.line-h3 { + border-color: rgb(0, 96, 168); +} + +.line-lzd { + border-color: rgb(61, 78, 167); +} + +.line-n1 { + border-color: rgb(191, 191, 191); +} + +.line-n4 { + border-color: rgb(102, 51, 102); +} + +.line-psa1 { + border-color: rgb(0, 153, 0); +} + +.line-psa4 { + border-color: rgb(0, 153, 0); +} + +.line-ptl { + border-color: rgb(150, 220, 153); +} + +.line-turistico { + border-color: rgb(102, 51, 102); +} + +.line-u1 { + border-color: rgb(172, 100, 4); +} + +.line-u2 { + border-color: rgb(172, 100, 4); +} \ No newline at end of file diff --git a/src/components/LineIcon.tsx b/src/components/LineIcon.tsx new file mode 100644 index 0000000..50fd1ec --- /dev/null +++ b/src/components/LineIcon.tsx @@ -0,0 +1,17 @@ +import React from 'react'; +import './LineIcon.css'; + +interface LineIconProps { + line: string; +} + +const LineIcon: React.FC = ({ line }) => { + const formattedLine = /^[a-zA-Z]/.test(line) ? line : `L${line}`; + return ( + + {formattedLine} + + ); +}; + +export default LineIcon; \ No newline at end of file diff --git a/src/components/StopItem.tsx b/src/components/StopItem.tsx new file mode 100644 index 0000000..6b48899 --- /dev/null +++ b/src/components/StopItem.tsx @@ -0,0 +1,23 @@ +import React from 'react'; +import { Link } from 'react-router'; +import { Stop } from '../data/StopDataProvider'; +import LineIcon from './LineIcon'; + +interface StopItemProps { + stop: Stop; +} + +const StopItem: React.FC = ({ stop }) => { + return ( +
  • + + ({stop.stopId}) {stop.name} +
    + {stop.lines?.map(line => )} +
    + +
  • + ); +}; + +export default StopItem; \ No newline at end of file diff --git a/src/data/StopDataProvider.ts b/src/data/StopDataProvider.ts index 73075c8..11674fd 100644 --- a/src/data/StopDataProvider.ts +++ b/src/data/StopDataProvider.ts @@ -1,5 +1,3 @@ -import stops from './stops.json'; - export interface CachedStopList { timestamp: number; data: Stop[]; @@ -22,6 +20,9 @@ export class StopDataProvider { favouriteStops = JSON.parse(rawFavouriteStops) as number[]; } + const response = await fetch('/api/GetStopList'); + const stops = await response.json() as Stop[]; + return stops.map((stop: Stop) => { return { ...stop, diff --git a/src/main.tsx b/src/main.tsx index f638946..cf9a20c 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -1,22 +1,44 @@ -import { StrictMode } from 'react' import { createRoot } from 'react-dom/client' -import { createBrowserRouter, RouterProvider } from 'react-router-dom' -import { Home } from './pages/Home.tsx' -import { Stop } from './pages/Stop.tsx' +import { createBrowserRouter, Navigate, RouterProvider } from 'react-router' +import { StopList } from './pages/StopList.tsx' +import { Estimates } from './pages/Estimates.tsx' +import { StopMap } from './pages/Map.tsx' +import { Layout } from './Layout.tsx' +import './styles/Pages.css' const router = createBrowserRouter([ { path: '/', - element: , + element: , }, { - path: '/:stopId', - element: + path: '/stops', + element: , + }, + { + path: '/map', + element: , + }, + { + path: '/estimates/:stopId', + element: + }, + { + path: '/about', + element: } ]) +function About() { + return ( +
    +

    About InfoBus App

    +

    This application helps you find bus stops and check bus arrival estimates.

    +

    Version 1.0.0

    +
    + ) +} + createRoot(document.getElementById('root')!).render( - - - , + , ) diff --git a/src/pages/Estimates.tsx b/src/pages/Estimates.tsx new file mode 100644 index 0000000..d3b4ced --- /dev/null +++ b/src/pages/Estimates.tsx @@ -0,0 +1,137 @@ +import { useEffect, useState } from "react"; +import { Link, useParams } from "react-router"; +import { StopDataProvider } from "../data/StopDataProvider"; +import LineIcon from "../components/LineIcon"; +import { Star } from 'lucide-react'; +import "../styles/Estimates.css"; + +interface StopDetails { + stop: { + id: number; + name: string; + latitude: number; + longitude: number; + } + estimates: { + line: string; + route: string; + minutes: number; + meters: number; + }[] +} + +export function Estimates(): JSX.Element { + const sdp = new StopDataProvider(); + const [data, setData] = useState(null); + const [favourited, setFavourited] = useState(false); + const params = useParams(); + + const loadData = () => { + fetch(`/api/GetStopEstimates?id=${params.stopId}`) + .then(r => r.json()) + .then((body: StopDetails) => setData(body)); + }; + + useEffect(() => { + loadData(); + + sdp.pushRecent(parseInt(params.stopId ?? "")); + + setFavourited( + sdp.isFavourite(parseInt(params.stopId ?? "")) + ); + }, []); + + const absoluteArrivalTime = (minutes: number) => { + const now = new Date() + const arrival = new Date(now.getTime() + minutes * 60000) + return Intl.DateTimeFormat(navigator.language, { + hour: '2-digit', + minute: '2-digit' + }).format(arrival) + } + + const formatDistance = (meters: number) => { + if (meters > 1024) { + return `${(meters / 1000).toFixed(1)} km`; + } else { + return `${meters} m`; + } + } + + const toggleFavourite = () => { + if (favourited) { + sdp.removeFavourite(parseInt(params.stopId ?? "")); + setFavourited(false); + } else { + sdp.addFavourite(parseInt(params.stopId ?? "")); + setFavourited(true); + } + } + + if (data === null) return

    Cargando datos en tiempo real...

    + + return ( +
    +
    +

    + + {data?.stop.name} ({data?.stop.id}) +

    +
    + +
    + + 🔙 Volver al listado de paradas + + + +
    + +
    + + + + + + + + + + + + + + {data.estimates + .sort((a, b) => a.minutes - b.minutes) + .map((estimate, idx) => ( + + + + + + + ))} + + + {data?.estimates.length === 0 && ( + + + + + + )} +
    Estimaciones de llegadas
    LíneaRutaMinutosMetros
    {estimate.route} + {estimate.minutes > 15 + ? absoluteArrivalTime(estimate.minutes) + : `${estimate.minutes} min`} + + {estimate.meters > -1 + ? formatDistance(estimate.meters) + : "No disponible" + } +
    No hay estimaciones disponibles
    +
    +
    + ) +} diff --git a/src/pages/Home.tsx b/src/pages/Home.tsx deleted file mode 100644 index b7c1675..0000000 --- a/src/pages/Home.tsx +++ /dev/null @@ -1,99 +0,0 @@ -import { useEffect, useMemo, useState } from "react"; -import { Link, useNavigate } from "react-router-dom"; -import { Stop, StopDataProvider } from "../data/StopDataProvider"; - -const sdp = new StopDataProvider(); - -export function Home() { - const [data, setData] = useState(null) - const navigate = useNavigate(); - - useEffect(() => { - sdp.getStops().then((stops: Stop[]) => setData(stops)) - }, []); - - const handleStopSearch = async (event: React.FormEvent) => { - event.preventDefault() - - const stopId = (event.target as HTMLFormElement).stopId.value - const searchNumber = parseInt(stopId) - if (data?.find(stop => stop.stopId === searchNumber)) { - navigate(`/${searchNumber}`) - } else { - alert("Parada no encontrada") - } - } - - const favouritedStops = useMemo(() => { - return data?.filter(stop => stop.favourite) ?? [] - }, [data]) - - const recentStops = useMemo(() => { - const recent = sdp.getRecent(); - - if (recent.length === 0) return null; - - return recent.map(stopId => data?.find(stop => stop.stopId === stopId) as Stop).reverse(); - }, [data]) - - if (data === null) return

    Loading...

    - - return ( - <> -

    UrbanoVigo Web

    - -
    -
    - - -
    - - -
    - -

    Paradas favoritas

    - - {favouritedStops?.length == 1 && ( -

    - Accede a una parada y márcala como favorita para verla aquí. -

    - )} - -
      - {favouritedStops?.sort((a, b) => a.stopId - b.stopId).map((stop: Stop) => ( -
    • - - ({stop.stopId}) {stop.name} - {stop.lines?.join(', ')} - -
    • - ))} -
    - -

    Recientes

    - -
      - {recentStops?.map((stop: Stop) => ( -
    • - - ({stop.stopId}) {stop.name} - {stop.lines?.join(', ')} - -
    • - ))} -
    - -

    Paradas

    - -
      - {data?.sort((a, b) => a.stopId - b.stopId).map((stop: Stop) => ( -
    • - - ({stop.stopId}) {stop.name} - {stop.lines?.join(', ')} - -
    • - ))} -
    - - ) -} diff --git a/src/pages/Map.tsx b/src/pages/Map.tsx new file mode 100644 index 0000000..dbf5b9f --- /dev/null +++ b/src/pages/Map.tsx @@ -0,0 +1,76 @@ +import { useEffect, useMemo, useState } from "react"; +import { Link, useNavigate } from "react-router"; +import { Stop, StopDataProvider } from "../data/StopDataProvider"; +import LineIcon from "../components/LineIcon"; + +const sdp = new StopDataProvider(); + +export function StopMap() { + const [data, setData] = useState(null) + const navigate = useNavigate(); + + useEffect(() => { + sdp.getStops().then((stops: Stop[]) => setData(stops)) + }, []); + + const handleStopSearch = async (event: React.FormEvent) => { + event.preventDefault() + + const stopId = (event.target as HTMLFormElement).stopId.value + const searchNumber = parseInt(stopId) + if (data?.find(stop => stop.stopId === searchNumber)) { + navigate(`/estimates/${searchNumber}`) + } else { + alert("Parada no encontrada") + } + } + + if (data === null) return

    Loading...

    + + return ( +
    +

    Map View

    + +
    + {/* Map placeholder - in a real implementation, this would be a map component */} +
    +

    Map will be displayed here

    +
    +
    + +
    +
    + + +
    + + +
    + +
    +

    Nearby Stops

    +
      + {data?.slice(0, 5).map((stop: Stop) => ( +
    • + + ({stop.stopId}) {stop.name} +
      + {stop.lines?.map(line => )} +
      + +
    • + ))} +
    +
    +
    + ) +} diff --git a/src/pages/Stop.tsx b/src/pages/Stop.tsx deleted file mode 100644 index aa6651c..0000000 --- a/src/pages/Stop.tsx +++ /dev/null @@ -1,131 +0,0 @@ -import { useEffect, useState } from "react"; -import { Link, useParams } from "react-router-dom"; -import { StopDataProvider } from "../data/StopDataProvider"; - -interface StopDetails { - stop: { - id: number; - name: string; - latitude: number; - longitude: number; - } - estimates: { - line: string; - route: string; - minutes: number; - meters: number; - }[] -} - -export function Stop(): JSX.Element { - const sdp = new StopDataProvider(); - const [data, setData] = useState(null); - const [favourited, setFavourited] = useState(false); - const params = useParams(); - - const loadData = () => { - fetch(`/api/GetStopEstimates?id=${params.stopId}`) - .then(r => r.json()) - .then((body: StopDetails) => setData(body)); - }; - - useEffect(() => { - loadData(); - - sdp.pushRecent(parseInt(params.stopId ?? "")); - - setFavourited( - sdp.isFavourite(parseInt(params.stopId ?? "")) - ); - }) - - const absoluteArrivalTime = (minutes: number) => { - const now = new Date() - const arrival = new Date(now.getTime() + minutes * 60000) - return Intl.DateTimeFormat(navigator.language, { - hour: '2-digit', - minute: '2-digit' - }).format(arrival) - } - - if (data === null) return

    Cargando datos en tiempo real...

    - - return ( - <> -
    -

    {data?.stop.name} ({data?.stop.id})

    -
    - -
    - - 🔙 Volver al listado de paradas - - - {!favourited && ( - - )} - - {favourited && ( - - )} - - -
    - - - - - - - - - - - - - - - {data.estimates - .sort((a, b) => a.minutes - b.minutes) - .map((estimate, idx) => ( - - - - - - - ))} - - - {data?.estimates.length === 0 && ( - - - - - - )} -
    Estimaciones de llegadas
    LíneaRutaMinutosMetros
    {estimate.line}{estimate.route} - {estimate.minutes} ({absoluteArrivalTime(estimate.minutes)}) - - {estimate.meters > -1 - ? `${estimate.meters} metros` - : "No disponible" - } -
    No hay estimaciones disponibles
    - -

    - Volver al inicio -

    - - ) -} diff --git a/src/pages/StopList.tsx b/src/pages/StopList.tsx new file mode 100644 index 0000000..2351b51 --- /dev/null +++ b/src/pages/StopList.tsx @@ -0,0 +1,102 @@ +import { useEffect, useMemo, useState } from "react"; +import { Stop, StopDataProvider } from "../data/StopDataProvider"; +import StopItem from "../components/StopItem"; +import Fuse from "fuse.js"; + +const sdp = new StopDataProvider(); + +export function StopList() { + const [data, setData] = useState(null) + const [searchResults, setSearchResults] = useState(null); + + useEffect(() => { + sdp.getStops().then((stops: Stop[]) => setData(stops)) + }, []); + + const handleStopSearch = (event: React.ChangeEvent) => { + const stopName = event.target.value; + if (data) { + const fuse = new Fuse(data, { keys: ['name'], threshold: 0.3 }); + const results = fuse.search(stopName).map(result => result.item); + setSearchResults(results); + } + } + + const favouritedStops = useMemo(() => { + return data?.filter(stop => stop.favourite) ?? [] + }, [data]) + + const recentStops = useMemo(() => { + const recent = sdp.getRecent(); + + if (recent.length === 0) return null; + + return recent.map(stopId => data?.find(stop => stop.stopId === stopId) as Stop).reverse(); + }, [data]) + + if (data === null) return

    Loading...

    + + return ( +
    +

    UrbanoVigo Web

    + +
    +
    + + +
    +
    + + {searchResults && searchResults.length > 0 && ( +
    +

    Resultados de la búsqueda

    +
      + {searchResults.map((stop: Stop) => ( + + ))} +
    +
    + )} + +
    +

    Paradas favoritas

    + + {favouritedStops?.length === 0 && ( +

    + Accede a una parada y márcala como favorita para verla aquí. +

    + )} + +
      + {favouritedStops?.sort((a, b) => a.stopId - b.stopId).map((stop: Stop) => ( + + ))} +
    +
    + + {recentStops && recentStops.length > 0 && ( +
    +

    Recientes

    + +
      + {recentStops.map((stop: Stop) => ( + + ))} +
    +
    + )} + +
    +

    Paradas

    + +
      + {data?.sort((a, b) => a.stopId - b.stopId).map((stop: Stop) => ( + + ))} +
    +
    +
    + ) +} diff --git a/src/styles/Estimates.css b/src/styles/Estimates.css new file mode 100644 index 0000000..d9fa0ab --- /dev/null +++ b/src/styles/Estimates.css @@ -0,0 +1,28 @@ +.table-responsive { + overflow-x: auto; + margin-bottom: 1.5rem; +} + +.table { + width: 100%; + border-collapse: collapse; +} + +.table caption { + margin-bottom: 0.5rem; + font-weight: 500; +} + +.table th, .table td { + padding: 0.75rem; + text-align: left; + border-bottom: 1px solid #eee; +} + +.table th { + border-bottom: 2px solid #ddd; +} + +.table tfoot td { + text-align: center; +} \ No newline at end of file diff --git a/src/styles/Pages.css b/src/styles/Pages.css new file mode 100644 index 0000000..ecd7122 --- /dev/null +++ b/src/styles/Pages.css @@ -0,0 +1,245 @@ +/* Mobile-first page styles */ + +/* Common page styles */ +.page-container { + max-width: 100%; + padding: 0 16px; +} + +.page-title { + font-size: 1.8rem; + margin-bottom: 1rem; + font-weight: 600; + color: #333; +} + +.page-subtitle { + font-size: 1.4rem; + margin-top: 1.5rem; + margin-bottom: 0.75rem; + font-weight: 500; + color: #444; +} + +/* Form styles */ +.search-form { + margin-bottom: 1.5rem; +} + +.form-group { + margin-bottom: 1rem; + display: flex; + flex-direction: column; +} + +.form-label { + font-size: 0.9rem; + margin-bottom: 0.25rem; + font-weight: 500; +} + +.form-input { + padding: 0.75rem; + font-size: 1rem; + border: 1px solid #ddd; + border-radius: 8px; +} + +.form-button { + padding: 0.75rem 1rem; + background-color: #007bff; + 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: #0069d9; +} + +/* List styles */ +.list-container { + margin-bottom: 1.5rem; +} + +.list { + list-style: none; + padding: 0; + margin: 0; +} + +.list-item { + padding: 1rem; + border-bottom: 1px solid #eee; +} + +.list-item:last-child { + border-bottom: none; +} + +.list-item-link { + display: block; + color: #333; + text-decoration: none; + font-size: 1.1rem; /* Increased font size for stop name */ +} + +.list-item-link:hover { + color: #007bff; +} + +.list-item-link:hover .line-icon { + color: #333; + } + +/* Message styles */ +.message { + padding: 1rem; + background-color: #f8f9fa; + border-radius: 8px; + margin-bottom: 1rem; +} + +/* About page specific styles */ +.about-page { + text-align: center; + padding: 1rem; +} + +.about-version { + color: #666; + font-size: 0.9rem; + margin-top: 2rem; +} + +.about-description { + margin-top: 1rem; + line-height: 1.6; +} + +/* Map page specific styles */ +.map-container { + height: calc(100vh - 140px); + margin: -16px; + margin-bottom: 1rem; +} + +/* Estimates page specific styles */ +.estimates-header { + display: flex; + align-items: center; + margin-bottom: 1rem; +} + +.estimates-stop-id { + font-size: 1rem; + color: #666; + 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: #007bff; + 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: #0069d9; +} + +.button:disabled { + background-color: #cccccc; + cursor: not-allowed; +} + +.star-icon { + margin-right: 0.5rem; + color: #ccc; + fill: none; +} + +.star-icon.active { + color: #ffcc00; /* Yellow color for active star */ + fill: #ffcc00; +} + +/* Tablet and larger breakpoint */ +@media (min-width: 768px) { + .page-container { + width: 90%; + max-width: 768px; + margin: 0 auto; + } + + .page-title { + font-size: 2.2rem; + } + + .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 #eee; + border-radius: 8px; + margin-bottom: 0; + } +} + +/* Desktop breakpoint */ +@media (min-width: 1024px) { + .page-container { + max-width: 1024px; + } + + .list { + grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); + } +} \ No newline at end of file -- cgit v1.3