Merged in feat/sw-3242-move-opening-hours-to-design-system (pull request #2629)

feat(SW-32429: Move OpeningHours to design-system

* Move OpeningHours to design-system


Approved-by: Joakim Jäderberg
This commit is contained in:
Anton Gunnarsson
2025-08-13 07:05:23 +00:00
parent 124f743df7
commit e92c0465cc
10 changed files with 225 additions and 206 deletions

View File

@@ -1,8 +1,8 @@
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
import Link from "@scandic-hotels/design-system/Link"
import OpeningHours from "@scandic-hotels/design-system/OpeningHours"
import { Typography } from "@scandic-hotels/design-system/Typography"
import OpeningHours from "@/components/OpeningHours"
import { getIntl } from "@/i18n"
import { appendSlugToPathname } from "@/utils/appendSlugToPathname"

View File

@@ -1,10 +1,10 @@
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
import Link from "@scandic-hotels/design-system/Link"
import { OldDSButton as Button } from "@scandic-hotels/design-system/OldDSButton"
import OpeningHours from "@scandic-hotels/design-system/OpeningHours"
import { Typography } from "@scandic-hotels/design-system/Typography"
import LocalCallCharges from "@/components/LocalCallCharges"
import OpeningHours from "@/components/OpeningHours"
import { getIntl } from "@/i18n"
import styles from "./sidebar.module.css"

View File

@@ -1,57 +0,0 @@
"use client"
import { useIntl } from "react-intl"
import { Divider } from "@scandic-hotels/design-system/Divider"
import { Typography } from "@scandic-hotels/design-system/Typography"
import { getGroupedOpeningHours } from "../utils"
import styles from "../openingHours.module.css"
import type { RestaurantOpeningHours } from "@scandic-hotels/trpc/types/hotel"
interface AlternateOpeningHoursProps {
alternateOpeningHours: RestaurantOpeningHours
}
export default function AlternateOpeningHours({
alternateOpeningHours,
}: AlternateOpeningHoursProps) {
const intl = useIntl()
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 (
<Typography variant="Body/Supporting text (caption)/smRegular">
<p className={styles.caption}>{alternateOpeningHours.name}</p>
</Typography>
)
}
return (
<>
<Divider />
<Typography variant="Body/Paragraph/mdBold">
<h5 className={styles.heading}>
{intl.formatMessage(
{
defaultMessage: "Alternate opening hours ({name})",
},
{ name: alternateOpeningHours.name }
)}
</h5>
</Typography>
<Typography variant="Body/Paragraph/mdRegular">
<div className={styles.text}>
{groupedAlternateOpeningHours.map((groupedOpeningHour) => (
<p key={groupedOpeningHour}>{groupedOpeningHour}</p>
))}
</div>
</Typography>
</>
)
}

View File

@@ -1,49 +0,0 @@
"use client"
import { useIntl } from "react-intl"
import { Divider } from "@scandic-hotels/design-system/Divider"
import { Typography } from "@scandic-hotels/design-system/Typography"
import AlternateOpeningHours from "./AlternateOpeningHours"
import { getGroupedOpeningHours, getTranslatedName } from "./utils"
import styles from "./openingHours.module.css"
import type { RestaurantOpeningHours } from "@scandic-hotels/trpc/types/hotel"
interface OpeningHoursProps {
openingHours: RestaurantOpeningHours
alternateOpeningHours?: RestaurantOpeningHours
heading?: string
}
export default function OpeningHours({
openingHours,
alternateOpeningHours,
heading,
}: OpeningHoursProps) {
const intl = useIntl()
const groupedOpeningHours = getGroupedOpeningHours(openingHours, intl)
return (
<div className={styles.wrapper}>
<Typography variant="Title/Overline/sm">
<h5 className={styles.heading}>
{heading ?? getTranslatedName(openingHours.nameEnglish, intl)}
</h5>
</Typography>
<Divider />
<Typography variant="Body/Paragraph/mdRegular">
<div className={styles.text}>
{groupedOpeningHours.map((groupedOpeningHour) => (
<p key={groupedOpeningHour}>{groupedOpeningHour}</p>
))}
</div>
</Typography>
{alternateOpeningHours ? (
<AlternateOpeningHours alternateOpeningHours={alternateOpeningHours} />
) : null}
</div>
)
}

View File

@@ -1,19 +0,0 @@
.wrapper {
display: grid;
padding: var(--Space-x2) var(--Space-x3);
gap: var(--Space-x1);
border-radius: var(--Corner-radius-md);
background: var(--Surface-Secondary-Default);
}
.heading {
color: var(--Text-Secondary);
}
.caption {
color: var(--Text-Secondary);
}
.text {
color: var(--Text-Default);
}

View File

@@ -1,401 +0,0 @@
import { describe, expect, it } from "vitest"
import { getGroupedOpeningHours } from "./utils"
import type { RestaurantOpeningHours } from "@scandic-hotels/trpc/types/hotel"
import type { IntlShape } from "react-intl"
// Mock IntlShape for testing
const mockIntl = {
formatMessage: ({ defaultMessage }: { defaultMessage: string }) => {
const messages: Record<string, string> = {
Monday: "Monday",
Tuesday: "Tuesday",
Wednesday: "Wednesday",
Thursday: "Thursday",
Friday: "Friday",
Saturday: "Saturday",
Sunday: "Sunday",
Closed: "Closed",
"Always open": "Always open",
}
return messages[defaultMessage] || defaultMessage
},
} as IntlShape
describe("getGroupedOpeningHours", () => {
it("should group all days as closed", () => {
const allDaysClosed: RestaurantOpeningHours = {
isActive: true,
name: "Opening hours",
nameEnglish: "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",
nameEnglish: "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",
nameEnglish: "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",
nameEnglish: "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",
nameEnglish: "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",
nameEnglish: "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",
nameEnglish: "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",
nameEnglish: "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"])
})
})

View File

@@ -1,171 +0,0 @@
import { logger } from "@scandic-hotels/common/logger"
import type { RestaurantOpeningHours } from "@scandic-hotels/trpc/types/hotel"
import type { IntlShape } from "react-intl"
export function getGroupedOpeningHours(
openingHours: RestaurantOpeningHours,
intl: IntlShape
): string[] {
const closedMsg = intl.formatMessage({
defaultMessage: "Closed",
})
const alwaysOpenMsg = intl.formatMessage({
defaultMessage: "Always open",
})
// In order
const weekdayDefinitions = [
{
key: "monday",
label: intl.formatMessage({
defaultMessage: "Monday",
}),
},
{
key: "tuesday",
label: intl.formatMessage({
defaultMessage: "Tuesday",
}),
},
{
key: "wednesday",
label: intl.formatMessage({
defaultMessage: "Wednesday",
}),
},
{
key: "thursday",
label: intl.formatMessage({
defaultMessage: "Thursday",
}),
},
{
key: "friday",
label: intl.formatMessage({
defaultMessage: "Friday",
}),
},
{
key: "saturday",
label: intl.formatMessage({
defaultMessage: "Saturday",
}),
},
{
key: "sunday",
label: intl.formatMessage({
defaultMessage: "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}`)
}
} else if (rangeValue) {
groupedOpeningHours.push(`${rangeWeekdays.join("-")}: ${rangeValue}`)
rangeValue = ""
rangeWeekdays = []
}
}
return groupedOpeningHours
}
export function getTranslatedName(name: string, intl: IntlShape) {
switch (name) {
case "Breakfast":
return intl.formatMessage({
defaultMessage: "Breakfast",
})
case "Brunch":
return intl.formatMessage({
defaultMessage: "Brunch",
})
case "After Work":
return intl.formatMessage({
defaultMessage: "After Work",
})
case "Cafe":
return intl.formatMessage({
defaultMessage: "Cafe",
})
case "Lunch":
return intl.formatMessage({
defaultMessage: "Lunch",
})
case "Dinner":
return intl.formatMessage({
defaultMessage: "Dinner",
})
case "Bar":
return intl.formatMessage({
defaultMessage: "Bar",
})
case "Snacks & drinks":
return intl.formatMessage({
defaultMessage: "Snacks & drinks",
})
case "Takeaway":
return intl.formatMessage({
defaultMessage: "Takeaway",
})
case "Changes":
return intl.formatMessage({
defaultMessage: "Changes",
})
case "Live events":
return intl.formatMessage({
defaultMessage: "Live events",
})
case "Terrace":
return intl.formatMessage({
defaultMessage: "Terrace",
})
default:
logger.warn(`Unsupported name given: ${name}`)
return intl.formatMessage({
defaultMessage: "N/A",
})
}
}

View File

@@ -5,10 +5,10 @@ import { useIntl } from "react-intl"
import { isDefined } from "@scandic-hotels/common/utils/isDefined"
import AccordionItem from "@scandic-hotels/design-system/Accordion/AccordionItem"
import { IconName } from "@scandic-hotels/design-system/Icons/iconName"
import OpeningHours from "@scandic-hotels/design-system/OpeningHours"
import { Typography } from "@scandic-hotels/design-system/Typography"
import { HotelTypeEnum } from "@scandic-hotels/trpc/enums/hotelType"
import OpeningHours from "@/components/OpeningHours"
import { trackAccordionClick } from "@/utils/tracking"
import styles from "./sidePeekAccordion.module.css"