Merge branch 'feat/sw-929-release-preps' of bitbucket.org:scandic-swap/web into feat/sw-929-release-preps
This commit is contained in:
@@ -1,89 +0,0 @@
|
|||||||
"use server"
|
|
||||||
|
|
||||||
import { parsePhoneNumber } from "libphonenumber-js"
|
|
||||||
import { redirect } from "next/navigation"
|
|
||||||
import { z } from "zod"
|
|
||||||
|
|
||||||
import { signupVerify } from "@/constants/routes/signup"
|
|
||||||
import * as api from "@/lib/api"
|
|
||||||
import { serviceServerActionProcedure } from "@/server/trpc"
|
|
||||||
|
|
||||||
import { signUpSchema } from "@/components/Forms/Signup/schema"
|
|
||||||
import { passwordValidator } from "@/utils/passwordValidator"
|
|
||||||
import { phoneValidator } from "@/utils/phoneValidator"
|
|
||||||
|
|
||||||
const registerUserPayload = z.object({
|
|
||||||
language: z.string(),
|
|
||||||
firstName: z.string(),
|
|
||||||
lastName: z.string(),
|
|
||||||
email: z.string(),
|
|
||||||
phoneNumber: phoneValidator("Phone is required"),
|
|
||||||
dateOfBirth: z.string(),
|
|
||||||
address: z.object({
|
|
||||||
city: z.string().default(""),
|
|
||||||
country: z.string().default(""),
|
|
||||||
countryCode: z.string().default(""),
|
|
||||||
zipCode: z.string().default(""),
|
|
||||||
streetAddress: z.string().default(""),
|
|
||||||
}),
|
|
||||||
password: passwordValidator("Password is required"),
|
|
||||||
})
|
|
||||||
|
|
||||||
export const registerUser = serviceServerActionProcedure
|
|
||||||
.input(signUpSchema)
|
|
||||||
.mutation(async function ({ ctx, input }) {
|
|
||||||
const payload = {
|
|
||||||
...input,
|
|
||||||
language: ctx.lang,
|
|
||||||
phoneNumber: input.phoneNumber.replace(/\s+/g, ""),
|
|
||||||
}
|
|
||||||
|
|
||||||
const parsedPayload = registerUserPayload.safeParse(payload)
|
|
||||||
if (!parsedPayload.success) {
|
|
||||||
console.error(
|
|
||||||
"registerUser payload validation error",
|
|
||||||
JSON.stringify({
|
|
||||||
query: input,
|
|
||||||
error: parsedPayload.error,
|
|
||||||
})
|
|
||||||
)
|
|
||||||
|
|
||||||
return { success: false, error: "Validation error" }
|
|
||||||
}
|
|
||||||
|
|
||||||
let apiResponse
|
|
||||||
try {
|
|
||||||
apiResponse = await api.post(api.endpoints.v1.Profile.profile, {
|
|
||||||
body: parsedPayload.data,
|
|
||||||
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(
|
|
||||||
"registerUser 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("registerUser: json", json)
|
|
||||||
|
|
||||||
// Note: The redirect needs to be called after the try/catch block.
|
|
||||||
// See: https://nextjs.org/docs/app/api-reference/functions/redirect
|
|
||||||
redirect(signupVerify[ctx.lang])
|
|
||||||
})
|
|
||||||
@@ -12,7 +12,7 @@ import {
|
|||||||
import { MapModal } from "@/components/MapModal"
|
import { MapModal } from "@/components/MapModal"
|
||||||
import { setLang } from "@/i18n/serverContext"
|
import { setLang } from "@/i18n/serverContext"
|
||||||
|
|
||||||
import { fetchAvailableHotels } from "../../utils"
|
import { fetchAvailableHotels, getFiltersFromHotels } from "../../utils"
|
||||||
|
|
||||||
import type { SelectHotelSearchParams } from "@/types/components/hotelReservation/selectHotel/selectHotelSearchParams"
|
import type { SelectHotelSearchParams } from "@/types/components/hotelReservation/selectHotel/selectHotelSearchParams"
|
||||||
import type { LangParams, PageArgs } from "@/types/params"
|
import type { LangParams, PageArgs } from "@/types/params"
|
||||||
@@ -57,6 +57,7 @@ export default async function SelectHotelMapPage({
|
|||||||
})
|
})
|
||||||
|
|
||||||
const hotelPins = getHotelPins(hotels)
|
const hotelPins = getHotelPins(hotels)
|
||||||
|
const filterList = getFiltersFromHotels(hotels)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MapModal>
|
<MapModal>
|
||||||
@@ -65,6 +66,7 @@ export default async function SelectHotelMapPage({
|
|||||||
hotelPins={hotelPins}
|
hotelPins={hotelPins}
|
||||||
mapId={googleMapId}
|
mapId={googleMapId}
|
||||||
hotels={hotels}
|
hotels={hotels}
|
||||||
|
filterList={filterList}
|
||||||
/>
|
/>
|
||||||
</MapModal>
|
</MapModal>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
getFiltersFromHotels,
|
getFiltersFromHotels,
|
||||||
} from "@/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/utils"
|
} from "@/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/utils"
|
||||||
import HotelCardListing from "@/components/HotelReservation/HotelCardListing"
|
import HotelCardListing from "@/components/HotelReservation/HotelCardListing"
|
||||||
|
import HotelCount from "@/components/HotelReservation/SelectHotel/HotelCount"
|
||||||
import HotelFilter from "@/components/HotelReservation/SelectHotel/HotelFilter"
|
import HotelFilter from "@/components/HotelReservation/SelectHotel/HotelFilter"
|
||||||
import HotelSorter from "@/components/HotelReservation/SelectHotel/HotelSorter"
|
import HotelSorter from "@/components/HotelReservation/SelectHotel/HotelSorter"
|
||||||
import MobileMapButtonContainer from "@/components/HotelReservation/SelectHotel/MobileMapButtonContainer"
|
import MobileMapButtonContainer from "@/components/HotelReservation/SelectHotel/MobileMapButtonContainer"
|
||||||
@@ -20,7 +21,6 @@ import StaticMap from "@/components/Maps/StaticMap"
|
|||||||
import Alert from "@/components/TempDesignSystem/Alert"
|
import Alert from "@/components/TempDesignSystem/Alert"
|
||||||
import Button from "@/components/TempDesignSystem/Button"
|
import Button from "@/components/TempDesignSystem/Button"
|
||||||
import Link from "@/components/TempDesignSystem/Link"
|
import Link from "@/components/TempDesignSystem/Link"
|
||||||
import Preamble from "@/components/TempDesignSystem/Text/Preamble"
|
|
||||||
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
|
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
|
||||||
import { getIntl } from "@/i18n"
|
import { getIntl } from "@/i18n"
|
||||||
import { setLang } from "@/i18n/serverContext"
|
import { setLang } from "@/i18n/serverContext"
|
||||||
@@ -66,13 +66,15 @@ export default async function SelectHotelPage({
|
|||||||
|
|
||||||
const filterList = getFiltersFromHotels(hotels)
|
const filterList = getFiltersFromHotels(hotels)
|
||||||
|
|
||||||
|
const isAllUnavailable = hotels.every((hotel) => hotel.price === undefined)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<header className={styles.header}>
|
<header className={styles.header}>
|
||||||
<div className={styles.title}>
|
<div className={styles.title}>
|
||||||
<div className={styles.cityInformation}>
|
<div className={styles.cityInformation}>
|
||||||
<Subtitle>{city.name}</Subtitle>
|
<Subtitle>{city.name}</Subtitle>
|
||||||
<Preamble>{hotels.length} hotels</Preamble>
|
<HotelCount />
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.sorter}>
|
<div className={styles.sorter}>
|
||||||
<HotelSorter discreet />
|
<HotelSorter discreet />
|
||||||
@@ -123,7 +125,7 @@ export default async function SelectHotelPage({
|
|||||||
<HotelFilter filters={filterList} className={styles.filter} />
|
<HotelFilter filters={filterList} className={styles.filter} />
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.hotelList}>
|
<div className={styles.hotelList}>
|
||||||
{!hotels.length && (
|
{isAllUnavailable && (
|
||||||
<Alert
|
<Alert
|
||||||
type={AlertTypeEnum.Info}
|
type={AlertTypeEnum.Info}
|
||||||
heading={intl.formatMessage({ id: "No availability" })}
|
heading={intl.formatMessage({ id: "No availability" })}
|
||||||
|
|||||||
@@ -19,6 +19,15 @@ const hotelSurroundingsFilterNames = [
|
|||||||
"Omgivningar",
|
"Omgivningar",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
const hotelFacilitiesFilterNames = [
|
||||||
|
"Hotel facilities",
|
||||||
|
"Hotellfaciliteter",
|
||||||
|
"Hotelfaciliteter",
|
||||||
|
"Hotel faciliteter",
|
||||||
|
"Hotel-Infos",
|
||||||
|
"Hotellin palvelut",
|
||||||
|
]
|
||||||
|
|
||||||
export async function fetchAvailableHotels(
|
export async function fetchAvailableHotels(
|
||||||
input: AvailabilityInput
|
input: AvailabilityInput
|
||||||
): Promise<HotelData[]> {
|
): Promise<HotelData[]> {
|
||||||
@@ -52,6 +61,7 @@ export function getFiltersFromHotels(hotels: HotelData[]): CategorizedFilters {
|
|||||||
const filterList: Filter[] = uniqueFilterIds
|
const filterList: Filter[] = uniqueFilterIds
|
||||||
.map((filterId) => filters.find((filter) => filter.id === filterId))
|
.map((filterId) => filters.find((filter) => filter.id === filterId))
|
||||||
.filter((filter): filter is Filter => filter !== undefined)
|
.filter((filter): filter is Filter => filter !== undefined)
|
||||||
|
.sort((a, b) => b.sortOrder - a.sortOrder)
|
||||||
|
|
||||||
return filterList.reduce<CategorizedFilters>(
|
return filterList.reduce<CategorizedFilters>(
|
||||||
(acc, filter) => {
|
(acc, filter) => {
|
||||||
@@ -61,10 +71,13 @@ export function getFiltersFromHotels(hotels: HotelData[]): CategorizedFilters {
|
|||||||
surroundingsFilters: [...acc.surroundingsFilters, filter],
|
surroundingsFilters: [...acc.surroundingsFilters, filter],
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
if (filter.filter && hotelFacilitiesFilterNames.includes(filter.filter))
|
||||||
facilityFilters: [...acc.facilityFilters, filter],
|
return {
|
||||||
surroundingsFilters: acc.surroundingsFilters,
|
facilityFilters: [...acc.facilityFilters, filter],
|
||||||
}
|
surroundingsFilters: acc.surroundingsFilters,
|
||||||
|
}
|
||||||
|
|
||||||
|
return acc
|
||||||
},
|
},
|
||||||
{ facilityFilters: [], surroundingsFilters: [] }
|
{ facilityFilters: [], surroundingsFilters: [] }
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -206,11 +206,12 @@ export default function Search({ locations }: SearchProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function SearchSkeleton() {
|
export function SearchSkeleton() {
|
||||||
|
const intl = useIntl()
|
||||||
return (
|
return (
|
||||||
<div className={styles.container}>
|
<div className={styles.container}>
|
||||||
<div className={styles.label}>
|
<div className={styles.label}>
|
||||||
<Caption type="bold" color="red" asChild>
|
<Caption type="bold" color="red" asChild>
|
||||||
<span>Where to</span>
|
<span>{intl.formatMessage({ id: "Where to" })}</span>
|
||||||
</Caption>
|
</Caption>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.input}>
|
<div className={styles.input}>
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { zodResolver } from "@hookform/resolvers/zod"
|
import { zodResolver } from "@hookform/resolvers/zod"
|
||||||
|
import { useRouter } from "next/navigation"
|
||||||
import { FormProvider, useForm } from "react-hook-form"
|
import { FormProvider, useForm } from "react-hook-form"
|
||||||
import { useIntl } from "react-intl"
|
import { useIntl } from "react-intl"
|
||||||
|
|
||||||
import { privacyPolicy } from "@/constants/currentWebHrefs"
|
import { privacyPolicy } from "@/constants/currentWebHrefs"
|
||||||
|
import { trpc } from "@/lib/trpc/client"
|
||||||
|
|
||||||
import { registerUser } from "@/actions/registerUser"
|
|
||||||
import Button from "@/components/TempDesignSystem/Button"
|
import Button from "@/components/TempDesignSystem/Button"
|
||||||
import Checkbox from "@/components/TempDesignSystem/Form/Checkbox"
|
import Checkbox from "@/components/TempDesignSystem/Form/Checkbox"
|
||||||
import CountrySelect from "@/components/TempDesignSystem/Form/Country"
|
import CountrySelect from "@/components/TempDesignSystem/Form/Country"
|
||||||
@@ -30,11 +31,28 @@ import type { SignUpFormProps } from "@/types/components/form/signupForm"
|
|||||||
|
|
||||||
export default function SignupForm({ link, subtitle, title }: SignUpFormProps) {
|
export default function SignupForm({ link, subtitle, title }: SignUpFormProps) {
|
||||||
const intl = useIntl()
|
const intl = useIntl()
|
||||||
|
const router = useRouter()
|
||||||
const lang = useLang()
|
const lang = useLang()
|
||||||
const country = intl.formatMessage({ id: "Country" })
|
const country = intl.formatMessage({ id: "Country" })
|
||||||
const email = intl.formatMessage({ id: "Email address" })
|
const email = intl.formatMessage({ id: "Email address" })
|
||||||
const phoneNumber = intl.formatMessage({ id: "Phone number" })
|
const phoneNumber = intl.formatMessage({ id: "Phone number" })
|
||||||
const zipCode = intl.formatMessage({ id: "Zip code" })
|
const zipCode = intl.formatMessage({ id: "Zip code" })
|
||||||
|
const signupButtonText = intl.formatMessage({
|
||||||
|
id: "Sign up to Scandic Friends",
|
||||||
|
})
|
||||||
|
const signingUpPendingText = intl.formatMessage({ id: "Signing up..." })
|
||||||
|
|
||||||
|
const signup = trpc.user.signup.useMutation({
|
||||||
|
onSuccess: (data) => {
|
||||||
|
if (data.success && data.redirectUrl) {
|
||||||
|
router.push(data.redirectUrl)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
toast.error(intl.formatMessage({ id: "Something went wrong!" }))
|
||||||
|
console.error("Component Signup error:", error)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
const methods = useForm<SignUpSchema>({
|
const methods = useForm<SignUpSchema>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
@@ -56,19 +74,7 @@ export default function SignupForm({ link, subtitle, title }: SignUpFormProps) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
async function onSubmit(data: SignUpSchema) {
|
async function onSubmit(data: SignUpSchema) {
|
||||||
try {
|
signup.mutate({ ...data, language: lang })
|
||||||
const result = await registerUser(data)
|
|
||||||
if (result && !result.success) {
|
|
||||||
toast.error(intl.formatMessage({ id: "Something went wrong!" }))
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
// The server-side redirect will throw an error, which we can ignore
|
|
||||||
// as it's handled by Next.js.
|
|
||||||
if (error instanceof Error && error.message.includes("NEXT_REDIRECT")) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
toast.error(intl.formatMessage({ id: "Something went wrong!" }))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -79,11 +85,6 @@ export default function SignupForm({ link, subtitle, title }: SignUpFormProps) {
|
|||||||
className={styles.form}
|
className={styles.form}
|
||||||
id="register"
|
id="register"
|
||||||
onSubmit={methods.handleSubmit(onSubmit)}
|
onSubmit={methods.handleSubmit(onSubmit)}
|
||||||
/**
|
|
||||||
* Ignoring since ts doesn't recognize that tRPC
|
|
||||||
* parses FormData before reaching the route
|
|
||||||
* @ts-ignore */
|
|
||||||
action={registerUser}
|
|
||||||
>
|
>
|
||||||
<section className={styles.userInfo}>
|
<section className={styles.userInfo}>
|
||||||
<div className={styles.container}>
|
<div className={styles.container}>
|
||||||
@@ -186,7 +187,7 @@ export default function SignupForm({ link, subtitle, title }: SignUpFormProps) {
|
|||||||
onClick={() => methods.trigger()}
|
onClick={() => methods.trigger()}
|
||||||
data-testid="trigger-validation"
|
data-testid="trigger-validation"
|
||||||
>
|
>
|
||||||
{intl.formatMessage({ id: "Sign up to Scandic Friends" })}
|
{signupButtonText}
|
||||||
</Button>
|
</Button>
|
||||||
) : (
|
) : (
|
||||||
<Button
|
<Button
|
||||||
@@ -194,10 +195,12 @@ export default function SignupForm({ link, subtitle, title }: SignUpFormProps) {
|
|||||||
type="submit"
|
type="submit"
|
||||||
theme="base"
|
theme="base"
|
||||||
intent="primary"
|
intent="primary"
|
||||||
disabled={methods.formState.isSubmitting}
|
disabled={methods.formState.isSubmitting || signup.isPending}
|
||||||
data-testid="submit"
|
data-testid="submit"
|
||||||
>
|
>
|
||||||
{intl.formatMessage({ id: "Sign up to Scandic Friends" })}
|
{methods.formState.isSubmitting || signup.isPending
|
||||||
|
? signingUpPendingText
|
||||||
|
: signupButtonText}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -94,7 +94,7 @@ export default function ChildInfoSelector({
|
|||||||
updateSelectedAge(key as number)
|
updateSelectedAge(key as number)
|
||||||
}}
|
}}
|
||||||
placeholder={ageLabel}
|
placeholder={ageLabel}
|
||||||
maxHeight={150}
|
maxHeight={180}
|
||||||
{...register(ageFieldName, {
|
{...register(ageFieldName, {
|
||||||
required: true,
|
required: true,
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -99,7 +99,7 @@ export default function GuestsRoomsPickerDialog({
|
|||||||
<Tooltip
|
<Tooltip
|
||||||
heading={disabledBookingOptionsHeader}
|
heading={disabledBookingOptionsHeader}
|
||||||
text={disabledBookingOptionsText}
|
text={disabledBookingOptionsText}
|
||||||
position="top"
|
position="bottom"
|
||||||
arrow="left"
|
arrow="left"
|
||||||
>
|
>
|
||||||
{rooms.length < 4 ? (
|
{rooms.length < 4 ? (
|
||||||
@@ -124,7 +124,7 @@ export default function GuestsRoomsPickerDialog({
|
|||||||
<Tooltip
|
<Tooltip
|
||||||
heading={disabledBookingOptionsHeader}
|
heading={disabledBookingOptionsHeader}
|
||||||
text={disabledBookingOptionsText}
|
text={disabledBookingOptionsText}
|
||||||
position="top"
|
position="bottom"
|
||||||
arrow="left"
|
arrow="left"
|
||||||
>
|
>
|
||||||
{rooms.length < 4 ? (
|
{rooms.length < 4 ? (
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
.form {
|
.form {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: var(--Spacing-x3);
|
gap: var(--Spacing-x3);
|
||||||
margin-bottom: var(--Spacing-x3);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.container {
|
.container {
|
||||||
|
|||||||
@@ -75,7 +75,11 @@ export default function SectionAccordion({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<header className={styles.header}>
|
<header className={styles.header}>
|
||||||
<button onClick={onModify} className={styles.modifyButton}>
|
<button
|
||||||
|
onClick={onModify}
|
||||||
|
disabled={!isComplete}
|
||||||
|
className={styles.modifyButton}
|
||||||
|
>
|
||||||
<Footnote
|
<Footnote
|
||||||
className={styles.title}
|
className={styles.title}
|
||||||
asChild
|
asChild
|
||||||
@@ -94,7 +98,9 @@ export default function SectionAccordion({
|
|||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
</header>
|
</header>
|
||||||
<div className={styles.content}>{children}</div>
|
<div className={styles.content}>
|
||||||
|
<div className={styles.contentWrapper}>{children}</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,6 +33,10 @@
|
|||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.modifyButton:disabled {
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
.title {
|
.title {
|
||||||
grid-area: title;
|
grid-area: title;
|
||||||
text-align: start;
|
text-align: start;
|
||||||
@@ -79,7 +83,10 @@
|
|||||||
|
|
||||||
.accordion[data-open="true"] {
|
.accordion[data-open="true"] {
|
||||||
grid-template-rows: var(--header-height) 1fr;
|
grid-template-rows: var(--header-height) 1fr;
|
||||||
gap: var(--Spacing-x3);
|
}
|
||||||
|
|
||||||
|
.contentWrapper {
|
||||||
|
padding-bottom: var(--Spacing-x3);
|
||||||
}
|
}
|
||||||
|
|
||||||
.content {
|
.content {
|
||||||
@@ -90,7 +97,7 @@
|
|||||||
|
|
||||||
@media screen and (min-width: 768px) {
|
@media screen and (min-width: 768px) {
|
||||||
.accordion {
|
.accordion {
|
||||||
gap: var(--Spacing-x3);
|
column-gap: var(--Spacing-x3);
|
||||||
grid-template-areas: "circle header" "circle content";
|
grid-template-areas: "circle header" "circle content";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -81,6 +81,10 @@
|
|||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
.close {
|
.close {
|
||||||
background: none;
|
background: none;
|
||||||
border: none;
|
border: none;
|
||||||
@@ -97,3 +101,80 @@
|
|||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
border-top: 1px solid var(--Base-Border-Subtle);
|
border-top: 1px solid var(--Base-Border-Subtle);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media screen and (min-width: 768px) {
|
||||||
|
.modal {
|
||||||
|
left: 50%;
|
||||||
|
bottom: 50%;
|
||||||
|
height: min(80dvh, 680px);
|
||||||
|
width: min(80dvw, 960px);
|
||||||
|
translate: -50% 50%;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: auto 1fr;
|
||||||
|
padding: var(--Spacing-x2) var(--Spacing-x3);
|
||||||
|
align-items: center;
|
||||||
|
border-bottom: 1px solid var(--Base-Border-Subtle);
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
background: var(--Base-Surface-Primary-light-Normal);
|
||||||
|
z-index: 1;
|
||||||
|
border-top-left-radius: var(--Corner-radius-large);
|
||||||
|
border-top-right-radius: var(--Corner-radius-large);
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
gap: var(--Spacing-x4);
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filters {
|
||||||
|
overflow-y: unset;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sorter,
|
||||||
|
.filters,
|
||||||
|
.footer,
|
||||||
|
.divider {
|
||||||
|
padding: 0 var(--Spacing-x3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
flex-direction: row-reverse;
|
||||||
|
justify-content: space-between;
|
||||||
|
position: sticky;
|
||||||
|
bottom: 0;
|
||||||
|
background: var(--Base-Surface-Primary-light-Normal);
|
||||||
|
z-index: 1;
|
||||||
|
border-bottom-left-radius: var(--Corner-radius-large);
|
||||||
|
border-bottom-right-radius: var(--Corner-radius-large);
|
||||||
|
padding: var(--Spacing-x2) var(--Spacing-x3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.filters aside h1 {
|
||||||
|
margin-bottom: var(--Spacing-x2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.filters aside > div:last-child {
|
||||||
|
margin-top: var(--Spacing-x4);
|
||||||
|
padding-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filters aside ul {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
margin-top: var(--Spacing-x3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@media screen and (min-width: 1024) {
|
||||||
|
.facilities ul {
|
||||||
|
grid-template-columns: 1fr 1fr 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ import { useHotelFilterStore } from "@/stores/hotel-filters"
|
|||||||
|
|
||||||
import { CloseLargeIcon, FilterIcon } from "@/components/Icons"
|
import { CloseLargeIcon, FilterIcon } from "@/components/Icons"
|
||||||
import Button from "@/components/TempDesignSystem/Button"
|
import Button from "@/components/TempDesignSystem/Button"
|
||||||
|
import Divider from "@/components/TempDesignSystem/Divider"
|
||||||
|
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
|
||||||
|
|
||||||
import HotelFilter from "../HotelFilter"
|
import HotelFilter from "../HotelFilter"
|
||||||
import HotelSorter from "../HotelSorter"
|
import HotelSorter from "../HotelSorter"
|
||||||
@@ -47,10 +49,18 @@ export default function FilterAndSortModal({
|
|||||||
>
|
>
|
||||||
<CloseLargeIcon />
|
<CloseLargeIcon />
|
||||||
</button>
|
</button>
|
||||||
|
<Subtitle
|
||||||
|
type="two"
|
||||||
|
textAlign="center"
|
||||||
|
className={styles.title}
|
||||||
|
>
|
||||||
|
{intl.formatMessage({ id: "Filter and sort" })}
|
||||||
|
</Subtitle>
|
||||||
</header>
|
</header>
|
||||||
<div className={styles.sorter}>
|
<div className={styles.sorter}>
|
||||||
<HotelSorter />
|
<HotelSorter />
|
||||||
</div>
|
</div>
|
||||||
|
<Divider color="subtle" className="divider" />
|
||||||
<div className={styles.filters}>
|
<div className={styles.filters}>
|
||||||
<HotelFilter filters={filters} />
|
<HotelFilter filters={filters} />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
22
components/HotelReservation/SelectHotel/HotelCount/index.tsx
Normal file
22
components/HotelReservation/SelectHotel/HotelCount/index.tsx
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
"use client"
|
||||||
|
import { useIntl } from "react-intl"
|
||||||
|
|
||||||
|
import { useHotelFilterStore } from "@/stores/hotel-filters"
|
||||||
|
|
||||||
|
import Preamble from "@/components/TempDesignSystem/Text/Preamble"
|
||||||
|
|
||||||
|
export default function HotelCount() {
|
||||||
|
const intl = useIntl()
|
||||||
|
const resultCount = useHotelFilterStore((state) => state.resultCount)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Preamble>
|
||||||
|
{intl.formatMessage(
|
||||||
|
{
|
||||||
|
id: "Hotel(s)",
|
||||||
|
},
|
||||||
|
{ amount: resultCount }
|
||||||
|
)}
|
||||||
|
</Preamble>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -13,6 +13,7 @@ import { BackToTopButton } from "@/components/TempDesignSystem/BackToTopButton"
|
|||||||
import Button from "@/components/TempDesignSystem/Button"
|
import Button from "@/components/TempDesignSystem/Button"
|
||||||
import useLang from "@/hooks/useLang"
|
import useLang from "@/hooks/useLang"
|
||||||
|
|
||||||
|
import FilterAndSortModal from "../FilterAndSortModal"
|
||||||
import HotelListing from "./HotelListing"
|
import HotelListing from "./HotelListing"
|
||||||
import { getCentralCoordinates } from "./utils"
|
import { getCentralCoordinates } from "./utils"
|
||||||
|
|
||||||
@@ -25,6 +26,7 @@ export default function SelectHotelMap({
|
|||||||
hotelPins,
|
hotelPins,
|
||||||
mapId,
|
mapId,
|
||||||
hotels,
|
hotels,
|
||||||
|
filterList,
|
||||||
}: SelectHotelMapProps) {
|
}: SelectHotelMapProps) {
|
||||||
const searchParams = useSearchParams()
|
const searchParams = useSearchParams()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
@@ -102,8 +104,7 @@ export default function SelectHotelMap({
|
|||||||
>
|
>
|
||||||
<CloseLargeIcon />
|
<CloseLargeIcon />
|
||||||
</Button>
|
</Button>
|
||||||
<span>Filter and sort</span>
|
<FilterAndSortModal filters={filterList} />
|
||||||
{/* TODO: Add filter and sort button */}
|
|
||||||
</div>
|
</div>
|
||||||
<HotelListing
|
<HotelListing
|
||||||
hotels={hotels}
|
hotels={hotels}
|
||||||
|
|||||||
@@ -41,4 +41,9 @@
|
|||||||
.container {
|
.container {
|
||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.filterContainer {
|
||||||
|
justify-content: flex-end;
|
||||||
|
padding: 0 0 var(--Spacing-x1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -60,7 +60,6 @@ export default function RoomCard({
|
|||||||
|
|
||||||
const getBreakfastMessage = (rate: RateDefinition | undefined) => {
|
const getBreakfastMessage = (rate: RateDefinition | undefined) => {
|
||||||
const breakfastIncluded = getRateDefinitionForRate(rate)?.breakfastIncluded
|
const breakfastIncluded = getRateDefinitionForRate(rate)?.breakfastIncluded
|
||||||
|
|
||||||
switch (breakfastIncluded) {
|
switch (breakfastIncluded) {
|
||||||
case true:
|
case true:
|
||||||
return intl.formatMessage({ id: "Breakfast is included." })
|
return intl.formatMessage({ id: "Breakfast is included." })
|
||||||
@@ -83,7 +82,6 @@ export default function RoomCard({
|
|||||||
)
|
)
|
||||||
|
|
||||||
const { roomSize, occupancy, images } = selectedRoom || {}
|
const { roomSize, occupancy, images } = selectedRoom || {}
|
||||||
const mainImage = images?.[0]
|
|
||||||
|
|
||||||
const freeCancelation = intl.formatMessage({ id: "Free cancellation" })
|
const freeCancelation = intl.formatMessage({ id: "Free cancellation" })
|
||||||
const nonRefundable = intl.formatMessage({ id: "Non-refundable" })
|
const nonRefundable = intl.formatMessage({ id: "Non-refundable" })
|
||||||
@@ -115,53 +113,56 @@ export default function RoomCard({
|
|||||||
return (
|
return (
|
||||||
<div className={classNames}>
|
<div className={classNames}>
|
||||||
<div>
|
<div>
|
||||||
{mainImage && (
|
<div className={styles.imageContainer}>
|
||||||
<div className={styles.imageContainer}>
|
<div className={styles.chipContainer}>
|
||||||
<div className={styles.chipContainer}>
|
{roomConfiguration.roomsLeft < 5 && (
|
||||||
{roomConfiguration.roomsLeft < 5 && (
|
<span className={styles.chip}>
|
||||||
<span className={styles.chip}>
|
<Footnote
|
||||||
<Footnote
|
color="burgundy"
|
||||||
color="burgundy"
|
textTransform="uppercase"
|
||||||
textTransform="uppercase"
|
>{`${roomConfiguration.roomsLeft} ${intl.formatMessage({ id: "Left" })}`}</Footnote>
|
||||||
>{`${roomConfiguration.roomsLeft} ${intl.formatMessage({ id: "Left" })}`}</Footnote>
|
</span>
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
{roomConfiguration.features
|
|
||||||
.filter((feature) => selectedPackages.includes(feature.code))
|
|
||||||
.map((feature) => (
|
|
||||||
<span className={styles.chip} key={feature.code}>
|
|
||||||
{createElement(getIconForFeatureCode(feature.code), {
|
|
||||||
width: 16,
|
|
||||||
height: 16,
|
|
||||||
color: "burgundy",
|
|
||||||
})}
|
|
||||||
</span>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
{/*NOTE: images from the test API are hosted on test3.scandichotels.com,
|
|
||||||
which can't be accessed unless on Scandic's Wifi or using Citrix. */}
|
|
||||||
<ImageGallery
|
|
||||||
images={images}
|
|
||||||
title={roomConfiguration.roomType}
|
|
||||||
fill
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div className={styles.specification}>
|
|
||||||
<Caption color="uiTextMediumContrast" className={styles.guests}>
|
|
||||||
{intl.formatMessage(
|
|
||||||
{
|
|
||||||
id: "booking.guests",
|
|
||||||
},
|
|
||||||
{ nrOfGuests: occupancy?.total }
|
|
||||||
)}
|
)}
|
||||||
</Caption>
|
{roomConfiguration.features
|
||||||
<Caption color="uiTextMediumContrast">
|
.filter((feature) => selectedPackages.includes(feature.code))
|
||||||
{roomSize?.min === roomSize?.max
|
.map((feature) => (
|
||||||
? roomSize?.min
|
<span className={styles.chip} key={feature.code}>
|
||||||
: `${roomSize?.min}-${roomSize?.max}`}
|
{createElement(getIconForFeatureCode(feature.code), {
|
||||||
m²
|
width: 16,
|
||||||
</Caption>
|
height: 16,
|
||||||
|
color: "burgundy",
|
||||||
|
})}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
{/*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. */}
|
||||||
|
<ImageGallery
|
||||||
|
images={images}
|
||||||
|
title={roomConfiguration.roomType}
|
||||||
|
fill
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.specification}>
|
||||||
|
{occupancy && (
|
||||||
|
<Caption color="uiTextMediumContrast" className={styles.guests}>
|
||||||
|
{intl.formatMessage(
|
||||||
|
{
|
||||||
|
id: "booking.guests",
|
||||||
|
},
|
||||||
|
{ nrOfGuests: occupancy?.total }
|
||||||
|
)}
|
||||||
|
</Caption>
|
||||||
|
)}
|
||||||
|
{roomSize && (
|
||||||
|
<Caption color="uiTextMediumContrast">
|
||||||
|
{roomSize.min === roomSize.max
|
||||||
|
? roomSize.min
|
||||||
|
: `${roomSize.min}-${roomSize.max}`}
|
||||||
|
m²
|
||||||
|
</Caption>
|
||||||
|
)}
|
||||||
<div className={styles.toggleSidePeek}>
|
<div className={styles.toggleSidePeek}>
|
||||||
{roomConfiguration.roomTypeCode && (
|
{roomConfiguration.roomTypeCode && (
|
||||||
<ToggleSidePeek
|
<ToggleSidePeek
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
border: 1px solid var(--Base-Border-Subtle);
|
border: 1px solid var(--Base-Border-Subtle);
|
||||||
position: relative;
|
position: relative;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
min-height: 730px;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ export function filterDuplicateRoomTypesByLowestPrice(
|
|||||||
Math.min(
|
Math.min(
|
||||||
Number(
|
Number(
|
||||||
previousLowest.products[0].productType.public.requestedPrice
|
previousLowest.products[0].productType.public.requestedPrice
|
||||||
.pricePerNight
|
?.pricePerNight
|
||||||
) ?? Infinity,
|
) ?? Infinity,
|
||||||
Number(
|
Number(
|
||||||
previousLowest.products[0].productType.member?.requestedPrice
|
previousLowest.products[0].productType.member?.requestedPrice
|
||||||
@@ -77,7 +77,7 @@ export function filterDuplicateRoomTypesByLowestPrice(
|
|||||||
Math.min(
|
Math.min(
|
||||||
Number(
|
Number(
|
||||||
previousLowest.products[0].productType.public.requestedPrice
|
previousLowest.products[0].productType.public.requestedPrice
|
||||||
.pricePerNight
|
?.pricePerNight
|
||||||
) ?? Infinity,
|
) ?? Infinity,
|
||||||
Number(
|
Number(
|
||||||
previousLowest.products[0].productType.member?.requestedPrice
|
previousLowest.products[0].productType.member?.requestedPrice
|
||||||
@@ -88,7 +88,7 @@ export function filterDuplicateRoomTypesByLowestPrice(
|
|||||||
Math.min(
|
Math.min(
|
||||||
Number(
|
Number(
|
||||||
previousLowest.products[0].productType.public.localPrice
|
previousLowest.products[0].productType.public.localPrice
|
||||||
.pricePerNight
|
?.pricePerNight
|
||||||
) ?? Infinity,
|
) ?? Infinity,
|
||||||
Number(
|
Number(
|
||||||
previousLowest.products[0].productType.member?.localPrice
|
previousLowest.products[0].productType.member?.localPrice
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ export function MapModal({ children }: { children: React.ReactNode }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.wrapper} ref={rootDiv}>
|
<div className={styles.wrapper} ref={rootDiv}>
|
||||||
<Modal isDismissable isOpen={isOpen} onOpenChange={handleOnOpenChange}>
|
<Modal isOpen={isOpen} onOpenChange={handleOnOpenChange}>
|
||||||
<Dialog
|
<Dialog
|
||||||
style={
|
style={
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ export function Tooltip<P extends TooltipPosition>({
|
|||||||
role="tooltip"
|
role="tooltip"
|
||||||
aria-label={text}
|
aria-label={text}
|
||||||
onClick={handleToggle}
|
onClick={handleToggle}
|
||||||
|
onTouchStart={handleToggle}
|
||||||
data-active={isActive}
|
data-active={isActive}
|
||||||
>
|
>
|
||||||
<div className={className}>
|
<div className={className}>
|
||||||
|
|||||||
@@ -16,6 +16,7 @@
|
|||||||
transition: opacity 0.3s;
|
transition: opacity 0.3s;
|
||||||
max-width: 200px;
|
max-width: 200px;
|
||||||
min-width: 150px;
|
min-width: 150px;
|
||||||
|
height: fit-content;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tooltipContainer:hover .tooltip {
|
.tooltipContainer:hover .tooltip {
|
||||||
|
|||||||
@@ -154,6 +154,7 @@
|
|||||||
"Hotel": "Hotel",
|
"Hotel": "Hotel",
|
||||||
"Hotel facilities": "Hotel faciliteter",
|
"Hotel facilities": "Hotel faciliteter",
|
||||||
"Hotel surroundings": "Hotel omgivelser",
|
"Hotel surroundings": "Hotel omgivelser",
|
||||||
|
"Hotel(s)": "{amount} {amount, plural, one {hotel} other {hoteller}}",
|
||||||
"Hotels": "Hoteller",
|
"Hotels": "Hoteller",
|
||||||
"How do you want to sleep?": "Hvordan vil du sove?",
|
"How do you want to sleep?": "Hvordan vil du sove?",
|
||||||
"How it works": "Hvordan det virker",
|
"How it works": "Hvordan det virker",
|
||||||
@@ -336,6 +337,7 @@
|
|||||||
"Show wellness & exercise": "Vis velvære og motion",
|
"Show wellness & exercise": "Vis velvære og motion",
|
||||||
"Sign up bonus": "Velkomstbonus",
|
"Sign up bonus": "Velkomstbonus",
|
||||||
"Sign up to Scandic Friends": "Tilmeld dig Scandic Friends",
|
"Sign up to Scandic Friends": "Tilmeld dig Scandic Friends",
|
||||||
|
"Signing up...": "Tilmelder...",
|
||||||
"Skip to main content": "Spring over og gå til hovedindhold",
|
"Skip to main content": "Spring over og gå til hovedindhold",
|
||||||
"Something went wrong and we couldn't add your card. Please try again later.": "Noget gik galt, og vi kunne ikke tilføje dit kort. Prøv venligst igen senere.",
|
"Something went wrong and we couldn't add your card. Please try again later.": "Noget gik galt, og vi kunne ikke tilføje dit kort. Prøv venligst igen senere.",
|
||||||
"Something went wrong and we couldn't remove your card. Please try again later.": "Noget gik galt, og vi kunne ikke fjerne dit kort. Prøv venligst igen senere.",
|
"Something went wrong and we couldn't remove your card. Please try again later.": "Noget gik galt, og vi kunne ikke fjerne dit kort. Prøv venligst igen senere.",
|
||||||
|
|||||||
@@ -154,6 +154,7 @@
|
|||||||
"Hotel": "Hotel",
|
"Hotel": "Hotel",
|
||||||
"Hotel facilities": "Hotel-Infos",
|
"Hotel facilities": "Hotel-Infos",
|
||||||
"Hotel surroundings": "Umgebung des Hotels",
|
"Hotel surroundings": "Umgebung des Hotels",
|
||||||
|
"Hotel(s)": "{amount} {amount, plural, one {hotel} other {hotels}}",
|
||||||
"Hotels": "Hotels",
|
"Hotels": "Hotels",
|
||||||
"How do you want to sleep?": "Wie möchtest du schlafen?",
|
"How do you want to sleep?": "Wie möchtest du schlafen?",
|
||||||
"How it works": "Wie es funktioniert",
|
"How it works": "Wie es funktioniert",
|
||||||
@@ -335,6 +336,7 @@
|
|||||||
"Show wellness & exercise": "Zeige Wellness und Bewegung",
|
"Show wellness & exercise": "Zeige Wellness und Bewegung",
|
||||||
"Sign up bonus": "Anmelde-Bonus",
|
"Sign up bonus": "Anmelde-Bonus",
|
||||||
"Sign up to Scandic Friends": "Treten Sie Scandic Friends bei",
|
"Sign up to Scandic Friends": "Treten Sie Scandic Friends bei",
|
||||||
|
"Signing up...": "Registrierung läuft...",
|
||||||
"Skip to main content": "Direkt zum Inhalt",
|
"Skip to main content": "Direkt zum Inhalt",
|
||||||
"Something went wrong and we couldn't add your card. Please try again later.": "Ein Fehler ist aufgetreten und wir konnten Ihre Karte nicht hinzufügen. Bitte versuchen Sie es später erneut.",
|
"Something went wrong and we couldn't add your card. Please try again later.": "Ein Fehler ist aufgetreten und wir konnten Ihre Karte nicht hinzufügen. Bitte versuchen Sie es später erneut.",
|
||||||
"Something went wrong and we couldn't remove your card. Please try again later.": "Ein Fehler ist aufgetreten und wir konnten Ihre Karte nicht entfernen. Bitte versuchen Sie es später noch einmal.",
|
"Something went wrong and we couldn't remove your card. Please try again later.": "Ein Fehler ist aufgetreten und wir konnten Ihre Karte nicht entfernen. Bitte versuchen Sie es später noch einmal.",
|
||||||
|
|||||||
@@ -166,6 +166,7 @@
|
|||||||
"Hotel": "Hotel",
|
"Hotel": "Hotel",
|
||||||
"Hotel facilities": "Hotel facilities",
|
"Hotel facilities": "Hotel facilities",
|
||||||
"Hotel surroundings": "Hotel surroundings",
|
"Hotel surroundings": "Hotel surroundings",
|
||||||
|
"Hotel(s)": "{amount} {amount, plural, one {hotel} other {hotels}}",
|
||||||
"Hotels": "Hotels",
|
"Hotels": "Hotels",
|
||||||
"How do you want to sleep?": "How do you want to sleep?",
|
"How do you want to sleep?": "How do you want to sleep?",
|
||||||
"How it works": "How it works",
|
"How it works": "How it works",
|
||||||
@@ -365,6 +366,7 @@
|
|||||||
"Show wellness & exercise": "Show wellness & exercise",
|
"Show wellness & exercise": "Show wellness & exercise",
|
||||||
"Sign up bonus": "Sign up bonus",
|
"Sign up bonus": "Sign up bonus",
|
||||||
"Sign up to Scandic Friends": "Sign up to Scandic Friends",
|
"Sign up to Scandic Friends": "Sign up to Scandic Friends",
|
||||||
|
"Signing up...": "Signing up...",
|
||||||
"Skip to main content": "Skip to main content",
|
"Skip to main content": "Skip to main content",
|
||||||
"Something went wrong and we couldn't add your card. Please try again later.": "Something went wrong and we couldn't add your card. Please try again later.",
|
"Something went wrong and we couldn't add your card. Please try again later.": "Something went wrong and we couldn't add your card. Please try again later.",
|
||||||
"Something went wrong and we couldn't remove your card. Please try again later.": "Something went wrong and we couldn't remove your card. Please try again later.",
|
"Something went wrong and we couldn't remove your card. Please try again later.": "Something went wrong and we couldn't remove your card. Please try again later.",
|
||||||
|
|||||||
@@ -154,6 +154,7 @@
|
|||||||
"Hotel": "Hotelli",
|
"Hotel": "Hotelli",
|
||||||
"Hotel facilities": "Hotellin palvelut",
|
"Hotel facilities": "Hotellin palvelut",
|
||||||
"Hotel surroundings": "Hotellin ympäristö",
|
"Hotel surroundings": "Hotellin ympäristö",
|
||||||
|
"Hotel(s)": "{amount} {amount, plural, one {hotelli} other {hotellit}}",
|
||||||
"Hotels": "Hotellit",
|
"Hotels": "Hotellit",
|
||||||
"How do you want to sleep?": "Kuinka haluat nukkua?",
|
"How do you want to sleep?": "Kuinka haluat nukkua?",
|
||||||
"How it works": "Kuinka se toimii",
|
"How it works": "Kuinka se toimii",
|
||||||
@@ -337,6 +338,7 @@
|
|||||||
"Show wellness & exercise": "Näytä hyvinvointi ja liikunta",
|
"Show wellness & exercise": "Näytä hyvinvointi ja liikunta",
|
||||||
"Sign up bonus": "Liittymisbonus",
|
"Sign up bonus": "Liittymisbonus",
|
||||||
"Sign up to Scandic Friends": "Liity Scandic Friends -jäseneksi",
|
"Sign up to Scandic Friends": "Liity Scandic Friends -jäseneksi",
|
||||||
|
"Signing up...": "Rekisteröidytään...",
|
||||||
"Skip to main content": "Siirry pääsisältöön",
|
"Skip to main content": "Siirry pääsisältöön",
|
||||||
"Something went wrong and we couldn't add your card. Please try again later.": "Jotain meni pieleen, emmekä voineet lisätä korttiasi. Yritä myöhemmin uudelleen.",
|
"Something went wrong and we couldn't add your card. Please try again later.": "Jotain meni pieleen, emmekä voineet lisätä korttiasi. Yritä myöhemmin uudelleen.",
|
||||||
"Something went wrong and we couldn't remove your card. Please try again later.": "Jotain meni pieleen, emmekä voineet poistaa korttiasi. Yritä myöhemmin uudelleen.",
|
"Something went wrong and we couldn't remove your card. Please try again later.": "Jotain meni pieleen, emmekä voineet poistaa korttiasi. Yritä myöhemmin uudelleen.",
|
||||||
|
|||||||
@@ -153,6 +153,7 @@
|
|||||||
"Hotel": "Hotel",
|
"Hotel": "Hotel",
|
||||||
"Hotel facilities": "Hotelfaciliteter",
|
"Hotel facilities": "Hotelfaciliteter",
|
||||||
"Hotel surroundings": "Hotellomgivelser",
|
"Hotel surroundings": "Hotellomgivelser",
|
||||||
|
"Hotel(s)": "{amount} {amount, plural, one {hotell} other {hoteller}}",
|
||||||
"Hotels": "Hoteller",
|
"Hotels": "Hoteller",
|
||||||
"How do you want to sleep?": "Hvordan vil du sove?",
|
"How do you want to sleep?": "Hvordan vil du sove?",
|
||||||
"How it works": "Hvordan det fungerer",
|
"How it works": "Hvordan det fungerer",
|
||||||
@@ -334,6 +335,7 @@
|
|||||||
"Show wellness & exercise": "Vis velvære og trening",
|
"Show wellness & exercise": "Vis velvære og trening",
|
||||||
"Sign up bonus": "Velkomstbonus",
|
"Sign up bonus": "Velkomstbonus",
|
||||||
"Sign up to Scandic Friends": "Bli med i Scandic Friends",
|
"Sign up to Scandic Friends": "Bli med i Scandic Friends",
|
||||||
|
"Signing up...": "Registrerer...",
|
||||||
"Skip to main content": "Gå videre til hovedsiden",
|
"Skip to main content": "Gå videre til hovedsiden",
|
||||||
"Something went wrong and we couldn't add your card. Please try again later.": "Noe gikk galt, og vi kunne ikke legge til kortet ditt. Prøv igjen senere.",
|
"Something went wrong and we couldn't add your card. Please try again later.": "Noe gikk galt, og vi kunne ikke legge til kortet ditt. Prøv igjen senere.",
|
||||||
"Something went wrong and we couldn't remove your card. Please try again later.": "Noe gikk galt, og vi kunne ikke fjerne kortet ditt. Vennligst prøv igjen senere.",
|
"Something went wrong and we couldn't remove your card. Please try again later.": "Noe gikk galt, og vi kunne ikke fjerne kortet ditt. Vennligst prøv igjen senere.",
|
||||||
|
|||||||
@@ -153,6 +153,7 @@
|
|||||||
"Hotel": "Hotell",
|
"Hotel": "Hotell",
|
||||||
"Hotel facilities": "Hotellfaciliteter",
|
"Hotel facilities": "Hotellfaciliteter",
|
||||||
"Hotel surroundings": "Hotellomgivning",
|
"Hotel surroundings": "Hotellomgivning",
|
||||||
|
"Hotel(s)": "{amount} hotell",
|
||||||
"Hotels": "Hotell",
|
"Hotels": "Hotell",
|
||||||
"How do you want to sleep?": "Hur vill du sova?",
|
"How do you want to sleep?": "Hur vill du sova?",
|
||||||
"How it works": "Hur det fungerar",
|
"How it works": "Hur det fungerar",
|
||||||
@@ -334,6 +335,7 @@
|
|||||||
"Show wellness & exercise": "Visa välbefinnande och träning",
|
"Show wellness & exercise": "Visa välbefinnande och träning",
|
||||||
"Sign up bonus": "Välkomstbonus",
|
"Sign up bonus": "Välkomstbonus",
|
||||||
"Sign up to Scandic Friends": "Bli medlem i Scandic Friends",
|
"Sign up to Scandic Friends": "Bli medlem i Scandic Friends",
|
||||||
|
"Signing up...": "Registrerar...",
|
||||||
"Skip to main content": "Fortsätt till huvudinnehåll",
|
"Skip to main content": "Fortsätt till huvudinnehåll",
|
||||||
"Something went wrong and we couldn't add your card. Please try again later.": "Något gick fel och vi kunde inte lägga till ditt kort. Försök igen senare.",
|
"Something went wrong and we couldn't add your card. Please try again later.": "Något gick fel och vi kunde inte lägga till ditt kort. Försök igen senare.",
|
||||||
"Something went wrong and we couldn't remove your card. Please try again later.": "Något gick fel och vi kunde inte ta bort ditt kort. Försök igen senare.",
|
"Something went wrong and we couldn't remove your card. Please try again later.": "Något gick fel och vi kunde inte ta bort ditt kort. Försök igen senare.",
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
import { z } from "zod"
|
import { z } from "zod"
|
||||||
|
|
||||||
|
import { Lang } from "@/constants/languages"
|
||||||
|
|
||||||
|
import { signUpSchema } from "@/components/Forms/Signup/schema"
|
||||||
|
|
||||||
// Query
|
// Query
|
||||||
export const staysInput = z
|
export const staysInput = z
|
||||||
.object({
|
.object({
|
||||||
@@ -35,3 +39,19 @@ export const saveCreditCardInput = z.object({
|
|||||||
transactionId: z.string(),
|
transactionId: z.string(),
|
||||||
merchantId: z.string().optional(),
|
merchantId: z.string().optional(),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
export const signupInput = signUpSchema
|
||||||
|
.extend({
|
||||||
|
language: z.nativeEnum(Lang),
|
||||||
|
})
|
||||||
|
.omit({ termsAccepted: true })
|
||||||
|
.transform((data) => ({
|
||||||
|
...data,
|
||||||
|
phoneNumber: data.phoneNumber.replace(/\s+/g, ""),
|
||||||
|
address: {
|
||||||
|
...data.address,
|
||||||
|
city: "",
|
||||||
|
country: "",
|
||||||
|
streetAddress: "",
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|||||||
@@ -1,17 +1,20 @@
|
|||||||
import { metrics } from "@opentelemetry/api"
|
import { metrics } from "@opentelemetry/api"
|
||||||
|
|
||||||
|
import { signupVerify } from "@/constants/routes/signup"
|
||||||
import { env } from "@/env/server"
|
import { env } from "@/env/server"
|
||||||
import * as api from "@/lib/api"
|
import * as api from "@/lib/api"
|
||||||
|
import { serverErrorByStatus } from "@/server/errors/trpc"
|
||||||
import {
|
import {
|
||||||
initiateSaveCardSchema,
|
initiateSaveCardSchema,
|
||||||
subscriberIdSchema,
|
subscriberIdSchema,
|
||||||
} from "@/server/routers/user/output"
|
} from "@/server/routers/user/output"
|
||||||
import { protectedProcedure, router } from "@/server/trpc"
|
import { protectedProcedure, router, serviceProcedure } from "@/server/trpc"
|
||||||
|
|
||||||
import {
|
import {
|
||||||
addCreditCardInput,
|
addCreditCardInput,
|
||||||
deleteCreditCardInput,
|
deleteCreditCardInput,
|
||||||
saveCreditCardInput,
|
saveCreditCardInput,
|
||||||
|
signupInput,
|
||||||
} from "./input"
|
} from "./input"
|
||||||
|
|
||||||
const meter = metrics.getMeter("trpc.user")
|
const meter = metrics.getMeter("trpc.user")
|
||||||
@@ -24,6 +27,9 @@ const generatePreferencesLinkSuccessCounter = meter.createCounter(
|
|||||||
const generatePreferencesLinkFailCounter = meter.createCounter(
|
const generatePreferencesLinkFailCounter = meter.createCounter(
|
||||||
"trpc.user.generatePreferencesLink-fail"
|
"trpc.user.generatePreferencesLink-fail"
|
||||||
)
|
)
|
||||||
|
const signupCounter = meter.createCounter("trpc.user.signup")
|
||||||
|
const signupSuccessCounter = meter.createCounter("trpc.user.signup-success")
|
||||||
|
const signupFailCounter = meter.createCounter("trpc.user.signup-fail")
|
||||||
|
|
||||||
export const userMutationRouter = router({
|
export const userMutationRouter = router({
|
||||||
creditCard: router({
|
creditCard: router({
|
||||||
@@ -208,4 +214,46 @@ export const userMutationRouter = router({
|
|||||||
generatePreferencesLinkSuccessCounter.add(1)
|
generatePreferencesLinkSuccessCounter.add(1)
|
||||||
return preferencesLink.toString()
|
return preferencesLink.toString()
|
||||||
}),
|
}),
|
||||||
|
signup: serviceProcedure.input(signupInput).mutation(async function ({
|
||||||
|
ctx,
|
||||||
|
input,
|
||||||
|
}) {
|
||||||
|
signupCounter.add(1)
|
||||||
|
|
||||||
|
const apiResponse = await api.post(api.endpoints.v1.Profile.profile, {
|
||||||
|
body: input,
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${ctx.serviceToken}`,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!apiResponse.ok) {
|
||||||
|
const text = await apiResponse.text()
|
||||||
|
signupFailCounter.add(1, {
|
||||||
|
error_type: "http_error",
|
||||||
|
error: JSON.stringify({
|
||||||
|
status: apiResponse.status,
|
||||||
|
statusText: apiResponse.statusText,
|
||||||
|
error: text,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
console.error(
|
||||||
|
"api.user.signup api error",
|
||||||
|
JSON.stringify({
|
||||||
|
error: {
|
||||||
|
status: apiResponse.status,
|
||||||
|
statusText: apiResponse.statusText,
|
||||||
|
error: text,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
)
|
||||||
|
throw serverErrorByStatus(apiResponse.status, text)
|
||||||
|
}
|
||||||
|
signupSuccessCounter.add(1)
|
||||||
|
console.info("api.user.signup success")
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
redirectUrl: signupVerify[input.language],
|
||||||
|
}
|
||||||
|
}),
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { z } from "zod"
|
import { z } from "zod"
|
||||||
|
|
||||||
import { countriesMap } from "@/components/TempDesignSystem/Form/Country/countries"
|
import { countriesMap } from "@/components/TempDesignSystem/Form/Country/countries"
|
||||||
|
import { passwordValidator } from "@/utils/passwordValidator"
|
||||||
|
import { phoneValidator } from "@/utils/phoneValidator"
|
||||||
import { getMembership } from "@/utils/user"
|
import { getMembership } from "@/utils/user"
|
||||||
|
|
||||||
export const membershipSchema = z.object({
|
export const membershipSchema = z.object({
|
||||||
|
|||||||
@@ -121,7 +121,7 @@ export const safeProtectedProcedure = t.procedure.use(async function (opts) {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
export const serviceProcedure = t.procedure.use(async (opts) => {
|
export const serviceProcedure = t.procedure.use(async function (opts) {
|
||||||
const { access_token } = await getServiceToken()
|
const { access_token } = await getServiceToken()
|
||||||
if (!access_token) {
|
if (!access_token) {
|
||||||
throw internalServerError(`[serviceProcedure] No service token`)
|
throw internalServerError(`[serviceProcedure] No service token`)
|
||||||
|
|||||||
@@ -16,3 +16,7 @@ export type Filter = {
|
|||||||
sortOrder: number
|
sortOrder: number
|
||||||
filter?: string
|
filter?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type HotelFilterModalProps = {
|
||||||
|
filters: CategorizedFilters
|
||||||
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import {
|
|||||||
} from "@/server/routers/hotels/schemas/image"
|
} from "@/server/routers/hotels/schemas/image"
|
||||||
|
|
||||||
import { HotelData } from "./hotelCardListingProps"
|
import { HotelData } from "./hotelCardListingProps"
|
||||||
import { Filter } from "./hotelFilters"
|
import { CategorizedFilters, Filter } from "./hotelFilters"
|
||||||
|
|
||||||
import type { Coordinates } from "@/types/components/maps/coordinates"
|
import type { Coordinates } from "@/types/components/maps/coordinates"
|
||||||
|
|
||||||
@@ -21,6 +21,7 @@ export interface SelectHotelMapProps {
|
|||||||
hotelPins: HotelPin[]
|
hotelPins: HotelPin[]
|
||||||
mapId: string
|
mapId: string
|
||||||
hotels: HotelData[]
|
hotels: HotelData[]
|
||||||
|
filterList: CategorizedFilters
|
||||||
}
|
}
|
||||||
|
|
||||||
type ImageSizes = z.infer<typeof imageSizesSchema>
|
type ImageSizes = z.infer<typeof imageSizesSchema>
|
||||||
|
|||||||
Reference in New Issue
Block a user