Merge branch 'develop' into feat/sw-222-staycard-link-loading
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
"use server"
|
"use server"
|
||||||
|
|
||||||
|
import { parsePhoneNumber } from "libphonenumber-js"
|
||||||
import { redirect } from "next/navigation"
|
import { redirect } from "next/navigation"
|
||||||
import { z } from "zod"
|
import { z } from "zod"
|
||||||
|
|
||||||
@@ -7,7 +8,7 @@ import { signupVerify } from "@/constants/routes/signup"
|
|||||||
import * as api from "@/lib/api"
|
import * as api from "@/lib/api"
|
||||||
import { serviceServerActionProcedure } from "@/server/trpc"
|
import { serviceServerActionProcedure } from "@/server/trpc"
|
||||||
|
|
||||||
import { registerSchema } from "@/components/Forms/Register/schema"
|
import { signUpSchema } from "@/components/Forms/Signup/schema"
|
||||||
import { passwordValidator } from "@/utils/passwordValidator"
|
import { passwordValidator } from "@/utils/passwordValidator"
|
||||||
import { phoneValidator } from "@/utils/phoneValidator"
|
import { phoneValidator } from "@/utils/phoneValidator"
|
||||||
|
|
||||||
@@ -29,12 +30,14 @@ const registerUserPayload = z.object({
|
|||||||
})
|
})
|
||||||
|
|
||||||
export const registerUser = serviceServerActionProcedure
|
export const registerUser = serviceServerActionProcedure
|
||||||
.input(registerSchema)
|
.input(signUpSchema)
|
||||||
.mutation(async function ({ ctx, input }) {
|
.mutation(async function ({ ctx, input }) {
|
||||||
const payload = {
|
const payload = {
|
||||||
...input,
|
...input,
|
||||||
language: ctx.lang,
|
language: ctx.lang,
|
||||||
phoneNumber: input.phoneNumber.replace(/\s+/g, ""),
|
phoneNumber: parsePhoneNumber(input.phoneNumber)
|
||||||
|
.formatNational()
|
||||||
|
.replace(/\s+/g, ""),
|
||||||
}
|
}
|
||||||
|
|
||||||
const parsedPayload = registerUserPayload.safeParse(payload)
|
const parsedPayload = registerUserPayload.safeParse(payload)
|
||||||
|
|||||||
@@ -17,11 +17,11 @@ export default async function SelectRatePage({
|
|||||||
}: PageArgs<LangParams & { section: string }, SelectRateSearchParams>) {
|
}: PageArgs<LangParams & { section: string }, SelectRateSearchParams>) {
|
||||||
setLang(params.lang)
|
setLang(params.lang)
|
||||||
|
|
||||||
const selecetRoomParams = new URLSearchParams(searchParams)
|
const selectRoomParams = new URLSearchParams(searchParams)
|
||||||
const selecetRoomParamsObject =
|
const selectRoomParamsObject =
|
||||||
getHotelReservationQueryParams(selecetRoomParams)
|
getHotelReservationQueryParams(selectRoomParams)
|
||||||
const adults = selecetRoomParamsObject.room[0].adults // TODO: Handle multiple rooms
|
const adults = selectRoomParamsObject.room[0].adults // TODO: Handle multiple rooms
|
||||||
const children = selecetRoomParamsObject.room[0].child.length // TODO: Handle multiple rooms
|
const children = selectRoomParamsObject.room[0].child?.length // TODO: Handle multiple rooms
|
||||||
|
|
||||||
const [hotelData, roomConfigurations, user] = await Promise.all([
|
const [hotelData, roomConfigurations, user] = await Promise.all([
|
||||||
serverClient().hotel.hotelData.get({
|
serverClient().hotel.hotelData.get({
|
||||||
@@ -33,8 +33,8 @@ export default async function SelectRatePage({
|
|||||||
hotelId: parseInt(searchParams.hotel, 10),
|
hotelId: parseInt(searchParams.hotel, 10),
|
||||||
roomStayStartDate: searchParams.fromDate,
|
roomStayStartDate: searchParams.fromDate,
|
||||||
roomStayEndDate: searchParams.toDate,
|
roomStayEndDate: searchParams.toDate,
|
||||||
adults: adults,
|
adults,
|
||||||
children: children,
|
children,
|
||||||
}),
|
}),
|
||||||
getProfileSafely(),
|
getProfileSafely(),
|
||||||
])
|
])
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { redirect } from "next/navigation"
|
|||||||
import { overview } from "@/constants/routes/myPages"
|
import { overview } from "@/constants/routes/myPages"
|
||||||
|
|
||||||
import { auth } from "@/auth"
|
import { auth } from "@/auth"
|
||||||
import Form from "@/components/Forms/Register"
|
import SignupForm from "@/components/Forms/Signup"
|
||||||
import { getLang } from "@/i18n/serverContext"
|
import { getLang } from "@/i18n/serverContext"
|
||||||
|
|
||||||
import { SignupFormWrapperProps } from "@/types/components/blocks/dynamicContent"
|
import { SignupFormWrapperProps } from "@/types/components/blocks/dynamicContent"
|
||||||
@@ -16,5 +16,5 @@ export default async function SignupFormWrapper({
|
|||||||
// We don't want to allow users to access signup if they are already authenticated.
|
// We don't want to allow users to access signup if they are already authenticated.
|
||||||
redirect(overview[getLang()])
|
redirect(overview[getLang()])
|
||||||
}
|
}
|
||||||
return <Form {...dynamic_content} />
|
return <SignupForm {...dynamic_content} />
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,8 +42,8 @@ export default function BookingWidgetClient({
|
|||||||
date: {
|
date: {
|
||||||
// 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.
|
||||||
from: dt().utc().format("YYYY-MM-DD"),
|
fromDate: dt().utc().format("YYYY-MM-DD"),
|
||||||
to: dt().utc().add(1, "day").format("YYYY-MM-DD"),
|
toDate: dt().utc().add(1, "day").format("YYYY-MM-DD"),
|
||||||
},
|
},
|
||||||
bookingCode: "",
|
bookingCode: "",
|
||||||
redemption: false,
|
redemption: false,
|
||||||
|
|||||||
@@ -29,6 +29,11 @@
|
|||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.overview {
|
||||||
|
display: grid;
|
||||||
|
gap: var(--Spacing-x3);
|
||||||
|
}
|
||||||
|
|
||||||
.introContainer {
|
.introContainer {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
@@ -37,6 +42,11 @@
|
|||||||
scroll-margin-top: var(--hotel-page-scroll-margin-top);
|
scroll-margin-top: var(--hotel-page-scroll-margin-top);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.alertsContainer {
|
||||||
|
display: grid;
|
||||||
|
gap: var(--Spacing-x2);
|
||||||
|
}
|
||||||
|
|
||||||
@media screen and (min-width: 1367px) {
|
@media screen and (min-width: 1367px) {
|
||||||
.pageContainer {
|
.pageContainer {
|
||||||
grid-template-areas:
|
grid-template-areas:
|
||||||
@@ -76,10 +86,4 @@
|
|||||||
padding-left: var(--Spacing-x5);
|
padding-left: var(--Spacing-x5);
|
||||||
padding-right: var(--Spacing-x5);
|
padding-right: var(--Spacing-x5);
|
||||||
}
|
}
|
||||||
.introContainer {
|
|
||||||
grid-template-columns: 38rem minmax(max-content, 16rem);
|
|
||||||
justify-content: space-between;
|
|
||||||
gap: var(--Spacing-x2);
|
|
||||||
align-items: end;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ 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/SidePeekProvider"
|
||||||
|
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"
|
||||||
import { getLang } from "@/i18n/serverContext"
|
import { getLang } from "@/i18n/serverContext"
|
||||||
@@ -49,6 +50,7 @@ export default async function HotelPage() {
|
|||||||
pointsOfInterest,
|
pointsOfInterest,
|
||||||
facilities,
|
facilities,
|
||||||
faq,
|
faq,
|
||||||
|
alerts,
|
||||||
} = hotelData
|
} = hotelData
|
||||||
|
|
||||||
const topThreePois = pointsOfInterest.slice(0, 3)
|
const topThreePois = pointsOfInterest.slice(0, 3)
|
||||||
@@ -69,16 +71,30 @@ export default async function HotelPage() {
|
|||||||
hasFAQ={!!faq}
|
hasFAQ={!!faq}
|
||||||
/>
|
/>
|
||||||
<main className={styles.mainSection}>
|
<main className={styles.mainSection}>
|
||||||
<div id={HotelHashValues.overview} className={styles.introContainer}>
|
<div id={HotelHashValues.overview} className={styles.overview}>
|
||||||
<IntroSection
|
<div className={styles.introContainer}>
|
||||||
hotelName={hotelName}
|
<IntroSection
|
||||||
hotelDescription={hotelDescription}
|
hotelName={hotelName}
|
||||||
location={hotelLocation}
|
hotelDescription={hotelDescription}
|
||||||
address={hotelAddress}
|
location={hotelLocation}
|
||||||
tripAdvisor={hotelRatings?.tripAdvisor}
|
address={hotelAddress}
|
||||||
/>
|
tripAdvisor={hotelRatings?.tripAdvisor}
|
||||||
|
/>
|
||||||
|
|
||||||
<AmenitiesList detailedFacilities={hotelDetailedFacilities} />
|
<AmenitiesList detailedFacilities={hotelDetailedFacilities} />
|
||||||
|
</div>
|
||||||
|
{alerts.length ? (
|
||||||
|
<div className={styles.alertsContainer}>
|
||||||
|
{alerts.map((alert) => (
|
||||||
|
<Alert
|
||||||
|
key={alert.id}
|
||||||
|
type={alert.type}
|
||||||
|
heading={alert.heading}
|
||||||
|
text={alert.text}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
<Rooms rooms={roomCategories} />
|
<Rooms rooms={roomCategories} />
|
||||||
<Facilities facilities={facilities} activitiesCard={activitiesCard} />
|
<Facilities facilities={facilities} activitiesCard={activitiesCard} />
|
||||||
|
|||||||
@@ -44,22 +44,22 @@ export default function DatePickerForm({ name = "date" }: DatePickerFormProps) {
|
|||||||
function handleSelectDate(selected: Date) {
|
function handleSelectDate(selected: Date) {
|
||||||
if (isSelectingFrom) {
|
if (isSelectingFrom) {
|
||||||
setValue(name, {
|
setValue(name, {
|
||||||
from: dt(selected).format("YYYY-MM-DD"),
|
fromDate: dt(selected).format("YYYY-MM-DD"),
|
||||||
to: undefined,
|
toDate: undefined,
|
||||||
})
|
})
|
||||||
setIsSelectingFrom(false)
|
setIsSelectingFrom(false)
|
||||||
} else {
|
} else {
|
||||||
const fromDate = dt(selectedDate.from)
|
const fromDate = dt(selectedDate.fromDate)
|
||||||
const toDate = dt(selected)
|
const toDate = dt(selected)
|
||||||
if (toDate.isAfter(fromDate)) {
|
if (toDate.isAfter(fromDate)) {
|
||||||
setValue(name, {
|
setValue(name, {
|
||||||
from: selectedDate.from,
|
fromDate: selectedDate.fromDate,
|
||||||
to: toDate.format("YYYY-MM-DD"),
|
toDate: toDate.format("YYYY-MM-DD"),
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
setValue(name, {
|
setValue(name, {
|
||||||
from: toDate.format("YYYY-MM-DD"),
|
fromDate: toDate.format("YYYY-MM-DD"),
|
||||||
to: selectedDate.from,
|
toDate: selectedDate.fromDate,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
setIsSelectingFrom(true)
|
setIsSelectingFrom(true)
|
||||||
@@ -79,11 +79,11 @@ export default function DatePickerForm({ name = "date" }: DatePickerFormProps) {
|
|||||||
}
|
}
|
||||||
}, [setIsOpen])
|
}, [setIsOpen])
|
||||||
|
|
||||||
const selectedFromDate = dt(selectedDate.from)
|
const selectedFromDate = dt(selectedDate.fromDate)
|
||||||
.locale(lang)
|
.locale(lang)
|
||||||
.format("ddd D MMM")
|
.format("ddd D MMM")
|
||||||
const selectedToDate = !!selectedDate.to
|
const selectedToDate = !!selectedDate.toDate
|
||||||
? dt(selectedDate.to).locale(lang).format("ddd D MMM")
|
? dt(selectedDate.toDate).locale(lang).format("ddd D MMM")
|
||||||
: ""
|
: ""
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -93,8 +93,8 @@ export default function DatePickerForm({ name = "date" }: DatePickerFormProps) {
|
|||||||
{selectedFromDate} - {selectedToDate}
|
{selectedFromDate} - {selectedToDate}
|
||||||
</Body>
|
</Body>
|
||||||
</button>
|
</button>
|
||||||
<input {...register("date.from")} type="hidden" />
|
<input {...register("date.fromDate")} type="hidden" />
|
||||||
<input {...register("date.to")} type="hidden" />
|
<input {...register("date.toDate")} type="hidden" />
|
||||||
<div aria-modal className={styles.hideWrapper} role="dialog">
|
<div aria-modal className={styles.hideWrapper} role="dialog">
|
||||||
<DatePickerDesktop
|
<DatePickerDesktop
|
||||||
close={close}
|
close={close}
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ export default function FormContent({
|
|||||||
|
|
||||||
const rooms = intl.formatMessage({ id: "Guests & Rooms" })
|
const rooms = intl.formatMessage({ id: "Guests & Rooms" })
|
||||||
|
|
||||||
const nights = dt(selectedDate.to).diff(dt(selectedDate.from), "days")
|
const nights = dt(selectedDate.toDate).diff(dt(selectedDate.fromDate), "days")
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -18,8 +18,8 @@ export const bookingWidgetSchema = z.object({
|
|||||||
bookingCode: z.string(), // Update this as required when working with booking codes component
|
bookingCode: z.string(), // Update this as required when working with booking codes component
|
||||||
date: z.object({
|
date: z.object({
|
||||||
// Update this as required once started working with Date picker in Nights component
|
// Update this as required once started working with Date picker in Nights component
|
||||||
from: z.string(),
|
fromDate: z.string(),
|
||||||
to: z.string(),
|
toDate: z.string(),
|
||||||
}),
|
}),
|
||||||
location: z.string().refine(
|
location: z.string().refine(
|
||||||
(value) => {
|
(value) => {
|
||||||
|
|||||||
@@ -46,4 +46,8 @@
|
|||||||
.nameInputs {
|
.nameInputs {
|
||||||
grid-template-columns: 1fr 1fr;
|
grid-template-columns: 1fr 1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.signUpButton {
|
||||||
|
width: fit-content;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -22,16 +22,21 @@ import Title from "@/components/TempDesignSystem/Text/Title"
|
|||||||
import { toast } from "@/components/TempDesignSystem/Toasts"
|
import { toast } from "@/components/TempDesignSystem/Toasts"
|
||||||
import useLang from "@/hooks/useLang"
|
import useLang from "@/hooks/useLang"
|
||||||
|
|
||||||
import { RegisterSchema, registerSchema } from "./schema"
|
import { SignUpSchema, signUpSchema } from "./schema"
|
||||||
|
|
||||||
import styles from "./form.module.css"
|
import styles from "./form.module.css"
|
||||||
|
|
||||||
import type { RegisterFormProps } from "@/types/components/form/registerForm"
|
import type { SignUpFormProps } from "@/types/components/form/signupForm"
|
||||||
|
|
||||||
export default function Form({ link, subtitle, title }: RegisterFormProps) {
|
export default function SignupForm({ link, subtitle, title }: SignUpFormProps) {
|
||||||
const intl = useIntl()
|
const intl = useIntl()
|
||||||
const lang = useLang()
|
const lang = useLang()
|
||||||
const methods = useForm<RegisterSchema>({
|
const country = intl.formatMessage({ id: "Country" })
|
||||||
|
const email = intl.formatMessage({ id: "Email address" })
|
||||||
|
const phoneNumber = intl.formatMessage({ id: "Phone number" })
|
||||||
|
const zipCode = intl.formatMessage({ id: "Zip code" })
|
||||||
|
|
||||||
|
const methods = useForm<SignUpSchema>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
firstName: "",
|
firstName: "",
|
||||||
lastName: "",
|
lastName: "",
|
||||||
@@ -47,15 +52,11 @@ export default function Form({ link, subtitle, title }: RegisterFormProps) {
|
|||||||
},
|
},
|
||||||
mode: "all",
|
mode: "all",
|
||||||
criteriaMode: "all",
|
criteriaMode: "all",
|
||||||
resolver: zodResolver(registerSchema),
|
resolver: zodResolver(signUpSchema),
|
||||||
reValidateMode: "onChange",
|
reValidateMode: "onChange",
|
||||||
})
|
})
|
||||||
const country = intl.formatMessage({ id: "Country" })
|
|
||||||
const email = `${intl.formatMessage({ id: "Email" })} ${intl.formatMessage({ id: "Address" }).toLowerCase()}`
|
|
||||||
const phoneNumber = intl.formatMessage({ id: "Phone number" })
|
|
||||||
const zipCode = intl.formatMessage({ id: "Zip code" })
|
|
||||||
|
|
||||||
async function handleSubmit(data: RegisterSchema) {
|
async function onSubmit(data: SignUpSchema) {
|
||||||
try {
|
try {
|
||||||
const result = await registerUser(data)
|
const result = await registerUser(data)
|
||||||
if (result && !result.success) {
|
if (result && !result.success) {
|
||||||
@@ -78,12 +79,12 @@ export default function Form({ link, subtitle, title }: RegisterFormProps) {
|
|||||||
<form
|
<form
|
||||||
className={styles.form}
|
className={styles.form}
|
||||||
id="register"
|
id="register"
|
||||||
|
onSubmit={methods.handleSubmit(onSubmit)}
|
||||||
/**
|
/**
|
||||||
* Ignoring since ts doesn't recognize that tRPC
|
* Ignoring since ts doesn't recognize that tRPC
|
||||||
* parses FormData before reaching the route
|
* parses FormData before reaching the route
|
||||||
* @ts-ignore */
|
* @ts-ignore */
|
||||||
action={registerUser}
|
action={registerUser}
|
||||||
onSubmit={methods.handleSubmit(handleSubmit)}
|
|
||||||
>
|
>
|
||||||
<section className={styles.userInfo}>
|
<section className={styles.userInfo}>
|
||||||
<div className={styles.container}>
|
<div className={styles.container}>
|
||||||
@@ -94,12 +95,12 @@ export default function Form({ link, subtitle, title }: RegisterFormProps) {
|
|||||||
</header>
|
</header>
|
||||||
<div className={styles.nameInputs}>
|
<div className={styles.nameInputs}>
|
||||||
<Input
|
<Input
|
||||||
label={"firstName"}
|
label={intl.formatMessage({ id: "First name" })}
|
||||||
name="firstName"
|
name="firstName"
|
||||||
registerOptions={{ required: true }}
|
registerOptions={{ required: true }}
|
||||||
/>
|
/>
|
||||||
<Input
|
<Input
|
||||||
label={"lastName"}
|
label={intl.formatMessage({ id: "Last name" })}
|
||||||
name="lastName"
|
name="lastName"
|
||||||
registerOptions={{ required: true }}
|
registerOptions={{ required: true }}
|
||||||
/>
|
/>
|
||||||
@@ -170,14 +171,36 @@ export default function Form({ link, subtitle, title }: RegisterFormProps) {
|
|||||||
</Body>
|
</Body>
|
||||||
</Checkbox>
|
</Checkbox>
|
||||||
</section>
|
</section>
|
||||||
<Button
|
|
||||||
type="submit"
|
{/*
|
||||||
intent="primary"
|
This is a manual validation trigger workaround:
|
||||||
disabled={methods.formState.isSubmitting}
|
- The Controller component (which Input uses) doesn't re-render on submit,
|
||||||
data-testid="submit"
|
which prevents automatic error display.
|
||||||
>
|
- Future fix requires Input component refactoring (out of scope for now).
|
||||||
{intl.formatMessage({ id: "Sign up to Scandic Friends" })}
|
*/}
|
||||||
</Button>
|
{!methods.formState.isValid ? (
|
||||||
|
<Button
|
||||||
|
className={styles.signUpButton}
|
||||||
|
type="button"
|
||||||
|
theme="base"
|
||||||
|
intent="primary"
|
||||||
|
onClick={() => methods.trigger()}
|
||||||
|
data-testid="trigger-validation"
|
||||||
|
>
|
||||||
|
{intl.formatMessage({ id: "Sign up to Scandic Friends" })}
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<Button
|
||||||
|
className={styles.signUpButton}
|
||||||
|
type="submit"
|
||||||
|
theme="base"
|
||||||
|
intent="primary"
|
||||||
|
disabled={methods.formState.isSubmitting}
|
||||||
|
data-testid="submit"
|
||||||
|
>
|
||||||
|
{intl.formatMessage({ id: "Sign up to Scandic Friends" })}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
</form>
|
</form>
|
||||||
</FormProvider>
|
</FormProvider>
|
||||||
</section>
|
</section>
|
||||||
@@ -3,19 +3,14 @@ import { z } from "zod"
|
|||||||
import { passwordValidator } from "@/utils/passwordValidator"
|
import { passwordValidator } from "@/utils/passwordValidator"
|
||||||
import { phoneValidator } from "@/utils/phoneValidator"
|
import { phoneValidator } from "@/utils/phoneValidator"
|
||||||
|
|
||||||
export const registerSchema = z.object({
|
const countryRequiredMsg = "Country is required"
|
||||||
firstName: z
|
export const signUpSchema = z.object({
|
||||||
.string()
|
firstName: z.string().max(250).trim().min(1, {
|
||||||
.max(250)
|
message: "First name is required",
|
||||||
.refine((value) => value.trim().length > 0, {
|
}),
|
||||||
message: "First name is required",
|
lastName: z.string().max(250).trim().min(1, {
|
||||||
}),
|
message: "Last name is required",
|
||||||
lastName: z
|
}),
|
||||||
.string()
|
|
||||||
.max(250)
|
|
||||||
.refine((value) => value.trim().length > 0, {
|
|
||||||
message: "Last name is required",
|
|
||||||
}),
|
|
||||||
email: z.string().max(250).email(),
|
email: z.string().max(250).email(),
|
||||||
phoneNumber: phoneValidator(
|
phoneNumber: phoneValidator(
|
||||||
"Phone is required",
|
"Phone is required",
|
||||||
@@ -23,7 +18,12 @@ export const registerSchema = z.object({
|
|||||||
),
|
),
|
||||||
dateOfBirth: z.string().min(1),
|
dateOfBirth: z.string().min(1),
|
||||||
address: z.object({
|
address: z.object({
|
||||||
countryCode: z.string(),
|
countryCode: z
|
||||||
|
.string({
|
||||||
|
required_error: countryRequiredMsg,
|
||||||
|
invalid_type_error: countryRequiredMsg,
|
||||||
|
})
|
||||||
|
.min(1, countryRequiredMsg),
|
||||||
zipCode: z.string().min(1),
|
zipCode: z.string().min(1),
|
||||||
}),
|
}),
|
||||||
password: passwordValidator("Password is required"),
|
password: passwordValidator("Password is required"),
|
||||||
@@ -32,4 +32,4 @@ export const registerSchema = z.object({
|
|||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
|
|
||||||
export type RegisterSchema = z.infer<typeof registerSchema>
|
export type SignUpSchema = z.infer<typeof signUpSchema>
|
||||||
@@ -14,15 +14,16 @@
|
|||||||
|
|
||||||
.imageContainer {
|
.imageContainer {
|
||||||
grid-area: image;
|
grid-area: image;
|
||||||
|
position: relative;
|
||||||
|
height: 100%;
|
||||||
|
width: 116px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tripAdvisor {
|
.tripAdvisor {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.image {
|
.imageContainer img {
|
||||||
height: 100%;
|
|
||||||
width: 116px;
|
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -77,6 +78,8 @@
|
|||||||
|
|
||||||
.imageContainer {
|
.imageContainer {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
min-height: 200px;
|
||||||
|
width: 518px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tripAdvisor {
|
.tripAdvisor {
|
||||||
@@ -86,10 +89,6 @@
|
|||||||
top: 7px;
|
top: 7px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.image {
|
|
||||||
width: 518px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hotelInformation {
|
.hotelInformation {
|
||||||
padding-top: var(--Spacing-x2);
|
padding-top: var(--Spacing-x2);
|
||||||
padding-right: var(--Spacing-x2);
|
padding-right: var(--Spacing-x2);
|
||||||
|
|||||||
@@ -11,10 +11,11 @@ import Title from "@/components/TempDesignSystem/Text/Title"
|
|||||||
import { getIntl } from "@/i18n"
|
import { getIntl } from "@/i18n"
|
||||||
|
|
||||||
import ReadMore from "../ReadMore"
|
import ReadMore from "../ReadMore"
|
||||||
|
import ImageGallery from "../SelectRate/ImageGallery"
|
||||||
|
|
||||||
import styles from "./hotelCard.module.css"
|
import styles from "./hotelCard.module.css"
|
||||||
|
|
||||||
import { HotelCardProps } from "@/types/components/hotelReservation/selectHotel/hotelCardProps"
|
import type { HotelCardProps } from "@/types/components/hotelReservation/selectHotel/hotelCardProps"
|
||||||
|
|
||||||
export default async function HotelCard({ hotel }: HotelCardProps) {
|
export default async function HotelCard({ hotel }: HotelCardProps) {
|
||||||
const intl = await getIntl()
|
const intl = await getIntl()
|
||||||
@@ -27,13 +28,15 @@ export default async function HotelCard({ hotel }: HotelCardProps) {
|
|||||||
return (
|
return (
|
||||||
<article className={styles.card}>
|
<article className={styles.card}>
|
||||||
<section className={styles.imageContainer}>
|
<section className={styles.imageContainer}>
|
||||||
<Image
|
{hotelData.gallery && (
|
||||||
src={hotelData.hotelContent.images.imageSizes.medium}
|
<ImageGallery
|
||||||
alt={hotelData.hotelContent.images.metaData.altText}
|
title={hotelData.name}
|
||||||
width={300}
|
images={[
|
||||||
height={200}
|
hotelData.hotelContent.images,
|
||||||
className={styles.image}
|
...hotelData.gallery.heroImages,
|
||||||
/>
|
]}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<div className={styles.tripAdvisor}>
|
<div className={styles.tripAdvisor}>
|
||||||
<Chip intent="primary" className={styles.tripAdvisor}>
|
<Chip intent="primary" className={styles.tripAdvisor}>
|
||||||
<TripAdvisorIcon color="white" />
|
<TripAdvisorIcon color="white" />
|
||||||
@@ -102,7 +105,11 @@ export default async function HotelCard({ hotel }: HotelCardProps) {
|
|||||||
className={styles.button}
|
className={styles.button}
|
||||||
>
|
>
|
||||||
{/* TODO: Localize link and also use correct search params */}
|
{/* TODO: Localize link and also use correct search params */}
|
||||||
<Link href="/en/hotelreservation/select-rate" color="none">
|
<Link
|
||||||
|
href={`/en/hotelreservation/select-rate?hotel=${hotelData.operaId}`}
|
||||||
|
color="none"
|
||||||
|
keepSearchParams
|
||||||
|
>
|
||||||
{intl.formatMessage({ id: "See rooms" })}
|
{intl.formatMessage({ id: "See rooms" })}
|
||||||
</Link>
|
</Link>
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -12,10 +12,6 @@
|
|||||||
gap: var(--Spacing-x2);
|
gap: var(--Spacing-x2);
|
||||||
}
|
}
|
||||||
|
|
||||||
.image {
|
|
||||||
border-radius: var(--Corner-radius-Medium);
|
|
||||||
}
|
|
||||||
|
|
||||||
.imageWrapper {
|
.imageWrapper {
|
||||||
position: relative;
|
position: relative;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
@@ -24,6 +20,10 @@
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.imageWrapper img {
|
||||||
|
border-radius: var(--Corner-radius-Medium);
|
||||||
|
}
|
||||||
|
|
||||||
.tripAdvisor {
|
.tripAdvisor {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import Caption from "@/components/TempDesignSystem/Text/Caption"
|
|||||||
import Title from "@/components/TempDesignSystem/Text/Title"
|
import Title from "@/components/TempDesignSystem/Text/Title"
|
||||||
|
|
||||||
import ReadMore from "../../ReadMore"
|
import ReadMore from "../../ReadMore"
|
||||||
|
import ImageGallery from "../ImageGallery"
|
||||||
|
|
||||||
import styles from "./hotelInfoCard.module.css"
|
import styles from "./hotelInfoCard.module.css"
|
||||||
|
|
||||||
@@ -28,12 +29,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}>
|
||||||
<Image
|
|
||||||
src={hotelAttributes.hotelContent.images.imageSizes.medium}
|
|
||||||
alt={hotelAttributes.hotelContent.images.metaData.altText}
|
|
||||||
className={styles.image}
|
|
||||||
fill
|
|
||||||
/>
|
|
||||||
{hotelAttributes.ratings?.tripAdvisor && (
|
{hotelAttributes.ratings?.tripAdvisor && (
|
||||||
<div className={styles.tripAdvisor}>
|
<div className={styles.tripAdvisor}>
|
||||||
<TripAdvisorIcon color="burgundy" />
|
<TripAdvisorIcon color="burgundy" />
|
||||||
@@ -42,7 +37,15 @@ export default function HotelInfoCard({ hotelData }: HotelInfoCardProps) {
|
|||||||
</Caption>
|
</Caption>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{/* TODO: gallery icon and image carousel */}
|
{hotelAttributes.gallery && (
|
||||||
|
<ImageGallery
|
||||||
|
title={hotelAttributes.name}
|
||||||
|
images={[
|
||||||
|
hotelAttributes.hotelContent.images,
|
||||||
|
...hotelAttributes.gallery.heroImages,
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.hotelContent}>
|
<div className={styles.hotelContent}>
|
||||||
<div className={styles.hotelInformation}>
|
<div className={styles.hotelInformation}>
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
.galleryIcon {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 16px;
|
||||||
|
right: 16px;
|
||||||
|
max-height: 32px;
|
||||||
|
width: 48px;
|
||||||
|
background-color: rgba(0, 0, 0, 0.6);
|
||||||
|
padding: var(--Spacing-x-quarter) var(--Spacing-x-half);
|
||||||
|
border-radius: var(--Corner-radius-Small);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--Spacing-x-quarter);
|
||||||
|
}
|
||||||
|
|
||||||
|
.triggerArea {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import { GalleryIcon } from "@/components/Icons"
|
||||||
|
import Image from "@/components/Image"
|
||||||
|
import Lightbox from "@/components/Lightbox"
|
||||||
|
import Footnote from "@/components/TempDesignSystem/Text/Footnote"
|
||||||
|
|
||||||
|
import styles from "./imageGallery.module.css"
|
||||||
|
|
||||||
|
import type { ImageGalleryProps } from "@/types/components/hotelReservation/selectRate/imageGallery"
|
||||||
|
|
||||||
|
export default function ImageGallery({ images, title }: ImageGalleryProps) {
|
||||||
|
return (
|
||||||
|
<Lightbox
|
||||||
|
images={images.map((image) => ({
|
||||||
|
url: image.imageSizes.small,
|
||||||
|
alt: image.metaData.altText,
|
||||||
|
title: image.metaData.title,
|
||||||
|
}))}
|
||||||
|
dialogTitle={title}
|
||||||
|
>
|
||||||
|
<div className={styles.triggerArea} id="lightboxTrigger">
|
||||||
|
<Image
|
||||||
|
src={images[0].imageSizes.medium}
|
||||||
|
alt={images[0].metaData.altText}
|
||||||
|
className={styles.image}
|
||||||
|
fill
|
||||||
|
/>
|
||||||
|
<div className={styles.galleryIcon}>
|
||||||
|
<GalleryIcon color="white" />
|
||||||
|
<Footnote color="white" type="label">
|
||||||
|
{images.length}
|
||||||
|
</Footnote>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Lightbox>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -5,18 +5,18 @@ import { useIntl } from "react-intl"
|
|||||||
import { RateDefinition } from "@/server/routers/hotels/output"
|
import { RateDefinition } from "@/server/routers/hotels/output"
|
||||||
|
|
||||||
import FlexibilityOption from "@/components/HotelReservation/SelectRate/RoomSelection/FlexibilityOption"
|
import FlexibilityOption from "@/components/HotelReservation/SelectRate/RoomSelection/FlexibilityOption"
|
||||||
import { ChevronRightSmallIcon, GalleryIcon } from "@/components/Icons"
|
import { ChevronRightSmallIcon } from "@/components/Icons"
|
||||||
import Image from "@/components/Image"
|
|
||||||
import Lightbox from "@/components/Lightbox"
|
|
||||||
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 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 ImageGallery from "../../ImageGallery"
|
||||||
|
|
||||||
import styles from "./roomCard.module.css"
|
import styles from "./roomCard.module.css"
|
||||||
|
|
||||||
import { RoomCardProps } from "@/types/components/hotelReservation/selectRate/roomCard"
|
import type { RoomCardProps } from "@/types/components/hotelReservation/selectRate/roomCard"
|
||||||
|
|
||||||
export default function RoomCard({
|
export default function RoomCard({
|
||||||
rateDefinitions,
|
rateDefinitions,
|
||||||
@@ -25,7 +25,6 @@ export default function RoomCard({
|
|||||||
handleSelectRate,
|
handleSelectRate,
|
||||||
}: RoomCardProps) {
|
}: RoomCardProps) {
|
||||||
const intl = useIntl()
|
const intl = useIntl()
|
||||||
|
|
||||||
const saveRate = rateDefinitions.find(
|
const saveRate = rateDefinitions.find(
|
||||||
// TODO: Update string when API has decided
|
// TODO: Update string when API has decided
|
||||||
(rate) => rate.cancellationRule === "NonCancellable"
|
(rate) => rate.cancellationRule === "NonCancellable"
|
||||||
@@ -153,26 +152,8 @@ export default function RoomCard({
|
|||||||
)}
|
)}
|
||||||
{/*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. */}
|
||||||
<Image
|
|
||||||
src={mainImage.imageSizes.small}
|
|
||||||
alt={mainImage.metaData.altText}
|
|
||||||
width={330}
|
|
||||||
height={185}
|
|
||||||
/>
|
|
||||||
{images && (
|
{images && (
|
||||||
<Lightbox
|
<ImageGallery images={images} title={roomConfiguration.roomType} />
|
||||||
images={images.map((image) => ({
|
|
||||||
url: image.imageSizes.small,
|
|
||||||
alt: image.metaData.altText,
|
|
||||||
title: image.metaData.title,
|
|
||||||
}))}
|
|
||||||
dialogTitle={roomConfiguration.roomType}
|
|
||||||
>
|
|
||||||
<div className={styles.galleryIcon} id="lightboxTrigger">
|
|
||||||
<GalleryIcon color="white" />
|
|
||||||
<Footnote color="white">{images.length}</Footnote>
|
|
||||||
</div>
|
|
||||||
</Lightbox>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -77,17 +77,3 @@
|
|||||||
min-height: 185px;
|
min-height: 185px;
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.galleryIcon {
|
|
||||||
position: absolute;
|
|
||||||
bottom: 16px;
|
|
||||||
right: 16px;
|
|
||||||
height: 24px;
|
|
||||||
background-color: rgba(64, 57, 55, 0.9);
|
|
||||||
padding: 0 var(--Spacing-x-half);
|
|
||||||
border-radius: var(--Corner-radius-Small);
|
|
||||||
cursor: pointer;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: var(--Spacing-x-quarter);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ import type { SidepeekContent } from "@/types/trpc/routers/contentstack/siteConf
|
|||||||
export interface AlertProps extends VariantProps<typeof alertVariants> {
|
export interface AlertProps extends VariantProps<typeof alertVariants> {
|
||||||
className?: string
|
className?: string
|
||||||
type: AlertTypeEnum
|
type: AlertTypeEnum
|
||||||
heading?: string
|
heading?: string | null
|
||||||
text: string
|
text?: string | null
|
||||||
phoneContact?: {
|
phoneContact?: {
|
||||||
displayText: string
|
displayText: string
|
||||||
phoneNumber?: string
|
phoneNumber?: string
|
||||||
|
|||||||
@@ -27,6 +27,10 @@ export default function Alert({
|
|||||||
})
|
})
|
||||||
const Icon = getIconByAlertType(type)
|
const Icon = getIconByAlertType(type)
|
||||||
|
|
||||||
|
if (!text && !heading) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className={classNames}>
|
<section className={classNames}>
|
||||||
<div className={styles.content}>
|
<div className={styles.content}>
|
||||||
|
|||||||
@@ -21,7 +21,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.checkbox {
|
.checkbox {
|
||||||
flex-grow: 1;
|
|
||||||
width: 24px;
|
width: 24px;
|
||||||
height: 24px;
|
height: 24px;
|
||||||
min-width: 24px;
|
min-width: 24px;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
.phone {
|
.phone {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: var(--Spacing-x2);
|
gap: var(--Spacing-x2);
|
||||||
grid-template-columns: max(164px) 1fr;
|
grid-template-columns: minmax(124px, 164px) 1fr;
|
||||||
|
|
||||||
--react-international-phone-background-color: var(--Main-Grey-White);
|
--react-international-phone-background-color: var(--Main-Grey-White);
|
||||||
--react-international-phone-border-color: var(--Scandic-Beige-40);
|
--react-international-phone-border-color: var(--Scandic-Beige-40);
|
||||||
|
|||||||
@@ -51,9 +51,10 @@ export default function Link({
|
|||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
const fullUrl = useMemo(() => {
|
const fullUrl = useMemo(() => {
|
||||||
const search =
|
if (!keepSearchParams || !searchParams.size) return href
|
||||||
keepSearchParams && searchParams.size ? `?${searchParams}` : ""
|
|
||||||
return `${href}${search}`
|
const delimiter = href.includes("?") ? "&" : "?"
|
||||||
|
return `${href}${delimiter}${searchParams}`
|
||||||
}, [href, searchParams, keepSearchParams])
|
}, [href, searchParams, keepSearchParams])
|
||||||
|
|
||||||
// TODO: Remove this check (and hook) and only return <Link /> when current web is deleted
|
// TODO: Remove this check (and hook) and only return <Link /> when current web is deleted
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
import { z } from "zod"
|
import { z } from "zod"
|
||||||
|
|
||||||
|
import { dt } from "@/lib/dt"
|
||||||
import { toLang } from "@/server/utils"
|
import { toLang } from "@/server/utils"
|
||||||
|
|
||||||
import { imageMetaDataSchema, imageSizesSchema } from "./schemas/image"
|
import { imageMetaDataSchema, imageSizesSchema } from "./schemas/image"
|
||||||
import { roomSchema } from "./schemas/room"
|
import { roomSchema } from "./schemas/room"
|
||||||
import { getPoiGroupByCategoryName } from "./utils"
|
import { getPoiGroupByCategoryName } from "./utils"
|
||||||
|
|
||||||
|
import { AlertTypeEnum } from "@/types/enums/alert"
|
||||||
import { FacilityEnum } from "@/types/enums/facilities"
|
import { FacilityEnum } from "@/types/enums/facilities"
|
||||||
import { PointOfInterestCategoryNameEnum } from "@/types/hotel"
|
import { PointOfInterestCategoryNameEnum } from "@/types/hotel"
|
||||||
|
|
||||||
@@ -160,6 +162,21 @@ export const facilitySchema = z.object({
|
|||||||
),
|
),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
export const gallerySchema = z.object({
|
||||||
|
heroImages: z.array(
|
||||||
|
z.object({
|
||||||
|
metaData: imageMetaDataSchema,
|
||||||
|
imageSizes: imageSizesSchema,
|
||||||
|
})
|
||||||
|
),
|
||||||
|
smallerImages: z.array(
|
||||||
|
z.object({
|
||||||
|
metaData: imageMetaDataSchema,
|
||||||
|
imageSizes: imageSizesSchema,
|
||||||
|
})
|
||||||
|
),
|
||||||
|
})
|
||||||
|
|
||||||
const healthFacilitySchema = z.object({
|
const healthFacilitySchema = z.object({
|
||||||
type: z.string(),
|
type: z.string(),
|
||||||
content: z.object({
|
content: z.object({
|
||||||
@@ -321,6 +338,7 @@ const socialMediaSchema = z.object({
|
|||||||
|
|
||||||
const metaSpecialAlertSchema = z.object({
|
const metaSpecialAlertSchema = z.object({
|
||||||
type: z.string(),
|
type: z.string(),
|
||||||
|
title: z.string().optional(),
|
||||||
description: z.string().optional(),
|
description: z.string().optional(),
|
||||||
displayInBookingFlow: z.boolean(),
|
displayInBookingFlow: z.boolean(),
|
||||||
startDate: z.string(),
|
startDate: z.string(),
|
||||||
@@ -328,7 +346,23 @@ const metaSpecialAlertSchema = z.object({
|
|||||||
})
|
})
|
||||||
|
|
||||||
const metaSchema = z.object({
|
const metaSchema = z.object({
|
||||||
specialAlerts: z.array(metaSpecialAlertSchema),
|
specialAlerts: z
|
||||||
|
.array(metaSpecialAlertSchema)
|
||||||
|
.transform((data) => {
|
||||||
|
const now = dt().utc().format("YYYY-MM-DD")
|
||||||
|
const filteredAlerts = data.filter((alert) => {
|
||||||
|
const shouldShowNow = alert.startDate <= now && alert.endDate >= now
|
||||||
|
const hasText = alert.description || alert.title
|
||||||
|
return shouldShowNow && hasText
|
||||||
|
})
|
||||||
|
return filteredAlerts.map((alert, idx) => ({
|
||||||
|
id: `alert-${alert.type}-${idx}`,
|
||||||
|
type: AlertTypeEnum.Info,
|
||||||
|
heading: alert.title || null,
|
||||||
|
text: alert.description || null,
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
.default([]),
|
||||||
})
|
})
|
||||||
|
|
||||||
const relationshipsSchema = z.object({
|
const relationshipsSchema = z.object({
|
||||||
@@ -422,6 +456,7 @@ export const getHotelDataSchema = z.object({
|
|||||||
conferencesAndMeetings: facilitySchema.optional(),
|
conferencesAndMeetings: facilitySchema.optional(),
|
||||||
healthAndWellness: facilitySchema.optional(),
|
healthAndWellness: facilitySchema.optional(),
|
||||||
restaurantImages: facilitySchema.optional(),
|
restaurantImages: facilitySchema.optional(),
|
||||||
|
gallery: gallerySchema.optional(),
|
||||||
}),
|
}),
|
||||||
relationships: relationshipsSchema,
|
relationships: relationshipsSchema,
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -219,6 +219,7 @@ export const hotelQueryRouter = router({
|
|||||||
|
|
||||||
const hotelAttributes = validatedHotelData.data.data.attributes
|
const hotelAttributes = validatedHotelData.data.data.attributes
|
||||||
const images = extractHotelImages(hotelAttributes)
|
const images = extractHotelImages(hotelAttributes)
|
||||||
|
const hotelAlerts = hotelAttributes.meta?.specialAlerts || []
|
||||||
|
|
||||||
const roomCategories = included
|
const roomCategories = included
|
||||||
? included.filter((item) => item.type === "roomcategories")
|
? included.filter((item) => item.type === "roomcategories")
|
||||||
@@ -262,6 +263,7 @@ export const hotelQueryRouter = router({
|
|||||||
roomCategories,
|
roomCategories,
|
||||||
activitiesCard: activities?.upcoming_activities_card,
|
activitiesCard: activities?.upcoming_activities_card,
|
||||||
facilities,
|
facilities,
|
||||||
|
alerts: hotelAlerts,
|
||||||
faq: contentstackData?.faq,
|
faq: contentstackData?.faq,
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
export type RegisterFormProps = {
|
export type SignUpFormProps = {
|
||||||
link?: { href: string; text: string }
|
link?: { href: string; text: string }
|
||||||
subtitle?: string
|
subtitle?: string
|
||||||
title: string
|
title: string
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
import type { GalleryImages } from "@/types/hotel"
|
||||||
|
|
||||||
|
export type ImageGalleryProps = { images: GalleryImages; title: string }
|
||||||
@@ -7,9 +7,9 @@ interface Child {
|
|||||||
|
|
||||||
interface Room {
|
interface Room {
|
||||||
adults: number
|
adults: number
|
||||||
roomtypecode: string
|
roomcode?: string
|
||||||
ratecode: string
|
ratecode?: string
|
||||||
child: Child[]
|
child?: Child[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SelectRateSearchParams {
|
export interface SelectRateSearchParams {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { z } from "zod"
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
facilitySchema,
|
facilitySchema,
|
||||||
|
gallerySchema,
|
||||||
getHotelDataSchema,
|
getHotelDataSchema,
|
||||||
parkingSchema,
|
parkingSchema,
|
||||||
pointOfInterestSchema,
|
pointOfInterestSchema,
|
||||||
@@ -13,7 +14,6 @@ export type HotelData = z.infer<typeof getHotelDataSchema>
|
|||||||
export type Hotel = HotelData["data"]["attributes"]
|
export type Hotel = HotelData["data"]["attributes"]
|
||||||
export type HotelAddress = HotelData["data"]["attributes"]["address"]
|
export type HotelAddress = HotelData["data"]["attributes"]["address"]
|
||||||
export type HotelLocation = HotelData["data"]["attributes"]["location"]
|
export type HotelLocation = HotelData["data"]["attributes"]["location"]
|
||||||
|
|
||||||
export type Amenities = HotelData["data"]["attributes"]["detailedFacilities"]
|
export type Amenities = HotelData["data"]["attributes"]["detailedFacilities"]
|
||||||
|
|
||||||
type HotelRatings = HotelData["data"]["attributes"]["ratings"]
|
type HotelRatings = HotelData["data"]["attributes"]["ratings"]
|
||||||
@@ -22,6 +22,8 @@ export type HotelTripAdvisor =
|
|||||||
| undefined
|
| undefined
|
||||||
|
|
||||||
export type RoomData = z.infer<typeof roomSchema>
|
export type RoomData = z.infer<typeof roomSchema>
|
||||||
|
export type GallerySchema = z.infer<typeof gallerySchema>
|
||||||
|
export type GalleryImages = GallerySchema["heroImages"]
|
||||||
|
|
||||||
export type PointOfInterest = z.output<typeof pointOfInterestSchema>
|
export type PointOfInterest = z.output<typeof pointOfInterestSchema>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user