aboutsummaryrefslogtreecommitdiff
path: root/src/frontend/app/components
diff options
context:
space:
mode:
Diffstat (limited to 'src/frontend/app/components')
-rw-r--r--src/frontend/app/components/ServiceAlerts.css72
-rw-r--r--src/frontend/app/components/ServiceAlerts.tsx22
-rw-r--r--src/frontend/app/components/StopGallery.css161
-rw-r--r--src/frontend/app/components/StopGallery.tsx58
-rw-r--r--src/frontend/app/components/StopGalleryItem.tsx38
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;