diff --git a/package.json b/package.json index 6d53d03d..386591d2 100644 --- a/package.json +++ b/package.json @@ -53,6 +53,7 @@ "next-codegrid": "^1.0.3", "next-cookies": "^2.0.3", "next-pwa": "^5.2.21", + "opening_hours": "^3.8.0", "osm-auth": "^2.4.0", "react": "^18.2.0", "react-custom-scrollbars": "^4.2.1", @@ -60,7 +61,6 @@ "react-jss": "^10.6.0", "react-split-pane": "^0.1.92", "react-zoom-pan-pinch": "^3.3.0", - "simple-opening-hours": "^0.1.1", "styled-components": "^6.1.11", "styled-jsx": "^3.4.4" }, diff --git a/src/components/FeaturePanel/renderers/OpeningHoursRenderer.tsx b/src/components/FeaturePanel/renderers/OpeningHoursRenderer.tsx index 49779d31..74775063 100644 --- a/src/components/FeaturePanel/renderers/OpeningHoursRenderer.tsx +++ b/src/components/FeaturePanel/renderers/OpeningHoursRenderer.tsx @@ -1,29 +1,12 @@ import React from 'react'; -import { SimpleOpeningHours } from 'simple-opening-hours'; import styled from 'styled-components'; import AccessTime from '@mui/icons-material/AccessTime'; import { useToggleState } from '../../helpers'; import { t } from '../../../services/intl'; import { ToggleButton } from '../helpers/ToggleButton'; - -interface SimpleOpeningHoursTable { - su: string[]; - mo: string[]; - tu: string[]; - we: string[]; - th: string[]; - fr: string[]; - sa: string[]; - ph: string[]; -} - -const parseOpeningHours = (value) => { - const sanitized = value.match(/^[0-9:]+-[0-9:]+$/) ? `Mo-Su ${value}` : value; - const opening = new SimpleOpeningHours(sanitized); - const daysTable = opening.getTable() as SimpleOpeningHoursTable; - const isOpen = opening.isOpenNow(); - return { daysTable, isOpen }; -}; +import { parseOpeningHours } from './openingHours'; +import { SimpleOpeningHoursTable } from './openingHours/types'; +import { useFeatureContext } from '../../utils/FeatureContext'; const Table = styled.table` margin: 1em; @@ -39,7 +22,7 @@ const Table = styled.table` // const weekDays = ['su', 'mo', 'tu', 'we', 'th', 'fr', 'sa']; const weekDays = t('opening_hours.days_su_mo_tu_we_th_fr_sa').split('|'); -const formatTimes = (times) => +const formatTimes = (times: string[]) => times.length ? times.map((x) => x.replace(/:00/g, '')).join(', ') : '-'; const formatDescription = (isOpen: boolean, days: SimpleOpeningHoursTable) => { @@ -60,7 +43,16 @@ const formatDescription = (isOpen: boolean, days: SimpleOpeningHoursTable) => { const OpeningHoursRenderer = ({ v }) => { const [isExpanded, toggle] = useToggleState(false); - const { daysTable, isOpen } = parseOpeningHours(v); + + const { countryCode, center } = useFeatureContext().feature; + + const openingHours = parseOpeningHours(v, center, { + country_code: countryCode, + state: '', + }); + if (!openingHours) return null; + const { daysTable, isOpen } = openingHours; + const { ph, ...days } = daysTable; const timesByDay = Object.values(days).map((times, idx) => ({ times, diff --git a/src/components/FeaturePanel/renderers/openingHours/complex.ts b/src/components/FeaturePanel/renderers/openingHours/complex.ts new file mode 100644 index 00000000..021e9e08 --- /dev/null +++ b/src/components/FeaturePanel/renderers/openingHours/complex.ts @@ -0,0 +1,62 @@ +import OpeningHours from 'opening_hours'; +import { isInRange } from './utils'; +import { Address, SimpleOpeningHoursTable } from './types'; +import { LonLat } from '../../../../services/types'; + +type Weekday = keyof SimpleOpeningHoursTable; +const weekdays: Weekday[] = ['su', 'mo', 'tu', 'we', 'th', 'fr', 'sa', 'ph']; +const weekdayMappings: Record = { + Sun: 'su', + Mon: 'mo', + Tue: 'tu', + Wed: 'we', + Thu: 'th', + Fri: 'fr', + Sat: 'sa', +}; + +const fmtDate = (d: Date) => + d.toLocaleTimeString(undefined, { hour: 'numeric', minute: 'numeric' }); + +export const parseComplexOpeningHours = ( + value: string, + [lon, lat]: LonLat, + address: Address, +) => { + const oh = new OpeningHours(value, { + lat, + lon, + address, + }); + + const today = new Date(); + today.setHours(0, 0, 0, 0); + + const oneWeekLater = new Date(today); + oneWeekLater.setDate(oneWeekLater.getDate() + 7); + + const intervals = oh.getOpenIntervals(today, oneWeekLater); + const grouped = weekdays.map((w) => { + const daysIntervals = intervals.filter( + ([from]) => + w === weekdayMappings[from.toLocaleString('en', { weekday: 'short' })], + ); + + return [w, daysIntervals] as const; + }); + + const daysTable = Object.fromEntries( + grouped.map((entry) => { + const strings = entry[1].map( + ([from, due]) => `${fmtDate(from)}-${fmtDate(due)}`, + ); + + return [entry[0], strings] as const; + }), + ) as unknown as SimpleOpeningHoursTable; + + return { + daysTable, + isOpen: intervals.some(([from, due]) => isInRange([from, due], new Date())), + }; +}; diff --git a/src/components/FeaturePanel/renderers/openingHours/index.ts b/src/components/FeaturePanel/renderers/openingHours/index.ts new file mode 100644 index 00000000..7880abb5 --- /dev/null +++ b/src/components/FeaturePanel/renderers/openingHours/index.ts @@ -0,0 +1,15 @@ +import { LonLat } from '../../../../services/types'; +import { parseComplexOpeningHours } from './complex'; +import { Address } from './types'; + +export const parseOpeningHours = ( + value: string, + coords: LonLat, + address: Address, +): ReturnType | null => { + try { + return parseComplexOpeningHours(value, coords, address); + } catch { + return null; + } +}; diff --git a/src/components/FeaturePanel/renderers/openingHours/types.ts b/src/components/FeaturePanel/renderers/openingHours/types.ts new file mode 100644 index 00000000..a3c68818 --- /dev/null +++ b/src/components/FeaturePanel/renderers/openingHours/types.ts @@ -0,0 +1,15 @@ +export interface SimpleOpeningHoursTable { + su: string[]; + mo: string[]; + tu: string[]; + we: string[]; + th: string[]; + fr: string[]; + sa: string[]; + ph: string[]; +} + +export type Address = { + country_code: string; + state: string; +}; diff --git a/src/components/FeaturePanel/renderers/openingHours/utils.ts b/src/components/FeaturePanel/renderers/openingHours/utils.ts new file mode 100644 index 00000000..7b6faacd --- /dev/null +++ b/src/components/FeaturePanel/renderers/openingHours/utils.ts @@ -0,0 +1,2 @@ +export const isInRange = ([startDate, endDate]: [Date, Date], date: Date) => + date.getTime() >= startDate.getTime() && date.getTime() <= endDate.getTime(); diff --git a/yarn.lock b/yarn.lock index d27e1107..26244023 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1218,6 +1218,13 @@ dependencies: regenerator-runtime "^0.14.0" +"@babel/runtime@^7.17.2", "@babel/runtime@^7.19.0": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.24.7.tgz#f4f0d5530e8dbdf59b3451b9b3e594b6ba082e12" + integrity sha512-UwgBRMjJP+xv857DCngvqXI3Iq6J4v0wXmwc6sapg+zyhbwmQX67LUEFrkK5tbyJ30jGuG3ZvWpBiB9LCy1kWw== + dependencies: + regenerator-runtime "^0.14.0" + "@babel/template@^7.12.13", "@babel/template@^7.18.10", "@babel/template@^7.20.7": version "7.20.7" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.20.7.tgz#a15090c2839a83b02aa996c0b4994005841fd5a8" @@ -4448,6 +4455,20 @@ hyphenate-style-name@^1.0.3: resolved "https://registry.yarnpkg.com/hyphenate-style-name/-/hyphenate-style-name-1.0.4.tgz#691879af8e220aea5750e8827db4ef62a54e361d" integrity sha512-ygGZLjmXfPHj+ZWh6LwbC37l43MhfztxetbFCoYTM2VjkIUpeHgSNn7QIyVFj7YQ1Wl9Cbw5sholVJPzWvC2MQ== +i18next-browser-languagedetector@^6.1.4: + version "6.1.8" + resolved "https://registry.yarnpkg.com/i18next-browser-languagedetector/-/i18next-browser-languagedetector-6.1.8.tgz#8e9c61b32a4dfe9b959b38bc9d2a8b95f799b27c" + integrity sha512-Svm+MduCElO0Meqpj1kJAriTC6OhI41VhlT/A0UPjGoPZBhAHIaGE5EfsHlTpgdH09UVX7rcc72pSDDBeKSQQA== + dependencies: + "@babel/runtime" "^7.19.0" + +i18next@^21.8.3: + version "21.10.0" + resolved "https://registry.yarnpkg.com/i18next/-/i18next-21.10.0.tgz#85429af55fdca4858345d0e16b584ec29520197d" + integrity sha512-YeuIBmFsGjUfO3qBmMOc0rQaun4mIpGKET5WDwvu8lU7gvwpcariZLNtL0Fzj+zazcHUrlXHiptcFhBMFaxzfg== + dependencies: + "@babel/runtime" "^7.17.2" + iconv-lite@0.4.24: version "0.4.24" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" @@ -6130,6 +6151,15 @@ opencollective-postinstall@^2.0.2: resolved "https://registry.yarnpkg.com/opencollective-postinstall/-/opencollective-postinstall-2.0.3.tgz#7a0fff978f6dbfa4d006238fbac98ed4198c3259" integrity sha512-8AV/sCtuzUeTo8gQK5qDZzARrulB3egtLzFgteqB2tcT4Mw7B8Kt7JcDHmltjz6FOAHsvTevk70gZEbhM4ZS9Q== +opening_hours@^3.8.0: + version "3.8.0" + resolved "https://registry.yarnpkg.com/opening_hours/-/opening_hours-3.8.0.tgz#e6d0a0bfd4e0f2cb8f62321e468dcaa8a798bd79" + integrity sha512-bRJroECQSe/itVcNmC3j9PPicxn/LBowdd1Hi+4Aa7hCswdt7w81WHfUwrEMbtk1BBYmGJEbSepl8oYYPviSuA== + dependencies: + i18next "^21.8.3" + i18next-browser-languagedetector "^6.1.4" + suncalc "^1.9.0" + optionator@^0.8.1: version "0.8.3" resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.3.tgz#84fa1d036fe9d3c7e21d99884b601167ec8fb495" @@ -7006,11 +7036,6 @@ signal-exit@^3.0.2, signal-exit@^3.0.3: resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.3.tgz#a1410c2edd8f077b08b4e253c8eacfcaf057461c" integrity sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA== -simple-opening-hours@^0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/simple-opening-hours/-/simple-opening-hours-0.1.1.tgz#f6059d8aaae63953bbcb4705ffe3250b5bee4d30" - integrity sha512-IbHhKCeh0AMUIdgTTGA6R3ZJIfvnDm/jml7+OCygoH3ZQTmxwTnq2ZwBc1dr9VK0XQbAAeBUBVfIRwGKRzdXhQ== - sisteransi@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/sisteransi/-/sisteransi-1.0.5.tgz#134d681297756437cc05ca01370d3a7a571075ed" @@ -7354,6 +7379,11 @@ stylis@4.3.2: resolved "https://registry.yarnpkg.com/stylis/-/stylis-4.3.2.tgz#8f76b70777dd53eb669c6f58c997bf0a9972e444" integrity sha512-bhtUjWd/z6ltJiQwg0dUfxEJ+W+jdqQd8TbWLWyeIJHlnsqmGLRFFd8e5mA0AZi/zx90smXRlN66YMTcaSFifg== +suncalc@^1.9.0: + version "1.9.0" + resolved "https://registry.yarnpkg.com/suncalc/-/suncalc-1.9.0.tgz#26212353fae61edb287c2d558fc4932ecf0e1532" + integrity sha512-vMJ8Byp1uIPoj+wb9c1AdK4jpkSKVAywgHX0lqY7zt6+EWRRC3Z+0Ucfjy/0yxTVO1hwwchZe4uoFNqrIC24+A== + supercluster@^8.0.1: version "8.0.1" resolved "https://registry.yarnpkg.com/supercluster/-/supercluster-8.0.1.tgz#9946ba123538e9e9ab15de472531f604e7372df5"