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,
|
||||
|
||||
Reference in New Issue
Block a user