aboutsummaryrefslogtreecommitdiff
path: root/src/frontend/app/components/ServiceAlerts.tsx
diff options
context:
space:
mode:
authorAriel Costas Guerrero <ariel@costas.dev>2026-04-02 12:38:10 +0200
committerAriel Costas Guerrero <ariel@costas.dev>2026-04-02 12:45:33 +0200
commit1b4f4a674ac533c0b51260ba35ab91dd2cf9486d (patch)
tree9fdaf418bef86c51737bcf203483089c9e2b908b /src/frontend/app/components/ServiceAlerts.tsx
parent749e04d6fc2304bb29920db297d1fa4d73b57648 (diff)
Basic push notification system for service alerts
Co-authored-by: Copilot <copilot@github.com>
Diffstat (limited to 'src/frontend/app/components/ServiceAlerts.tsx')
-rw-r--r--src/frontend/app/components/ServiceAlerts.tsx115
1 files changed, 105 insertions, 10 deletions
diff --git a/src/frontend/app/components/ServiceAlerts.tsx b/src/frontend/app/components/ServiceAlerts.tsx
index a6a1ee8..168ea83 100644
--- a/src/frontend/app/components/ServiceAlerts.tsx
+++ b/src/frontend/app/components/ServiceAlerts.tsx
@@ -1,22 +1,117 @@
+import { useQuery } from "@tanstack/react-query";
import React from "react";
import { useTranslation } from "react-i18next";
import "./ServiceAlerts.css";
-const ServiceAlerts: React.FC = () => {
- const { t } = useTranslation();
+interface ServiceAlert {
+ id: string;
+ version: number;
+ phase: string;
+ cause: string;
+ effect: string;
+ header: Record<string, string>;
+ description: Record<string, string>;
+ selectors: string[];
+ infoUrls: string[];
+ eventStartDate: string;
+ eventEndDate: string;
+}
+
+/** Maps an alert effect to one of the three CSS severity classes. */
+function effectToSeverity(effect: string): "info" | "warning" | "error" {
+ if (["NoService", "SignificantDelays", "AccessibilityIssue"].includes(effect))
+ return "error";
+ if (
+ ["ReducedService", "Detour", "ModifiedService", "StopMoved"].includes(
+ effect
+ )
+ )
+ return "warning";
+ return "info";
+}
+
+/** Maps an effect to an emoji icon. */
+function effectToIcon(effect: string): string {
+ const map: Record<string, string> = {
+ NoService: "đŸšĢ",
+ ReducedService: "âš ī¸",
+ SignificantDelays: "🕐",
+ Detour: "â†Šī¸",
+ AdditionalService: "➕",
+ ModifiedService: "🔄",
+ StopMoved: "📍",
+ AccessibilityIssue: "â™ŋ",
+ };
+ return map[effect] ?? "â„šī¸";
+}
+
+interface ServiceAlertsProps {
+ /** If provided, only alerts whose selectors overlap with this list are shown. */
+ selectorFilter?: string[];
+}
+
+const ServiceAlerts: React.FC<ServiceAlertsProps> = ({ selectorFilter }) => {
+ const { t, i18n } = useTranslation();
+ const lang = i18n.language.slice(0, 2);
+
+ const { data: alerts, isLoading } = useQuery<ServiceAlert[]>({
+ queryKey: ["service-alerts"],
+ queryFn: () => fetch("/api/alerts").then((r) => r.json()),
+ staleTime: 5 * 60 * 1000,
+ retry: false,
+ });
+
+ if (isLoading || !alerts) return null;
+
+ const visible = alerts.filter((alert) => {
+ if (!selectorFilter || selectorFilter.length === 0) return true;
+ return alert.selectors.some((s) => selectorFilter.includes(s));
+ });
+
+ if (visible.length === 0) return null;
return (
<div className="service-alerts-container stoplist-section">
<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")}
+ {visible.map((alert) => {
+ const severity = effectToSeverity(alert.effect);
+ const icon = effectToIcon(alert.effect);
+ const title =
+ alert.header[lang] ??
+ alert.header["es"] ??
+ Object.values(alert.header)[0] ??
+ "";
+ const body =
+ alert.description[lang] ??
+ alert.description["es"] ??
+ Object.values(alert.description)[0] ??
+ "";
+
+ return (
+ <div key={alert.id} className={`service-alert ${severity}`}>
+ <div className="alert-icon">{icon}</div>
+ <div className="alert-content">
+ <div className="alert-title">{title}</div>
+ {body && <div className="alert-message">{body}</div>}
+ {alert.infoUrls.length > 0 && (
+ <div className="alert-message" style={{ marginTop: "0.25rem" }}>
+ {alert.infoUrls.map((url, i) => (
+ <a
+ key={i}
+ href={url}
+ target="_blank"
+ rel="noopener noreferrer"
+ style={{ display: "block" }}
+ >
+ {url}
+ </a>
+ ))}
+ </div>
+ )}
+ </div>
</div>
- </div>
- </div>
+ );
+ })}
</div>
);
};