feat: add time-dependent Europe/Brussels dashboard greeting
The dashboard greeting was a fully static "Goedemorgen..." regardless of
actual time of day. New frontend/src/i18n/greeting.ts::getGreetingPeriod
is a pure, clock-injectable function resolving one of 4 periods (05:00-
11:59 morning, 12:00-17:59 afternoon, 18:00-22:59 evening, 23:00-04:59
night) against Europe/Brussels wall-clock time via
Intl.DateTimeFormat({ timeZone, hourCycle: "h23" }), which is DST-safe
by construction.
useGreetingPeriod.ts wires this into React with a 30s poll so the
greeting rolls over live while the app stays open, no reload required.
Each period now has its own greeting word and accompanying sentence in
all 3 languages (dashboard.json), replacing both the fixed "Goedemorgen"
and the fixed "Here's the fleet" follow-up sentence. Night never says
"Goedenacht" (used as a farewell, not a welcome, in Dutch).
This commit is contained in:
@@ -0,0 +1,28 @@
|
||||
export type GreetingPeriod = "morning" | "afternoon" | "evening" | "night";
|
||||
|
||||
const BRUSSELS_TIME_ZONE = "Europe/Brussels";
|
||||
|
||||
// DST-safe: Intl.DateTimeFormat resolves the correct Europe/Brussels wall-clock hour for
|
||||
// any instant, automatically accounting for the CET/CEST transition -- no manual UTC
|
||||
// offset math (which would silently break twice a year) is needed. `hourCycle: "h23"`
|
||||
// pins the output to a plain 0-23 range (some engines otherwise render midnight as "24").
|
||||
export function getBrusselsHour(date: Date): number {
|
||||
const formatter = new Intl.DateTimeFormat("en-GB", {
|
||||
timeZone: BRUSSELS_TIME_ZONE,
|
||||
hour: "numeric",
|
||||
hourCycle: "h23",
|
||||
});
|
||||
return Number(formatter.format(date));
|
||||
}
|
||||
|
||||
// Boundaries per the Fleet Ops final localization brief (section 9):
|
||||
// 05:00-11:59 morning, 12:00-17:59 afternoon, 18:00-22:59 evening, 23:00-04:59 night.
|
||||
// `date` defaults to `new Date()` but accepts any Date so callers (and tests) can inject
|
||||
// a fixed clock instead of depending on the real wall clock.
|
||||
export function getGreetingPeriod(date: Date = new Date()): GreetingPeriod {
|
||||
const hour = getBrusselsHour(date);
|
||||
if (hour >= 5 && hour < 12) return "morning";
|
||||
if (hour >= 12 && hour < 18) return "afternoon";
|
||||
if (hour >= 18 && hour < 23) return "evening";
|
||||
return "night";
|
||||
}
|
||||
@@ -1,6 +1,17 @@
|
||||
{
|
||||
"eyebrow": "Operations / Live overview",
|
||||
"title": "Good morning. Here's the fleet.",
|
||||
"greeting": {
|
||||
"morning": "Good morning",
|
||||
"afternoon": "Good afternoon",
|
||||
"evening": "Good evening",
|
||||
"night": "Welcome back"
|
||||
},
|
||||
"greetingBody": {
|
||||
"morning": "Here's today's fleet status.",
|
||||
"afternoon": "Here's the current overview of your fleet.",
|
||||
"evening": "Here's this evening's fleet overview.",
|
||||
"night": "Here's the latest overview of your fleet."
|
||||
},
|
||||
"description": "Readiness, exceptions and hand-offs across today's operation.",
|
||||
"viewFleet": "View fleet",
|
||||
"demoStart": {
|
||||
|
||||
@@ -1,6 +1,17 @@
|
||||
{
|
||||
"eyebrow": "Exploitation / Aperçu en direct",
|
||||
"title": "Bonjour. Voici votre flotte.",
|
||||
"greeting": {
|
||||
"morning": "Bonjour",
|
||||
"afternoon": "Bonjour",
|
||||
"evening": "Bonsoir",
|
||||
"night": "Bon retour"
|
||||
},
|
||||
"greetingBody": {
|
||||
"morning": "Voici l'état de votre flotte pour aujourd'hui.",
|
||||
"afternoon": "Voici l'aperçu actuel de votre flotte.",
|
||||
"evening": "Voici l'aperçu de votre flotte pour ce soir.",
|
||||
"night": "Voici le dernier aperçu de votre flotte."
|
||||
},
|
||||
"description": "Disponibilité, exceptions et transferts de l'activité en cours.",
|
||||
"viewFleet": "Voir la flotte",
|
||||
"demoStart": {
|
||||
|
||||
@@ -1,6 +1,17 @@
|
||||
{
|
||||
"eyebrow": "Uitvoeren / Live overzicht",
|
||||
"title": "Goedemorgen. Hier is je wagenpark.",
|
||||
"greeting": {
|
||||
"morning": "Goedemorgen",
|
||||
"afternoon": "Goedemiddag",
|
||||
"evening": "Goedenavond",
|
||||
"night": "Welkom terug"
|
||||
},
|
||||
"greetingBody": {
|
||||
"morning": "Hier is de status van je wagenpark voor vandaag.",
|
||||
"afternoon": "Hier is het actuele overzicht van je wagenpark.",
|
||||
"evening": "Hier is het overzicht van je wagenpark voor vanavond.",
|
||||
"night": "Hier is het laatste overzicht van je wagenpark."
|
||||
},
|
||||
"description": "Beschikbaarheid, uitzonderingen en overdrachten doorheen de huidige werking.",
|
||||
"viewFleet": "Wagenpark bekijken",
|
||||
"demoStart": {
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { getGreetingPeriod, type GreetingPeriod } from "./greeting";
|
||||
|
||||
// Checked frequently enough that a period rollover (e.g. 11:59 -> 12:00) is picked up
|
||||
// within the Dashboard while the app stays open, without a page reload -- see section 9
|
||||
// of the Fleet Ops final localization brief.
|
||||
const CHECK_INTERVAL_MS = 30_000;
|
||||
|
||||
export function useGreetingPeriod(): GreetingPeriod {
|
||||
const [period, setPeriod] = useState<GreetingPeriod>(() => getGreetingPeriod());
|
||||
|
||||
useEffect(() => {
|
||||
const id = setInterval(() => {
|
||||
setPeriod((current) => {
|
||||
const next = getGreetingPeriod();
|
||||
return next === current ? current : next;
|
||||
});
|
||||
}, CHECK_INTERVAL_MS);
|
||||
return () => clearInterval(id);
|
||||
}, []);
|
||||
|
||||
return period;
|
||||
}
|
||||
Reference in New Issue
Block a user