diff --git a/components/Blocks/DynamicContent/Overview/Stats/index.tsx b/components/Blocks/DynamicContent/Overview/Stats/index.tsx index 73443da1b..3786a2d86 100644 --- a/components/Blocks/DynamicContent/Overview/Stats/index.tsx +++ b/components/Blocks/DynamicContent/Overview/Stats/index.tsx @@ -11,7 +11,7 @@ export default function Stats({ user }: UserProps) { return (
- +
) diff --git a/components/BookingWidget/Client.tsx b/components/BookingWidget/Client.tsx new file mode 100644 index 000000000..ce8370bab --- /dev/null +++ b/components/BookingWidget/Client.tsx @@ -0,0 +1,107 @@ +"use client" +import { zodResolver } from "@hookform/resolvers/zod" +import { useEffect, useState } from "react" +import { FormProvider, useForm } from "react-hook-form" + +import { dt } from "@/lib/dt" + +import Form from "@/components/Forms/BookingWidget" +import { bookingWidgetSchema } from "@/components/Forms/BookingWidget/schema" +import { CloseLarge } from "@/components/Icons" +import { debounce } from "@/utils/debounce" + +import MobileToggleButton from "./MobileToggleButton" + +import styles from "./bookingWidget.module.css" + +import type { + BookingWidgetClientProps, + BookingWidgetSchema, +} from "@/types/components/bookingWidget" +import type { Location } from "@/types/trpc/routers/hotel/locations" + +export default function BookingWidgetClient({ + locations, +}: BookingWidgetClientProps) { + const [isOpen, setIsOpen] = useState(false) + + const sessionStorageSearchData = + typeof window !== "undefined" + ? sessionStorage.getItem("searchData") + : undefined + const initialSelectedLocation: Location | undefined = sessionStorageSearchData + ? JSON.parse(sessionStorageSearchData) + : undefined + const methods = useForm({ + defaultValues: { + search: initialSelectedLocation?.name ?? "", + location: sessionStorageSearchData + ? encodeURIComponent(sessionStorageSearchData) + : undefined, + date: { + // UTC is required to handle requests from far away timezones https://scandichotels.atlassian.net/browse/SWAP-6375 & PET-507 + // This is specifically to handle timezones falling in different dates. + from: dt().utc().format("YYYY-MM-DD"), + to: dt().utc().add(1, "day").format("YYYY-MM-DD"), + }, + bookingCode: "", + redemption: false, + voucher: false, + rooms: [ + { + adults: 1, + children: [], + }, + ], + }, + shouldFocusError: false, + mode: "all", + resolver: zodResolver(bookingWidgetSchema), + reValidateMode: "onChange", + }) + + function closeMobileSearch() { + setIsOpen(false) + document.body.style.overflowY = "visible" + } + + function openMobileSearch() { + setIsOpen(true) + document.body.style.overflowY = "hidden" + } + + useEffect(() => { + const debouncedResizeHandler = debounce(function ([ + entry, + ]: ResizeObserverEntry[]) { + if (entry.contentRect.width > 1366) { + closeMobileSearch() + } + }) + const observer = new ResizeObserver(debouncedResizeHandler) + + observer.observe(document.body) + + return () => { + if (observer) { + observer.unobserve(document.body) + } + } + }, []) + + return ( + +
+ +
+
+ +
+ ) +} diff --git a/components/BookingWidget/MobileToggleButton/button.module.css b/components/BookingWidget/MobileToggleButton/button.module.css new file mode 100644 index 000000000..f4a3d80fc --- /dev/null +++ b/components/BookingWidget/MobileToggleButton/button.module.css @@ -0,0 +1,35 @@ +.complete, +.partial { + align-items: center; + box-shadow: 0px 8px 24px 0px rgba(0, 0, 0, 0.16); + cursor: pointer; + display: grid; + gap: var(--Spacing-x-one-and-half); + padding: var(--Spacing-x2); +} + +.complete { + grid-template-columns: 1fr 36px; +} + +.partial { + grid-template-columns: min(1fr, 150px) min-content min(1fr, 150px) 1fr; +} + +.icon { + align-items: center; + background-color: var(--Base-Button-Primary-Fill-Normal); + border-radius: 50%; + display: flex; + height: 36px; + justify-content: center; + justify-self: flex-end; + width: 36px; +} + +@media screen and (min-width: 768px) { + .complete, + .partial { + display: none; + } +} diff --git a/components/BookingWidget/MobileToggleButton/index.tsx b/components/BookingWidget/MobileToggleButton/index.tsx new file mode 100644 index 000000000..113d8b365 --- /dev/null +++ b/components/BookingWidget/MobileToggleButton/index.tsx @@ -0,0 +1,102 @@ +"use client" +import { useEffect, useState } from "react" +import { useWatch } from "react-hook-form" +import { useIntl } from "react-intl" + +import { dt } from "@/lib/dt" + +import { EditIcon, SearchIcon } from "@/components/Icons" +import Divider from "@/components/TempDesignSystem/Divider" +import Body from "@/components/TempDesignSystem/Text/Body" +import Caption from "@/components/TempDesignSystem/Text/Caption" +import useLang from "@/hooks/useLang" + +import styles from "./button.module.css" + +import type { + BookingWidgetSchema, + BookingWidgetToggleButtonProps, +} from "@/types/components/bookingWidget" +import type { Location } from "@/types/trpc/routers/hotel/locations" + +export default function MobileToggleButton({ + openMobileSearch, +}: BookingWidgetToggleButtonProps) { + const [hasMounted, setHasMounted] = useState(false) + const intl = useIntl() + const lang = useLang() + const d = useWatch({ name: "date" }) + const location = useWatch({ name: "location" }) + const rooms: BookingWidgetSchema["rooms"] = useWatch({ name: "rooms" }) + + const parsedLocation: Location | null = location + ? JSON.parse(decodeURIComponent(location)) + : null + + const nights = dt(d.to).diff(dt(d.from), "days") + + const selectedFromDate = dt(d.from).locale(lang).format("D MMM") + const selectedToDate = dt(d.to).locale(lang).format("D MMM") + + useEffect(() => { + setHasMounted(true) + }, []) + + if (!hasMounted) { + return null + } + + if (parsedLocation && d) { + const totalRooms = rooms.length + const totalAdults = rooms.reduce((acc, room) => { + if (room.adults) { + acc = acc + room.adults + } + return acc + }, 0) + return ( +
+
+ {parsedLocation.name} + + {`${selectedFromDate} - ${selectedToDate} (${intl.formatMessage( + { id: "booking.nights" }, + { totalNights: nights } + )}) ${intl.formatMessage({ id: "booking.adults" }, { totalAdults })}, ${intl.formatMessage({ id: "booking.rooms" }, { totalRooms })}`} + +
+
+ +
+
+ ) + } + + return ( +
+
+ {intl.formatMessage({ id: "Where to" })} + + {parsedLocation + ? parsedLocation.name + : intl.formatMessage({ id: "Destination" })} + +
+ +
+ + {intl.formatMessage( + { id: "booking.nights" }, + { totalNights: nights } + )} + + + {selectedFromDate} - {selectedToDate} + +
+
+ +
+
+ ) +} diff --git a/components/BookingWidget/bookingWidget.module.css b/components/BookingWidget/bookingWidget.module.css index 7e1c1615e..5384dac60 100644 --- a/components/BookingWidget/bookingWidget.module.css +++ b/components/BookingWidget/bookingWidget.module.css @@ -1,5 +1,28 @@ -.container { - display: none; +@media screen and (max-width: 1366px) { + .container { + background-color: var(--UI-Input-Controls-Surface-Normal); + bottom: -100%; + display: grid; + gap: var(--Spacing-x3); + grid-template-rows: 36px 1fr; + height: 100dvh; + padding: var(--Spacing-x3) var(--Spacing-x2) var(--Spacing-x7); + position: fixed; + transition: bottom 300ms ease; + width: 100%; + z-index: 10000; + } + + .container[data-open="true"] { + bottom: 0; + } + + .close { + background: none; + border: none; + cursor: pointer; + justify-self: flex-end; + } } @media screen and (min-width: 1367px) { @@ -8,4 +31,8 @@ border-top: 1px solid var(--Base-Border-Subtle); display: block; } + + .close { + display: none; + } } diff --git a/components/BookingWidget/index.tsx b/components/BookingWidget/index.tsx index de5a972ae..1c8b02f8e 100644 --- a/components/BookingWidget/index.tsx +++ b/components/BookingWidget/index.tsx @@ -1,8 +1,6 @@ import { getLocations } from "@/lib/trpc/memoizedRequests" -import Form from "@/components/Forms/BookingWidget" - -import styles from "./bookingWidget.module.css" +import BookingWidgetClient from "./Client" export function preload() { void getLocations() @@ -15,9 +13,5 @@ export default async function BookingWidget() { return null } - return ( -
- -
- ) + return } diff --git a/components/DatePicker/DatePicker.tsx b/components/DatePicker/Screen/Desktop.tsx similarity index 84% rename from components/DatePicker/DatePicker.tsx rename to components/DatePicker/Screen/Desktop.tsx index 0e78ac59c..8656fb8e5 100644 --- a/components/DatePicker/DatePicker.tsx +++ b/components/DatePicker/Screen/Desktop.tsx @@ -1,49 +1,30 @@ "use client" -import { da, de, fi, nb, sv } from "date-fns/locale" -import { useState } from "react" -import { type DateRange, DayPicker } from "react-day-picker" +import { DayPicker } from "react-day-picker" import { useIntl } from "react-intl" import { Lang } from "@/constants/languages" import { dt } from "@/lib/dt" +import { ChevronLeftIcon } from "@/components/Icons" import Button from "@/components/TempDesignSystem/Button" import Divider from "@/components/TempDesignSystem/Divider" import Caption from "@/components/TempDesignSystem/Text/Caption" import Subtitle from "@/components/TempDesignSystem/Text/Subtitle" import useLang from "@/hooks/useLang" -import { ChevronLeftIcon } from "../Icons" - -import styles from "./date-picker.module.css" +import styles from "./desktop.module.css" import classNames from "react-day-picker/style.module.css" import type { DatePickerProps } from "@/types/components/datepicker" -const locales = { - [Lang.da]: da, - [Lang.de]: de, - [Lang.fi]: fi, - [Lang.no]: nb, - [Lang.sv]: sv, -} - -export default function DatePicker({ +export default function DatePickerDesktop({ close, handleOnSelect, - initialSelected = { - from: undefined, - to: undefined, - }, + locales, + selectedDate, }: DatePickerProps) { const lang = useLang() const intl = useIntl() - const [selectedDate, setSelectedDate] = useState(initialSelected) - - function handleSelectDate(selected: DateRange) { - handleOnSelect(selected) - setSelectedDate(selected) - } /** English is default language and doesn't need to be imported */ const locale = lang === Lang.en ? undefined : locales[lang] @@ -63,6 +44,7 @@ export default function DatePicker({ range_end: styles.rangeEnd, range_middle: styles.rangeMiddle, range_start: styles.rangeStart, + root: `${classNames.root} ${styles.container}`, week: styles.week, weekday: `${classNames.weekday} ${styles.weekDay}`, }} @@ -78,7 +60,7 @@ export default function DatePicker({ locale={locale} mode="range" numberOfMonths={2} - onSelect={handleSelectDate} + onSelect={handleOnSelect} pagedNavigation required selected={selectedDate} diff --git a/components/DatePicker/Screen/Mobile.tsx b/components/DatePicker/Screen/Mobile.tsx new file mode 100644 index 000000000..c56a79cd6 --- /dev/null +++ b/components/DatePicker/Screen/Mobile.tsx @@ -0,0 +1,140 @@ +"use client" +import { type ChangeEvent, useState } from "react" +import { DayPicker } from "react-day-picker" +import { useIntl } from "react-intl" + +import { Lang } from "@/constants/languages" +import { dt } from "@/lib/dt" + +import { CloseLarge } from "@/components/Icons" +import Button from "@/components/TempDesignSystem/Button" +import Body from "@/components/TempDesignSystem/Text/Body" +import Subtitle from "@/components/TempDesignSystem/Text/Subtitle" +import useLang from "@/hooks/useLang" + +import styles from "./mobile.module.css" +import classNames from "react-day-picker/style.module.css" + +import type { DatePickerProps } from "@/types/components/datepicker" + +function addOneYear(_: undefined, i: number) { + return new Date().getFullYear() + i +} + +const fiftyYearsAhead = Array.from({ length: 50 }, addOneYear) + +export default function DatePickerMobile({ + close, + handleOnSelect, + locales, + selectedDate, +}: DatePickerProps) { + const [selectedYear, setSelectedYear] = useState(() => dt().year()) + const lang = useLang() + const intl = useIntl() + + function handleSelectYear(evt: ChangeEvent) { + setSelectedYear(Number(evt.currentTarget.value)) + } + + /** English is default language and doesn't need to be imported */ + const locale = lang === Lang.en ? undefined : locales[lang] + const currentDate = dt().toDate() + const startOfCurrentMonth = dt(currentDate).set("date", 1).toDate() + const yesterday = dt(currentDate).subtract(1, "day").toDate() + + const startMonth = dt().set("year", selectedYear).startOf("year").toDate() + const decemberOfYear = dt().set("year", selectedYear).endOf("year").toDate() + return ( + + +
+ + ) + }, + MonthCaption(props) { + return ( +
+ + {props.children} + +
+ ) + }, + Root({ children, ...props }) { + return ( +
+
+ + +
+ {children} +
+ ) + }, + }} + /> + ) +} diff --git a/components/DatePicker/Screen/desktop.module.css b/components/DatePicker/Screen/desktop.module.css new file mode 100644 index 000000000..afbb286a7 --- /dev/null +++ b/components/DatePicker/Screen/desktop.module.css @@ -0,0 +1,120 @@ +@media screen and (max-width: 1366px) { + .container { + display: none; + } +} + +div.months { + flex-wrap: nowrap; +} + +.monthCaption { + justify-content: center; +} + +.captionLabel { + text-transform: capitalize; +} + +td.day, +td.rangeEnd, +td.rangeStart { + font-family: var(--typography-Body-Bold-fontFamily); + font-size: var(--typography-Body-Bold-fontSize); + font-weight: 500; + letter-spacing: var(--typography-Body-Bold-letterSpacing); + line-height: var(--typography-Body-Bold-lineHeight); + text-decoration: var(--typography-Body-Bold-textDecoration); +} + +td.rangeEnd, +td.rangeStart { + background: var(--Base-Background-Primary-Normal); +} + +td.rangeEnd[aria-selected="true"]:not([data-outside="true"]) { + border-radius: 0 50% 50% 0; +} + +td.rangeStart[aria-selected="true"] { + border-radius: 50% 0 0 50%; +} + +td.rangeEnd[aria-selected="true"] button.dayButton:hover, +td.rangeStart[aria-selected="true"] button.dayButton:hover { + background: var(--Primary-Light-On-Surface-Accent); + border-radius: 50%; +} + +td.rangeEnd[aria-selected="true"]:not([data-outside="true"]) button.dayButton, +td.rangeStart[aria-selected="true"]:not([data-outside="true"]) + button.dayButton { + background: var(--Primary-Light-On-Surface-Accent); + border: none; + color: var(--Base-Button-Inverted-Fill-Normal); +} + +td.day, +td.day[data-today="true"] { + color: var(--UI-Text-High-contrast); + height: 40px; + padding: var(--Spacing-x-half); + width: 40px; +} + +td.day button.dayButton:hover { + background: var(--Base-Surface-Secondary-light-Hover); +} + +td.day[data-outside="true"] button.dayButton { + border: none; +} + +td.day:not(td.rangeEnd, td.rangeStart)[aria-selected="true"], +td.rangeMiddle[aria-selected="true"] button.dayButton { + background: var(--Base-Background-Primary-Normal); + border: none; + border-radius: 0; +} + +td.day[data-disabled="true"], +td.day[data-disabled="true"] button.dayButton, +td.day[data-outside="true"] ~ td.day[data-disabled="true"], +td.day[data-outside="true"] ~ td.day[data-disabled="true"] button.dayButton, +.week:has(td.day[data-outside="true"] ~ td.day[data-disabled="true"]) + td.day[data-outside="true"] + button.dayButton { + background: none; + color: var(--Base-Text-Disabled); + cursor: not-allowed; +} + +.weekDay { + color: var(--UI-Text-Placeholder); + font-family: var(--typography-Footnote-Labels-fontFamily); + font-size: var(--typography-Footnote-Labels-fontSize); + font-weight: var(--typography-Footnote-Labels-fontWeight); + letter-spacing: var(--typography-Footnote-Labels-letterSpacing); + line-height: var(--typography-Footnote-Labels-lineHeight); + text-decoration: var(--typography-Footnote-Labels-textDecoration); + text-transform: uppercase; +} + +.footer { + display: flex; + justify-content: flex-end; + margin-top: var(--Spacing-x2); +} + +.divider { + margin-top: var(--Spacing-x2); +} + +.nextButton { + transform: rotate(180deg); + right: 0; +} + +.previousButton { + left: 0; +} diff --git a/components/DatePicker/Screen/mobile.module.css b/components/DatePicker/Screen/mobile.module.css new file mode 100644 index 000000000..4f820fd5f --- /dev/null +++ b/components/DatePicker/Screen/mobile.module.css @@ -0,0 +1,173 @@ +.container { + --header-height: 68px; + --sticky-button-height: 120px; + + display: grid; + grid-template-areas: + "header" + "content"; + grid-template-rows: var(--header-height) calc(100dvh - var(--header-height)); + position: relative; +} + +.header { + align-self: flex-start; + background-color: var(--Main-Grey-White); + display: grid; + grid-area: header; + grid-template-columns: 1fr 24px; + padding: var(--Spacing-x3) var(--Spacing-x2) var(--Spacing-x2); + position: sticky; + top: 0; + z-index: 10; +} + +.select { + justify-self: center; + min-width: 100px; + transform: translateX(24px); +} + +.close { + align-items: center; + background: none; + border: none; + cursor: pointer; + display: flex; + justify-self: flex-end; +} + +div.months { + display: grid; + grid-area: content; + overflow-y: scroll; + scroll-snap-type: y mandatory; +} + +.month { + display: grid; + justify-items: center; + scroll-snap-align: start; +} + +.month:last-of-type { + padding-bottom: var(--sticky-button-height); +} + +.monthCaption { + justify-content: center; +} + +.captionLabel { + text-transform: capitalize; +} + +.footer { + align-self: flex-start; + background: linear-gradient( + 180deg, + rgba(255, 255, 255, 0) 7.5%, + #ffffff 82.5% + ); + display: flex; + grid-area: content; + padding: var(--Spacing-x1) var(--Spacing-x2) var(--Spacing-x7); + position: sticky; + top: calc(100vh - var(--sticky-button-height)); + width: 100%; + z-index: 10; +} + +.footer .button { + width: 100%; +} + +td.day, +td.rangeEnd, +td.rangeStart { + font-family: var(--typography-Body-Bold-fontFamily); + font-size: var(--typography-Body-Bold-fontSize); + font-weight: 500; + letter-spacing: var(--typography-Body-Bold-letterSpacing); + line-height: var(--typography-Body-Bold-lineHeight); + text-decoration: var(--typography-Body-Bold-textDecoration); +} + +td.rangeEnd, +td.rangeStart { + background: var(--Base-Background-Primary-Normal); +} + +td.rangeEnd[aria-selected="true"]:not([data-outside="true"]) { + border-radius: 0 50% 50% 0; +} + +td.rangeStart[aria-selected="true"] { + border-radius: 50% 0 0 50%; +} + +td.rangeEnd[aria-selected="true"] button.dayButton:hover, +td.rangeStart[aria-selected="true"] button.dayButton:hover { + background: var(--Primary-Light-On-Surface-Accent); + border-radius: 50%; +} + +td.rangeEnd[aria-selected="true"]:not([data-outside="true"]) button.dayButton, +td.rangeStart[aria-selected="true"]:not([data-outside="true"]) + button.dayButton { + background: var(--Primary-Light-On-Surface-Accent); + border: none; + color: var(--Base-Button-Inverted-Fill-Normal); +} + +td.day, +td.day[data-today="true"] { + color: var(--UI-Text-High-contrast); + height: 40px; + padding: var(--Spacing-x-half); + width: 40px; +} + +td.day button.dayButton:hover { + background: var(--Base-Surface-Secondary-light-Hover); +} + +td.day[data-outside="true"] button.dayButton { + border: none; +} + +td.day:not(td.rangeEnd, td.rangeStart)[aria-selected="true"], +td.rangeMiddle[aria-selected="true"] button.dayButton { + background: var(--Base-Background-Primary-Normal); + border: none; + border-radius: 0; +} + +td.day[data-disabled="true"], +td.day[data-disabled="true"] button.dayButton, +td.day[data-outside="true"] ~ td.day[data-disabled="true"], +td.day[data-outside="true"] ~ td.day[data-disabled="true"] button.dayButton, +.week:has(td.day[data-outside="true"] ~ td.day[data-disabled="true"]) + td.day[data-outside="true"] + button.dayButton { + background: none; + color: var(--Base-Text-Disabled); + cursor: not-allowed; +} + +.weekDay { + color: var(--Base-Text-Medium-contrast); + font-family: var(--typography-Footnote-Labels-fontFamily); + font-size: var(--typography-Footnote-Labels-fontSize); + font-weight: var(--typography-Footnote-Labels-fontWeight); + letter-spacing: var(--typography-Footnote-Labels-letterSpacing); + line-height: var(--typography-Footnote-Labels-lineHeight); + text-decoration: var(--typography-Footnote-Labels-textDecoration); + text-transform: uppercase; +} + +@media screen and (min-width: 1367px) { + .container { + display: none; + } +} diff --git a/components/DatePicker/date-picker.module.css b/components/DatePicker/date-picker.module.css index 77cc0b140..faaac7dd0 100644 --- a/components/DatePicker/date-picker.module.css +++ b/components/DatePicker/date-picker.module.css @@ -7,20 +7,6 @@ } } -.hideWrapper { - background-color: var(--Main-Grey-White); - border-radius: var(--Corner-radius-Large); - box-shadow: 0px 0px 14px 6px rgba(0, 0, 0, 0.1); - padding: var(--Spacing-x2) var(--Spacing-x3); - position: absolute; - /** - BookingWidget padding + - border-width + - wanted space below booking widget - */ - top: calc(100% + var(--Spacing-x2) + 1px + var(--Spacing-x4)); -} - .btn { background: none; border: none; @@ -34,117 +20,42 @@ opacity: 0.8; } -div.months { - flex-wrap: nowrap; +.hideWrapper { + background-color: var(--Main-Grey-White); } -.monthCaption { - justify-content: center; +@media screen and (max-width: 1366px) { + .container { + z-index: 10001; + } + + .hideWrapper { + bottom: 0; + left: 0; + overflow: auto; + position: fixed; + right: 0; + top: 100%; + transition: top 300ms ease; + z-index: 10001; + } + + .container[data-isopen="true"] .hideWrapper { + top: 0; + } } -.captionLabel { - text-transform: capitalize; -} - -td.day, -td.rangeEnd, -td.rangeStart { - font-family: var(--typography-Body-Bold-fontFamily); - font-size: var(--typography-Body-Bold-fontSize); - font-weight: 500; - letter-spacing: var(--typography-Body-Bold-letterSpacing); - line-height: var(--typography-Body-Bold-lineHeight); - text-decoration: var(--typography-Body-Bold-textDecoration); -} - -td.rangeEnd, -td.rangeStart { - background: var(--Base-Background-Primary-Normal); -} - -td.rangeEnd[aria-selected="true"]:not([data-outside="true"]) { - border-radius: 0 50% 50% 0; -} - -td.rangeStart[aria-selected="true"] { - border-radius: 50% 0 0 50%; -} - -td.rangeEnd[aria-selected="true"] button.dayButton:hover, -td.rangeStart[aria-selected="true"] button.dayButton:hover { - background: var(--Primary-Light-On-Surface-Accent); - border-radius: 50%; -} - -td.rangeEnd[aria-selected="true"]:not([data-outside="true"]) button.dayButton, -td.rangeStart[aria-selected="true"]:not([data-outside="true"]) - button.dayButton { - background: var(--Primary-Light-On-Surface-Accent); - border: none; - color: var(--Base-Button-Inverted-Fill-Normal); -} - -td.day, -td.day[data-today="true"] { - color: var(--UI-Text-High-contrast); - height: 40px; - padding: var(--Spacing-x-half); - width: 40px; -} - -td.day button.dayButton:hover { - background: var(--Base-Surface-Secondary-light-Hover); -} - -td.day[data-outside="true"] button.dayButton { - border: none; -} - -td.day:not(td.rangeEnd, td.rangeStart)[aria-selected="true"], -td.rangeMiddle[aria-selected="true"] button.dayButton { - background: var(--Base-Background-Primary-Normal); - border: none; - border-radius: 0; -} - -td.day[data-disabled="true"], -td.day[data-disabled="true"] button.dayButton, -td.day[data-outside="true"] ~ td.day[data-disabled="true"], -td.day[data-outside="true"] ~ td.day[data-disabled="true"] button.dayButton, -.week:has(td.day[data-outside="true"] ~ td.day[data-disabled="true"]) - td.day[data-outside="true"] - button.dayButton { - background: none; - color: var(--Base-Text-Disabled); - cursor: not-allowed; -} - -.weekDay { - color: var(--UI-Text-Placeholder); - font-family: var(--typography-Footnote-Labels-fontFamily); - font-size: var(--typography-Footnote-Labels-fontSize); - font-weight: var(--typography-Footnote-Labels-fontWeight); - letter-spacing: var(--typography-Footnote-Labels-letterSpacing); - line-height: var(--typography-Footnote-Labels-lineHeight); - text-decoration: var(--typography-Footnote-Labels-textDecoration); - text-transform: uppercase; -} - -.footer { - display: flex; - justify-content: flex-end; - margin-top: var(--Spacing-x2); -} - -.divider { - margin-top: var(--Spacing-x2); -} - -.nextButton { - transform: rotate(180deg); - right: 0; -} - -.previousButton { - left: 0; +@media screen and (min-width: 1367px) { + .hideWrapper { + border-radius: var(--Corner-radius-Large); + box-shadow: 0px 0px 14px 6px rgba(0, 0, 0, 0.1); + padding: var(--Spacing-x2) var(--Spacing-x3); + position: absolute; + /** + BookingWidget padding + + border-width + + wanted space below booking widget + */ + top: calc(100% + var(--Spacing-x2) + 1px + var(--Spacing-x4)); + } } diff --git a/components/DatePicker/index.tsx b/components/DatePicker/index.tsx index 0da5bb263..586da5208 100644 --- a/components/DatePicker/index.tsx +++ b/components/DatePicker/index.tsx @@ -1,13 +1,16 @@ "use client" +import { da, de, fi, nb, sv } from "date-fns/locale" import { useEffect, useRef, useState } from "react" import { useFormContext, useWatch } from "react-hook-form" +import { Lang } from "@/constants/languages" import { dt } from "@/lib/dt" import Body from "@/components/TempDesignSystem/Text/Body" import useLang from "@/hooks/useLang" -import DatePicker from "./DatePicker" +import DatePickerDesktop from "./Screen/Desktop" +import DatePickerMobile from "./Screen/Mobile" import styles from "./date-picker.module.css" @@ -15,6 +18,14 @@ import type { DateRange } from "react-day-picker" import type { DatePickerFormProps } from "@/types/components/datepicker" +const locales = { + [Lang.da]: da, + [Lang.de]: de, + [Lang.fi]: fi, + [Lang.no]: nb, + [Lang.sv]: sv, +} + export default function DatePickerForm({ name = "date" }: DatePickerFormProps) { const lang = useLang() const [isOpen, setIsOpen] = useState(false) @@ -44,11 +55,9 @@ export default function DatePickerForm({ name = "date" }: DatePickerFormProps) { setIsOpen(false) } } - - document.addEventListener("click", handleClickOutside) - + document.body.addEventListener("click", handleClickOutside) return () => { - document.removeEventListener("click", handleClickOutside) + document.body.removeEventListener("click", handleClickOutside) } }, [setIsOpen]) @@ -67,10 +76,17 @@ export default function DatePickerForm({ name = "date" }: DatePickerFormProps) {
- +
diff --git a/components/Forms/BookingWidget/FormContent/formContent.module.css b/components/Forms/BookingWidget/FormContent/formContent.module.css index eea85e3d3..4b62ecdcb 100644 --- a/components/Forms/BookingWidget/FormContent/formContent.module.css +++ b/components/Forms/BookingWidget/FormContent/formContent.module.css @@ -1,45 +1,80 @@ -.input { - display: flex; - gap: var(--Spacing-x2); -} - -.input input[type="text"] { - border: none; - height: 24px; -} - -.rooms, -.vouchers, -.when, -.where { - border-right: 1px solid var(--Base-Surface-Subtle-Normal); - width: 100%; -} - -.rooms, -.when { - max-width: 240px; - padding: var(--Spacing-x1) var(--Spacing-x-one-and-half); -} - -.vouchers { - max-width: 200px; - padding: var(--Spacing-x1) 0; -} - -.where { - max-width: 280px; - position: relative; -} - .options { display: flex; flex-direction: column; justify-content: center; width: 100%; - max-width: 158px; } .option { display: flex; -} \ No newline at end of file +} + +@media screen and (max-width: 1366px) { + .input { + display: grid; + gap: var(--Spacing-x2); + } + + .rooms, + .vouchers, + .when, + .where { + background-color: var(--Base-Background-Primary-Normal); + border-radius: var(--Corner-radius-Medium); + } + + .rooms, + .vouchers, + .when { + padding: var(--Spacing-x1) var(--Spacing-x-one-and-half); + } + + .options { + gap: var(--Spacing-x2); + margin-top: var(--Spacing-x2); + } + + .option { + gap: var(--Spacing-x2); + } +} + +@media screen and (min-width: 1367px) { + .input { + display: flex; + gap: var(--Spacing-x2); + } + + .rooms, + .vouchers, + .when, + .where { + border-right: 1px solid var(--Base-Surface-Subtle-Normal); + width: 100%; + } + + .input input[type="text"] { + border: none; + height: 24px; + } + + .rooms, + .when { + max-width: 240px; + padding: var(--Spacing-x1) var(--Spacing-x-one-and-half); + } + + .vouchers { + max-width: 200px; + padding: var(--Spacing-x1) 0; + } + + .where { + max-width: 280px; + position: relative; + } + + .options { + max-width: 158px; + } +} diff --git a/components/Forms/BookingWidget/FormContent/index.tsx b/components/Forms/BookingWidget/FormContent/index.tsx index 371140a7f..fcb5bb847 100644 --- a/components/Forms/BookingWidget/FormContent/index.tsx +++ b/components/Forms/BookingWidget/FormContent/index.tsx @@ -19,8 +19,8 @@ export default function FormContent({ const intl = useIntl() const selectedDate = useWatch({ name: "date" }) - const rooms = intl.formatMessage({ id: "Rooms & Guests" }) - const vouchers = intl.formatMessage({ id: "Booking codes and vouchers" }) + const rooms = intl.formatMessage({ id: "Guests & Rooms" }) + const vouchers = intl.formatMessage({ id: "Code / Voucher" }) const bonus = intl.formatMessage({ id: "Use bonus cheque" }) const reward = intl.formatMessage({ id: "Book reward night" }) @@ -47,20 +47,20 @@ export default function FormContent({
- + {vouchers}
-
+
-
+ +
+
) diff --git a/components/Forms/BookingWidget/form.module.css b/components/Forms/BookingWidget/form.module.css index e3c71df66..323d84037 100644 --- a/components/Forms/BookingWidget/form.module.css +++ b/components/Forms/BookingWidget/form.module.css @@ -1,15 +1,36 @@ .section { align-items: center; - display: flex; + display: grid; margin: 0 auto; max-width: var(--max-width-navigation); -} - -.form { width: 100%; } -.button { - width: 118px; - justify-content: center; +.form { + display: grid; + gap: var(--Spacing-x2); + width: 100%; +} + +@media screen and (max-width: 1366px) { + .form { + align-self: flex-start; + } + + .button { + align-self: flex-end; + justify-content: center; + width: 100%; + } +} + +@media screen and (min-width: 1367px) { + .section { + display: flex; + } + + .button { + justify-content: center; + width: 118px; + } } diff --git a/components/Forms/BookingWidget/index.tsx b/components/Forms/BookingWidget/index.tsx index 153d8417c..f78e38be0 100644 --- a/components/Forms/BookingWidget/index.tsx +++ b/components/Forms/BookingWidget/index.tsx @@ -1,22 +1,17 @@ "use client" -import { zodResolver } from "@hookform/resolvers/zod" import { useRouter } from "next/navigation" -import { FormProvider, useForm } from "react-hook-form" +import { useFormContext } from "react-hook-form" import { useIntl } from "react-intl" -import { dt } from "@/lib/dt" - import Button from "@/components/TempDesignSystem/Button" import Caption from "@/components/TempDesignSystem/Text/Caption" import FormContent from "./FormContent" -import { bookingWidgetSchema } from "./schema" import styles from "./form.module.css" import type { BookingWidgetSchema } from "@/types/components/bookingWidget" import type { BookingWidgetFormProps } from "@/types/components/form/bookingwidget" -import type { Location } from "@/types/trpc/routers/hotel/locations" const formId = "booking-widget" @@ -24,40 +19,8 @@ export default function Form({ locations }: BookingWidgetFormProps) { const intl = useIntl() const router = useRouter() - const sessionStorageSearchData = - typeof window !== "undefined" - ? sessionStorage.getItem("searchData") - : undefined - const initialSelectedLocation: Location | undefined = sessionStorageSearchData - ? JSON.parse(sessionStorageSearchData) - : undefined - const methods = useForm({ - defaultValues: { - search: initialSelectedLocation?.name ?? "", - location: sessionStorageSearchData - ? encodeURIComponent(sessionStorageSearchData) - : undefined, - date: { - // UTC is required to handle requests from far away timezones https://scandichotels.atlassian.net/browse/SWAP-6375 & PET-507 - // This is specifically to handle timezones falling in different dates. - from: dt().utc().format("YYYY-MM-DD"), - to: dt().utc().add(1, "day").format("YYYY-MM-DD"), - }, - bookingCode: "", - redemption: false, - voucher: false, - rooms: [ - { - adults: 1, - childs: [], - }, - ], - }, - shouldFocusError: false, - mode: "all", - resolver: zodResolver(bookingWidgetSchema), - reValidateMode: "onChange", - }) + const { formState, handleSubmit, register } = + useFormContext() function onSubmit(data: BookingWidgetSchema) { data.location = JSON.parse(decodeURIComponent(data.location)) @@ -70,25 +33,24 @@ export default function Form({ locations }: BookingWidgetFormProps) { return (
- - - - + +
diff --git a/components/Forms/BookingWidget/schema.ts b/components/Forms/BookingWidget/schema.ts index 1455f65a7..f474a72e1 100644 --- a/components/Forms/BookingWidget/schema.ts +++ b/components/Forms/BookingWidget/schema.ts @@ -29,7 +29,7 @@ export const bookingWidgetSchema = z.object({ // This will be updated when working in guests component z.object({ adults: z.number().default(1), - childs: z.array( + children: z.array( z.object({ age: z.number(), bed: z.number(), diff --git a/components/Icons/Edit.tsx b/components/Icons/Edit.tsx new file mode 100644 index 000000000..209e16883 --- /dev/null +++ b/components/Icons/Edit.tsx @@ -0,0 +1,36 @@ +import { iconVariants } from "./variants" + +import type { IconProps } from "@/types/components/icon" + +export default function EditIcon({ className, color, ...props }: IconProps) { + const classNames = iconVariants({ className, color }) + return ( + + + + + + + + + ) +} diff --git a/components/Icons/index.tsx b/components/Icons/index.tsx index db5e29bbb..78e3f8de1 100644 --- a/components/Icons/index.tsx +++ b/components/Icons/index.tsx @@ -23,6 +23,7 @@ export { default as CrossCircle } from "./CrossCircle" export { default as CulturalIcon } from "./Cultural" export { default as DeleteIcon } from "./Delete" export { default as DoorOpenIcon } from "./DoorOpen" +export { default as EditIcon } from "./Edit" export { default as ElectricBikeIcon } from "./ElectricBike" export { default as EmailIcon } from "./Email" export { default as ErrorCircleIcon } from "./ErrorCircle" diff --git a/components/TempDesignSystem/Divider/divider.module.css b/components/TempDesignSystem/Divider/divider.module.css index 1896c546b..6753e7533 100644 --- a/components/TempDesignSystem/Divider/divider.module.css +++ b/components/TempDesignSystem/Divider/divider.module.css @@ -1,40 +1,47 @@ .divider { - border-bottom-style: solid; - border-bottom-width: 1px; + pointer-events: none; +} + +.horizontal { height: 1px; width: 100%; } -.dotted { - border-bottom-style: dotted; +.vertical { + height: 100%; + width: 1px; } .burgundy { - border-bottom-color: var(--Scandic-Brand-Burgundy); + background-color: var(--Scandic-Brand-Burgundy); } .pale { - border-bottom-color: var(--Primary-Dark-On-Surface-Text); + background-color: var(--Primary-Dark-On-Surface-Text); } .peach { - border-bottom-color: var(--Primary-Light-On-Surface-Divider); + background-color: var(--Primary-Light-On-Surface-Divider); } .beige { - border-bottom-color: var(--Scandic-Beige-20); + background-color: var(--Scandic-Beige-20); } .white { - border-bottom-color: var(--UI-Opacity-White-100); + background-color: var(--UI-Opacity-White-100); } .subtle { - border-bottom-color: var(--Base-Border-Subtle); + background-color: var(--Base-Border-Subtle); } .primaryLightSubtle { - border-bottom-color: var(--Primary-Light-On-Surface-Divider-subtle); + background-color: var(--Primary-Light-On-Surface-Divider-subtle); +} + +.baseSurfaceSubtleNormal { + background-color: var(--Base-Surface-Subtle-Normal); } .opacity100 { diff --git a/components/TempDesignSystem/Divider/variants.ts b/components/TempDesignSystem/Divider/variants.ts index 8611474fa..43f7899e0 100644 --- a/components/TempDesignSystem/Divider/variants.ts +++ b/components/TempDesignSystem/Divider/variants.ts @@ -5,6 +5,7 @@ import styles from "./divider.module.css" export const dividerVariants = cva(styles.divider, { variants: { color: { + baseSurfaceSubtleNormal: styles.baseSurfaceSubtleNormal, beige: styles.beige, burgundy: styles.burgundy, pale: styles.pale, @@ -18,13 +19,13 @@ export const dividerVariants = cva(styles.divider, { 8: styles.opacity8, }, variant: { - default: styles.default, - dotted: styles.dotted, + horizontal: styles.horizontal, + vertical: styles.vertical, }, }, defaultVariants: { color: "burgundy", opacity: 100, - variant: "default", + variant: "horizontal", }, }) diff --git a/components/TempDesignSystem/Text/Caption/caption.module.css b/components/TempDesignSystem/Text/Caption/caption.module.css index 18bd252b6..627f7132a 100644 --- a/components/TempDesignSystem/Text/Caption/caption.module.css +++ b/components/TempDesignSystem/Text/Caption/caption.module.css @@ -63,6 +63,10 @@ p.caption { color: var(--UI-Text-Active); } +.uiTextMediumContrast { + color: var(--UI-Text-Medium-contrast); +} + .center { text-align: center; } diff --git a/components/TempDesignSystem/Text/Caption/variants.ts b/components/TempDesignSystem/Text/Caption/variants.ts index dc0437fff..4b0dd96af 100644 --- a/components/TempDesignSystem/Text/Caption/variants.ts +++ b/components/TempDesignSystem/Text/Caption/variants.ts @@ -12,6 +12,7 @@ const config = { red: styles.red, white: styles.white, uiTextActive: styles.uiTextActive, + uiTextMediumContrast: styles.uiTextMediumContrast, }, textTransform: { bold: styles.bold, diff --git a/i18n/dictionaries/da.json b/i18n/dictionaries/da.json index 499f67cb3..35b5e759a 100644 --- a/i18n/dictionaries/da.json +++ b/i18n/dictionaries/da.json @@ -23,7 +23,7 @@ "Bed type": "Seng type", "Book": "Book", "Book reward night": "Book bonusnat", - "Booking codes and vouchers": "Bookingkoder og vouchers", + "Code / Voucher": "Bookingkoder / voucher", "Booking number": "Bookingnummer", "Breakfast": "Morgenmad", "Breakfast excluded": "Morgenmad ikke inkluderet", @@ -183,10 +183,11 @@ "Room & Terms": "Værelse & Vilkår", "Room facilities": "Værelsesfaciliteter", "Rooms": "Værelser", - "Rooms & Guests": "Værelser & gæster", + "Guests & Rooms": "Gæster & værelser", "Save": "Gemme", "Scandic Friends Mastercard": "Scandic Friends Mastercard", "Scandic Friends Point Shop": "Scandic Friends Point Shop", + "Search": "Søge", "See all photos": "Se alle billeder", "See hotel details": "Se hoteloplysninger", "See room details": "Se værelsesdetaljer", diff --git a/i18n/dictionaries/de.json b/i18n/dictionaries/de.json index a215a8263..916804097 100644 --- a/i18n/dictionaries/de.json +++ b/i18n/dictionaries/de.json @@ -23,7 +23,7 @@ "Bed type": "Bettentyp", "Book": "Buchen", "Book reward night": "Bonusnacht buchen", - "Booking codes and vouchers": "Buchungscodes und Gutscheine", + "Code / Voucher": "Buchungscodes / Gutscheine", "Booking number": "Buchungsnummer", "Breakfast": "Frühstück", "Breakfast excluded": "Frühstück nicht inbegriffen", @@ -182,10 +182,11 @@ "Room & Terms": "Zimmer & Bedingungen", "Room facilities": "Zimmerausstattung", "Rooms": "Räume", - "Rooms & Guests": "Zimmer & Gäste", + "Guests & Rooms": "Gäste & Zimmer", "Save": "Speichern", "Scandic Friends Mastercard": "Scandic Friends Mastercard", "Scandic Friends Point Shop": "Scandic Friends Point Shop", + "Search": "Suchen", "See all photos": "Alle Fotos ansehen", "See hotel details": "Hotelinformationen ansehen", "See room details": "Zimmerdetails ansehen", diff --git a/i18n/dictionaries/en.json b/i18n/dictionaries/en.json index fa90f4f4e..c663a0f80 100644 --- a/i18n/dictionaries/en.json +++ b/i18n/dictionaries/en.json @@ -23,7 +23,7 @@ "Bed type": "Bed type", "Book": "Book", "Book reward night": "Book reward night", - "Booking codes and vouchers": "Booking codes and vouchers", + "Code / Voucher": "Code / Voucher", "Booking number": "Booking number", "Breakfast": "Breakfast", "Breakfast excluded": "Breakfast excluded", @@ -60,6 +60,7 @@ "Day": "Day", "Description": "Description", "Destinations & hotels": "Destinations & hotels", + "Destination": "Destination", "Discard changes": "Discard changes", "Discard unsaved changes?": "Discard unsaved changes?", "Distance to city centre": "{number}km to city centre", @@ -183,11 +184,12 @@ "Room & Terms": "Room & Terms", "Room facilities": "Room facilities", "Rooms": "Rooms", - "Rooms & Guests": "Rooms & Guests", + "Guests & Rooms": "Guests & Rooms", "Save": "Save", "Scandic Friends Mastercard": "Scandic Friends Mastercard", "Scandic Friends Point Shop": "Scandic Friends Point Shop", "See all photos": "See all photos", + "Search": "Search", "See hotel details": "See hotel details", "See room details": "See room details", "See rooms": "See rooms", @@ -264,7 +266,9 @@ "Zoom in": "Zoom in", "Zoom out": "Zoom out", "as of today": "as of today", + "booking.adults": "{totalAdults, plural, one {# adult} other {# adults}}", "booking.nights": "{totalNights, plural, one {# night} other {# nights}}", + "booking.rooms": "{totalRooms, plural, one {# room} other {# rooms}}", "by": "by", "characters": "characters", "hotelPages.rooms.roomCard.person": "person", diff --git a/i18n/dictionaries/fi.json b/i18n/dictionaries/fi.json index 117fcad58..aa12eccf0 100644 --- a/i18n/dictionaries/fi.json +++ b/i18n/dictionaries/fi.json @@ -23,7 +23,6 @@ "Bed type": "Vuodetyyppi", "Book": "Varaa", "Book reward night": "Kirjapalkinto-ilta", - "Booking codes and vouchers": "Varauskoodit ja kupongit", "Booking number": "Varausnumero", "Breakfast": "Aamiainen", "Breakfast excluded": "Aamiainen ei sisälly", @@ -45,6 +44,7 @@ "Close menu": "Sulje valikko", "Close my pages menu": "Sulje omat sivut -valikko", "Close the map": "Sulje kartta", + "Code / Voucher": "Varauskoodit / kupongit", "Coming up": "Tulossa", "Compare all levels": "Vertaa kaikkia tasoja", "Contact us": "Ota meihin yhteyttä", @@ -85,6 +85,7 @@ "Get inspired": "Inspiroidu", "Go back to edit": "Palaa muokkaamaan", "Go back to overview": "Palaa yleiskatsaukseen", + "Guests & Rooms": "Vieraat & Huoneet", "Hi": "Hi", "Highest level": "Korkein taso", "Hospital": "Sairaala", @@ -183,11 +184,10 @@ "Room & Terms": "Huone & Ehdot", "Room facilities": "Huoneen varustelu", "Rooms": "Huoneet", - "Rooms & Guests": "Huoneet & Vieraat", - "Rooms & Guestss": "Huoneet & Vieraat", "Save": "Tallenna", "Scandic Friends Mastercard": "Scandic Friends Mastercard", "Scandic Friends Point Shop": "Scandic Friends Point Shop", + "Search": "Haku", "See all photos": "Katso kaikki kuvat", "See hotel details": "Katso hotellin tiedot", "See room details": "Katso huoneen tiedot", @@ -281,4 +281,4 @@ "spendable points expiring by": "{points} pistettä vanhenee {date} mennessä", "to": "to", "uppercase letter": "iso kirjain" -} +} \ No newline at end of file diff --git a/i18n/dictionaries/no.json b/i18n/dictionaries/no.json index ea8aa4548..ed600ecb5 100644 --- a/i18n/dictionaries/no.json +++ b/i18n/dictionaries/no.json @@ -23,7 +23,7 @@ "Bed type": "Seng type", "Book": "Bestill", "Book reward night": "Bestill belønningskveld", - "Booking codes and vouchers": "Bestillingskoder og kuponger", + "Code / Voucher": "Bestillingskoder / kuponger", "Booking number": "Bestillingsnummer", "Breakfast": "Frokost", "Breakfast excluded": "Frokost ekskludert", @@ -183,10 +183,11 @@ "Room & Terms": "Rom & Vilkår", "Room facilities": "Romfasiliteter", "Rooms": "Rom", - "Rooms & Guests": "Rom og gjester", + "Guests & Rooms": "Gjester & rom", "Save": "Lagre", "Scandic Friends Mastercard": "Scandic Friends Mastercard", "Scandic Friends Point Shop": "Scandic Friends Point Shop", + "Search": "Søk", "See all photos": "Se alle bilder", "See hotel details": "Se hotellinformasjon", "See room details": "Se detaljer om rommet", diff --git a/i18n/dictionaries/sv.json b/i18n/dictionaries/sv.json index 864b6e4ae..9f3f71511 100644 --- a/i18n/dictionaries/sv.json +++ b/i18n/dictionaries/sv.json @@ -23,7 +23,7 @@ "Bed type": "Sängtyp", "Book": "Boka", "Book reward night": "Boka frinatt", - "Booking codes and vouchers": "Bokningskoder och kuponger", + "Code / Voucher": "Bokningskoder / kuponger", "Booking number": "Bokningsnummer", "Breakfast": "Frukost", "Breakfast excluded": "Frukost ingår ej", @@ -183,10 +183,11 @@ "Room & Terms": "Rum & Villkor", "Room facilities": "Rumfaciliteter", "Rooms": "Rum", - "Rooms & Guests": "Rum och gäster", + "Guests & Rooms": "Gäster & rum", "Save": "Spara", "Scandic Friends Mastercard": "Scandic Friends Mastercard", "Scandic Friends Point Shop": "Scandic Friends Point Shop", + "Search": "Sök", "See all photos": "Se alla foton", "See hotel details": "Se hotellinformation", "See room details": "Se rumsdetaljer", diff --git a/types/components/bookingWidget/index.ts b/types/components/bookingWidget/index.ts index 1998e3778..ab4d194d9 100644 --- a/types/components/bookingWidget/index.ts +++ b/types/components/bookingWidget/index.ts @@ -2,4 +2,14 @@ import { z } from "zod" import { bookingWidgetSchema } from "@/components/Forms/BookingWidget/schema" +import type { Locations } from "@/types/trpc/routers/hotel/locations" + export type BookingWidgetSchema = z.output + +export interface BookingWidgetClientProps { + locations: Locations +} + +export interface BookingWidgetToggleButtonProps { + openMobileSearch: () => void +} diff --git a/types/components/datepicker.ts b/types/components/datepicker.ts index 2b82966c1..1d30aa3ae 100644 --- a/types/components/datepicker.ts +++ b/types/components/datepicker.ts @@ -1,11 +1,17 @@ +import { Lang } from "@/constants/languages" + +import type { Locale } from "date-fns" import type { DateRange } from "react-day-picker" export interface DatePickerFormProps { name?: string } +type LangWithoutEn = Lang.da | Lang.de | Lang.fi | Lang.no | Lang.sv + export interface DatePickerProps { close: () => void handleOnSelect: (selected: DateRange) => void - initialSelected?: DateRange + locales: Record + selectedDate: DateRange } diff --git a/utils/debounce.ts b/utils/debounce.ts new file mode 100644 index 000000000..80eb13e96 --- /dev/null +++ b/utils/debounce.ts @@ -0,0 +1,10 @@ +export function debounce(func: Function, delay = 300) { + let debounceTimer: ReturnType + return function () { + // @ts-expect-error this in TypeScript + const context = this + const args = arguments + clearTimeout(debounceTimer) + debounceTimer = setTimeout(() => func.apply(context, args), delay) + } +}