diff options
| author | Ariel Costas Guerrero <ariel@costas.dev> | 2025-06-24 13:29:50 +0200 |
|---|---|---|
| committer | Ariel Costas Guerrero <ariel@costas.dev> | 2025-06-24 13:29:50 +0200 |
| commit | 894e67863dbb89a4819e825fcdf7117021082b2a (patch) | |
| tree | fb544ef7fa99ff86489717e793595f503783bb72 /src/frontend/app/components | |
| parent | 7dd9ea97a2f34a35e80c28d59d046f839eb6c60b (diff) | |
Replace leaflet for maplibre, use react-router in framework mode
Diffstat (limited to 'src/frontend/app/components')
| -rw-r--r-- | src/frontend/app/components/GroupedTable.tsx | 74 | ||||
| -rw-r--r-- | src/frontend/app/components/LineIcon.css | 239 | ||||
| -rw-r--r-- | src/frontend/app/components/LineIcon.tsx | 17 | ||||
| -rw-r--r-- | src/frontend/app/components/RegularTable.tsx | 70 | ||||
| -rw-r--r-- | src/frontend/app/components/StopItem.css | 54 | ||||
| -rw-r--r-- | src/frontend/app/components/StopItem.tsx | 25 |
6 files changed, 479 insertions, 0 deletions
diff --git a/src/frontend/app/components/GroupedTable.tsx b/src/frontend/app/components/GroupedTable.tsx new file mode 100644 index 0000000..3a16d89 --- /dev/null +++ b/src/frontend/app/components/GroupedTable.tsx @@ -0,0 +1,74 @@ +import { type StopDetails } from "../routes/estimates-$id"; +import LineIcon from "./LineIcon"; + +interface GroupedTable { + data: StopDetails; + dataDate: Date | null; +} + +export const GroupedTable: React.FC<GroupedTable> = ({ data, dataDate }) => { + const formatDistance = (meters: number) => { + if (meters > 1024) { + return `${(meters / 1000).toFixed(1)} km`; + } else { + return `${meters} m`; + } + } + + const groupedEstimates = data.estimates.reduce((acc, estimate) => { + if (!acc[estimate.line]) { + acc[estimate.line] = []; + } + acc[estimate.line].push(estimate); + return acc; + }, {} as Record<string, typeof data.estimates>); + + const sortedLines = Object.keys(groupedEstimates).sort((a, b) => { + const firstArrivalA = groupedEstimates[a][0].minutes; + const firstArrivalB = groupedEstimates[b][0].minutes; + return firstArrivalA - firstArrivalB; + }); + + return <table className="table"> + <caption>Estimaciones de llegadas a las {dataDate?.toLocaleTimeString()}</caption> + + <thead> + <tr> + <th>Línea</th> + <th>Ruta</th> + <th>Llegada</th> + <th>Distancia</th> + </tr> + </thead> + + <tbody> + {sortedLines.map((line) => ( + groupedEstimates[line].map((estimate, idx) => ( + <tr key={`${line}-${idx}`}> + {idx === 0 && ( + <td rowSpan={groupedEstimates[line].length}> + <LineIcon line={line} /> + </td> + )} + <td>{estimate.route}</td> + <td>{`${estimate.minutes} min`}</td> + <td> + {estimate.meters > -1 + ? formatDistance(estimate.meters) + : "No disponible" + } + </td> + </tr> + )) + ))} + </tbody> + + {data?.estimates.length === 0 && ( + <tfoot> + <tr> + <td colSpan={4}>No hay estimaciones disponibles</td> + </tr> + </tfoot> + )} + </table> +} diff --git a/src/frontend/app/components/LineIcon.css b/src/frontend/app/components/LineIcon.css new file mode 100644 index 0000000..e7e8949 --- /dev/null +++ b/src/frontend/app/components/LineIcon.css @@ -0,0 +1,239 @@ +:root { + --line-c1: rgb(237, 71, 19); + --line-c3d: rgb(255, 204, 0); + --line-c3i: rgb(255, 204, 0); + --line-l4a: rgb(0, 153, 0); + --line-l4c: rgb(0, 153, 0); + --line-l5a: rgb(0, 176, 240); + --line-l5b: rgb(0, 176, 240); + --line-l6: rgb(204, 51, 153); + --line-l7: rgb(150, 220, 153); + --line-l9b: rgb(244, 202, 140); + --line-l10: rgb(153, 51, 0); + --line-l11: rgb(226, 0, 38); + --line-l12a: rgb(106, 150, 190); + --line-l12b: rgb(106, 150, 190); + --line-l13: rgb(0, 176, 240); + --line-l14: rgb(129, 142, 126); + --line-l15a: rgb(216, 168, 206); + --line-l15b: rgb(216, 168, 206); + --line-l15c: rgb(216, 168, 168); + --line-l16: rgb(129, 142, 126); + --line-l17: rgb(214, 245, 31); + --line-l18a: rgb(212, 80, 168); + --line-l18b: rgb(0, 0, 0); + --line-l18h: rgb(0, 0, 0); + --line-l23: rgb(0, 70, 210); + --line-l24: rgb(191, 191, 191); + --line-l25: rgb(172, 100, 4); + --line-l27: rgb(112, 74, 42); + --line-l28: rgb(176, 189, 254); + --line-l29: rgb(248, 184, 90); + --line-l31: rgb(255, 255, 0); + --line-a: rgb(119, 41, 143); + --line-h: rgb(0, 96, 168); + --line-h1: rgb(0, 96, 168); + --line-h2: rgb(0, 96, 168); + --line-h3: rgb(0, 96, 168); + --line-lzd: rgb(61, 78, 167); + --line-n1: rgb(191, 191, 191); + --line-n4: rgb(102, 51, 102); + --line-psa1: rgb(0, 153, 0); + --line-psa4: rgb(0, 153, 0); + --line-ptl: rgb(150, 220, 153); + --line-turistico: rgb(102, 51, 102); + --line-u1: rgb(172, 100, 4); + --line-u2: rgb(172, 100, 4); +} + +.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: var(--line-c1); +} + +.line-c3d { + border-color: var(--line-c3d); +} + +.line-c3i { + border-color: var(--line-c3i); +} + +.line-l4a { + border-color: var(--line-l4a); +} + +.line-l4c { + border-color: var(--line-l4c); +} + +.line-l5a { + border-color: var(--line-l5a); +} + +.line-l5b { + border-color: var(--line-l5b); +} + +.line-l6 { + border-color: var(--line-l6); +} + +.line-l7 { + border-color: var(--line-l7); +} + +.line-l9b { + border-color: var(--line-l9b); +} + +.line-l10 { + border-color: var(--line-l10); +} + +.line-l11 { + border-color: var(--line-l11); +} + +.line-l12a { + border-color: var(--line-l12a); +} + +.line-l12b { + border-color: var(--line-l12b); +} + +.line-l13 { + border-color: var(--line-l13); +} + +.line-l14 { + border-color: var(--line-l14); +} + +.line-l15a { + border-color: var(--line-l15a); +} + +.line-l15b { + border-color: var(--line-l15b); +} + +.line-l15c { + border-color: var(--line-l15c); +} + +.line-l16 { + border-color: var(--line-l16); +} + +.line-l17 { + border-color: var(--line-l17); +} + +.line-l18a { + border-color: var(--line-l18a); +} + +.line-l18b { + border-color: var(--line-l18b); +} + +.line-l18h { + border-color: var(--line-l18h); +} + +.line-l23 { + border-color: var(--line-l23); +} + +.line-l24 { + border-color: var(--line-l24); +} + +.line-l25 { + border-color: var(--line-l25); +} + +.line-l27 { + border-color: var(--line-l27); +} + +.line-l28 { + border-color: var(--line-l28); +} + +.line-l29 { + border-color: var(--line-l29); +} + +.line-l31 { + border-color: var(--line-l31); +} + +.line-a { + border-color: var(--line-a); +} + +.line-h { + border-color: var(--line-h); +} + +.line-h1 { + border-color: var(--line-h1); +} + +.line-h2 { + border-color: var(--line-h2); +} + +.line-h3 { + border-color: var(--line-h3); +} + +.line-lzd { + border-color: var(--line-lzd); +} + +.line-n1 { + border-color: var(--line-n1); +} + +.line-n4 { + border-color: var(--line-n4); +} + +.line-psa1 { + border-color: var(--line-psa1); +} + +.line-psa4 { + border-color: var(--line-psa4); +} + +.line-ptl { + border-color: var(--line-ptl); +} + +.line-turistico { + border-color: var(--line-turistico); +} + +.line-u1 { + border-color: var(--line-u1); +} + +.line-u2 { + border-color: var(--line-u2); +}
\ No newline at end of file diff --git a/src/frontend/app/components/LineIcon.tsx b/src/frontend/app/components/LineIcon.tsx new file mode 100644 index 0000000..291b444 --- /dev/null +++ b/src/frontend/app/components/LineIcon.tsx @@ -0,0 +1,17 @@ +import React from 'react'; +import './LineIcon.css'; + +interface LineIconProps { + line: string; +} + +const LineIcon: React.FC<LineIconProps> = ({ line }) => { + const formattedLine = /^[a-zA-Z]/.test(line) ? line : `L${line}`; + return ( + <span className={`line-icon line-${formattedLine.toLowerCase()}`}> + {formattedLine} + </span> + ); +}; + +export default LineIcon; diff --git a/src/frontend/app/components/RegularTable.tsx b/src/frontend/app/components/RegularTable.tsx new file mode 100644 index 0000000..75b598b --- /dev/null +++ b/src/frontend/app/components/RegularTable.tsx @@ -0,0 +1,70 @@ +import { type StopDetails } from "../routes/estimates-$id"; +import LineIcon from "./LineIcon"; + +interface RegularTableProps { + data: StopDetails; + dataDate: Date | null; +} + +export const RegularTable: React.FC<RegularTableProps> = ({ data, dataDate }) => { + + 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`; + } + } + + return <table className="table"> + <caption>Estimaciones de llegadas a las {dataDate?.toLocaleTimeString()}</caption> + + <thead> + <tr> + <th>Línea</th> + <th>Ruta</th> + <th>Llegada</th> + <th>Distancia</th> + </tr> + </thead> + + <tbody> + {data.estimates + .sort((a, b) => a.minutes - b.minutes) + .map((estimate, idx) => ( + <tr key={idx}> + <td><LineIcon line={estimate.line} /></td> + <td>{estimate.route}</td> + <td> + {estimate.minutes > 15 + ? absoluteArrivalTime(estimate.minutes) + : `${estimate.minutes} min`} + </td> + <td> + {estimate.meters > -1 + ? formatDistance(estimate.meters) + : "No disponible" + } + </td> + </tr> + ))} + </tbody> + + {data?.estimates.length === 0 && ( + <tfoot> + <tr> + <td colSpan={4}>No hay estimaciones disponibles</td> + </tr> + </tfoot> + )} + </table> +} diff --git a/src/frontend/app/components/StopItem.css b/src/frontend/app/components/StopItem.css new file mode 100644 index 0000000..9feb2d1 --- /dev/null +++ b/src/frontend/app/components/StopItem.css @@ -0,0 +1,54 @@ +/* Stop Item Styling */ + +.stop-notes { + font-size: 0.85rem; + font-style: italic; + color: #666; + margin: 2px 0; +} + +.stop-amenities { + display: flex; + flex-wrap: wrap; + gap: 4px; + margin-top: 4px; +} + +.amenity-tag { + font-size: 0.75rem; + background-color: #e8f4f8; + color: #0078d4; + border-radius: 4px; + padding: 2px 6px; + display: inline-block; +} + +/* Different colors for different amenity types */ +.amenity-tag[data-amenity="shelter"] { + background-color: #e3f1df; + color: #107c41; +} + +.amenity-tag[data-amenity="bench"] { + background-color: #f0e8fc; + color: #5c2e91; +} + +.amenity-tag[data-amenity="real-time display"] { + background-color: #fff4ce; + color: #986f0b; +} + +/* When there are alternate names available, show an indicator */ +.has-alternate-names { + position: relative; +} + +.has-alternate-names::after { + content: "⋯"; + position: absolute; + right: -15px; + top: 0; + color: #0078d4; + font-weight: bold; +}
\ No newline at end of file diff --git a/src/frontend/app/components/StopItem.tsx b/src/frontend/app/components/StopItem.tsx new file mode 100644 index 0000000..29370b7 --- /dev/null +++ b/src/frontend/app/components/StopItem.tsx @@ -0,0 +1,25 @@ +import React from 'react'; +import { Link } from 'react-router'; +import StopDataProvider, { type Stop } from '../data/StopDataProvider'; +import LineIcon from './LineIcon'; + +interface StopItemProps { + stop: Stop; +} + +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(stop)} + <div className="line-icons"> + {stop.lines?.map(line => <LineIcon key={line} line={line} />)} + </div> + + </Link> + </li> + ); +}; + +export default StopItem; |
