diff --git a/frontend/package.json b/frontend/package.json index 6253869a7..bb4173b90 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -41,6 +41,7 @@ "dependencies": { "@azure/msal-browser": "^2.19.0", "@azure/msal-react": "^1.1.1", + "@date-fns/tz": "^1.2.0", "@fortawesome/fontawesome-free": "^6.5.1", "@fortawesome/fontawesome-svg-core": "^6.5.1", "@fortawesome/free-brands-svg-icons": "^6.5.1", @@ -74,7 +75,7 @@ "comlink": "^4.4.1", "customize-cra": "^1.0.0", "d3-fetch": "^1.1.2", - "date-fns": "^2.28.0", + "date-fns": "^4.1.0", "domhandler": "^4.2.2", "dompurify": "^2.5.4", "enzyme": "^3.11.0", diff --git a/frontend/src/components/MapView/DateSelector/TimelineItems/AAStormTooltipContent/index.tsx b/frontend/src/components/MapView/DateSelector/TimelineItems/AAStormTooltipContent/index.tsx index 7d459944e..7d394d980 100644 --- a/frontend/src/components/MapView/DateSelector/TimelineItems/AAStormTooltipContent/index.tsx +++ b/frontend/src/components/MapView/DateSelector/TimelineItems/AAStormTooltipContent/index.tsx @@ -47,7 +47,7 @@ function AAStormTooltipContent({ date }: AAStormTooltipContentProps) { > {windStates.states.map(item => { const itemDate = new Date(item.ref_time); - const formattedItemTime = formatInUTC(itemDate, 'h aaa'); + const formattedItemTime = formatInUTC(itemDate, "haaa 'UTC'"); return ( fontWeight: 400, lineHeight: '15px', color: '#101010', + whiteSpace: 'nowrap', }, toggleButton: { padding: '6px 6px', diff --git a/frontend/src/components/MapView/DateSelector/TimelineItems/TimelineLabel/index.test.tsx b/frontend/src/components/MapView/DateSelector/TimelineItems/TimelineLabel/index.test.tsx index 0925162ee..613589fbc 100644 --- a/frontend/src/components/MapView/DateSelector/TimelineItems/TimelineLabel/index.test.tsx +++ b/frontend/src/components/MapView/DateSelector/TimelineItems/TimelineLabel/index.test.tsx @@ -1,5 +1,5 @@ import { render } from '@testing-library/react'; -import TimelineItem, { TimelineLabelProps } from '.'; +import TimelineLabel, { TimelineLabelProps } from '.'; test('TimelineLabel renders as expected', () => { // Arrange @@ -15,7 +15,7 @@ test('TimelineLabel renders as expected', () => { }; // Act - const { container } = render(); + const { container } = render(); // Assert expect(container).toMatchSnapshot(); diff --git a/frontend/src/components/MapView/Layers/AnticipatoryActionStormLayer/AAStormLandfallPopup/PopupContent/__snapshots__/index.test.tsx.snap b/frontend/src/components/MapView/Layers/AnticipatoryActionStormLayer/AAStormLandfallPopup/PopupContent/__snapshots__/index.test.tsx.snap index 4f00b9778..6d8c1860d 100644 --- a/frontend/src/components/MapView/Layers/AnticipatoryActionStormLayer/AAStormLandfallPopup/PopupContent/__snapshots__/index.test.tsx.snap +++ b/frontend/src/components/MapView/Layers/AnticipatoryActionStormLayer/AAStormLandfallPopup/PopupContent/__snapshots__/index.test.tsx.snap @@ -10,7 +10,7 @@ exports[`AAStormLandfallPopup component renders as expected 1`] = ` variant="body1" > Report date: - 2024-03-01 6pm + 2024-03-01 8pm (GMT+2)
- 2024-03-12 01:00 + 2024-03-12 03:00 GMT+2 diff --git a/frontend/src/components/MapView/Layers/AnticipatoryActionStormLayer/utils.test.ts b/frontend/src/components/MapView/Layers/AnticipatoryActionStormLayer/utils.test.ts index 2501b25d5..b1476b23c 100644 --- a/frontend/src/components/MapView/Layers/AnticipatoryActionStormLayer/utils.test.ts +++ b/frontend/src/components/MapView/Layers/AnticipatoryActionStormLayer/utils.test.ts @@ -1,6 +1,8 @@ import { formatInUTC, + formatLandfallDate, formatLandfallEstimatedLeadtime, + formatReportDate, getDateInUTC, isDateSameAsCurrentDate, } from './utils'; @@ -48,6 +50,44 @@ describe('utils', () => { }); }); + describe('formatLandfallDate()', () => { + const tests = [ + { + dateRange: ['2024-03-12 00:00:00', '2024-03-12 06:00:00'], + expected: '2024-03-12 02:00 GMT+2', + }, + { + dateRange: ['2024-03-12 23:00:00', '2024-03-12 06:00:00'], + expected: '2024-03-13 01:00 GMT+2', + }, + ]; + it.each(tests)( + 'returns landfall estimated date in local time', + ({ dateRange, expected }) => { + expect(formatLandfallDate(dateRange)).toBe(expected); + }, + ); + }); + + describe('formatReportDate()', () => { + const tests = [ + { + date: '2024-03-12 00:00:00', + expected: '2024-03-12 2am (GMT+2)', + }, + { + date: '2024-03-12 23:00:00', + expected: '2024-03-13 1am (GMT+2)', + }, + ]; + it.each(tests)( + 'returns report date in local time', + ({ date, expected }) => { + expect(formatReportDate(date)).toBe(expected); + }, + ); + }); + describe('formatLandfallEstimatedLeadtime()', () => { const tests = [ { diff --git a/frontend/src/components/MapView/Layers/AnticipatoryActionStormLayer/utils.ts b/frontend/src/components/MapView/Layers/AnticipatoryActionStormLayer/utils.ts index 4a49a6905..9323c5ff4 100644 --- a/frontend/src/components/MapView/Layers/AnticipatoryActionStormLayer/utils.ts +++ b/frontend/src/components/MapView/Layers/AnticipatoryActionStormLayer/utils.ts @@ -2,14 +2,9 @@ import { AAStormTimeSeriesFeature, TimeSerieFeatureProperty, } from 'context/anticipatoryAction/AAStormStateSlice/rawStormDataTypes'; -import { - isSameDay, - parseJSON, - format, - addHours, - differenceInHours, -} from 'date-fns'; +import { isSameDay, parseJSON, format, differenceInHours } from 'date-fns'; import { MapGeoJSONFeature } from 'maplibre-gl'; +import { TZDate } from '@date-fns/tz'; export function getDateInUTC( time: string | undefined, @@ -42,15 +37,25 @@ export function formatReportDate(date: string) { return ''; } - return formatInUTC(parsedDate, 'yyy-MM-dd Kaaa'); + return formatInLocalTime(parsedDate, 'yyy-MM-dd Kaaa (O)'); } -export function formatInUTC(dateInUTC: Date, fmt: string) { - const localTimeZone = new Date().getTimezoneOffset(); // tz in minutes positive or negative - const hoursToAddOrRemove = Math.round(localTimeZone / 60); - const shiftedDate = addHours(dateInUTC, hoursToAddOrRemove); +export function formatInUTC(date: Date, fmt: string) { + const dateInUTC = new TZDate(date, 'Universal'); + + return format(dateInUTC, fmt); +} + +/* + * Format a date to local time + * note: So far, the storm Anticipatory Action module is only used by countries using the mozambic time (namely Mozambic and Zimbabwe). + * When additional countries will need to access this module, this function will have to be revisited + */ + +function formatInLocalTime(date: Date, fmt: string): string { + const dateInLocalTime = new TZDate(date, 'Africa/Blantyre'); - return format(shiftedDate, fmt); + return format(dateInLocalTime, fmt); } export function formatLandfallDate(dateRange: string[]) { @@ -60,7 +65,7 @@ export function formatLandfallDate(dateRange: string[]) { return ''; } - return formatInUTC(parsedDate, 'yyy-MM-dd HH:mm'); + return formatInLocalTime(parsedDate, 'yyy-MM-dd HH:mm O'); } export function formatLandfallTimeRange(dateRange: string[]) { @@ -108,7 +113,7 @@ export function formatWindPointDate(time: string) { return ''; } - return formatInUTC(dateInUTC, 'dd - Kaaa'); + return formatInLocalTime(dateInUTC, 'dd - Kaaa (O)'); } export function parseGeoJsonFeature( diff --git a/frontend/src/components/MapView/LeftPanel/AnticipatoryActionPanel/AnticipatoryActionStormPanel/index.tsx b/frontend/src/components/MapView/LeftPanel/AnticipatoryActionPanel/AnticipatoryActionStormPanel/index.tsx index df9b533b5..b16143a6b 100644 --- a/frontend/src/components/MapView/LeftPanel/AnticipatoryActionPanel/AnticipatoryActionStormPanel/index.tsx +++ b/frontend/src/components/MapView/LeftPanel/AnticipatoryActionPanel/AnticipatoryActionStormPanel/index.tsx @@ -62,7 +62,7 @@ function AnticipatoryActionStormPanel() { <> CYCLONE{' '} {t(AAData.forecastDetails?.cyclone_name || 'Unknown Cyclone')}{' '} - {hour} + {hour} UTC
{date} FORECAST diff --git a/frontend/src/i18n.ts b/frontend/src/i18n.ts index ab7eb50b2..fe91bf4f1 100644 --- a/frontend/src/i18n.ts +++ b/frontend/src/i18n.ts @@ -2,13 +2,7 @@ import { merge } from 'lodash'; import i18n from 'i18next'; import { initReactI18next, useTranslation } from 'react-i18next'; import { registerLocale } from 'react-datepicker'; -import en from 'date-fns/locale/en-US'; -import fr from 'date-fns/locale/fr'; -import km from 'date-fns/locale/km'; -import pt from 'date-fns/locale/pt'; -import es from 'date-fns/locale/es'; -import ru from 'date-fns/locale/ru'; -import mn from 'date-fns/locale/mn'; +import { fr, km, pt, es, ru, mn, enUS } from 'date-fns/locale'; import { translation } from './config'; @@ -16,7 +10,7 @@ const TRANSLATION_DEBUG = false; // Register other date locales to be used by our DatePicker // TODO - extract registerLocale imports and loading into a separate file for clarity. // Test for missing locales -registerLocale('en', en); +registerLocale('en', enUS); registerLocale('fr', fr); registerLocale('km', km); registerLocale('pt', pt); @@ -123,7 +117,7 @@ export function isEnglishLanguageSelected(lang: typeof i18n): boolean { } export const locales = { - en, + en: enUS, fr, km, pt, diff --git a/frontend/yarn.lock b/frontend/yarn.lock index ad002b279..21f73b089 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -2058,6 +2058,11 @@ dependencies: "@jridgewell/trace-mapping" "0.3.9" +"@date-fns/tz@^1.2.0": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@date-fns/tz/-/tz-1.2.0.tgz#81cb3211693830babaf3b96aff51607e143030a6" + integrity sha512-LBrd7MiJZ9McsOgxqWX7AaxrDjcFVjWH/tIKJd7pnR7McaslGYOP1QmmiBXdJH/H/yLCT+rcQ7FaPBUxRGUtrg== + "@emotion/hash@^0.8.0": version "0.8.0" resolved "https://registry.yarnpkg.com/@emotion/hash/-/hash-0.8.0.tgz#bbbff68978fefdbe68ccb533bc8cbe1d1afb5413" @@ -6897,13 +6902,18 @@ data-view-byte-offset@^1.0.0: es-errors "^1.3.0" is-data-view "^1.0.1" -date-fns@^2.0.1, date-fns@^2.28.0: +date-fns@^2.0.1: version "2.30.0" resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.30.0.tgz#f367e644839ff57894ec6ac480de40cae4b0f4d0" integrity sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw== dependencies: "@babel/runtime" "^7.21.0" +date-fns@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-4.1.0.tgz#64b3d83fff5aa80438f5b1a633c2e83b8a1c2d14" + integrity sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg== + debug@4, debug@^4.1.0, debug@^4.1.1, debug@^4.2.0, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4: version "4.3.4" resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865"