feat: add mobile ui to calendar
This commit is contained in:
@@ -11,7 +11,7 @@ export default function Stats({ user }: UserProps) {
|
|||||||
return (
|
return (
|
||||||
<section className={styles.stats}>
|
<section className={styles.stats}>
|
||||||
<Points user={user} />
|
<Points user={user} />
|
||||||
<Divider variant="default" color="pale" />
|
<Divider color="pale" />
|
||||||
<ExpiringPoints user={user} />
|
<ExpiringPoints user={user} />
|
||||||
</section>
|
</section>
|
||||||
)
|
)
|
||||||
|
|||||||
107
components/BookingWidget/Client.tsx
Normal file
107
components/BookingWidget/Client.tsx
Normal file
@@ -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<BookingWidgetSchema>({
|
||||||
|
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 (
|
||||||
|
<FormProvider {...methods}>
|
||||||
|
<section className={styles.container} data-open={isOpen}>
|
||||||
|
<button
|
||||||
|
className={styles.close}
|
||||||
|
onClick={closeMobileSearch}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<CloseLarge />
|
||||||
|
</button>
|
||||||
|
<Form locations={locations} />
|
||||||
|
</section>
|
||||||
|
<MobileToggleButton openMobileSearch={openMobileSearch} />
|
||||||
|
</FormProvider>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
102
components/BookingWidget/MobileToggleButton/index.tsx
Normal file
102
components/BookingWidget/MobileToggleButton/index.tsx
Normal file
@@ -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 (
|
||||||
|
<div className={styles.complete} onClick={openMobileSearch} role="button">
|
||||||
|
<div>
|
||||||
|
<Caption color="red">{parsedLocation.name}</Caption>
|
||||||
|
<Caption>
|
||||||
|
{`${selectedFromDate} - ${selectedToDate} (${intl.formatMessage(
|
||||||
|
{ id: "booking.nights" },
|
||||||
|
{ totalNights: nights }
|
||||||
|
)}) ${intl.formatMessage({ id: "booking.adults" }, { totalAdults })}, ${intl.formatMessage({ id: "booking.rooms" }, { totalRooms })}`}
|
||||||
|
</Caption>
|
||||||
|
</div>
|
||||||
|
<div className={styles.icon}>
|
||||||
|
<EditIcon color="white" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.partial} onClick={openMobileSearch} role="button">
|
||||||
|
<div>
|
||||||
|
<Caption color="red">{intl.formatMessage({ id: "Where to" })}</Caption>
|
||||||
|
<Body color="uiTextPlaceholder">
|
||||||
|
{parsedLocation
|
||||||
|
? parsedLocation.name
|
||||||
|
: intl.formatMessage({ id: "Destination" })}
|
||||||
|
</Body>
|
||||||
|
</div>
|
||||||
|
<Divider color="baseSurfaceSubtleNormal" variant="vertical" />
|
||||||
|
<div>
|
||||||
|
<Caption color="red">
|
||||||
|
{intl.formatMessage(
|
||||||
|
{ id: "booking.nights" },
|
||||||
|
{ totalNights: nights }
|
||||||
|
)}
|
||||||
|
</Caption>
|
||||||
|
<Body>
|
||||||
|
{selectedFromDate} - {selectedToDate}
|
||||||
|
</Body>
|
||||||
|
</div>
|
||||||
|
<div className={styles.icon}>
|
||||||
|
<SearchIcon color="white" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,5 +1,28 @@
|
|||||||
.container {
|
@media screen and (max-width: 1366px) {
|
||||||
display: none;
|
.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) {
|
@media screen and (min-width: 1367px) {
|
||||||
@@ -8,4 +31,8 @@
|
|||||||
border-top: 1px solid var(--Base-Border-Subtle);
|
border-top: 1px solid var(--Base-Border-Subtle);
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.close {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
import { getLocations } from "@/lib/trpc/memoizedRequests"
|
import { getLocations } from "@/lib/trpc/memoizedRequests"
|
||||||
|
|
||||||
import Form from "@/components/Forms/BookingWidget"
|
import BookingWidgetClient from "./Client"
|
||||||
|
|
||||||
import styles from "./bookingWidget.module.css"
|
|
||||||
|
|
||||||
export function preload() {
|
export function preload() {
|
||||||
void getLocations()
|
void getLocations()
|
||||||
@@ -15,9 +13,5 @@ export default async function BookingWidget() {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return <BookingWidgetClient locations={locations.data} />
|
||||||
<section className={styles.container}>
|
|
||||||
<Form locations={locations.data} />
|
|
||||||
</section>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,49 +1,30 @@
|
|||||||
"use client"
|
"use client"
|
||||||
import { da, de, fi, nb, sv } from "date-fns/locale"
|
import { DayPicker } from "react-day-picker"
|
||||||
import { useState } from "react"
|
|
||||||
import { type DateRange, DayPicker } from "react-day-picker"
|
|
||||||
import { useIntl } from "react-intl"
|
import { useIntl } from "react-intl"
|
||||||
|
|
||||||
import { Lang } from "@/constants/languages"
|
import { Lang } from "@/constants/languages"
|
||||||
import { dt } from "@/lib/dt"
|
import { dt } from "@/lib/dt"
|
||||||
|
|
||||||
|
import { ChevronLeftIcon } from "@/components/Icons"
|
||||||
import Button from "@/components/TempDesignSystem/Button"
|
import Button from "@/components/TempDesignSystem/Button"
|
||||||
import Divider from "@/components/TempDesignSystem/Divider"
|
import Divider from "@/components/TempDesignSystem/Divider"
|
||||||
import Caption from "@/components/TempDesignSystem/Text/Caption"
|
import Caption from "@/components/TempDesignSystem/Text/Caption"
|
||||||
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
|
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
|
||||||
import useLang from "@/hooks/useLang"
|
import useLang from "@/hooks/useLang"
|
||||||
|
|
||||||
import { ChevronLeftIcon } from "../Icons"
|
import styles from "./desktop.module.css"
|
||||||
|
|
||||||
import styles from "./date-picker.module.css"
|
|
||||||
import classNames from "react-day-picker/style.module.css"
|
import classNames from "react-day-picker/style.module.css"
|
||||||
|
|
||||||
import type { DatePickerProps } from "@/types/components/datepicker"
|
import type { DatePickerProps } from "@/types/components/datepicker"
|
||||||
|
|
||||||
const locales = {
|
export default function DatePickerDesktop({
|
||||||
[Lang.da]: da,
|
|
||||||
[Lang.de]: de,
|
|
||||||
[Lang.fi]: fi,
|
|
||||||
[Lang.no]: nb,
|
|
||||||
[Lang.sv]: sv,
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function DatePicker({
|
|
||||||
close,
|
close,
|
||||||
handleOnSelect,
|
handleOnSelect,
|
||||||
initialSelected = {
|
locales,
|
||||||
from: undefined,
|
selectedDate,
|
||||||
to: undefined,
|
|
||||||
},
|
|
||||||
}: DatePickerProps) {
|
}: DatePickerProps) {
|
||||||
const lang = useLang()
|
const lang = useLang()
|
||||||
const intl = useIntl()
|
const intl = useIntl()
|
||||||
const [selectedDate, setSelectedDate] = useState<DateRange>(initialSelected)
|
|
||||||
|
|
||||||
function handleSelectDate(selected: DateRange) {
|
|
||||||
handleOnSelect(selected)
|
|
||||||
setSelectedDate(selected)
|
|
||||||
}
|
|
||||||
|
|
||||||
/** English is default language and doesn't need to be imported */
|
/** English is default language and doesn't need to be imported */
|
||||||
const locale = lang === Lang.en ? undefined : locales[lang]
|
const locale = lang === Lang.en ? undefined : locales[lang]
|
||||||
@@ -63,6 +44,7 @@ export default function DatePicker({
|
|||||||
range_end: styles.rangeEnd,
|
range_end: styles.rangeEnd,
|
||||||
range_middle: styles.rangeMiddle,
|
range_middle: styles.rangeMiddle,
|
||||||
range_start: styles.rangeStart,
|
range_start: styles.rangeStart,
|
||||||
|
root: `${classNames.root} ${styles.container}`,
|
||||||
week: styles.week,
|
week: styles.week,
|
||||||
weekday: `${classNames.weekday} ${styles.weekDay}`,
|
weekday: `${classNames.weekday} ${styles.weekDay}`,
|
||||||
}}
|
}}
|
||||||
@@ -78,7 +60,7 @@ export default function DatePicker({
|
|||||||
locale={locale}
|
locale={locale}
|
||||||
mode="range"
|
mode="range"
|
||||||
numberOfMonths={2}
|
numberOfMonths={2}
|
||||||
onSelect={handleSelectDate}
|
onSelect={handleOnSelect}
|
||||||
pagedNavigation
|
pagedNavigation
|
||||||
required
|
required
|
||||||
selected={selectedDate}
|
selected={selectedDate}
|
||||||
140
components/DatePicker/Screen/Mobile.tsx
Normal file
140
components/DatePicker/Screen/Mobile.tsx
Normal file
@@ -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<HTMLSelectElement>) {
|
||||||
|
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 (
|
||||||
|
<DayPicker
|
||||||
|
classNames={{
|
||||||
|
...classNames,
|
||||||
|
caption_label: `${classNames.caption_label} ${styles.captionLabel}`,
|
||||||
|
day: `${classNames.day} ${styles.day}`,
|
||||||
|
day_button: `${classNames.day_button} ${styles.dayButton}`,
|
||||||
|
footer: styles.footer,
|
||||||
|
month: styles.month,
|
||||||
|
month_caption: `${classNames.month_caption} ${styles.monthCaption}`,
|
||||||
|
months: styles.months,
|
||||||
|
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}`,
|
||||||
|
}}
|
||||||
|
disabled={{ from: startOfCurrentMonth, to: yesterday }}
|
||||||
|
endMonth={decemberOfYear}
|
||||||
|
excludeDisabled
|
||||||
|
footer
|
||||||
|
formatters={{
|
||||||
|
formatWeekdayName(weekday) {
|
||||||
|
return dt(weekday).locale(lang).format("ddd")
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
hideNavigation
|
||||||
|
lang={lang}
|
||||||
|
locale={locale}
|
||||||
|
mode="range"
|
||||||
|
/** Showing full year or what's left of it */
|
||||||
|
numberOfMonths={12}
|
||||||
|
onSelect={handleOnSelect}
|
||||||
|
required
|
||||||
|
selected={selectedDate}
|
||||||
|
startMonth={startMonth}
|
||||||
|
weekStartsOn={1}
|
||||||
|
components={{
|
||||||
|
Footer(props) {
|
||||||
|
return (
|
||||||
|
<footer className={props.className}>
|
||||||
|
<Button
|
||||||
|
className={styles.button}
|
||||||
|
intent="tertiary"
|
||||||
|
onPress={close}
|
||||||
|
size="large"
|
||||||
|
theme="base"
|
||||||
|
>
|
||||||
|
<Body color="white" textTransform="bold">
|
||||||
|
{intl.formatMessage({ id: "Select dates" })}
|
||||||
|
</Body>
|
||||||
|
</Button>
|
||||||
|
<div className={styles.backdrop} />
|
||||||
|
</footer>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
MonthCaption(props) {
|
||||||
|
return (
|
||||||
|
<div className={props.className}>
|
||||||
|
<Subtitle asChild type="two">
|
||||||
|
{props.children}
|
||||||
|
</Subtitle>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
Root({ children, ...props }) {
|
||||||
|
return (
|
||||||
|
<div {...props}>
|
||||||
|
<header className={styles.header}>
|
||||||
|
<select
|
||||||
|
className={styles.select}
|
||||||
|
defaultValue={selectedYear}
|
||||||
|
onChange={handleSelectYear}
|
||||||
|
>
|
||||||
|
{fiftyYearsAhead.map((year) => (
|
||||||
|
<option key={year} value={year}>
|
||||||
|
{year}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
<button className={styles.close} onClick={close} type="button">
|
||||||
|
<CloseLarge />
|
||||||
|
</button>
|
||||||
|
</header>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
120
components/DatePicker/Screen/desktop.module.css
Normal file
120
components/DatePicker/Screen/desktop.module.css
Normal file
@@ -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;
|
||||||
|
}
|
||||||
173
components/DatePicker/Screen/mobile.module.css
Normal file
173
components/DatePicker/Screen/mobile.module.css
Normal file
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 {
|
.btn {
|
||||||
background: none;
|
background: none;
|
||||||
border: none;
|
border: none;
|
||||||
@@ -34,117 +20,42 @@
|
|||||||
opacity: 0.8;
|
opacity: 0.8;
|
||||||
}
|
}
|
||||||
|
|
||||||
div.months {
|
.hideWrapper {
|
||||||
flex-wrap: nowrap;
|
background-color: var(--Main-Grey-White);
|
||||||
}
|
}
|
||||||
|
|
||||||
.monthCaption {
|
@media screen and (max-width: 1366px) {
|
||||||
justify-content: center;
|
.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 {
|
@media screen and (min-width: 1367px) {
|
||||||
text-transform: capitalize;
|
.hideWrapper {
|
||||||
}
|
border-radius: var(--Corner-radius-Large);
|
||||||
|
box-shadow: 0px 0px 14px 6px rgba(0, 0, 0, 0.1);
|
||||||
td.day,
|
padding: var(--Spacing-x2) var(--Spacing-x3);
|
||||||
td.rangeEnd,
|
position: absolute;
|
||||||
td.rangeStart {
|
/**
|
||||||
font-family: var(--typography-Body-Bold-fontFamily);
|
BookingWidget padding +
|
||||||
font-size: var(--typography-Body-Bold-fontSize);
|
border-width +
|
||||||
font-weight: 500;
|
wanted space below booking widget
|
||||||
letter-spacing: var(--typography-Body-Bold-letterSpacing);
|
*/
|
||||||
line-height: var(--typography-Body-Bold-lineHeight);
|
top: calc(100% + var(--Spacing-x2) + 1px + var(--Spacing-x4));
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,16 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
import { da, de, fi, nb, sv } from "date-fns/locale"
|
||||||
import { useEffect, useRef, useState } from "react"
|
import { useEffect, useRef, useState } from "react"
|
||||||
import { useFormContext, useWatch } from "react-hook-form"
|
import { useFormContext, useWatch } from "react-hook-form"
|
||||||
|
|
||||||
|
import { Lang } from "@/constants/languages"
|
||||||
import { dt } from "@/lib/dt"
|
import { dt } from "@/lib/dt"
|
||||||
|
|
||||||
import Body from "@/components/TempDesignSystem/Text/Body"
|
import Body from "@/components/TempDesignSystem/Text/Body"
|
||||||
import useLang from "@/hooks/useLang"
|
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"
|
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"
|
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) {
|
export default function DatePickerForm({ name = "date" }: DatePickerFormProps) {
|
||||||
const lang = useLang()
|
const lang = useLang()
|
||||||
const [isOpen, setIsOpen] = useState(false)
|
const [isOpen, setIsOpen] = useState(false)
|
||||||
@@ -44,11 +55,9 @@ export default function DatePickerForm({ name = "date" }: DatePickerFormProps) {
|
|||||||
setIsOpen(false)
|
setIsOpen(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
document.body.addEventListener("click", handleClickOutside)
|
||||||
document.addEventListener("click", handleClickOutside)
|
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
document.removeEventListener("click", handleClickOutside)
|
document.body.removeEventListener("click", handleClickOutside)
|
||||||
}
|
}
|
||||||
}, [setIsOpen])
|
}, [setIsOpen])
|
||||||
|
|
||||||
@@ -67,10 +76,17 @@ export default function DatePickerForm({ name = "date" }: DatePickerFormProps) {
|
|||||||
<input {...register("date.from")} type="hidden" />
|
<input {...register("date.from")} type="hidden" />
|
||||||
<input {...register("date.to")} type="hidden" />
|
<input {...register("date.to")} type="hidden" />
|
||||||
<div aria-modal className={styles.hideWrapper} role="dialog">
|
<div aria-modal className={styles.hideWrapper} role="dialog">
|
||||||
<DatePicker
|
<DatePickerDesktop
|
||||||
close={close}
|
close={close}
|
||||||
handleOnSelect={handleSelectDate}
|
handleOnSelect={handleSelectDate}
|
||||||
initialSelected={selectedDate}
|
locales={locales}
|
||||||
|
selectedDate={selectedDate}
|
||||||
|
/>
|
||||||
|
<DatePickerMobile
|
||||||
|
close={close}
|
||||||
|
handleOnSelect={handleSelectDate}
|
||||||
|
locales={locales}
|
||||||
|
selectedDate={selectedDate}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -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 {
|
.options {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 158px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.option {
|
.option {
|
||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -19,8 +19,8 @@ export default function FormContent({
|
|||||||
const intl = useIntl()
|
const intl = useIntl()
|
||||||
const selectedDate = useWatch({ name: "date" })
|
const selectedDate = useWatch({ name: "date" })
|
||||||
|
|
||||||
const rooms = intl.formatMessage({ id: "Rooms & Guests" })
|
const rooms = intl.formatMessage({ id: "Guests & Rooms" })
|
||||||
const vouchers = intl.formatMessage({ id: "Booking codes and vouchers" })
|
const vouchers = intl.formatMessage({ id: "Code / Voucher" })
|
||||||
const bonus = intl.formatMessage({ id: "Use bonus cheque" })
|
const bonus = intl.formatMessage({ id: "Use bonus cheque" })
|
||||||
const reward = intl.formatMessage({ id: "Book reward night" })
|
const reward = intl.formatMessage({ id: "Book reward night" })
|
||||||
|
|
||||||
@@ -47,20 +47,20 @@ export default function FormContent({
|
|||||||
<input type="text" placeholder={rooms} />
|
<input type="text" placeholder={rooms} />
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.vouchers}>
|
<div className={styles.vouchers}>
|
||||||
<Caption color="textMediumContrast" textTransform="bold">
|
<Caption color="uiTextMediumContrast" textTransform="bold">
|
||||||
{vouchers}
|
{vouchers}
|
||||||
</Caption>
|
</Caption>
|
||||||
<input type="text" placeholder={vouchers} />
|
<input type="text" placeholder={vouchers} />
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.options}>
|
<div className={styles.options}>
|
||||||
<div className={styles.option}>
|
<label className={styles.option}>
|
||||||
<input type="checkbox" />
|
<input type="checkbox" />
|
||||||
<Caption color="textMediumContrast">{bonus}</Caption>
|
<Caption color="textMediumContrast">{bonus}</Caption>
|
||||||
</div>
|
</label>
|
||||||
<div className={styles.option}>
|
<label className={styles.option}>
|
||||||
<input type="checkbox" />
|
<input type="checkbox" />
|
||||||
<Caption color="textMediumContrast">{reward}</Caption>
|
<Caption color="textMediumContrast">{reward}</Caption>
|
||||||
</div>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,15 +1,36 @@
|
|||||||
.section {
|
.section {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
display: flex;
|
display: grid;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
max-width: var(--max-width-navigation);
|
max-width: var(--max-width-navigation);
|
||||||
}
|
|
||||||
|
|
||||||
.form {
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.button {
|
.form {
|
||||||
width: 118px;
|
display: grid;
|
||||||
justify-content: center;
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,22 +1,17 @@
|
|||||||
"use client"
|
"use client"
|
||||||
import { zodResolver } from "@hookform/resolvers/zod"
|
|
||||||
import { useRouter } from "next/navigation"
|
import { useRouter } from "next/navigation"
|
||||||
import { FormProvider, useForm } from "react-hook-form"
|
import { useFormContext } from "react-hook-form"
|
||||||
import { useIntl } from "react-intl"
|
import { useIntl } from "react-intl"
|
||||||
|
|
||||||
import { dt } from "@/lib/dt"
|
|
||||||
|
|
||||||
import Button from "@/components/TempDesignSystem/Button"
|
import Button from "@/components/TempDesignSystem/Button"
|
||||||
import Caption from "@/components/TempDesignSystem/Text/Caption"
|
import Caption from "@/components/TempDesignSystem/Text/Caption"
|
||||||
|
|
||||||
import FormContent from "./FormContent"
|
import FormContent from "./FormContent"
|
||||||
import { bookingWidgetSchema } from "./schema"
|
|
||||||
|
|
||||||
import styles from "./form.module.css"
|
import styles from "./form.module.css"
|
||||||
|
|
||||||
import type { BookingWidgetSchema } from "@/types/components/bookingWidget"
|
import type { BookingWidgetSchema } from "@/types/components/bookingWidget"
|
||||||
import type { BookingWidgetFormProps } from "@/types/components/form/bookingwidget"
|
import type { BookingWidgetFormProps } from "@/types/components/form/bookingwidget"
|
||||||
import type { Location } from "@/types/trpc/routers/hotel/locations"
|
|
||||||
|
|
||||||
const formId = "booking-widget"
|
const formId = "booking-widget"
|
||||||
|
|
||||||
@@ -24,40 +19,8 @@ export default function Form({ locations }: BookingWidgetFormProps) {
|
|||||||
const intl = useIntl()
|
const intl = useIntl()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
const sessionStorageSearchData =
|
const { formState, handleSubmit, register } =
|
||||||
typeof window !== "undefined"
|
useFormContext<BookingWidgetSchema>()
|
||||||
? sessionStorage.getItem("searchData")
|
|
||||||
: undefined
|
|
||||||
const initialSelectedLocation: Location | undefined = sessionStorageSearchData
|
|
||||||
? JSON.parse(sessionStorageSearchData)
|
|
||||||
: undefined
|
|
||||||
const methods = useForm<BookingWidgetSchema>({
|
|
||||||
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",
|
|
||||||
})
|
|
||||||
|
|
||||||
function onSubmit(data: BookingWidgetSchema) {
|
function onSubmit(data: BookingWidgetSchema) {
|
||||||
data.location = JSON.parse(decodeURIComponent(data.location))
|
data.location = JSON.parse(decodeURIComponent(data.location))
|
||||||
@@ -70,25 +33,24 @@ export default function Form({ locations }: BookingWidgetFormProps) {
|
|||||||
return (
|
return (
|
||||||
<section className={styles.section}>
|
<section className={styles.section}>
|
||||||
<form
|
<form
|
||||||
onSubmit={methods.handleSubmit(onSubmit)}
|
onSubmit={handleSubmit(onSubmit)}
|
||||||
className={styles.form}
|
className={styles.form}
|
||||||
id={formId}
|
id={formId}
|
||||||
>
|
>
|
||||||
<FormProvider {...methods}>
|
<input {...register("location")} type="hidden" />
|
||||||
<input {...methods.register("location")} type="hidden" />
|
<FormContent locations={locations} />
|
||||||
<FormContent locations={locations} />
|
|
||||||
</FormProvider>
|
|
||||||
</form>
|
</form>
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
className={styles.button}
|
||||||
|
disabled={!formState.isValid}
|
||||||
form={formId}
|
form={formId}
|
||||||
|
intent="primary"
|
||||||
size="small"
|
size="small"
|
||||||
theme="base"
|
theme="base"
|
||||||
intent="primary"
|
type="submit"
|
||||||
className={styles.button}
|
|
||||||
>
|
>
|
||||||
<Caption color="white" textTransform="bold">
|
<Caption color="white" textTransform="bold">
|
||||||
{intl.formatMessage({ id: "Find hotels" })}
|
{intl.formatMessage({ id: "Search" })}
|
||||||
</Caption>
|
</Caption>
|
||||||
</Button>
|
</Button>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ export const bookingWidgetSchema = z.object({
|
|||||||
// This will be updated when working in guests component
|
// This will be updated when working in guests component
|
||||||
z.object({
|
z.object({
|
||||||
adults: z.number().default(1),
|
adults: z.number().default(1),
|
||||||
childs: z.array(
|
children: z.array(
|
||||||
z.object({
|
z.object({
|
||||||
age: z.number(),
|
age: z.number(),
|
||||||
bed: z.number(),
|
bed: z.number(),
|
||||||
|
|||||||
36
components/Icons/Edit.tsx
Normal file
36
components/Icons/Edit.tsx
Normal file
@@ -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 (
|
||||||
|
<svg
|
||||||
|
className={classNames}
|
||||||
|
fill="none"
|
||||||
|
height="20"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
width="20"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<mask
|
||||||
|
height="20"
|
||||||
|
id="mask0_162_2666"
|
||||||
|
maskUnits="userSpaceOnUse"
|
||||||
|
style={{ maskType: "alpha" }}
|
||||||
|
width="20"
|
||||||
|
x="0"
|
||||||
|
y="0"
|
||||||
|
>
|
||||||
|
<rect width="20" height="20" fill="#D9D9D9" />
|
||||||
|
</mask>
|
||||||
|
<g mask="url(#mask0_162_2666)">
|
||||||
|
<path
|
||||||
|
d="M4.58333 16.8125C4.19949 16.8125 3.87088 16.6758 3.59752 16.4025C3.32417 16.1291 3.1875 15.8005 3.1875 15.4167V4.58333C3.1875 4.19375 3.32313 3.86024 3.5944 3.58281C3.86566 3.30538 4.19531 3.17361 4.58333 3.1875H11.7292L10.3333 4.58333H4.58333V15.4167H15.4167V9.65625L16.8125 8.26042V15.4167C16.8125 15.8005 16.6758 16.1291 16.4025 16.4025C16.1291 16.6758 15.8005 16.8125 15.4167 16.8125H4.58333ZM8.05208 11.9479V8.83333L15.4583 1.42708C15.6042 1.28125 15.7569 1.17882 15.9167 1.11979C16.0764 1.06076 16.245 1.03125 16.4224 1.03125C16.6116 1.03125 16.7899 1.06076 16.9573 1.11979C17.1247 1.17882 17.2808 1.27974 17.4259 1.42256L18.5625 2.5625C18.7083 2.70833 18.8125 2.86585 18.875 3.03504C18.9375 3.20424 18.9688 3.38006 18.9688 3.5625C18.9688 3.74529 18.9373 3.91971 18.8745 4.08575C18.8118 4.25179 18.7077 4.40724 18.5625 4.55208L11.1667 11.9479H8.05208ZM9.44792 10.5521H10.5938L15.4583 5.67708L14.8958 5.09375L14.3229 4.54167L9.44792 9.40625V10.5521Z"
|
||||||
|
fill="#26201E"
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -23,6 +23,7 @@ export { default as CrossCircle } from "./CrossCircle"
|
|||||||
export { default as CulturalIcon } from "./Cultural"
|
export { default as CulturalIcon } from "./Cultural"
|
||||||
export { default as DeleteIcon } from "./Delete"
|
export { default as DeleteIcon } from "./Delete"
|
||||||
export { default as DoorOpenIcon } from "./DoorOpen"
|
export { default as DoorOpenIcon } from "./DoorOpen"
|
||||||
|
export { default as EditIcon } from "./Edit"
|
||||||
export { default as ElectricBikeIcon } from "./ElectricBike"
|
export { default as ElectricBikeIcon } from "./ElectricBike"
|
||||||
export { default as EmailIcon } from "./Email"
|
export { default as EmailIcon } from "./Email"
|
||||||
export { default as ErrorCircleIcon } from "./ErrorCircle"
|
export { default as ErrorCircleIcon } from "./ErrorCircle"
|
||||||
|
|||||||
@@ -1,40 +1,47 @@
|
|||||||
.divider {
|
.divider {
|
||||||
border-bottom-style: solid;
|
pointer-events: none;
|
||||||
border-bottom-width: 1px;
|
}
|
||||||
|
|
||||||
|
.horizontal {
|
||||||
height: 1px;
|
height: 1px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dotted {
|
.vertical {
|
||||||
border-bottom-style: dotted;
|
height: 100%;
|
||||||
|
width: 1px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.burgundy {
|
.burgundy {
|
||||||
border-bottom-color: var(--Scandic-Brand-Burgundy);
|
background-color: var(--Scandic-Brand-Burgundy);
|
||||||
}
|
}
|
||||||
|
|
||||||
.pale {
|
.pale {
|
||||||
border-bottom-color: var(--Primary-Dark-On-Surface-Text);
|
background-color: var(--Primary-Dark-On-Surface-Text);
|
||||||
}
|
}
|
||||||
|
|
||||||
.peach {
|
.peach {
|
||||||
border-bottom-color: var(--Primary-Light-On-Surface-Divider);
|
background-color: var(--Primary-Light-On-Surface-Divider);
|
||||||
}
|
}
|
||||||
|
|
||||||
.beige {
|
.beige {
|
||||||
border-bottom-color: var(--Scandic-Beige-20);
|
background-color: var(--Scandic-Beige-20);
|
||||||
}
|
}
|
||||||
|
|
||||||
.white {
|
.white {
|
||||||
border-bottom-color: var(--UI-Opacity-White-100);
|
background-color: var(--UI-Opacity-White-100);
|
||||||
}
|
}
|
||||||
|
|
||||||
.subtle {
|
.subtle {
|
||||||
border-bottom-color: var(--Base-Border-Subtle);
|
background-color: var(--Base-Border-Subtle);
|
||||||
}
|
}
|
||||||
|
|
||||||
.primaryLightSubtle {
|
.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 {
|
.opacity100 {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import styles from "./divider.module.css"
|
|||||||
export const dividerVariants = cva(styles.divider, {
|
export const dividerVariants = cva(styles.divider, {
|
||||||
variants: {
|
variants: {
|
||||||
color: {
|
color: {
|
||||||
|
baseSurfaceSubtleNormal: styles.baseSurfaceSubtleNormal,
|
||||||
beige: styles.beige,
|
beige: styles.beige,
|
||||||
burgundy: styles.burgundy,
|
burgundy: styles.burgundy,
|
||||||
pale: styles.pale,
|
pale: styles.pale,
|
||||||
@@ -18,13 +19,13 @@ export const dividerVariants = cva(styles.divider, {
|
|||||||
8: styles.opacity8,
|
8: styles.opacity8,
|
||||||
},
|
},
|
||||||
variant: {
|
variant: {
|
||||||
default: styles.default,
|
horizontal: styles.horizontal,
|
||||||
dotted: styles.dotted,
|
vertical: styles.vertical,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
defaultVariants: {
|
defaultVariants: {
|
||||||
color: "burgundy",
|
color: "burgundy",
|
||||||
opacity: 100,
|
opacity: 100,
|
||||||
variant: "default",
|
variant: "horizontal",
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -63,6 +63,10 @@ p.caption {
|
|||||||
color: var(--UI-Text-Active);
|
color: var(--UI-Text-Active);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.uiTextMediumContrast {
|
||||||
|
color: var(--UI-Text-Medium-contrast);
|
||||||
|
}
|
||||||
|
|
||||||
.center {
|
.center {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ const config = {
|
|||||||
red: styles.red,
|
red: styles.red,
|
||||||
white: styles.white,
|
white: styles.white,
|
||||||
uiTextActive: styles.uiTextActive,
|
uiTextActive: styles.uiTextActive,
|
||||||
|
uiTextMediumContrast: styles.uiTextMediumContrast,
|
||||||
},
|
},
|
||||||
textTransform: {
|
textTransform: {
|
||||||
bold: styles.bold,
|
bold: styles.bold,
|
||||||
|
|||||||
@@ -23,7 +23,7 @@
|
|||||||
"Bed type": "Seng type",
|
"Bed type": "Seng type",
|
||||||
"Book": "Book",
|
"Book": "Book",
|
||||||
"Book reward night": "Book bonusnat",
|
"Book reward night": "Book bonusnat",
|
||||||
"Booking codes and vouchers": "Bookingkoder og vouchers",
|
"Code / Voucher": "Bookingkoder / voucher",
|
||||||
"Booking number": "Bookingnummer",
|
"Booking number": "Bookingnummer",
|
||||||
"Breakfast": "Morgenmad",
|
"Breakfast": "Morgenmad",
|
||||||
"Breakfast excluded": "Morgenmad ikke inkluderet",
|
"Breakfast excluded": "Morgenmad ikke inkluderet",
|
||||||
@@ -183,10 +183,11 @@
|
|||||||
"Room & Terms": "Værelse & Vilkår",
|
"Room & Terms": "Værelse & Vilkår",
|
||||||
"Room facilities": "Værelsesfaciliteter",
|
"Room facilities": "Værelsesfaciliteter",
|
||||||
"Rooms": "Værelser",
|
"Rooms": "Værelser",
|
||||||
"Rooms & Guests": "Værelser & gæster",
|
"Guests & Rooms": "Gæster & værelser",
|
||||||
"Save": "Gemme",
|
"Save": "Gemme",
|
||||||
"Scandic Friends Mastercard": "Scandic Friends Mastercard",
|
"Scandic Friends Mastercard": "Scandic Friends Mastercard",
|
||||||
"Scandic Friends Point Shop": "Scandic Friends Point Shop",
|
"Scandic Friends Point Shop": "Scandic Friends Point Shop",
|
||||||
|
"Search": "Søge",
|
||||||
"See all photos": "Se alle billeder",
|
"See all photos": "Se alle billeder",
|
||||||
"See hotel details": "Se hoteloplysninger",
|
"See hotel details": "Se hoteloplysninger",
|
||||||
"See room details": "Se værelsesdetaljer",
|
"See room details": "Se værelsesdetaljer",
|
||||||
|
|||||||
@@ -23,7 +23,7 @@
|
|||||||
"Bed type": "Bettentyp",
|
"Bed type": "Bettentyp",
|
||||||
"Book": "Buchen",
|
"Book": "Buchen",
|
||||||
"Book reward night": "Bonusnacht buchen",
|
"Book reward night": "Bonusnacht buchen",
|
||||||
"Booking codes and vouchers": "Buchungscodes und Gutscheine",
|
"Code / Voucher": "Buchungscodes / Gutscheine",
|
||||||
"Booking number": "Buchungsnummer",
|
"Booking number": "Buchungsnummer",
|
||||||
"Breakfast": "Frühstück",
|
"Breakfast": "Frühstück",
|
||||||
"Breakfast excluded": "Frühstück nicht inbegriffen",
|
"Breakfast excluded": "Frühstück nicht inbegriffen",
|
||||||
@@ -182,10 +182,11 @@
|
|||||||
"Room & Terms": "Zimmer & Bedingungen",
|
"Room & Terms": "Zimmer & Bedingungen",
|
||||||
"Room facilities": "Zimmerausstattung",
|
"Room facilities": "Zimmerausstattung",
|
||||||
"Rooms": "Räume",
|
"Rooms": "Räume",
|
||||||
"Rooms & Guests": "Zimmer & Gäste",
|
"Guests & Rooms": "Gäste & Zimmer",
|
||||||
"Save": "Speichern",
|
"Save": "Speichern",
|
||||||
"Scandic Friends Mastercard": "Scandic Friends Mastercard",
|
"Scandic Friends Mastercard": "Scandic Friends Mastercard",
|
||||||
"Scandic Friends Point Shop": "Scandic Friends Point Shop",
|
"Scandic Friends Point Shop": "Scandic Friends Point Shop",
|
||||||
|
"Search": "Suchen",
|
||||||
"See all photos": "Alle Fotos ansehen",
|
"See all photos": "Alle Fotos ansehen",
|
||||||
"See hotel details": "Hotelinformationen ansehen",
|
"See hotel details": "Hotelinformationen ansehen",
|
||||||
"See room details": "Zimmerdetails ansehen",
|
"See room details": "Zimmerdetails ansehen",
|
||||||
|
|||||||
@@ -23,7 +23,7 @@
|
|||||||
"Bed type": "Bed type",
|
"Bed type": "Bed type",
|
||||||
"Book": "Book",
|
"Book": "Book",
|
||||||
"Book reward night": "Book reward night",
|
"Book reward night": "Book reward night",
|
||||||
"Booking codes and vouchers": "Booking codes and vouchers",
|
"Code / Voucher": "Code / Voucher",
|
||||||
"Booking number": "Booking number",
|
"Booking number": "Booking number",
|
||||||
"Breakfast": "Breakfast",
|
"Breakfast": "Breakfast",
|
||||||
"Breakfast excluded": "Breakfast excluded",
|
"Breakfast excluded": "Breakfast excluded",
|
||||||
@@ -60,6 +60,7 @@
|
|||||||
"Day": "Day",
|
"Day": "Day",
|
||||||
"Description": "Description",
|
"Description": "Description",
|
||||||
"Destinations & hotels": "Destinations & hotels",
|
"Destinations & hotels": "Destinations & hotels",
|
||||||
|
"Destination": "Destination",
|
||||||
"Discard changes": "Discard changes",
|
"Discard changes": "Discard changes",
|
||||||
"Discard unsaved changes?": "Discard unsaved changes?",
|
"Discard unsaved changes?": "Discard unsaved changes?",
|
||||||
"Distance to city centre": "{number}km to city centre",
|
"Distance to city centre": "{number}km to city centre",
|
||||||
@@ -183,11 +184,12 @@
|
|||||||
"Room & Terms": "Room & Terms",
|
"Room & Terms": "Room & Terms",
|
||||||
"Room facilities": "Room facilities",
|
"Room facilities": "Room facilities",
|
||||||
"Rooms": "Rooms",
|
"Rooms": "Rooms",
|
||||||
"Rooms & Guests": "Rooms & Guests",
|
"Guests & Rooms": "Guests & Rooms",
|
||||||
"Save": "Save",
|
"Save": "Save",
|
||||||
"Scandic Friends Mastercard": "Scandic Friends Mastercard",
|
"Scandic Friends Mastercard": "Scandic Friends Mastercard",
|
||||||
"Scandic Friends Point Shop": "Scandic Friends Point Shop",
|
"Scandic Friends Point Shop": "Scandic Friends Point Shop",
|
||||||
"See all photos": "See all photos",
|
"See all photos": "See all photos",
|
||||||
|
"Search": "Search",
|
||||||
"See hotel details": "See hotel details",
|
"See hotel details": "See hotel details",
|
||||||
"See room details": "See room details",
|
"See room details": "See room details",
|
||||||
"See rooms": "See rooms",
|
"See rooms": "See rooms",
|
||||||
@@ -264,7 +266,9 @@
|
|||||||
"Zoom in": "Zoom in",
|
"Zoom in": "Zoom in",
|
||||||
"Zoom out": "Zoom out",
|
"Zoom out": "Zoom out",
|
||||||
"as of today": "as of today",
|
"as of today": "as of today",
|
||||||
|
"booking.adults": "{totalAdults, plural, one {# adult} other {# adults}}",
|
||||||
"booking.nights": "{totalNights, plural, one {# night} other {# nights}}",
|
"booking.nights": "{totalNights, plural, one {# night} other {# nights}}",
|
||||||
|
"booking.rooms": "{totalRooms, plural, one {# room} other {# rooms}}",
|
||||||
"by": "by",
|
"by": "by",
|
||||||
"characters": "characters",
|
"characters": "characters",
|
||||||
"hotelPages.rooms.roomCard.person": "person",
|
"hotelPages.rooms.roomCard.person": "person",
|
||||||
|
|||||||
@@ -23,7 +23,6 @@
|
|||||||
"Bed type": "Vuodetyyppi",
|
"Bed type": "Vuodetyyppi",
|
||||||
"Book": "Varaa",
|
"Book": "Varaa",
|
||||||
"Book reward night": "Kirjapalkinto-ilta",
|
"Book reward night": "Kirjapalkinto-ilta",
|
||||||
"Booking codes and vouchers": "Varauskoodit ja kupongit",
|
|
||||||
"Booking number": "Varausnumero",
|
"Booking number": "Varausnumero",
|
||||||
"Breakfast": "Aamiainen",
|
"Breakfast": "Aamiainen",
|
||||||
"Breakfast excluded": "Aamiainen ei sisälly",
|
"Breakfast excluded": "Aamiainen ei sisälly",
|
||||||
@@ -45,6 +44,7 @@
|
|||||||
"Close menu": "Sulje valikko",
|
"Close menu": "Sulje valikko",
|
||||||
"Close my pages menu": "Sulje omat sivut -valikko",
|
"Close my pages menu": "Sulje omat sivut -valikko",
|
||||||
"Close the map": "Sulje kartta",
|
"Close the map": "Sulje kartta",
|
||||||
|
"Code / Voucher": "Varauskoodit / kupongit",
|
||||||
"Coming up": "Tulossa",
|
"Coming up": "Tulossa",
|
||||||
"Compare all levels": "Vertaa kaikkia tasoja",
|
"Compare all levels": "Vertaa kaikkia tasoja",
|
||||||
"Contact us": "Ota meihin yhteyttä",
|
"Contact us": "Ota meihin yhteyttä",
|
||||||
@@ -85,6 +85,7 @@
|
|||||||
"Get inspired": "Inspiroidu",
|
"Get inspired": "Inspiroidu",
|
||||||
"Go back to edit": "Palaa muokkaamaan",
|
"Go back to edit": "Palaa muokkaamaan",
|
||||||
"Go back to overview": "Palaa yleiskatsaukseen",
|
"Go back to overview": "Palaa yleiskatsaukseen",
|
||||||
|
"Guests & Rooms": "Vieraat & Huoneet",
|
||||||
"Hi": "Hi",
|
"Hi": "Hi",
|
||||||
"Highest level": "Korkein taso",
|
"Highest level": "Korkein taso",
|
||||||
"Hospital": "Sairaala",
|
"Hospital": "Sairaala",
|
||||||
@@ -183,11 +184,10 @@
|
|||||||
"Room & Terms": "Huone & Ehdot",
|
"Room & Terms": "Huone & Ehdot",
|
||||||
"Room facilities": "Huoneen varustelu",
|
"Room facilities": "Huoneen varustelu",
|
||||||
"Rooms": "Huoneet",
|
"Rooms": "Huoneet",
|
||||||
"Rooms & Guests": "Huoneet & Vieraat",
|
|
||||||
"Rooms & Guestss": "Huoneet & Vieraat",
|
|
||||||
"Save": "Tallenna",
|
"Save": "Tallenna",
|
||||||
"Scandic Friends Mastercard": "Scandic Friends Mastercard",
|
"Scandic Friends Mastercard": "Scandic Friends Mastercard",
|
||||||
"Scandic Friends Point Shop": "Scandic Friends Point Shop",
|
"Scandic Friends Point Shop": "Scandic Friends Point Shop",
|
||||||
|
"Search": "Haku",
|
||||||
"See all photos": "Katso kaikki kuvat",
|
"See all photos": "Katso kaikki kuvat",
|
||||||
"See hotel details": "Katso hotellin tiedot",
|
"See hotel details": "Katso hotellin tiedot",
|
||||||
"See room details": "Katso huoneen tiedot",
|
"See room details": "Katso huoneen tiedot",
|
||||||
@@ -281,4 +281,4 @@
|
|||||||
"spendable points expiring by": "{points} pistettä vanhenee {date} mennessä",
|
"spendable points expiring by": "{points} pistettä vanhenee {date} mennessä",
|
||||||
"to": "to",
|
"to": "to",
|
||||||
"uppercase letter": "iso kirjain"
|
"uppercase letter": "iso kirjain"
|
||||||
}
|
}
|
||||||
@@ -23,7 +23,7 @@
|
|||||||
"Bed type": "Seng type",
|
"Bed type": "Seng type",
|
||||||
"Book": "Bestill",
|
"Book": "Bestill",
|
||||||
"Book reward night": "Bestill belønningskveld",
|
"Book reward night": "Bestill belønningskveld",
|
||||||
"Booking codes and vouchers": "Bestillingskoder og kuponger",
|
"Code / Voucher": "Bestillingskoder / kuponger",
|
||||||
"Booking number": "Bestillingsnummer",
|
"Booking number": "Bestillingsnummer",
|
||||||
"Breakfast": "Frokost",
|
"Breakfast": "Frokost",
|
||||||
"Breakfast excluded": "Frokost ekskludert",
|
"Breakfast excluded": "Frokost ekskludert",
|
||||||
@@ -183,10 +183,11 @@
|
|||||||
"Room & Terms": "Rom & Vilkår",
|
"Room & Terms": "Rom & Vilkår",
|
||||||
"Room facilities": "Romfasiliteter",
|
"Room facilities": "Romfasiliteter",
|
||||||
"Rooms": "Rom",
|
"Rooms": "Rom",
|
||||||
"Rooms & Guests": "Rom og gjester",
|
"Guests & Rooms": "Gjester & rom",
|
||||||
"Save": "Lagre",
|
"Save": "Lagre",
|
||||||
"Scandic Friends Mastercard": "Scandic Friends Mastercard",
|
"Scandic Friends Mastercard": "Scandic Friends Mastercard",
|
||||||
"Scandic Friends Point Shop": "Scandic Friends Point Shop",
|
"Scandic Friends Point Shop": "Scandic Friends Point Shop",
|
||||||
|
"Search": "Søk",
|
||||||
"See all photos": "Se alle bilder",
|
"See all photos": "Se alle bilder",
|
||||||
"See hotel details": "Se hotellinformasjon",
|
"See hotel details": "Se hotellinformasjon",
|
||||||
"See room details": "Se detaljer om rommet",
|
"See room details": "Se detaljer om rommet",
|
||||||
|
|||||||
@@ -23,7 +23,7 @@
|
|||||||
"Bed type": "Sängtyp",
|
"Bed type": "Sängtyp",
|
||||||
"Book": "Boka",
|
"Book": "Boka",
|
||||||
"Book reward night": "Boka frinatt",
|
"Book reward night": "Boka frinatt",
|
||||||
"Booking codes and vouchers": "Bokningskoder och kuponger",
|
"Code / Voucher": "Bokningskoder / kuponger",
|
||||||
"Booking number": "Bokningsnummer",
|
"Booking number": "Bokningsnummer",
|
||||||
"Breakfast": "Frukost",
|
"Breakfast": "Frukost",
|
||||||
"Breakfast excluded": "Frukost ingår ej",
|
"Breakfast excluded": "Frukost ingår ej",
|
||||||
@@ -183,10 +183,11 @@
|
|||||||
"Room & Terms": "Rum & Villkor",
|
"Room & Terms": "Rum & Villkor",
|
||||||
"Room facilities": "Rumfaciliteter",
|
"Room facilities": "Rumfaciliteter",
|
||||||
"Rooms": "Rum",
|
"Rooms": "Rum",
|
||||||
"Rooms & Guests": "Rum och gäster",
|
"Guests & Rooms": "Gäster & rum",
|
||||||
"Save": "Spara",
|
"Save": "Spara",
|
||||||
"Scandic Friends Mastercard": "Scandic Friends Mastercard",
|
"Scandic Friends Mastercard": "Scandic Friends Mastercard",
|
||||||
"Scandic Friends Point Shop": "Scandic Friends Point Shop",
|
"Scandic Friends Point Shop": "Scandic Friends Point Shop",
|
||||||
|
"Search": "Sök",
|
||||||
"See all photos": "Se alla foton",
|
"See all photos": "Se alla foton",
|
||||||
"See hotel details": "Se hotellinformation",
|
"See hotel details": "Se hotellinformation",
|
||||||
"See room details": "Se rumsdetaljer",
|
"See room details": "Se rumsdetaljer",
|
||||||
|
|||||||
@@ -2,4 +2,14 @@ import { z } from "zod"
|
|||||||
|
|
||||||
import { bookingWidgetSchema } from "@/components/Forms/BookingWidget/schema"
|
import { bookingWidgetSchema } from "@/components/Forms/BookingWidget/schema"
|
||||||
|
|
||||||
|
import type { Locations } from "@/types/trpc/routers/hotel/locations"
|
||||||
|
|
||||||
export type BookingWidgetSchema = z.output<typeof bookingWidgetSchema>
|
export type BookingWidgetSchema = z.output<typeof bookingWidgetSchema>
|
||||||
|
|
||||||
|
export interface BookingWidgetClientProps {
|
||||||
|
locations: Locations
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BookingWidgetToggleButtonProps {
|
||||||
|
openMobileSearch: () => void
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,11 +1,17 @@
|
|||||||
|
import { Lang } from "@/constants/languages"
|
||||||
|
|
||||||
|
import type { Locale } from "date-fns"
|
||||||
import type { DateRange } from "react-day-picker"
|
import type { DateRange } from "react-day-picker"
|
||||||
|
|
||||||
export interface DatePickerFormProps {
|
export interface DatePickerFormProps {
|
||||||
name?: string
|
name?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type LangWithoutEn = Lang.da | Lang.de | Lang.fi | Lang.no | Lang.sv
|
||||||
|
|
||||||
export interface DatePickerProps {
|
export interface DatePickerProps {
|
||||||
close: () => void
|
close: () => void
|
||||||
handleOnSelect: (selected: DateRange) => void
|
handleOnSelect: (selected: DateRange) => void
|
||||||
initialSelected?: DateRange
|
locales: Record<LangWithoutEn, Locale>
|
||||||
|
selectedDate: DateRange
|
||||||
}
|
}
|
||||||
|
|||||||
10
utils/debounce.ts
Normal file
10
utils/debounce.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
export function debounce(func: Function, delay = 300) {
|
||||||
|
let debounceTimer: ReturnType<typeof setTimeout>
|
||||||
|
return function () {
|
||||||
|
// @ts-expect-error this in TypeScript
|
||||||
|
const context = this
|
||||||
|
const args = arguments
|
||||||
|
clearTimeout(debounceTimer)
|
||||||
|
debounceTimer = setTimeout(() => func.apply(context, args), delay)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user