diff options
| author | copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> | 2025-11-06 22:49:47 +0000 |
|---|---|---|
| committer | Ariel Costas Guerrero <ariel@costas.dev> | 2025-11-07 10:46:51 +0100 |
| commit | e51cdd89afc08274ca622e18b8127feca29e90a3 (patch) | |
| tree | 1e016c9bb977f8db4e7c61ad5fe1b3be311b6fef /src/frontend/app/components | |
| parent | 43ea6cc94b6c1f2bfaf3f8787991d3283765da0b (diff) | |
Add gallery components and improve search functionality
Co-authored-by: arielcostas <94913521+arielcostas@users.noreply.github.com>
Diffstat (limited to 'src/frontend/app/components')
| -rw-r--r-- | src/frontend/app/components/ServiceAlerts.css | 72 | ||||
| -rw-r--r-- | src/frontend/app/components/ServiceAlerts.tsx | 22 | ||||
| -rw-r--r-- | src/frontend/app/components/StopGallery.css | 161 | ||||
| -rw-r--r-- | src/frontend/app/components/StopGallery.tsx | 58 | ||||
| -rw-r--r-- | src/frontend/app/components/StopGalleryItem.tsx | 38 |
5 files changed, 351 insertions, 0 deletions
diff --git a/src/frontend/app/components/ServiceAlerts.css b/src/frontend/app/components/ServiceAlerts.css new file mode 100644 index 0000000..484b45b --- /dev/null +++ b/src/frontend/app/components/ServiceAlerts.css @@ -0,0 +1,72 @@ +/* Service Alerts Container */ +.service-alerts-container { + margin-bottom: 1.5rem; +} + +.service-alert { + display: flex; + gap: 1rem; + padding: 1rem; + border-radius: 8px; + margin-bottom: 0.75rem; + border: 1px solid; +} + +.service-alert.info { + background-color: #e3f2fd; + border-color: #2196f3; + color: #0d47a1; +} + +.service-alert.warning { + background-color: #fff3e0; + border-color: #ff9800; + color: #e65100; +} + +.service-alert.error { + background-color: #ffebee; + border-color: #f44336; + color: #b71c1c; +} + +/* Dark mode adjustments */ +@media (prefers-color-scheme: dark) { + .service-alert.info { + background-color: #0d47a1; + border-color: #2196f3; + color: #e3f2fd; + } + + .service-alert.warning { + background-color: #e65100; + border-color: #ff9800; + color: #fff3e0; + } + + .service-alert.error { + background-color: #b71c1c; + border-color: #f44336; + color: #ffebee; + } +} + +.alert-icon { + font-size: 1.5rem; + flex-shrink: 0; +} + +.alert-content { + flex: 1; +} + +.alert-title { + font-weight: 600; + font-size: 1rem; + margin-bottom: 0.25rem; +} + +.alert-message { + font-size: 0.9rem; + line-height: 1.4; +} diff --git a/src/frontend/app/components/ServiceAlerts.tsx b/src/frontend/app/components/ServiceAlerts.tsx new file mode 100644 index 0000000..7966f2a --- /dev/null +++ b/src/frontend/app/components/ServiceAlerts.tsx @@ -0,0 +1,22 @@ +import React from "react"; +import { useTranslation } from "react-i18next"; +import "./ServiceAlerts.css"; + +const ServiceAlerts: React.FC = () => { + const { t } = useTranslation(); + + return ( + <div className="service-alerts-container"> + <h2 className="page-subtitle">{t("stoplist.service_alerts")}</h2> + <div className="service-alert info"> + <div className="alert-icon">ℹ️</div> + <div className="alert-content"> + <div className="alert-title">{t("stoplist.alerts_coming_soon")}</div> + <div className="alert-message">{t("stoplist.alerts_description")}</div> + </div> + </div> + </div> + ); +}; + +export default ServiceAlerts; diff --git a/src/frontend/app/components/StopGallery.css b/src/frontend/app/components/StopGallery.css new file mode 100644 index 0000000..f53f2a5 --- /dev/null +++ b/src/frontend/app/components/StopGallery.css @@ -0,0 +1,161 @@ +/* Gallery Container */ +.gallery-container { + margin-bottom: 1.5rem; +} + +.gallery-header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 0.75rem; +} + +.gallery-counter { + display: inline-flex; + align-items: center; + justify-content: center; + min-width: 2rem; + height: 2rem; + padding: 0 0.5rem; + background-color: var(--button-background-color); + color: white; + border-radius: 1rem; + font-size: 0.9rem; + font-weight: 600; +} + +/* Scroll Container */ +.gallery-scroll-container { + overflow-x: auto; + overflow-y: hidden; + -webkit-overflow-scrolling: touch; + scroll-snap-type: x mandatory; + scrollbar-width: none; /* Firefox */ + -ms-overflow-style: none; /* IE and Edge */ + margin: 0 -1rem; + padding: 0 1rem; +} + +.gallery-scroll-container::-webkit-scrollbar { + display: none; /* Chrome, Safari, Opera */ +} + +/* Gallery Track */ +.gallery-track { + display: flex; + gap: 1rem; + padding-bottom: 0.5rem; +} + +/* Gallery Item */ +.gallery-item { + flex: 0 0 280px; + scroll-snap-align: start; + scroll-snap-stop: always; +} + +.gallery-item-link { + display: block; + padding: 1rem; + background-color: var(--card-background-color, var(--message-background-color)); + border: 1px solid var(--border-color); + border-radius: 12px; + text-decoration: none; + color: var(--text-color); + transition: all 0.2s ease-in-out; + height: 100%; + min-height: 120px; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); +} + +.gallery-item-link:hover { + transform: translateY(-2px); + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15); + border-color: var(--button-background-color); +} + +.gallery-item-header { + display: flex; + align-items: center; + gap: 0.5rem; + margin-bottom: 0.5rem; +} + +.gallery-item-header .favourite-icon { + color: var(--star-color); + font-size: 1rem; +} + +.gallery-item-code { + font-size: 0.85rem; + color: var(--subtitle-color); + font-weight: 500; +} + +.gallery-item-name { + font-size: 1rem; + font-weight: 600; + margin-bottom: 0.75rem; + line-height: 1.3; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; + min-height: 2.6em; +} + +.gallery-item-lines { + display: flex; + flex-wrap: wrap; + gap: 0.25rem; + align-items: center; +} + +.more-lines { + font-size: 0.75rem; + color: var(--subtitle-color); + font-weight: 500; + padding: 0.125rem 0.375rem; + background-color: var(--border-color); + border-radius: 4px; +} + +/* Gallery Indicators */ +.gallery-indicators { + display: flex; + justify-content: center; + gap: 0.5rem; + margin-top: 1rem; + padding: 0.5rem 0; +} + +.gallery-indicator { + width: 8px; + height: 8px; + border-radius: 50%; + background-color: var(--border-color); + transition: background-color 0.2s ease-in-out; +} + +.gallery-indicator.active { + background-color: var(--button-background-color); +} + +/* Tablet and larger */ +@media (min-width: 768px) { + .gallery-item { + flex: 0 0 320px; + } + + .gallery-scroll-container { + margin: 0; + padding: 0; + } +} + +/* Desktop */ +@media (min-width: 1024px) { + .gallery-item { + flex: 0 0 340px; + } +} diff --git a/src/frontend/app/components/StopGallery.tsx b/src/frontend/app/components/StopGallery.tsx new file mode 100644 index 0000000..7dda637 --- /dev/null +++ b/src/frontend/app/components/StopGallery.tsx @@ -0,0 +1,58 @@ +import React, { useRef } from "react"; +import { motion, useMotionValue } from "framer-motion"; +import { type Stop } from "../data/StopDataProvider"; +import StopGalleryItem from "./StopGalleryItem"; +import "./StopGallery.css"; + +interface StopGalleryProps { + stops: Stop[]; + title: string; + emptyMessage?: string; +} + +const StopGallery: React.FC<StopGalleryProps> = ({ stops, title, emptyMessage }) => { + const scrollRef = useRef<HTMLDivElement>(null); + const x = useMotionValue(0); + + if (stops.length === 0 && emptyMessage) { + return ( + <div className="gallery-container"> + <h2 className="page-subtitle">{title}</h2> + <p className="message">{emptyMessage}</p> + </div> + ); + } + + if (stops.length === 0) { + return null; + } + + return ( + <div className="gallery-container"> + <div className="gallery-header"> + <h2 className="page-subtitle">{title}</h2> + <span className="gallery-counter">{stops.length}</span> + </div> + + <motion.div + className="gallery-scroll-container" + ref={scrollRef} + style={{ x }} + > + <div className="gallery-track"> + {stops.map((stop) => ( + <StopGalleryItem key={stop.stopId} stop={stop} /> + ))} + </div> + </motion.div> + + <div className="gallery-indicators"> + {stops.map((_, index) => ( + <div key={index} className="gallery-indicator" /> + ))} + </div> + </div> + ); +}; + +export default StopGallery; diff --git a/src/frontend/app/components/StopGalleryItem.tsx b/src/frontend/app/components/StopGalleryItem.tsx new file mode 100644 index 0000000..24d92a2 --- /dev/null +++ b/src/frontend/app/components/StopGalleryItem.tsx @@ -0,0 +1,38 @@ +import React from "react"; +import { Link } from "react-router"; +import { type Stop } from "../data/StopDataProvider"; +import LineIcon from "./LineIcon"; +import { useApp } from "../AppContext"; +import StopDataProvider from "../data/StopDataProvider"; + +interface StopGalleryItemProps { + stop: Stop; +} + +const StopGalleryItem: React.FC<StopGalleryItemProps> = ({ stop }) => { + const { region } = useApp(); + + return ( + <div className="gallery-item"> + <Link className="gallery-item-link" to={`/estimates/${stop.stopId}`}> + <div className="gallery-item-header"> + {stop.favourite && <span className="favourite-icon">★</span>} + <span className="gallery-item-code">({stop.stopId})</span> + </div> + <div className="gallery-item-name"> + {StopDataProvider.getDisplayName(region, stop)} + </div> + <div className="gallery-item-lines"> + {stop.lines?.slice(0, 3).map((line) => ( + <LineIcon key={line} line={line} region={region} /> + ))} + {stop.lines && stop.lines.length > 3 && ( + <span className="more-lines">+{stop.lines.length - 3}</span> + )} + </div> + </Link> + </div> + ); +}; + +export default StopGalleryItem; |
