Merge branch 'develop' into feature/sw-561-design-fixes
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
.form {
|
||||
display: grid;
|
||||
gap: var(--Spacing-x2);
|
||||
grid-template-columns: repeat(auto-fit, minmax(230px, 1fr));
|
||||
padding-bottom: var(--Spacing-x3);
|
||||
width: min(600px, 100%);
|
||||
}
|
||||
72
components/HotelReservation/EnterDetails/BedType/index.tsx
Normal file
72
components/HotelReservation/EnterDetails/BedType/index.tsx
Normal file
@@ -0,0 +1,72 @@
|
||||
"use client"
|
||||
|
||||
import { zodResolver } from "@hookform/resolvers/zod"
|
||||
import { FormProvider, useForm } from "react-hook-form"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import { KingBedIcon } from "@/components/Icons"
|
||||
import RadioCard from "@/components/TempDesignSystem/Form/Card/Radio"
|
||||
|
||||
import { bedTypeSchema } from "./schema"
|
||||
|
||||
import styles from "./bedOptions.module.css"
|
||||
|
||||
import type { BedTypeSchema } from "@/types/components/enterDetails/bedType"
|
||||
import { bedTypeEnum } from "@/types/enums/bedType"
|
||||
|
||||
export default function BedType() {
|
||||
const intl = useIntl()
|
||||
|
||||
const methods = useForm<BedTypeSchema>({
|
||||
criteriaMode: "all",
|
||||
mode: "all",
|
||||
resolver: zodResolver(bedTypeSchema),
|
||||
reValidateMode: "onChange",
|
||||
})
|
||||
|
||||
// @ts-expect-error - Types mismatch docs as this is
|
||||
// a pattern that is allowed https://formatjs.io/docs/react-intl/api#usage
|
||||
const text = intl.formatMessage(
|
||||
{ id: "<b>Included</b> (based on availability)" },
|
||||
{ b: (str) => <b>{str}</b> }
|
||||
)
|
||||
|
||||
return (
|
||||
<FormProvider {...methods}>
|
||||
<form className={styles.form}>
|
||||
<RadioCard
|
||||
Icon={KingBedIcon}
|
||||
iconWidth={46}
|
||||
id={bedTypeEnum.KING}
|
||||
name="bed"
|
||||
subtitle={intl.formatMessage(
|
||||
{ id: "{width} cm × {length} cm" },
|
||||
{
|
||||
length: "210",
|
||||
width: "180",
|
||||
}
|
||||
)}
|
||||
text={text}
|
||||
title={intl.formatMessage({ id: "King bed" })}
|
||||
value={bedTypeEnum.KING}
|
||||
/>
|
||||
<RadioCard
|
||||
Icon={KingBedIcon}
|
||||
iconWidth={46}
|
||||
id={bedTypeEnum.QUEEN}
|
||||
name="bed"
|
||||
subtitle={intl.formatMessage(
|
||||
{ id: "{width} cm × {length} cm" },
|
||||
{
|
||||
length: "200",
|
||||
width: "160",
|
||||
}
|
||||
)}
|
||||
text={text}
|
||||
title={intl.formatMessage({ id: "Queen bed" })}
|
||||
value={bedTypeEnum.QUEEN}
|
||||
/>
|
||||
</form>
|
||||
</FormProvider>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import { z } from "zod"
|
||||
|
||||
import { bedTypeEnum } from "@/types/enums/bedType"
|
||||
|
||||
export const bedTypeSchema = z.object({
|
||||
bed: z.nativeEnum(bedTypeEnum),
|
||||
})
|
||||
@@ -0,0 +1,7 @@
|
||||
.form {
|
||||
display: grid;
|
||||
gap: var(--Spacing-x2);
|
||||
grid-template-columns: repeat(auto-fit, minmax(230px, 1fr));
|
||||
padding-bottom: var(--Spacing-x3);
|
||||
width: min(600px, 100%);
|
||||
}
|
||||
70
components/HotelReservation/EnterDetails/Breakfast/index.tsx
Normal file
70
components/HotelReservation/EnterDetails/Breakfast/index.tsx
Normal file
@@ -0,0 +1,70 @@
|
||||
"use client"
|
||||
|
||||
import { zodResolver } from "@hookform/resolvers/zod"
|
||||
import { FormProvider, useForm } from "react-hook-form"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import { BreakfastIcon, NoBreakfastIcon } from "@/components/Icons"
|
||||
import RadioCard from "@/components/TempDesignSystem/Form/Card/Radio"
|
||||
|
||||
import { breakfastSchema } from "./schema"
|
||||
|
||||
import styles from "./breakfast.module.css"
|
||||
|
||||
import type { BreakfastSchema } from "@/types/components/enterDetails/breakfast"
|
||||
import { breakfastEnum } from "@/types/enums/breakfast"
|
||||
|
||||
export default function Breakfast() {
|
||||
const intl = useIntl()
|
||||
|
||||
const methods = useForm<BreakfastSchema>({
|
||||
criteriaMode: "all",
|
||||
mode: "all",
|
||||
resolver: zodResolver(breakfastSchema),
|
||||
reValidateMode: "onChange",
|
||||
})
|
||||
|
||||
return (
|
||||
<FormProvider {...methods}>
|
||||
<form className={styles.form}>
|
||||
<RadioCard
|
||||
Icon={BreakfastIcon}
|
||||
id={breakfastEnum.BREAKFAST}
|
||||
name="breakfast"
|
||||
// @ts-expect-error - Types mismatch docs as this is
|
||||
// a pattern that is allowed https://formatjs.io/docs/react-intl/api#usage
|
||||
subtitle={intl.formatMessage(
|
||||
{ id: "<b>{amount} {currency}</b>/night per adult" },
|
||||
{
|
||||
amount: "150",
|
||||
b: (str) => <b>{str}</b>,
|
||||
currency: "SEK",
|
||||
}
|
||||
)}
|
||||
text={intl.formatMessage({
|
||||
id: "All our breakfast buffets offer gluten free, vegan, and allergy-friendly options.",
|
||||
})}
|
||||
title={intl.formatMessage({ id: "Breakfast buffet" })}
|
||||
value={breakfastEnum.BREAKFAST}
|
||||
/>
|
||||
<RadioCard
|
||||
Icon={NoBreakfastIcon}
|
||||
id={breakfastEnum.NO_BREAKFAST}
|
||||
name="breakfast"
|
||||
subtitle={intl.formatMessage(
|
||||
{ id: "{amount} {currency}" },
|
||||
{
|
||||
amount: "0",
|
||||
currency: "SEK",
|
||||
}
|
||||
)}
|
||||
text={intl.formatMessage({
|
||||
id: "You can always change your mind later and add breakfast at the hotel.",
|
||||
})}
|
||||
title={intl.formatMessage({ id: "No breakfast" })}
|
||||
value={breakfastEnum.NO_BREAKFAST}
|
||||
/>
|
||||
</form>
|
||||
</FormProvider>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import { z } from "zod"
|
||||
|
||||
import { breakfastEnum } from "@/types/enums/breakfast"
|
||||
|
||||
export const breakfastSchema = z.object({
|
||||
breakfast: z.nativeEnum(breakfastEnum),
|
||||
})
|
||||
@@ -0,0 +1,25 @@
|
||||
.container {
|
||||
display: grid;
|
||||
gap: var(--Spacing-x2);
|
||||
padding: var(--Spacing-x3) 0px;
|
||||
}
|
||||
|
||||
.form {
|
||||
display: grid;
|
||||
gap: var(--Spacing-x2);
|
||||
grid-template-columns: 1fr 1fr;
|
||||
width: min(100%, 600px);
|
||||
}
|
||||
|
||||
.country,
|
||||
.email,
|
||||
.phone {
|
||||
grid-column: 1/-1;
|
||||
}
|
||||
|
||||
.footer {
|
||||
display: grid;
|
||||
gap: var(--Spacing-x3);
|
||||
justify-items: flex-start;
|
||||
margin-top: var(--Spacing-x1);
|
||||
}
|
||||
118
components/HotelReservation/EnterDetails/Details/index.tsx
Normal file
118
components/HotelReservation/EnterDetails/Details/index.tsx
Normal file
@@ -0,0 +1,118 @@
|
||||
"use client"
|
||||
import { zodResolver } from "@hookform/resolvers/zod"
|
||||
import { FormProvider, useForm } from "react-hook-form"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import Button from "@/components/TempDesignSystem/Button"
|
||||
import CheckboxCard from "@/components/TempDesignSystem/Form/Card/Checkbox"
|
||||
import CountrySelect from "@/components/TempDesignSystem/Form/Country"
|
||||
import Input from "@/components/TempDesignSystem/Form/Input"
|
||||
import Phone from "@/components/TempDesignSystem/Form/Phone"
|
||||
import Body from "@/components/TempDesignSystem/Text/Body"
|
||||
|
||||
import { detailsSchema, signedInDetailsSchema } from "./schema"
|
||||
|
||||
import styles from "./details.module.css"
|
||||
|
||||
import type {
|
||||
DetailsProps,
|
||||
DetailsSchema,
|
||||
} from "@/types/components/enterDetails/details"
|
||||
|
||||
export default function Details({ user }: DetailsProps) {
|
||||
const intl = useIntl()
|
||||
|
||||
const list = [
|
||||
{ title: intl.formatMessage({ id: "Earn bonus nights & points" }) },
|
||||
{ title: intl.formatMessage({ id: "Get member benefits & offers" }) },
|
||||
{ title: intl.formatMessage({ id: "Join at no cost" }) },
|
||||
]
|
||||
|
||||
const methods = useForm<DetailsSchema>({
|
||||
defaultValues: {
|
||||
countryCode: user?.address?.countryCode ?? "",
|
||||
email: user?.email ?? "",
|
||||
firstname: user?.firstName ?? "",
|
||||
lastname: user?.lastName ?? "",
|
||||
phoneNumber: user?.phoneNumber ?? "",
|
||||
},
|
||||
criteriaMode: "all",
|
||||
mode: "all",
|
||||
resolver: zodResolver(user ? signedInDetailsSchema : detailsSchema),
|
||||
reValidateMode: "onChange",
|
||||
})
|
||||
|
||||
return (
|
||||
<FormProvider {...methods}>
|
||||
<section className={styles.container}>
|
||||
<header>
|
||||
<Body color="uiTextHighContrast" textTransform="bold">
|
||||
{intl.formatMessage({ id: "Guest information" })}
|
||||
</Body>
|
||||
</header>
|
||||
<form className={styles.form}>
|
||||
<Input
|
||||
label={intl.formatMessage({ id: "Firstname" })}
|
||||
name="firstname"
|
||||
readOnly={!!user}
|
||||
registerOptions={{ required: true }}
|
||||
/>
|
||||
<Input
|
||||
label={intl.formatMessage({ id: "Lastname" })}
|
||||
name="lastname"
|
||||
readOnly={!!user}
|
||||
registerOptions={{ required: true }}
|
||||
/>
|
||||
<CountrySelect
|
||||
className={styles.country}
|
||||
label={intl.formatMessage({ id: "Country" })}
|
||||
name="countryCode"
|
||||
readOnly={!!user}
|
||||
registerOptions={{ required: true }}
|
||||
/>
|
||||
<Input
|
||||
className={styles.email}
|
||||
label={intl.formatMessage({ id: "Email address" })}
|
||||
name="email"
|
||||
readOnly={!!user}
|
||||
registerOptions={{ required: true }}
|
||||
/>
|
||||
<Phone
|
||||
className={styles.phone}
|
||||
label={intl.formatMessage({ id: "Phone number" })}
|
||||
name="phoneNumber"
|
||||
readOnly={!!user}
|
||||
registerOptions={{ required: true }}
|
||||
/>
|
||||
</form>
|
||||
<footer className={styles.footer}>
|
||||
{user ? null : (
|
||||
<CheckboxCard
|
||||
list={list}
|
||||
saving
|
||||
subtitle={intl.formatMessage(
|
||||
{
|
||||
id: "{difference}{amount} {currency}",
|
||||
},
|
||||
{
|
||||
amount: "491",
|
||||
currency: "SEK",
|
||||
difference: "-",
|
||||
}
|
||||
)}
|
||||
title={intl.formatMessage({ id: "Join Scandic Friends" })}
|
||||
/>
|
||||
)}
|
||||
<Button
|
||||
disabled={!methods.formState.isValid}
|
||||
intent="secondary"
|
||||
size="small"
|
||||
theme="base"
|
||||
>
|
||||
{intl.formatMessage({ id: "Proceed to payment method" })}
|
||||
</Button>
|
||||
</footer>
|
||||
</section>
|
||||
</FormProvider>
|
||||
)
|
||||
}
|
||||
19
components/HotelReservation/EnterDetails/Details/schema.ts
Normal file
19
components/HotelReservation/EnterDetails/Details/schema.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { z } from "zod"
|
||||
|
||||
import { phoneValidator } from "@/utils/phoneValidator"
|
||||
|
||||
export const detailsSchema = z.object({
|
||||
countryCode: z.string(),
|
||||
email: z.string().email(),
|
||||
firstname: z.string(),
|
||||
lastname: z.string(),
|
||||
phoneNumber: phoneValidator(),
|
||||
})
|
||||
|
||||
export const signedInDetailsSchema = z.object({
|
||||
countryCode: z.string().optional(),
|
||||
email: z.string().email().optional(),
|
||||
firstname: z.string().optional(),
|
||||
lastname: z.string().optional(),
|
||||
phoneNumber: phoneValidator().optional(),
|
||||
})
|
||||
@@ -48,10 +48,10 @@ export default async function HotelCard({ hotel }: HotelCardProps) {
|
||||
<Title as="h4" textTransform="capitalize">
|
||||
{hotelData.name}
|
||||
</Title>
|
||||
<Footnote color="textMediumContrast">
|
||||
<Footnote color="uiTextMediumContrast">
|
||||
{`${hotelData.address.streetAddress}, ${hotelData.address.city}`}
|
||||
</Footnote>
|
||||
<Footnote color="textMediumContrast">
|
||||
<Footnote color="uiTextMediumContrast">
|
||||
{`${hotelData.location.distanceToCentre} ${intl.formatMessage({ id: "km to city center" })}`}
|
||||
</Footnote>
|
||||
</section>
|
||||
@@ -79,7 +79,7 @@ export default async function HotelCard({ hotel }: HotelCardProps) {
|
||||
{price?.regularAmount} {price?.currency} /
|
||||
{intl.formatMessage({ id: "night" })}
|
||||
</Caption>
|
||||
<Footnote color="textMediumContrast">approx 280 eur</Footnote>
|
||||
<Footnote color="uiTextMediumContrast">approx 280 eur</Footnote>
|
||||
</div>
|
||||
<div>
|
||||
<Chip intent="primary" className={styles.member}>
|
||||
@@ -90,7 +90,7 @@ export default async function HotelCard({ hotel }: HotelCardProps) {
|
||||
{price?.memberAmount} {price?.currency} /
|
||||
{intl.formatMessage({ id: "night" })}
|
||||
</Caption>
|
||||
<Footnote color="textMediumContrast">approx 280 eur</Footnote>
|
||||
<Footnote color="uiTextMediumContrast">approx 280 eur</Footnote>
|
||||
</div>
|
||||
<Button
|
||||
asChild
|
||||
|
||||
@@ -5,11 +5,10 @@ import RoomCard from "./RoomCard"
|
||||
|
||||
import styles from "./roomSelection.module.css"
|
||||
|
||||
import { RoomSelectionProps } from "@/types/components/hotelReservation/selectRate/section"
|
||||
import { RoomSelectionProps } from "@/types/components/hotelReservation/selectRate/roomSelection"
|
||||
|
||||
export default function RoomSelection({
|
||||
alternatives,
|
||||
nextPath,
|
||||
rates,
|
||||
nrOfNights,
|
||||
nrOfAdults,
|
||||
}: RoomSelectionProps) {
|
||||
@@ -21,17 +20,17 @@ export default function RoomSelection({
|
||||
const queryParams = new URLSearchParams(searchParams)
|
||||
queryParams.set("roomClass", e.currentTarget.roomClass?.value)
|
||||
queryParams.set("flexibility", e.currentTarget.flexibility?.value)
|
||||
router.push(`${nextPath}?${queryParams}`)
|
||||
router.push(`select-bed?${queryParams}`)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.wrapper}>
|
||||
<ul className={styles.roomList}>
|
||||
{alternatives.map((room) => (
|
||||
{rates.map((room) => (
|
||||
<li key={room.id}>
|
||||
<form
|
||||
method="GET"
|
||||
action={`${nextPath}?${searchParams}`}
|
||||
action={`select-bed?${searchParams}`}
|
||||
onSubmit={handleSubmit}
|
||||
>
|
||||
<input
|
||||
@@ -50,6 +49,7 @@ export default function RoomSelection({
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<div className={styles.summary}>This is summary</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -21,3 +21,10 @@
|
||||
position: fixed;
|
||||
width: 0;
|
||||
}
|
||||
|
||||
.summary {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user