diff options
| author | Ariel Costas Guerrero <ariel@costas.dev> | 2025-12-08 00:33:16 +0100 |
|---|---|---|
| committer | Ariel Costas Guerrero <ariel@costas.dev> | 2025-12-08 00:33:27 +0100 |
| commit | 417930355fbe6089536c60c3ffba75c8691ca581 (patch) | |
| tree | e6f9a1abd1b29b9690944385a7d4011782abea01 /src/frontend/app/components | |
| parent | 579f61a84c351e8c2e0f1e3962d1969541ca39fa (diff) | |
feat: add StopHelpModal component and integrate help functionality in Estimates
Diffstat (limited to 'src/frontend/app/components')
| -rw-r--r-- | src/frontend/app/components/StopHelpModal.tsx | 153 | ||||
| -rw-r--r-- | src/frontend/app/components/Stops/ConsolidatedCirculationCard.tsx | 60 |
2 files changed, 199 insertions, 14 deletions
diff --git a/src/frontend/app/components/StopHelpModal.tsx b/src/frontend/app/components/StopHelpModal.tsx new file mode 100644 index 0000000..87e02f9 --- /dev/null +++ b/src/frontend/app/components/StopHelpModal.tsx @@ -0,0 +1,153 @@ +import { AlertTriangle, Clock, LocateIcon } from "lucide-react"; +import React from "react"; +import { useTranslation } from "react-i18next"; +import { Sheet } from "react-modal-sheet"; + +interface StopHelpModalProps { + isOpen: boolean; + onClose: () => void; +} + +export const StopHelpModal: React.FC<StopHelpModalProps> = ({ + isOpen, + onClose, +}) => { + const { t } = useTranslation(); + + return ( + <Sheet isOpen={isOpen} onClose={onClose} detent="content"> + <Sheet.Container className="bg-white! dark:bg-black! !rounded-t-[20px]"> + <Sheet.Header className="bg-white! dark:bg-black! !rounded-t-[20px]" /> + <Sheet.Content> + <div className="p-6 pb-10 flex flex-col gap-8 overflow-y-auto max-h-[80vh] text-slate-900 dark:text-slate-100"> + <div> + <h2 className="text-xl font-bold mb-4"> + {t("stop_help.title")} + </h2> + + <div className="space-y-5"> + <div className="flex gap-4 items-start"> + <div className="w-10 h-10 rounded-full bg-green-600/20 flex items-center justify-center shrink-0 mt-0.5"> + <Clock className="w-6 h-6 text-green-700 dark:text-green-400" /> + </div> + <div> + <h3 className="font-semibold text-green-700 dark:text-green-400 text-base"> + {t("stop_help.realtime_ok")} + </h3> + <p className="text-sm text-slate-600 dark:text-slate-400 leading-relaxed"> + {t("stop_help.realtime_ok_desc")} + </p> + </div> + </div> + + <div className="flex gap-4 items-start"> + <div className="w-10 h-10 rounded-full bg-orange-600/20 flex items-center justify-center shrink-0 mt-0.5"> + <AlertTriangle className="w-6 h-6 text-orange-700 dark:text-orange-400" /> + </div> + <div> + <h3 className="font-semibold text-orange-700 dark:text-orange-400 text-base"> + {t("stop_help.realtime_warning")} + </h3> + <p className="text-sm text-slate-600 dark:text-slate-400 leading-relaxed"> + {t("stop_help.realtime_warning_desc")} + </p> + </div> + </div> + + <div className="flex gap-4 items-start"> + <div className="w-10 h-10 rounded-full bg-blue-900/20 flex items-center justify-center shrink-0 mt-0.5"> + <Clock className="w-6 h-6 text-blue-900 dark:text-blue-400" /> + </div> + <div> + <h3 className="font-semibold text-blue-900 dark:text-blue-400 text-base"> + {t("stop_help.scheduled")} + </h3> + <p className="text-sm text-slate-600 dark:text-slate-400 leading-relaxed"> + {t("stop_help.scheduled_desc")} + </p> + </div> + </div> + + <div className="flex gap-4 items-start"> + <div className="w-10 h-10 rounded-full bg-slate-100 dark:bg-slate-800 flex items-center justify-center shrink-0 mt-0.5"> + <LocateIcon className="w-6 h-6 text-slate-700 dark:text-slate-300" /> + </div> + <div> + <h3 className="font-semibold text-slate-900 dark:text-slate-100 text-base"> + {t("stop_help.gps")} + </h3> + <p className="text-sm text-slate-600 dark:text-slate-400 leading-relaxed"> + {t("stop_help.gps_desc")} + </p> + </div> + </div> + </div> + </div> + + <div> + <h2 className="text-lg font-bold mb-4"> + {t("stop_help.punctuality")} + </h2> + + <div className="space-y-3"> + <div className="flex items-center gap-3"> + <span className="px-2 py-0.5 rounded-full text-xs font-medium bg-green-600/20 dark:bg-green-600/30 text-green-700 dark:text-green-300 shrink-0"> + {t("stop_help.punctuality_ontime_label", "En hora")} + </span> + <p className="text-sm text-slate-600 dark:text-slate-400"> + {t("stop_help.punctuality_ontime")} + </p> + </div> + + <div className="flex items-center gap-3"> + <span className="px-2 py-0.5 rounded-full text-xs font-medium bg-blue-400/20 dark:bg-blue-600/30 text-blue-700 dark:text-blue-300 shrink-0"> + {t("stop_help.punctuality_early_label", "Adelanto")} + </span> + <p className="text-sm text-slate-600 dark:text-slate-400"> + {t("stop_help.punctuality_early")} + </p> + </div> + + <div className="flex items-center gap-3"> + <span className="px-2 py-0.5 rounded-full text-xs font-medium bg-amber-600/20 dark:bg-yellow-600/30 text-amber-700 dark:text-yellow-300 shrink-0"> + {t("stop_help.punctuality_late_label", "Retraso")} + </span> + <p className="text-sm text-slate-600 dark:text-slate-400"> + {t("stop_help.punctuality_late")} + </p> + </div> + </div> + </div> + + <div> + <h2 className="text-lg font-bold mb-4"> + {t("stop_help.gps_quality")} + </h2> + + <div className="space-y-3"> + <div className="flex items-center gap-3"> + <span className="px-2 py-0.5 rounded-full text-xs font-medium shrink-0"> + {t("stop_help.gps_reliable_label", "GPS fiable")} + </span> + <p className="text-sm text-slate-600 dark:text-slate-400"> + {t("stop_help.gps_reliable")} + </p> + </div> + + <div className="flex items-center gap-3"> + <span className="px-2 py-0.5 rounded-full text-xs font-medium shrink-0"> + {t("stop_help.gps_imprecise_label", "GPS impreciso")} + </span> + <p className="text-sm text-slate-600 dark:text-slate-400"> + {t("stop_help.gps_imprecise")} + </p> + </div> + </div> + </div> + </div> + </Sheet.Content> + </Sheet.Container> + <Sheet.Backdrop onTap={onClose} /> + </Sheet> + ); +}; diff --git a/src/frontend/app/components/Stops/ConsolidatedCirculationCard.tsx b/src/frontend/app/components/Stops/ConsolidatedCirculationCard.tsx index 8a2eb94..635c0ce 100644 --- a/src/frontend/app/components/Stops/ConsolidatedCirculationCard.tsx +++ b/src/frontend/app/components/Stops/ConsolidatedCirculationCard.tsx @@ -4,6 +4,7 @@ import { useTranslation } from "react-i18next"; import LineIcon from "~components/LineIcon"; import { type ConsolidatedCirculation } from "~routes/stops-$id"; +import { AlertTriangle, LocateIcon } from "lucide-react"; import "./ConsolidatedCirculationCard.css"; interface ConsolidatedCirculationCardProps { @@ -119,8 +120,9 @@ export const ConsolidatedCirculationCard: React.FC< // On time if (delta === 0) { return { - label: reduced ? "OK" : t("estimates.delay_on_time", "En hora (0 min)"), + label: reduced ? "OK" : t("estimates.delay_on_time"), tone: "delay-ok", + kind: "delay", } as const; } @@ -129,40 +131,71 @@ export const ConsolidatedCirculationCard: React.FC< const tone = delta <= 2 ? "delay-ok" : delta <= 10 ? "delay-warn" : "delay-critical"; return { - label: reduced ? `R${delta}` : t("estimates.delay_positive", "Retraso de {{minutes}} min", { + label: reduced ? `R${delta}` : t("estimates.delay_positive", { minutes: delta, }), tone, + kind: "delay", } as const; } // Early const tone = absDelta <= 2 ? "delay-ok" : "delay-early"; return { - label: reduced ? `A${absDelta}` : t("estimates.delay_negative", "Adelanto de {{minutes}} min", { + label: reduced ? `A${absDelta}` : t("estimates.delay_negative", { minutes: absDelta, }), tone, + kind: "delay", } as const; }, [estimate.schedule, estimate.realTime, t, reduced]); const metaChips = useMemo(() => { - const chips: Array<{ label: string; tone?: string }> = []; + const chips: Array<{ label: string; tone?: string, kind?: "regular" | "gps" | "delay" | "warning" }> = []; + if (delayChip) { chips.push(delayChip); } + if (estimate.schedule) { chips.push({ label: `${parseServiceId(estimate.schedule.serviceId)} · ${getTripIdDisplay( estimate.schedule.tripId )}`, + kind: "regular", }); } + if (estimate.realTime && estimate.realTime.distance >= 0) { - chips.push({ label: formatDistance(estimate.realTime.distance) }); + chips.push({ label: formatDistance(estimate.realTime.distance), kind: "regular" }); + } + + if (estimate.currentPosition) { + if (estimate.isPreviousTrip) { + chips.push({ label: t("estimates.previous_trip"), kind: "gps" }); + } else { + chips.push({ label: t("estimates.bus_gps_position"), kind: "gps" }); + } + } + + if (timeClass === "time-delayed") { + chips.push({ + label: reduced ? "!" : t("estimates.low_accuracy"), + tone: "warning", + kind: "warning", + }); } + + if (timeClass === "time-scheduled") { + chips.push({ + label: reduced ? "⧗" : t("estimates.no_realtime"), + tone: "warning", + kind: "warning", + }); + } + return chips; - }, [delayChip, estimate.schedule, estimate.realTime]); + }, [delayChip, estimate.schedule, estimate.realTime, timeClass, t, reduced]); // Check if bus has GPS position (live tracking) const hasGpsPosition = !!estimate.currentPosition; @@ -218,6 +251,9 @@ export const ConsolidatedCirculationCard: React.FC< case "delay-early": chipColourClasses = "bg-blue-400/20 dark:bg-blue-600/30 text-blue-700 dark:text-blue-300"; break; + case "warning": + chipColourClasses = "bg-orange-400/20 dark:bg-orange-600/30 text-orange-700 dark:text-orange-300"; + break; default: chipColourClasses = "bg-black/[0.06] dark:bg-white/[0.12] text-[var(--text-color)]"; } @@ -227,6 +263,8 @@ export const ConsolidatedCirculationCard: React.FC< key={`${chip.label}-${idx}`} className={`text-xs px-2 py-0.5 rounded-full flex items-center justify-center gap-1 shrink-0 ${chipColourClasses}`} > + {chip.kind === "gps" && (<LocateIcon className="w-3 h-3 inline-block" />)} + {chip.kind === "warning" && (<AlertTriangle className="w-3 h-3 inline-block" />)} {chip.label} </span> ); @@ -284,14 +322,6 @@ export const ConsolidatedCirculationCard: React.FC< ); })()} </div> - {hasGpsPosition && ( - <div className="gps-indicator" title="Live GPS tracking"> - <span - className={`gps-pulse ${estimate.isPreviousTrip ? "previous-trip" : "" - }`} - /> - </div> - )} <div className={`eta-badge ${timeClass}`}> <div className="eta-text"> <span className="eta-value">{etaValue}</span> @@ -307,6 +337,8 @@ export const ConsolidatedCirculationCard: React.FC< key={`${chip.label}-${idx}`} className={`meta-chip ${chip.tone ?? ""}`.trim()} > + {chip.kind === "gps" && (<LocateIcon className="w-3 h-3 inline-block" />)} + {chip.kind === "warning" && (<AlertTriangle className="w-3 h-3 inline-block" />)} {chip.label} </span> ))} |
