diff --git a/apps/scandic-web/components/OpeningHours/AlternateOpeningHours/index.tsx b/apps/scandic-web/components/OpeningHours/AlternateOpeningHours/index.tsx
new file mode 100644
index 000000000..f670e40b0
--- /dev/null
+++ b/apps/scandic-web/components/OpeningHours/AlternateOpeningHours/index.tsx
@@ -0,0 +1,56 @@
+import { Typography } from "@scandic-hotels/design-system/Typography"
+
+import { getIntl } from "@/i18n"
+
+import { getGroupedOpeningHours } from "../utils"
+
+import styles from "../openingHours.module.css"
+
+import type { RestaurantOpeningHours } from "@/types/hotel"
+
+interface AlternateOpeningHoursProps {
+ alternateOpeningHours: RestaurantOpeningHours
+}
+
+export default async function AlternateOpeningHours({
+ alternateOpeningHours,
+}: AlternateOpeningHoursProps) {
+ const intl = await getIntl()
+ const groupedAlternateOpeningHours = alternateOpeningHours
+ ? getGroupedOpeningHours(alternateOpeningHours, intl)
+ : null
+
+ // If there are alternate hours but no grouped hours with length, we return the name of the alternate hours
+ if (!groupedAlternateOpeningHours?.length) {
+ return (
+
+ {alternateOpeningHours.name}
+
+ )
+ }
+
+ return (
+ <>
+
+
+ {intl.formatMessage(
+ { id: "Alternate opening hours ({name})" },
+ { name: alternateOpeningHours.name }
+ )}
+
+
+ {groupedAlternateOpeningHours.map((groupedOpeningHour) => (
+
+ {groupedOpeningHour}
+
+ ))}
+ >
+ )
+}
diff --git a/apps/scandic-web/components/OpeningHours/index.tsx b/apps/scandic-web/components/OpeningHours/index.tsx
index 20d76b8d3..5a2a162d5 100644
--- a/apps/scandic-web/components/OpeningHours/index.tsx
+++ b/apps/scandic-web/components/OpeningHours/index.tsx
@@ -2,6 +2,9 @@ import { Typography } from "@scandic-hotels/design-system/Typography"
import { getIntl } from "@/i18n"
+import AlternateOpeningHours from "./AlternateOpeningHours"
+import { getGroupedOpeningHours } from "./utils"
+
import styles from "./openingHours.module.css"
import type { OpeningHoursProps } from "@/types/components/hotelPage/sidepeek/openingHours"
@@ -14,84 +17,7 @@ export default async function OpeningHours({
}: OpeningHoursProps) {
const intl = await getIntl()
- const closedMsg = intl.formatMessage({ id: "Closed" })
- const alwaysOpenMsg = intl.formatMessage({ id: "Always open" })
-
- // In order
- const weekdayDefinitions = [
- {
- key: "monday",
- label: intl.formatMessage({ id: "Monday" }),
- },
- {
- key: "tuesday",
- label: intl.formatMessage({ id: "Tuesday" }),
- },
- {
- key: "wednesday",
- label: intl.formatMessage({ id: "Wednesday" }),
- },
- {
- key: "thursday",
- label: intl.formatMessage({ id: "Thursday" }),
- },
- {
- key: "friday",
- label: intl.formatMessage({ id: "Friday" }),
- },
- {
- key: "saturday",
- label: intl.formatMessage({ id: "Saturday" }),
- },
- {
- key: "sunday",
- label: intl.formatMessage({ id: "Sunday" }),
- },
- ] as const
-
- const groupedOpeningHours: string[] = []
-
- let rangeWeekdays: string[] = []
- let rangeValue = ""
- for (let i = 0, n = weekdayDefinitions.length; i < n; ++i) {
- const weekdayDefinition = weekdayDefinitions[i]
- const weekday = openingHours[weekdayDefinition.key]
- const label = weekdayDefinition.label
- if (weekday) {
- let newValue = null
-
- if (weekday.alwaysOpen) {
- newValue = alwaysOpenMsg
- } else if (weekday.isClosed) {
- newValue = closedMsg
- } else if (weekday.openingTime && weekday.closingTime) {
- newValue = `${weekday.openingTime}-${weekday.closingTime}`
- }
-
- if (newValue !== null) {
- if (rangeValue === newValue) {
- if (rangeWeekdays.length > 1) {
- rangeWeekdays.splice(-1, 1, label) // Replace last element
- } else {
- rangeWeekdays.push(label)
- }
- } else {
- if (rangeValue) {
- groupedOpeningHours.push(
- `${rangeWeekdays.join("-")}: ${rangeValue}`
- )
- }
- rangeValue = newValue
- rangeWeekdays = [label]
- }
- }
-
- if (rangeValue && i === n - 1) {
- // Flush everything at the end
- groupedOpeningHours.push(`${rangeWeekdays.join("-")}: ${rangeValue}`)
- }
- }
- }
+ const groupedOpeningHours = getGroupedOpeningHours(openingHours, intl)
return (
@@ -109,13 +35,8 @@ export default async function OpeningHours({
)
})}
- {alternateOpeningHours?.name ? (
-
- {alternateOpeningHours.name}
-
+ {alternateOpeningHours ? (
+
) : null}
)
diff --git a/apps/scandic-web/components/OpeningHours/openingHours.test.ts b/apps/scandic-web/components/OpeningHours/openingHours.test.ts
new file mode 100644
index 000000000..ba37c468e
--- /dev/null
+++ b/apps/scandic-web/components/OpeningHours/openingHours.test.ts
@@ -0,0 +1,394 @@
+import { describe, expect, it } from "@jest/globals"
+
+import { getGroupedOpeningHours } from "./utils"
+
+import type { IntlShape } from "react-intl"
+
+import type { RestaurantOpeningHours } from "@/types/hotel"
+
+// Mock IntlShape for testing
+const mockIntl = {
+ formatMessage: ({ id }: { id: string }) => {
+ const messages: Record = {
+ Monday: "Monday",
+ Tuesday: "Tuesday",
+ Wednesday: "Wednesday",
+ Thursday: "Thursday",
+ Friday: "Friday",
+ Saturday: "Saturday",
+ Sunday: "Sunday",
+ Closed: "Closed",
+ "Always open": "Always open",
+ }
+ return messages[id] || id
+ },
+} as IntlShape
+
+describe("getGroupedOpeningHours", () => {
+ it("should group all days as closed", () => {
+ const allDaysClosed: RestaurantOpeningHours = {
+ isActive: true,
+ name: "Opening hours",
+ monday: {
+ isClosed: true,
+ alwaysOpen: false,
+ openingTime: "",
+ closingTime: "",
+ sortOrder: 1,
+ },
+ tuesday: {
+ isClosed: true,
+ alwaysOpen: false,
+ openingTime: "",
+ closingTime: "",
+ sortOrder: 2,
+ },
+ wednesday: {
+ isClosed: true,
+ alwaysOpen: false,
+ openingTime: "",
+ closingTime: "",
+ sortOrder: 3,
+ },
+ thursday: {
+ isClosed: true,
+ alwaysOpen: false,
+ openingTime: "",
+ closingTime: "",
+ sortOrder: 4,
+ },
+ friday: {
+ isClosed: true,
+ alwaysOpen: false,
+ openingTime: "",
+ closingTime: "",
+ sortOrder: 5,
+ },
+ saturday: {
+ isClosed: true,
+ alwaysOpen: false,
+ openingTime: "",
+ closingTime: "",
+ sortOrder: 6,
+ },
+ sunday: {
+ isClosed: true,
+ alwaysOpen: false,
+ openingTime: "",
+ closingTime: "",
+ sortOrder: 7,
+ },
+ }
+ const result = getGroupedOpeningHours(allDaysClosed, mockIntl)
+ expect(result).toEqual(["Monday-Sunday: Closed"])
+ })
+
+ it("should group all days with same opening hours", () => {
+ const allDaysSameHours: RestaurantOpeningHours = {
+ isActive: true,
+ name: "Opening hours",
+ monday: {
+ openingTime: "09:00",
+ closingTime: "17:00",
+ isClosed: false,
+ alwaysOpen: false,
+ sortOrder: 1,
+ },
+ tuesday: {
+ openingTime: "09:00",
+ closingTime: "17:00",
+ isClosed: false,
+ alwaysOpen: false,
+ sortOrder: 2,
+ },
+ wednesday: {
+ openingTime: "09:00",
+ closingTime: "17:00",
+ isClosed: false,
+ alwaysOpen: false,
+ sortOrder: 3,
+ },
+ thursday: {
+ openingTime: "09:00",
+ closingTime: "17:00",
+ isClosed: false,
+ alwaysOpen: false,
+ sortOrder: 4,
+ },
+ friday: {
+ openingTime: "09:00",
+ closingTime: "17:00",
+ isClosed: false,
+ alwaysOpen: false,
+ sortOrder: 5,
+ },
+ saturday: {
+ openingTime: "09:00",
+ closingTime: "17:00",
+ isClosed: false,
+ alwaysOpen: false,
+ sortOrder: 6,
+ },
+ sunday: {
+ openingTime: "09:00",
+ closingTime: "17:00",
+ isClosed: false,
+ alwaysOpen: false,
+ sortOrder: 7,
+ },
+ }
+ const result = getGroupedOpeningHours(allDaysSameHours, mockIntl)
+ expect(result).toEqual(["Monday-Sunday: 09:00-17:00"])
+ })
+
+ it("should handle mixed opening hours", () => {
+ const mixedOpeningHours: RestaurantOpeningHours = {
+ isActive: true,
+ name: "Opening hours",
+ monday: {
+ openingTime: "09:00",
+ closingTime: "17:00",
+ isClosed: false,
+ alwaysOpen: false,
+ sortOrder: 1,
+ },
+ tuesday: {
+ openingTime: "09:00",
+ closingTime: "17:00",
+ isClosed: false,
+ alwaysOpen: false,
+ sortOrder: 2,
+ },
+ wednesday: {
+ openingTime: "09:00",
+ closingTime: "17:00",
+ isClosed: false,
+ alwaysOpen: false,
+ sortOrder: 3,
+ },
+ thursday: {
+ openingTime: "09:00",
+ closingTime: "17:00",
+ isClosed: false,
+ alwaysOpen: false,
+ sortOrder: 4,
+ },
+ friday: {
+ openingTime: "09:00",
+ closingTime: "17:00",
+ isClosed: false,
+ alwaysOpen: false,
+ sortOrder: 5,
+ },
+ saturday: {
+ openingTime: "10:00",
+ closingTime: "15:00",
+ isClosed: false,
+ alwaysOpen: false,
+ sortOrder: 6,
+ },
+ sunday: {
+ isClosed: true,
+ alwaysOpen: false,
+ openingTime: "",
+ closingTime: "",
+ sortOrder: 7,
+ },
+ }
+ const result = getGroupedOpeningHours(mixedOpeningHours, mockIntl)
+ expect(result).toEqual([
+ "Monday-Friday: 09:00-17:00",
+ "Saturday: 10:00-15:00",
+ "Sunday: Closed",
+ ])
+ })
+
+ it("should handle always open days", () => {
+ const someAlwaysOpen: RestaurantOpeningHours = {
+ isActive: true,
+ name: "Opening hours",
+ monday: {
+ alwaysOpen: true,
+ isClosed: false,
+ openingTime: "",
+ closingTime: "",
+ sortOrder: 1,
+ },
+ tuesday: {
+ alwaysOpen: true,
+ isClosed: false,
+ openingTime: "",
+ closingTime: "",
+ sortOrder: 2,
+ },
+ wednesday: {
+ openingTime: "09:00",
+ closingTime: "17:00",
+ isClosed: false,
+ alwaysOpen: false,
+ sortOrder: 3,
+ },
+ thursday: {
+ openingTime: "09:00",
+ closingTime: "17:00",
+ isClosed: false,
+ alwaysOpen: false,
+ sortOrder: 4,
+ },
+ friday: {
+ openingTime: "09:00",
+ closingTime: "17:00",
+ isClosed: false,
+ alwaysOpen: false,
+ sortOrder: 5,
+ },
+ saturday: {
+ isClosed: true,
+ alwaysOpen: false,
+ openingTime: "",
+ closingTime: "",
+ sortOrder: 6,
+ },
+ sunday: {
+ isClosed: true,
+ alwaysOpen: false,
+ openingTime: "",
+ closingTime: "",
+ sortOrder: 7,
+ },
+ }
+ const result = getGroupedOpeningHours(someAlwaysOpen, mockIntl)
+ expect(result).toEqual([
+ "Monday-Tuesday: Always open",
+ "Wednesday-Friday: 09:00-17:00",
+ "Saturday-Sunday: Closed",
+ ])
+ })
+
+ it("should handle missing days", () => {
+ const missingDays: RestaurantOpeningHours = {
+ isActive: true,
+ name: "Opening hours",
+ monday: {
+ openingTime: "09:00",
+ closingTime: "17:00",
+ isClosed: false,
+ alwaysOpen: false,
+ sortOrder: 1,
+ },
+ wednesday: {
+ openingTime: "09:00",
+ closingTime: "17:00",
+ isClosed: false,
+ alwaysOpen: false,
+ sortOrder: 3,
+ },
+ friday: {
+ openingTime: "09:00",
+ closingTime: "17:00",
+ isClosed: false,
+ alwaysOpen: false,
+ sortOrder: 5,
+ },
+ }
+ const result = getGroupedOpeningHours(missingDays, mockIntl)
+ expect(result).toEqual([
+ "Monday: 09:00-17:00",
+ "Wednesday: 09:00-17:00",
+ "Friday: 09:00-17:00",
+ ])
+ })
+
+ it("should not group non-consecutive days with same hours", () => {
+ const nonConsecutiveSameHours: RestaurantOpeningHours = {
+ isActive: true,
+ name: "Opening hours",
+ monday: {
+ openingTime: "09:00",
+ closingTime: "17:00",
+ isClosed: false,
+ alwaysOpen: false,
+ sortOrder: 1,
+ },
+ tuesday: {
+ openingTime: "10:00",
+ closingTime: "18:00",
+ isClosed: false,
+ alwaysOpen: false,
+ sortOrder: 2,
+ },
+ wednesday: {
+ openingTime: "09:00",
+ closingTime: "17:00",
+ isClosed: false,
+ alwaysOpen: false,
+ sortOrder: 3,
+ },
+ }
+ const result = getGroupedOpeningHours(nonConsecutiveSameHours, mockIntl)
+ expect(result).toEqual([
+ "Monday: 09:00-17:00",
+ "Tuesday: 10:00-18:00",
+ "Wednesday: 09:00-17:00",
+ ])
+ })
+
+ it("should handle nullable opening/closing times", () => {
+ const nullableHours: RestaurantOpeningHours = {
+ isActive: true,
+ name: "Opening hours",
+ monday: {
+ openingTime: "",
+ closingTime: "",
+ isClosed: true,
+ alwaysOpen: false,
+ sortOrder: 1,
+ },
+ tuesday: {
+ openingTime: "09:00",
+ closingTime: "",
+ isClosed: false,
+ alwaysOpen: false,
+ sortOrder: 2,
+ },
+ wednesday: {
+ openingTime: "",
+ closingTime: "17:00",
+ isClosed: false,
+ alwaysOpen: false,
+ sortOrder: 3,
+ },
+ }
+ const result = getGroupedOpeningHours(nullableHours, mockIntl)
+ expect(result).toEqual([
+ "Monday: Closed",
+ // Tuesday and Wednesday won't appear in the result because they have empty string values
+ // that don't match any of the conditions in the getGroupedOpeningHours function
+ ])
+ })
+
+ it("should handle inactive restaurant hours", () => {
+ const inactiveHours: RestaurantOpeningHours = {
+ isActive: false,
+ name: "Opening hours",
+ monday: {
+ openingTime: "09:00",
+ closingTime: "17:00",
+ isClosed: false,
+ alwaysOpen: false,
+ sortOrder: 1,
+ },
+ tuesday: {
+ openingTime: "09:00",
+ closingTime: "17:00",
+ isClosed: false,
+ alwaysOpen: false,
+ sortOrder: 2,
+ },
+ }
+ // Even though isActive is false, the function should still process the hours
+ // as it doesn't check for the isActive flag
+ const result = getGroupedOpeningHours(inactiveHours, mockIntl)
+ expect(result).toEqual(["Monday-Tuesday: 09:00-17:00"])
+ })
+})
diff --git a/apps/scandic-web/components/OpeningHours/utils.ts b/apps/scandic-web/components/OpeningHours/utils.ts
new file mode 100644
index 000000000..ced787107
--- /dev/null
+++ b/apps/scandic-web/components/OpeningHours/utils.ts
@@ -0,0 +1,88 @@
+import type { IntlShape } from "react-intl"
+
+import type { RestaurantOpeningHours } from "@/types/hotel"
+
+export function getGroupedOpeningHours(
+ openingHours: RestaurantOpeningHours,
+ intl: IntlShape
+): string[] {
+ const closedMsg = intl.formatMessage({ id: "Closed" })
+ const alwaysOpenMsg = intl.formatMessage({ id: "Always open" })
+
+ // In order
+ const weekdayDefinitions = [
+ {
+ key: "monday",
+ label: intl.formatMessage({ id: "Monday" }),
+ },
+ {
+ key: "tuesday",
+ label: intl.formatMessage({ id: "Tuesday" }),
+ },
+ {
+ key: "wednesday",
+ label: intl.formatMessage({ id: "Wednesday" }),
+ },
+ {
+ key: "thursday",
+ label: intl.formatMessage({ id: "Thursday" }),
+ },
+ {
+ key: "friday",
+ label: intl.formatMessage({ id: "Friday" }),
+ },
+ {
+ key: "saturday",
+ label: intl.formatMessage({ id: "Saturday" }),
+ },
+ {
+ key: "sunday",
+ label: intl.formatMessage({ id: "Sunday" }),
+ },
+ ] as const
+
+ const groupedOpeningHours: string[] = []
+
+ let rangeWeekdays: string[] = []
+ let rangeValue = ""
+ for (let i = 0, n = weekdayDefinitions.length; i < n; ++i) {
+ const weekdayDefinition = weekdayDefinitions[i]
+ const weekday = openingHours[weekdayDefinition.key]
+ const label = weekdayDefinition.label
+ if (weekday) {
+ let newValue = null
+
+ if (weekday.alwaysOpen) {
+ newValue = alwaysOpenMsg
+ } else if (weekday.isClosed) {
+ newValue = closedMsg
+ } else if (weekday.openingTime && weekday.closingTime) {
+ newValue = `${weekday.openingTime}-${weekday.closingTime}`
+ }
+
+ if (newValue !== null) {
+ if (rangeValue === newValue) {
+ if (rangeWeekdays.length > 1) {
+ rangeWeekdays.splice(-1, 1, label) // Replace last element
+ } else {
+ rangeWeekdays.push(label)
+ }
+ } else {
+ if (rangeValue) {
+ groupedOpeningHours.push(
+ `${rangeWeekdays.join("-")}: ${rangeValue}`
+ )
+ }
+ rangeValue = newValue
+ rangeWeekdays = [label]
+ }
+ }
+
+ if (rangeValue && i === n - 1) {
+ // Flush everything at the end
+ groupedOpeningHours.push(`${rangeWeekdays.join("-")}: ${rangeValue}`)
+ }
+ }
+ }
+ return groupedOpeningHours
+}
diff --git a/apps/scandic-web/i18n/dictionaries/da.json b/apps/scandic-web/i18n/dictionaries/da.json
index d5c2b38dd..66e44e26e 100644
--- a/apps/scandic-web/i18n/dictionaries/da.json
+++ b/apps/scandic-web/i18n/dictionaries/da.json
@@ -49,6 +49,7 @@
"All-day breakfast": "Morgenmad hele dagen",
"Allergy-friendly room": "Allergirum",
"Already a friend?": "Allerede en ven?",
+ "Alternate opening hours ({name})": "Alternate opening hours ({name})",
"Alternatives for {value}": "Alternatives for {value}",
"Always open": "Altid åben",
"Amenities": "Faciliteter",
diff --git a/apps/scandic-web/i18n/dictionaries/de.json b/apps/scandic-web/i18n/dictionaries/de.json
index 74db95108..613122338 100644
--- a/apps/scandic-web/i18n/dictionaries/de.json
+++ b/apps/scandic-web/i18n/dictionaries/de.json
@@ -49,6 +49,7 @@
"All-day breakfast": "Ganztag-Frühstück",
"Allergy-friendly room": "Allergikerzimmer",
"Already a friend?": "Sind wir schon Freunde?",
+ "Alternate opening hours ({name})": "Alternate opening hours ({name})",
"Alternatives for {value}": "Alternatives for {value}",
"Always open": "Immer geöffnet",
"Amenities": "Annehmlichkeiten",
diff --git a/apps/scandic-web/i18n/dictionaries/en.json b/apps/scandic-web/i18n/dictionaries/en.json
index 29d884464..5dad001e5 100644
--- a/apps/scandic-web/i18n/dictionaries/en.json
+++ b/apps/scandic-web/i18n/dictionaries/en.json
@@ -49,6 +49,7 @@
"All-day breakfast": "All-day breakfast",
"Allergy-friendly room": "Allergy-friendly room",
"Already a friend?": "Already a friend?",
+ "Alternate opening hours ({name})": "Alternate opening hours ({name})",
"Alternatives for {value}": "Alternatives for {value}",
"Always open": "Always open",
"Amenities": "Amenities",
diff --git a/apps/scandic-web/i18n/dictionaries/fi.json b/apps/scandic-web/i18n/dictionaries/fi.json
index 08ba0ddd3..7cae938af 100644
--- a/apps/scandic-web/i18n/dictionaries/fi.json
+++ b/apps/scandic-web/i18n/dictionaries/fi.json
@@ -49,6 +49,7 @@
"All-day breakfast": "Koko päivän aamiainen",
"Allergy-friendly room": "Allergiahuone",
"Already a friend?": "Oletko jo ystävä?",
+ "Alternate opening hours ({name})": "Vaihtoehtoiset aukioloajat ({name})",
"Alternatives for {value}": "Alternatives for {value}",
"Always open": "Aina auki",
"Amenities": "Mukavuudet",
diff --git a/apps/scandic-web/i18n/dictionaries/no.json b/apps/scandic-web/i18n/dictionaries/no.json
index 479d125c0..0538b482e 100644
--- a/apps/scandic-web/i18n/dictionaries/no.json
+++ b/apps/scandic-web/i18n/dictionaries/no.json
@@ -49,6 +49,7 @@
"All-day breakfast": "Frokost hele dagen",
"Allergy-friendly room": "Allergirom",
"Already a friend?": "Allerede Friend?",
+ "Alternate opening hours ({name})": "Alternativ åpningstider ({name})",
"Alternatives for {value}": "Alternatives for {value}",
"Always open": "Alltid åpen",
"Amenities": "Fasiliteter",
diff --git a/apps/scandic-web/i18n/dictionaries/sv.json b/apps/scandic-web/i18n/dictionaries/sv.json
index ef4093e87..72a4e73f1 100644
--- a/apps/scandic-web/i18n/dictionaries/sv.json
+++ b/apps/scandic-web/i18n/dictionaries/sv.json
@@ -49,6 +49,7 @@
"All-day breakfast": "Frukost hela dagen",
"Allergy-friendly room": "Allergirum",
"Already a friend?": "Är du redan en vän?",
+ "Alternate opening hours ({name})": "Alternativa öppettider ({name})",
"Alternatives for {value}": "Alternatives for {value}",
"Always open": "Alltid öppet",
"Amenities": "Bekvämligheter",
diff --git a/packages/design-system/lib/components/Typography/Typography.tsx b/packages/design-system/lib/components/Typography/Typography.tsx
index e1db31f5d..ea2d0404b 100644
--- a/packages/design-system/lib/components/Typography/Typography.tsx
+++ b/packages/design-system/lib/components/Typography/Typography.tsx
@@ -4,7 +4,7 @@ import { variants } from './variants'
import type { TypographyProps } from './types'
-export function Typography({ variant, children }: TypographyProps) {
+export function Typography({ variant, className, children }: TypographyProps) {
if (!isValidElement(children)) return null
const classNames = variants({
@@ -13,6 +13,8 @@ export function Typography({ variant, children }: TypographyProps) {
return cloneElement(children, {
...children.props,
- className: [children.props.className, classNames].filter(Boolean).join(' '),
+ className: [className, children.props.className, classNames]
+ .filter(Boolean)
+ .join(' '),
})
}