feat: add conditional signup values
This commit is contained in:
67
actions/registerUserBookingFlow.ts
Normal file
67
actions/registerUserBookingFlow.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
"use server"
|
||||
|
||||
import { parsePhoneNumber } from "libphonenumber-js"
|
||||
import { z } from "zod"
|
||||
|
||||
import { serviceServerActionProcedure } from "@/server/trpc"
|
||||
|
||||
import { phoneValidator } from "@/utils/phoneValidator"
|
||||
|
||||
const registerUserPayload = z.object({
|
||||
firstName: z.string(),
|
||||
lastName: z.string(),
|
||||
dateOfBirth: z.string(),
|
||||
address: z.object({
|
||||
countryCode: z.string(),
|
||||
zipCode: z.string(),
|
||||
}),
|
||||
email: z.string(),
|
||||
phoneNumber: phoneValidator("Phone is required"),
|
||||
})
|
||||
|
||||
export const registerUserBookingFlow = serviceServerActionProcedure
|
||||
.input(registerUserPayload)
|
||||
.mutation(async function ({ ctx, input }) {
|
||||
const payload = {
|
||||
...input,
|
||||
language: ctx.lang,
|
||||
phoneNumber: parsePhoneNumber(input.phoneNumber)
|
||||
.formatNational()
|
||||
.replace(/\s+/g, ""),
|
||||
}
|
||||
|
||||
// TODO: Consume the API to register the user as soon as passwordless signup is enabled.
|
||||
// let apiResponse
|
||||
// try {
|
||||
// apiResponse = await api.post(api.endpoints.v1.profile, {
|
||||
// body: payload,
|
||||
// headers: {
|
||||
// Authorization: `Bearer ${ctx.serviceToken}`,
|
||||
// },
|
||||
// })
|
||||
// } catch (error) {
|
||||
// console.error("Unexpected error", error)
|
||||
// return { success: false, error: "Unexpected error" }
|
||||
// }
|
||||
|
||||
// if (!apiResponse.ok) {
|
||||
// const text = await apiResponse.text()
|
||||
// console.error(text)
|
||||
// console.error(
|
||||
// "registerUserBookingFlow api error",
|
||||
// JSON.stringify({
|
||||
// query: input,
|
||||
// error: {
|
||||
// status: apiResponse.status,
|
||||
// statusText: apiResponse.statusText,
|
||||
// error: text,
|
||||
// },
|
||||
// })
|
||||
// )
|
||||
// return { success: false, error: "API error" }
|
||||
// }
|
||||
// const json = await apiResponse.json()
|
||||
// console.log("registerUserBookingFlow: json", json)
|
||||
|
||||
return { success: true, data: payload }
|
||||
})
|
||||
@@ -56,7 +56,7 @@ export default function AdultSelector({ roomIndex = 0 }: AdultSelectorProps) {
|
||||
|
||||
return (
|
||||
<section className={styles.container}>
|
||||
<Caption color="uiTextHighContrast" textTransform="bold">
|
||||
<Caption color="uiTextHighContrast" type="bold">
|
||||
{adultsLabel}
|
||||
</Caption>
|
||||
<Counter
|
||||
|
||||
@@ -50,7 +50,7 @@ export default function ChildSelector({ roomIndex = 0 }: ChildSelectorProps) {
|
||||
return (
|
||||
<>
|
||||
<section className={styles.container}>
|
||||
<Caption color="uiTextHighContrast" textTransform="bold">
|
||||
<Caption color="uiTextHighContrast" type="bold">
|
||||
{childrenLabel}
|
||||
</Caption>
|
||||
<Counter
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
"use client"
|
||||
|
||||
import { useEffect, useState } from "react"
|
||||
import { useWatch } from "react-hook-form"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import { privacyPolicy } from "@/constants/currentWebHrefs"
|
||||
|
||||
import Checkbox from "@/components/TempDesignSystem/Form/Checkbox"
|
||||
import CheckboxCard from "@/components/TempDesignSystem/Form/ChoiceCard/Checkbox"
|
||||
import DateSelect from "@/components/TempDesignSystem/Form/Date"
|
||||
import Input from "@/components/TempDesignSystem/Form/Input"
|
||||
import Link from "@/components/TempDesignSystem/Link"
|
||||
import Body from "@/components/TempDesignSystem/Text/Body"
|
||||
import Caption from "@/components/TempDesignSystem/Text/Caption"
|
||||
import useLang from "@/hooks/useLang"
|
||||
|
||||
import styles from "./signup.module.css"
|
||||
|
||||
export default function Signup({ name }: { name: string }) {
|
||||
const lang = useLang()
|
||||
const intl = useIntl()
|
||||
|
||||
const [isJoinChecked, setIsJoinChecked] = useState(false)
|
||||
|
||||
const joinValue = useWatch({ name })
|
||||
|
||||
useEffect(() => {
|
||||
// In order to avoid hydration errors the state needs to be set as side effect,
|
||||
// since the join value can come from search params
|
||||
setIsJoinChecked(joinValue)
|
||||
}, [joinValue])
|
||||
|
||||
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" }) },
|
||||
]
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<CheckboxCard
|
||||
highlightSubtitle
|
||||
list={list}
|
||||
name={name}
|
||||
subtitle={intl.formatMessage(
|
||||
{
|
||||
id: "{difference}{amount} {currency}",
|
||||
},
|
||||
{
|
||||
amount: "491",
|
||||
currency: "SEK",
|
||||
difference: "-",
|
||||
}
|
||||
)}
|
||||
title={intl.formatMessage({ id: "Join Scandic Friends" })}
|
||||
/>
|
||||
{isJoinChecked ? (
|
||||
<div className={styles.additionalFormData}>
|
||||
<div className={styles.dateField}>
|
||||
<header>
|
||||
<Caption type="bold">
|
||||
{intl.formatMessage({ id: "Birth date" })} *
|
||||
</Caption>
|
||||
</header>
|
||||
<DateSelect
|
||||
name="dateOfBirth"
|
||||
registerOptions={{ required: true }}
|
||||
/>
|
||||
<Input
|
||||
name="zipCode"
|
||||
label={intl.formatMessage({ id: "Zip code" })}
|
||||
registerOptions={{ required: true }}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Checkbox name="termsAccepted" registerOptions={{ required: true }}>
|
||||
<Body>
|
||||
{intl.formatMessage({
|
||||
id: "Yes, I accept the Terms and conditions for Scandic Friends and understand that Scandic will process my personal data in accordance with",
|
||||
})}{" "}
|
||||
<Link
|
||||
variant="underscored"
|
||||
color="peach80"
|
||||
target="_blank"
|
||||
href={privacyPolicy[lang]}
|
||||
>
|
||||
{intl.formatMessage({ id: "Scandic's Privacy Policy." })}
|
||||
</Link>
|
||||
</Body>
|
||||
</Checkbox>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
.container {
|
||||
display: grid;
|
||||
grid-column: 1/-1;
|
||||
gap: var(--Spacing-x3);
|
||||
}
|
||||
|
||||
.additionalFormData {
|
||||
display: grid;
|
||||
gap: var(--Spacing-x4);
|
||||
}
|
||||
|
||||
.dateField {
|
||||
display: grid;
|
||||
gap: var(--Spacing-x1);
|
||||
}
|
||||
@@ -6,14 +6,16 @@ import { useIntl } from "react-intl"
|
||||
|
||||
import { useEnterDetailsStore } from "@/stores/enter-details"
|
||||
|
||||
import { registerUserBookingFlow } from "@/actions/registerUserBookingFlow"
|
||||
import Button from "@/components/TempDesignSystem/Button"
|
||||
import CheckboxCard from "@/components/TempDesignSystem/Form/ChoiceCard/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 { toast } from "@/components/TempDesignSystem/Toasts"
|
||||
|
||||
import { detailsSchema, signedInDetailsSchema } from "./schema"
|
||||
import Signup from "./Signup"
|
||||
|
||||
import styles from "./details.module.css"
|
||||
|
||||
@@ -25,28 +27,30 @@ import type {
|
||||
const formID = "enter-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 initialData = useEnterDetailsStore((state) => ({
|
||||
countryCode: state.data.countryCode,
|
||||
email: state.data.email,
|
||||
firstname: state.data.firstname,
|
||||
lastname: state.data.lastname,
|
||||
firstName: state.data.firstName,
|
||||
lastName: state.data.lastName,
|
||||
phoneNumber: state.data.phoneNumber,
|
||||
join: state.data.join,
|
||||
dateOfBirth: state.data.dateOfBirth,
|
||||
zipCode: state.data.zipCode,
|
||||
termsAccepted: state.data.termsAccepted,
|
||||
}))
|
||||
|
||||
const methods = useForm<DetailsSchema>({
|
||||
defaultValues: {
|
||||
countryCode: user?.address?.countryCode ?? initialData.countryCode,
|
||||
email: user?.email ?? initialData.email,
|
||||
firstname: user?.firstName ?? initialData.firstname,
|
||||
lastname: user?.lastName ?? initialData.lastname,
|
||||
firstName: user?.firstName ?? initialData.firstName,
|
||||
lastName: user?.lastName ?? initialData.lastName,
|
||||
phoneNumber: user?.phoneNumber ?? initialData.phoneNumber,
|
||||
//@ts-expect-error: We use a literal for join to be true or false, which does not convert to a boolean
|
||||
join: initialData.join,
|
||||
dateOfBirth: initialData.dateOfBirth,
|
||||
zipCode: initialData.zipCode,
|
||||
termsAccepted: initialData.termsAccepted,
|
||||
},
|
||||
criteriaMode: "all",
|
||||
mode: "all",
|
||||
@@ -56,10 +60,39 @@ export default function Details({ user }: DetailsProps) {
|
||||
|
||||
const completeStep = useEnterDetailsStore((state) => state.completeStep)
|
||||
|
||||
// const errorMessage = intl.formatMessage({
|
||||
// id: "An error occurred. Please try again.",
|
||||
// })
|
||||
|
||||
const onSubmit = useCallback(
|
||||
(values: DetailsSchema) => {
|
||||
async function (values: DetailsSchema) {
|
||||
if (values.join) {
|
||||
const signupVals = {
|
||||
firstName: values.firstName,
|
||||
lastName: values.lastName,
|
||||
email: values.email,
|
||||
phoneNumber: values.phoneNumber,
|
||||
address: {
|
||||
zipCode: values.zipCode,
|
||||
countryCode: values.countryCode,
|
||||
},
|
||||
dateOfBirth: values.dateOfBirth,
|
||||
}
|
||||
|
||||
const res = await registerUserBookingFlow(signupVals)
|
||||
if (!res.success) {
|
||||
// if (res.error) {
|
||||
// toast.error(res.error)
|
||||
// } else {
|
||||
// toast.error(errorMessage)
|
||||
// }
|
||||
return
|
||||
}
|
||||
console.log("Signed up user: ", res)
|
||||
}
|
||||
completeStep(values)
|
||||
},
|
||||
|
||||
[completeStep]
|
||||
)
|
||||
|
||||
@@ -77,14 +110,14 @@ export default function Details({ user }: DetailsProps) {
|
||||
onSubmit={methods.handleSubmit(onSubmit)}
|
||||
>
|
||||
<Input
|
||||
label={intl.formatMessage({ id: "Firstname" })}
|
||||
name="firstname"
|
||||
label={intl.formatMessage({ id: "First name" })}
|
||||
name="firstName"
|
||||
readOnly={!!user}
|
||||
registerOptions={{ required: true }}
|
||||
/>
|
||||
<Input
|
||||
label={intl.formatMessage({ id: "Lastname" })}
|
||||
name="lastname"
|
||||
label={intl.formatMessage({ id: "Last name" })}
|
||||
name="lastName"
|
||||
readOnly={!!user}
|
||||
registerOptions={{ required: true }}
|
||||
/>
|
||||
@@ -109,26 +142,9 @@ export default function Details({ user }: DetailsProps) {
|
||||
readOnly={!!user}
|
||||
registerOptions={{ required: true }}
|
||||
/>
|
||||
{user ? null : <Signup name="join" />}
|
||||
</form>
|
||||
<footer className={styles.footer}>
|
||||
{user ? null : (
|
||||
<CheckboxCard
|
||||
highlightSubtitle
|
||||
list={list}
|
||||
name="join"
|
||||
subtitle={intl.formatMessage(
|
||||
{
|
||||
id: "{difference}{amount} {currency}",
|
||||
},
|
||||
{
|
||||
amount: "491",
|
||||
currency: "SEK",
|
||||
difference: "-",
|
||||
}
|
||||
)}
|
||||
title={intl.formatMessage({ id: "Join Scandic Friends" })}
|
||||
/>
|
||||
)}
|
||||
<Button
|
||||
disabled={!methods.formState.isValid}
|
||||
form={formID}
|
||||
|
||||
@@ -2,18 +2,49 @@ import { z } from "zod"
|
||||
|
||||
import { phoneValidator } from "@/utils/phoneValidator"
|
||||
|
||||
export const detailsSchema = z.object({
|
||||
export const baseDetailsSchema = z.object({
|
||||
countryCode: z.string(),
|
||||
email: z.string().email(),
|
||||
firstname: z.string(),
|
||||
lastname: z.string(),
|
||||
firstName: z.string(),
|
||||
lastName: z.string(),
|
||||
phoneNumber: phoneValidator(),
|
||||
})
|
||||
|
||||
export const notJoinDetailsSchema = baseDetailsSchema.merge(
|
||||
z.object({
|
||||
join: z.literal(false),
|
||||
zipCode: z.string().optional(),
|
||||
dateOfBirth: z.string().optional(),
|
||||
termsAccepted: z.boolean().default(false),
|
||||
})
|
||||
)
|
||||
|
||||
export const joinDetailsSchema = baseDetailsSchema.merge(
|
||||
z.object({
|
||||
join: z.literal(true),
|
||||
zipCode: z.string().min(1, { message: "Zip code is required" }),
|
||||
dateOfBirth: z.string(),
|
||||
termsAccepted: z.literal(true, {
|
||||
errorMap: (err, ctx) => {
|
||||
switch (err.code) {
|
||||
case "invalid_literal":
|
||||
return { message: "You must accept the terms and conditions" }
|
||||
}
|
||||
return { message: ctx.defaultError }
|
||||
},
|
||||
}),
|
||||
})
|
||||
)
|
||||
|
||||
export const detailsSchema = z.discriminatedUnion("join", [
|
||||
notJoinDetailsSchema,
|
||||
joinDetailsSchema,
|
||||
])
|
||||
|
||||
export const signedInDetailsSchema = z.object({
|
||||
countryCode: z.string().optional(),
|
||||
email: z.string().email().optional(),
|
||||
firstname: z.string().optional(),
|
||||
lastname: z.string().optional(),
|
||||
firstName: z.string().optional(),
|
||||
lastName: z.string().optional(),
|
||||
phoneNumber: phoneValidator().optional(),
|
||||
})
|
||||
|
||||
@@ -20,54 +20,30 @@ export default function SectionAccordion({
|
||||
children,
|
||||
}: React.PropsWithChildren<SectionAccordionProps>) {
|
||||
const intl = useIntl()
|
||||
const [isComplete, setIsComplete] = useState(false)
|
||||
const currentStep = useEnterDetailsStore((state) => state.currentStep)
|
||||
const [isComplete, setIsComplete] = useState(false)
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
|
||||
const isValid = useEnterDetailsStore((state) => state.isValid[step])
|
||||
|
||||
const navigate = useEnterDetailsStore((state) => state.navigate)
|
||||
|
||||
const contentRef = useRef<HTMLDivElement>(null)
|
||||
const circleRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
const isOpen = currentStep === step
|
||||
|
||||
useEffect(() => {
|
||||
const content = contentRef.current
|
||||
const circle = circleRef.current
|
||||
if (content) {
|
||||
if (isOpen) {
|
||||
content.style.maxHeight = `${content.scrollHeight}px`
|
||||
} else {
|
||||
content.style.maxHeight = "0"
|
||||
}
|
||||
}
|
||||
|
||||
if (circle) {
|
||||
if (isOpen) {
|
||||
circle.style.backgroundColor = `var(--UI-Text-Placeholder);`
|
||||
} else {
|
||||
circle.style.backgroundColor = `var(--Base-Surface-Subtle-Hover);`
|
||||
}
|
||||
}
|
||||
}, [isOpen])
|
||||
|
||||
useEffect(() => {
|
||||
// We need to set the state on mount because of hydration errors
|
||||
setIsComplete(isValid)
|
||||
}, [isValid])
|
||||
|
||||
useEffect(() => {
|
||||
setIsOpen(currentStep === step)
|
||||
}, [currentStep, step])
|
||||
|
||||
function onModify() {
|
||||
navigate(step)
|
||||
}
|
||||
|
||||
return (
|
||||
<section className={styles.wrapper} data-open={isOpen}>
|
||||
<section className={styles.wrapper} data-open={isOpen} data-step={step}>
|
||||
<div className={styles.iconWrapper}>
|
||||
<div
|
||||
className={styles.circle}
|
||||
data-checked={isComplete}
|
||||
ref={circleRef}
|
||||
>
|
||||
<div className={styles.circle} data-checked={isComplete}>
|
||||
{isComplete ? (
|
||||
<CheckIcon color="white" height="16" width="16" />
|
||||
) : null}
|
||||
@@ -79,6 +55,7 @@ export default function SectionAccordion({
|
||||
<Footnote
|
||||
asChild
|
||||
textTransform="uppercase"
|
||||
type="label"
|
||||
color="uiTextPlaceholder"
|
||||
>
|
||||
<h2>{header}</h2>
|
||||
@@ -105,9 +82,7 @@ export default function SectionAccordion({
|
||||
</Button>
|
||||
)}
|
||||
</header>
|
||||
<div className={styles.content} ref={contentRef}>
|
||||
{children}
|
||||
</div>
|
||||
<div className={styles.content}>{children}</div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
|
||||
@@ -22,12 +22,14 @@
|
||||
}
|
||||
|
||||
.main {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
display: grid;
|
||||
gap: var(--Spacing-x3);
|
||||
width: 100%;
|
||||
border-bottom: 1px solid var(--Primary-Light-On-Surface-Divider-subtle);
|
||||
padding-bottom: var(--Spacing-x3);
|
||||
|
||||
transition: 0.4s ease-out;
|
||||
grid-template-rows: 2em 0fr;
|
||||
}
|
||||
|
||||
.headerContainer {
|
||||
@@ -70,12 +72,23 @@
|
||||
background-color: var(--Base-Surface-Subtle-Hover);
|
||||
}
|
||||
|
||||
.wrapper[data-open="true"] .main {
|
||||
grid-template-rows: 2em 1fr;
|
||||
}
|
||||
|
||||
.content {
|
||||
overflow: hidden;
|
||||
transition: max-height 0.4s ease-out;
|
||||
max-height: 0;
|
||||
}
|
||||
|
||||
@keyframes allowOverflow {
|
||||
0% {
|
||||
overflow: hidden;
|
||||
}
|
||||
100% {
|
||||
overflow: visible;
|
||||
}
|
||||
}
|
||||
|
||||
.wrapper[data-open="true"] .content {
|
||||
max-height: 1000px;
|
||||
animation: allowOverflow 0.4s 0.4s ease;
|
||||
}
|
||||
|
||||
@@ -20,8 +20,8 @@ import type { DateProps } from "./date"
|
||||
|
||||
export default function DateSelect({ name, registerOptions = {} }: DateProps) {
|
||||
const intl = useIntl()
|
||||
const d = useWatch({ name })
|
||||
const { control, setValue } = useFormContext()
|
||||
const currentValue = useWatch({ name })
|
||||
const { control, setValue, trigger } = useFormContext()
|
||||
const { field } = useController({
|
||||
control,
|
||||
name,
|
||||
@@ -47,7 +47,7 @@ export default function DateSelect({ name, registerOptions = {} }: DateProps) {
|
||||
}))
|
||||
const years = rangeArray(1900, currentYear - 18)
|
||||
.reverse()
|
||||
.map((year) => ({ value: year, label: `${year}` }))
|
||||
.map((year) => ({ value: year, label: year.toString() }))
|
||||
|
||||
function createOnSelect(selector: DateName) {
|
||||
/**
|
||||
@@ -68,6 +68,8 @@ export default function DateSelect({ name, registerOptions = {} }: DateProps) {
|
||||
const month = selector === DateName.month ? value : newSegments.month
|
||||
if (year !== null && month !== null) {
|
||||
newSegments.daysInMonth = dt().year(year).month(month).daysInMonth()
|
||||
} else if (month !== null) {
|
||||
newSegments.daysInMonth = dt().month(month).daysInMonth()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,6 +81,7 @@ export default function DateSelect({ name, registerOptions = {} }: DateProps) {
|
||||
.set("date", Math.min(newSegments.date!, newSegments.daysInMonth))
|
||||
|
||||
setValue(name, newDate.format("YYYY-MM-DD"))
|
||||
trigger(name)
|
||||
}
|
||||
setDateSegment(newSegments)
|
||||
}
|
||||
@@ -95,9 +98,9 @@ export default function DateSelect({ name, registerOptions = {} }: DateProps) {
|
||||
* date, but we can't check isNan since
|
||||
* we recieve the date as "1999-01-01"
|
||||
*/
|
||||
dateValue = dt(d).isValid() ? parseDate(d) : null
|
||||
dateValue = dt(currentValue).isValid() ? parseDate(currentValue) : null
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
console.warn("Known error for parse date in DateSelect: ", error)
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -133,6 +136,7 @@ export default function DateSelect({ name, registerOptions = {} }: DateProps) {
|
||||
defaultSelectedKey={
|
||||
segment.isPlaceholder ? undefined : segment.value
|
||||
}
|
||||
value={segment.isPlaceholder ? undefined : segment.value}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
@@ -151,6 +155,7 @@ export default function DateSelect({ name, registerOptions = {} }: DateProps) {
|
||||
defaultSelectedKey={
|
||||
segment.isPlaceholder ? undefined : segment.value
|
||||
}
|
||||
value={segment.isPlaceholder ? undefined : segment.value}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
@@ -169,6 +174,7 @@ export default function DateSelect({ name, registerOptions = {} }: DateProps) {
|
||||
defaultSelectedKey={
|
||||
segment.isPlaceholder ? undefined : segment.value
|
||||
}
|
||||
value={segment.isPlaceholder ? undefined : segment.value}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
"Already a friend?": "Allerede en ven?",
|
||||
"Amenities": "Faciliteter",
|
||||
"Amusement park": "Forlystelsespark",
|
||||
"An error occurred. Please try again.": "Der opstod en fejl. Prøv venligst igen.",
|
||||
"An error occurred trying to manage your preferences, please try again later.": "Der opstod en fejl under forsøget på at administrere dine præferencer. Prøv venligst igen senere.",
|
||||
"An error occurred when adding a credit card, please try again later.": "Der opstod en fejl under tilføjelse af et kreditkort. Prøv venligst igen senere.",
|
||||
"An error occurred when trying to update profile.": "Der opstod en fejl under forsøg på at opdatere profilen.",
|
||||
@@ -105,7 +106,7 @@
|
||||
"Fair": "Messe",
|
||||
"Find booking": "Find booking",
|
||||
"Find hotels": "Find hotel",
|
||||
"Firstname": "Fornavn",
|
||||
"First name": "Fornavn",
|
||||
"Flexibility": "Fleksibilitet",
|
||||
"Follow us": "Følg os",
|
||||
"Former Scandic Hotel": "Tidligere Scandic Hotel",
|
||||
@@ -138,7 +139,7 @@
|
||||
"Join at no cost": "Tilmeld dig uden omkostninger",
|
||||
"King bed": "Kingsize-seng",
|
||||
"Language": "Sprog",
|
||||
"Lastname": "Efternavn",
|
||||
"Last name": "Efternavn",
|
||||
"Latest searches": "Seneste søgninger",
|
||||
"Left": "tilbage",
|
||||
"Level": "Niveau",
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
"Already a friend?": "Sind wir schon Freunde?",
|
||||
"Amenities": "Annehmlichkeiten",
|
||||
"Amusement park": "Vergnügungspark",
|
||||
"An error occurred. Please try again.": "Es ist ein Fehler aufgetreten. Bitte versuchen Sie es erneut.",
|
||||
"An error occurred trying to manage your preferences, please try again later.": "Beim Versuch, Ihre Einstellungen zu verwalten, ist ein Fehler aufgetreten. Bitte versuchen Sie es später erneut.",
|
||||
"An error occurred when adding a credit card, please try again later.": "Beim Hinzufügen einer Kreditkarte ist ein Fehler aufgetreten. Bitte versuchen Sie es später erneut.",
|
||||
"An error occurred when trying to update profile.": "Beim Versuch, das Profil zu aktualisieren, ist ein Fehler aufgetreten.",
|
||||
@@ -105,7 +106,7 @@
|
||||
"Fair": "Messe",
|
||||
"Find booking": "Buchung finden",
|
||||
"Find hotels": "Hotels finden",
|
||||
"Firstname": "Vorname",
|
||||
"First name": "Vorname",
|
||||
"Flexibility": "Flexibilität",
|
||||
"Follow us": "Folgen Sie uns",
|
||||
"Former Scandic Hotel": "Ehemaliges Scandic Hotel",
|
||||
@@ -138,7 +139,7 @@
|
||||
"Join at no cost": "Kostenlos beitreten",
|
||||
"King bed": "Kingsize-Bett",
|
||||
"Language": "Sprache",
|
||||
"Lastname": "Nachname",
|
||||
"Last name": "Nachname",
|
||||
"Latest searches": "Letzte Suchanfragen",
|
||||
"Left": "übrig",
|
||||
"Level": "Level",
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
"Already a friend?": "Already a friend?",
|
||||
"Amenities": "Amenities",
|
||||
"Amusement park": "Amusement park",
|
||||
"An error occurred. Please try again.": "An error occurred. Please try again.",
|
||||
"An error occurred trying to manage your preferences, please try again later.": "An error occurred trying to manage your preferences, please try again later.",
|
||||
"An error occurred when adding a credit card, please try again later.": "An error occurred when adding a credit card, please try again later.",
|
||||
"An error occurred when trying to update profile.": "An error occurred when trying to update profile.",
|
||||
@@ -108,7 +109,7 @@
|
||||
"Fair": "Fair",
|
||||
"Find booking": "Find booking",
|
||||
"Find hotels": "Find hotels",
|
||||
"Firstname": "Firstname",
|
||||
"First name": "First name",
|
||||
"Flexibility": "Flexibility",
|
||||
"Follow us": "Follow us",
|
||||
"Former Scandic Hotel": "Former Scandic Hotel",
|
||||
@@ -141,7 +142,7 @@
|
||||
"Join at no cost": "Join at no cost",
|
||||
"King bed": "King bed",
|
||||
"Language": "Language",
|
||||
"Lastname": "Lastname",
|
||||
"Last name": "Last name",
|
||||
"Latest searches": "Latest searches",
|
||||
"Left": "left",
|
||||
"Level": "Level",
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
"Already a friend?": "Oletko jo ystävä?",
|
||||
"Amenities": "Mukavuudet",
|
||||
"Amusement park": "Huvipuisto",
|
||||
"An error occurred. Please try again.": "Tapahtui virhe. Yritä uudelleen.",
|
||||
"An error occurred trying to manage your preferences, please try again later.": "Asetusten hallinnassa tapahtui virhe. Yritä myöhemmin uudelleen.",
|
||||
"An error occurred when adding a credit card, please try again later.": "Luottokorttia lisättäessä tapahtui virhe. Yritä myöhemmin uudelleen.",
|
||||
"An error occurred when trying to update profile.": "Profiilia päivitettäessä tapahtui virhe.",
|
||||
@@ -105,7 +106,7 @@
|
||||
"Fair": "Messukeskus",
|
||||
"Find booking": "Etsi varaus",
|
||||
"Find hotels": "Etsi hotelleja",
|
||||
"Firstname": "Etunimi",
|
||||
"First name": "Etunimi",
|
||||
"Flexibility": "Joustavuus",
|
||||
"Follow us": "Seuraa meitä",
|
||||
"Former Scandic Hotel": "Entinen Scandic-hotelli",
|
||||
@@ -138,7 +139,7 @@
|
||||
"Join at no cost": "Liity maksutta",
|
||||
"King bed": "King-vuode",
|
||||
"Language": "Kieli",
|
||||
"Lastname": "Sukunimi",
|
||||
"Last name": "Sukunimi",
|
||||
"Latest searches": "Viimeisimmät haut",
|
||||
"Left": "jäljellä",
|
||||
"Level": "Level",
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
"Already a friend?": "Allerede Friend?",
|
||||
"Amenities": "Fasiliteter",
|
||||
"Amusement park": "Tivoli",
|
||||
"An error occurred. Please try again.": "Det oppsto en feil. Vennligst prøv igjen.",
|
||||
"An error occurred trying to manage your preferences, please try again later.": "Det oppstod en feil under forsøket på å administrere innstillingene dine. Prøv igjen senere.",
|
||||
"An error occurred when adding a credit card, please try again later.": "Det oppstod en feil ved å legge til et kredittkort. Prøv igjen senere.",
|
||||
"An error occurred when trying to update profile.": "Det oppstod en feil under forsøk på å oppdatere profilen.",
|
||||
@@ -104,7 +105,7 @@
|
||||
"Fair": "Messe",
|
||||
"Find booking": "Finn booking",
|
||||
"Find hotels": "Finn hotell",
|
||||
"Firstname": "Fornavn",
|
||||
"First name": "Fornavn",
|
||||
"Flexibility": "Fleksibilitet",
|
||||
"Follow us": "Følg oss",
|
||||
"Former Scandic Hotel": "Tidligere Scandic-hotell",
|
||||
@@ -136,7 +137,7 @@
|
||||
"Join at no cost": "Bli med uten kostnad",
|
||||
"King bed": "King-size-seng",
|
||||
"Language": "Språk",
|
||||
"Lastname": "Etternavn",
|
||||
"Last name": "Etternavn",
|
||||
"Latest searches": "Siste søk",
|
||||
"Left": "igjen",
|
||||
"Level": "Nivå",
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
"Already a friend?": "Är du redan en vän?",
|
||||
"Amenities": "Bekvämligheter",
|
||||
"Amusement park": "Nöjespark",
|
||||
"An error occurred. Please try again.": "Ett fel uppstod. Försök igen.",
|
||||
"An error occurred trying to manage your preferences, please try again later.": "Ett fel uppstod när du försökte hantera dina inställningar, försök igen senare.",
|
||||
"An error occurred when adding a credit card, please try again later.": "Ett fel uppstod när ett kreditkort lades till, försök igen senare.",
|
||||
"An error occurred when trying to update profile.": "Ett fel uppstod när du försökte uppdatera profilen.",
|
||||
@@ -104,7 +105,7 @@
|
||||
"Fair": "Mässa",
|
||||
"Find booking": "Hitta bokning",
|
||||
"Find hotels": "Hitta hotell",
|
||||
"Firstname": "Förnamn",
|
||||
"First name": "Förnamn",
|
||||
"Flexibility": "Flexibilitet",
|
||||
"Follow us": "Följ oss",
|
||||
"Former Scandic Hotel": "Tidigare Scandichotell",
|
||||
@@ -136,7 +137,7 @@
|
||||
"Join at no cost": "Gå med utan kostnad",
|
||||
"King bed": "King size-säng",
|
||||
"Language": "Språk",
|
||||
"Lastname": "Efternamn",
|
||||
"Last name": "Efternamn",
|
||||
"Latest searches": "Senaste sökningarna",
|
||||
"Left": "kvar",
|
||||
"Level": "Nivå",
|
||||
|
||||
@@ -22,7 +22,10 @@ interface EnterDetailsState {
|
||||
activeSidePeek: SidePeekEnum | null
|
||||
isValid: Record<StepEnum, boolean>
|
||||
completeStep: (updatedData: Partial<EnterDetailsState["data"]>) => void
|
||||
navigate: (step: StepEnum, searchParams?: Record<string, string>) => void
|
||||
navigate: (
|
||||
step: StepEnum,
|
||||
searchParams?: Record<string, string | boolean>
|
||||
) => void
|
||||
openSidePeek: (key: SidePeekEnum | null) => void
|
||||
closeSidePeek: () => void
|
||||
}
|
||||
@@ -37,26 +40,34 @@ export function initEditDetailsState(currentStep: StepEnum) {
|
||||
breakfast: undefined,
|
||||
countryCode: "",
|
||||
email: "",
|
||||
firstname: "",
|
||||
lastname: "",
|
||||
firstName: "",
|
||||
lastName: "",
|
||||
phoneNumber: "",
|
||||
join: false,
|
||||
zipCode: "",
|
||||
dateOfBirth: undefined,
|
||||
termsAccepted: false,
|
||||
}
|
||||
|
||||
let inputData = {}
|
||||
if (search?.size) {
|
||||
const searchParams: Record<string, string> = {}
|
||||
const searchParams: Record<string, string | boolean> = {}
|
||||
search.forEach((value, key) => {
|
||||
searchParams[key] = value
|
||||
// Handle boolean values
|
||||
|
||||
if (value === "true" || value === "false") {
|
||||
searchParams[key] = JSON.parse(value) as true | false
|
||||
} else {
|
||||
searchParams[key] = value
|
||||
}
|
||||
})
|
||||
|
||||
inputData = searchParams
|
||||
} else if (sessionData) {
|
||||
inputData = JSON.parse(sessionData)
|
||||
}
|
||||
|
||||
const validPaths = [StepEnum.selectBed]
|
||||
|
||||
let initialData = defaultData
|
||||
let initialData: EnterDetailsState["data"] = defaultData
|
||||
|
||||
const isValid = {
|
||||
[StepEnum.selectBed]: false,
|
||||
@@ -100,7 +111,7 @@ export function initEditDetailsState(currentStep: StepEnum) {
|
||||
const query = new URLSearchParams(window.location.search)
|
||||
if (searchParams) {
|
||||
Object.entries(searchParams).forEach(([key, value]) => {
|
||||
query.set(key, value)
|
||||
query.set(key, value ? value.toString() : "")
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import { detailsSchema } from "@/components/HotelReservation/EnterDetails/Detail
|
||||
|
||||
import type { SafeUser } from "@/types/user"
|
||||
|
||||
export interface DetailsSchema extends z.output<typeof detailsSchema> {}
|
||||
export type DetailsSchema = z.output<typeof detailsSchema>
|
||||
|
||||
export interface DetailsProps {
|
||||
user: SafeUser
|
||||
|
||||
Reference in New Issue
Block a user