Merge branch 'develop' into feat/sw-703-image-loader
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -42,5 +42,8 @@ certificates
|
|||||||
#vscode
|
#vscode
|
||||||
.vscode/
|
.vscode/
|
||||||
|
|
||||||
|
#cursor
|
||||||
|
.cursorrules
|
||||||
|
|
||||||
# localfile with all the CSS variables exported from design system
|
# localfile with all the CSS variables exported from design system
|
||||||
variables.css
|
variables.css
|
||||||
@@ -148,7 +148,7 @@ export const editProfile = protectedServerActionProcedure
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const apiResponse = await api.patch(api.endpoints.v1.profile, {
|
const apiResponse = await api.patch(api.endpoints.v1.Profile.profile, {
|
||||||
body,
|
body,
|
||||||
cache: "no-store",
|
cache: "no-store",
|
||||||
headers: {
|
headers: {
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ export const registerUser = serviceServerActionProcedure
|
|||||||
|
|
||||||
let apiResponse
|
let apiResponse
|
||||||
try {
|
try {
|
||||||
apiResponse = await api.post(api.endpoints.v1.profile, {
|
apiResponse = await api.post(api.endpoints.v1.Profile.profile, {
|
||||||
body: parsedPayload.data,
|
body: parsedPayload.data,
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${ctx.serviceToken}`,
|
Authorization: `Bearer ${ctx.serviceToken}`,
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ export const registerUserBookingFlow = serviceServerActionProcedure
|
|||||||
// TODO: Consume the API to register the user as soon as passwordless signup is enabled.
|
// TODO: Consume the API to register the user as soon as passwordless signup is enabled.
|
||||||
// let apiResponse
|
// let apiResponse
|
||||||
// try {
|
// try {
|
||||||
// apiResponse = await api.post(api.endpoints.v1.profile, {
|
// apiResponse = await api.post(api.endpoints.v1.Profile.profile, {
|
||||||
// body: payload,
|
// body: payload,
|
||||||
// headers: {
|
// headers: {
|
||||||
// Authorization: `Bearer ${ctx.serviceToken}`,
|
// Authorization: `Bearer ${ctx.serviceToken}`,
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
import { env } from "@/env/server"
|
|
||||||
|
|
||||||
import Divider from "@/components/TempDesignSystem/Divider"
|
import Divider from "@/components/TempDesignSystem/Divider"
|
||||||
|
|
||||||
import type { ProfileLayoutProps } from "@/types/components/myPages/myProfile/layout"
|
import type { ProfileLayoutProps } from "@/types/components/myPages/myProfile/layout"
|
||||||
@@ -17,7 +15,7 @@ export default function ProfileLayout({
|
|||||||
{profile}
|
{profile}
|
||||||
<Divider color="burgundy" opacity={8} />
|
<Divider color="burgundy" opacity={8} />
|
||||||
{creditCards}
|
{creditCards}
|
||||||
{env.HIDE_FOR_NEXT_RELEASE ? null : communication}
|
{communication}
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -45,8 +45,8 @@ export default async function BookingConfirmationPage({
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
const fromDate = dt(booking.temp.fromDate).locale(params.lang)
|
const fromDate = dt(booking.checkInDate).locale(params.lang)
|
||||||
const toDate = dt(booking.temp.toDate).locale(params.lang)
|
const toDate = dt(booking.checkOutDate).locale(params.lang)
|
||||||
const nights = intl.formatMessage(
|
const nights = intl.formatMessage(
|
||||||
{ id: "booking.nights" },
|
{ id: "booking.nights" },
|
||||||
{
|
{
|
||||||
@@ -77,7 +77,7 @@ export default async function BookingConfirmationPage({
|
|||||||
textTransform="regular"
|
textTransform="regular"
|
||||||
type="h1"
|
type="h1"
|
||||||
>
|
>
|
||||||
{booking.hotel.name}
|
{booking.hotel?.data.attributes.name}
|
||||||
</Title>
|
</Title>
|
||||||
</hgroup>
|
</hgroup>
|
||||||
<Body className={styles.body} textAlign="center">
|
<Body className={styles.body} textAlign="center">
|
||||||
@@ -91,7 +91,7 @@ export default async function BookingConfirmationPage({
|
|||||||
<Subtitle color="burgundy" type="two">
|
<Subtitle color="burgundy" type="two">
|
||||||
{intl.formatMessage(
|
{intl.formatMessage(
|
||||||
{ id: "Reference #{bookingNr}" },
|
{ id: "Reference #{bookingNr}" },
|
||||||
{ bookingNr: "A92320VV" }
|
{ bookingNr: booking.confirmationNumber }
|
||||||
)}
|
)}
|
||||||
</Subtitle>
|
</Subtitle>
|
||||||
</header>
|
</header>
|
||||||
@@ -183,11 +183,13 @@ export default async function BookingConfirmationPage({
|
|||||||
</Caption>
|
</Caption>
|
||||||
<div>
|
<div>
|
||||||
<Body color="burgundy" textTransform="bold">
|
<Body color="burgundy" textTransform="bold">
|
||||||
{booking.hotel.name}
|
{booking.hotel?.data.attributes.name}
|
||||||
</Body>
|
</Body>
|
||||||
<Body color="uiTextHighContrast">{booking.hotel.email}</Body>
|
|
||||||
<Body color="uiTextHighContrast">
|
<Body color="uiTextHighContrast">
|
||||||
{booking.hotel.phoneNumber}
|
{booking.hotel?.data.attributes.contactInformation.email}
|
||||||
|
</Body>
|
||||||
|
<Body color="uiTextHighContrast">
|
||||||
|
{booking.hotel?.data.attributes.contactInformation.phoneNumber}
|
||||||
</Body>
|
</Body>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -219,7 +221,16 @@ export default async function BookingConfirmationPage({
|
|||||||
<Body color="burgundy" textTransform="bold">
|
<Body color="burgundy" textTransform="bold">
|
||||||
{intl.formatMessage({ id: "Total cost" })}
|
{intl.formatMessage({ id: "Total cost" })}
|
||||||
</Body>
|
</Body>
|
||||||
<Body color="uiTextHighContrast">{booking.temp.total}</Body>
|
<Body color="uiTextHighContrast">
|
||||||
|
{" "}
|
||||||
|
{intl.formatMessage(
|
||||||
|
{ id: "{amount} {currency}" },
|
||||||
|
{
|
||||||
|
amount: intl.formatNumber(booking.totalPrice),
|
||||||
|
currency: booking.currencyCode,
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
</Body>
|
||||||
<Caption color="uiTextPlaceholder">
|
<Caption color="uiTextPlaceholder">
|
||||||
{`${intl.formatMessage({ id: "Approx." })} ${booking.temp.totalInEuro}`}
|
{`${intl.formatMessage({ id: "Approx." })} ${booking.temp.totalInEuro}`}
|
||||||
</Caption>
|
</Caption>
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
export { default } from "../page"
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import LoadingSpinner from "@/components/LoadingSpinner"
|
import LoadingSpinner from "@/components/LoadingSpinner"
|
||||||
|
|
||||||
export default function Loading() {
|
export default function LoadingHotelHeader() {
|
||||||
return <LoadingSpinner />
|
return <LoadingSpinner />
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import { redirect } from "next/navigation"
|
||||||
|
|
||||||
|
import { getHotelData } from "@/lib/trpc/memoizedRequests"
|
||||||
|
|
||||||
|
import HotelSelectionHeader from "@/components/HotelReservation/HotelSelectionHeader"
|
||||||
|
|
||||||
|
import type { LangParams, PageArgs } from "@/types/params"
|
||||||
|
|
||||||
|
export default async function HotelHeader({
|
||||||
|
params,
|
||||||
|
searchParams,
|
||||||
|
}: PageArgs<LangParams, { hotel: string }>) {
|
||||||
|
const home = `/${params.lang}`
|
||||||
|
if (!searchParams.hotel) {
|
||||||
|
redirect(home)
|
||||||
|
}
|
||||||
|
const hotel = await getHotelData({
|
||||||
|
hotelId: searchParams.hotel,
|
||||||
|
language: params.lang,
|
||||||
|
})
|
||||||
|
if (!hotel?.data) {
|
||||||
|
redirect(home)
|
||||||
|
}
|
||||||
|
return <HotelSelectionHeader hotel={hotel.data.attributes} />
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { default } from "../page"
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import LoadingSpinner from "@/components/LoadingSpinner"
|
||||||
|
|
||||||
|
export default function LoadingHotelSidePeek() {
|
||||||
|
return <LoadingSpinner />
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import { redirect } from "next/navigation"
|
||||||
|
|
||||||
|
import { getHotelData } from "@/lib/trpc/memoizedRequests"
|
||||||
|
|
||||||
|
import SidePeek from "@/components/HotelReservation/EnterDetails/SidePeek"
|
||||||
|
|
||||||
|
import type { LangParams, PageArgs } from "@/types/params"
|
||||||
|
|
||||||
|
export default async function HotelSidePeek({
|
||||||
|
params,
|
||||||
|
searchParams,
|
||||||
|
}: PageArgs<LangParams, { hotel: string }>) {
|
||||||
|
if (!searchParams.hotel) {
|
||||||
|
redirect(`/${params.lang}`)
|
||||||
|
}
|
||||||
|
const hotel = await getHotelData({
|
||||||
|
hotelId: searchParams.hotel,
|
||||||
|
language: params.lang,
|
||||||
|
})
|
||||||
|
if (!hotel?.data) {
|
||||||
|
redirect(`/${params.lang}`)
|
||||||
|
}
|
||||||
|
return <SidePeek hotel={hotel.data.attributes} />
|
||||||
|
}
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
import {
|
||||||
|
getProfileSafely,
|
||||||
|
getSelectedRoomAvailability,
|
||||||
|
} from "@/lib/trpc/memoizedRequests"
|
||||||
|
|
||||||
|
import Summary from "@/components/HotelReservation/EnterDetails/Summary"
|
||||||
|
import { getQueryParamsForEnterDetails } from "@/components/HotelReservation/SelectRate/RoomSelection/utils"
|
||||||
|
|
||||||
|
import { SelectRateSearchParams } from "@/types/components/hotelReservation/selectRate/selectRate"
|
||||||
|
import { LangParams, PageArgs, SearchParams } from "@/types/params"
|
||||||
|
|
||||||
|
export default async function SummaryPage({
|
||||||
|
searchParams,
|
||||||
|
}: PageArgs<LangParams, SearchParams<SelectRateSearchParams>>) {
|
||||||
|
const selectRoomParams = new URLSearchParams(searchParams)
|
||||||
|
const { hotel, adults, children, roomTypeCode, rateCode, fromDate, toDate } =
|
||||||
|
getQueryParamsForEnterDetails(selectRoomParams)
|
||||||
|
|
||||||
|
const availability = await getSelectedRoomAvailability({
|
||||||
|
hotelId: parseInt(hotel),
|
||||||
|
adults,
|
||||||
|
children,
|
||||||
|
roomStayStartDate: fromDate,
|
||||||
|
roomStayEndDate: toDate,
|
||||||
|
rateCode,
|
||||||
|
roomTypeCode,
|
||||||
|
})
|
||||||
|
const user = await getProfileSafely()
|
||||||
|
|
||||||
|
if (!availability) {
|
||||||
|
console.error("No hotel or availability data", availability)
|
||||||
|
// TODO: handle this case
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const prices = user
|
||||||
|
? {
|
||||||
|
local: {
|
||||||
|
price: availability.memberRate?.localPrice.pricePerStay,
|
||||||
|
currency: availability.memberRate?.localPrice.currency,
|
||||||
|
},
|
||||||
|
euro: {
|
||||||
|
price: availability.memberRate?.requestedPrice?.pricePerStay,
|
||||||
|
currency: availability.memberRate?.requestedPrice?.currency,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
local: {
|
||||||
|
price: availability.publicRate?.localPrice.pricePerStay,
|
||||||
|
currency: availability.publicRate?.localPrice.currency,
|
||||||
|
},
|
||||||
|
euro: {
|
||||||
|
price: availability.publicRate?.requestedPrice?.pricePerStay,
|
||||||
|
currency: availability.publicRate?.requestedPrice?.currency,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Summary
|
||||||
|
isMember={!!user}
|
||||||
|
room={{
|
||||||
|
roomType: availability.selectedRoom.roomType,
|
||||||
|
localPrice: prices.local,
|
||||||
|
euroPrice: prices.euro,
|
||||||
|
adults,
|
||||||
|
cancellationText: availability.cancellationText,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
.layout {
|
.layout {
|
||||||
min-height: 100dvh;
|
|
||||||
background-color: var(--Scandic-Brand-Warm-White);
|
background-color: var(--Scandic-Brand-Warm-White);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -9,7 +8,6 @@
|
|||||||
grid-template-columns: 1fr 340px;
|
grid-template-columns: 1fr 340px;
|
||||||
grid-template-rows: auto 1fr;
|
grid-template-rows: auto 1fr;
|
||||||
margin: var(--Spacing-x5) auto 0;
|
margin: var(--Spacing-x5) auto 0;
|
||||||
padding-top: var(--Spacing-x6);
|
|
||||||
/* simulates padding on viewport smaller than --max-width-navigation */
|
/* simulates padding on viewport smaller than --max-width-navigation */
|
||||||
width: min(
|
width: min(
|
||||||
calc(100dvw - (var(--Spacing-x2) * 2)),
|
calc(100dvw - (var(--Spacing-x2) * 2)),
|
||||||
@@ -17,8 +15,81 @@
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
.summary {
|
.summaryContainer {
|
||||||
align-self: flex-start;
|
|
||||||
grid-column: 2 / 3;
|
grid-column: 2 / 3;
|
||||||
grid-row: 1/-1;
|
grid-row: 1/-1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.summary {
|
||||||
|
background-color: var(--Main-Grey-White);
|
||||||
|
|
||||||
|
border-color: var(--Primary-Light-On-Surface-Divider-subtle);
|
||||||
|
border-style: solid;
|
||||||
|
border-width: 1px;
|
||||||
|
border-radius: var(--Corner-radius-Large);
|
||||||
|
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hider {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shadow {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (min-width: 950px) {
|
||||||
|
.summaryContainer {
|
||||||
|
display: grid;
|
||||||
|
grid-template-rows: auto auto 1fr;
|
||||||
|
margin-top: calc(0px - var(--Spacing-x9));
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary {
|
||||||
|
position: sticky;
|
||||||
|
top: calc(
|
||||||
|
var(--booking-widget-desktop-height) +
|
||||||
|
var(--booking-widget-desktop-height) + var(--Spacing-x-one-and-half)
|
||||||
|
);
|
||||||
|
margin-top: calc(0px - var(--Spacing-x9));
|
||||||
|
border-bottom: none;
|
||||||
|
border-radius: var(--Corner-radius-Large) var(--Corner-radius-Large) 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hider {
|
||||||
|
display: block;
|
||||||
|
background-color: var(--Scandic-Brand-Warm-White);
|
||||||
|
position: sticky;
|
||||||
|
margin-top: var(--Spacing-x4);
|
||||||
|
top: calc(
|
||||||
|
var(--booking-widget-desktop-height) +
|
||||||
|
var(--booking-widget-desktop-height) - 6px
|
||||||
|
);
|
||||||
|
height: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shadow {
|
||||||
|
display: block;
|
||||||
|
background-color: var(--Main-Grey-White);
|
||||||
|
border-color: var(--Primary-Light-On-Surface-Divider-subtle);
|
||||||
|
border-style: solid;
|
||||||
|
border-left-width: 1px;
|
||||||
|
border-right-width: 1px;
|
||||||
|
border-top: none;
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (min-width: 1367px) {
|
||||||
|
.summary {
|
||||||
|
top: calc(
|
||||||
|
var(--booking-widget-desktop-height) + var(--Spacing-x2) +
|
||||||
|
var(--Spacing-x-half)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hider {
|
||||||
|
top: calc(var(--booking-widget-desktop-height) - 6px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,54 +1,42 @@
|
|||||||
import { redirect } from "next/navigation"
|
|
||||||
|
|
||||||
import {
|
|
||||||
getCreditCardsSafely,
|
|
||||||
getHotelData,
|
|
||||||
getProfileSafely,
|
|
||||||
} from "@/lib/trpc/memoizedRequests"
|
|
||||||
|
|
||||||
import EnterDetailsProvider from "@/components/HotelReservation/EnterDetails/Provider"
|
import EnterDetailsProvider from "@/components/HotelReservation/EnterDetails/Provider"
|
||||||
import SelectedRoom from "@/components/HotelReservation/EnterDetails/SelectedRoom"
|
import SelectedRoom from "@/components/HotelReservation/EnterDetails/SelectedRoom"
|
||||||
import SidePeek from "@/components/HotelReservation/EnterDetails/SidePeek"
|
|
||||||
import Summary from "@/components/HotelReservation/EnterDetails/Summary"
|
|
||||||
import HotelSelectionHeader from "@/components/HotelReservation/HotelSelectionHeader"
|
|
||||||
import { setLang } from "@/i18n/serverContext"
|
import { setLang } from "@/i18n/serverContext"
|
||||||
|
|
||||||
|
import { preload } from "./page"
|
||||||
|
|
||||||
import styles from "./layout.module.css"
|
import styles from "./layout.module.css"
|
||||||
|
|
||||||
import { StepEnum } from "@/types/components/enterDetails/step"
|
import { StepEnum } from "@/types/components/hotelReservation/enterDetails/step"
|
||||||
import type { LangParams, LayoutArgs } from "@/types/params"
|
import type { LangParams, LayoutArgs } from "@/types/params"
|
||||||
|
|
||||||
function preload(id: string, lang: string) {
|
|
||||||
void getHotelData(id, lang)
|
|
||||||
void getProfileSafely()
|
|
||||||
void getCreditCardsSafely()
|
|
||||||
}
|
|
||||||
|
|
||||||
export default async function StepLayout({
|
export default async function StepLayout({
|
||||||
|
summary,
|
||||||
children,
|
children,
|
||||||
|
hotelHeader,
|
||||||
params,
|
params,
|
||||||
}: React.PropsWithChildren<LayoutArgs<LangParams & { step: StepEnum }>>) {
|
sidePeek,
|
||||||
|
}: React.PropsWithChildren<
|
||||||
|
LayoutArgs<LangParams & { step: StepEnum }> & {
|
||||||
|
hotelHeader: React.ReactNode
|
||||||
|
sidePeek: React.ReactNode
|
||||||
|
summary: React.ReactNode
|
||||||
|
}>) {
|
||||||
setLang(params.lang)
|
setLang(params.lang)
|
||||||
preload("811", params.lang)
|
preload()
|
||||||
|
|
||||||
const hotel = await getHotelData("811", params.lang)
|
|
||||||
|
|
||||||
if (!hotel?.data) {
|
|
||||||
redirect(`/${params.lang}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<EnterDetailsProvider step={params.step}>
|
<EnterDetailsProvider step={params.step} >
|
||||||
<main className={styles.layout}>
|
<main className={styles.layout}>
|
||||||
<HotelSelectionHeader hotel={hotel.data.attributes} />
|
{hotelHeader}
|
||||||
<div className={styles.content}>
|
<div className={styles.content}>
|
||||||
<SelectedRoom />
|
<SelectedRoom />
|
||||||
{children}
|
{children}
|
||||||
<aside className={styles.summary}>
|
<aside className={styles.summaryContainer}>
|
||||||
<Summary />
|
<div className={styles.hider} />
|
||||||
|
<div className={styles.summary}>{summary}</div>
|
||||||
|
<div className={styles.shadow} />
|
||||||
</aside>
|
</aside>
|
||||||
</div>
|
</div>
|
||||||
<SidePeek hotel={hotel.data.attributes} />
|
{sidePeek}
|
||||||
</main>
|
</main>
|
||||||
</EnterDetailsProvider>
|
</EnterDetailsProvider>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
import { notFound } from "next/navigation"
|
import { notFound } from "next/navigation"
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
getBreakfastPackages,
|
||||||
getCreditCardsSafely,
|
getCreditCardsSafely,
|
||||||
getHotelData,
|
getHotelData,
|
||||||
getProfileSafely,
|
getProfileSafely,
|
||||||
|
getSelectedRoomAvailability,
|
||||||
} from "@/lib/trpc/memoizedRequests"
|
} from "@/lib/trpc/memoizedRequests"
|
||||||
|
import { HotelIncludeEnum } from "@/server/routers/hotels/input"
|
||||||
|
|
||||||
import BedType from "@/components/HotelReservation/EnterDetails/BedType"
|
import BedType from "@/components/HotelReservation/EnterDetails/BedType"
|
||||||
import Breakfast from "@/components/HotelReservation/EnterDetails/Breakfast"
|
import Breakfast from "@/components/HotelReservation/EnterDetails/Breakfast"
|
||||||
@@ -12,66 +15,140 @@ import Details from "@/components/HotelReservation/EnterDetails/Details"
|
|||||||
import HistoryStateManager from "@/components/HotelReservation/EnterDetails/HistoryStateManager"
|
import HistoryStateManager from "@/components/HotelReservation/EnterDetails/HistoryStateManager"
|
||||||
import Payment from "@/components/HotelReservation/EnterDetails/Payment"
|
import Payment from "@/components/HotelReservation/EnterDetails/Payment"
|
||||||
import SectionAccordion from "@/components/HotelReservation/EnterDetails/SectionAccordion"
|
import SectionAccordion from "@/components/HotelReservation/EnterDetails/SectionAccordion"
|
||||||
|
import { getQueryParamsForEnterDetails } from "@/components/HotelReservation/SelectRate/RoomSelection/utils"
|
||||||
import { getIntl } from "@/i18n"
|
import { getIntl } from "@/i18n"
|
||||||
|
|
||||||
import { StepEnum } from "@/types/components/enterDetails/step"
|
import { StepEnum } from "@/types/components/hotelReservation/enterDetails/step"
|
||||||
|
import { SelectRateSearchParams } from "@/types/components/hotelReservation/selectRate/selectRate"
|
||||||
import type { LangParams, PageArgs } from "@/types/params"
|
import type { LangParams, PageArgs } from "@/types/params"
|
||||||
|
|
||||||
|
export function preload() {
|
||||||
|
void getProfileSafely()
|
||||||
|
void getCreditCardsSafely()
|
||||||
|
}
|
||||||
|
|
||||||
function isValidStep(step: string): step is StepEnum {
|
function isValidStep(step: string): step is StepEnum {
|
||||||
return Object.values(StepEnum).includes(step as StepEnum)
|
return Object.values(StepEnum).includes(step as StepEnum)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default async function StepPage({
|
export default async function StepPage({
|
||||||
params,
|
params,
|
||||||
}: PageArgs<LangParams & { step: StepEnum }>) {
|
searchParams,
|
||||||
const { step, lang } = params
|
}: PageArgs<LangParams & { step: StepEnum }, SelectRateSearchParams>) {
|
||||||
|
const { lang } = params
|
||||||
|
|
||||||
const intl = await getIntl()
|
const intl = await getIntl()
|
||||||
|
const selectRoomParams = new URLSearchParams(searchParams)
|
||||||
|
const {
|
||||||
|
hotel: hotelId,
|
||||||
|
adults,
|
||||||
|
children,
|
||||||
|
roomTypeCode,
|
||||||
|
rateCode,
|
||||||
|
fromDate,
|
||||||
|
toDate,
|
||||||
|
} = getQueryParamsForEnterDetails(selectRoomParams)
|
||||||
|
|
||||||
const hotel = await getHotelData("811", lang)
|
const breakfastInput = { adults, fromDate, hotelId, toDate }
|
||||||
|
void getBreakfastPackages(breakfastInput)
|
||||||
|
void getSelectedRoomAvailability({
|
||||||
|
hotelId: parseInt(searchParams.hotel),
|
||||||
|
adults,
|
||||||
|
children,
|
||||||
|
roomStayStartDate: fromDate,
|
||||||
|
roomStayEndDate: toDate,
|
||||||
|
rateCode,
|
||||||
|
roomTypeCode,
|
||||||
|
})
|
||||||
|
|
||||||
|
const hotelData = await getHotelData({
|
||||||
|
hotelId,
|
||||||
|
language: lang,
|
||||||
|
include: [HotelIncludeEnum.RoomCategories],
|
||||||
|
})
|
||||||
|
const roomAvailability = await getSelectedRoomAvailability({
|
||||||
|
hotelId: parseInt(searchParams.hotel),
|
||||||
|
adults,
|
||||||
|
children,
|
||||||
|
roomStayStartDate: fromDate,
|
||||||
|
roomStayEndDate: toDate,
|
||||||
|
rateCode,
|
||||||
|
roomTypeCode,
|
||||||
|
})
|
||||||
|
const breakfastPackages = await getBreakfastPackages(breakfastInput)
|
||||||
const user = await getProfileSafely()
|
const user = await getProfileSafely()
|
||||||
const savedCreditCards = await getCreditCardsSafely()
|
const savedCreditCards = await getCreditCardsSafely()
|
||||||
|
|
||||||
if (!isValidStep(step) || !hotel) {
|
if (!isValidStep(params.step) || !hotelData || !roomAvailability) {
|
||||||
return notFound()
|
return notFound()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const mustBeGuaranteed = roomAvailability?.mustBeGuaranteed ?? false
|
||||||
|
|
||||||
|
const paymentGuarantee = intl.formatMessage({
|
||||||
|
id: "Payment Guarantee",
|
||||||
|
})
|
||||||
|
const payment = intl.formatMessage({
|
||||||
|
id: "Payment",
|
||||||
|
})
|
||||||
|
const guaranteeWithCard = intl.formatMessage({
|
||||||
|
id: "Guarantee booking with credit card",
|
||||||
|
})
|
||||||
|
const selectPaymentMethod = intl.formatMessage({
|
||||||
|
id: "Select payment method",
|
||||||
|
})
|
||||||
|
|
||||||
|
const availableRoom = roomAvailability.selectedRoom?.roomType
|
||||||
|
const bedTypes = hotelData.included
|
||||||
|
?.find((room) => room.name === availableRoom)
|
||||||
|
?.roomTypes.map((room) => ({
|
||||||
|
description: room.mainBed.description,
|
||||||
|
size: room.mainBed.widthRange,
|
||||||
|
value: room.code,
|
||||||
|
}))
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section>
|
<section>
|
||||||
<HistoryStateManager />
|
<HistoryStateManager />
|
||||||
|
|
||||||
|
{/* TODO: How to handle no beds found? */}
|
||||||
|
{bedTypes ? (
|
||||||
<SectionAccordion
|
<SectionAccordion
|
||||||
header="Select bed"
|
header="Select bed"
|
||||||
step={StepEnum.selectBed}
|
step={StepEnum.selectBed}
|
||||||
label={intl.formatMessage({ id: "Request bedtype" })}
|
label={intl.formatMessage({ id: "Request bedtype" })}
|
||||||
>
|
>
|
||||||
<BedType />
|
<BedType bedTypes={bedTypes} />
|
||||||
</SectionAccordion>
|
</SectionAccordion>
|
||||||
|
) : null}
|
||||||
|
|
||||||
<SectionAccordion
|
<SectionAccordion
|
||||||
header="Food options"
|
header={intl.formatMessage({ id: "Food options" })}
|
||||||
step={StepEnum.breakfast}
|
step={StepEnum.breakfast}
|
||||||
label={intl.formatMessage({ id: "Select breakfast options" })}
|
label={intl.formatMessage({ id: "Select breakfast options" })}
|
||||||
>
|
>
|
||||||
<Breakfast />
|
<Breakfast packages={breakfastPackages} />
|
||||||
</SectionAccordion>
|
</SectionAccordion>
|
||||||
<SectionAccordion
|
<SectionAccordion
|
||||||
header="Details"
|
header={intl.formatMessage({ id: "Details" })}
|
||||||
step={StepEnum.details}
|
step={StepEnum.details}
|
||||||
label={intl.formatMessage({ id: "Enter your details" })}
|
label={intl.formatMessage({ id: "Enter your details" })}
|
||||||
>
|
>
|
||||||
<Details user={user} />
|
<Details user={user} />
|
||||||
</SectionAccordion>
|
</SectionAccordion>
|
||||||
<SectionAccordion
|
<SectionAccordion
|
||||||
header="Payment"
|
header={mustBeGuaranteed ? paymentGuarantee : payment}
|
||||||
step={StepEnum.payment}
|
step={StepEnum.payment}
|
||||||
label={intl.formatMessage({ id: "Select payment method" })}
|
label={mustBeGuaranteed ? guaranteeWithCard : selectPaymentMethod}
|
||||||
>
|
>
|
||||||
<Payment
|
<Payment
|
||||||
hotelId={hotel.data.attributes.operaId}
|
hotelId={searchParams.hotel}
|
||||||
otherPaymentOptions={
|
otherPaymentOptions={
|
||||||
hotel.data.attributes.merchantInformationData
|
hotelData.data.attributes.merchantInformationData
|
||||||
.alternatePaymentOptions
|
.alternatePaymentOptions
|
||||||
}
|
}
|
||||||
savedCreditCards={savedCreditCards}
|
savedCreditCards={savedCreditCards}
|
||||||
|
mustBeGuaranteed={mustBeGuaranteed}
|
||||||
/>
|
/>
|
||||||
</SectionAccordion>
|
</SectionAccordion>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
.layout {
|
.layout {
|
||||||
min-height: 100dvh;
|
|
||||||
background-color: var(--Base-Background-Primary-Normal);
|
background-color: var(--Base-Background-Primary-Normal);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,11 +5,12 @@ import { getLocations } from "@/lib/trpc/memoizedRequests"
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
fetchAvailableHotels,
|
fetchAvailableHotels,
|
||||||
|
generateChildrenString,
|
||||||
getFiltersFromHotels,
|
getFiltersFromHotels,
|
||||||
} from "@/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/utils"
|
} from "@/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/utils"
|
||||||
import HotelCardListing from "@/components/HotelReservation/HotelCardListing"
|
import HotelCardListing from "@/components/HotelReservation/HotelCardListing"
|
||||||
import HotelFilter from "@/components/HotelReservation/SelectHotel/HotelFilter"
|
import HotelFilter from "@/components/HotelReservation/SelectHotel/HotelFilter"
|
||||||
import getHotelReservationQueryParams from "@/components/HotelReservation/SelectRate/RoomSelection/utils"
|
import { getHotelReservationQueryParams } from "@/components/HotelReservation/SelectRate/RoomSelection/utils"
|
||||||
import { ChevronRightIcon } from "@/components/Icons"
|
import { ChevronRightIcon } from "@/components/Icons"
|
||||||
import StaticMap from "@/components/Maps/StaticMap"
|
import StaticMap from "@/components/Maps/StaticMap"
|
||||||
import Link from "@/components/TempDesignSystem/Link"
|
import Link from "@/components/TempDesignSystem/Link"
|
||||||
@@ -42,7 +43,9 @@ export default async function SelectHotelPage({
|
|||||||
const selectHotelParamsObject =
|
const selectHotelParamsObject =
|
||||||
getHotelReservationQueryParams(selectHotelParams)
|
getHotelReservationQueryParams(selectHotelParams)
|
||||||
const adults = selectHotelParamsObject.room[0].adults // TODO: Handle multiple rooms
|
const adults = selectHotelParamsObject.room[0].adults // TODO: Handle multiple rooms
|
||||||
const children = selectHotelParamsObject.room[0].child?.length // TODO: Handle multiple rooms
|
const children = selectHotelParamsObject.room[0].child
|
||||||
|
? generateChildrenString(selectHotelParamsObject.room[0].child)
|
||||||
|
: undefined // TODO: Handle multiple rooms
|
||||||
|
|
||||||
const hotels = await fetchAvailableHotels({
|
const hotels = await fetchAvailableHotels({
|
||||||
cityId: city.id,
|
cityId: city.id,
|
||||||
|
|||||||
@@ -2,9 +2,11 @@ import { serverClient } from "@/lib/trpc/server"
|
|||||||
|
|
||||||
import { getLang } from "@/i18n/serverContext"
|
import { getLang } from "@/i18n/serverContext"
|
||||||
|
|
||||||
|
import { BedTypeEnum } from "@/types/components/bookingWidget/enums"
|
||||||
import { AvailabilityInput } from "@/types/components/hotelReservation/selectHotel/availabilityInput"
|
import { AvailabilityInput } from "@/types/components/hotelReservation/selectHotel/availabilityInput"
|
||||||
import { HotelData } from "@/types/components/hotelReservation/selectHotel/hotelCardListingProps"
|
import { HotelData } from "@/types/components/hotelReservation/selectHotel/hotelCardListingProps"
|
||||||
import { Filter } from "@/types/components/hotelReservation/selectHotel/hotelFilters"
|
import { Filter } from "@/types/components/hotelReservation/selectHotel/hotelFilters"
|
||||||
|
import { Child } from "@/types/components/hotelReservation/selectRate/selectRate"
|
||||||
|
|
||||||
export async function fetchAvailableHotels(
|
export async function fetchAvailableHotels(
|
||||||
input: AvailabilityInput
|
input: AvailabilityInput
|
||||||
@@ -41,3 +43,19 @@ export function getFiltersFromHotels(hotels: HotelData[]) {
|
|||||||
|
|
||||||
return filterList
|
return filterList
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const bedTypeMap: Record<number, string> = {
|
||||||
|
[BedTypeEnum.IN_ADULTS_BED]: "ParentsBed",
|
||||||
|
[BedTypeEnum.IN_CRIB]: "Crib",
|
||||||
|
[BedTypeEnum.IN_EXTRA_BED]: "ExtraBed",
|
||||||
|
}
|
||||||
|
|
||||||
|
export function generateChildrenString(children: Child[]): string {
|
||||||
|
return `[${children
|
||||||
|
?.map((child) => {
|
||||||
|
const age = child.age
|
||||||
|
const bedType = bedTypeMap[+child.bed]
|
||||||
|
return `${age}:${bedType}`
|
||||||
|
})
|
||||||
|
.join(",")}]`
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,24 +0,0 @@
|
|||||||
.page {
|
|
||||||
min-height: 100dvh;
|
|
||||||
padding-top: var(--Spacing-x6);
|
|
||||||
padding-left: var(--Spacing-x2);
|
|
||||||
padding-right: var(--Spacing-x2);
|
|
||||||
background-color: var(--Scandic-Brand-Warm-White);
|
|
||||||
}
|
|
||||||
|
|
||||||
.content {
|
|
||||||
max-width: var(--max-width);
|
|
||||||
margin: 0 auto;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: var(--Spacing-x7);
|
|
||||||
padding: var(--Spacing-x2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.main {
|
|
||||||
flex-grow: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.summary {
|
|
||||||
max-width: 340px;
|
|
||||||
}
|
|
||||||
@@ -1,14 +1,18 @@
|
|||||||
|
import { notFound } from "next/navigation"
|
||||||
|
|
||||||
import { getProfileSafely } from "@/lib/trpc/memoizedRequests"
|
import { getProfileSafely } from "@/lib/trpc/memoizedRequests"
|
||||||
import { serverClient } from "@/lib/trpc/server"
|
import { serverClient } from "@/lib/trpc/server"
|
||||||
|
import { HotelIncludeEnum } from "@/server/routers/hotels/input"
|
||||||
|
|
||||||
import HotelInfoCard from "@/components/HotelReservation/SelectRate/HotelInfoCard"
|
import HotelInfoCard from "@/components/HotelReservation/SelectRate/HotelInfoCard"
|
||||||
import RoomSelection from "@/components/HotelReservation/SelectRate/RoomSelection"
|
import Rooms from "@/components/HotelReservation/SelectRate/Rooms"
|
||||||
import getHotelReservationQueryParams from "@/components/HotelReservation/SelectRate/RoomSelection/utils"
|
import { getHotelReservationQueryParams } from "@/components/HotelReservation/SelectRate/RoomSelection/utils"
|
||||||
import { setLang } from "@/i18n/serverContext"
|
import { setLang } from "@/i18n/serverContext"
|
||||||
|
|
||||||
import styles from "./page.module.css"
|
import { generateChildrenString } from "../select-hotel/utils"
|
||||||
|
|
||||||
import { SelectRateSearchParams } from "@/types/components/hotelReservation/selectRate/selectRate"
|
import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter"
|
||||||
|
import type { SelectRateSearchParams } from "@/types/components/hotelReservation/selectRate/selectRate"
|
||||||
import { LangParams, PageArgs } from "@/types/params"
|
import { LangParams, PageArgs } from "@/types/params"
|
||||||
|
|
||||||
export default async function SelectRatePage({
|
export default async function SelectRatePage({
|
||||||
@@ -20,14 +24,22 @@ export default async function SelectRatePage({
|
|||||||
const selectRoomParams = new URLSearchParams(searchParams)
|
const selectRoomParams = new URLSearchParams(searchParams)
|
||||||
const selectRoomParamsObject =
|
const selectRoomParamsObject =
|
||||||
getHotelReservationQueryParams(selectRoomParams)
|
getHotelReservationQueryParams(selectRoomParams)
|
||||||
const adults = selectRoomParamsObject.room[0].adults // TODO: Handle multiple rooms
|
|
||||||
const children = selectRoomParamsObject.room[0].child?.length // TODO: Handle multiple rooms
|
|
||||||
|
|
||||||
const [hotelData, roomConfigurations, user] = await Promise.all([
|
if (!selectRoomParamsObject.room) {
|
||||||
|
return notFound()
|
||||||
|
}
|
||||||
|
|
||||||
|
const adults = selectRoomParamsObject.room[0].adults // TODO: Handle multiple rooms
|
||||||
|
const childrenCount = selectRoomParamsObject.room[0].child?.length
|
||||||
|
const children = selectRoomParamsObject.room[0].child
|
||||||
|
? generateChildrenString(selectRoomParamsObject.room[0].child)
|
||||||
|
: undefined // TODO: Handle multiple rooms
|
||||||
|
|
||||||
|
const [hotelData, roomsAvailability, packages, user] = await Promise.all([
|
||||||
serverClient().hotel.hotelData.get({
|
serverClient().hotel.hotelData.get({
|
||||||
hotelId: searchParams.hotel,
|
hotelId: searchParams.hotel,
|
||||||
language: params.lang,
|
language: params.lang,
|
||||||
include: ["RoomCategories"],
|
include: [HotelIncludeEnum.RoomCategories],
|
||||||
}),
|
}),
|
||||||
serverClient().hotel.availability.rooms({
|
serverClient().hotel.availability.rooms({
|
||||||
hotelId: parseInt(searchParams.hotel, 10),
|
hotelId: parseInt(searchParams.hotel, 10),
|
||||||
@@ -36,10 +48,22 @@ export default async function SelectRatePage({
|
|||||||
adults,
|
adults,
|
||||||
children,
|
children,
|
||||||
}),
|
}),
|
||||||
|
serverClient().hotel.packages.get({
|
||||||
|
hotelId: searchParams.hotel,
|
||||||
|
startDate: searchParams.fromDate,
|
||||||
|
endDate: searchParams.toDate,
|
||||||
|
adults,
|
||||||
|
children: childrenCount,
|
||||||
|
packageCodes: [
|
||||||
|
RoomPackageCodeEnum.ACCESSIBILITY_ROOM,
|
||||||
|
RoomPackageCodeEnum.PET_ROOM,
|
||||||
|
RoomPackageCodeEnum.ALLERGY_ROOM,
|
||||||
|
],
|
||||||
|
}),
|
||||||
getProfileSafely(),
|
getProfileSafely(),
|
||||||
])
|
])
|
||||||
|
|
||||||
if (!roomConfigurations) {
|
if (!roomsAvailability) {
|
||||||
return "No rooms found" // TODO: Add a proper error message
|
return "No rooms found" // TODO: Add a proper error message
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -50,17 +74,14 @@ export default async function SelectRatePage({
|
|||||||
const roomCategories = hotelData?.included
|
const roomCategories = hotelData?.included
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<>
|
||||||
<HotelInfoCard hotelData={hotelData} />
|
<HotelInfoCard hotelData={hotelData} />
|
||||||
<div className={styles.content}>
|
<Rooms
|
||||||
<div className={styles.main}>
|
roomsAvailability={roomsAvailability}
|
||||||
<RoomSelection
|
|
||||||
roomConfigurations={roomConfigurations}
|
|
||||||
roomCategories={roomCategories ?? []}
|
roomCategories={roomCategories ?? []}
|
||||||
user={user}
|
user={user}
|
||||||
|
packages={packages ?? []}
|
||||||
/>
|
/>
|
||||||
</div>
|
</>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,13 +3,11 @@ import { serverClient } from "@/lib/trpc/server"
|
|||||||
|
|
||||||
import BookingWidget, { preload } from "@/components/BookingWidget"
|
import BookingWidget, { preload } from "@/components/BookingWidget"
|
||||||
|
|
||||||
import { BookingWidgetSearchParams } from "@/types/components/bookingWidget"
|
import { PageArgs } from "@/types/params"
|
||||||
import { LangParams, PageArgs } from "@/types/params"
|
|
||||||
|
|
||||||
export default async function BookingWidgetPage({
|
export default async function BookingWidgetPage({
|
||||||
params,
|
|
||||||
searchParams,
|
searchParams,
|
||||||
}: PageArgs<LangParams, BookingWidgetSearchParams>) {
|
}: PageArgs<{}, URLSearchParams>) {
|
||||||
if (env.HIDE_FOR_NEXT_RELEASE) {
|
if (env.HIDE_FOR_NEXT_RELEASE) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
@font-face {
|
@font-face {
|
||||||
font-display: swap;
|
font-display: fallback;
|
||||||
font-family: "biro script plus";
|
font-family: "biro script plus";
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
@@ -7,7 +7,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-display: swap;
|
font-display: fallback;
|
||||||
font-family: "brandon text";
|
font-family: "brandon text";
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
src:
|
src:
|
||||||
@@ -16,7 +16,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-display: swap;
|
font-display: fallback;
|
||||||
font-family: "brandon text";
|
font-family: "brandon text";
|
||||||
font-weight: 900;
|
font-weight: 900;
|
||||||
src:
|
src:
|
||||||
@@ -25,7 +25,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-display: swap;
|
font-display: fallback;
|
||||||
font-family: "fira mono";
|
font-family: "fira mono";
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
@@ -33,7 +33,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-display: swap;
|
font-display: fallback;
|
||||||
font-family: "fira mono";
|
font-family: "fira mono";
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
@@ -41,7 +41,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-display: swap;
|
font-display: fallback;
|
||||||
font-family: "fira mono";
|
font-family: "fira mono";
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
@@ -49,7 +49,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-display: swap;
|
font-display: fallback;
|
||||||
font-family: "fira sans";
|
font-family: "fira sans";
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 300;
|
font-weight: 300;
|
||||||
@@ -57,7 +57,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-display: swap;
|
font-display: fallback;
|
||||||
font-family: "fira sans";
|
font-family: "fira sans";
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
@@ -65,7 +65,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-display: swap;
|
font-display: fallback;
|
||||||
font-family: "fira sans";
|
font-family: "fira sans";
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
@@ -73,7 +73,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-display: swap;
|
font-display: fallback;
|
||||||
font-family: "fira sans";
|
font-family: "fira sans";
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
@@ -81,7 +81,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-display: swap;
|
font-display: fallback;
|
||||||
font-family: "fira sans";
|
font-family: "fira sans";
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
@@ -89,7 +89,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-display: swap;
|
font-display: fallback;
|
||||||
font-family: "fira sans";
|
font-family: "fira sans";
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 900;
|
font-weight: 900;
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { dt } from "@/lib/dt"
|
|||||||
import Body from "@/components/TempDesignSystem/Text/Body"
|
import Body from "@/components/TempDesignSystem/Text/Body"
|
||||||
import { getIntl } from "@/i18n"
|
import { getIntl } from "@/i18n"
|
||||||
import { getLang } from "@/i18n/serverContext"
|
import { getLang } from "@/i18n/serverContext"
|
||||||
|
import { formatNumber } from "@/utils/format"
|
||||||
import { getMembership } from "@/utils/user"
|
import { getMembership } from "@/utils/user"
|
||||||
|
|
||||||
import type { UserProps } from "@/types/components/myPages/user"
|
import type { UserProps } from "@/types/components/myPages/user"
|
||||||
@@ -16,9 +17,6 @@ export default async function ExpiringPoints({ user }: UserProps) {
|
|||||||
// TODO: handle this case?
|
// TODO: handle this case?
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
// sv hardcoded to force space on thousands
|
|
||||||
const formatter = new Intl.NumberFormat(Lang.sv)
|
|
||||||
const d = dt(membership.pointsExpiryDate)
|
const d = dt(membership.pointsExpiryDate)
|
||||||
|
|
||||||
const dateFormat = getLang() == Lang.fi ? "DD.MM.YYYY" : "YYYY-MM-DD"
|
const dateFormat = getLang() == Lang.fi ? "DD.MM.YYYY" : "YYYY-MM-DD"
|
||||||
@@ -29,7 +27,7 @@ export default async function ExpiringPoints({ user }: UserProps) {
|
|||||||
{intl.formatMessage(
|
{intl.formatMessage(
|
||||||
{ id: "spendable points expiring by" },
|
{ id: "spendable points expiring by" },
|
||||||
{
|
{
|
||||||
points: formatter.format(membership.pointsToExpire),
|
points: formatNumber(membership.pointsToExpire),
|
||||||
date: d.format(dateFormat),
|
date: d.format(dateFormat),
|
||||||
}
|
}
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import { useIntl } from "react-intl"
|
import { useIntl } from "react-intl"
|
||||||
|
|
||||||
import { Lang } from "@/constants/languages"
|
|
||||||
|
|
||||||
import Body from "@/components/TempDesignSystem/Text/Body"
|
import Body from "@/components/TempDesignSystem/Text/Body"
|
||||||
|
import { formatNumber } from "@/utils/format"
|
||||||
|
|
||||||
import { awardPointsVariants } from "./awardPointsVariants"
|
import { awardPointsVariants } from "./awardPointsVariants"
|
||||||
|
|
||||||
@@ -32,12 +31,10 @@ export default function AwardPoints({
|
|||||||
variant,
|
variant,
|
||||||
})
|
})
|
||||||
|
|
||||||
// sv hardcoded to force space on thousands
|
|
||||||
const formatter = new Intl.NumberFormat(Lang.sv)
|
|
||||||
return (
|
return (
|
||||||
<Body textTransform="bold" className={classNames}>
|
<Body textTransform="bold" className={classNames}>
|
||||||
{isCalculated
|
{isCalculated
|
||||||
? formatter.format(awardPoints)
|
? formatNumber(awardPoints)
|
||||||
: intl.formatMessage({ id: "Points being calculated" })}
|
: intl.formatMessage({ id: "Points being calculated" })}
|
||||||
</Body>
|
</Body>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,16 +1,18 @@
|
|||||||
"use client"
|
"use client"
|
||||||
import { zodResolver } from "@hookform/resolvers/zod"
|
import { zodResolver } from "@hookform/resolvers/zod"
|
||||||
import { useEffect, useState } from "react"
|
import { useEffect, useRef, useState } from "react"
|
||||||
import { FormProvider, useForm } from "react-hook-form"
|
import { FormProvider, useForm } from "react-hook-form"
|
||||||
|
|
||||||
import { dt } from "@/lib/dt"
|
import { dt } from "@/lib/dt"
|
||||||
|
import { StickyElementNameEnum } from "@/stores/sticky-position"
|
||||||
|
|
||||||
import Form from "@/components/Forms/BookingWidget"
|
import Form from "@/components/Forms/BookingWidget"
|
||||||
import { bookingWidgetSchema } from "@/components/Forms/BookingWidget/schema"
|
import { bookingWidgetSchema } from "@/components/Forms/BookingWidget/schema"
|
||||||
import { CloseLargeIcon } from "@/components/Icons"
|
import { CloseLargeIcon } from "@/components/Icons"
|
||||||
|
import useStickyPosition from "@/hooks/useStickyPosition"
|
||||||
import { debounce } from "@/utils/debounce"
|
import { debounce } from "@/utils/debounce"
|
||||||
|
import { getFormattedUrlQueryParams } from "@/utils/url"
|
||||||
|
|
||||||
import getHotelReservationQueryParams from "../HotelReservation/SelectRate/RoomSelection/utils"
|
|
||||||
import MobileToggleButton from "./MobileToggleButton"
|
import MobileToggleButton from "./MobileToggleButton"
|
||||||
|
|
||||||
import styles from "./bookingWidget.module.css"
|
import styles from "./bookingWidget.module.css"
|
||||||
@@ -18,6 +20,7 @@ import styles from "./bookingWidget.module.css"
|
|||||||
import type {
|
import type {
|
||||||
BookingWidgetClientProps,
|
BookingWidgetClientProps,
|
||||||
BookingWidgetSchema,
|
BookingWidgetSchema,
|
||||||
|
BookingWidgetSearchParams,
|
||||||
} from "@/types/components/bookingWidget"
|
} from "@/types/components/bookingWidget"
|
||||||
import type { Location } from "@/types/trpc/routers/hotel/locations"
|
import type { Location } from "@/types/trpc/routers/hotel/locations"
|
||||||
|
|
||||||
@@ -27,6 +30,11 @@ export default function BookingWidgetClient({
|
|||||||
searchParams,
|
searchParams,
|
||||||
}: BookingWidgetClientProps) {
|
}: BookingWidgetClientProps) {
|
||||||
const [isOpen, setIsOpen] = useState(false)
|
const [isOpen, setIsOpen] = useState(false)
|
||||||
|
const bookingWidgetRef = useRef(null)
|
||||||
|
useStickyPosition({
|
||||||
|
ref: bookingWidgetRef,
|
||||||
|
name: StickyElementNameEnum.BOOKING_WIDGET,
|
||||||
|
})
|
||||||
|
|
||||||
const sessionStorageSearchData =
|
const sessionStorageSearchData =
|
||||||
typeof window !== "undefined"
|
typeof window !== "undefined"
|
||||||
@@ -36,11 +44,13 @@ export default function BookingWidgetClient({
|
|||||||
? JSON.parse(sessionStorageSearchData)
|
? JSON.parse(sessionStorageSearchData)
|
||||||
: undefined
|
: undefined
|
||||||
|
|
||||||
const bookingWidgetSearchParams = searchParams
|
const bookingWidgetSearchData: BookingWidgetSearchParams | undefined =
|
||||||
? new URLSearchParams(searchParams)
|
searchParams
|
||||||
: undefined
|
? (getFormattedUrlQueryParams(new URLSearchParams(searchParams), {
|
||||||
const bookingWidgetSearchData = bookingWidgetSearchParams
|
adults: "number",
|
||||||
? getHotelReservationQueryParams(bookingWidgetSearchParams)
|
age: "number",
|
||||||
|
bed: "number",
|
||||||
|
}) as BookingWidgetSearchParams)
|
||||||
: undefined
|
: undefined
|
||||||
|
|
||||||
const getLocationObj = (destination: string): Location | undefined => {
|
const getLocationObj = (destination: string): Location | undefined => {
|
||||||
@@ -83,7 +93,7 @@ export default function BookingWidgetClient({
|
|||||||
// UTC is required to handle requests from far away timezones https://scandichotels.atlassian.net/browse/SWAP-6375 & PET-507
|
// 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.
|
// This is specifically to handle timezones falling in different dates.
|
||||||
fromDate: isDateParamValid
|
fromDate: isDateParamValid
|
||||||
? bookingWidgetSearchData?.fromDate.toString()
|
? bookingWidgetSearchData?.fromDate?.toString()
|
||||||
: dt().utc().format("YYYY-MM-DD"),
|
: dt().utc().format("YYYY-MM-DD"),
|
||||||
toDate: isDateParamValid
|
toDate: isDateParamValid
|
||||||
? bookingWidgetSearchData?.toDate?.toString()
|
? bookingWidgetSearchData?.toDate?.toString()
|
||||||
@@ -92,10 +102,10 @@ export default function BookingWidgetClient({
|
|||||||
bookingCode: "",
|
bookingCode: "",
|
||||||
redemption: false,
|
redemption: false,
|
||||||
voucher: false,
|
voucher: false,
|
||||||
rooms: [
|
rooms: bookingWidgetSearchData?.room ?? [
|
||||||
{
|
{
|
||||||
adults: 1,
|
adults: 1,
|
||||||
children: [],
|
child: [],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@@ -136,7 +146,10 @@ export default function BookingWidgetClient({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<FormProvider {...methods}>
|
<FormProvider {...methods}>
|
||||||
<section className={styles.container} data-open={isOpen}>
|
<section ref={bookingWidgetRef} className={styles.containerDesktop}>
|
||||||
|
<Form locations={locations} type={type} />
|
||||||
|
</section>
|
||||||
|
<section className={styles.containerMobile} data-open={isOpen}>
|
||||||
<button
|
<button
|
||||||
className={styles.close}
|
className={styles.close}
|
||||||
onClick={closeMobileSearch}
|
onClick={closeMobileSearch}
|
||||||
|
|||||||
@@ -1,15 +1,17 @@
|
|||||||
"use client"
|
"use client"
|
||||||
import { useEffect, useState } from "react"
|
import { useEffect, useMemo, useRef, useState } from "react"
|
||||||
import { useWatch } from "react-hook-form"
|
import { useWatch } from "react-hook-form"
|
||||||
import { useIntl } from "react-intl"
|
import { useIntl } from "react-intl"
|
||||||
|
|
||||||
import { dt } from "@/lib/dt"
|
import { dt } from "@/lib/dt"
|
||||||
|
import { StickyElementNameEnum } from "@/stores/sticky-position"
|
||||||
|
|
||||||
import { EditIcon, SearchIcon } from "@/components/Icons"
|
import { EditIcon, SearchIcon } from "@/components/Icons"
|
||||||
import Divider from "@/components/TempDesignSystem/Divider"
|
import Divider from "@/components/TempDesignSystem/Divider"
|
||||||
import Body from "@/components/TempDesignSystem/Text/Body"
|
import Body from "@/components/TempDesignSystem/Text/Body"
|
||||||
import Caption from "@/components/TempDesignSystem/Text/Caption"
|
import Caption from "@/components/TempDesignSystem/Text/Caption"
|
||||||
import useLang from "@/hooks/useLang"
|
import useLang from "@/hooks/useLang"
|
||||||
|
import useStickyPosition from "@/hooks/useStickyPosition"
|
||||||
|
|
||||||
import styles from "./button.module.css"
|
import styles from "./button.module.css"
|
||||||
|
|
||||||
@@ -29,6 +31,12 @@ export default function MobileToggleButton({
|
|||||||
const location = useWatch({ name: "location" })
|
const location = useWatch({ name: "location" })
|
||||||
const rooms: BookingWidgetSchema["rooms"] = useWatch({ name: "rooms" })
|
const rooms: BookingWidgetSchema["rooms"] = useWatch({ name: "rooms" })
|
||||||
|
|
||||||
|
const bookingWidgetMobileRef = useRef(null)
|
||||||
|
useStickyPosition({
|
||||||
|
ref: bookingWidgetMobileRef,
|
||||||
|
name: StickyElementNameEnum.BOOKING_WIDGET_MOBILE,
|
||||||
|
})
|
||||||
|
|
||||||
const parsedLocation: Location | null = location
|
const parsedLocation: Location | null = location
|
||||||
? JSON.parse(decodeURIComponent(location))
|
? JSON.parse(decodeURIComponent(location))
|
||||||
: null
|
: null
|
||||||
@@ -46,7 +54,8 @@ export default function MobileToggleButton({
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
if (parsedLocation && d) {
|
const locationAndDateIsSet = parsedLocation && d
|
||||||
|
|
||||||
const totalRooms = rooms.length
|
const totalRooms = rooms.length
|
||||||
const totalAdults = rooms.reduce((acc, room) => {
|
const totalAdults = rooms.reduce((acc, room) => {
|
||||||
if (room.adults) {
|
if (room.adults) {
|
||||||
@@ -54,28 +63,26 @@ export default function MobileToggleButton({
|
|||||||
}
|
}
|
||||||
return acc
|
return acc
|
||||||
}, 0)
|
}, 0)
|
||||||
return (
|
const totalChildren = rooms.reduce((acc, room) => {
|
||||||
<div className={styles.complete} onClick={openMobileSearch} role="button">
|
if (room.child) {
|
||||||
<div>
|
acc = acc + room.child.length
|
||||||
<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 acc
|
||||||
|
}, 0)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.partial} onClick={openMobileSearch} role="button">
|
<div
|
||||||
|
className={locationAndDateIsSet ? styles.complete : styles.partial}
|
||||||
|
onClick={openMobileSearch}
|
||||||
|
role="button"
|
||||||
|
ref={bookingWidgetMobileRef}
|
||||||
|
>
|
||||||
|
{!locationAndDateIsSet && (
|
||||||
|
<>
|
||||||
<div>
|
<div>
|
||||||
<Caption color="red">{intl.formatMessage({ id: "Where to" })}</Caption>
|
<Caption color="red">
|
||||||
|
{intl.formatMessage({ id: "Where to" })}
|
||||||
|
</Caption>
|
||||||
<Body color="uiTextPlaceholder">
|
<Body color="uiTextPlaceholder">
|
||||||
{parsedLocation
|
{parsedLocation
|
||||||
? parsedLocation.name
|
? parsedLocation.name
|
||||||
@@ -97,6 +104,32 @@ export default function MobileToggleButton({
|
|||||||
<div className={styles.icon}>
|
<div className={styles.icon}>
|
||||||
<SearchIcon color="white" />
|
<SearchIcon color="white" />
|
||||||
</div>
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{locationAndDateIsSet && (
|
||||||
|
<>
|
||||||
|
<div>
|
||||||
|
<Caption color="red">{parsedLocation?.name}</Caption>
|
||||||
|
<Caption>
|
||||||
|
{`${selectedFromDate} - ${selectedToDate} (${intl.formatMessage(
|
||||||
|
{ id: "booking.nights" },
|
||||||
|
{ totalNights: nights }
|
||||||
|
)}) ${intl.formatMessage({ id: "booking.adults" }, { totalAdults })}, ${
|
||||||
|
totalChildren > 0
|
||||||
|
? intl.formatMessage(
|
||||||
|
{ id: "booking.children" },
|
||||||
|
{ totalChildren }
|
||||||
|
) + ", "
|
||||||
|
: ""
|
||||||
|
}${intl.formatMessage({ id: "booking.rooms" }, { totalRooms })}`}
|
||||||
|
</Caption>
|
||||||
|
</div>
|
||||||
|
<div className={styles.icon}>
|
||||||
|
<EditIcon color="white" />
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,11 @@
|
|||||||
|
.containerDesktop,
|
||||||
|
.containerMobile,
|
||||||
|
.close {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
@media screen and (max-width: 767px) {
|
@media screen and (max-width: 767px) {
|
||||||
.container {
|
.containerMobile {
|
||||||
background-color: var(--UI-Input-Controls-Surface-Normal);
|
background-color: var(--UI-Input-Controls-Surface-Normal);
|
||||||
bottom: -100%;
|
bottom: -100%;
|
||||||
display: grid;
|
display: grid;
|
||||||
@@ -14,7 +20,7 @@
|
|||||||
border-radius: var(--Corner-radius-Large) var(--Corner-radius-Large) 0 0;
|
border-radius: var(--Corner-radius-Large) var(--Corner-radius-Large) 0 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.container[data-open="true"] {
|
.containerMobile[data-open="true"] {
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -25,7 +31,7 @@
|
|||||||
justify-self: flex-end;
|
justify-self: flex-end;
|
||||||
}
|
}
|
||||||
|
|
||||||
.container[data-open="true"] + .backdrop {
|
.containerMobile[data-open="true"] + .backdrop {
|
||||||
background-color: rgba(0, 0, 0, 0.4);
|
background-color: rgba(0, 0, 0, 0.4);
|
||||||
height: 100%;
|
height: 100%;
|
||||||
left: 0;
|
left: 0;
|
||||||
@@ -37,7 +43,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (min-width: 768px) {
|
@media screen and (min-width: 768px) {
|
||||||
.container {
|
.containerDesktop {
|
||||||
display: block;
|
display: block;
|
||||||
box-shadow: 0px 4px 24px 0px rgba(0, 0, 0, 0.05);
|
box-shadow: 0px 4px 24px 0px rgba(0, 0, 0, 0.05);
|
||||||
position: sticky;
|
position: sticky;
|
||||||
@@ -45,10 +51,6 @@
|
|||||||
z-index: 10;
|
z-index: 10;
|
||||||
background-color: var(--Base-Surface-Primary-light-Normal);
|
background-color: var(--Base-Surface-Primary-light-Normal);
|
||||||
}
|
}
|
||||||
|
|
||||||
.close {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (min-width: 1367px) {
|
@media screen and (min-width: 1367px) {
|
||||||
|
|||||||
@@ -78,6 +78,7 @@ export default function Sidebar({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<>
|
||||||
<aside
|
<aside
|
||||||
className={`${styles.sidebar} ${
|
className={`${styles.sidebar} ${
|
||||||
isFullScreenSidebar ? styles.fullscreen : ""
|
isFullScreenSidebar ? styles.fullscreen : ""
|
||||||
@@ -86,6 +87,7 @@ export default function Sidebar({
|
|||||||
<Button
|
<Button
|
||||||
theme="base"
|
theme="base"
|
||||||
intent="text"
|
intent="text"
|
||||||
|
fullWidth
|
||||||
className={styles.sidebarToggle}
|
className={styles.sidebarToggle}
|
||||||
onClick={toggleFullScreenSidebar}
|
onClick={toggleFullScreenSidebar}
|
||||||
>
|
>
|
||||||
@@ -126,7 +128,9 @@ export default function Sidebar({
|
|||||||
className={`${styles.poiButton} ${activePoi === poi.name ? styles.active : ""}`}
|
className={`${styles.poiButton} ${activePoi === poi.name ? styles.active : ""}`}
|
||||||
onMouseEnter={() => handleMouseEnter(poi.name)}
|
onMouseEnter={() => handleMouseEnter(poi.name)}
|
||||||
onMouseLeave={handleMouseLeave}
|
onMouseLeave={handleMouseLeave}
|
||||||
onClick={() => handlePoiClick(poi.name, poi.coordinates)}
|
onClick={() =>
|
||||||
|
handlePoiClick(poi.name, poi.coordinates)
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<span>{poi.name}</span>
|
<span>{poi.name}</span>
|
||||||
<span>{poi.distance} km</span>
|
<span>{poi.distance} km</span>
|
||||||
@@ -139,5 +143,7 @@ export default function Sidebar({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
|
<div className={styles.backdrop} onClick={toggleFullScreenSidebar} />
|
||||||
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,50 +1,12 @@
|
|||||||
.sidebar {
|
.sidebar {
|
||||||
--sidebar-max-width: 26.25rem;
|
|
||||||
--sidebar-mobile-toggle-height: 91px;
|
|
||||||
--sidebar-mobile-fullscreen-height: calc(
|
|
||||||
100vh - var(--main-menu-mobile-height) - var(--sidebar-mobile-toggle-height)
|
|
||||||
);
|
|
||||||
|
|
||||||
position: absolute;
|
|
||||||
top: var(--sidebar-mobile-fullscreen-height);
|
|
||||||
height: 100%;
|
|
||||||
right: 0;
|
|
||||||
left: 0;
|
|
||||||
background-color: var(--Base-Surface-Primary-light-Normal);
|
background-color: var(--Base-Surface-Primary-light-Normal);
|
||||||
z-index: 1;
|
z-index: 2;
|
||||||
transition: top 0.3s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar:not(.fullscreen) {
|
|
||||||
border-radius: var(--Corner-radius-Large) var(--Corner-radius-Large) 0 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar.fullscreen {
|
|
||||||
top: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebarToggle {
|
|
||||||
position: relative;
|
|
||||||
margin: var(--Spacing-x4) 0 var(--Spacing-x2);
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebarToggle::before {
|
|
||||||
content: "";
|
|
||||||
position: absolute;
|
|
||||||
display: block;
|
|
||||||
top: -0.5rem;
|
|
||||||
width: 100px;
|
|
||||||
height: 3px;
|
|
||||||
background-color: var(--UI-Text-High-contrast);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebarContent {
|
.sidebarContent {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: var(--Spacing-x5);
|
gap: var(--Spacing-x5);
|
||||||
align-content: start;
|
align-content: start;
|
||||||
padding: var(--Spacing-x3) var(--Spacing-x2);
|
|
||||||
height: var(--sidebar-mobile-fullscreen-height);
|
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -90,12 +52,65 @@
|
|||||||
background-color: var(--Base-Surface-Primary-light-Hover);
|
background-color: var(--Base-Surface-Primary-light-Hover);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 767px) {
|
||||||
|
.sidebar {
|
||||||
|
--sidebar-mobile-toggle-height: 84px;
|
||||||
|
--sidebar-mobile-top-space: 40px;
|
||||||
|
--sidebar-mobile-content-height: calc(
|
||||||
|
var(--hotel-map-height) - var(--sidebar-mobile-toggle-height) -
|
||||||
|
var(--sidebar-mobile-top-space)
|
||||||
|
);
|
||||||
|
|
||||||
|
position: absolute;
|
||||||
|
bottom: calc(-1 * var(--sidebar-mobile-content-height));
|
||||||
|
width: 100%;
|
||||||
|
transition:
|
||||||
|
bottom 0.3s,
|
||||||
|
top 0.3s;
|
||||||
|
border-radius: var(--Corner-radius-Large) var(--Corner-radius-Large) 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar.fullscreen + .backdrop {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: 100%;
|
||||||
|
background-color: rgba(0, 0, 0, 0.4);
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar.fullscreen {
|
||||||
|
bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebarToggle {
|
||||||
|
position: relative;
|
||||||
|
margin-top: var(--Spacing-x4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebarToggle::before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
display: block;
|
||||||
|
top: -0.5rem;
|
||||||
|
width: 100px;
|
||||||
|
height: 3px;
|
||||||
|
background-color: var(--UI-Text-High-contrast);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebarContent {
|
||||||
|
padding: var(--Spacing-x3) var(--Spacing-x2);
|
||||||
|
height: var(--sidebar-mobile-content-height);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@media screen and (min-width: 768px) {
|
@media screen and (min-width: 768px) {
|
||||||
.sidebar {
|
.sidebar {
|
||||||
position: static;
|
position: static;
|
||||||
width: 40vw;
|
width: 40vw;
|
||||||
min-width: 10rem;
|
min-width: 10rem;
|
||||||
max-width: var(--sidebar-max-width);
|
max-width: 26.25rem;
|
||||||
background-color: var(--Base-Surface-Primary-light-Normal);
|
background-color: var(--Base-Surface-Primary-light-Normal);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,18 +1,19 @@
|
|||||||
.dynamicMap {
|
.dynamicMap {
|
||||||
position: fixed;
|
--hotel-map-height: 100dvh;
|
||||||
top: var(--main-menu-mobile-height);
|
|
||||||
right: 0;
|
position: absolute;
|
||||||
bottom: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
z-index: var(--dialog-z-index);
|
height: var(--hotel-map-height);
|
||||||
|
width: 100dvw;
|
||||||
|
z-index: var(--hotel-dynamic-map-z-index);
|
||||||
display: flex;
|
display: flex;
|
||||||
background-color: var(--Base-Surface-Primary-light-Normal);
|
background-color: var(--Base-Surface-Primary-light-Normal);
|
||||||
}
|
}
|
||||||
|
.wrapper {
|
||||||
@media screen and (min-width: 768px) {
|
position: absolute;
|
||||||
.dynamicMap {
|
top: 0;
|
||||||
top: var(--main-menu-desktop-height);
|
left: 0;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.closeButton {
|
.closeButton {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"use client"
|
"use client"
|
||||||
import { APIProvider } from "@vis.gl/react-google-maps"
|
import { APIProvider } from "@vis.gl/react-google-maps"
|
||||||
import { useEffect, useRef, useState } from "react"
|
import { useCallback, useEffect, useRef, useState } from "react"
|
||||||
import { Dialog, Modal } from "react-aria-components"
|
import { Dialog, Modal } from "react-aria-components"
|
||||||
import { useIntl } from "react-intl"
|
import { useIntl } from "react-intl"
|
||||||
|
|
||||||
@@ -10,6 +10,7 @@ import CloseLargeIcon from "@/components/Icons/CloseLarge"
|
|||||||
import InteractiveMap from "@/components/Maps/InteractiveMap"
|
import InteractiveMap from "@/components/Maps/InteractiveMap"
|
||||||
import Button from "@/components/TempDesignSystem/Button"
|
import Button from "@/components/TempDesignSystem/Button"
|
||||||
import { useHandleKeyUp } from "@/hooks/useHandleKeyUp"
|
import { useHandleKeyUp } from "@/hooks/useHandleKeyUp"
|
||||||
|
import { debounce } from "@/utils/debounce"
|
||||||
|
|
||||||
import Sidebar from "./Sidebar"
|
import Sidebar from "./Sidebar"
|
||||||
|
|
||||||
@@ -25,9 +26,10 @@ export default function DynamicMap({
|
|||||||
mapId,
|
mapId,
|
||||||
}: DynamicMapProps) {
|
}: DynamicMapProps) {
|
||||||
const intl = useIntl()
|
const intl = useIntl()
|
||||||
|
const rootDiv = useRef<HTMLDivElement | null>(null)
|
||||||
|
const [mapHeight, setMapHeight] = useState("0px")
|
||||||
const { isDynamicMapOpen, closeDynamicMap } = useHotelPageStore()
|
const { isDynamicMapOpen, closeDynamicMap } = useHotelPageStore()
|
||||||
const [scrollHeightWhenOpened, setScrollHeightWhenOpened] = useState(0)
|
const [scrollHeightWhenOpened, setScrollHeightWhenOpened] = useState(0)
|
||||||
const hasMounted = useRef(false)
|
|
||||||
const [activePoi, setActivePoi] = useState<string | null>(null)
|
const [activePoi, setActivePoi] = useState<string | null>(null)
|
||||||
|
|
||||||
useHandleKeyUp((event: KeyboardEvent) => {
|
useHandleKeyUp((event: KeyboardEvent) => {
|
||||||
@@ -36,23 +38,47 @@ export default function DynamicMap({
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// Making sure the map is always opened at the top of the page, just below the header.
|
// Calculate the height of the map based on the viewport height from the start-point (below the header and booking widget)
|
||||||
|
const handleMapHeight = useCallback(() => {
|
||||||
|
const topPosition = rootDiv.current?.getBoundingClientRect().top ?? 0
|
||||||
|
const scrollY = window.scrollY
|
||||||
|
setMapHeight(`calc(100dvh - ${topPosition + scrollY}px)`)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
// Making sure the map is always opened at the top of the page,
|
||||||
|
// just below the header and booking widget as these should stay visible.
|
||||||
// When closing, the page should scroll back to the position it was before opening the map.
|
// When closing, the page should scroll back to the position it was before opening the map.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Skip the first render
|
// Skip the first render
|
||||||
if (!hasMounted.current) {
|
if (!rootDiv.current) {
|
||||||
hasMounted.current = true
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isDynamicMapOpen && scrollHeightWhenOpened === 0) {
|
if (isDynamicMapOpen && scrollHeightWhenOpened === 0) {
|
||||||
setScrollHeightWhenOpened(window.scrollY)
|
const scrollY = window.scrollY
|
||||||
|
setScrollHeightWhenOpened(scrollY)
|
||||||
window.scrollTo({ top: 0, behavior: "instant" })
|
window.scrollTo({ top: 0, behavior: "instant" })
|
||||||
} else if (!isDynamicMapOpen && scrollHeightWhenOpened !== 0) {
|
} else if (!isDynamicMapOpen && scrollHeightWhenOpened !== 0) {
|
||||||
window.scrollTo({ top: scrollHeightWhenOpened, behavior: "instant" })
|
window.scrollTo({ top: scrollHeightWhenOpened, behavior: "instant" })
|
||||||
setScrollHeightWhenOpened(0)
|
setScrollHeightWhenOpened(0)
|
||||||
}
|
}
|
||||||
}, [isDynamicMapOpen, scrollHeightWhenOpened])
|
}, [isDynamicMapOpen, scrollHeightWhenOpened, rootDiv])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const debouncedResizeHandler = debounce(function () {
|
||||||
|
handleMapHeight()
|
||||||
|
})
|
||||||
|
|
||||||
|
const observer = new ResizeObserver(debouncedResizeHandler)
|
||||||
|
|
||||||
|
observer.observe(document.documentElement)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (observer) {
|
||||||
|
observer.unobserve(document.documentElement)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [rootDiv, isDynamicMapOpen, handleMapHeight])
|
||||||
|
|
||||||
const closeButton = (
|
const closeButton = (
|
||||||
<Button
|
<Button
|
||||||
@@ -70,9 +96,14 @@ export default function DynamicMap({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<APIProvider apiKey={apiKey}>
|
<APIProvider apiKey={apiKey}>
|
||||||
<Modal isOpen={isDynamicMapOpen}>
|
<div className={styles.wrapper} ref={rootDiv}>
|
||||||
|
<Modal
|
||||||
|
isOpen={isDynamicMapOpen}
|
||||||
|
UNSTABLE_portalContainer={rootDiv.current || undefined}
|
||||||
|
>
|
||||||
<Dialog
|
<Dialog
|
||||||
className={styles.dynamicMap}
|
className={styles.dynamicMap}
|
||||||
|
style={{ "--hotel-map-height": mapHeight } as React.CSSProperties}
|
||||||
aria-label={intl.formatMessage(
|
aria-label={intl.formatMessage(
|
||||||
{ id: "Things nearby HOTEL_NAME" },
|
{ id: "Things nearby HOTEL_NAME" },
|
||||||
{ hotelName }
|
{ hotelName }
|
||||||
@@ -95,6 +126,7 @@ export default function DynamicMap({
|
|||||||
/>
|
/>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
</div>
|
||||||
</APIProvider>
|
</APIProvider>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
24
components/ContentType/HotelPage/Map/MapWithCard/index.tsx
Normal file
24
components/ContentType/HotelPage/Map/MapWithCard/index.tsx
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import { PropsWithChildren, useRef } from "react"
|
||||||
|
|
||||||
|
import { StickyElementNameEnum } from "@/stores/sticky-position"
|
||||||
|
|
||||||
|
import useStickyPosition from "@/hooks/useStickyPosition"
|
||||||
|
|
||||||
|
import styles from "./mapWithCard.module.css"
|
||||||
|
|
||||||
|
export default function MapWithCard({ children }: PropsWithChildren) {
|
||||||
|
const mapWithCardRef = useRef<HTMLDivElement>(null)
|
||||||
|
useStickyPosition({
|
||||||
|
ref: mapWithCardRef,
|
||||||
|
name: StickyElementNameEnum.HOTEL_STATIC_MAP,
|
||||||
|
group: "hotelPage",
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div ref={mapWithCardRef} className={styles.mapWithCard}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
.mapWithCard {
|
||||||
|
position: sticky;
|
||||||
|
top: var(--booking-widget-desktop-height);
|
||||||
|
min-height: 500px; /* Fixed min to not cover the marker with the card */
|
||||||
|
height: calc(
|
||||||
|
100vh - var(--main-menu-desktop-height) -
|
||||||
|
var(--booking-widget-desktop-height)
|
||||||
|
); /* Full height without the header + booking widget */
|
||||||
|
max-height: 935px; /* Fixed max according to figma */
|
||||||
|
overflow: hidden;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
.mobileToggle {
|
.mobileToggle {
|
||||||
position: sticky;
|
position: sticky;
|
||||||
bottom: var(--Spacing-x5);
|
bottom: var(--Spacing-x5);
|
||||||
z-index: 1;
|
z-index: var(--hotel-mobile-map-toggle-button-z-index);
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(2, 1fr);
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
|||||||
@@ -21,9 +21,9 @@ export default async function PreviewImages({
|
|||||||
{images.slice(0, 3).map((image, index) => (
|
{images.slice(0, 3).map((image, index) => (
|
||||||
<Image
|
<Image
|
||||||
key={index}
|
key={index}
|
||||||
src={image.url}
|
src={image.imageSizes.medium}
|
||||||
alt={image.alt}
|
alt={image.metaData.altText}
|
||||||
title={image.title}
|
title={image.metaData.title}
|
||||||
width={index === 0 ? 752 : 292}
|
width={index === 0 ? 752 : 292}
|
||||||
height={index === 0 ? 540 : 266}
|
height={index === 0 ? 540 : 266}
|
||||||
className={styles.image}
|
className={styles.image}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
position: relative;
|
position: relative;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: var(--Spacing-x2) var(--Spacing-x2) 0;
|
padding: var(--Spacing-x2) var(--Spacing-x2) 0;
|
||||||
|
z-index: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.image {
|
.image {
|
||||||
|
|||||||
@@ -2,9 +2,9 @@
|
|||||||
|
|
||||||
import { useIntl } from "react-intl"
|
import { useIntl } from "react-intl"
|
||||||
|
|
||||||
import { ChevronRightIcon, ImageIcon } from "@/components/Icons"
|
import { ImageIcon } from "@/components/Icons"
|
||||||
import Image from "@/components/Image"
|
import Image from "@/components/Image"
|
||||||
import Button from "@/components/TempDesignSystem/Button"
|
import RoomSidePeek from "@/components/SidePeeks/RoomSidePeek"
|
||||||
import Body from "@/components/TempDesignSystem/Text/Body"
|
import Body from "@/components/TempDesignSystem/Text/Body"
|
||||||
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
|
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
|
||||||
|
|
||||||
@@ -12,26 +12,28 @@ import styles from "./roomCard.module.css"
|
|||||||
|
|
||||||
import type { RoomCardProps } from "@/types/components/hotelPage/room"
|
import type { RoomCardProps } from "@/types/components/hotelPage/room"
|
||||||
|
|
||||||
export function RoomCard({
|
export function RoomCard({ room }: RoomCardProps) {
|
||||||
badgeTextTransKey,
|
const { images, name, roomSize, occupancy, id } = room
|
||||||
id,
|
|
||||||
images,
|
|
||||||
subtitle,
|
|
||||||
title,
|
|
||||||
}: RoomCardProps) {
|
|
||||||
const intl = useIntl()
|
const intl = useIntl()
|
||||||
const mainImage = images[0]
|
const mainImage = images[0]
|
||||||
|
|
||||||
|
const size =
|
||||||
|
roomSize?.min === roomSize?.max
|
||||||
|
? `${roomSize.min} m²`
|
||||||
|
: `${roomSize.min} - ${roomSize.max} m²`
|
||||||
|
|
||||||
|
const personLabel = intl.formatMessage(
|
||||||
|
{ id: "hotelPages.rooms.roomCard.persons" },
|
||||||
|
{ totalOccupancy: occupancy.total }
|
||||||
|
)
|
||||||
|
|
||||||
|
const subtitle = `${size} (${personLabel})`
|
||||||
|
|
||||||
function handleImageClick() {
|
function handleImageClick() {
|
||||||
// TODO: Implement opening of a model with carousel
|
// TODO: Implement opening of a model with carousel
|
||||||
console.log("Image clicked: ", id)
|
console.log("Image clicked: ", id)
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleRoomCtaClick() {
|
|
||||||
// TODO: Implement opening side-peek component with room details
|
|
||||||
console.log("Room CTA clicked: ", id)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<article className={styles.roomCard}>
|
<article className={styles.roomCard}>
|
||||||
<button className={styles.imageWrapper} onClick={handleImageClick}>
|
<button className={styles.imageWrapper} onClick={handleImageClick}>
|
||||||
@@ -64,19 +66,11 @@ export function RoomCard({
|
|||||||
color="black"
|
color="black"
|
||||||
className={styles.title}
|
className={styles.title}
|
||||||
>
|
>
|
||||||
{title}
|
{name}
|
||||||
</Subtitle>
|
</Subtitle>
|
||||||
<Body color="grey">{subtitle}</Body>
|
<Body color="grey">{subtitle}</Body>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<RoomSidePeek room={room} buttonSize="medium" />
|
||||||
theme="base"
|
|
||||||
variant="icon"
|
|
||||||
intent="text"
|
|
||||||
onClick={handleRoomCtaClick}
|
|
||||||
>
|
|
||||||
{intl.formatMessage({ id: "See room details" })}
|
|
||||||
<ChevronRightIcon />
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -22,27 +22,6 @@ export function Rooms({ rooms }: RoomsProps) {
|
|||||||
|
|
||||||
const scrollRef = useRef<HTMLDivElement>(null)
|
const scrollRef = useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
const mappedRooms = rooms
|
|
||||||
.map((room) => {
|
|
||||||
const size = `${room.roomSize.min} - ${room.roomSize.max} m²`
|
|
||||||
const personLabel =
|
|
||||||
room.occupancy.total === 1
|
|
||||||
? intl.formatMessage({ id: "hotelPages.rooms.roomCard.person" })
|
|
||||||
: intl.formatMessage({ id: "hotelPages.rooms.roomCard.persons" })
|
|
||||||
|
|
||||||
const subtitle = `${size} (${room.occupancy.total} ${personLabel})`
|
|
||||||
|
|
||||||
return {
|
|
||||||
id: room.id,
|
|
||||||
images: room.images,
|
|
||||||
title: room.name,
|
|
||||||
subtitle: subtitle,
|
|
||||||
sortOrder: room.sortOrder,
|
|
||||||
popularChoice: null,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.sort((a, b) => a.sortOrder - b.sortOrder)
|
|
||||||
|
|
||||||
function handleShowMore() {
|
function handleShowMore() {
|
||||||
if (scrollRef.current && allRoomsVisible) {
|
if (scrollRef.current && allRoomsVisible) {
|
||||||
scrollRef.current.scrollIntoView({ behavior: "smooth" })
|
scrollRef.current.scrollIntoView({ behavior: "smooth" })
|
||||||
@@ -64,15 +43,9 @@ export function Rooms({ rooms }: RoomsProps) {
|
|||||||
<Grids.Stackable
|
<Grids.Stackable
|
||||||
className={`${styles.grid} ${allRoomsVisible ? styles.allVisible : ""}`}
|
className={`${styles.grid} ${allRoomsVisible ? styles.allVisible : ""}`}
|
||||||
>
|
>
|
||||||
{mappedRooms.map(({ id, images, title, subtitle, popularChoice }) => (
|
{rooms.map((room) => (
|
||||||
<div key={id}>
|
<div key={room.id}>
|
||||||
<RoomCard
|
<RoomCard room={room} />
|
||||||
id={id}
|
|
||||||
images={images}
|
|
||||||
title={title}
|
|
||||||
subtitle={subtitle}
|
|
||||||
badgeTextTransKey={popularChoice ? "Popular choice" : null}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</Grids.Stackable>
|
</Grids.Stackable>
|
||||||
|
|||||||
@@ -1,12 +1,15 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { useRouter } from "next/navigation"
|
import { useRouter } from "next/navigation"
|
||||||
import { useEffect } from "react"
|
import { useEffect, useRef } from "react"
|
||||||
import { useIntl } from "react-intl"
|
import { useIntl } from "react-intl"
|
||||||
|
|
||||||
|
import { StickyElementNameEnum } from "@/stores/sticky-position"
|
||||||
|
|
||||||
import Link from "@/components/TempDesignSystem/Link"
|
import Link from "@/components/TempDesignSystem/Link"
|
||||||
import useHash from "@/hooks/useHash"
|
import useHash from "@/hooks/useHash"
|
||||||
import useScrollSpy from "@/hooks/useScrollSpy"
|
import useScrollSpy from "@/hooks/useScrollSpy"
|
||||||
|
import useStickyPosition from "@/hooks/useStickyPosition"
|
||||||
|
|
||||||
import styles from "./tabNavigation.module.css"
|
import styles from "./tabNavigation.module.css"
|
||||||
|
|
||||||
@@ -23,6 +26,12 @@ export default function TabNavigation({
|
|||||||
const hash = useHash()
|
const hash = useHash()
|
||||||
const intl = useIntl()
|
const intl = useIntl()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
const tabNavigationRef = useRef<HTMLDivElement>(null)
|
||||||
|
useStickyPosition({
|
||||||
|
ref: tabNavigationRef,
|
||||||
|
name: StickyElementNameEnum.HOTEL_TAB_NAVIGATION,
|
||||||
|
group: "hotelPage",
|
||||||
|
})
|
||||||
|
|
||||||
const tabLinks: { hash: HotelHashValues; text: string }[] = [
|
const tabLinks: { hash: HotelHashValues; text: string }[] = [
|
||||||
{
|
{
|
||||||
@@ -71,7 +80,7 @@ export default function TabNavigation({
|
|||||||
}, [activeSectionId, router])
|
}, [activeSectionId, router])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.stickyWrapper}>
|
<div ref={tabNavigationRef} className={styles.stickyWrapper}>
|
||||||
<nav className={styles.tabsContainer}>
|
<nav className={styles.tabsContainer}>
|
||||||
{tabLinks.map((link) => {
|
{tabLinks.map((link) => {
|
||||||
const isActive =
|
const isActive =
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
.stickyWrapper {
|
.stickyWrapper {
|
||||||
position: sticky;
|
position: sticky;
|
||||||
top: var(--booking-widget-mobile-height);
|
top: var(--booking-widget-mobile-height);
|
||||||
z-index: 2;
|
z-index: var(--hotel-tab-navigation-z-index);
|
||||||
background-color: var(--Base-Surface-Subtle-Normal);
|
background-color: var(--Base-Surface-Subtle-Normal);
|
||||||
border-bottom: 1px solid var(--Base-Border-Subtle);
|
border-bottom: 1px solid var(--Base-Border-Subtle);
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
|
|||||||
@@ -1,4 +1,8 @@
|
|||||||
.pageContainer {
|
.pageContainer {
|
||||||
|
--hotel-tab-navigation-z-index: 2;
|
||||||
|
--hotel-mobile-map-toggle-button-z-index: 1;
|
||||||
|
--hotel-dynamic-map-z-index: 2;
|
||||||
|
|
||||||
--hotel-page-navigation-height: 59px;
|
--hotel-page-navigation-height: 59px;
|
||||||
--hotel-page-scroll-margin-top: calc(
|
--hotel-page-scroll-margin-top: calc(
|
||||||
var(--hotel-page-navigation-height) + var(--Spacing-x2)
|
var(--hotel-page-navigation-height) + var(--Spacing-x2)
|
||||||
@@ -69,19 +73,6 @@
|
|||||||
background-color: var(--Base-Surface-Primary-light-Normal);
|
background-color: var(--Base-Surface-Primary-light-Normal);
|
||||||
}
|
}
|
||||||
|
|
||||||
.mapWithCard {
|
|
||||||
position: sticky;
|
|
||||||
top: var(--booking-widget-desktop-height);
|
|
||||||
min-height: 500px; /* Fixed min to not cover the marker with the card */
|
|
||||||
height: calc(
|
|
||||||
100vh - var(--main-menu-desktop-height) -
|
|
||||||
var(--booking-widget-desktop-height)
|
|
||||||
); /* Full height without the header + booking widget */
|
|
||||||
max-height: 935px; /* Fixed max according to figma */
|
|
||||||
overflow: hidden;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pageContainer > nav {
|
.pageContainer > nav {
|
||||||
padding-left: var(--Spacing-x5);
|
padding-left: var(--Spacing-x5);
|
||||||
padding-right: var(--Spacing-x5);
|
padding-right: var(--Spacing-x5);
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { env } from "@/env/server"
|
|||||||
import { serverClient } from "@/lib/trpc/server"
|
import { serverClient } from "@/lib/trpc/server"
|
||||||
|
|
||||||
import AccordionSection from "@/components/Blocks/Accordion"
|
import AccordionSection from "@/components/Blocks/Accordion"
|
||||||
import SidePeekProvider from "@/components/SidePeekProvider"
|
import SidePeekProvider from "@/components/SidePeeks/SidePeekProvider"
|
||||||
import Alert from "@/components/TempDesignSystem/Alert"
|
import Alert from "@/components/TempDesignSystem/Alert"
|
||||||
import SidePeek from "@/components/TempDesignSystem/SidePeek"
|
import SidePeek from "@/components/TempDesignSystem/SidePeek"
|
||||||
import { getIntl } from "@/i18n"
|
import { getIntl } from "@/i18n"
|
||||||
@@ -12,6 +12,7 @@ import { getRestaurantHeading } from "@/utils/facilityCards"
|
|||||||
|
|
||||||
import DynamicMap from "./Map/DynamicMap"
|
import DynamicMap from "./Map/DynamicMap"
|
||||||
import MapCard from "./Map/MapCard"
|
import MapCard from "./Map/MapCard"
|
||||||
|
import MapWithCardWrapper from "./Map/MapWithCard"
|
||||||
import MobileMapToggle from "./Map/MobileMapToggle"
|
import MobileMapToggle from "./Map/MobileMapToggle"
|
||||||
import StaticMap from "./Map/StaticMap"
|
import StaticMap from "./Map/StaticMap"
|
||||||
import AmenitiesList from "./AmenitiesList"
|
import AmenitiesList from "./AmenitiesList"
|
||||||
@@ -63,12 +64,14 @@ export default async function HotelPage() {
|
|||||||
return (
|
return (
|
||||||
<div className={styles.pageContainer}>
|
<div className={styles.pageContainer}>
|
||||||
<div className={styles.hotelImages}>
|
<div className={styles.hotelImages}>
|
||||||
|
{hotelImages?.length && (
|
||||||
<PreviewImages images={hotelImages} hotelName={hotelName} />
|
<PreviewImages images={hotelImages} hotelName={hotelName} />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<TabNavigation
|
<TabNavigation
|
||||||
restaurantTitle={getRestaurantHeading(hotelDetailedFacilities)}
|
restaurantTitle={getRestaurantHeading(hotelDetailedFacilities)}
|
||||||
hasActivities={!!activitiesCard}
|
hasActivities={!!activitiesCard}
|
||||||
hasFAQ={!!faq}
|
hasFAQ={!!faq.accordions.length}
|
||||||
/>
|
/>
|
||||||
<main className={styles.mainSection}>
|
<main className={styles.mainSection}>
|
||||||
<div id={HotelHashValues.overview} className={styles.overview}>
|
<div id={HotelHashValues.overview} className={styles.overview}>
|
||||||
@@ -98,17 +101,17 @@ export default async function HotelPage() {
|
|||||||
</div>
|
</div>
|
||||||
<Rooms rooms={roomCategories} />
|
<Rooms rooms={roomCategories} />
|
||||||
<Facilities facilities={facilities} activitiesCard={activitiesCard} />
|
<Facilities facilities={facilities} activitiesCard={activitiesCard} />
|
||||||
{faq && (
|
{faq.accordions.length > 0 && (
|
||||||
<AccordionSection accordion={faq.accordions} title={faq.title} />
|
<AccordionSection accordion={faq.accordions} title={faq.title} />
|
||||||
)}
|
)}
|
||||||
</main>
|
</main>
|
||||||
{googleMapsApiKey ? (
|
{googleMapsApiKey ? (
|
||||||
<>
|
<>
|
||||||
<aside className={styles.mapContainer}>
|
<aside className={styles.mapContainer}>
|
||||||
<div className={styles.mapWithCard}>
|
<MapWithCardWrapper>
|
||||||
<StaticMap coordinates={coordinates} hotelName={hotelName} />
|
<StaticMap coordinates={coordinates} hotelName={hotelName} />
|
||||||
<MapCard hotelName={hotelName} pois={topThreePois} />
|
<MapCard hotelName={hotelName} pois={topThreePois} />
|
||||||
</div>
|
</MapWithCardWrapper>
|
||||||
</aside>
|
</aside>
|
||||||
<MobileMapToggle />
|
<MobileMapToggle />
|
||||||
<DynamicMap
|
<DynamicMap
|
||||||
|
|||||||
@@ -69,7 +69,13 @@ export default function DatePickerDesktop({
|
|||||||
weekStartsOn={1}
|
weekStartsOn={1}
|
||||||
components={{
|
components={{
|
||||||
Chevron(props) {
|
Chevron(props) {
|
||||||
return <ChevronLeftIcon {...props} height={20} width={20} />
|
return (
|
||||||
|
<ChevronLeftIcon
|
||||||
|
className={props.className}
|
||||||
|
height={20}
|
||||||
|
width={20}
|
||||||
|
/>
|
||||||
|
)
|
||||||
},
|
},
|
||||||
Footer(props) {
|
Footer(props) {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -32,10 +32,6 @@ export default async function FooterDetails() {
|
|||||||
<Link href={`/${lang}`}>
|
<Link href={`/${lang}`}>
|
||||||
<Image
|
<Image
|
||||||
alt="Scandic Hotels logo"
|
alt="Scandic Hotels logo"
|
||||||
className={styles.logo}
|
|
||||||
data-js="scandiclogoimg"
|
|
||||||
data-nosvgsrc="/_static/img/scandic-logotype.png"
|
|
||||||
itemProp="logo"
|
|
||||||
height={22}
|
height={22}
|
||||||
src="/_static/img/scandic-logotype-white.svg"
|
src="/_static/img/scandic-logotype-white.svg"
|
||||||
width={103}
|
width={103}
|
||||||
@@ -46,7 +42,6 @@ export default async function FooterDetails() {
|
|||||||
(link) =>
|
(link) =>
|
||||||
link.href && (
|
link.href && (
|
||||||
<a
|
<a
|
||||||
className={styles.socialLink}
|
|
||||||
color="white"
|
color="white"
|
||||||
href={link.href.href}
|
href={link.href.href}
|
||||||
key={link.href.title}
|
key={link.href.title}
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ export default function FooterSecondaryNav({
|
|||||||
<div className={styles.secondaryNavigation}>
|
<div className={styles.secondaryNavigation}>
|
||||||
{appDownloads && (
|
{appDownloads && (
|
||||||
<nav className={styles.secondaryNavigationGroup}>
|
<nav className={styles.secondaryNavigationGroup}>
|
||||||
<Body color="peach80" textTransform="uppercase">
|
<Body color="baseTextMediumContrast" textTransform="uppercase">
|
||||||
{appDownloads.title}
|
{appDownloads.title}
|
||||||
</Body>
|
</Body>
|
||||||
<ul className={styles.secondaryNavigationList}>
|
<ul className={styles.secondaryNavigationList}>
|
||||||
@@ -50,7 +50,7 @@ export default function FooterSecondaryNav({
|
|||||||
)}
|
)}
|
||||||
{secondaryLinks.map((link) => (
|
{secondaryLinks.map((link) => (
|
||||||
<nav className={styles.secondaryNavigationGroup} key={link.title}>
|
<nav className={styles.secondaryNavigationGroup} key={link.title}>
|
||||||
<Body color="peach80" textTransform="uppercase">
|
<Body color="baseTextMediumContrast" textTransform="uppercase">
|
||||||
{link.title}
|
{link.title}
|
||||||
</Body>
|
</Body>
|
||||||
<ul className={styles.secondaryNavigationList}>
|
<ul className={styles.secondaryNavigationList}>
|
||||||
|
|||||||
@@ -128,10 +128,12 @@ export default function Search({ locations }: SearchProps) {
|
|||||||
}) => (
|
}) => (
|
||||||
<div className={styles.container}>
|
<div className={styles.container}>
|
||||||
<label {...getLabelProps({ htmlFor: name })} className={styles.label}>
|
<label {...getLabelProps({ htmlFor: name })} className={styles.label}>
|
||||||
<Caption color={isOpen ? "uiTextActive" : "red"}>
|
<Caption color={isOpen ? "uiTextActive" : "red"} asChild>
|
||||||
|
<span>
|
||||||
{state.searchData?.type === "hotels"
|
{state.searchData?.type === "hotels"
|
||||||
? state.searchData?.relationships?.city?.name
|
? state.searchData?.relationships?.city?.name
|
||||||
: intl.formatMessage({ id: "Where to" })}
|
: intl.formatMessage({ id: "Where to" })}
|
||||||
|
</span>
|
||||||
</Caption>
|
</Caption>
|
||||||
</label>
|
</label>
|
||||||
<div {...getRootProps({}, { suppressRefError: true })}>
|
<div {...getRootProps({}, { suppressRefError: true })}>
|
||||||
|
|||||||
@@ -34,8 +34,8 @@ export default function Voucher() {
|
|||||||
>
|
>
|
||||||
<div className={styles.vouchers}>
|
<div className={styles.vouchers}>
|
||||||
<label>
|
<label>
|
||||||
<Caption color="disabled" type="bold">
|
<Caption color="disabled" type="bold" asChild>
|
||||||
{vouchers}
|
<span>{vouchers}</span>
|
||||||
</Caption>
|
</Caption>
|
||||||
{/* <InfoCircleIcon color="white" className={styles.infoIcon} /> Out of scope for this release */}
|
{/* <InfoCircleIcon color="white" className={styles.infoIcon} /> Out of scope for this release */}
|
||||||
</label>
|
</label>
|
||||||
@@ -49,21 +49,30 @@ export default function Voucher() {
|
|||||||
arrow="left"
|
arrow="left"
|
||||||
>
|
>
|
||||||
<div className={styles.options}>
|
<div className={styles.options}>
|
||||||
<label className={`${styles.option} ${styles.checkboxVoucher}`}>
|
<div className={`${styles.option} ${styles.checkboxVoucher}`}>
|
||||||
<Checkbox name="useVouchers" registerOptions={{ disabled: true }} />
|
<Checkbox name="useVouchers" registerOptions={{ disabled: true }}>
|
||||||
<Caption color="disabled">{useVouchers}</Caption>
|
<Caption color="disabled" asChild>
|
||||||
|
<span>{useVouchers}</span>
|
||||||
|
</Caption>
|
||||||
|
</Checkbox>
|
||||||
{/* <InfoCircleIcon color="white" className={styles.infoIcon} /> Out of scope for this release */}
|
{/* <InfoCircleIcon color="white" className={styles.infoIcon} /> Out of scope for this release */}
|
||||||
</label>
|
</div>
|
||||||
<label className={styles.option}>
|
<div className={styles.option}>
|
||||||
<Checkbox name="useBonus" registerOptions={{ disabled: true }} />
|
<Checkbox name="useBonus" registerOptions={{ disabled: true }}>
|
||||||
<Caption color="disabled">{bonus}</Caption>
|
<Caption color="disabled" asChild>
|
||||||
|
<span>{bonus}</span>
|
||||||
|
</Caption>
|
||||||
|
</Checkbox>
|
||||||
{/* <InfoCircleIcon color="white" className={styles.infoIcon} /> Out of scope for this release */}
|
{/* <InfoCircleIcon color="white" className={styles.infoIcon} /> Out of scope for this release */}
|
||||||
</label>
|
</div>
|
||||||
<label className={styles.option}>
|
<div className={styles.option}>
|
||||||
<Checkbox name="useReward" registerOptions={{ disabled: true }} />
|
<Checkbox name="useReward" registerOptions={{ disabled: true }}>
|
||||||
<Caption color="disabled">{reward}</Caption>
|
<Caption color="disabled" asChild>
|
||||||
|
<span>{reward}</span>
|
||||||
|
</Caption>
|
||||||
|
</Checkbox>
|
||||||
{/* <InfoCircleIcon color="white" className={styles.infoIcon} /> Out of scope for this release */}
|
{/* <InfoCircleIcon color="white" className={styles.infoIcon} /> Out of scope for this release */}
|
||||||
</label>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { dt } from "@/lib/dt"
|
|||||||
|
|
||||||
import DatePicker from "@/components/DatePicker"
|
import DatePicker from "@/components/DatePicker"
|
||||||
import GuestsRoomsPickerForm from "@/components/GuestsRoomsPicker"
|
import GuestsRoomsPickerForm from "@/components/GuestsRoomsPicker"
|
||||||
|
import GuestsRoomsProvider from "@/components/GuestsRoomsPicker/Provider/GuestsRoomsProvider"
|
||||||
import { SearchIcon } from "@/components/Icons"
|
import { SearchIcon } from "@/components/Icons"
|
||||||
import Button from "@/components/TempDesignSystem/Button"
|
import Button from "@/components/TempDesignSystem/Button"
|
||||||
import Caption from "@/components/TempDesignSystem/Text/Caption"
|
import Caption from "@/components/TempDesignSystem/Text/Caption"
|
||||||
@@ -29,6 +30,8 @@ export default function FormContent({
|
|||||||
|
|
||||||
const nights = dt(selectedDate.toDate).diff(dt(selectedDate.fromDate), "days")
|
const nights = dt(selectedDate.toDate).diff(dt(selectedDate.fromDate), "days")
|
||||||
|
|
||||||
|
const selectedGuests = useWatch({ name: "rooms" })
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className={styles.input}>
|
<div className={styles.input}>
|
||||||
@@ -47,11 +50,13 @@ export default function FormContent({
|
|||||||
</div>
|
</div>
|
||||||
<div className={styles.rooms}>
|
<div className={styles.rooms}>
|
||||||
<label>
|
<label>
|
||||||
<Caption color="red" type="bold">
|
<Caption color="red" type="bold" asChild>
|
||||||
{rooms}
|
<span>{rooms}</span>
|
||||||
</Caption>
|
</Caption>
|
||||||
</label>
|
</label>
|
||||||
<GuestsRoomsPickerForm />
|
<GuestsRoomsProvider selectedGuests={selectedGuests}>
|
||||||
|
<GuestsRoomsPickerForm name="rooms" />
|
||||||
|
</GuestsRoomsProvider>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.voucherContainer}>
|
<div className={styles.voucherContainer}>
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ export default function Form({ locations, type }: BookingWidgetFormProps) {
|
|||||||
data.rooms.forEach((room, index) => {
|
data.rooms.forEach((room, index) => {
|
||||||
bookingWidgetParams.set(`room[${index}].adults`, room.adults.toString())
|
bookingWidgetParams.set(`room[${index}].adults`, room.adults.toString())
|
||||||
|
|
||||||
room.children.forEach((child, childIndex) => {
|
room.child.forEach((child, childIndex) => {
|
||||||
bookingWidgetParams.set(
|
bookingWidgetParams.set(
|
||||||
`room[${index}].child[${childIndex}].age`,
|
`room[${index}].child[${childIndex}].age`,
|
||||||
child.age.toString()
|
child.age.toString()
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import type { Location } from "@/types/trpc/routers/hotel/locations"
|
|||||||
|
|
||||||
export const guestRoomSchema = z.object({
|
export const guestRoomSchema = z.object({
|
||||||
adults: z.number().default(1),
|
adults: z.number().default(1),
|
||||||
children: z.array(
|
child: z.array(
|
||||||
z.object({
|
z.object({
|
||||||
age: z.number().nonnegative(),
|
age: z.number().nonnegative(),
|
||||||
bed: z.number(),
|
bed: z.number(),
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ export default function AdultSelector({ roomIndex = 0 }: AdultSelectorProps) {
|
|||||||
const intl = useIntl()
|
const intl = useIntl()
|
||||||
const adultsLabel = intl.formatMessage({ id: "Adults" })
|
const adultsLabel = intl.formatMessage({ id: "Adults" })
|
||||||
const { setValue } = useFormContext()
|
const { setValue } = useFormContext()
|
||||||
const { adults, children, childrenInAdultsBed } = useGuestsRoomsStore(
|
const { adults, child, childrenInAdultsBed } = useGuestsRoomsStore(
|
||||||
(state) => state.rooms[roomIndex]
|
(state) => state.rooms[roomIndex]
|
||||||
)
|
)
|
||||||
const increaseAdults = useGuestsRoomsStore((state) => state.increaseAdults)
|
const increaseAdults = useGuestsRoomsStore((state) => state.increaseAdults)
|
||||||
@@ -39,13 +39,13 @@ export default function AdultSelector({ roomIndex = 0 }: AdultSelectorProps) {
|
|||||||
decreaseAdults(roomIndex)
|
decreaseAdults(roomIndex)
|
||||||
setValue(`rooms.${roomIndex}.adults`, adults - 1)
|
setValue(`rooms.${roomIndex}.adults`, adults - 1)
|
||||||
if (childrenInAdultsBed > adults) {
|
if (childrenInAdultsBed > adults) {
|
||||||
const toUpdateIndex = children.findIndex(
|
const toUpdateIndex = child.findIndex(
|
||||||
(child: Child) => child.bed == BedTypeEnum.IN_ADULTS_BED
|
(child: Child) => child.bed == BedTypeEnum.IN_ADULTS_BED
|
||||||
)
|
)
|
||||||
if (toUpdateIndex != -1) {
|
if (toUpdateIndex != -1) {
|
||||||
setValue(
|
setValue(
|
||||||
`rooms.${roomIndex}.children.${toUpdateIndex}.bed`,
|
`rooms.${roomIndex}.children.${toUpdateIndex}.bed`,
|
||||||
children[toUpdateIndex].age < 3
|
child[toUpdateIndex].age < 3
|
||||||
? BedTypeEnum.IN_CRIB
|
? BedTypeEnum.IN_CRIB
|
||||||
: BedTypeEnum.IN_EXTRA_BED
|
: BedTypeEnum.IN_EXTRA_BED
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ export default function ChildInfoSelector({
|
|||||||
const ageLabel = intl.formatMessage({ id: "Age" })
|
const ageLabel = intl.formatMessage({ id: "Age" })
|
||||||
const ageReqdErrMsg = intl.formatMessage({ id: "Child age is required" })
|
const ageReqdErrMsg = intl.formatMessage({ id: "Child age is required" })
|
||||||
const bedLabel = intl.formatMessage({ id: "Bed" })
|
const bedLabel = intl.formatMessage({ id: "Bed" })
|
||||||
const { setValue, trigger } = useFormContext()
|
const { setValue } = useFormContext()
|
||||||
const { adults, childrenInAdultsBed } = useGuestsRoomsStore(
|
const { adults, childrenInAdultsBed } = useGuestsRoomsStore(
|
||||||
(state) => state.rooms[roomIndex]
|
(state) => state.rooms[roomIndex]
|
||||||
)
|
)
|
||||||
@@ -51,10 +51,11 @@ export default function ChildInfoSelector({
|
|||||||
|
|
||||||
function updateSelectedAge(age: number) {
|
function updateSelectedAge(age: number) {
|
||||||
updateChildAge(age, roomIndex, index)
|
updateChildAge(age, roomIndex, index)
|
||||||
setValue(`rooms.${roomIndex}.children.${index}.age`, age)
|
setValue(`rooms.${roomIndex}.child.${index}.age`, age, {
|
||||||
|
shouldValidate: true,
|
||||||
|
})
|
||||||
const availableBedTypes = getAvailableBeds(age)
|
const availableBedTypes = getAvailableBeds(age)
|
||||||
updateSelectedBed(availableBedTypes[0].value)
|
updateSelectedBed(availableBedTypes[0].value)
|
||||||
trigger("rooms")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateSelectedBed(bed: number) {
|
function updateSelectedBed(bed: number) {
|
||||||
@@ -64,7 +65,7 @@ export default function ChildInfoSelector({
|
|||||||
decreaseChildInAdultsBed(roomIndex)
|
decreaseChildInAdultsBed(roomIndex)
|
||||||
}
|
}
|
||||||
updateChildBed(bed, roomIndex, index)
|
updateChildBed(bed, roomIndex, index)
|
||||||
setValue(`rooms.${roomIndex}.children.${index}.bed`, bed)
|
setValue(`rooms.${roomIndex}.child.${index}.bed`, bed)
|
||||||
}
|
}
|
||||||
|
|
||||||
const allBedTypes: ChildBed[] = [
|
const allBedTypes: ChildBed[] = [
|
||||||
@@ -109,8 +110,9 @@ export default function ChildInfoSelector({
|
|||||||
onSelect={(key) => {
|
onSelect={(key) => {
|
||||||
updateSelectedAge(key as number)
|
updateSelectedAge(key as number)
|
||||||
}}
|
}}
|
||||||
name={`rooms.${roomIndex}.children.${index}.age`}
|
name={`rooms.${roomIndex}.child.${index}.age`}
|
||||||
placeholder={ageLabel}
|
placeholder={ageLabel}
|
||||||
|
maxHeight={150}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
@@ -123,7 +125,7 @@ export default function ChildInfoSelector({
|
|||||||
onSelect={(key) => {
|
onSelect={(key) => {
|
||||||
updateSelectedBed(key as number)
|
updateSelectedBed(key as number)
|
||||||
}}
|
}}
|
||||||
name={`rooms.${roomIndex}.children.${index}.age`}
|
name={`rooms.${roomIndex}.child.${index}.age`}
|
||||||
placeholder={bedLabel}
|
placeholder={bedLabel}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
|
|||||||
@@ -19,9 +19,7 @@ export default function ChildSelector({ roomIndex = 0 }: ChildSelectorProps) {
|
|||||||
const intl = useIntl()
|
const intl = useIntl()
|
||||||
const childrenLabel = intl.formatMessage({ id: "Children" })
|
const childrenLabel = intl.formatMessage({ id: "Children" })
|
||||||
const { setValue, trigger } = useFormContext<BookingWidgetSchema>()
|
const { setValue, trigger } = useFormContext<BookingWidgetSchema>()
|
||||||
const children = useGuestsRoomsStore(
|
const children = useGuestsRoomsStore((state) => state.rooms[roomIndex].child)
|
||||||
(state) => state.rooms[roomIndex].children
|
|
||||||
)
|
|
||||||
const increaseChildren = useGuestsRoomsStore(
|
const increaseChildren = useGuestsRoomsStore(
|
||||||
(state) => state.increaseChildren
|
(state) => state.increaseChildren
|
||||||
)
|
)
|
||||||
@@ -32,18 +30,22 @@ export default function ChildSelector({ roomIndex = 0 }: ChildSelectorProps) {
|
|||||||
function increaseChildrenCount(roomIndex: number) {
|
function increaseChildrenCount(roomIndex: number) {
|
||||||
if (children.length < 5) {
|
if (children.length < 5) {
|
||||||
increaseChildren(roomIndex)
|
increaseChildren(roomIndex)
|
||||||
setValue(`rooms.${roomIndex}.children.${children.length}`, {
|
setValue(
|
||||||
|
`rooms.${roomIndex}.child.${children.length}`,
|
||||||
|
{
|
||||||
age: -1,
|
age: -1,
|
||||||
bed: -1,
|
bed: -1,
|
||||||
})
|
},
|
||||||
trigger("rooms")
|
{ shouldValidate: true }
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
function decreaseChildrenCount(roomIndex: number) {
|
function decreaseChildrenCount(roomIndex: number) {
|
||||||
if (children.length > 0) {
|
if (children.length > 0) {
|
||||||
const newChildrenList = decreaseChildren(roomIndex)
|
const newChildrenList = decreaseChildren(roomIndex)
|
||||||
setValue(`rooms.${roomIndex}.children`, newChildrenList)
|
setValue(`rooms.${roomIndex}.child`, newChildrenList, {
|
||||||
trigger("rooms")
|
shouldValidate: true,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
"use client"
|
||||||
|
import { PropsWithChildren, useRef } from "react"
|
||||||
|
|
||||||
|
import {
|
||||||
|
GuestsRoomsContext,
|
||||||
|
type GuestsRoomsStore,
|
||||||
|
initGuestsRoomsState,
|
||||||
|
} from "@/stores/guests-rooms"
|
||||||
|
|
||||||
|
import { GuestsRoom } from "@/types/components/bookingWidget/guestsRoomsPicker"
|
||||||
|
|
||||||
|
export default function GuestsRoomsProvider({
|
||||||
|
selectedGuests,
|
||||||
|
children,
|
||||||
|
}: PropsWithChildren<{ selectedGuests?: GuestsRoom[] }>) {
|
||||||
|
const initialStore = useRef<GuestsRoomsStore>()
|
||||||
|
if (!initialStore.current) {
|
||||||
|
initialStore.current = initGuestsRoomsState(selectedGuests)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<GuestsRoomsContext.Provider value={initialStore.current}>
|
||||||
|
{children}
|
||||||
|
</GuestsRoomsContext.Provider>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { useCallback, useEffect, useRef, useState } from "react"
|
import { useCallback, useEffect, useRef, useState } from "react"
|
||||||
|
import { useFormContext } from "react-hook-form"
|
||||||
import { useIntl } from "react-intl"
|
import { useIntl } from "react-intl"
|
||||||
|
|
||||||
import { useGuestsRoomsStore } from "@/stores/guests-rooms"
|
import { useGuestsRoomsStore } from "@/stores/guests-rooms"
|
||||||
@@ -12,9 +13,14 @@ import GuestsRoomsPicker from "./GuestsRoomsPicker"
|
|||||||
|
|
||||||
import styles from "./guests-rooms-picker.module.css"
|
import styles from "./guests-rooms-picker.module.css"
|
||||||
|
|
||||||
export default function GuestsRoomsPickerForm() {
|
export default function GuestsRoomsPickerForm({
|
||||||
|
name = "rooms",
|
||||||
|
}: {
|
||||||
|
name: string
|
||||||
|
}) {
|
||||||
const intl = useIntl()
|
const intl = useIntl()
|
||||||
const [isOpen, setIsOpen] = useState(false)
|
const [isOpen, setIsOpen] = useState(false)
|
||||||
|
const { setValue } = useFormContext()
|
||||||
const { rooms, adultCount, childCount, setIsValidated } = useGuestsRoomsStore(
|
const { rooms, adultCount, childCount, setIsValidated } = useGuestsRoomsStore(
|
||||||
(state) => ({
|
(state) => ({
|
||||||
rooms: state.rooms,
|
rooms: state.rooms,
|
||||||
@@ -32,10 +38,11 @@ export default function GuestsRoomsPickerForm() {
|
|||||||
if (guestRoomsValidData.success) {
|
if (guestRoomsValidData.success) {
|
||||||
setIsOpen(false)
|
setIsOpen(false)
|
||||||
setIsValidated(false)
|
setIsValidated(false)
|
||||||
|
setValue(name, guestRoomsValidData.data, { shouldValidate: true })
|
||||||
} else {
|
} else {
|
||||||
setIsValidated(true)
|
setIsValidated(true)
|
||||||
}
|
}
|
||||||
}, [rooms, setIsValidated, setIsOpen])
|
}, [rooms, name, setValue, setIsValidated, setIsOpen])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
function handleClickOutside(evt: Event) {
|
function handleClickOutside(evt: Event) {
|
||||||
|
|||||||
@@ -23,9 +23,6 @@ export default async function MainMenu() {
|
|||||||
<Image
|
<Image
|
||||||
alt={intl.formatMessage({ id: "Back to scandichotels.com" })}
|
alt={intl.formatMessage({ id: "Back to scandichotels.com" })}
|
||||||
className={styles.logo}
|
className={styles.logo}
|
||||||
data-js="scandiclogoimg"
|
|
||||||
data-nosvgsrc="/_static/img/scandic-logotype.png"
|
|
||||||
itemProp="logo"
|
|
||||||
height={22}
|
height={22}
|
||||||
src="/_static/img/scandic-logotype.svg"
|
src="/_static/img/scandic-logotype.svg"
|
||||||
width={103}
|
width={103}
|
||||||
|
|||||||
@@ -14,12 +14,14 @@ import { bedTypeSchema } from "./schema"
|
|||||||
|
|
||||||
import styles from "./bedOptions.module.css"
|
import styles from "./bedOptions.module.css"
|
||||||
|
|
||||||
import type { BedTypeSchema } from "@/types/components/enterDetails/bedType"
|
import type {
|
||||||
import { bedTypeEnum } from "@/types/enums/bedType"
|
BedTypeProps,
|
||||||
|
BedTypeSchema,
|
||||||
|
} from "@/types/components/hotelReservation/enterDetails/bedType"
|
||||||
|
|
||||||
export default function BedType() {
|
export default function BedType({ bedTypes }: BedTypeProps) {
|
||||||
const intl = useIntl()
|
const intl = useIntl()
|
||||||
const bedType = useEnterDetailsStore((state) => state.data.bedType)
|
const bedType = useEnterDetailsStore((state) => state.userData.bedType)
|
||||||
|
|
||||||
const methods = useForm<BedTypeSchema>({
|
const methods = useForm<BedTypeSchema>({
|
||||||
defaultValues: bedType
|
defaultValues: bedType
|
||||||
@@ -33,10 +35,6 @@ export default function BedType() {
|
|||||||
reValidateMode: "onChange",
|
reValidateMode: "onChange",
|
||||||
})
|
})
|
||||||
|
|
||||||
const text = intl.formatMessage<React.ReactNode>(
|
|
||||||
{ id: "<b>Included</b> (based on availability)" },
|
|
||||||
{ b: (str) => <b>{str}</b> }
|
|
||||||
)
|
|
||||||
const completeStep = useEnterDetailsStore((state) => state.completeStep)
|
const completeStep = useEnterDetailsStore((state) => state.completeStep)
|
||||||
|
|
||||||
const onSubmit = useCallback(
|
const onSubmit = useCallback(
|
||||||
@@ -58,38 +56,24 @@ export default function BedType() {
|
|||||||
return (
|
return (
|
||||||
<FormProvider {...methods}>
|
<FormProvider {...methods}>
|
||||||
<form className={styles.form} onSubmit={methods.handleSubmit(onSubmit)}>
|
<form className={styles.form} onSubmit={methods.handleSubmit(onSubmit)}>
|
||||||
|
{bedTypes.map((roomType) => {
|
||||||
|
const width =
|
||||||
|
roomType.size.max === roomType.size.min
|
||||||
|
? `${roomType.size.min} cm`
|
||||||
|
: `${roomType.size.min} cm - ${roomType.size.max} cm`
|
||||||
|
return (
|
||||||
<RadioCard
|
<RadioCard
|
||||||
|
key={roomType.value}
|
||||||
Icon={KingBedIcon}
|
Icon={KingBedIcon}
|
||||||
iconWidth={46}
|
iconWidth={46}
|
||||||
id={bedTypeEnum.KING}
|
id={roomType.value}
|
||||||
name="bedType"
|
name="bedType"
|
||||||
subtitle={intl.formatMessage(
|
subtitle={width}
|
||||||
{ id: "{width} cm × {length} cm" },
|
title={roomType.description}
|
||||||
{
|
value={roomType.description}
|
||||||
length: "210",
|
|
||||||
width: "180",
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
text={text}
|
|
||||||
title={intl.formatMessage({ id: "King bed" })}
|
|
||||||
value={bedTypeEnum.KING}
|
|
||||||
/>
|
|
||||||
<RadioCard
|
|
||||||
Icon={KingBedIcon}
|
|
||||||
iconWidth={46}
|
|
||||||
id={bedTypeEnum.QUEEN}
|
|
||||||
name="bedType"
|
|
||||||
subtitle={intl.formatMessage(
|
|
||||||
{ id: "{width} cm × {length} cm" },
|
|
||||||
{
|
|
||||||
length: "200",
|
|
||||||
width: "160",
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
text={text}
|
|
||||||
title={intl.formatMessage({ id: "Queen bed" })}
|
|
||||||
value={bedTypeEnum.QUEEN}
|
|
||||||
/>
|
/>
|
||||||
|
)
|
||||||
|
})}
|
||||||
</form>
|
</form>
|
||||||
</FormProvider>
|
</FormProvider>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { z } from "zod"
|
import { z } from "zod"
|
||||||
|
|
||||||
import { bedTypeEnum } from "@/types/enums/bedType"
|
import { BedTypeEnum } from "@/types/enums/bedType"
|
||||||
|
|
||||||
export const bedTypeSchema = z.object({
|
export const bedTypeSchema = z.object({
|
||||||
bedType: z.nativeEnum(bedTypeEnum),
|
bedType: z.string(),
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -7,36 +7,50 @@ import { useIntl } from "react-intl"
|
|||||||
|
|
||||||
import { useEnterDetailsStore } from "@/stores/enter-details"
|
import { useEnterDetailsStore } from "@/stores/enter-details"
|
||||||
|
|
||||||
import { BreakfastIcon, NoBreakfastIcon } from "@/components/Icons"
|
import { Highlight } from "@/components/TempDesignSystem/Form/ChoiceCard/_Card"
|
||||||
import RadioCard from "@/components/TempDesignSystem/Form/ChoiceCard/Radio"
|
import RadioCard from "@/components/TempDesignSystem/Form/ChoiceCard/Radio"
|
||||||
|
|
||||||
import { breakfastSchema } from "./schema"
|
import { breakfastFormSchema } from "./schema"
|
||||||
|
|
||||||
import styles from "./breakfast.module.css"
|
import styles from "./breakfast.module.css"
|
||||||
|
|
||||||
import type { BreakfastSchema } from "@/types/components/enterDetails/breakfast"
|
import type {
|
||||||
import { breakfastEnum } from "@/types/enums/breakfast"
|
BreakfastFormSchema,
|
||||||
|
BreakfastProps,
|
||||||
|
} from "@/types/components/hotelReservation/enterDetails/breakfast"
|
||||||
|
import { BreakfastPackageEnum } from "@/types/enums/breakfast"
|
||||||
|
|
||||||
export default function Breakfast() {
|
export default function Breakfast({ packages }: BreakfastProps) {
|
||||||
const intl = useIntl()
|
const intl = useIntl()
|
||||||
|
|
||||||
const breakfast = useEnterDetailsStore((state) => state.data.breakfast)
|
const breakfast = useEnterDetailsStore((state) => state.userData.breakfast)
|
||||||
|
|
||||||
const methods = useForm<BreakfastSchema>({
|
let defaultValues = undefined
|
||||||
defaultValues: breakfast ? { breakfast } : undefined,
|
if (breakfast === BreakfastPackageEnum.NO_BREAKFAST) {
|
||||||
|
defaultValues = { breakfast: BreakfastPackageEnum.NO_BREAKFAST }
|
||||||
|
} else if (breakfast?.code) {
|
||||||
|
defaultValues = { breakfast: breakfast.code }
|
||||||
|
}
|
||||||
|
const methods = useForm<BreakfastFormSchema>({
|
||||||
|
defaultValues,
|
||||||
criteriaMode: "all",
|
criteriaMode: "all",
|
||||||
mode: "all",
|
mode: "all",
|
||||||
resolver: zodResolver(breakfastSchema),
|
resolver: zodResolver(breakfastFormSchema),
|
||||||
reValidateMode: "onChange",
|
reValidateMode: "onChange",
|
||||||
})
|
})
|
||||||
|
|
||||||
const completeStep = useEnterDetailsStore((state) => state.completeStep)
|
const completeStep = useEnterDetailsStore((state) => state.completeStep)
|
||||||
|
|
||||||
const onSubmit = useCallback(
|
const onSubmit = useCallback(
|
||||||
(values: BreakfastSchema) => {
|
(values: BreakfastFormSchema) => {
|
||||||
completeStep(values)
|
const pkg = packages?.find((p) => p.code === values.breakfast)
|
||||||
|
if (pkg) {
|
||||||
|
completeStep({ breakfast: pkg })
|
||||||
|
} else {
|
||||||
|
completeStep({ breakfast: BreakfastPackageEnum.NO_BREAKFAST })
|
||||||
|
}
|
||||||
},
|
},
|
||||||
[completeStep]
|
[completeStep, packages]
|
||||||
)
|
)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -47,30 +61,46 @@ export default function Breakfast() {
|
|||||||
return () => subscription.unsubscribe()
|
return () => subscription.unsubscribe()
|
||||||
}, [methods, onSubmit])
|
}, [methods, onSubmit])
|
||||||
|
|
||||||
|
if (!packages) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FormProvider {...methods}>
|
<FormProvider {...methods}>
|
||||||
<form className={styles.form} onSubmit={methods.handleSubmit(onSubmit)}>
|
<form className={styles.form} onSubmit={methods.handleSubmit(onSubmit)}>
|
||||||
|
{packages.map((pkg) => (
|
||||||
<RadioCard
|
<RadioCard
|
||||||
Icon={BreakfastIcon}
|
key={pkg.code}
|
||||||
id={breakfastEnum.BREAKFAST}
|
id={pkg.code}
|
||||||
name="breakfast"
|
name="breakfast"
|
||||||
subtitle={intl.formatMessage<React.ReactNode>(
|
subtitle={
|
||||||
{ id: "<b>{amount} {currency}</b>/night per adult" },
|
pkg.code === BreakfastPackageEnum.FREE_MEMBER_BREAKFAST
|
||||||
|
? intl.formatMessage<React.ReactNode>(
|
||||||
|
{ id: "breakfast.price.free" },
|
||||||
{
|
{
|
||||||
amount: "150",
|
amount: pkg.originalPrice,
|
||||||
b: (str) => <b>{str}</b>,
|
currency: pkg.currency,
|
||||||
currency: "SEK",
|
free: (str) => <Highlight>{str}</Highlight>,
|
||||||
|
strikethrough: (str) => <s>{str}</s>,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
: intl.formatMessage(
|
||||||
|
{ id: "breakfast.price" },
|
||||||
|
{
|
||||||
|
amount: pkg.packagePrice,
|
||||||
|
currency: pkg.currency,
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
)}
|
|
||||||
text={intl.formatMessage({
|
text={intl.formatMessage({
|
||||||
id: "All our breakfast buffets offer gluten free, vegan, and allergy-friendly options.",
|
id: "All our breakfast buffets offer gluten free, vegan, and allergy-friendly options.",
|
||||||
})}
|
})}
|
||||||
title={intl.formatMessage({ id: "Breakfast buffet" })}
|
title={intl.formatMessage({ id: "Breakfast buffet" })}
|
||||||
value={breakfastEnum.BREAKFAST}
|
value={pkg.code}
|
||||||
/>
|
/>
|
||||||
|
))}
|
||||||
<RadioCard
|
<RadioCard
|
||||||
Icon={NoBreakfastIcon}
|
id={BreakfastPackageEnum.NO_BREAKFAST}
|
||||||
id={breakfastEnum.NO_BREAKFAST}
|
|
||||||
name="breakfast"
|
name="breakfast"
|
||||||
subtitle={intl.formatMessage(
|
subtitle={intl.formatMessage(
|
||||||
{ id: "{amount} {currency}" },
|
{ id: "{amount} {currency}" },
|
||||||
@@ -83,7 +113,7 @@ export default function Breakfast() {
|
|||||||
id: "You can always change your mind later and add breakfast at the hotel.",
|
id: "You can always change your mind later and add breakfast at the hotel.",
|
||||||
})}
|
})}
|
||||||
title={intl.formatMessage({ id: "No breakfast" })}
|
title={intl.formatMessage({ id: "No breakfast" })}
|
||||||
value={breakfastEnum.NO_BREAKFAST}
|
value={BreakfastPackageEnum.NO_BREAKFAST}
|
||||||
/>
|
/>
|
||||||
</form>
|
</form>
|
||||||
</FormProvider>
|
</FormProvider>
|
||||||
|
|||||||
@@ -1,7 +1,15 @@
|
|||||||
import { z } from "zod"
|
import { z } from "zod"
|
||||||
|
|
||||||
import { breakfastEnum } from "@/types/enums/breakfast"
|
import { breakfastPackageSchema } from "@/server/routers/hotels/output"
|
||||||
|
|
||||||
export const breakfastSchema = z.object({
|
import { BreakfastPackageEnum } from "@/types/enums/breakfast"
|
||||||
breakfast: z.nativeEnum(breakfastEnum),
|
|
||||||
|
export const breakfastStoreSchema = z.object({
|
||||||
|
breakfast: breakfastPackageSchema.or(
|
||||||
|
z.literal(BreakfastPackageEnum.NO_BREAKFAST)
|
||||||
|
),
|
||||||
|
})
|
||||||
|
|
||||||
|
export const breakfastFormSchema = z.object({
|
||||||
|
breakfast: z.string().or(z.literal(BreakfastPackageEnum.NO_BREAKFAST)),
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ import CountrySelect from "@/components/TempDesignSystem/Form/Country"
|
|||||||
import Input from "@/components/TempDesignSystem/Form/Input"
|
import Input from "@/components/TempDesignSystem/Form/Input"
|
||||||
import Phone from "@/components/TempDesignSystem/Form/Phone"
|
import Phone from "@/components/TempDesignSystem/Form/Phone"
|
||||||
import Body from "@/components/TempDesignSystem/Text/Body"
|
import Body from "@/components/TempDesignSystem/Text/Body"
|
||||||
import { toast } from "@/components/TempDesignSystem/Toasts"
|
|
||||||
|
|
||||||
import { detailsSchema, signedInDetailsSchema } from "./schema"
|
import { detailsSchema, signedInDetailsSchema } from "./schema"
|
||||||
import Signup from "./Signup"
|
import Signup from "./Signup"
|
||||||
@@ -22,21 +21,21 @@ import styles from "./details.module.css"
|
|||||||
import type {
|
import type {
|
||||||
DetailsProps,
|
DetailsProps,
|
||||||
DetailsSchema,
|
DetailsSchema,
|
||||||
} from "@/types/components/enterDetails/details"
|
} from "@/types/components/hotelReservation/enterDetails/details"
|
||||||
|
|
||||||
const formID = "enter-details"
|
const formID = "enter-details"
|
||||||
export default function Details({ user }: DetailsProps) {
|
export default function Details({ user }: DetailsProps) {
|
||||||
const intl = useIntl()
|
const intl = useIntl()
|
||||||
const initialData = useEnterDetailsStore((state) => ({
|
const initialData = useEnterDetailsStore((state) => ({
|
||||||
countryCode: state.data.countryCode,
|
countryCode: state.userData.countryCode,
|
||||||
email: state.data.email,
|
email: state.userData.email,
|
||||||
firstName: state.data.firstName,
|
firstName: state.userData.firstName,
|
||||||
lastName: state.data.lastName,
|
lastName: state.userData.lastName,
|
||||||
phoneNumber: state.data.phoneNumber,
|
phoneNumber: state.userData.phoneNumber,
|
||||||
join: state.data.join,
|
join: state.userData.join,
|
||||||
dateOfBirth: state.data.dateOfBirth,
|
dateOfBirth: state.userData.dateOfBirth,
|
||||||
zipCode: state.data.zipCode,
|
zipCode: state.userData.zipCode,
|
||||||
termsAccepted: state.data.termsAccepted,
|
termsAccepted: state.userData.termsAccepted,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
const methods = useForm<DetailsSchema>({
|
const methods = useForm<DetailsSchema>({
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
.content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--Spacing-x1);
|
||||||
|
padding-top: var(--Spacing-x2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.content ol {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary {
|
||||||
|
list-style: none;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--Spacing-x-half);
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary::-webkit-details-marker,
|
||||||
|
.summary::marker {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
import { useIntl } from "react-intl"
|
||||||
|
|
||||||
|
import ChevronDown from "@/components/Icons/ChevronDown"
|
||||||
|
import Body from "@/components/TempDesignSystem/Text/Body"
|
||||||
|
import Caption from "@/components/TempDesignSystem/Text/Caption"
|
||||||
|
|
||||||
|
import styles from "./guaranteeDetails.module.css"
|
||||||
|
|
||||||
|
export default function GuaranteeDetails() {
|
||||||
|
const intl = useIntl()
|
||||||
|
return (
|
||||||
|
<details>
|
||||||
|
<Caption color="burgundy" type="bold" asChild>
|
||||||
|
<summary className={styles.summary}>
|
||||||
|
{intl.formatMessage({ id: "How it works" })}
|
||||||
|
<ChevronDown color="burgundy" height={16} />
|
||||||
|
</summary>
|
||||||
|
</Caption>
|
||||||
|
<section className={styles.content}>
|
||||||
|
<Body>
|
||||||
|
{intl.formatMessage({
|
||||||
|
id: "When guaranteeing your booking, we will hold the booking until 07:00 until the day after check-in. This will provide you as a guest with added flexibility for check-in times.",
|
||||||
|
})}
|
||||||
|
</Body>
|
||||||
|
<Body>
|
||||||
|
{intl.formatMessage({
|
||||||
|
id: "What you have to do to guarantee booking:",
|
||||||
|
})}
|
||||||
|
</Body>
|
||||||
|
<ol>
|
||||||
|
<Body asChild>
|
||||||
|
<li>{intl.formatMessage({ id: "Complete the booking" })}</li>
|
||||||
|
</Body>
|
||||||
|
<Body asChild>
|
||||||
|
<li>
|
||||||
|
{intl.formatMessage({
|
||||||
|
id: "Provide a payment card in the next step",
|
||||||
|
})}
|
||||||
|
</li>
|
||||||
|
</Body>
|
||||||
|
</ol>
|
||||||
|
<Body>
|
||||||
|
{intl.formatMessage({
|
||||||
|
id: "Please note that this is mandatory, and that your card will only be charged in the event of a no-show.",
|
||||||
|
})}
|
||||||
|
</Body>
|
||||||
|
</section>
|
||||||
|
</details>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -22,7 +22,7 @@ import { useEnterDetailsStore } from "@/stores/enter-details"
|
|||||||
|
|
||||||
import LoadingSpinner from "@/components/LoadingSpinner"
|
import LoadingSpinner from "@/components/LoadingSpinner"
|
||||||
import Button from "@/components/TempDesignSystem/Button"
|
import Button from "@/components/TempDesignSystem/Button"
|
||||||
import Checkbox from "@/components/TempDesignSystem/Checkbox"
|
import Checkbox from "@/components/TempDesignSystem/Form/Checkbox"
|
||||||
import Link from "@/components/TempDesignSystem/Link"
|
import Link from "@/components/TempDesignSystem/Link"
|
||||||
import Body from "@/components/TempDesignSystem/Text/Body"
|
import Body from "@/components/TempDesignSystem/Text/Body"
|
||||||
import Caption from "@/components/TempDesignSystem/Text/Caption"
|
import Caption from "@/components/TempDesignSystem/Text/Caption"
|
||||||
@@ -30,6 +30,7 @@ import { toast } from "@/components/TempDesignSystem/Toasts"
|
|||||||
import { useHandleBookingStatus } from "@/hooks/booking/useHandleBookingStatus"
|
import { useHandleBookingStatus } from "@/hooks/booking/useHandleBookingStatus"
|
||||||
import useLang from "@/hooks/useLang"
|
import useLang from "@/hooks/useLang"
|
||||||
|
|
||||||
|
import GuaranteeDetails from "./GuaranteeDetails"
|
||||||
import PaymentOption from "./PaymentOption"
|
import PaymentOption from "./PaymentOption"
|
||||||
import { PaymentFormData, paymentSchema } from "./schema"
|
import { PaymentFormData, paymentSchema } from "./schema"
|
||||||
|
|
||||||
@@ -48,13 +49,14 @@ export default function Payment({
|
|||||||
hotelId,
|
hotelId,
|
||||||
otherPaymentOptions,
|
otherPaymentOptions,
|
||||||
savedCreditCards,
|
savedCreditCards,
|
||||||
|
mustBeGuaranteed,
|
||||||
}: PaymentProps) {
|
}: PaymentProps) {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const lang = useLang()
|
const lang = useLang()
|
||||||
const intl = useIntl()
|
const intl = useIntl()
|
||||||
const queryParams = useSearchParams()
|
const queryParams = useSearchParams()
|
||||||
const { firstName, lastName, email, phoneNumber, countryCode } =
|
const { firstName, lastName, email, phoneNumber, countryCode } =
|
||||||
useEnterDetailsStore((state) => state.data)
|
useEnterDetailsStore((state) => state.userData)
|
||||||
const [confirmationNumber, setConfirmationNumber] = useState<string>("")
|
const [confirmationNumber, setConfirmationNumber] = useState<string>("")
|
||||||
|
|
||||||
const methods = useForm<PaymentFormData>({
|
const methods = useForm<PaymentFormData>({
|
||||||
@@ -169,12 +171,26 @@ export default function Payment({
|
|||||||
return <LoadingSpinner />
|
return <LoadingSpinner />
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const guaranteeing = intl.formatMessage({ id: "guaranteeing" })
|
||||||
|
const paying = intl.formatMessage({ id: "paying" })
|
||||||
|
const paymentVerb = mustBeGuaranteed ? guaranteeing : paying
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FormProvider {...methods}>
|
<FormProvider {...methods}>
|
||||||
<form
|
<form
|
||||||
className={styles.paymentContainer}
|
className={styles.paymentContainer}
|
||||||
onSubmit={methods.handleSubmit(handleSubmit)}
|
onSubmit={methods.handleSubmit(handleSubmit)}
|
||||||
>
|
>
|
||||||
|
{mustBeGuaranteed ? (
|
||||||
|
<section className={styles.section}>
|
||||||
|
<Body>
|
||||||
|
{intl.formatMessage({
|
||||||
|
id: "To secure your reservation, we kindly ask you to provide your payment card details. Rest assured, no charges will be made at this time.",
|
||||||
|
})}
|
||||||
|
</Body>
|
||||||
|
<GuaranteeDetails />
|
||||||
|
</section>
|
||||||
|
) : null}
|
||||||
{savedCreditCards?.length ? (
|
{savedCreditCards?.length ? (
|
||||||
<section className={styles.section}>
|
<section className={styles.section}>
|
||||||
<Body color="uiTextHighContrast" textTransform="bold">
|
<Body color="uiTextHighContrast" textTransform="bold">
|
||||||
@@ -238,6 +254,7 @@ export default function Payment({
|
|||||||
id: "booking.terms",
|
id: "booking.terms",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
paymentVerb,
|
||||||
termsLink: (str) => (
|
termsLink: (str) => (
|
||||||
<Link
|
<Link
|
||||||
className={styles.link}
|
className={styles.link}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
import { useSearchParams } from "next/navigation"
|
||||||
import { PropsWithChildren, useRef } from "react"
|
import { PropsWithChildren, useRef } from "react"
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -7,15 +8,16 @@ import {
|
|||||||
initEditDetailsState,
|
initEditDetailsState,
|
||||||
} from "@/stores/enter-details"
|
} from "@/stores/enter-details"
|
||||||
|
|
||||||
import { StepEnum } from "@/types/components/enterDetails/step"
|
import { StepEnum } from "@/types/components/hotelReservation/enterDetails/step"
|
||||||
|
|
||||||
export default function EnterDetailsProvider({
|
export default function EnterDetailsProvider({
|
||||||
step,
|
step,
|
||||||
children,
|
children,
|
||||||
}: PropsWithChildren<{ step: StepEnum }>) {
|
}: PropsWithChildren<{ step: StepEnum }>) {
|
||||||
|
const searchParams = useSearchParams()
|
||||||
const initialStore = useRef<EnterDetailsStore>()
|
const initialStore = useRef<EnterDetailsStore>()
|
||||||
if (!initialStore.current) {
|
if (!initialStore.current) {
|
||||||
initialStore.current = initEditDetailsState(step)
|
initialStore.current = initEditDetailsState(step, searchParams)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import styles from "./enterDetailsSidePeek.module.css"
|
|||||||
import {
|
import {
|
||||||
SidePeekEnum,
|
SidePeekEnum,
|
||||||
SidePeekProps,
|
SidePeekProps,
|
||||||
} from "@/types/components/enterDetails/sidePeek"
|
} from "@/types/components/hotelReservation/enterDetails/sidePeek"
|
||||||
|
|
||||||
export default function EnterDetailsSidePeek({ hotel }: SidePeekProps) {
|
export default function EnterDetailsSidePeek({ hotel }: SidePeekProps) {
|
||||||
const activeSidePeek = useEnterDetailsStore((state) => state.activeSidePeek)
|
const activeSidePeek = useEnterDetailsStore((state) => state.activeSidePeek)
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { useEnterDetailsStore } from "@/stores/enter-details"
|
|||||||
import { ChevronRightSmallIcon } from "@/components/Icons"
|
import { ChevronRightSmallIcon } from "@/components/Icons"
|
||||||
import Button from "@/components/TempDesignSystem/Button"
|
import Button from "@/components/TempDesignSystem/Button"
|
||||||
|
|
||||||
import { SidePeekEnum } from "@/types/components/enterDetails/sidePeek"
|
import { SidePeekEnum } from "@/types/components/hotelReservation/enterDetails/sidePeek"
|
||||||
|
|
||||||
export default function ToggleSidePeek() {
|
export default function ToggleSidePeek() {
|
||||||
const intl = useIntl()
|
const intl = useIntl()
|
||||||
|
|||||||
@@ -1,153 +1,201 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import { useEffect, useState } from "react"
|
||||||
|
import { useIntl } from "react-intl"
|
||||||
|
|
||||||
import { dt } from "@/lib/dt"
|
import { dt } from "@/lib/dt"
|
||||||
|
import { useEnterDetailsStore } from "@/stores/enter-details"
|
||||||
|
|
||||||
import { ArrowRightIcon } from "@/components/Icons"
|
import { ArrowRightIcon } from "@/components/Icons"
|
||||||
import Divider from "@/components/TempDesignSystem/Divider"
|
import Divider from "@/components/TempDesignSystem/Divider"
|
||||||
|
import Link from "@/components/TempDesignSystem/Link"
|
||||||
import Body from "@/components/TempDesignSystem/Text/Body"
|
import Body from "@/components/TempDesignSystem/Text/Body"
|
||||||
import Caption from "@/components/TempDesignSystem/Text/Caption"
|
import Caption from "@/components/TempDesignSystem/Text/Caption"
|
||||||
import { getIntl } from "@/i18n"
|
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
|
||||||
import { getLang } from "@/i18n/serverContext"
|
import useLang from "@/hooks/useLang"
|
||||||
|
import { formatNumber } from "@/utils/format"
|
||||||
import ToggleSidePeek from "./ToggleSidePeek"
|
|
||||||
|
|
||||||
import styles from "./summary.module.css"
|
import styles from "./summary.module.css"
|
||||||
|
|
||||||
// TEMP
|
import { RoomsData } from "@/types/components/hotelReservation/enterDetails/bookingData"
|
||||||
const rooms = [
|
import { BreakfastPackage } from "@/types/components/hotelReservation/enterDetails/breakfast"
|
||||||
{
|
import { BreakfastPackageEnum } from "@/types/enums/breakfast"
|
||||||
adults: 1,
|
|
||||||
type: "Cozy cabin",
|
export default function Summary({
|
||||||
},
|
isMember,
|
||||||
]
|
room,
|
||||||
|
}: {
|
||||||
|
isMember: boolean
|
||||||
|
room: RoomsData
|
||||||
|
}) {
|
||||||
|
const [chosenBed, setChosenBed] = useState<string>()
|
||||||
|
const [chosenBreakfast, setChosenBreakfast] = useState<
|
||||||
|
BreakfastPackage | BreakfastPackageEnum.NO_BREAKFAST
|
||||||
|
>()
|
||||||
|
const intl = useIntl()
|
||||||
|
const lang = useLang()
|
||||||
|
const { fromDate, toDate, bedType, breakfast } = useEnterDetailsStore(
|
||||||
|
(state) => ({
|
||||||
|
fromDate: state.roomData.fromDate,
|
||||||
|
toDate: state.roomData.toDate,
|
||||||
|
bedType: state.userData.bedType,
|
||||||
|
breakfast: state.userData.breakfast,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
export default async function Summary() {
|
|
||||||
const intl = await getIntl()
|
|
||||||
const lang = getLang()
|
|
||||||
const fromDate = dt().locale(lang).format("ddd, D MMM")
|
|
||||||
const toDate = dt().add(1, "day").locale(lang).format("ddd, D MMM")
|
|
||||||
const diff = dt(toDate).diff(fromDate, "days")
|
const diff = dt(toDate).diff(fromDate, "days")
|
||||||
|
|
||||||
const totalAdults = rooms.reduce((total, room) => total + room.adults, 0)
|
|
||||||
|
|
||||||
const adults = intl.formatMessage(
|
|
||||||
{ id: "booking.adults" },
|
|
||||||
{ totalAdults: totalAdults }
|
|
||||||
)
|
|
||||||
const nights = intl.formatMessage(
|
const nights = intl.formatMessage(
|
||||||
{ id: "booking.nights" },
|
{ id: "booking.nights" },
|
||||||
{ totalNights: diff }
|
{ totalNights: diff }
|
||||||
)
|
)
|
||||||
|
|
||||||
const addOns = [
|
let color: "uiTextHighContrast" | "red" = "uiTextHighContrast"
|
||||||
{
|
if (isMember) {
|
||||||
price: intl.formatMessage({ id: "Included" }),
|
color = "red"
|
||||||
title: intl.formatMessage({ id: "King bed" }),
|
}
|
||||||
},
|
|
||||||
{
|
|
||||||
price: intl.formatMessage({ id: "Included" }),
|
|
||||||
title: intl.formatMessage({ id: "Breakfast buffet" }),
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
const mappedRooms = Array.from(
|
useEffect(() => {
|
||||||
rooms
|
setChosenBed(bedType)
|
||||||
.reduce((acc, room) => {
|
|
||||||
const currentRoom = acc.get(room.type)
|
if (breakfast) {
|
||||||
acc.set(room.type, {
|
setChosenBreakfast(breakfast)
|
||||||
total: currentRoom ? currentRoom.total + 1 : 1,
|
}
|
||||||
type: room.type,
|
}, [bedType, breakfast])
|
||||||
})
|
|
||||||
return acc
|
|
||||||
}, new Map())
|
|
||||||
.values()
|
|
||||||
)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className={styles.summary}>
|
<section className={styles.summary}>
|
||||||
<header>
|
<header>
|
||||||
<Body textTransform="bold">
|
<Subtitle type="two">{intl.formatMessage({ id: "Summary" })}</Subtitle>
|
||||||
{mappedRooms.map(
|
<Body className={styles.date} color="baseTextMediumContrast">
|
||||||
(room, idx) =>
|
{dt(fromDate).locale(lang).format("ddd, D MMM")}
|
||||||
`${room.total} x ${room.type}${mappedRooms.length > 1 && idx + 1 !== mappedRooms.length ? ", " : ""}`
|
<ArrowRightIcon color="peach80" height={15} width={15} />
|
||||||
)}
|
{dt(toDate).locale(lang).format("ddd, D MMM")} ({nights})
|
||||||
</Body>
|
</Body>
|
||||||
<Body className={styles.date} color="textMediumContrast">
|
|
||||||
{fromDate}
|
|
||||||
<ArrowRightIcon color="uiTextMediumContrast" height={15} width={15} />
|
|
||||||
{toDate}
|
|
||||||
</Body>
|
|
||||||
|
|
||||||
<ToggleSidePeek />
|
|
||||||
</header>
|
</header>
|
||||||
<Divider color="primaryLightSubtle" />
|
<Divider color="primaryLightSubtle" />
|
||||||
<div className={styles.addOns}>
|
<div className={styles.addOns}>
|
||||||
|
<div>
|
||||||
<div className={styles.entry}>
|
<div className={styles.entry}>
|
||||||
<Caption color="uiTextMediumContrast">
|
<Body color="textHighContrast">{room.roomType}</Body>
|
||||||
{`${nights}, ${adults}`}
|
<Caption color={color}>
|
||||||
</Caption>
|
|
||||||
<Caption color="uiTextHighContrast">
|
|
||||||
{intl.formatMessage(
|
{intl.formatMessage(
|
||||||
{ id: "{amount} {currency}" },
|
{ id: "{amount} {currency}" },
|
||||||
{ amount: "4536", currency: "SEK" }
|
{
|
||||||
|
amount: formatNumber(parseInt(room.localPrice.price ?? "0")),
|
||||||
|
currency: room.localPrice.currency,
|
||||||
|
}
|
||||||
)}
|
)}
|
||||||
</Caption>
|
</Caption>
|
||||||
</div>
|
</div>
|
||||||
{addOns.map((addOn) => (
|
<Caption color="uiTextMediumContrast">
|
||||||
<div className={styles.entry} key={addOn.title}>
|
{intl.formatMessage(
|
||||||
<Caption color="uiTextMediumContrast">{addOn.title}</Caption>
|
{ id: "booking.adults" },
|
||||||
<Caption color="uiTextHighContrast">{addOn.price}</Caption>
|
{ totalAdults: room.adults }
|
||||||
|
)}
|
||||||
|
</Caption>
|
||||||
|
{room.children?.length ? (
|
||||||
|
<Caption color="uiTextMediumContrast">
|
||||||
|
{intl.formatMessage(
|
||||||
|
{ id: "booking.children" },
|
||||||
|
{ totalChildren: room.children.length }
|
||||||
|
)}
|
||||||
|
</Caption>
|
||||||
|
) : null}
|
||||||
|
<Caption color="uiTextMediumContrast">
|
||||||
|
{room.cancellationText}
|
||||||
|
</Caption>
|
||||||
|
<Link color="burgundy" href="#" variant="underscored" size="small">
|
||||||
|
{intl.formatMessage({ id: "Rate details" })}
|
||||||
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
))}
|
|
||||||
|
{chosenBed ? (
|
||||||
|
<div className={styles.entry}>
|
||||||
|
<div>
|
||||||
|
<Body color="textHighContrast">{chosenBed}</Body>
|
||||||
|
<Caption color="uiTextMediumContrast">
|
||||||
|
{intl.formatMessage({ id: "Based on availability" })}
|
||||||
|
</Caption>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Caption color="uiTextMediumContrast">
|
||||||
|
{intl.formatMessage(
|
||||||
|
{ id: "{amount} {currency}" },
|
||||||
|
{ amount: "0", currency: room.localPrice.currency }
|
||||||
|
)}
|
||||||
|
</Caption>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{chosenBreakfast ? (
|
||||||
|
chosenBreakfast === BreakfastPackageEnum.NO_BREAKFAST ? (
|
||||||
|
<div className={styles.entry}>
|
||||||
|
<Body color="textHighContrast">
|
||||||
|
{intl.formatMessage({ id: "No breakfast" })}
|
||||||
|
</Body>
|
||||||
|
<Caption color="uiTextMediumContrast">
|
||||||
|
{intl.formatMessage(
|
||||||
|
{ id: "{amount} {currency}" },
|
||||||
|
{ amount: "0", currency: room.localPrice.currency }
|
||||||
|
)}
|
||||||
|
</Caption>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className={styles.entry}>
|
||||||
|
<Body color="textHighContrast">
|
||||||
|
{intl.formatMessage({ id: "Breakfast buffet" })}
|
||||||
|
</Body>
|
||||||
|
<Caption color="uiTextMediumContrast">
|
||||||
|
{intl.formatMessage(
|
||||||
|
{ id: "{amount} {currency}" },
|
||||||
|
{
|
||||||
|
amount: chosenBreakfast.totalPrice,
|
||||||
|
currency: chosenBreakfast.currency,
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
</Caption>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
<Divider color="primaryLightSubtle" />
|
<Divider color="primaryLightSubtle" />
|
||||||
<div className={styles.total}>
|
<div className={styles.total}>
|
||||||
<div>
|
|
||||||
<div className={styles.entry}>
|
<div className={styles.entry}>
|
||||||
<Body textTransform="bold">
|
<div>
|
||||||
{intl.formatMessage({ id: "Total incl VAT" })}
|
<Body>
|
||||||
|
{intl.formatMessage<React.ReactNode>(
|
||||||
|
{ id: "<b>Total price</b> (incl VAT)" },
|
||||||
|
{ b: (str) => <b>{str}</b> }
|
||||||
|
)}
|
||||||
</Body>
|
</Body>
|
||||||
|
<Link color="burgundy" href="#" variant="underscored" size="small">
|
||||||
|
{intl.formatMessage({ id: "Price details" })}
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
<Body textTransform="bold">
|
<Body textTransform="bold">
|
||||||
{intl.formatMessage(
|
{intl.formatMessage(
|
||||||
{ id: "{amount} {currency}" },
|
{ id: "{amount} {currency}" },
|
||||||
{ amount: "4686", currency: "SEK" }
|
{
|
||||||
|
amount: formatNumber(parseInt(room.localPrice.price ?? "0")),
|
||||||
|
currency: room.localPrice.currency,
|
||||||
|
}
|
||||||
)}
|
)}
|
||||||
</Body>
|
</Body>
|
||||||
</div>
|
|
||||||
<div className={styles.entry}>
|
|
||||||
<Caption color="uiTextMediumContrast">
|
|
||||||
{intl.formatMessage({ id: "Approx." })}
|
|
||||||
</Caption>
|
|
||||||
<Caption color="uiTextMediumContrast">
|
<Caption color="uiTextMediumContrast">
|
||||||
|
{intl.formatMessage({ id: "Approx." })}{" "}
|
||||||
{intl.formatMessage(
|
{intl.formatMessage(
|
||||||
{ id: "{amount} {currency}" },
|
{ id: "{amount} {currency}" },
|
||||||
{ amount: "455", currency: "EUR" }
|
{
|
||||||
)}
|
amount: formatNumber(parseInt(room.euroPrice.price ?? "0")),
|
||||||
</Caption>
|
currency: room.euroPrice.currency,
|
||||||
</div>
|
}
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div className={styles.entry}>
|
|
||||||
<Body color="red" textTransform="bold">
|
|
||||||
{intl.formatMessage({ id: "Member price" })}
|
|
||||||
</Body>
|
|
||||||
<Body color="red" textTransform="bold">
|
|
||||||
{intl.formatMessage(
|
|
||||||
{ id: "{amount} {currency}" },
|
|
||||||
{ amount: "4219", currency: "SEK" }
|
|
||||||
)}
|
|
||||||
</Body>
|
|
||||||
</div>
|
|
||||||
<div className={styles.entry}>
|
|
||||||
<Caption color="uiTextMediumContrast">
|
|
||||||
{intl.formatMessage({ id: "Approx." })}
|
|
||||||
</Caption>
|
|
||||||
<Caption color="uiTextMediumContrast">
|
|
||||||
{intl.formatMessage(
|
|
||||||
{ id: "{amount} {currency}" },
|
|
||||||
{ amount: "412", currency: "EUR" }
|
|
||||||
)}
|
)}
|
||||||
</Caption>
|
</Caption>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<Divider color="primaryLightSubtle" />
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
.summary {
|
.summary {
|
||||||
background-color: var(--Main-Grey-White);
|
|
||||||
border: 1px solid var(--Primary-Light-On-Surface-Divider-subtle);
|
|
||||||
border-radius: var(--Corner-radius-Large);
|
border-radius: var(--Corner-radius-Large);
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: var(--Spacing-x2);
|
gap: var(--Spacing-x2);
|
||||||
padding: var(--Spacing-x2);
|
padding: var(--Spacing-x3);
|
||||||
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.date {
|
.date {
|
||||||
@@ -22,7 +21,7 @@
|
|||||||
.addOns {
|
.addOns {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: var(--Spacing-x1);
|
gap: var(--Spacing-x-one-and-half);
|
||||||
}
|
}
|
||||||
|
|
||||||
.entry {
|
.entry {
|
||||||
@@ -31,6 +30,9 @@
|
|||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.entry > :last-child {
|
||||||
|
justify-items: flex-end;
|
||||||
|
}
|
||||||
.total {
|
.total {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|||||||
@@ -16,22 +16,17 @@ import Contact from "../Contact"
|
|||||||
import styles from "./readMore.module.css"
|
import styles from "./readMore.module.css"
|
||||||
|
|
||||||
import {
|
import {
|
||||||
DetailedAmenity,
|
|
||||||
ParkingProps,
|
ParkingProps,
|
||||||
ReadMoreProps,
|
ReadMoreProps,
|
||||||
} from "@/types/components/hotelReservation/selectHotel/selectHotel"
|
} from "@/types/components/hotelReservation/selectHotel/selectHotel"
|
||||||
import { Hotel } from "@/types/hotel"
|
import type { Amenities, Hotel } from "@/types/hotel"
|
||||||
|
|
||||||
function getAmenitiesList(hotel: Hotel) {
|
function getAmenitiesList(hotel: Hotel) {
|
||||||
const detailedAmenities: DetailedAmenity[] = Object.entries(
|
const detailedAmenities: Amenities = hotel.detailedFacilities.filter(
|
||||||
hotel.hotelFacts.hotelFacilityDetail
|
|
||||||
).map(([key, value]) => ({ name: key, ...value }))
|
|
||||||
|
|
||||||
// Remove Parking facilities since parking accordion is based on hotel.parking
|
// Remove Parking facilities since parking accordion is based on hotel.parking
|
||||||
const simpleAmenities = hotel.detailedFacilities.filter(
|
(facility) => !facility.name.startsWith("Parking") && facility.public
|
||||||
(facility) => !facility.name.startsWith("Parking")
|
|
||||||
)
|
)
|
||||||
return [...detailedAmenities, ...simpleAmenities]
|
return detailedAmenities
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ReadMore({ label, hotel, hotelId }: ReadMoreProps) {
|
export default function ReadMore({ label, hotel, hotelId }: ReadMoreProps) {
|
||||||
@@ -80,11 +75,7 @@ export default function ReadMore({ label, hotel, hotelId }: ReadMoreProps) {
|
|||||||
TODO: What content should be in the accessibility section?
|
TODO: What content should be in the accessibility section?
|
||||||
</AccordionItem>
|
</AccordionItem>
|
||||||
{amenitiesList.map((amenity) => {
|
{amenitiesList.map((amenity) => {
|
||||||
return "description" in amenity ? (
|
return (
|
||||||
<AccordionItem key={amenity.name} title={amenity.heading}>
|
|
||||||
{amenity.description}
|
|
||||||
</AccordionItem>
|
|
||||||
) : (
|
|
||||||
<div key={amenity.id} className={styles.amenity}>
|
<div key={amenity.id} className={styles.amenity}>
|
||||||
{amenity.name}
|
{amenity.name}
|
||||||
</div>
|
</div>
|
||||||
@@ -112,7 +103,7 @@ function Parking({ parking }: ParkingProps) {
|
|||||||
</li>
|
</li>
|
||||||
<li>{`${intl.formatMessage({ id: "Parking can be reserved in advance" })}: ${parking.canMakeReservation ? intl.formatMessage({ id: "Yes" }) : intl.formatMessage({ id: "No" })}`}</li>
|
<li>{`${intl.formatMessage({ id: "Parking can be reserved in advance" })}: ${parking.canMakeReservation ? intl.formatMessage({ id: "Yes" }) : intl.formatMessage({ id: "No" })}`}</li>
|
||||||
<li>{`${intl.formatMessage({ id: "Number of parking spots" })}: ${parking.numberOfParkingSpots}`}</li>
|
<li>{`${intl.formatMessage({ id: "Number of parking spots" })}: ${parking.numberOfParkingSpots}`}</li>
|
||||||
<li>{`${intl.formatMessage({ id: "Distance to hotel" })}: ${parking.distanceToHotel}`}</li>
|
<li>{`${intl.formatMessage({ id: "Distance to hotel" })}: ${parking.distanceToHotel} m`}</li>
|
||||||
<li>{`${intl.formatMessage({ id: "Address" })}: ${parking.address}`}</li>
|
<li>{`${intl.formatMessage({ id: "Address" })}: ${parking.address}`}</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import { useIntl } from "react-intl"
|
|||||||
|
|
||||||
import { mapFacilityToIcon } from "@/components/ContentType/HotelPage/data"
|
import { mapFacilityToIcon } from "@/components/ContentType/HotelPage/data"
|
||||||
import TripAdvisorIcon from "@/components/Icons/TripAdvisor"
|
import TripAdvisorIcon from "@/components/Icons/TripAdvisor"
|
||||||
import Image from "@/components/Image"
|
|
||||||
import Divider from "@/components/TempDesignSystem/Divider"
|
import Divider from "@/components/TempDesignSystem/Divider"
|
||||||
import Body from "@/components/TempDesignSystem/Text/Body"
|
import Body from "@/components/TempDesignSystem/Text/Body"
|
||||||
import Caption from "@/components/TempDesignSystem/Text/Caption"
|
import Caption from "@/components/TempDesignSystem/Text/Caption"
|
||||||
@@ -29,14 +28,6 @@ export default function HotelInfoCard({ hotelData }: HotelInfoCardProps) {
|
|||||||
{hotelAttributes && (
|
{hotelAttributes && (
|
||||||
<section className={styles.wrapper}>
|
<section className={styles.wrapper}>
|
||||||
<div className={styles.imageWrapper}>
|
<div className={styles.imageWrapper}>
|
||||||
{hotelAttributes.ratings?.tripAdvisor && (
|
|
||||||
<div className={styles.tripAdvisor}>
|
|
||||||
<TripAdvisorIcon color="burgundy" />
|
|
||||||
<Caption color="burgundy">
|
|
||||||
{hotelAttributes.ratings.tripAdvisor.rating}
|
|
||||||
</Caption>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{hotelAttributes.gallery && (
|
{hotelAttributes.gallery && (
|
||||||
<ImageGallery
|
<ImageGallery
|
||||||
title={hotelAttributes.name}
|
title={hotelAttributes.name}
|
||||||
@@ -46,6 +37,14 @@ export default function HotelInfoCard({ hotelData }: HotelInfoCardProps) {
|
|||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{hotelAttributes.ratings?.tripAdvisor && (
|
||||||
|
<div className={styles.tripAdvisor}>
|
||||||
|
<TripAdvisorIcon color="burgundy" />
|
||||||
|
<Caption color="burgundy">
|
||||||
|
{hotelAttributes.ratings.tripAdvisor.rating}
|
||||||
|
</Caption>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.hotelContent}>
|
<div className={styles.hotelContent}>
|
||||||
<div className={styles.hotelInformation}>
|
<div className={styles.hotelInformation}>
|
||||||
|
|||||||
@@ -9,14 +9,7 @@ import type { ImageGalleryProps } from "@/types/components/hotelReservation/sele
|
|||||||
|
|
||||||
export default function ImageGallery({ images, title }: ImageGalleryProps) {
|
export default function ImageGallery({ images, title }: ImageGalleryProps) {
|
||||||
return (
|
return (
|
||||||
<Lightbox
|
<Lightbox images={images} dialogTitle={title}>
|
||||||
images={images.map((image) => ({
|
|
||||||
url: image.imageSizes.small,
|
|
||||||
alt: image.metaData.altText,
|
|
||||||
title: image.metaData.title,
|
|
||||||
}))}
|
|
||||||
dialogTitle={title}
|
|
||||||
>
|
|
||||||
<div className={styles.triggerArea} id="lightboxTrigger">
|
<div className={styles.triggerArea} id="lightboxTrigger">
|
||||||
<Image
|
<Image
|
||||||
src={images[0].imageSizes.medium}
|
src={images[0].imageSizes.medium}
|
||||||
|
|||||||
124
components/HotelReservation/SelectRate/RoomFilter/index.tsx
Normal file
124
components/HotelReservation/SelectRate/RoomFilter/index.tsx
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod"
|
||||||
|
import { useCallback, useEffect, useMemo } from "react"
|
||||||
|
import { FormProvider, useForm } from "react-hook-form"
|
||||||
|
import { useIntl } from "react-intl"
|
||||||
|
import { z } from "zod"
|
||||||
|
|
||||||
|
import { InfoCircleIcon } from "@/components/Icons"
|
||||||
|
import CheckboxChip from "@/components/TempDesignSystem/Form/FilterChip/Checkbox"
|
||||||
|
import Body from "@/components/TempDesignSystem/Text/Body"
|
||||||
|
import Caption from "@/components/TempDesignSystem/Text/Caption"
|
||||||
|
import { Tooltip } from "@/components/TempDesignSystem/Tooltip"
|
||||||
|
|
||||||
|
import { getIconForFeatureCode } from "../utils"
|
||||||
|
|
||||||
|
import styles from "./roomFilter.module.css"
|
||||||
|
|
||||||
|
import {
|
||||||
|
type RoomFilterProps,
|
||||||
|
RoomPackageCodeEnum,
|
||||||
|
} from "@/types/components/hotelReservation/selectRate/roomFilter"
|
||||||
|
|
||||||
|
export default function RoomFilter({
|
||||||
|
numberOfRooms,
|
||||||
|
onFilter,
|
||||||
|
filterOptions,
|
||||||
|
}: RoomFilterProps) {
|
||||||
|
const initialFilterValues = useMemo(
|
||||||
|
() =>
|
||||||
|
filterOptions.reduce(
|
||||||
|
(acc, option) => {
|
||||||
|
acc[option.code] = false
|
||||||
|
return acc
|
||||||
|
},
|
||||||
|
{} as Record<string, boolean | undefined>
|
||||||
|
),
|
||||||
|
[filterOptions]
|
||||||
|
)
|
||||||
|
|
||||||
|
const intl = useIntl()
|
||||||
|
const methods = useForm<Record<string, boolean | undefined>>({
|
||||||
|
defaultValues: initialFilterValues,
|
||||||
|
mode: "all",
|
||||||
|
reValidateMode: "onChange",
|
||||||
|
resolver: zodResolver(z.object({})),
|
||||||
|
})
|
||||||
|
|
||||||
|
const { watch, getValues, handleSubmit } = methods
|
||||||
|
const petFriendly = watch(RoomPackageCodeEnum.PET_ROOM)
|
||||||
|
const allergyFriendly = watch(RoomPackageCodeEnum.ALLERGY_ROOM)
|
||||||
|
|
||||||
|
const selectedFilters = getValues()
|
||||||
|
|
||||||
|
const tooltipText = intl.formatMessage({
|
||||||
|
id: "Pet-friendly rooms have an additional fee of 20 EUR per stay",
|
||||||
|
})
|
||||||
|
|
||||||
|
const submitFilter = useCallback(() => {
|
||||||
|
const data = getValues()
|
||||||
|
onFilter(data)
|
||||||
|
}, [onFilter, getValues])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const subscription = watch(() => handleSubmit(submitFilter)())
|
||||||
|
return () => subscription.unsubscribe()
|
||||||
|
}, [handleSubmit, watch, submitFilter])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.container}>
|
||||||
|
<div className={styles.infoDesktop}>
|
||||||
|
<Body color="uiTextHighContrast">
|
||||||
|
{intl.formatMessage(
|
||||||
|
{ id: "Room types available" },
|
||||||
|
{ numberOfRooms }
|
||||||
|
)}
|
||||||
|
</Body>
|
||||||
|
</div>
|
||||||
|
<div className={styles.infoMobile}>
|
||||||
|
<div className={styles.filterInfo}>
|
||||||
|
<Caption type="label" color="burgundy" textTransform="uppercase">
|
||||||
|
{intl.formatMessage({ id: "Filter" })}
|
||||||
|
</Caption>
|
||||||
|
<Caption type="label" color="burgundy">
|
||||||
|
{Object.entries(selectedFilters)
|
||||||
|
.filter(([_, value]) => value)
|
||||||
|
.map(([key]) => intl.formatMessage({ id: key }))
|
||||||
|
.join(", ")}
|
||||||
|
</Caption>
|
||||||
|
</div>
|
||||||
|
<Caption color="uiTextHighContrast">
|
||||||
|
{intl.formatMessage(
|
||||||
|
{ id: "Room types available" },
|
||||||
|
{ numberOfRooms }
|
||||||
|
)}
|
||||||
|
</Caption>
|
||||||
|
</div>
|
||||||
|
<FormProvider {...methods}>
|
||||||
|
<form onSubmit={handleSubmit(submitFilter)}>
|
||||||
|
<div className={styles.roomsFilter}>
|
||||||
|
{filterOptions.map((option) => (
|
||||||
|
<CheckboxChip
|
||||||
|
name={option.code}
|
||||||
|
key={option.code}
|
||||||
|
label={intl.formatMessage({ id: option.description })}
|
||||||
|
disabled={
|
||||||
|
(option.code === RoomPackageCodeEnum.ALLERGY_ROOM &&
|
||||||
|
petFriendly) ||
|
||||||
|
(option.code === RoomPackageCodeEnum.PET_ROOM &&
|
||||||
|
allergyFriendly)
|
||||||
|
}
|
||||||
|
selected={getValues(option.code)}
|
||||||
|
Icon={getIconForFeatureCode(option.code)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
<Tooltip text={tooltipText} position="bottom" arrow="right">
|
||||||
|
<InfoCircleIcon className={styles.infoIcon} />
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</FormProvider>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
.container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.roomsFilter {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: var(--Spacing-x1);
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.roomsFilter .infoIcon,
|
||||||
|
.roomsFilter .infoIcon path {
|
||||||
|
stroke: var(--UI-Text-Medium-contrast);
|
||||||
|
fill: transparent;
|
||||||
|
}
|
||||||
|
.filterInfo {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: var(--Spacing-x-half);
|
||||||
|
align-items: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.infoDesktop {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.infoMobile {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
.infoDesktop {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.infoMobile {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -40,6 +40,9 @@ export default function PriceList({
|
|||||||
</Subtitle>
|
</Subtitle>
|
||||||
<Body color="uiTextHighContrast" textTransform="bold">
|
<Body color="uiTextHighContrast" textTransform="bold">
|
||||||
{publicLocalPrice.currency}
|
{publicLocalPrice.currency}
|
||||||
|
<span className={styles.perNight}>
|
||||||
|
/{intl.formatMessage({ id: "night" })}
|
||||||
|
</span>
|
||||||
</Body>
|
</Body>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
@@ -64,6 +67,9 @@ export default function PriceList({
|
|||||||
</Subtitle>
|
</Subtitle>
|
||||||
<Body color="red" textTransform="bold">
|
<Body color="red" textTransform="bold">
|
||||||
{memberLocalPrice.currency}
|
{memberLocalPrice.currency}
|
||||||
|
<span className={styles.perNight}>
|
||||||
|
/{intl.formatMessage({ id: "night" })}
|
||||||
|
</span>
|
||||||
</Body>
|
</Body>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -12,3 +12,8 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
gap: var(--Spacing-x-half);
|
gap: var(--Spacing-x-half);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.perNight {
|
||||||
|
font-weight: 400;
|
||||||
|
font-size: var(--typography-Caption-Regular-fontSize);
|
||||||
|
}
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ export default function FlexibilityOption({
|
|||||||
priceInformation,
|
priceInformation,
|
||||||
roomType,
|
roomType,
|
||||||
roomTypeCode,
|
roomTypeCode,
|
||||||
|
features,
|
||||||
handleSelectRate,
|
handleSelectRate,
|
||||||
}: FlexibilityOptionProps) {
|
}: FlexibilityOptionProps) {
|
||||||
const [rootDiv, setRootDiv] = useState<Element | undefined>(undefined)
|
const [rootDiv, setRootDiv] = useState<Element | undefined>(undefined)
|
||||||
@@ -52,6 +53,7 @@ export default function FlexibilityOption({
|
|||||||
priceName: name,
|
priceName: name,
|
||||||
public: publicPrice,
|
public: publicPrice,
|
||||||
member: memberPrice,
|
member: memberPrice,
|
||||||
|
features,
|
||||||
}
|
}
|
||||||
handleSelectRate(rate)
|
handleSelectRate(rate)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,29 +1,56 @@
|
|||||||
|
import { differenceInCalendarDays } from "date-fns"
|
||||||
import { useIntl } from "react-intl"
|
import { useIntl } from "react-intl"
|
||||||
|
|
||||||
import Button from "@/components/TempDesignSystem/Button"
|
import Button from "@/components/TempDesignSystem/Button"
|
||||||
import Body from "@/components/TempDesignSystem/Text/Body"
|
import Body from "@/components/TempDesignSystem/Text/Body"
|
||||||
|
import Caption from "@/components/TempDesignSystem/Text/Caption"
|
||||||
|
import Footnote from "@/components/TempDesignSystem/Text/Footnote"
|
||||||
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
|
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
|
||||||
|
|
||||||
import styles from "./rateSummary.module.css"
|
import styles from "./rateSummary.module.css"
|
||||||
|
|
||||||
import { RateSummaryProps } from "@/types/components/hotelReservation/selectRate/rateSummary"
|
import type { RateSummaryProps } from "@/types/components/hotelReservation/selectRate/rateSummary"
|
||||||
|
import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter"
|
||||||
|
|
||||||
export default function RateSummary({
|
export default function RateSummary({
|
||||||
rateSummary,
|
rateSummary,
|
||||||
isUserLoggedIn,
|
isUserLoggedIn,
|
||||||
|
packages,
|
||||||
|
roomsAvailability,
|
||||||
}: RateSummaryProps) {
|
}: RateSummaryProps) {
|
||||||
const intl = useIntl()
|
const intl = useIntl()
|
||||||
|
const {
|
||||||
|
member,
|
||||||
|
public: publicRate,
|
||||||
|
features,
|
||||||
|
roomType,
|
||||||
|
priceName,
|
||||||
|
} = rateSummary
|
||||||
|
const priceToShow = isUserLoggedIn ? member : publicRate
|
||||||
|
|
||||||
const priceToShow = isUserLoggedIn ? rateSummary.member : rateSummary.public
|
const isPetRoomSelected = features.some(
|
||||||
|
(feature) => feature.code === RoomPackageCodeEnum.PET_ROOM
|
||||||
|
)
|
||||||
|
|
||||||
|
const petRoomPackage = packages.find(
|
||||||
|
(pkg) => pkg.code === RoomPackageCodeEnum.PET_ROOM
|
||||||
|
)
|
||||||
|
|
||||||
|
const petRoomPrice = petRoomPackage?.calculatedPrice ?? null
|
||||||
|
const petRoomCurrency = petRoomPackage?.currency ?? null
|
||||||
|
|
||||||
|
const checkInDate = new Date(roomsAvailability.checkInDate)
|
||||||
|
const checkOutDate = new Date(roomsAvailability.checkOutDate)
|
||||||
|
const nights = differenceInCalendarDays(checkOutDate, checkInDate)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.summary}>
|
<div className={styles.summary}>
|
||||||
<div className={styles.summaryText}>
|
<div className={styles.summaryText}>
|
||||||
<Subtitle color="uiTextHighContrast">{rateSummary.roomType}</Subtitle>
|
<Subtitle color="uiTextHighContrast">{roomType}</Subtitle>
|
||||||
<Body color="uiTextMediumContrast">{rateSummary.priceName}</Body>
|
<Body color="uiTextMediumContrast">{priceName}</Body>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.summaryPrice}>
|
<div className={styles.summaryPrice}>
|
||||||
<div className={styles.summaryPriceText}>
|
<div className={styles.summaryPriceTextDesktop}>
|
||||||
<Subtitle color={isUserLoggedIn ? "red" : "uiTextHighContrast"}>
|
<Subtitle color={isUserLoggedIn ? "red" : "uiTextHighContrast"}>
|
||||||
{priceToShow?.localPrice.pricePerStay}{" "}
|
{priceToShow?.localPrice.pricePerStay}{" "}
|
||||||
{priceToShow?.localPrice.currency}
|
{priceToShow?.localPrice.currency}
|
||||||
@@ -34,7 +61,49 @@ export default function RateSummary({
|
|||||||
{priceToShow?.requestedPrice?.currency}
|
{priceToShow?.requestedPrice?.currency}
|
||||||
</Body>
|
</Body>
|
||||||
</div>
|
</div>
|
||||||
<Button type="submit" theme="base">
|
<div className={styles.summaryPriceTextMobile}>
|
||||||
|
<Caption color="uiTextHighContrast">
|
||||||
|
{intl.formatMessage({ id: "Total price" })}
|
||||||
|
</Caption>
|
||||||
|
<Subtitle color={isUserLoggedIn ? "red" : "uiTextHighContrast"}>
|
||||||
|
{priceToShow?.localPrice.pricePerStay}{" "}
|
||||||
|
{priceToShow?.localPrice.currency}
|
||||||
|
</Subtitle>
|
||||||
|
<Footnote
|
||||||
|
color="uiTextMediumContrast"
|
||||||
|
className={styles.summaryPriceTextMobile}
|
||||||
|
>
|
||||||
|
{intl.formatMessage(
|
||||||
|
{ id: "booking.nights" },
|
||||||
|
{ totalNights: nights }
|
||||||
|
)}
|
||||||
|
,{" "}
|
||||||
|
{intl.formatMessage(
|
||||||
|
{ id: "booking.adults" },
|
||||||
|
{ totalAdults: roomsAvailability.occupancy?.adults }
|
||||||
|
)}
|
||||||
|
{roomsAvailability.occupancy?.children?.length && (
|
||||||
|
<>
|
||||||
|
,{" "}
|
||||||
|
{intl.formatMessage(
|
||||||
|
{ id: "booking.children" },
|
||||||
|
{ totalChildren: roomsAvailability.occupancy.children.length }
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Footnote>
|
||||||
|
</div>
|
||||||
|
{isPetRoomSelected && (
|
||||||
|
<div className={styles.petInfo}>
|
||||||
|
<Body color="uiTextHighContrast" textTransform="bold">
|
||||||
|
+ {petRoomPrice} {petRoomCurrency}
|
||||||
|
</Body>
|
||||||
|
<Body color="uiTextMediumContrast">
|
||||||
|
{intl.formatMessage({ id: "Pet charge" })}
|
||||||
|
</Body>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<Button type="submit" theme="base" className={styles.continueButton}>
|
||||||
{intl.formatMessage({ id: "Continue" })}
|
{intl.formatMessage({ id: "Continue" })}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
background-color: var(--Base-Surface-Primary-light-Normal);
|
background-color: var(--Base-Surface-Primary-light-Normal);
|
||||||
padding: var(--Spacing-x3) var(--Spacing-x7) var(--Spacing-x5);
|
padding: var(--Spacing-x2) var(--Spacing-x3) var(--Spacing-x5);
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -13,5 +13,50 @@
|
|||||||
|
|
||||||
.summaryPrice {
|
.summaryPrice {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
gap: var(--Spacing-x4);
|
gap: var(--Spacing-x4);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.petInfo {
|
||||||
|
border-left: 1px solid var(--Primary-Light-On-Surface-Divider-subtle);
|
||||||
|
padding-left: var(--Spacing-x2);
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summaryText {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summaryPriceTextDesktop {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.continueButton {
|
||||||
|
margin-left: auto;
|
||||||
|
height: fit-content;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summaryPriceTextMobile {
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
.summary {
|
||||||
|
padding: var(--Spacing-x3) var(--Spacing-x7) var(--Spacing-x5);
|
||||||
|
}
|
||||||
|
.petInfo,
|
||||||
|
.summaryText,
|
||||||
|
.summaryPriceTextDesktop {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.summaryPriceTextMobile {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.summaryPrice {
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
.continueButton {
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
|
import { createElement } from "react"
|
||||||
import { useIntl } from "react-intl"
|
import { useIntl } from "react-intl"
|
||||||
|
|
||||||
import { RateDefinition } from "@/server/routers/hotels/output"
|
import { RateDefinition } from "@/server/routers/hotels/output"
|
||||||
@@ -10,8 +11,9 @@ import Caption from "@/components/TempDesignSystem/Text/Caption"
|
|||||||
import Footnote from "@/components/TempDesignSystem/Text/Footnote"
|
import Footnote from "@/components/TempDesignSystem/Text/Footnote"
|
||||||
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
|
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
|
||||||
|
|
||||||
|
import RoomSidePeek from "../../../../SidePeeks/RoomSidePeek"
|
||||||
import ImageGallery from "../../ImageGallery"
|
import ImageGallery from "../../ImageGallery"
|
||||||
import RoomSidePeek from "../RoomSidePeek"
|
import { getIconForFeatureCode } from "../../utils"
|
||||||
|
|
||||||
import styles from "./roomCard.module.css"
|
import styles from "./roomCard.module.css"
|
||||||
|
|
||||||
@@ -21,21 +23,22 @@ export default function RoomCard({
|
|||||||
rateDefinitions,
|
rateDefinitions,
|
||||||
roomConfiguration,
|
roomConfiguration,
|
||||||
roomCategories,
|
roomCategories,
|
||||||
|
selectedPackages,
|
||||||
handleSelectRate,
|
handleSelectRate,
|
||||||
}: RoomCardProps) {
|
}: RoomCardProps) {
|
||||||
const intl = useIntl()
|
const intl = useIntl()
|
||||||
const saveRate = rateDefinitions.find(
|
|
||||||
// TODO: Update string when API has decided
|
const rates = {
|
||||||
|
saveRate: rateDefinitions.find(
|
||||||
(rate) => rate.cancellationRule === "NonCancellable"
|
(rate) => rate.cancellationRule === "NonCancellable"
|
||||||
)
|
),
|
||||||
const changeRate = rateDefinitions.find(
|
changeRate: rateDefinitions.find(
|
||||||
// TODO: Update string when API has decided
|
|
||||||
(rate) => rate.cancellationRule === "Modifiable"
|
(rate) => rate.cancellationRule === "Modifiable"
|
||||||
)
|
),
|
||||||
const flexRate = rateDefinitions.find(
|
flexRate: rateDefinitions.find(
|
||||||
// TODO: Update string when API has decided
|
|
||||||
(rate) => rate.cancellationRule === "CancellableBefore6PM"
|
(rate) => rate.cancellationRule === "CancellableBefore6PM"
|
||||||
)
|
),
|
||||||
|
}
|
||||||
|
|
||||||
function findProductForRate(rate: RateDefinition | undefined) {
|
function findProductForRate(rate: RateDefinition | undefined) {
|
||||||
return rate
|
return rate
|
||||||
@@ -47,20 +50,16 @@ export default function RoomCard({
|
|||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
function getPriceForRate(
|
function getPriceInformationForRate(rate: RateDefinition | undefined) {
|
||||||
rate: typeof saveRate | typeof changeRate | typeof flexRate
|
|
||||||
) {
|
|
||||||
return rateDefinitions.find((def) => def.rateCode === rate?.rateCode)
|
return rateDefinitions.find((def) => def.rateCode === rate?.rateCode)
|
||||||
?.generalTerms
|
?.generalTerms
|
||||||
}
|
}
|
||||||
|
|
||||||
const selectedRoom = roomCategories.find(
|
const selectedRoom = roomCategories.find(
|
||||||
(room) => room.name === roomConfiguration.roomType
|
(room) => room.name === roomConfiguration.roomType
|
||||||
)
|
)
|
||||||
|
|
||||||
const roomSize = selectedRoom?.roomSize
|
const { roomSize, occupancy, descriptions, images } = selectedRoom || {}
|
||||||
const occupancy = selectedRoom?.occupancy.total
|
|
||||||
const roomDescription = selectedRoom?.descriptions.short
|
|
||||||
const images = selectedRoom?.images
|
|
||||||
const mainImage = images?.[0]
|
const mainImage = images?.[0]
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -68,12 +67,11 @@ export default function RoomCard({
|
|||||||
<div className={styles.cardBody}>
|
<div className={styles.cardBody}>
|
||||||
<div className={styles.specification}>
|
<div className={styles.specification}>
|
||||||
<Caption color="uiTextMediumContrast" className={styles.guests}>
|
<Caption color="uiTextMediumContrast" className={styles.guests}>
|
||||||
{/*TODO: Handle pluralisation*/}
|
|
||||||
{intl.formatMessage(
|
{intl.formatMessage(
|
||||||
{
|
{
|
||||||
id: "booking.guests",
|
id: "booking.guests",
|
||||||
},
|
},
|
||||||
{ nrOfGuests: occupancy }
|
{ nrOfGuests: occupancy?.total }
|
||||||
)}
|
)}
|
||||||
</Caption>
|
</Caption>
|
||||||
<Caption color="uiTextMediumContrast">
|
<Caption color="uiTextMediumContrast">
|
||||||
@@ -82,17 +80,16 @@ export default function RoomCard({
|
|||||||
: `${roomSize?.min}-${roomSize?.max}`}
|
: `${roomSize?.min}-${roomSize?.max}`}
|
||||||
m²
|
m²
|
||||||
</Caption>
|
</Caption>
|
||||||
<RoomSidePeek
|
{selectedRoom && (
|
||||||
selectedRoom={selectedRoom}
|
<RoomSidePeek room={selectedRoom} buttonSize="small" />
|
||||||
roomConfiguration={roomConfiguration}
|
)}
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.container}>
|
<div className={styles.container}>
|
||||||
<div className={styles.roomDetails}>
|
<div className={styles.roomDetails}>
|
||||||
<Subtitle className={styles.name} type="two">
|
<Subtitle className={styles.name} type="two">
|
||||||
{roomConfiguration.roomType}
|
{roomConfiguration.roomType}
|
||||||
</Subtitle>
|
</Subtitle>
|
||||||
<Body>{roomDescription}</Body>
|
<Body>{descriptions?.short}</Body>
|
||||||
</div>
|
</div>
|
||||||
<Caption color="uiTextHighContrast">
|
<Caption color="uiTextHighContrast">
|
||||||
{intl.formatMessage({
|
{intl.formatMessage({
|
||||||
@@ -100,49 +97,55 @@ export default function RoomCard({
|
|||||||
})}
|
})}
|
||||||
</Caption>
|
</Caption>
|
||||||
<div className={styles.flexibilityOptions}>
|
<div className={styles.flexibilityOptions}>
|
||||||
|
{Object.entries(rates).map(([key, rate]) => (
|
||||||
<FlexibilityOption
|
<FlexibilityOption
|
||||||
name={intl.formatMessage({ id: "Non-refundable" })}
|
key={key}
|
||||||
value="non-refundable"
|
name={intl.formatMessage({
|
||||||
paymentTerm={intl.formatMessage({ id: "Pay now" })}
|
id:
|
||||||
product={findProductForRate(saveRate)}
|
key === "flexRate"
|
||||||
priceInformation={getPriceForRate(saveRate)}
|
? "Free cancellation"
|
||||||
handleSelectRate={handleSelectRate}
|
: key === "saveRate"
|
||||||
roomType={roomConfiguration.roomType}
|
? "Non-refundable"
|
||||||
roomTypeCode={roomConfiguration.roomTypeCode}
|
: "Free rebooking",
|
||||||
/>
|
})}
|
||||||
<FlexibilityOption
|
value={key.toLowerCase()}
|
||||||
name={intl.formatMessage({ id: "Free rebooking" })}
|
paymentTerm={intl.formatMessage({
|
||||||
value="free-rebooking"
|
id: key === "flexRate" ? "Pay later" : "Pay now",
|
||||||
paymentTerm={intl.formatMessage({ id: "Pay now" })}
|
})}
|
||||||
product={findProductForRate(changeRate)}
|
product={findProductForRate(rate)}
|
||||||
priceInformation={getPriceForRate(changeRate)}
|
priceInformation={getPriceInformationForRate(rate)}
|
||||||
handleSelectRate={handleSelectRate}
|
|
||||||
roomType={roomConfiguration.roomType}
|
|
||||||
roomTypeCode={roomConfiguration.roomTypeCode}
|
|
||||||
/>
|
|
||||||
<FlexibilityOption
|
|
||||||
name={intl.formatMessage({ id: "Free cancellation" })}
|
|
||||||
value="free-cancellation"
|
|
||||||
paymentTerm={intl.formatMessage({ id: "Pay later" })}
|
|
||||||
product={findProductForRate(flexRate)}
|
|
||||||
priceInformation={getPriceForRate(flexRate)}
|
|
||||||
handleSelectRate={handleSelectRate}
|
handleSelectRate={handleSelectRate}
|
||||||
roomType={roomConfiguration.roomType}
|
roomType={roomConfiguration.roomType}
|
||||||
roomTypeCode={roomConfiguration.roomTypeCode}
|
roomTypeCode={roomConfiguration.roomTypeCode}
|
||||||
|
features={roomConfiguration.features}
|
||||||
/>
|
/>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{mainImage && (
|
{mainImage && (
|
||||||
<div className={styles.imageContainer}>
|
<div className={styles.imageContainer}>
|
||||||
|
<div className={styles.chipContainer}>
|
||||||
{roomConfiguration.roomsLeft < 5 && (
|
{roomConfiguration.roomsLeft < 5 && (
|
||||||
<span className={styles.roomsLeft}>
|
<span className={styles.chip}>
|
||||||
<Footnote
|
<Footnote
|
||||||
color="burgundy"
|
color="burgundy"
|
||||||
textTransform="uppercase"
|
textTransform="uppercase"
|
||||||
>{`${roomConfiguration.roomsLeft} ${intl.formatMessage({ id: "Left" })}`}</Footnote>
|
>{`${roomConfiguration.roomsLeft} ${intl.formatMessage({ id: "Left" })}`}</Footnote>
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
{roomConfiguration.features
|
||||||
|
.filter((feature) => selectedPackages.includes(feature.code))
|
||||||
|
.map((feature) => (
|
||||||
|
<span className={styles.chip} key={feature.code}>
|
||||||
|
{createElement(getIconForFeatureCode(feature.code), {
|
||||||
|
width: 16,
|
||||||
|
height: 16,
|
||||||
|
color: "burgundy",
|
||||||
|
})}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
{/*NOTE: images from the test API are hosted on test3.scandichotels.com,
|
{/*NOTE: images from the test API are hosted on test3.scandichotels.com,
|
||||||
which can't be accessed unless on Scandic's Wifi or using Citrix. */}
|
which can't be accessed unless on Scandic's Wifi or using Citrix. */}
|
||||||
{images && (
|
{images && (
|
||||||
|
|||||||
@@ -64,10 +64,17 @@
|
|||||||
gap: var(--Spacing-x2);
|
gap: var(--Spacing-x2);
|
||||||
}
|
}
|
||||||
|
|
||||||
.roomsLeft {
|
.chipContainer {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
z-index: 1;
|
||||||
top: 12px;
|
top: 12px;
|
||||||
left: 12px;
|
left: 12px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: var(--Spacing-x1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.chip {
|
||||||
background-color: var(--Main-Grey-White);
|
background-color: var(--Main-Grey-White);
|
||||||
padding: var(--Spacing-x-half) var(--Spacing-x1);
|
padding: var(--Spacing-x-half) var(--Spacing-x1);
|
||||||
border-radius: var(--Corner-radius-Small);
|
border-radius: var(--Corner-radius-Small);
|
||||||
|
|||||||
@@ -1,96 +0,0 @@
|
|||||||
import { useState } from "react"
|
|
||||||
import { useIntl } from "react-intl"
|
|
||||||
|
|
||||||
import { ChevronRightSmallIcon } from "@/components/Icons"
|
|
||||||
import Button from "@/components/TempDesignSystem/Button"
|
|
||||||
import SidePeek from "@/components/TempDesignSystem/SidePeek"
|
|
||||||
import Body from "@/components/TempDesignSystem/Text/Body"
|
|
||||||
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
|
|
||||||
|
|
||||||
import ImageGallery from "../../ImageGallery"
|
|
||||||
import { getFacilityIcon } from "./facilityIcon"
|
|
||||||
|
|
||||||
import styles from "./roomSidePeek.module.css"
|
|
||||||
|
|
||||||
import type { RoomSidePeekProps } from "@/types/components/hotelReservation/selectRate/roomSidePeek"
|
|
||||||
|
|
||||||
export default function RoomSidePeek({
|
|
||||||
selectedRoom,
|
|
||||||
roomConfiguration,
|
|
||||||
}: RoomSidePeekProps) {
|
|
||||||
const [isSidePeekOpen, setIsSidePeekOpen] = useState(false)
|
|
||||||
const intl = useIntl()
|
|
||||||
|
|
||||||
const roomSize = selectedRoom?.roomSize
|
|
||||||
const occupancy = selectedRoom?.occupancy.total
|
|
||||||
const roomDescription = selectedRoom?.descriptions.medium
|
|
||||||
const images = selectedRoom?.images
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<Button
|
|
||||||
intent="text"
|
|
||||||
type="button"
|
|
||||||
size="small"
|
|
||||||
theme="base"
|
|
||||||
className={styles.button}
|
|
||||||
onClick={() => setIsSidePeekOpen(true)}
|
|
||||||
>
|
|
||||||
{intl.formatMessage({ id: "See room details" })}
|
|
||||||
<ChevronRightSmallIcon color="burgundy" width={20} height={20} />
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<SidePeek
|
|
||||||
title={roomConfiguration.roomType}
|
|
||||||
isOpen={isSidePeekOpen}
|
|
||||||
handleClose={() => setIsSidePeekOpen(false)}
|
|
||||||
>
|
|
||||||
<Body color="baseTextMediumContrast">
|
|
||||||
{roomSize?.min === roomSize?.max
|
|
||||||
? roomSize?.min
|
|
||||||
: `${roomSize?.min} - ${roomSize?.max}`}
|
|
||||||
m².{" "}
|
|
||||||
{intl.formatMessage(
|
|
||||||
{
|
|
||||||
id: "booking.accommodatesUpTo",
|
|
||||||
},
|
|
||||||
{ nrOfGuests: occupancy }
|
|
||||||
)}
|
|
||||||
</Body>
|
|
||||||
|
|
||||||
{images && (
|
|
||||||
<div className={styles.imageContainer}>
|
|
||||||
<ImageGallery images={images} title={roomConfiguration.roomType} />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Body className={styles.description} color="uiTextHighContrast">
|
|
||||||
{roomDescription}
|
|
||||||
</Body>
|
|
||||||
<Subtitle type="two" color="uiTextHighContrast">
|
|
||||||
{intl.formatMessage({ id: "booking.thisRoomIsEquippedWith" })}
|
|
||||||
</Subtitle>
|
|
||||||
<ul className={styles.facilityList}>
|
|
||||||
{selectedRoom?.roomFacilities
|
|
||||||
.sort((a, b) => a.sortOrder - b.sortOrder)
|
|
||||||
.map((facility) => {
|
|
||||||
const Icon = getFacilityIcon(facility.name)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<li key={facility.name}>
|
|
||||||
{Icon && <Icon color="uiTextMediumContrast" />}
|
|
||||||
<Body
|
|
||||||
asChild
|
|
||||||
className={!Icon ? styles.noIcon : undefined}
|
|
||||||
color="uiTextMediumContrast"
|
|
||||||
>
|
|
||||||
<span>{facility.name}</span>
|
|
||||||
</Body>
|
|
||||||
</li>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</ul>
|
|
||||||
</SidePeek>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
.button {
|
|
||||||
margin-left: auto;
|
|
||||||
padding: 0 0 0 var(--Spacing-x-half);
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.imageContainer {
|
|
||||||
min-height: 185px;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.description {
|
|
||||||
margin-top: var(--Spacing-x-one-and-half);
|
|
||||||
margin-bottom: var(--Spacing-x2);
|
|
||||||
}
|
|
||||||
.facilityList {
|
|
||||||
margin-top: var(--Spacing-x-one-and-half);
|
|
||||||
column-count: 2;
|
|
||||||
column-gap: var(--Spacing-x2);
|
|
||||||
}
|
|
||||||
.facilityList li {
|
|
||||||
display: flex;
|
|
||||||
gap: var(--Spacing-x1);
|
|
||||||
margin-bottom: var(--Spacing-x-half);
|
|
||||||
}
|
|
||||||
|
|
||||||
.noIcon {
|
|
||||||
margin-left: var(--Spacing-x4);
|
|
||||||
}
|
|
||||||
@@ -1,20 +1,22 @@
|
|||||||
"use client"
|
"use client"
|
||||||
import { useRouter, useSearchParams } from "next/navigation"
|
import { useRouter, useSearchParams } from "next/navigation"
|
||||||
import { useState } from "react"
|
import { useMemo, useState } from "react"
|
||||||
|
|
||||||
import RateSummary from "./RateSummary"
|
import RateSummary from "./RateSummary"
|
||||||
import RoomCard from "./RoomCard"
|
import RoomCard from "./RoomCard"
|
||||||
import getHotelReservationQueryParams from "./utils"
|
import { getHotelReservationQueryParams } from "./utils"
|
||||||
|
|
||||||
import styles from "./roomSelection.module.css"
|
import styles from "./roomSelection.module.css"
|
||||||
|
|
||||||
import { RoomSelectionProps } from "@/types/components/hotelReservation/selectRate/roomSelection"
|
import type { RoomSelectionProps } from "@/types/components/hotelReservation/selectRate/roomSelection"
|
||||||
import { Rate } from "@/types/components/hotelReservation/selectRate/selectRate"
|
import type { Rate } from "@/types/components/hotelReservation/selectRate/selectRate"
|
||||||
|
|
||||||
export default function RoomSelection({
|
export default function RoomSelection({
|
||||||
roomConfigurations,
|
roomsAvailability,
|
||||||
roomCategories,
|
roomCategories,
|
||||||
user,
|
user,
|
||||||
|
packages,
|
||||||
|
selectedPackages,
|
||||||
}: RoomSelectionProps) {
|
}: RoomSelectionProps) {
|
||||||
const [rateSummary, setRateSummary] = useState<Rate | null>(null)
|
const [rateSummary, setRateSummary] = useState<Rate | null>(null)
|
||||||
|
|
||||||
@@ -22,27 +24,35 @@ export default function RoomSelection({
|
|||||||
const searchParams = useSearchParams()
|
const searchParams = useSearchParams()
|
||||||
const isUserLoggedIn = !!user
|
const isUserLoggedIn = !!user
|
||||||
|
|
||||||
function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
|
const { roomConfigurations, rateDefinitions } = roomsAvailability
|
||||||
e.preventDefault()
|
|
||||||
const searchParamsObject = getHotelReservationQueryParams(searchParams)
|
|
||||||
|
|
||||||
const queryParams = new URLSearchParams(searchParams)
|
const queryParams = useMemo(() => {
|
||||||
|
const params = new URLSearchParams(searchParams)
|
||||||
|
const searchParamsObject = getHotelReservationQueryParams(searchParams)
|
||||||
|
|
||||||
searchParamsObject.room.forEach((item, index) => {
|
searchParamsObject.room.forEach((item, index) => {
|
||||||
if (rateSummary?.roomTypeCode) {
|
if (rateSummary?.roomTypeCode) {
|
||||||
queryParams.set(`room[${index}].roomtype`, rateSummary.roomTypeCode)
|
params.set(`room[${index}].roomtype`, rateSummary.roomTypeCode)
|
||||||
}
|
}
|
||||||
if (rateSummary?.public?.rateCode) {
|
if (rateSummary?.public?.rateCode) {
|
||||||
queryParams.set(`room[${index}].ratecode`, rateSummary.public.rateCode)
|
params.set(`room[${index}].ratecode`, rateSummary.public.rateCode)
|
||||||
}
|
}
|
||||||
if (rateSummary?.member?.rateCode) {
|
if (rateSummary?.member?.rateCode) {
|
||||||
queryParams.set(
|
params.set(
|
||||||
`room[${index}].counterratecode`,
|
`room[${index}].counterratecode`,
|
||||||
rateSummary.member.rateCode
|
rateSummary.member.rateCode
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
if (selectedPackages.length > 0) {
|
||||||
|
params.set(`room[${index}].packages`, selectedPackages.join(","))
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
return params
|
||||||
|
}, [searchParams, rateSummary, selectedPackages])
|
||||||
|
|
||||||
|
function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
|
||||||
|
e.preventDefault()
|
||||||
router.push(`select-bed?${queryParams}`)
|
router.push(`select-bed?${queryParams}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -54,13 +64,14 @@ export default function RoomSelection({
|
|||||||
onSubmit={handleSubmit}
|
onSubmit={handleSubmit}
|
||||||
>
|
>
|
||||||
<ul className={styles.roomList}>
|
<ul className={styles.roomList}>
|
||||||
{roomConfigurations.roomConfigurations.map((roomConfiguration) => (
|
{roomConfigurations.map((roomConfiguration) => (
|
||||||
<li key={roomConfiguration.roomType}>
|
<li key={roomConfiguration.roomTypeCode}>
|
||||||
<RoomCard
|
<RoomCard
|
||||||
rateDefinitions={roomConfigurations.rateDefinitions}
|
rateDefinitions={rateDefinitions}
|
||||||
roomConfiguration={roomConfiguration}
|
roomConfiguration={roomConfiguration}
|
||||||
roomCategories={roomCategories}
|
roomCategories={roomCategories}
|
||||||
handleSelectRate={setRateSummary}
|
handleSelectRate={setRateSummary}
|
||||||
|
selectedPackages={selectedPackages}
|
||||||
/>
|
/>
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
@@ -69,6 +80,8 @@ export default function RoomSelection({
|
|||||||
<RateSummary
|
<RateSummary
|
||||||
rateSummary={rateSummary}
|
rateSummary={rateSummary}
|
||||||
isUserLoggedIn={isUserLoggedIn}
|
isUserLoggedIn={isUserLoggedIn}
|
||||||
|
packages={packages}
|
||||||
|
roomsAvailability={roomsAvailability}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.roomList {
|
.roomList {
|
||||||
margin-top: var(--Spacing-x4);
|
|
||||||
list-style: none;
|
list-style: none;
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
|
|||||||
@@ -1,28 +1,23 @@
|
|||||||
import { SelectRateSearchParams } from "@/types/components/hotelReservation/selectRate/selectRate"
|
import { getFormattedUrlQueryParams } from "@/utils/url"
|
||||||
|
|
||||||
function getHotelReservationQueryParams(searchParams: URLSearchParams) {
|
import type { SelectRateSearchParams } from "@/types/components/hotelReservation/selectRate/selectRate"
|
||||||
const searchParamsObject: Record<string, unknown> = Array.from(
|
|
||||||
searchParams.entries()
|
export function getHotelReservationQueryParams(searchParams: URLSearchParams) {
|
||||||
).reduce<Record<string, unknown>>(
|
return getFormattedUrlQueryParams(searchParams, {
|
||||||
(acc, [key, value]) => {
|
adults: "number",
|
||||||
const keys = key.replace(/\]/g, "").split(/\[|\./) // Split keys by '[' or '.'
|
age: "number",
|
||||||
keys.reduce((nestedAcc, k, i) => {
|
}) as SelectRateSearchParams
|
||||||
if (i === keys.length - 1) {
|
|
||||||
// Convert value to number if the key is 'adults' or 'age'
|
|
||||||
;(nestedAcc as Record<string, unknown>)[k] =
|
|
||||||
k === "adults" || k === "age" ? Number(value) : value
|
|
||||||
} else {
|
|
||||||
if (!nestedAcc[k]) {
|
|
||||||
nestedAcc[k] = isNaN(Number(keys[i + 1])) ? {} : [] // Initialize as object or array
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nestedAcc[k] as Record<string, unknown>
|
|
||||||
}, acc)
|
|
||||||
return acc
|
|
||||||
},
|
|
||||||
{} as Record<string, unknown>
|
|
||||||
)
|
|
||||||
return searchParamsObject as SelectRateSearchParams
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default getHotelReservationQueryParams
|
export function getQueryParamsForEnterDetails(searchParams: URLSearchParams) {
|
||||||
|
const selectRoomParamsObject = getHotelReservationQueryParams(searchParams)
|
||||||
|
|
||||||
|
const { room } = selectRoomParamsObject
|
||||||
|
return {
|
||||||
|
...selectRoomParamsObject,
|
||||||
|
adults: room[0].adults, // TODO: Handle multiple rooms
|
||||||
|
children: room[0].child?.length.toString(), // TODO: Handle multiple rooms and children
|
||||||
|
roomTypeCode: room[0].roomtype,
|
||||||
|
rateCode: room[0].ratecode,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
76
components/HotelReservation/SelectRate/Rooms/index.tsx
Normal file
76
components/HotelReservation/SelectRate/Rooms/index.tsx
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import { useCallback, useState } from "react"
|
||||||
|
|
||||||
|
import { RoomsAvailability } from "@/server/routers/hotels/output"
|
||||||
|
|
||||||
|
import RoomFilter from "../RoomFilter"
|
||||||
|
import RoomSelection from "../RoomSelection"
|
||||||
|
|
||||||
|
import styles from "./rooms.module.css"
|
||||||
|
|
||||||
|
import {
|
||||||
|
RoomPackageCodeEnum,
|
||||||
|
type RoomPackageCodes,
|
||||||
|
} from "@/types/components/hotelReservation/selectRate/roomFilter"
|
||||||
|
import type { RoomSelectionProps } from "@/types/components/hotelReservation/selectRate/roomSelection"
|
||||||
|
|
||||||
|
export default function Rooms({
|
||||||
|
roomsAvailability,
|
||||||
|
roomCategories = [],
|
||||||
|
user,
|
||||||
|
packages,
|
||||||
|
}: Omit<RoomSelectionProps, "selectedPackages">) {
|
||||||
|
const defaultRooms = roomsAvailability.roomConfigurations
|
||||||
|
const [rooms, setRooms] = useState<RoomsAvailability>({
|
||||||
|
...roomsAvailability,
|
||||||
|
roomConfigurations: defaultRooms,
|
||||||
|
})
|
||||||
|
const [selectedPackages, setSelectedPackages] = useState<RoomPackageCodes[]>(
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
|
||||||
|
const handleFilter = useCallback(
|
||||||
|
(filter: Record<RoomPackageCodeEnum, boolean | undefined>) => {
|
||||||
|
const filteredPackages = Object.keys(filter).filter(
|
||||||
|
(key) => filter[key as RoomPackageCodeEnum]
|
||||||
|
) as RoomPackageCodeEnum[]
|
||||||
|
|
||||||
|
setSelectedPackages(filteredPackages)
|
||||||
|
|
||||||
|
if (filteredPackages.length === 0) {
|
||||||
|
setRooms({
|
||||||
|
...roomsAvailability,
|
||||||
|
roomConfigurations: defaultRooms,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const filteredRooms = roomsAvailability.roomConfigurations.filter(
|
||||||
|
(room) =>
|
||||||
|
filteredPackages.every((filteredPackage) =>
|
||||||
|
room.features.some((feature) => feature.code === filteredPackage)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
setRooms({ ...roomsAvailability, roomConfigurations: filteredRooms })
|
||||||
|
},
|
||||||
|
[roomsAvailability, defaultRooms]
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.content}>
|
||||||
|
<RoomFilter
|
||||||
|
numberOfRooms={rooms.roomConfigurations.length}
|
||||||
|
onFilter={handleFilter}
|
||||||
|
filterOptions={packages}
|
||||||
|
/>
|
||||||
|
<RoomSelection
|
||||||
|
roomsAvailability={rooms}
|
||||||
|
roomCategories={roomCategories}
|
||||||
|
user={user}
|
||||||
|
packages={packages}
|
||||||
|
selectedPackages={selectedPackages}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
.content {
|
||||||
|
max-width: var(--max-width);
|
||||||
|
margin: 0 auto;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--Spacing-x2);
|
||||||
|
padding: var(--Spacing-x2);
|
||||||
|
}
|
||||||
19
components/HotelReservation/SelectRate/utils.ts
Normal file
19
components/HotelReservation/SelectRate/utils.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import { AllergyIcon, PetsIcon, WheelchairIcon } from "@/components/Icons"
|
||||||
|
|
||||||
|
import {
|
||||||
|
RoomPackageCodeEnum,
|
||||||
|
type RoomPackageCodes,
|
||||||
|
} from "@/types/components/hotelReservation/selectRate/roomFilter"
|
||||||
|
|
||||||
|
export function getIconForFeatureCode(featureCode: RoomPackageCodes) {
|
||||||
|
switch (featureCode) {
|
||||||
|
case RoomPackageCodeEnum.ACCESSIBILITY_ROOM:
|
||||||
|
return WheelchairIcon
|
||||||
|
case RoomPackageCodeEnum.ALLERGY_ROOM:
|
||||||
|
return AllergyIcon
|
||||||
|
case RoomPackageCodeEnum.PET_ROOM:
|
||||||
|
return PetsIcon
|
||||||
|
default:
|
||||||
|
return PetsIcon
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -14,23 +14,10 @@ export default function AcIcon({ className, color, ...props }: IconProps) {
|
|||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<mask
|
|
||||||
style={{ maskType: "alpha" }}
|
|
||||||
id="mask0_69_3450"
|
|
||||||
maskUnits="userSpaceOnUse"
|
|
||||||
x="0"
|
|
||||||
y="0"
|
|
||||||
width="24"
|
|
||||||
height="24"
|
|
||||||
>
|
|
||||||
<rect width="24" height="24" fill="#D9D9D9" />
|
|
||||||
</mask>
|
|
||||||
<g mask="url(#mask0_69_3450)">
|
|
||||||
<path
|
<path
|
||||||
d="M3.19995 20.8V6.13458C3.19995 5.31983 3.48554 4.62703 4.05673 4.0562C4.62791 3.48537 5.32148 3.19995 6.13745 3.19995H9.99995C10.8159 3.19995 11.5095 3.48513 12.0807 4.0555C12.6519 4.62587 12.9375 5.31846 12.9375 6.13328V20.8H3.19995ZM9.93745 15.0625H11.0625V9.93745H9.93745V15.0625ZM5.07495 18.925H11.0625V16.9375H9.93745C9.42182 16.9375 8.98041 16.7539 8.61323 16.3867C8.24604 16.0195 8.06245 15.5781 8.06245 15.0625V9.93745C8.06245 9.42182 8.24604 8.98041 8.61323 8.61323C8.98041 8.24604 9.42182 8.06245 9.93745 8.06245H11.0625V6.13745C11.0625 5.83642 10.9606 5.58408 10.757 5.38043C10.5533 5.17678 10.301 5.07495 9.99995 5.07495H6.13745C5.83642 5.07495 5.58408 5.17678 5.38043 5.38043C5.17678 5.58408 5.07495 5.83642 5.07495 6.13745V18.925ZM17.0875 13.5125C16.6671 13.5125 16.2549 13.4565 15.8507 13.3445C15.4465 13.2325 15.0505 13.0885 14.6625 12.9125L15.25 11.1375C15.5748 11.2803 15.8956 11.3994 16.2124 11.4946C16.5291 11.5898 16.8291 11.6375 17.1125 11.6375C17.318 11.6375 17.5236 11.6041 17.7291 11.5375C17.9347 11.4708 18.15 11.3666 18.375 11.225C18.7686 10.9465 19.1622 10.7581 19.5558 10.6599C19.9494 10.5616 20.3266 10.5125 20.6875 10.5125C21.0881 10.5125 21.5007 10.5666 21.9254 10.675C22.3501 10.7833 22.7541 10.9208 23.1375 11.0875L22.55 12.875C22.2333 12.7666 21.9062 12.6583 21.5687 12.55C21.2312 12.4416 20.9391 12.3875 20.6923 12.3875C20.4807 12.3875 20.252 12.427 20.0062 12.5062C19.7604 12.5854 19.5 12.7166 19.225 12.9C18.8833 13.1333 18.536 13.2937 18.1831 13.3812C17.8302 13.4687 17.465 13.5125 17.0875 13.5125ZM17.1125 9.61245C16.6958 9.61245 16.2791 9.5562 15.8625 9.4437C15.4458 9.3312 15.0458 9.18908 14.6625 9.01733L15.25 7.23745C15.675 7.42078 16.0333 7.54995 16.325 7.62495C16.6166 7.69995 16.8791 7.73745 17.1125 7.73745C17.318 7.73745 17.5236 7.7062 17.7291 7.6437C17.9347 7.5812 18.15 7.47495 18.375 7.32495C18.785 7.0465 19.1827 6.85814 19.5681 6.75988C19.9535 6.66159 20.3266 6.61245 20.6875 6.61245C21.0902 6.61245 21.493 6.66453 21.8958 6.7687C22.2986 6.87287 22.7125 7.01245 23.1375 7.18745L22.55 8.97495C22.125 8.83328 21.7666 8.71662 21.475 8.62495C21.1833 8.53328 20.9208 8.48745 20.6875 8.48745C20.4627 8.48745 20.2336 8.52078 20.0001 8.58745C19.7667 8.65412 19.5083 8.79162 19.225 8.99995C18.9333 9.20828 18.6027 9.36245 18.2332 9.46245C17.8637 9.56245 17.4902 9.61245 17.1125 9.61245ZM17.1147 17.4125C16.6882 17.4125 16.2708 17.3562 15.8625 17.2437C15.4541 17.1312 15.0541 16.9875 14.6625 16.8125L15.25 15.0375C15.6 15.1875 15.9333 15.3083 16.25 15.4C16.5666 15.4916 16.8541 15.5375 17.1125 15.5375C17.318 15.5375 17.5236 15.5062 17.7291 15.4437C17.9347 15.3812 18.15 15.275 18.375 15.125C18.7583 14.8666 19.1604 14.6833 19.5812 14.575C20.002 14.4666 20.3791 14.4125 20.7125 14.4125C21.1166 14.4125 21.5289 14.4684 21.9492 14.5802C22.3695 14.692 22.7656 14.8277 23.1375 14.9875L22.55 16.775C22.125 16.6333 21.7625 16.5166 21.4625 16.425C21.1625 16.3333 20.9041 16.2875 20.6875 16.2875C20.4541 16.2875 20.2125 16.327 19.9625 16.4062C19.7125 16.4854 19.4666 16.6166 19.225 16.8C18.95 16.9916 18.6279 17.1416 18.2588 17.25C17.8898 17.3583 17.5084 17.4125 17.1147 17.4125Z"
|
d="M3.19995 20.8V6.13458C3.19995 5.31983 3.48554 4.62703 4.05673 4.0562C4.62791 3.48537 5.32148 3.19995 6.13745 3.19995H9.99995C10.8159 3.19995 11.5095 3.48513 12.0807 4.0555C12.6519 4.62587 12.9375 5.31846 12.9375 6.13328V20.8H3.19995ZM9.93745 15.0625H11.0625V9.93745H9.93745V15.0625ZM5.07495 18.925H11.0625V16.9375H9.93745C9.42182 16.9375 8.98041 16.7539 8.61323 16.3867C8.24604 16.0195 8.06245 15.5781 8.06245 15.0625V9.93745C8.06245 9.42182 8.24604 8.98041 8.61323 8.61323C8.98041 8.24604 9.42182 8.06245 9.93745 8.06245H11.0625V6.13745C11.0625 5.83642 10.9606 5.58408 10.757 5.38043C10.5533 5.17678 10.301 5.07495 9.99995 5.07495H6.13745C5.83642 5.07495 5.58408 5.17678 5.38043 5.38043C5.17678 5.58408 5.07495 5.83642 5.07495 6.13745V18.925ZM17.0875 13.5125C16.6671 13.5125 16.2549 13.4565 15.8507 13.3445C15.4465 13.2325 15.0505 13.0885 14.6625 12.9125L15.25 11.1375C15.5748 11.2803 15.8956 11.3994 16.2124 11.4946C16.5291 11.5898 16.8291 11.6375 17.1125 11.6375C17.318 11.6375 17.5236 11.6041 17.7291 11.5375C17.9347 11.4708 18.15 11.3666 18.375 11.225C18.7686 10.9465 19.1622 10.7581 19.5558 10.6599C19.9494 10.5616 20.3266 10.5125 20.6875 10.5125C21.0881 10.5125 21.5007 10.5666 21.9254 10.675C22.3501 10.7833 22.7541 10.9208 23.1375 11.0875L22.55 12.875C22.2333 12.7666 21.9062 12.6583 21.5687 12.55C21.2312 12.4416 20.9391 12.3875 20.6923 12.3875C20.4807 12.3875 20.252 12.427 20.0062 12.5062C19.7604 12.5854 19.5 12.7166 19.225 12.9C18.8833 13.1333 18.536 13.2937 18.1831 13.3812C17.8302 13.4687 17.465 13.5125 17.0875 13.5125ZM17.1125 9.61245C16.6958 9.61245 16.2791 9.5562 15.8625 9.4437C15.4458 9.3312 15.0458 9.18908 14.6625 9.01733L15.25 7.23745C15.675 7.42078 16.0333 7.54995 16.325 7.62495C16.6166 7.69995 16.8791 7.73745 17.1125 7.73745C17.318 7.73745 17.5236 7.7062 17.7291 7.6437C17.9347 7.5812 18.15 7.47495 18.375 7.32495C18.785 7.0465 19.1827 6.85814 19.5681 6.75988C19.9535 6.66159 20.3266 6.61245 20.6875 6.61245C21.0902 6.61245 21.493 6.66453 21.8958 6.7687C22.2986 6.87287 22.7125 7.01245 23.1375 7.18745L22.55 8.97495C22.125 8.83328 21.7666 8.71662 21.475 8.62495C21.1833 8.53328 20.9208 8.48745 20.6875 8.48745C20.4627 8.48745 20.2336 8.52078 20.0001 8.58745C19.7667 8.65412 19.5083 8.79162 19.225 8.99995C18.9333 9.20828 18.6027 9.36245 18.2332 9.46245C17.8637 9.56245 17.4902 9.61245 17.1125 9.61245ZM17.1147 17.4125C16.6882 17.4125 16.2708 17.3562 15.8625 17.2437C15.4541 17.1312 15.0541 16.9875 14.6625 16.8125L15.25 15.0375C15.6 15.1875 15.9333 15.3083 16.25 15.4C16.5666 15.4916 16.8541 15.5375 17.1125 15.5375C17.318 15.5375 17.5236 15.5062 17.7291 15.4437C17.9347 15.3812 18.15 15.275 18.375 15.125C18.7583 14.8666 19.1604 14.6833 19.5812 14.575C20.002 14.4666 20.3791 14.4125 20.7125 14.4125C21.1166 14.4125 21.5289 14.4684 21.9492 14.5802C22.3695 14.692 22.7656 14.8277 23.1375 14.9875L22.55 16.775C22.125 16.6333 21.7625 16.5166 21.4625 16.425C21.1625 16.3333 20.9041 16.2875 20.6875 16.2875C20.4541 16.2875 20.2125 16.327 19.9625 16.4062C19.7125 16.4854 19.4666 16.6166 19.225 16.8C18.95 16.9916 18.6279 17.1416 18.2588 17.25C17.8898 17.3583 17.5084 17.4125 17.1147 17.4125Z"
|
||||||
fill="#26201E"
|
fill="#26201E"
|
||||||
/>
|
/>
|
||||||
</g>
|
|
||||||
</svg>
|
</svg>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,23 +18,10 @@ export default function AccesoriesIcon({
|
|||||||
fill="none"
|
fill="none"
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<mask
|
|
||||||
id="mask0_4039_3291"
|
|
||||||
style={{ maskType: "alpha" }}
|
|
||||||
maskUnits="userSpaceOnUse"
|
|
||||||
x="0"
|
|
||||||
y="0"
|
|
||||||
width="24"
|
|
||||||
height="24"
|
|
||||||
>
|
|
||||||
<rect width="24" height="24" fill="#D9D9D9" />
|
|
||||||
</mask>
|
|
||||||
<g mask="url(#mask0_4039_3291)">
|
|
||||||
<path
|
<path
|
||||||
d="M6.40085 22C5.55984 22 4.80868 21.4739 4.52127 20.6835L1.56362 12.5499C1.23633 11.6499 1.593 10.6442 2.41421 10.1515L6 8V3C6 2.44772 6.44772 2 7 2H9C9.55229 2 10 2.44772 10 3V8L13.5858 10.1515C14.407 10.6442 14.7637 11.6499 14.4364 12.5499L11.4787 20.6835C11.1913 21.4739 10.4402 22 9.59915 22H6.40085ZM17 22C16.7167 22 16.4792 21.9042 16.2875 21.7125C16.0958 21.5208 16 21.2833 16 21C16 20.7167 16.0958 20.4792 16.2875 20.2875C16.4792 20.0958 16.7167 20 17 20H20V18H17C16.7167 18 16.4792 17.9042 16.2875 17.7125C16.0958 17.5208 16 17.2833 16 17C16 16.7167 16.0958 16.4792 16.2875 16.2875C16.4792 16.0958 16.7167 16 17 16H20V14H17C16.7167 14 16.4792 13.9042 16.2875 13.7125C16.0958 13.5208 16 13.2833 16 13C16 12.7167 16.0958 12.4792 16.2875 12.2875C16.4792 12.0958 16.7167 12 17 12H20V10H17C16.7167 10 16.4792 9.90417 16.2875 9.7125C16.0958 9.52083 16 9.28333 16 9C16 8.71667 16.0958 8.47917 16.2875 8.2875C16.4792 8.09583 16.7167 8 17 8H20V6H17C16.7167 6 16.4792 5.90417 16.2875 5.7125C16.0958 5.52083 16 5.28333 16 5C16 4.71667 16.0958 4.47917 16.2875 4.2875C16.4792 4.09583 16.7167 4 17 4H21C21.55 4 22.0208 4.19583 22.4125 4.5875C22.8042 4.97917 23 5.45 23 6V20C23 20.55 22.8042 21.0208 22.4125 21.4125C22.0208 21.8042 21.55 22 21 22H17ZM6.16123 19.3404C6.30454 19.7363 6.68048 20 7.10153 20H8.89847C9.31952 20 9.69546 19.7363 9.83877 19.3404L12.2691 12.6261C12.4322 12.1755 12.2527 11.6726 11.8412 11.427L9.45 10H6.55L4.15876 11.427C3.74728 11.6726 3.56783 12.1755 3.73092 12.6261L6.16123 19.3404Z"
|
d="M6.40085 22C5.55984 22 4.80868 21.4739 4.52127 20.6835L1.56362 12.5499C1.23633 11.6499 1.593 10.6442 2.41421 10.1515L6 8V3C6 2.44772 6.44772 2 7 2H9C9.55229 2 10 2.44772 10 3V8L13.5858 10.1515C14.407 10.6442 14.7637 11.6499 14.4364 12.5499L11.4787 20.6835C11.1913 21.4739 10.4402 22 9.59915 22H6.40085ZM17 22C16.7167 22 16.4792 21.9042 16.2875 21.7125C16.0958 21.5208 16 21.2833 16 21C16 20.7167 16.0958 20.4792 16.2875 20.2875C16.4792 20.0958 16.7167 20 17 20H20V18H17C16.7167 18 16.4792 17.9042 16.2875 17.7125C16.0958 17.5208 16 17.2833 16 17C16 16.7167 16.0958 16.4792 16.2875 16.2875C16.4792 16.0958 16.7167 16 17 16H20V14H17C16.7167 14 16.4792 13.9042 16.2875 13.7125C16.0958 13.5208 16 13.2833 16 13C16 12.7167 16.0958 12.4792 16.2875 12.2875C16.4792 12.0958 16.7167 12 17 12H20V10H17C16.7167 10 16.4792 9.90417 16.2875 9.7125C16.0958 9.52083 16 9.28333 16 9C16 8.71667 16.0958 8.47917 16.2875 8.2875C16.4792 8.09583 16.7167 8 17 8H20V6H17C16.7167 6 16.4792 5.90417 16.2875 5.7125C16.0958 5.52083 16 5.28333 16 5C16 4.71667 16.0958 4.47917 16.2875 4.2875C16.4792 4.09583 16.7167 4 17 4H21C21.55 4 22.0208 4.19583 22.4125 4.5875C22.8042 4.97917 23 5.45 23 6V20C23 20.55 22.8042 21.0208 22.4125 21.4125C22.0208 21.8042 21.55 22 21 22H17ZM6.16123 19.3404C6.30454 19.7363 6.68048 20 7.10153 20H8.89847C9.31952 20 9.69546 19.7363 9.83877 19.3404L12.2691 12.6261C12.4322 12.1755 12.2527 11.6726 11.8412 11.427L9.45 10H6.55L4.15876 11.427C3.74728 11.6726 3.56783 12.1755 3.73092 12.6261L6.16123 19.3404Z"
|
||||||
fill="#1C1B1F"
|
fill="#1C1B1F"
|
||||||
/>
|
/>
|
||||||
</g>
|
|
||||||
</svg>
|
</svg>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,23 +18,10 @@ export default function AccessibilityIcon({
|
|||||||
fill="none"
|
fill="none"
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<mask
|
|
||||||
id="mask0_554_11936"
|
|
||||||
style={{ maskType: "alpha" }}
|
|
||||||
maskUnits="userSpaceOnUse"
|
|
||||||
x="0"
|
|
||||||
y="0"
|
|
||||||
width="24"
|
|
||||||
height="24"
|
|
||||||
>
|
|
||||||
<rect width="24" height="24" fill="#D9D9D9" />
|
|
||||||
</mask>
|
|
||||||
<g mask="url(#mask0_554_11936)">
|
|
||||||
<path
|
<path
|
||||||
d="M12.0001 6.0499C11.4584 6.0499 10.998 5.86032 10.6188 5.48115C10.2397 5.10199 10.0501 4.64157 10.0501 4.0999C10.0501 3.55824 10.2397 3.09782 10.6188 2.71865C10.998 2.33949 11.4584 2.1499 12.0001 2.1499C12.5418 2.1499 13.0022 2.33949 13.3813 2.71865C13.7605 3.09782 13.9501 3.55824 13.9501 4.0999C13.9501 4.64157 13.7605 5.10199 13.3813 5.48115C13.0022 5.86032 12.5418 6.0499 12.0001 6.0499ZM9.1251 20.8624V8.9749H4.1626C3.90426 8.9749 3.68343 8.88324 3.5001 8.6999C3.31676 8.51657 3.2251 8.29574 3.2251 8.0374C3.2251 7.77907 3.31676 7.55824 3.5001 7.3749C3.68343 7.19157 3.90426 7.0999 4.1626 7.0999H19.8376C20.0959 7.0999 20.3168 7.19157 20.5001 7.3749C20.6834 7.55824 20.7751 7.77907 20.7751 8.0374C20.7751 8.29574 20.6834 8.51657 20.5001 8.6999C20.3168 8.88324 20.0959 8.9749 19.8376 8.9749H14.8751V20.8624C14.8751 21.1207 14.7834 21.3416 14.6001 21.5249C14.4168 21.7082 14.1959 21.7999 13.9376 21.7999C13.6793 21.7999 13.4584 21.7082 13.2751 21.5249C13.0918 21.3416 13.0001 21.1207 13.0001 20.8624V15.9249H11.0001V20.8624C11.0001 21.1207 10.9084 21.3416 10.7251 21.5249C10.5418 21.7082 10.3209 21.7999 10.0626 21.7999C9.80427 21.7999 9.58343 21.7082 9.4001 21.5249C9.21676 21.3416 9.1251 21.1207 9.1251 20.8624Z"
|
d="M12.0001 6.0499C11.4584 6.0499 10.998 5.86032 10.6188 5.48115C10.2397 5.10199 10.0501 4.64157 10.0501 4.0999C10.0501 3.55824 10.2397 3.09782 10.6188 2.71865C10.998 2.33949 11.4584 2.1499 12.0001 2.1499C12.5418 2.1499 13.0022 2.33949 13.3813 2.71865C13.7605 3.09782 13.9501 3.55824 13.9501 4.0999C13.9501 4.64157 13.7605 5.10199 13.3813 5.48115C13.0022 5.86032 12.5418 6.0499 12.0001 6.0499ZM9.1251 20.8624V8.9749H4.1626C3.90426 8.9749 3.68343 8.88324 3.5001 8.6999C3.31676 8.51657 3.2251 8.29574 3.2251 8.0374C3.2251 7.77907 3.31676 7.55824 3.5001 7.3749C3.68343 7.19157 3.90426 7.0999 4.1626 7.0999H19.8376C20.0959 7.0999 20.3168 7.19157 20.5001 7.3749C20.6834 7.55824 20.7751 7.77907 20.7751 8.0374C20.7751 8.29574 20.6834 8.51657 20.5001 8.6999C20.3168 8.88324 20.0959 8.9749 19.8376 8.9749H14.8751V20.8624C14.8751 21.1207 14.7834 21.3416 14.6001 21.5249C14.4168 21.7082 14.1959 21.7999 13.9376 21.7999C13.6793 21.7999 13.4584 21.7082 13.2751 21.5249C13.0918 21.3416 13.0001 21.1207 13.0001 20.8624V15.9249H11.0001V20.8624C11.0001 21.1207 10.9084 21.3416 10.7251 21.5249C10.5418 21.7082 10.3209 21.7999 10.0626 21.7999C9.80427 21.7999 9.58343 21.7082 9.4001 21.5249C9.21676 21.3416 9.1251 21.1207 9.1251 20.8624Z"
|
||||||
fill="#4D001B"
|
fill="#4D001B"
|
||||||
/>
|
/>
|
||||||
</g>
|
|
||||||
</svg>
|
</svg>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,23 +14,10 @@ export default function AirIcon({ className, color, ...props }: IconProps) {
|
|||||||
fill="none"
|
fill="none"
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<mask
|
|
||||||
id="mask0_69_3423"
|
|
||||||
style={{ maskType: "alpha" }}
|
|
||||||
maskUnits="userSpaceOnUse"
|
|
||||||
x="0"
|
|
||||||
y="0"
|
|
||||||
width="24"
|
|
||||||
height="24"
|
|
||||||
>
|
|
||||||
<rect width="24" height="24" fill="#D9D9D9" />
|
|
||||||
</mask>
|
|
||||||
<g mask="url(#mask0_69_3423)">
|
|
||||||
<path
|
<path
|
||||||
d="M11.5125 19.8C10.9884 19.8 10.5011 19.6708 10.0507 19.4125C9.60022 19.1541 9.24167 18.8 8.975 18.35C8.8 18.0416 8.79453 17.7312 8.9586 17.4187C9.12267 17.1062 9.37657 16.95 9.7203 16.95C9.92343 16.95 10.1042 17.0104 10.2625 17.1312C10.4208 17.252 10.5583 17.3916 10.675 17.55C10.764 17.675 10.8842 17.7687 11.0355 17.8312C11.1868 17.8937 11.3458 17.925 11.5125 17.925C11.7958 17.925 12.0375 17.825 12.2375 17.625C12.4375 17.425 12.5375 17.1833 12.5375 16.9C12.5375 16.6166 12.4375 16.375 12.2375 16.175C12.0375 15.975 11.7976 15.875 11.5178 15.875H3.125C2.86667 15.875 2.64583 15.7833 2.4625 15.6C2.27917 15.4166 2.1875 15.1958 2.1875 14.9375C2.1875 14.6791 2.27917 14.4583 2.4625 14.275C2.64583 14.0916 2.86667 14 3.125 14H11.5125C12.318 14 13.0028 14.2822 13.5667 14.8467C14.1306 15.4111 14.4125 16.0965 14.4125 16.9029C14.4125 17.7093 14.1306 18.3937 13.5667 18.9562C13.0028 19.5187 12.318 19.8 11.5125 19.8ZM3.125 9.99995C2.86667 9.99995 2.64583 9.90828 2.4625 9.72495C2.27917 9.54162 2.1875 9.32078 2.1875 9.06245C2.1875 8.80412 2.27917 8.58328 2.4625 8.39995C2.64583 8.21662 2.86667 8.12495 3.125 8.12495H15.4375C15.8611 8.12495 16.2212 7.97654 16.5177 7.67973C16.8142 7.38291 16.9625 7.02249 16.9625 6.59848C16.9625 6.17446 16.8148 5.81453 16.5193 5.5187C16.2237 5.22287 15.8649 5.07495 15.4428 5.07495C15.1726 5.07495 14.9208 5.13593 14.6875 5.25788C14.4542 5.37983 14.2708 5.55463 14.1375 5.78228C14.0208 5.97739 13.8816 6.15412 13.7199 6.31245C13.5581 6.47078 13.364 6.54995 13.1375 6.54995C12.8208 6.54995 12.5688 6.42495 12.3813 6.17495C12.1938 5.92495 12.1542 5.65828 12.2625 5.37495C12.4958 4.70828 12.9038 4.17912 13.4863 3.78745C14.0689 3.39578 14.7193 3.19995 15.4375 3.19995C16.3765 3.19995 17.178 3.53213 17.8418 4.19648C18.5056 4.86083 18.8375 5.66291 18.8375 6.60273C18.8375 7.54254 18.5056 8.3437 17.8418 9.0062C17.178 9.6687 16.3765 9.99995 15.4375 9.99995H3.125ZM19.85 17.5625C19.525 17.7041 19.2125 17.6864 18.9125 17.5094C18.6125 17.3323 18.4625 17.0745 18.4625 16.7358C18.4625 16.5202 18.5375 16.3354 18.6875 16.1812C18.8375 16.027 19.0083 15.9 19.2 15.8C19.4417 15.6666 19.625 15.478 19.75 15.2341C19.875 14.9901 19.9375 14.7287 19.9375 14.45C19.9375 14.0263 19.7892 13.6663 19.4927 13.3698C19.1962 13.0732 18.8361 12.925 18.4125 12.925H3.125C2.86667 12.925 2.64583 12.8333 2.4625 12.65C2.27917 12.4666 2.1875 12.2458 2.1875 11.9875C2.1875 11.7291 2.27917 11.5083 2.4625 11.325C2.64583 11.1416 2.86667 11.05 3.125 11.05H18.4125C19.3516 11.05 20.153 11.3813 20.8168 12.044C21.4806 12.7067 21.8125 13.5067 21.8125 14.4442C21.8125 15.123 21.637 15.7433 21.286 16.3049C20.935 16.8665 20.4563 17.2857 19.85 17.5625Z"
|
d="M11.5125 19.8C10.9884 19.8 10.5011 19.6708 10.0507 19.4125C9.60022 19.1541 9.24167 18.8 8.975 18.35C8.8 18.0416 8.79453 17.7312 8.9586 17.4187C9.12267 17.1062 9.37657 16.95 9.7203 16.95C9.92343 16.95 10.1042 17.0104 10.2625 17.1312C10.4208 17.252 10.5583 17.3916 10.675 17.55C10.764 17.675 10.8842 17.7687 11.0355 17.8312C11.1868 17.8937 11.3458 17.925 11.5125 17.925C11.7958 17.925 12.0375 17.825 12.2375 17.625C12.4375 17.425 12.5375 17.1833 12.5375 16.9C12.5375 16.6166 12.4375 16.375 12.2375 16.175C12.0375 15.975 11.7976 15.875 11.5178 15.875H3.125C2.86667 15.875 2.64583 15.7833 2.4625 15.6C2.27917 15.4166 2.1875 15.1958 2.1875 14.9375C2.1875 14.6791 2.27917 14.4583 2.4625 14.275C2.64583 14.0916 2.86667 14 3.125 14H11.5125C12.318 14 13.0028 14.2822 13.5667 14.8467C14.1306 15.4111 14.4125 16.0965 14.4125 16.9029C14.4125 17.7093 14.1306 18.3937 13.5667 18.9562C13.0028 19.5187 12.318 19.8 11.5125 19.8ZM3.125 9.99995C2.86667 9.99995 2.64583 9.90828 2.4625 9.72495C2.27917 9.54162 2.1875 9.32078 2.1875 9.06245C2.1875 8.80412 2.27917 8.58328 2.4625 8.39995C2.64583 8.21662 2.86667 8.12495 3.125 8.12495H15.4375C15.8611 8.12495 16.2212 7.97654 16.5177 7.67973C16.8142 7.38291 16.9625 7.02249 16.9625 6.59848C16.9625 6.17446 16.8148 5.81453 16.5193 5.5187C16.2237 5.22287 15.8649 5.07495 15.4428 5.07495C15.1726 5.07495 14.9208 5.13593 14.6875 5.25788C14.4542 5.37983 14.2708 5.55463 14.1375 5.78228C14.0208 5.97739 13.8816 6.15412 13.7199 6.31245C13.5581 6.47078 13.364 6.54995 13.1375 6.54995C12.8208 6.54995 12.5688 6.42495 12.3813 6.17495C12.1938 5.92495 12.1542 5.65828 12.2625 5.37495C12.4958 4.70828 12.9038 4.17912 13.4863 3.78745C14.0689 3.39578 14.7193 3.19995 15.4375 3.19995C16.3765 3.19995 17.178 3.53213 17.8418 4.19648C18.5056 4.86083 18.8375 5.66291 18.8375 6.60273C18.8375 7.54254 18.5056 8.3437 17.8418 9.0062C17.178 9.6687 16.3765 9.99995 15.4375 9.99995H3.125ZM19.85 17.5625C19.525 17.7041 19.2125 17.6864 18.9125 17.5094C18.6125 17.3323 18.4625 17.0745 18.4625 16.7358C18.4625 16.5202 18.5375 16.3354 18.6875 16.1812C18.8375 16.027 19.0083 15.9 19.2 15.8C19.4417 15.6666 19.625 15.478 19.75 15.2341C19.875 14.9901 19.9375 14.7287 19.9375 14.45C19.9375 14.0263 19.7892 13.6663 19.4927 13.3698C19.1962 13.0732 18.8361 12.925 18.4125 12.925H3.125C2.86667 12.925 2.64583 12.8333 2.4625 12.65C2.27917 12.4666 2.1875 12.2458 2.1875 11.9875C2.1875 11.7291 2.27917 11.5083 2.4625 11.325C2.64583 11.1416 2.86667 11.05 3.125 11.05H18.4125C19.3516 11.05 20.153 11.3813 20.8168 12.044C21.4806 12.7067 21.8125 13.5067 21.8125 14.4442C21.8125 15.123 21.637 15.7433 21.286 16.3049C20.935 16.8665 20.4563 17.2857 19.85 17.5625Z"
|
||||||
fill="#26201E"
|
fill="#26201E"
|
||||||
/>
|
/>
|
||||||
</g>
|
|
||||||
</svg>
|
</svg>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,23 +18,10 @@ export default function AirplaneIcon({
|
|||||||
fill="none"
|
fill="none"
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<mask
|
|
||||||
id="mask0_4597_1552"
|
|
||||||
style={{ maskType: "alpha" }}
|
|
||||||
maskUnits="userSpaceOnUse"
|
|
||||||
x="0"
|
|
||||||
y="0"
|
|
||||||
width="24"
|
|
||||||
height="24"
|
|
||||||
>
|
|
||||||
<rect width="24" height="24" fill="#D9D9D9" />
|
|
||||||
</mask>
|
|
||||||
<g mask="url(#mask0_4597_1552)">
|
|
||||||
<path
|
<path
|
||||||
d="M9.9251 21.125L7.4501 16.525L2.8501 14.05L4.6251 12.3L8.2501 12.925L10.8001 10.375L2.8751 7L4.9751 4.85L14.6001 6.55L17.7001 3.45C18.0834 3.06667 18.5584 2.875 19.1251 2.875C19.6918 2.875 20.1668 3.06667 20.5501 3.45C20.9334 3.83333 21.1251 4.30417 21.1251 4.8625C21.1251 5.42083 20.9334 5.89167 20.5501 6.275L17.4251 9.4L19.1251 19L17.0001 21.125L13.6001 13.2L11.0501 15.75L11.7001 19.35L9.9251 21.125Z"
|
d="M9.9251 21.125L7.4501 16.525L2.8501 14.05L4.6251 12.3L8.2501 12.925L10.8001 10.375L2.8751 7L4.9751 4.85L14.6001 6.55L17.7001 3.45C18.0834 3.06667 18.5584 2.875 19.1251 2.875C19.6918 2.875 20.1668 3.06667 20.5501 3.45C20.9334 3.83333 21.1251 4.30417 21.1251 4.8625C21.1251 5.42083 20.9334 5.89167 20.5501 6.275L17.4251 9.4L19.1251 19L17.0001 21.125L13.6001 13.2L11.0501 15.75L11.7001 19.35L9.9251 21.125Z"
|
||||||
fill="#26201E"
|
fill="#26201E"
|
||||||
/>
|
/>
|
||||||
</g>
|
|
||||||
</svg>
|
</svg>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
36
components/Icons/Allergy.tsx
Normal file
36
components/Icons/Allergy.tsx
Normal file
File diff suppressed because one or more lines are too long
@@ -20,23 +20,10 @@ export default function ArrowRightIcon({
|
|||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<mask
|
|
||||||
height="25"
|
|
||||||
id="mask0_4176_4181"
|
|
||||||
maskUnits="userSpaceOnUse"
|
|
||||||
style={{ maskType: "alpha" }}
|
|
||||||
width="24"
|
|
||||||
x="0"
|
|
||||||
y="0"
|
|
||||||
>
|
|
||||||
<rect y="0.632812" width="24" height="24" fill="#D9D9D9" />
|
|
||||||
</mask>
|
|
||||||
<g mask="url(#mask0_4176_4181)">
|
|
||||||
<path
|
<path
|
||||||
d="M16.15 13.5703H5.1875C4.92917 13.5703 4.70833 13.4786 4.525 13.2953C4.34167 13.112 4.25 12.8911 4.25 12.6328C4.25 12.3745 4.34167 12.1536 4.525 11.9703C4.70833 11.787 4.92917 11.6953 5.1875 11.6953H16.15L11.325 6.8703C11.1333 6.67863 11.0396 6.4578 11.0438 6.2078C11.0479 5.9578 11.1458 5.73697 11.3375 5.5453C11.5292 5.36197 11.75 5.26822 12 5.26405C12.25 5.25988 12.4708 5.35363 12.6625 5.5453L19.0875 11.9703C19.1792 12.062 19.2479 12.164 19.2937 12.2765C19.3396 12.389 19.3625 12.5078 19.3625 12.6328C19.3625 12.7578 19.3396 12.8765 19.2937 12.989C19.2479 13.1016 19.1792 13.2036 19.0875 13.2953L12.6625 19.7203C12.4792 19.9036 12.2604 19.9953 12.0062 19.9953C11.7521 19.9953 11.5292 19.9036 11.3375 19.7203C11.1458 19.5286 11.05 19.3057 11.05 19.0516C11.05 18.7974 11.1458 18.5745 11.3375 18.3828L16.15 13.5703Z"
|
d="M16.15 13.5703H5.1875C4.92917 13.5703 4.70833 13.4786 4.525 13.2953C4.34167 13.112 4.25 12.8911 4.25 12.6328C4.25 12.3745 4.34167 12.1536 4.525 11.9703C4.70833 11.787 4.92917 11.6953 5.1875 11.6953H16.15L11.325 6.8703C11.1333 6.67863 11.0396 6.4578 11.0438 6.2078C11.0479 5.9578 11.1458 5.73697 11.3375 5.5453C11.5292 5.36197 11.75 5.26822 12 5.26405C12.25 5.25988 12.4708 5.35363 12.6625 5.5453L19.0875 11.9703C19.1792 12.062 19.2479 12.164 19.2937 12.2765C19.3396 12.389 19.3625 12.5078 19.3625 12.6328C19.3625 12.7578 19.3396 12.8765 19.2937 12.989C19.2479 13.1016 19.1792 13.2036 19.0875 13.2953L12.6625 19.7203C12.4792 19.9036 12.2604 19.9953 12.0062 19.9953C11.7521 19.9953 11.5292 19.9036 11.3375 19.7203C11.1458 19.5286 11.05 19.3057 11.05 19.0516C11.05 18.7974 11.1458 18.5745 11.3375 18.3828L16.15 13.5703Z"
|
||||||
fill="#4D001B"
|
fill="#4D001B"
|
||||||
/>
|
/>
|
||||||
</g>
|
|
||||||
</svg>
|
</svg>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,23 +14,10 @@ export default function BarIcon({ className, color, ...props }: IconProps) {
|
|||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<mask
|
|
||||||
id="mask0_554_11961"
|
|
||||||
style={{ maskType: "alpha" }}
|
|
||||||
maskUnits="userSpaceOnUse"
|
|
||||||
x="0"
|
|
||||||
y="0"
|
|
||||||
width="24"
|
|
||||||
height="24"
|
|
||||||
>
|
|
||||||
<rect width="24" height="24" fill="#D9D9D9" />
|
|
||||||
</mask>
|
|
||||||
<g mask="url(#mask0_554_11961)">
|
|
||||||
<path
|
<path
|
||||||
d="M11.0625 18.9501V13.8751L3.68755 5.5626C3.57088 5.42926 3.47713 5.28551 3.4063 5.13135C3.33547 4.97718 3.30005 4.8126 3.30005 4.6376C3.30005 4.2376 3.43963 3.90218 3.7188 3.63135C3.99797 3.36051 4.33755 3.2251 4.73755 3.2251H19.2625C19.6625 3.2251 20.0021 3.36051 20.2813 3.63135C20.5605 3.90218 20.7 4.2376 20.7 4.6376C20.7 4.8126 20.6646 4.97718 20.5938 5.13135C20.523 5.28551 20.4292 5.42926 20.3125 5.5626L12.9375 13.8751V18.9501H16.875C17.1334 18.9501 17.3542 19.0418 17.5375 19.2251C17.7209 19.4084 17.8125 19.6293 17.8125 19.8876C17.8125 20.1459 17.7209 20.3668 17.5375 20.5501C17.3542 20.7334 17.1334 20.8251 16.875 20.8251H7.12505C6.86672 20.8251 6.64588 20.7334 6.46255 20.5501C6.27922 20.3668 6.18755 20.1459 6.18755 19.8876C6.18755 19.6293 6.27922 19.4084 6.46255 19.2251C6.64588 19.0418 6.86672 18.9501 7.12505 18.9501H11.0625ZM7.52505 7.0751H16.475L18.25 5.1001H5.75005L7.52505 7.0751ZM12 12.0751L14.8125 8.9501H9.18755L12 12.0751Z"
|
d="M11.0625 18.9501V13.8751L3.68755 5.5626C3.57088 5.42926 3.47713 5.28551 3.4063 5.13135C3.33547 4.97718 3.30005 4.8126 3.30005 4.6376C3.30005 4.2376 3.43963 3.90218 3.7188 3.63135C3.99797 3.36051 4.33755 3.2251 4.73755 3.2251H19.2625C19.6625 3.2251 20.0021 3.36051 20.2813 3.63135C20.5605 3.90218 20.7 4.2376 20.7 4.6376C20.7 4.8126 20.6646 4.97718 20.5938 5.13135C20.523 5.28551 20.4292 5.42926 20.3125 5.5626L12.9375 13.8751V18.9501H16.875C17.1334 18.9501 17.3542 19.0418 17.5375 19.2251C17.7209 19.4084 17.8125 19.6293 17.8125 19.8876C17.8125 20.1459 17.7209 20.3668 17.5375 20.5501C17.3542 20.7334 17.1334 20.8251 16.875 20.8251H7.12505C6.86672 20.8251 6.64588 20.7334 6.46255 20.5501C6.27922 20.3668 6.18755 20.1459 6.18755 19.8876C6.18755 19.6293 6.27922 19.4084 6.46255 19.2251C6.64588 19.0418 6.86672 18.9501 7.12505 18.9501H11.0625ZM7.52505 7.0751H16.475L18.25 5.1001H5.75005L7.52505 7.0751ZM12 12.0751L14.8125 8.9501H9.18755L12 12.0751Z"
|
||||||
fill="#4D001B"
|
fill="#4D001B"
|
||||||
/>
|
/>
|
||||||
</g>
|
|
||||||
</svg>
|
</svg>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,23 +14,10 @@ export default function BathtubIcon({ className, color, ...props }: IconProps) {
|
|||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<mask
|
|
||||||
style={{ maskType: "alpha" }}
|
|
||||||
id="mask0_69_3451"
|
|
||||||
maskUnits="userSpaceOnUse"
|
|
||||||
x="0"
|
|
||||||
y="0"
|
|
||||||
width="24"
|
|
||||||
height="24"
|
|
||||||
>
|
|
||||||
<rect width="24" height="24" fill="#D9D9D9" />
|
|
||||||
</mask>
|
|
||||||
<g mask="url(#mask0_69_3451)">
|
|
||||||
<path
|
<path
|
||||||
d="M7.075 9.13755C6.54563 9.13755 6.09246 8.94906 5.71548 8.57207C5.33849 8.19511 5.15 7.74193 5.15 7.21255C5.15 6.68317 5.33849 6.22999 5.71548 5.85302C6.09246 5.47604 6.54563 5.28755 7.075 5.28755C7.60438 5.28755 8.05756 5.47604 8.43453 5.85302C8.81151 6.22999 9 6.68317 9 7.21255C9 7.74193 8.81151 8.19511 8.43453 8.57207C8.05756 8.94906 7.60438 9.13755 7.075 9.13755ZM5.125 21.75C4.84167 21.75 4.60417 21.6554 4.4125 21.4661C4.22083 21.2769 4.125 21.0423 4.125 20.7625C3.60937 20.7625 3.16796 20.579 2.80077 20.2118C2.43359 19.8446 2.25 19.4032 2.25 18.8875V13.9875C2.25 13.7292 2.34167 13.5084 2.525 13.325C2.70833 13.1417 2.92917 13.05 3.1875 13.05H5.175V12.3276C5.175 11.7176 5.38444 11.2021 5.80332 10.7813C6.22222 10.3605 6.73778 10.15 7.35 10.15C7.66667 10.15 7.96667 10.2125 8.25 10.3375C8.53333 10.4625 8.78333 10.6417 9 10.875L10.35 12.375C10.475 12.5 10.6 12.6209 10.725 12.7375C10.85 12.8542 10.9833 12.9584 11.125 13.05H17.875V5.00005C17.875 4.75642 17.7892 4.54759 17.6177 4.37357C17.4461 4.19956 17.2402 4.11255 17 4.11255C16.8941 4.11255 16.7926 4.13338 16.6956 4.17505C16.5985 4.21672 16.509 4.27869 16.4269 4.36097L15.1962 5.59522C15.2782 5.87497 15.2946 6.15061 15.2454 6.42215C15.1962 6.69368 15.0977 6.94465 14.95 7.17505L12.3 4.50005C12.5333 4.35005 12.7833 4.25422 13.05 4.21255C13.3167 4.17088 13.5833 4.20005 13.85 4.30005L15.075 3.06255C15.3323 2.80373 15.6258 2.60152 15.9554 2.45592C16.2851 2.31034 16.6348 2.23755 17.0047 2.23755C17.7766 2.23755 18.4271 2.50411 18.9563 3.03722C19.4854 3.57034 19.75 4.22462 19.75 5.00005V13.05H20.8125C21.0708 13.05 21.2917 13.1417 21.475 13.325C21.6583 13.5084 21.75 13.7292 21.75 13.9875V18.8875C21.75 19.4032 21.5664 19.8446 21.1992 20.2118C20.832 20.579 20.3906 20.7625 19.875 20.7625C19.875 21.0423 19.7792 21.2769 19.5875 21.4661C19.3958 21.6554 19.1583 21.75 18.875 21.75H5.125ZM4.125 18.8875H19.875V14.925H4.125V18.8875Z"
|
d="M7.075 9.13755C6.54563 9.13755 6.09246 8.94906 5.71548 8.57207C5.33849 8.19511 5.15 7.74193 5.15 7.21255C5.15 6.68317 5.33849 6.22999 5.71548 5.85302C6.09246 5.47604 6.54563 5.28755 7.075 5.28755C7.60438 5.28755 8.05756 5.47604 8.43453 5.85302C8.81151 6.22999 9 6.68317 9 7.21255C9 7.74193 8.81151 8.19511 8.43453 8.57207C8.05756 8.94906 7.60438 9.13755 7.075 9.13755ZM5.125 21.75C4.84167 21.75 4.60417 21.6554 4.4125 21.4661C4.22083 21.2769 4.125 21.0423 4.125 20.7625C3.60937 20.7625 3.16796 20.579 2.80077 20.2118C2.43359 19.8446 2.25 19.4032 2.25 18.8875V13.9875C2.25 13.7292 2.34167 13.5084 2.525 13.325C2.70833 13.1417 2.92917 13.05 3.1875 13.05H5.175V12.3276C5.175 11.7176 5.38444 11.2021 5.80332 10.7813C6.22222 10.3605 6.73778 10.15 7.35 10.15C7.66667 10.15 7.96667 10.2125 8.25 10.3375C8.53333 10.4625 8.78333 10.6417 9 10.875L10.35 12.375C10.475 12.5 10.6 12.6209 10.725 12.7375C10.85 12.8542 10.9833 12.9584 11.125 13.05H17.875V5.00005C17.875 4.75642 17.7892 4.54759 17.6177 4.37357C17.4461 4.19956 17.2402 4.11255 17 4.11255C16.8941 4.11255 16.7926 4.13338 16.6956 4.17505C16.5985 4.21672 16.509 4.27869 16.4269 4.36097L15.1962 5.59522C15.2782 5.87497 15.2946 6.15061 15.2454 6.42215C15.1962 6.69368 15.0977 6.94465 14.95 7.17505L12.3 4.50005C12.5333 4.35005 12.7833 4.25422 13.05 4.21255C13.3167 4.17088 13.5833 4.20005 13.85 4.30005L15.075 3.06255C15.3323 2.80373 15.6258 2.60152 15.9554 2.45592C16.2851 2.31034 16.6348 2.23755 17.0047 2.23755C17.7766 2.23755 18.4271 2.50411 18.9563 3.03722C19.4854 3.57034 19.75 4.22462 19.75 5.00005V13.05H20.8125C21.0708 13.05 21.2917 13.1417 21.475 13.325C21.6583 13.5084 21.75 13.7292 21.75 13.9875V18.8875C21.75 19.4032 21.5664 19.8446 21.1992 20.2118C20.832 20.579 20.3906 20.7625 19.875 20.7625C19.875 21.0423 19.7792 21.2769 19.5875 21.4661C19.3958 21.6554 19.1583 21.75 18.875 21.75H5.125ZM4.125 18.8875H19.875V14.925H4.125V18.8875Z"
|
||||||
fill="#26201E"
|
fill="#26201E"
|
||||||
/>
|
/>
|
||||||
</g>
|
|
||||||
</svg>
|
</svg>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,23 +18,10 @@ export default function BedDoubleIcon({
|
|||||||
className={classNames}
|
className={classNames}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<mask
|
|
||||||
style={{ maskType: "alpha" }}
|
|
||||||
id="mask0_69_3418"
|
|
||||||
maskUnits="userSpaceOnUse"
|
|
||||||
x="0"
|
|
||||||
y="0"
|
|
||||||
width="24"
|
|
||||||
height="24"
|
|
||||||
>
|
|
||||||
<rect width="24" height="24" fill="#D9D9D9" />
|
|
||||||
</mask>
|
|
||||||
<g mask="url(#mask0_69_3418)">
|
|
||||||
<path
|
<path
|
||||||
d="M3.25 17.8875V13C3.25 12.575 3.3375 12.1813 3.5125 11.8188C3.6875 11.4563 3.925 11.1417 4.225 10.875V8.17505C4.225 7.38338 4.50625 6.7063 5.06875 6.1438C5.63125 5.5813 6.30833 5.30005 7.1 5.30005H10.075C10.4417 5.30005 10.7875 5.37088 11.1125 5.51255C11.4375 5.65422 11.7333 5.85005 12 6.10005C12.2667 5.85005 12.5625 5.65422 12.8875 5.51255C13.2125 5.37088 13.5583 5.30005 13.925 5.30005H16.9C17.6917 5.30005 18.3687 5.5813 18.9312 6.1438C19.4937 6.7063 19.775 7.38338 19.775 8.17505V10.875C20.075 11.1417 20.3125 11.4563 20.4875 11.8188C20.6625 12.1813 20.75 12.575 20.75 13V17.8875C20.75 18.1459 20.6583 18.3667 20.475 18.55C20.2917 18.7334 20.0708 18.825 19.8125 18.825C19.5542 18.825 19.3333 18.7334 19.15 18.55C18.9667 18.3667 18.875 18.1459 18.875 17.8875V16.85H5.125V17.8875C5.125 18.1459 5.03333 18.3667 4.85 18.55C4.66667 18.7334 4.44583 18.825 4.1875 18.825C3.92917 18.825 3.70833 18.7334 3.525 18.55C3.34167 18.3667 3.25 18.1459 3.25 17.8875ZM12.95 10.15H17.9V8.17067C17.9 7.89026 17.8046 7.65422 17.6138 7.46255C17.423 7.27088 17.1866 7.17505 16.9046 7.17505H13.923C13.641 7.17505 13.4083 7.27088 13.225 7.46255C13.0417 7.65422 12.95 7.89172 12.95 8.17505V10.15ZM6.1 10.15H11.05V8.17067C11.05 7.89026 10.9583 7.65422 10.775 7.46255C10.5917 7.27088 10.359 7.17505 10.077 7.17505H7.0954C6.81337 7.17505 6.57696 7.27088 6.38618 7.46255C6.19539 7.65422 6.1 7.89172 6.1 8.17505V10.15ZM5.125 14.975H18.875V12.9957C18.875 12.7153 18.7833 12.4834 18.6 12.3C18.4167 12.1167 18.1852 12.025 17.9057 12.025H6.09427C5.81476 12.025 5.58333 12.1167 5.4 12.3C5.21667 12.4834 5.125 12.7153 5.125 12.9957V14.975Z"
|
d="M3.25 17.8875V13C3.25 12.575 3.3375 12.1813 3.5125 11.8188C3.6875 11.4563 3.925 11.1417 4.225 10.875V8.17505C4.225 7.38338 4.50625 6.7063 5.06875 6.1438C5.63125 5.5813 6.30833 5.30005 7.1 5.30005H10.075C10.4417 5.30005 10.7875 5.37088 11.1125 5.51255C11.4375 5.65422 11.7333 5.85005 12 6.10005C12.2667 5.85005 12.5625 5.65422 12.8875 5.51255C13.2125 5.37088 13.5583 5.30005 13.925 5.30005H16.9C17.6917 5.30005 18.3687 5.5813 18.9312 6.1438C19.4937 6.7063 19.775 7.38338 19.775 8.17505V10.875C20.075 11.1417 20.3125 11.4563 20.4875 11.8188C20.6625 12.1813 20.75 12.575 20.75 13V17.8875C20.75 18.1459 20.6583 18.3667 20.475 18.55C20.2917 18.7334 20.0708 18.825 19.8125 18.825C19.5542 18.825 19.3333 18.7334 19.15 18.55C18.9667 18.3667 18.875 18.1459 18.875 17.8875V16.85H5.125V17.8875C5.125 18.1459 5.03333 18.3667 4.85 18.55C4.66667 18.7334 4.44583 18.825 4.1875 18.825C3.92917 18.825 3.70833 18.7334 3.525 18.55C3.34167 18.3667 3.25 18.1459 3.25 17.8875ZM12.95 10.15H17.9V8.17067C17.9 7.89026 17.8046 7.65422 17.6138 7.46255C17.423 7.27088 17.1866 7.17505 16.9046 7.17505H13.923C13.641 7.17505 13.4083 7.27088 13.225 7.46255C13.0417 7.65422 12.95 7.89172 12.95 8.17505V10.15ZM6.1 10.15H11.05V8.17067C11.05 7.89026 10.9583 7.65422 10.775 7.46255C10.5917 7.27088 10.359 7.17505 10.077 7.17505H7.0954C6.81337 7.17505 6.57696 7.27088 6.38618 7.46255C6.19539 7.65422 6.1 7.89172 6.1 8.17505V10.15ZM5.125 14.975H18.875V12.9957C18.875 12.7153 18.7833 12.4834 18.6 12.3C18.4167 12.1167 18.1852 12.025 17.9057 12.025H6.09427C5.81476 12.025 5.58333 12.1167 5.4 12.3C5.21667 12.4834 5.125 12.7153 5.125 12.9957V14.975Z"
|
||||||
fill="#26201E"
|
fill="#26201E"
|
||||||
/>
|
/>
|
||||||
</g>
|
|
||||||
</svg>
|
</svg>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user