feat: add mobile ui to calendar
This commit is contained in:
@@ -11,7 +11,7 @@ export default function Stats({ user }: UserProps) {
|
||||
return (
|
||||
<section className={styles.stats}>
|
||||
<Points user={user} />
|
||||
<Divider variant="default" color="pale" />
|
||||
<Divider color="pale" />
|
||||
<ExpiringPoints user={user} />
|
||||
</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 {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 (
|
||||
<section className={styles.container}>
|
||||
<Form locations={locations.data} />
|
||||
</section>
|
||||
)
|
||||
return <BookingWidgetClient locations={locations.data} />
|
||||
}
|
||||
|
||||
@@ -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<DateRange>(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}
|
||||
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 {
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
<input {...register("date.from")} type="hidden" />
|
||||
<input {...register("date.to")} type="hidden" />
|
||||
<div aria-modal className={styles.hideWrapper} role="dialog">
|
||||
<DatePicker
|
||||
<DatePickerDesktop
|
||||
close={close}
|
||||
handleOnSelect={handleSelectDate}
|
||||
initialSelected={selectedDate}
|
||||
locales={locales}
|
||||
selectedDate={selectedDate}
|
||||
/>
|
||||
<DatePickerMobile
|
||||
close={close}
|
||||
handleOnSelect={handleSelectDate}
|
||||
locales={locales}
|
||||
selectedDate={selectedDate}
|
||||
/>
|
||||
</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 {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
max-width: 158px;
|
||||
}
|
||||
|
||||
.option {
|
||||
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 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({
|
||||
<input type="text" placeholder={rooms} />
|
||||
</div>
|
||||
<div className={styles.vouchers}>
|
||||
<Caption color="textMediumContrast" textTransform="bold">
|
||||
<Caption color="uiTextMediumContrast" textTransform="bold">
|
||||
{vouchers}
|
||||
</Caption>
|
||||
<input type="text" placeholder={vouchers} />
|
||||
</div>
|
||||
<div className={styles.options}>
|
||||
<div className={styles.option}>
|
||||
<label className={styles.option}>
|
||||
<input type="checkbox" />
|
||||
<Caption color="textMediumContrast">{bonus}</Caption>
|
||||
</div>
|
||||
<div className={styles.option}>
|
||||
</label>
|
||||
<label className={styles.option}>
|
||||
<input type="checkbox" />
|
||||
<Caption color="textMediumContrast">{reward}</Caption>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<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",
|
||||
})
|
||||
const { formState, handleSubmit, register } =
|
||||
useFormContext<BookingWidgetSchema>()
|
||||
|
||||
function onSubmit(data: BookingWidgetSchema) {
|
||||
data.location = JSON.parse(decodeURIComponent(data.location))
|
||||
@@ -70,25 +33,24 @@ export default function Form({ locations }: BookingWidgetFormProps) {
|
||||
return (
|
||||
<section className={styles.section}>
|
||||
<form
|
||||
onSubmit={methods.handleSubmit(onSubmit)}
|
||||
onSubmit={handleSubmit(onSubmit)}
|
||||
className={styles.form}
|
||||
id={formId}
|
||||
>
|
||||
<FormProvider {...methods}>
|
||||
<input {...methods.register("location")} type="hidden" />
|
||||
<FormContent locations={locations} />
|
||||
</FormProvider>
|
||||
<input {...register("location")} type="hidden" />
|
||||
<FormContent locations={locations} />
|
||||
</form>
|
||||
<Button
|
||||
type="submit"
|
||||
className={styles.button}
|
||||
disabled={!formState.isValid}
|
||||
form={formId}
|
||||
intent="primary"
|
||||
size="small"
|
||||
theme="base"
|
||||
intent="primary"
|
||||
className={styles.button}
|
||||
type="submit"
|
||||
>
|
||||
<Caption color="white" textTransform="bold">
|
||||
{intl.formatMessage({ id: "Find hotels" })}
|
||||
{intl.formatMessage({ id: "Search" })}
|
||||
</Caption>
|
||||
</Button>
|
||||
</section>
|
||||
|
||||
@@ -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(),
|
||||
|
||||
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 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"
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
})
|
||||
|
||||
@@ -63,6 +63,10 @@ p.caption {
|
||||
color: var(--UI-Text-Active);
|
||||
}
|
||||
|
||||
.uiTextMediumContrast {
|
||||
color: var(--UI-Text-Medium-contrast);
|
||||
}
|
||||
|
||||
.center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ const config = {
|
||||
red: styles.red,
|
||||
white: styles.white,
|
||||
uiTextActive: styles.uiTextActive,
|
||||
uiTextMediumContrast: styles.uiTextMediumContrast,
|
||||
},
|
||||
textTransform: {
|
||||
bold: styles.bold,
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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<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"
|
||||
|
||||
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<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