feat: add conditional signup values

This commit is contained in:
Christel Westerberg
2024-10-16 12:09:18 +02:00
parent 5870a31275
commit 74c2a9d1e1
18 changed files with 347 additions and 110 deletions

View 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 }
})

View File

@@ -56,7 +56,7 @@ export default function AdultSelector({ roomIndex = 0 }: AdultSelectorProps) {
return ( return (
<section className={styles.container}> <section className={styles.container}>
<Caption color="uiTextHighContrast" textTransform="bold"> <Caption color="uiTextHighContrast" type="bold">
{adultsLabel} {adultsLabel}
</Caption> </Caption>
<Counter <Counter

View File

@@ -50,7 +50,7 @@ export default function ChildSelector({ roomIndex = 0 }: ChildSelectorProps) {
return ( return (
<> <>
<section className={styles.container}> <section className={styles.container}>
<Caption color="uiTextHighContrast" textTransform="bold"> <Caption color="uiTextHighContrast" type="bold">
{childrenLabel} {childrenLabel}
</Caption> </Caption>
<Counter <Counter

View File

@@ -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>
)
}

View File

@@ -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);
}

View File

@@ -6,14 +6,16 @@ import { useIntl } from "react-intl"
import { useEnterDetailsStore } from "@/stores/enter-details" import { useEnterDetailsStore } from "@/stores/enter-details"
import { registerUserBookingFlow } from "@/actions/registerUserBookingFlow"
import Button from "@/components/TempDesignSystem/Button" import Button from "@/components/TempDesignSystem/Button"
import CheckboxCard from "@/components/TempDesignSystem/Form/ChoiceCard/Checkbox"
import CountrySelect from "@/components/TempDesignSystem/Form/Country" 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 styles from "./details.module.css" import styles from "./details.module.css"
@@ -25,28 +27,30 @@ import type {
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 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) => ({ const initialData = useEnterDetailsStore((state) => ({
countryCode: state.data.countryCode, countryCode: state.data.countryCode,
email: state.data.email, email: state.data.email,
firstname: state.data.firstname, firstName: state.data.firstName,
lastname: state.data.lastname, lastName: state.data.lastName,
phoneNumber: state.data.phoneNumber, phoneNumber: state.data.phoneNumber,
join: state.data.join,
dateOfBirth: state.data.dateOfBirth,
zipCode: state.data.zipCode,
termsAccepted: state.data.termsAccepted,
})) }))
const methods = useForm<DetailsSchema>({ const methods = useForm<DetailsSchema>({
defaultValues: { defaultValues: {
countryCode: user?.address?.countryCode ?? initialData.countryCode, countryCode: user?.address?.countryCode ?? initialData.countryCode,
email: user?.email ?? initialData.email, email: user?.email ?? initialData.email,
firstname: user?.firstName ?? initialData.firstname, firstName: user?.firstName ?? initialData.firstName,
lastname: user?.lastName ?? initialData.lastname, lastName: user?.lastName ?? initialData.lastName,
phoneNumber: user?.phoneNumber ?? initialData.phoneNumber, 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", criteriaMode: "all",
mode: "all", mode: "all",
@@ -56,10 +60,39 @@ export default function Details({ user }: DetailsProps) {
const completeStep = useEnterDetailsStore((state) => state.completeStep) const completeStep = useEnterDetailsStore((state) => state.completeStep)
// const errorMessage = intl.formatMessage({
// id: "An error occurred. Please try again.",
// })
const onSubmit = useCallback( 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(values)
}, },
[completeStep] [completeStep]
) )
@@ -77,14 +110,14 @@ export default function Details({ user }: DetailsProps) {
onSubmit={methods.handleSubmit(onSubmit)} onSubmit={methods.handleSubmit(onSubmit)}
> >
<Input <Input
label={intl.formatMessage({ id: "Firstname" })} label={intl.formatMessage({ id: "First name" })}
name="firstname" name="firstName"
readOnly={!!user} readOnly={!!user}
registerOptions={{ required: true }} registerOptions={{ required: true }}
/> />
<Input <Input
label={intl.formatMessage({ id: "Lastname" })} label={intl.formatMessage({ id: "Last name" })}
name="lastname" name="lastName"
readOnly={!!user} readOnly={!!user}
registerOptions={{ required: true }} registerOptions={{ required: true }}
/> />
@@ -109,26 +142,9 @@ export default function Details({ user }: DetailsProps) {
readOnly={!!user} readOnly={!!user}
registerOptions={{ required: true }} registerOptions={{ required: true }}
/> />
{user ? null : <Signup name="join" />}
</form> </form>
<footer className={styles.footer}> <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 <Button
disabled={!methods.formState.isValid} disabled={!methods.formState.isValid}
form={formID} form={formID}

View File

@@ -2,18 +2,49 @@ import { z } from "zod"
import { phoneValidator } from "@/utils/phoneValidator" import { phoneValidator } from "@/utils/phoneValidator"
export const detailsSchema = z.object({ export const baseDetailsSchema = z.object({
countryCode: z.string(), countryCode: z.string(),
email: z.string().email(), email: z.string().email(),
firstname: z.string(), firstName: z.string(),
lastname: z.string(), lastName: z.string(),
phoneNumber: phoneValidator(), 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({ export const signedInDetailsSchema = z.object({
countryCode: z.string().optional(), countryCode: z.string().optional(),
email: z.string().email().optional(), email: z.string().email().optional(),
firstname: z.string().optional(), firstName: z.string().optional(),
lastname: z.string().optional(), lastName: z.string().optional(),
phoneNumber: phoneValidator().optional(), phoneNumber: phoneValidator().optional(),
}) })

View File

@@ -20,54 +20,30 @@ export default function SectionAccordion({
children, children,
}: React.PropsWithChildren<SectionAccordionProps>) { }: React.PropsWithChildren<SectionAccordionProps>) {
const intl = useIntl() const intl = useIntl()
const [isComplete, setIsComplete] = useState(false)
const currentStep = useEnterDetailsStore((state) => state.currentStep) const currentStep = useEnterDetailsStore((state) => state.currentStep)
const [isComplete, setIsComplete] = useState(false)
const [isOpen, setIsOpen] = useState(false)
const isValid = useEnterDetailsStore((state) => state.isValid[step]) const isValid = useEnterDetailsStore((state) => state.isValid[step])
const navigate = useEnterDetailsStore((state) => state.navigate) 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(() => { useEffect(() => {
// We need to set the state on mount because of hydration errors // We need to set the state on mount because of hydration errors
setIsComplete(isValid) setIsComplete(isValid)
}, [isValid]) }, [isValid])
useEffect(() => {
setIsOpen(currentStep === step)
}, [currentStep, step])
function onModify() { function onModify() {
navigate(step) navigate(step)
} }
return ( 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.iconWrapper}>
<div <div className={styles.circle} data-checked={isComplete}>
className={styles.circle}
data-checked={isComplete}
ref={circleRef}
>
{isComplete ? ( {isComplete ? (
<CheckIcon color="white" height="16" width="16" /> <CheckIcon color="white" height="16" width="16" />
) : null} ) : null}
@@ -79,6 +55,7 @@ export default function SectionAccordion({
<Footnote <Footnote
asChild asChild
textTransform="uppercase" textTransform="uppercase"
type="label"
color="uiTextPlaceholder" color="uiTextPlaceholder"
> >
<h2>{header}</h2> <h2>{header}</h2>
@@ -105,9 +82,7 @@ export default function SectionAccordion({
</Button> </Button>
)} )}
</header> </header>
<div className={styles.content} ref={contentRef}> <div className={styles.content}>{children}</div>
{children}
</div>
</div> </div>
</section> </section>
) )

View File

@@ -22,12 +22,14 @@
} }
.main { .main {
display: flex; display: grid;
flex-direction: column;
gap: var(--Spacing-x3); gap: var(--Spacing-x3);
width: 100%; width: 100%;
border-bottom: 1px solid var(--Primary-Light-On-Surface-Divider-subtle); border-bottom: 1px solid var(--Primary-Light-On-Surface-Divider-subtle);
padding-bottom: var(--Spacing-x3); padding-bottom: var(--Spacing-x3);
transition: 0.4s ease-out;
grid-template-rows: 2em 0fr;
} }
.headerContainer { .headerContainer {
@@ -70,12 +72,23 @@
background-color: var(--Base-Surface-Subtle-Hover); background-color: var(--Base-Surface-Subtle-Hover);
} }
.wrapper[data-open="true"] .main {
grid-template-rows: 2em 1fr;
}
.content { .content {
overflow: hidden; 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 { .wrapper[data-open="true"] .content {
max-height: 1000px; animation: allowOverflow 0.4s 0.4s ease;
} }

View File

@@ -20,8 +20,8 @@ import type { DateProps } from "./date"
export default function DateSelect({ name, registerOptions = {} }: DateProps) { export default function DateSelect({ name, registerOptions = {} }: DateProps) {
const intl = useIntl() const intl = useIntl()
const d = useWatch({ name }) const currentValue = useWatch({ name })
const { control, setValue } = useFormContext() const { control, setValue, trigger } = useFormContext()
const { field } = useController({ const { field } = useController({
control, control,
name, name,
@@ -47,7 +47,7 @@ export default function DateSelect({ name, registerOptions = {} }: DateProps) {
})) }))
const years = rangeArray(1900, currentYear - 18) const years = rangeArray(1900, currentYear - 18)
.reverse() .reverse()
.map((year) => ({ value: year, label: `${year}` })) .map((year) => ({ value: year, label: year.toString() }))
function createOnSelect(selector: DateName) { function createOnSelect(selector: DateName) {
/** /**
@@ -68,6 +68,8 @@ export default function DateSelect({ name, registerOptions = {} }: DateProps) {
const month = selector === DateName.month ? value : newSegments.month const month = selector === DateName.month ? value : newSegments.month
if (year !== null && month !== null) { if (year !== null && month !== null) {
newSegments.daysInMonth = dt().year(year).month(month).daysInMonth() 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)) .set("date", Math.min(newSegments.date!, newSegments.daysInMonth))
setValue(name, newDate.format("YYYY-MM-DD")) setValue(name, newDate.format("YYYY-MM-DD"))
trigger(name)
} }
setDateSegment(newSegments) setDateSegment(newSegments)
} }
@@ -95,9 +98,9 @@ export default function DateSelect({ name, registerOptions = {} }: DateProps) {
* date, but we can't check isNan since * date, but we can't check isNan since
* we recieve the date as "1999-01-01" * we recieve the date as "1999-01-01"
*/ */
dateValue = dt(d).isValid() ? parseDate(d) : null dateValue = dt(currentValue).isValid() ? parseDate(currentValue) : null
} catch (error) { } catch (error) {
console.error(error) console.warn("Known error for parse date in DateSelect: ", error)
} }
return ( return (
@@ -133,6 +136,7 @@ export default function DateSelect({ name, registerOptions = {} }: DateProps) {
defaultSelectedKey={ defaultSelectedKey={
segment.isPlaceholder ? undefined : segment.value segment.isPlaceholder ? undefined : segment.value
} }
value={segment.isPlaceholder ? undefined : segment.value}
/> />
</div> </div>
) )
@@ -151,6 +155,7 @@ export default function DateSelect({ name, registerOptions = {} }: DateProps) {
defaultSelectedKey={ defaultSelectedKey={
segment.isPlaceholder ? undefined : segment.value segment.isPlaceholder ? undefined : segment.value
} }
value={segment.isPlaceholder ? undefined : segment.value}
/> />
</div> </div>
) )
@@ -169,6 +174,7 @@ export default function DateSelect({ name, registerOptions = {} }: DateProps) {
defaultSelectedKey={ defaultSelectedKey={
segment.isPlaceholder ? undefined : segment.value segment.isPlaceholder ? undefined : segment.value
} }
value={segment.isPlaceholder ? undefined : segment.value}
/> />
</div> </div>
) )

View File

@@ -15,6 +15,7 @@
"Already a friend?": "Allerede en ven?", "Already a friend?": "Allerede en ven?",
"Amenities": "Faciliteter", "Amenities": "Faciliteter",
"Amusement park": "Forlystelsespark", "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 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 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.", "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", "Fair": "Messe",
"Find booking": "Find booking", "Find booking": "Find booking",
"Find hotels": "Find hotel", "Find hotels": "Find hotel",
"Firstname": "Fornavn", "First name": "Fornavn",
"Flexibility": "Fleksibilitet", "Flexibility": "Fleksibilitet",
"Follow us": "Følg os", "Follow us": "Følg os",
"Former Scandic Hotel": "Tidligere Scandic Hotel", "Former Scandic Hotel": "Tidligere Scandic Hotel",
@@ -138,7 +139,7 @@
"Join at no cost": "Tilmeld dig uden omkostninger", "Join at no cost": "Tilmeld dig uden omkostninger",
"King bed": "Kingsize-seng", "King bed": "Kingsize-seng",
"Language": "Sprog", "Language": "Sprog",
"Lastname": "Efternavn", "Last name": "Efternavn",
"Latest searches": "Seneste søgninger", "Latest searches": "Seneste søgninger",
"Left": "tilbage", "Left": "tilbage",
"Level": "Niveau", "Level": "Niveau",

View File

@@ -15,6 +15,7 @@
"Already a friend?": "Sind wir schon Freunde?", "Already a friend?": "Sind wir schon Freunde?",
"Amenities": "Annehmlichkeiten", "Amenities": "Annehmlichkeiten",
"Amusement park": "Vergnügungspark", "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 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 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.", "An error occurred when trying to update profile.": "Beim Versuch, das Profil zu aktualisieren, ist ein Fehler aufgetreten.",
@@ -105,7 +106,7 @@
"Fair": "Messe", "Fair": "Messe",
"Find booking": "Buchung finden", "Find booking": "Buchung finden",
"Find hotels": "Hotels finden", "Find hotels": "Hotels finden",
"Firstname": "Vorname", "First name": "Vorname",
"Flexibility": "Flexibilität", "Flexibility": "Flexibilität",
"Follow us": "Folgen Sie uns", "Follow us": "Folgen Sie uns",
"Former Scandic Hotel": "Ehemaliges Scandic Hotel", "Former Scandic Hotel": "Ehemaliges Scandic Hotel",
@@ -138,7 +139,7 @@
"Join at no cost": "Kostenlos beitreten", "Join at no cost": "Kostenlos beitreten",
"King bed": "Kingsize-Bett", "King bed": "Kingsize-Bett",
"Language": "Sprache", "Language": "Sprache",
"Lastname": "Nachname", "Last name": "Nachname",
"Latest searches": "Letzte Suchanfragen", "Latest searches": "Letzte Suchanfragen",
"Left": "übrig", "Left": "übrig",
"Level": "Level", "Level": "Level",

View File

@@ -16,6 +16,7 @@
"Already a friend?": "Already a friend?", "Already a friend?": "Already a friend?",
"Amenities": "Amenities", "Amenities": "Amenities",
"Amusement park": "Amusement park", "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 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 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.", "An error occurred when trying to update profile.": "An error occurred when trying to update profile.",
@@ -108,7 +109,7 @@
"Fair": "Fair", "Fair": "Fair",
"Find booking": "Find booking", "Find booking": "Find booking",
"Find hotels": "Find hotels", "Find hotels": "Find hotels",
"Firstname": "Firstname", "First name": "First name",
"Flexibility": "Flexibility", "Flexibility": "Flexibility",
"Follow us": "Follow us", "Follow us": "Follow us",
"Former Scandic Hotel": "Former Scandic Hotel", "Former Scandic Hotel": "Former Scandic Hotel",
@@ -141,7 +142,7 @@
"Join at no cost": "Join at no cost", "Join at no cost": "Join at no cost",
"King bed": "King bed", "King bed": "King bed",
"Language": "Language", "Language": "Language",
"Lastname": "Lastname", "Last name": "Last name",
"Latest searches": "Latest searches", "Latest searches": "Latest searches",
"Left": "left", "Left": "left",
"Level": "Level", "Level": "Level",

View File

@@ -15,6 +15,7 @@
"Already a friend?": "Oletko jo ystävä?", "Already a friend?": "Oletko jo ystävä?",
"Amenities": "Mukavuudet", "Amenities": "Mukavuudet",
"Amusement park": "Huvipuisto", "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 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 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.", "An error occurred when trying to update profile.": "Profiilia päivitettäessä tapahtui virhe.",
@@ -105,7 +106,7 @@
"Fair": "Messukeskus", "Fair": "Messukeskus",
"Find booking": "Etsi varaus", "Find booking": "Etsi varaus",
"Find hotels": "Etsi hotelleja", "Find hotels": "Etsi hotelleja",
"Firstname": "Etunimi", "First name": "Etunimi",
"Flexibility": "Joustavuus", "Flexibility": "Joustavuus",
"Follow us": "Seuraa meitä", "Follow us": "Seuraa meitä",
"Former Scandic Hotel": "Entinen Scandic-hotelli", "Former Scandic Hotel": "Entinen Scandic-hotelli",
@@ -138,7 +139,7 @@
"Join at no cost": "Liity maksutta", "Join at no cost": "Liity maksutta",
"King bed": "King-vuode", "King bed": "King-vuode",
"Language": "Kieli", "Language": "Kieli",
"Lastname": "Sukunimi", "Last name": "Sukunimi",
"Latest searches": "Viimeisimmät haut", "Latest searches": "Viimeisimmät haut",
"Left": "jäljellä", "Left": "jäljellä",
"Level": "Level", "Level": "Level",

View File

@@ -15,6 +15,7 @@
"Already a friend?": "Allerede Friend?", "Already a friend?": "Allerede Friend?",
"Amenities": "Fasiliteter", "Amenities": "Fasiliteter",
"Amusement park": "Tivoli", "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 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 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.", "An error occurred when trying to update profile.": "Det oppstod en feil under forsøk på å oppdatere profilen.",
@@ -104,7 +105,7 @@
"Fair": "Messe", "Fair": "Messe",
"Find booking": "Finn booking", "Find booking": "Finn booking",
"Find hotels": "Finn hotell", "Find hotels": "Finn hotell",
"Firstname": "Fornavn", "First name": "Fornavn",
"Flexibility": "Fleksibilitet", "Flexibility": "Fleksibilitet",
"Follow us": "Følg oss", "Follow us": "Følg oss",
"Former Scandic Hotel": "Tidligere Scandic-hotell", "Former Scandic Hotel": "Tidligere Scandic-hotell",
@@ -136,7 +137,7 @@
"Join at no cost": "Bli med uten kostnad", "Join at no cost": "Bli med uten kostnad",
"King bed": "King-size-seng", "King bed": "King-size-seng",
"Language": "Språk", "Language": "Språk",
"Lastname": "Etternavn", "Last name": "Etternavn",
"Latest searches": "Siste søk", "Latest searches": "Siste søk",
"Left": "igjen", "Left": "igjen",
"Level": "Nivå", "Level": "Nivå",

View File

@@ -15,6 +15,7 @@
"Already a friend?": "Är du redan en vän?", "Already a friend?": "Är du redan en vän?",
"Amenities": "Bekvämligheter", "Amenities": "Bekvämligheter",
"Amusement park": "Nöjespark", "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 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 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.", "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", "Fair": "Mässa",
"Find booking": "Hitta bokning", "Find booking": "Hitta bokning",
"Find hotels": "Hitta hotell", "Find hotels": "Hitta hotell",
"Firstname": "Förnamn", "First name": "Förnamn",
"Flexibility": "Flexibilitet", "Flexibility": "Flexibilitet",
"Follow us": "Följ oss", "Follow us": "Följ oss",
"Former Scandic Hotel": "Tidigare Scandichotell", "Former Scandic Hotel": "Tidigare Scandichotell",
@@ -136,7 +137,7 @@
"Join at no cost": "Gå med utan kostnad", "Join at no cost": "Gå med utan kostnad",
"King bed": "King size-säng", "King bed": "King size-säng",
"Language": "Språk", "Language": "Språk",
"Lastname": "Efternamn", "Last name": "Efternamn",
"Latest searches": "Senaste sökningarna", "Latest searches": "Senaste sökningarna",
"Left": "kvar", "Left": "kvar",
"Level": "Nivå", "Level": "Nivå",

View File

@@ -22,7 +22,10 @@ interface EnterDetailsState {
activeSidePeek: SidePeekEnum | null activeSidePeek: SidePeekEnum | null
isValid: Record<StepEnum, boolean> isValid: Record<StepEnum, boolean>
completeStep: (updatedData: Partial<EnterDetailsState["data"]>) => void 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 openSidePeek: (key: SidePeekEnum | null) => void
closeSidePeek: () => void closeSidePeek: () => void
} }
@@ -37,26 +40,34 @@ export function initEditDetailsState(currentStep: StepEnum) {
breakfast: undefined, breakfast: undefined,
countryCode: "", countryCode: "",
email: "", email: "",
firstname: "", firstName: "",
lastname: "", lastName: "",
phoneNumber: "", phoneNumber: "",
join: false,
zipCode: "",
dateOfBirth: undefined,
termsAccepted: false,
} }
let inputData = {} let inputData = {}
if (search?.size) { if (search?.size) {
const searchParams: Record<string, string> = {} const searchParams: Record<string, string | boolean> = {}
search.forEach((value, key) => { 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 inputData = searchParams
} else if (sessionData) {
inputData = JSON.parse(sessionData)
} }
const validPaths = [StepEnum.selectBed] const validPaths = [StepEnum.selectBed]
let initialData = defaultData let initialData: EnterDetailsState["data"] = defaultData
const isValid = { const isValid = {
[StepEnum.selectBed]: false, [StepEnum.selectBed]: false,
@@ -100,7 +111,7 @@ export function initEditDetailsState(currentStep: StepEnum) {
const query = new URLSearchParams(window.location.search) const query = new URLSearchParams(window.location.search)
if (searchParams) { if (searchParams) {
Object.entries(searchParams).forEach(([key, value]) => { Object.entries(searchParams).forEach(([key, value]) => {
query.set(key, value) query.set(key, value ? value.toString() : "")
}) })
} }

View File

@@ -4,7 +4,7 @@ import { detailsSchema } from "@/components/HotelReservation/EnterDetails/Detail
import type { SafeUser } from "@/types/user" import type { SafeUser } from "@/types/user"
export interface DetailsSchema extends z.output<typeof detailsSchema> {} export type DetailsSchema = z.output<typeof detailsSchema>
export interface DetailsProps { export interface DetailsProps {
user: SafeUser user: SafeUser