diff --git a/actions/registerUser.ts b/actions/registerUser.ts deleted file mode 100644 index 8def69ec3..000000000 --- a/actions/registerUser.ts +++ /dev/null @@ -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]) - }) diff --git a/app/[lang]/(live)/(public)/[contentType]/[uid]/@preview/loading.tsx b/app/[lang]/(live)/(public)/[contentType]/[uid]/@preview/loading.tsx new file mode 100644 index 000000000..45c9c0019 --- /dev/null +++ b/app/[lang]/(live)/(public)/[contentType]/[uid]/@preview/loading.tsx @@ -0,0 +1,11 @@ +import { env } from "@/env/server" + +import CurrentLoadingSpinner from "@/components/Current/LoadingSpinner" +import LoadingSpinner from "@/components/LoadingSpinner" + +export default function Loading() { + if (env.HIDE_FOR_NEXT_RELEASE) { + return + } + return +} diff --git a/app/[lang]/(live)/(public)/[contentType]/[uid]/loading.tsx b/app/[lang]/(live)/(public)/[contentType]/[uid]/loading.tsx new file mode 100644 index 000000000..92ff5739e --- /dev/null +++ b/app/[lang]/(live)/(public)/[contentType]/[uid]/loading.tsx @@ -0,0 +1,5 @@ +import LoadingSpinner from "@/components/LoadingSpinner" + +export default function Loading() { + return +} diff --git a/app/[lang]/(live)/(public)/hotelreservation/(confirmation)/loading.tsx b/app/[lang]/(live)/(public)/hotelreservation/(confirmation)/loading.tsx new file mode 100644 index 000000000..92ff5739e --- /dev/null +++ b/app/[lang]/(live)/(public)/hotelreservation/(confirmation)/loading.tsx @@ -0,0 +1,5 @@ +import LoadingSpinner from "@/components/LoadingSpinner" + +export default function Loading() { + return +} diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/loading.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/loading.tsx new file mode 100644 index 000000000..92ff5739e --- /dev/null +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/loading.tsx @@ -0,0 +1,5 @@ +import LoadingSpinner from "@/components/LoadingSpinner" + +export default function Loading() { + return +} diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/@modal/(.)map/loading.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/@modal/(.)map/loading.tsx new file mode 100644 index 000000000..8f6f8657c --- /dev/null +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/@modal/(.)map/loading.tsx @@ -0,0 +1,5 @@ +import LoadingSpinner from "@/components/LoadingSpinner" + +export default function LoadingModal() { + return +} diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/@modal/(.)map/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/@modal/(.)map/page.tsx index fd9cc33c5..7c5573536 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/@modal/(.)map/page.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/@modal/(.)map/page.tsx @@ -12,7 +12,7 @@ import { import { MapModal } from "@/components/MapModal" 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 { LangParams, PageArgs } from "@/types/params" @@ -57,6 +57,7 @@ export default async function SelectHotelMapPage({ }) const hotelPins = getHotelPins(hotels) + const filterList = getFiltersFromHotels(hotels) return ( @@ -65,6 +66,7 @@ export default async function SelectHotelMapPage({ hotelPins={hotelPins} mapId={googleMapId} hotels={hotels} + filterList={filterList} /> ) diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/page.tsx index a12639acf..62cab6b85 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/page.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/page.tsx @@ -8,6 +8,7 @@ import { getFiltersFromHotels, } from "@/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/utils" import HotelCardListing from "@/components/HotelReservation/HotelCardListing" +import HotelCount from "@/components/HotelReservation/SelectHotel/HotelCount" import HotelFilter from "@/components/HotelReservation/SelectHotel/HotelFilter" import HotelSorter from "@/components/HotelReservation/SelectHotel/HotelSorter" import MobileMapButtonContainer from "@/components/HotelReservation/SelectHotel/MobileMapButtonContainer" @@ -20,7 +21,6 @@ import StaticMap from "@/components/Maps/StaticMap" import Alert from "@/components/TempDesignSystem/Alert" import Button from "@/components/TempDesignSystem/Button" import Link from "@/components/TempDesignSystem/Link" -import Preamble from "@/components/TempDesignSystem/Text/Preamble" import Subtitle from "@/components/TempDesignSystem/Text/Subtitle" import { getIntl } from "@/i18n" import { setLang } from "@/i18n/serverContext" @@ -66,13 +66,15 @@ export default async function SelectHotelPage({ const filterList = getFiltersFromHotels(hotels) + const isAllUnavailable = hotels.every((hotel) => hotel.price === undefined) + return ( <>
{city.name} - {hotels.length} hotels +
@@ -123,7 +125,7 @@ export default async function SelectHotelPage({
- {!hotels.length && ( + {isAllUnavailable && ( { @@ -52,6 +61,7 @@ export function getFiltersFromHotels(hotels: HotelData[]): CategorizedFilters { const filterList: Filter[] = uniqueFilterIds .map((filterId) => filters.find((filter) => filter.id === filterId)) .filter((filter): filter is Filter => filter !== undefined) + .sort((a, b) => b.sortOrder - a.sortOrder) return filterList.reduce( (acc, filter) => { @@ -61,10 +71,13 @@ export function getFiltersFromHotels(hotels: HotelData[]): CategorizedFilters { surroundingsFilters: [...acc.surroundingsFilters, filter], } - return { - facilityFilters: [...acc.facilityFilters, filter], - surroundingsFilters: acc.surroundingsFilters, - } + if (filter.filter && hotelFacilitiesFilterNames.includes(filter.filter)) + return { + facilityFilters: [...acc.facilityFilters, filter], + surroundingsFilters: acc.surroundingsFilters, + } + + return acc }, { facilityFilters: [], surroundingsFilters: [] } ) diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/step/@summary/loading.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/step/@summary/loading.tsx new file mode 100644 index 000000000..78b79a040 --- /dev/null +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/step/@summary/loading.tsx @@ -0,0 +1,5 @@ +import LoadingSpinner from "@/components/LoadingSpinner" + +export default function LoadingSummaryHeader() { + return +} diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/step/@summary/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/step/@summary/page.tsx index 337d501b3..0444913f1 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/step/@summary/page.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/step/@summary/page.tsx @@ -83,10 +83,10 @@ export default async function SummaryPage({ price: availability.publicRate.localPrice.pricePerStay, currency: availability.publicRate.localPrice.currency, }, - euro: availability.publicRate.requestedPrice + euro: availability.publicRate?.requestedPrice ? { - price: availability.publicRate.requestedPrice.pricePerStay, - currency: availability.publicRate.requestedPrice.currency, + price: availability.publicRate?.requestedPrice.pricePerStay, + currency: availability.publicRate?.requestedPrice.currency, } : undefined, } diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/step/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/step/page.tsx index 648cdff93..d175fc25f 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/step/page.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/step/page.tsx @@ -1,6 +1,6 @@ import "./enterDetailsLayout.css" -import { notFound, redirect, RedirectType } from "next/navigation" +import { notFound } from "next/navigation" import { getBreakfastPackages, @@ -38,6 +38,8 @@ export default async function StepPage({ }: PageArgs) { const intl = await getIntl() const selectRoomParams = new URLSearchParams(searchParams) + selectRoomParams.delete("step") + const searchParamsString = selectRoomParams.toString() const { hotel: hotelId, rooms, @@ -111,11 +113,19 @@ export default async function StepPage({ publicPrice: roomAvailability.publicRate!.localPrice.pricePerStay, } + const memberPrice = roomAvailability.memberRate + ? { + price: roomAvailability.memberRate.localPrice.pricePerStay, + currency: roomAvailability.memberRate.localPrice.currency, + } + : undefined + return (
@@ -152,7 +162,7 @@ export default async function StepPage({ step={StepEnum.details} label={intl.formatMessage({ id: "Enter your details" })} > -
+
) { } setLang(params.lang) - preload() + void preload() return ( diff --git a/components/BookingWidget/Client.tsx b/components/BookingWidget/Client.tsx index cb5e308b5..922522742 100644 --- a/components/BookingWidget/Client.tsx +++ b/components/BookingWidget/Client.tsx @@ -177,7 +177,7 @@ export default function BookingWidgetClient({ > -
+
diff --git a/components/BookingWidget/index.tsx b/components/BookingWidget/index.tsx index c7674b9d7..ecff629c6 100644 --- a/components/BookingWidget/index.tsx +++ b/components/BookingWidget/index.tsx @@ -1,4 +1,4 @@ -import { getLocations } from "@/lib/trpc/memoizedRequests" +import { getLocations, getSiteConfig } from "@/lib/trpc/memoizedRequests" import BookingWidgetClient from "./Client" @@ -13,8 +13,9 @@ export default async function BookingWidget({ searchParams, }: BookingWidgetProps) { const locations = await getLocations() + const siteConfig = await getSiteConfig() - if (!locations || "error" in locations) { + if (!locations || "error" in locations || siteConfig?.bookingWidgetDisabled) { return null } diff --git a/components/Forms/BookingWidget/FormContent/Search/index.tsx b/components/Forms/BookingWidget/FormContent/Search/index.tsx index abbb28112..0d1ee36d4 100644 --- a/components/Forms/BookingWidget/FormContent/Search/index.tsx +++ b/components/Forms/BookingWidget/FormContent/Search/index.tsx @@ -206,11 +206,12 @@ export default function Search({ locations }: SearchProps) { } export function SearchSkeleton() { + const intl = useIntl() return (
- Where to + {intl.formatMessage({ id: "Where to" })}
diff --git a/components/Forms/BookingWidget/index.tsx b/components/Forms/BookingWidget/index.tsx index b47ae74aa..3f7e5aa82 100644 --- a/components/Forms/BookingWidget/index.tsx +++ b/components/Forms/BookingWidget/index.tsx @@ -20,7 +20,7 @@ const formId = "booking-widget" export default function Form({ locations, type, - setIsOpen, + onClose, }: BookingWidgetFormProps) { const router = useRouter() const lang = useLang() @@ -56,7 +56,7 @@ export default function Form({ ) }) }) - setIsOpen(false) + onClose() router.push(`${bookingFlowPage}?${bookingWidgetParams.toString()}`) } diff --git a/components/Forms/Edit/Profile/FormContent/formContent.module.css b/components/Forms/Edit/Profile/FormContent/formContent.module.css index 64eb85410..aec012aa6 100644 --- a/components/Forms/Edit/Profile/FormContent/formContent.module.css +++ b/components/Forms/Edit/Profile/FormContent/formContent.module.css @@ -3,12 +3,14 @@ align-self: flex-start; display: grid; gap: var(--Spacing-x2); + container-name: addressContainer; + container-type: inline-size; } .container { display: grid; gap: var(--Spacing-x2); - grid-template-columns: max(164px) 1fr; + grid-template-columns: minmax(100px, 164px) 1fr; } @media (min-width: 768px) { @@ -16,3 +18,9 @@ display: none; } } + +@container addressContainer (max-width: 350px) { + .container { + grid-template-columns: 1fr; + } +} diff --git a/components/Forms/Signup/index.tsx b/components/Forms/Signup/index.tsx index ad66f6282..24fe2bd95 100644 --- a/components/Forms/Signup/index.tsx +++ b/components/Forms/Signup/index.tsx @@ -1,12 +1,13 @@ "use client" import { zodResolver } from "@hookform/resolvers/zod" +import { useRouter } from "next/navigation" import { FormProvider, useForm } from "react-hook-form" import { useIntl } from "react-intl" import { privacyPolicy } from "@/constants/currentWebHrefs" +import { trpc } from "@/lib/trpc/client" -import { registerUser } from "@/actions/registerUser" import Button from "@/components/TempDesignSystem/Button" import Checkbox from "@/components/TempDesignSystem/Form/Checkbox" 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) { const intl = useIntl() + const router = useRouter() const lang = useLang() const country = intl.formatMessage({ id: "Country" }) const email = intl.formatMessage({ id: "Email address" }) const phoneNumber = intl.formatMessage({ id: "Phone number" }) const zipCode = intl.formatMessage({ id: "Zip code" }) + const 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({ defaultValues: { @@ -48,7 +66,6 @@ export default function SignupForm({ link, subtitle, title }: SignUpFormProps) { zipCode: "", }, password: "", - termsAccepted: false, }, mode: "all", criteriaMode: "all", @@ -57,19 +74,7 @@ export default function SignupForm({ link, subtitle, title }: SignUpFormProps) { }) async function onSubmit(data: SignUpSchema) { - try { - 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!" })) - } + signup.mutate({ ...data, language: lang }) } return ( @@ -80,11 +85,6 @@ export default function SignupForm({ link, subtitle, title }: SignUpFormProps) { className={styles.form} id="register" onSubmit={methods.handleSubmit(onSubmit)} - /** - * Ignoring since ts doesn't recognize that tRPC - * parses FormData before reaching the route - * @ts-ignore */ - action={registerUser} >
@@ -187,7 +187,7 @@ export default function SignupForm({ link, subtitle, title }: SignUpFormProps) { onClick={() => methods.trigger()} data-testid="trigger-validation" > - {intl.formatMessage({ id: "Sign up to Scandic Friends" })} + {signupButtonText} ) : ( )} diff --git a/components/GuestsRoomsPicker/ChildSelector/ChildInfoSelector.tsx b/components/GuestsRoomsPicker/ChildSelector/ChildInfoSelector.tsx index b5a34c168..b39821483 100644 --- a/components/GuestsRoomsPicker/ChildSelector/ChildInfoSelector.tsx +++ b/components/GuestsRoomsPicker/ChildSelector/ChildInfoSelector.tsx @@ -33,11 +33,10 @@ export default function ChildInfoSelector({ const ageLabel = intl.formatMessage({ id: "Age" }) const bedLabel = intl.formatMessage({ id: "Bed" }) const errorMessage = intl.formatMessage({ id: "Child age is required" }) - const { setValue, formState, register, trigger } = useFormContext() + const { setValue, formState, register } = useFormContext() function updateSelectedBed(bed: number) { setValue(`rooms.${roomIndex}.child.${index}.bed`, bed) - trigger() } function updateSelectedAge(age: number) { @@ -95,7 +94,7 @@ export default function ChildInfoSelector({ updateSelectedAge(key as number) }} placeholder={ageLabel} - maxHeight={150} + maxHeight={180} {...register(ageFieldName, { required: true, })} diff --git a/components/GuestsRoomsPicker/Form.tsx b/components/GuestsRoomsPicker/Form.tsx index 01ded876a..062347761 100644 --- a/components/GuestsRoomsPicker/Form.tsx +++ b/components/GuestsRoomsPicker/Form.tsx @@ -99,7 +99,7 @@ export default function GuestsRoomsPickerDialog({ {rooms.length < 4 ? ( @@ -124,7 +124,7 @@ export default function GuestsRoomsPickerDialog({ {rooms.length < 4 ? ( diff --git a/components/HotelReservation/EnterDetails/Details/JoinScandicFriendsCard/index.tsx b/components/HotelReservation/EnterDetails/Details/JoinScandicFriendsCard/index.tsx new file mode 100644 index 000000000..e6d0e500b --- /dev/null +++ b/components/HotelReservation/EnterDetails/Details/JoinScandicFriendsCard/index.tsx @@ -0,0 +1,108 @@ +"use client" + +import { useIntl } from "react-intl" + +import { privacyPolicy } from "@/constants/currentWebHrefs" + +import { CheckIcon } from "@/components/Icons" +import LoginButton from "@/components/LoginButton" +import Checkbox from "@/components/TempDesignSystem/Form/Checkbox" +import Link from "@/components/TempDesignSystem/Link" +import Caption from "@/components/TempDesignSystem/Text/Caption" +import Footnote from "@/components/TempDesignSystem/Text/Footnote" +import useLang from "@/hooks/useLang" + +import styles from "./joinScandicFriendsCard.module.css" + +import { JoinScandicFriendsCardProps } from "@/types/components/hotelReservation/enterDetails/details" + +export default function JoinScandicFriendsCard({ + name, + memberPrice, +}: JoinScandicFriendsCardProps) { + const lang = useLang() + 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 saveOnJoiningLabel = intl.formatMessage( + { + id: "Only pay {amount} {currency}", + }, + { + amount: intl.formatNumber(memberPrice?.price ?? 0), + currency: memberPrice?.currency ?? "SEK", + } + ) + + return ( +
+ +
+ {memberPrice ? ( + + {saveOnJoiningLabel} + + ) : null} + + {intl.formatMessage({ id: "Join Scandic Friends" })} + +
+
+ + + {intl.formatMessage({ id: "Already a friend?" })}{" "} + + {intl.formatMessage({ id: "Log in" })} + + + +
+ {list.map((item) => ( + + {item.title} + + ))} +
+ + {intl.formatMessage( + { + id: "signup.terms", + }, + { + termsLink: (str) => ( + + {str} + + ), + } + )} + +
+ ) +} diff --git a/components/HotelReservation/EnterDetails/Details/JoinScandicFriendsCard/joinScandicFriendsCard.module.css b/components/HotelReservation/EnterDetails/Details/JoinScandicFriendsCard/joinScandicFriendsCard.module.css new file mode 100644 index 000000000..507633fe4 --- /dev/null +++ b/components/HotelReservation/EnterDetails/Details/JoinScandicFriendsCard/joinScandicFriendsCard.module.css @@ -0,0 +1,55 @@ +.cardContainer { + align-self: flex-start; + background-color: var(--Base-Surface-Primary-light-Normal); + border: 1px solid var(--Base-Border-Subtle); + border-radius: var(--Corner-radius-Large); + display: grid; + gap: var(--Spacing-x-one-and-half); + padding: var(--Spacing-x-one-and-half) var(--Spacing-x2); + grid-template-areas: + "checkbox" + "list" + "login" + "terms"; + width: min(100%, 600px); +} + +.login { + grid-area: login; +} + +.checkBox { + align-self: center; + grid-area: checkbox; +} + +.list { + display: grid; + grid-area: list; + gap: var(--Spacing-x1); +} + +.listItem { + display: flex; +} + +.terms { + border-top: 1px solid var(--Base-Border-Normal); + grid-area: terms; + padding-top: var(--Spacing-x1); +} + +@media screen and (min-width: 768px) { + .cardContainer { + grid-template-columns: 1fr auto; + gap: var(--Spacing-x2); + grid-template-areas: + "checkbox login" + "list list" + "terms terms"; + } + .list { + display: flex; + gap: var(--Spacing-x1); + } +} diff --git a/components/HotelReservation/EnterDetails/Details/Signup/index.tsx b/components/HotelReservation/EnterDetails/Details/Signup/index.tsx index 74a17e911..85559aa4c 100644 --- a/components/HotelReservation/EnterDetails/Details/Signup/index.tsx +++ b/components/HotelReservation/EnterDetails/Details/Signup/index.tsx @@ -4,14 +4,8 @@ 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" @@ -31,67 +25,27 @@ export default function Signup({ name }: { name: string }) { 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 ( -
- + - {isJoinChecked ? ( -
-
-
- - {intl.formatMessage({ id: "Birth date" })} * - -
- - -
-
- - - {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", - })}{" "} - - {intl.formatMessage({ id: "Scandic's Privacy Policy." })} - - - -
-
- ) : null} +
+
+ + {intl.formatMessage({ id: "Birth date" })} * + +
+ +
+ ) : ( + ) } diff --git a/components/HotelReservation/EnterDetails/Details/details.module.css b/components/HotelReservation/EnterDetails/Details/details.module.css index f89dfa7cc..8781a9be2 100644 --- a/components/HotelReservation/EnterDetails/Details/details.module.css +++ b/components/HotelReservation/EnterDetails/Details/details.module.css @@ -1,25 +1,34 @@ .form { display: grid; - gap: var(--Spacing-x2); + gap: var(--Spacing-x3); } .container { display: grid; gap: var(--Spacing-x2); - grid-template-columns: 1fr 1fr; width: min(100%, 600px); } +.header, .country, .email, -.membershipNo, +.signup, .phone { grid-column: 1/-1; } .footer { - display: grid; - gap: var(--Spacing-x3); - justify-items: flex-start; margin-top: var(--Spacing-x1); } + +@media screen and (min-width: 768px) { + .form { + gap: var(--Spacing-x3); + } + + .container { + gap: var(--Spacing-x2); + grid-template-columns: 1fr 1fr; + width: min(100%, 600px); + } +} diff --git a/components/HotelReservation/EnterDetails/Details/index.tsx b/components/HotelReservation/EnterDetails/Details/index.tsx index dd5959c31..819ad5243 100644 --- a/components/HotelReservation/EnterDetails/Details/index.tsx +++ b/components/HotelReservation/EnterDetails/Details/index.tsx @@ -13,6 +13,7 @@ import Input from "@/components/TempDesignSystem/Form/Input" import Phone from "@/components/TempDesignSystem/Form/Phone" import Footnote from "@/components/TempDesignSystem/Text/Footnote" +import JoinScandicFriendsCard from "./JoinScandicFriendsCard" import { guestDetailsSchema, signedInDetailsSchema } from "./schema" import Signup from "./Signup" @@ -24,7 +25,7 @@ import type { } from "@/types/components/hotelReservation/enterDetails/details" const formID = "enter-details" -export default function Details({ user }: DetailsProps) { +export default function Details({ user, memberPrice }: DetailsProps) { const intl = useIntl() const initialData = useDetailsStore((state) => ({ countryCode: state.data.countryCode, @@ -35,7 +36,6 @@ export default function Details({ user }: DetailsProps) { join: state.data.join, dateOfBirth: state.data.dateOfBirth, zipCode: state.data.zipCode, - termsAccepted: state.data.termsAccepted, membershipNo: state.data.membershipNo, })) @@ -52,7 +52,6 @@ export default function Details({ user }: DetailsProps) { join: initialData.join, dateOfBirth: initialData.dateOfBirth, zipCode: initialData.zipCode, - termsAccepted: initialData.termsAccepted, membershipNo: initialData.membershipNo, }, criteriaMode: "all", @@ -76,15 +75,18 @@ export default function Details({ user }: DetailsProps) { id={formID} onSubmit={methods.handleSubmit(onSubmit)} > - {user ? null : } - - {intl.formatMessage({ id: "Guest information" })} - + {user ? null : ( + + )}
+ + {intl.formatMessage({ id: "Guest information" })} + - {user || methods.watch("join") ? null : ( - + {user ? null : ( +
+ +
)}
diff --git a/components/HotelReservation/EnterDetails/Details/schema.ts b/components/HotelReservation/EnterDetails/Details/schema.ts index 2b8075da9..abb29ac2b 100644 --- a/components/HotelReservation/EnterDetails/Details/schema.ts +++ b/components/HotelReservation/EnterDetails/Details/schema.ts @@ -15,7 +15,6 @@ export const notJoinDetailsSchema = baseDetailsSchema.merge( join: z.literal(false), zipCode: z.string().optional(), dateOfBirth: z.string().optional(), - termsAccepted: z.boolean().default(false), membershipNo: z .string() .optional() @@ -39,15 +38,6 @@ export const joinDetailsSchema = baseDetailsSchema.merge( join: z.literal(true), zipCode: z.string().min(1, { message: "Zip code is required" }), dateOfBirth: z.string().min(1, { message: "Date of birth is required" }), - 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 } - }, - }), membershipNo: z.string().optional(), }) ) diff --git a/components/HotelReservation/EnterDetails/SectionAccordion/index.tsx b/components/HotelReservation/EnterDetails/SectionAccordion/index.tsx index ce548ae74..488d941b5 100644 --- a/components/HotelReservation/EnterDetails/SectionAccordion/index.tsx +++ b/components/HotelReservation/EnterDetails/SectionAccordion/index.tsx @@ -66,7 +66,7 @@ export default function SectionAccordion({ const textColor = isComplete || isOpen ? "uiTextHighContrast" : "baseTextDisabled" return ( -
+
{isComplete ? ( @@ -74,31 +74,33 @@ export default function SectionAccordion({ ) : null}
-
-
- -
-
-
{children}
-
+ {isComplete && !isOpen && ( + + )} + +
+
+
{children}
- + ) } diff --git a/components/HotelReservation/EnterDetails/SectionAccordion/sectionAccordion.module.css b/components/HotelReservation/EnterDetails/SectionAccordion/sectionAccordion.module.css index ed91cb9e2..125905317 100644 --- a/components/HotelReservation/EnterDetails/SectionAccordion/sectionAccordion.module.css +++ b/components/HotelReservation/EnterDetails/SectionAccordion/sectionAccordion.module.css @@ -1,15 +1,28 @@ -.wrapper { - position: relative; - display: flex; - flex-direction: row; - gap: var(--Spacing-x-one-and-half); +.accordion { + --header-height: 2.4em; + --circle-height: 24px; + + gap: var(--Spacing-x3); + width: 100%; padding-top: var(--Spacing-x3); + transition: 0.4s ease-out; + + display: grid; + grid-template-areas: "circle header" "content content"; + grid-template-columns: auto 1fr; + grid-template-rows: var(--header-height) 0fr; + + column-gap: var(--Spacing-x-one-and-half); } -.wrapper:last-child .main { +.accordion:last-child { border-bottom: none; } +.header { + grid-area: header; +} + .modifyButton { display: grid; grid-template-areas: "title button" "selection button"; @@ -17,6 +30,11 @@ background-color: transparent; border: none; width: 100%; + padding: 0; +} + +.modifyButton:disabled { + cursor: default; } .title { @@ -29,15 +47,6 @@ justify-self: flex-end; } -.main { - display: grid; - 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; -} - .selection { font-weight: 450; font-size: var(--typography-Title-4-fontSize); @@ -46,11 +55,12 @@ .iconWrapper { position: relative; + grid-area: circle; } .circle { - width: 24px; - height: 24px; + width: var(--circle-height); + height: var(--circle-height); border-radius: 100px; transition: background-color 0.4s; border: 2px solid var(--Base-Border-Inverted); @@ -63,42 +73,45 @@ background-color: var(--UI-Input-Controls-Fill-Selected); } -.wrapper[data-open="true"] .circle[data-checked="false"] { +.accordion[data-open="true"] .circle[data-checked="false"] { background-color: var(--UI-Text-Placeholder); } -.wrapper[data-open="false"] .circle[data-checked="false"] { +.accordion[data-open="false"] .circle[data-checked="false"] { background-color: var(--Base-Surface-Subtle-Hover); } -.wrapper[data-open="true"] .main { - grid-template-rows: 2em 1fr; +.accordion[data-open="true"] { + grid-template-rows: var(--header-height) 1fr; +} + +.contentWrapper { + padding-bottom: var(--Spacing-x3); } .content { overflow: hidden; + grid-area: content; + border-bottom: 1px solid var(--Primary-Light-On-Surface-Divider-subtle); } -.contentWrapper { - padding-top: var(--Spacing-x3); -} - -@media screen and (min-width: 1367px) { - .wrapper { - gap: var(--Spacing-x3); +@media screen and (min-width: 768px) { + .accordion { + column-gap: var(--Spacing-x3); + grid-template-areas: "circle header" "circle content"; } .iconWrapper { top: var(--Spacing-x1); } - .wrapper:not(:last-child)::after { + .accordion:not(:last-child) .iconWrapper::after { position: absolute; left: 12px; - bottom: 0; - top: var(--Spacing-x7); - height: 100%; + bottom: calc(0px - var(--Spacing-x7)); + top: var(--circle-height); + content: ""; border-left: 1px solid var(--Primary-Light-On-Surface-Divider-subtle); } -} \ No newline at end of file +} diff --git a/components/HotelReservation/EnterDetails/SelectedRoom/selectedRoom.module.css b/components/HotelReservation/EnterDetails/SelectedRoom/selectedRoom.module.css index 3149cf709..e979c1ee2 100644 --- a/components/HotelReservation/EnterDetails/SelectedRoom/selectedRoom.module.css +++ b/components/HotelReservation/EnterDetails/SelectedRoom/selectedRoom.module.css @@ -63,7 +63,7 @@ justify-content: flex-start; } -@media screen and (min-width: 1367px) { +@media screen and (min-width: 768px) { .wrapper { gap: var(--Spacing-x3); padding-top: var(--Spacing-x3); diff --git a/components/HotelReservation/EnterDetails/Summary/index.tsx b/components/HotelReservation/EnterDetails/Summary/index.tsx index 7507a6945..5b0a6a420 100644 --- a/components/HotelReservation/EnterDetails/Summary/index.tsx +++ b/components/HotelReservation/EnterDetails/Summary/index.tsx @@ -81,7 +81,6 @@ export default function Summary({ showMemberPrice, room }: SummaryProps) { useEffect(() => { setChosenBed(bedType) - setChosenBreakfast(breakfast) if (breakfast || breakfast === false) { setChosenBreakfast(breakfast) @@ -94,9 +93,9 @@ export default function Summary({ showMemberPrice, room }: SummaryProps) { euro: room.euroPrice && roomsPriceEuro ? { - price: roomsPriceEuro, - currency: room.euroPrice.currency, - } + price: roomsPriceEuro, + currency: room.euroPrice.currency, + } : undefined, }) } else { @@ -108,11 +107,11 @@ export default function Summary({ showMemberPrice, room }: SummaryProps) { euro: room.euroPrice && roomsPriceEuro ? { - price: - roomsPriceEuro + - parseInt(breakfast.requestedPrice.totalPrice), - currency: room.euroPrice.currency, - } + price: + roomsPriceEuro + + parseInt(breakfast.requestedPrice.totalPrice), + currency: room.euroPrice.currency, + } : undefined, }) } @@ -199,24 +198,24 @@ export default function Summary({ showMemberPrice, room }: SummaryProps) { {room.packages ? room.packages.map((roomPackage) => ( -
-
- - {roomPackage.description} - -
+
+
+ + {roomPackage.description} + +
- - {intl.formatMessage( - { id: "{amount} {currency}" }, - { - amount: roomPackage.localPrice.price, - currency: roomPackage.localPrice.currency, - } - )} - -
- )) + + {intl.formatMessage( + { id: "{amount} {currency}" }, + { + amount: roomPackage.localPrice.price, + currency: roomPackage.localPrice.currency, + } + )} + +
+ )) : null} {chosenBed ? (
@@ -263,9 +262,8 @@ export default function Summary({ showMemberPrice, room }: SummaryProps) { )}
- ) : null - } - + ) : null} +
@@ -306,6 +304,6 @@ export default function Summary({ showMemberPrice, room }: SummaryProps) {
- + ) } diff --git a/components/HotelReservation/HotelCard/HotelPriceList/hotelPriceList.module.css b/components/HotelReservation/HotelCard/HotelPriceList/hotelPriceList.module.css index bd81f1170..fb67d45d9 100644 --- a/components/HotelReservation/HotelCard/HotelPriceList/hotelPriceList.module.css +++ b/components/HotelReservation/HotelCard/HotelPriceList/hotelPriceList.module.css @@ -15,7 +15,6 @@ display: flex; flex-direction: column; gap: var(--Spacing-x-one-and-half); - max-width: 260px; } .divider { @@ -38,3 +37,9 @@ font-weight: 400; font-size: var(--typography-Caption-Regular-fontSize); } + +@media screen and (min-width: 1367px) { + .prices { + max-width: 260px; + } +} diff --git a/components/HotelReservation/HotelCard/index.tsx b/components/HotelReservation/HotelCard/index.tsx index 808e9ac9f..1836d4130 100644 --- a/components/HotelReservation/HotelCard/index.tsx +++ b/components/HotelReservation/HotelCard/index.tsx @@ -7,7 +7,6 @@ import { selectHotelMap, selectRate } from "@/constants/routes/hotelReservation" import { mapFacilityToIcon } from "@/components/ContentType/HotelPage/data" import ImageGallery from "@/components/ImageGallery" -import Alert from "@/components/TempDesignSystem/Alert" import Button from "@/components/TempDesignSystem/Button" import Divider from "@/components/TempDesignSystem/Divider" import Link from "@/components/TempDesignSystem/Link" @@ -133,13 +132,6 @@ export default function HotelCard({ hotel={hotelData} showCTA={true} /> - {hotelData.specialAlerts.length > 0 && ( -
- {hotelData.specialAlerts.map((alert) => ( - - ))} -
- )} diff --git a/components/HotelReservation/HotelCardListing/index.tsx b/components/HotelReservation/HotelCardListing/index.tsx index ab2d45d3e..6ceb9aa85 100644 --- a/components/HotelReservation/HotelCardListing/index.tsx +++ b/components/HotelReservation/HotelCardListing/index.tsx @@ -1,9 +1,11 @@ "use client" import { useSearchParams } from "next/navigation" -import { useMemo } from "react" +import { useEffect, useMemo, useState } from "react" import { useHotelFilterStore } from "@/stores/hotel-filters" +import { BackToTopButton } from "@/components/TempDesignSystem/BackToTopButton" + import HotelCard from "../HotelCard" import { DEFAULT_SORT } from "../SelectHotel/HotelSorter" @@ -25,6 +27,7 @@ export default function HotelCardListing({ const searchParams = useSearchParams() const activeFilters = useHotelFilterStore((state) => state.activeFilters) const setResultCount = useHotelFilterStore((state) => state.setResultCount) + const [showBackToTop, setShowBackToTop] = useState(false) const sortBy = useMemo( () => searchParams.get("sort") ?? DEFAULT_SORT, @@ -48,7 +51,7 @@ export default function HotelCardListing({ return ( hotel.price?.member?.localPrice?.pricePerNight ?? hotel.price?.public?.localPrice?.pricePerNight ?? - 0 + Infinity ) } return [...hotelData].sort( @@ -82,6 +85,20 @@ export default function HotelCardListing({ return filteredHotels }, [activeFilters, sortedHotels, setResultCount]) + useEffect(() => { + const handleScroll = () => { + const hasScrolledPast = window.scrollY > 490 + setShowBackToTop(hasScrolledPast) + } + + window.addEventListener("scroll", handleScroll, { passive: true }) + return () => window.removeEventListener("scroll", handleScroll) + }, []) + + function scrollToTop() { + window.scrollTo({ top: 0, behavior: "smooth" }) + } + return (
{hotels?.length @@ -95,6 +112,7 @@ export default function HotelCardListing({ /> )) : null} + {showBackToTop && }
) } diff --git a/components/HotelReservation/SelectHotel/FilterAndSortModal/filterAndSortModal.module.css b/components/HotelReservation/SelectHotel/FilterAndSortModal/filterAndSortModal.module.css index 6768d2c56..eab6a1c1d 100644 --- a/components/HotelReservation/SelectHotel/FilterAndSortModal/filterAndSortModal.module.css +++ b/components/HotelReservation/SelectHotel/FilterAndSortModal/filterAndSortModal.module.css @@ -81,6 +81,10 @@ flex: 0 0 auto; } +.title { + display: none; +} + .close { background: none; border: none; @@ -97,3 +101,80 @@ flex: 0 0 auto; 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; + } +} diff --git a/components/HotelReservation/SelectHotel/FilterAndSortModal/index.tsx b/components/HotelReservation/SelectHotel/FilterAndSortModal/index.tsx index be1a1bc9c..286042ca0 100644 --- a/components/HotelReservation/SelectHotel/FilterAndSortModal/index.tsx +++ b/components/HotelReservation/SelectHotel/FilterAndSortModal/index.tsx @@ -12,6 +12,8 @@ import { useHotelFilterStore } from "@/stores/hotel-filters" import { CloseLargeIcon, FilterIcon } from "@/components/Icons" import Button from "@/components/TempDesignSystem/Button" +import Divider from "@/components/TempDesignSystem/Divider" +import Subtitle from "@/components/TempDesignSystem/Text/Subtitle" import HotelFilter from "../HotelFilter" import HotelSorter from "../HotelSorter" @@ -47,10 +49,18 @@ export default function FilterAndSortModal({ > + + {intl.formatMessage({ id: "Filter and sort" })} +
+
diff --git a/components/HotelReservation/SelectHotel/HotelCount/index.tsx b/components/HotelReservation/SelectHotel/HotelCount/index.tsx new file mode 100644 index 000000000..1556dcf30 --- /dev/null +++ b/components/HotelReservation/SelectHotel/HotelCount/index.tsx @@ -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 ( + + {intl.formatMessage( + { + id: "Hotel(s)", + }, + { amount: resultCount } + )} + + ) +} diff --git a/components/HotelReservation/SelectHotel/SelectHotelMap/index.tsx b/components/HotelReservation/SelectHotel/SelectHotelMap/index.tsx index 215c7ae66..4fe186988 100644 --- a/components/HotelReservation/SelectHotel/SelectHotelMap/index.tsx +++ b/components/HotelReservation/SelectHotel/SelectHotelMap/index.tsx @@ -7,11 +7,13 @@ import { useMediaQuery } from "usehooks-ts" import { selectHotel } from "@/constants/routes/hotelReservation" -import { CloseIcon, CloseLargeIcon } from "@/components/Icons" +import { ArrowUpIcon, CloseIcon, CloseLargeIcon } from "@/components/Icons" import InteractiveMap from "@/components/Maps/InteractiveMap" +import { BackToTopButton } from "@/components/TempDesignSystem/BackToTopButton" import Button from "@/components/TempDesignSystem/Button" import useLang from "@/hooks/useLang" +import FilterAndSortModal from "../FilterAndSortModal" import HotelListing from "./HotelListing" import { getCentralCoordinates } from "./utils" @@ -24,6 +26,7 @@ export default function SelectHotelMap({ hotelPins, mapId, hotels, + filterList, }: SelectHotelMapProps) { const searchParams = useSearchParams() const router = useRouter() @@ -101,25 +104,14 @@ export default function SelectHotelMap({ > - Filter and sort - {/* TODO: Add filter and sort button */} + - {showBackToTop && ( - - )} + {showBackToTop && } def.rateCode === rate?.rateCode) - ?.generalTerms + } + + const getBreakfastMessage = (rate: RateDefinition | undefined) => { + const breakfastIncluded = getRateDefinitionForRate(rate)?.breakfastIncluded + switch (breakfastIncluded) { + case true: + return intl.formatMessage({ id: "Breakfast is included." }) + case false: + return intl.formatMessage({ id: "Breakfast selection in next step." }) + default: + return intl.formatMessage({ + id: "Breakfast deal can be purchased at the hotel.", + }) + } } const petRoomPackage = @@ -69,7 +82,6 @@ export default function RoomCard({ ) const { roomSize, occupancy, images } = selectedRoom || {} - const mainImage = images?.[0] const freeCancelation = intl.formatMessage({ id: "Free cancellation" }) const nonRefundable = intl.formatMessage({ id: "Non-refundable" }) @@ -101,53 +113,56 @@ export default function RoomCard({ return (
- {mainImage && ( -
-
- {roomConfiguration.roomsLeft < 5 && ( - - {`${roomConfiguration.roomsLeft} ${intl.formatMessage({ id: "Left" })}`} - - )} - {roomConfiguration.features - .filter((feature) => selectedPackages.includes(feature.code)) - .map((feature) => ( - - {createElement(getIconForFeatureCode(feature.code), { - width: 16, - height: 16, - color: "burgundy", - })} - - ))} -
- {/*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. */} - -
- )} -
- - {intl.formatMessage( - { - id: "booking.guests", - }, - { nrOfGuests: occupancy?.total } +
+
+ {roomConfiguration.roomsLeft < 5 && ( + + {`${roomConfiguration.roomsLeft} ${intl.formatMessage({ id: "Left" })}`} + )} - - - {roomSize?.min === roomSize?.max - ? roomSize?.min - : `${roomSize?.min}-${roomSize?.max}`} - m² - + {roomConfiguration.features + .filter((feature) => selectedPackages.includes(feature.code)) + .map((feature) => ( + + {createElement(getIconForFeatureCode(feature.code), { + width: 16, + height: 16, + color: "burgundy", + })} + + ))} +
+ {/*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. */} + +
+ +
+ {occupancy && ( + + {intl.formatMessage( + { + id: "booking.guests", + }, + { nrOfGuests: occupancy?.total } + )} + + )} + {roomSize && ( + + {roomSize.min === roomSize.max + ? roomSize.min + : `${roomSize.min}-${roomSize.max}`} + m² + + )}
{roomConfiguration.roomTypeCode && (
- {intl.formatMessage({ - id: "Breakfast selection in next step.", - })} + {getBreakfastMessage(rates.flexRate)} {roomConfiguration.status === "NotAvailable" ? (
@@ -192,7 +205,7 @@ export default function RoomCard({ value={key.toLowerCase()} paymentTerm={key === "flexRate" ? payLater : payNow} product={findProductForRate(rate)} - priceInformation={getPriceInformationForRate(rate)} + priceInformation={getRateDefinitionForRate(rate)?.generalTerms} handleSelectRate={handleSelectRate} roomType={roomConfiguration.roomType} roomTypeCode={roomConfiguration.roomTypeCode} diff --git a/components/HotelReservation/SelectRate/RoomSelection/RoomCard/roomCard.module.css b/components/HotelReservation/SelectRate/RoomSelection/RoomCard/roomCard.module.css index 8ec1978e6..b05bbdb8b 100644 --- a/components/HotelReservation/SelectRate/RoomSelection/RoomCard/roomCard.module.css +++ b/components/HotelReservation/SelectRate/RoomSelection/RoomCard/roomCard.module.css @@ -7,23 +7,13 @@ border: 1px solid var(--Base-Border-Subtle); position: relative; height: 100%; + min-height: 730px; justify-content: space-between; } .card.noAvailability { justify-content: flex-start; -} - -.card.noAvailability:before { - background-color: rgba(0, 0, 0, 40%); - content: ""; - display: block; - position: absolute; - top: 0; - bottom: 0; - left: 0; - right: 0; - z-index: 2; + opacity: 0.6; } .specification { diff --git a/components/HotelReservation/SelectRate/Rooms/utils.ts b/components/HotelReservation/SelectRate/Rooms/utils.ts index af80e1f4d..fbd353aa1 100644 --- a/components/HotelReservation/SelectRate/Rooms/utils.ts +++ b/components/HotelReservation/SelectRate/Rooms/utils.ts @@ -31,7 +31,10 @@ export function filterDuplicateRoomTypesByLowestPrice( products.forEach((product) => { const { productType } = product - const publicProduct = productType.public + const publicProduct = productType.public || { + requestedPrice: null, + localPrice: null, + } const memberProduct = productType.member || { requestedPrice: null, localPrice: null, @@ -53,7 +56,7 @@ export function filterDuplicateRoomTypesByLowestPrice( Number(memberRequestedPrice?.pricePerNight) ?? Infinity ) const currentLocalPrice = Math.min( - Number(publicLocalPrice.pricePerNight) ?? Infinity, + Number(publicLocalPrice?.pricePerNight) ?? Infinity, Number(memberLocalPrice?.pricePerNight) ?? Infinity ) @@ -63,7 +66,7 @@ export function filterDuplicateRoomTypesByLowestPrice( Math.min( Number( previousLowest.products[0].productType.public.requestedPrice - .pricePerNight + ?.pricePerNight ) ?? Infinity, Number( previousLowest.products[0].productType.member?.requestedPrice @@ -74,7 +77,7 @@ export function filterDuplicateRoomTypesByLowestPrice( Math.min( Number( previousLowest.products[0].productType.public.requestedPrice - .pricePerNight + ?.pricePerNight ) ?? Infinity, Number( previousLowest.products[0].productType.member?.requestedPrice @@ -85,7 +88,7 @@ export function filterDuplicateRoomTypesByLowestPrice( Math.min( Number( previousLowest.products[0].productType.public.localPrice - .pricePerNight + ?.pricePerNight ) ?? Infinity, Number( previousLowest.products[0].productType.member?.localPrice diff --git a/components/Icons/ArrowUp.tsx b/components/Icons/ArrowUp.tsx new file mode 100644 index 000000000..d85323685 --- /dev/null +++ b/components/Icons/ArrowUp.tsx @@ -0,0 +1,33 @@ +import { iconVariants } from "./variants" + +import type { IconProps } from "@/types/components/icon" + +export default function ArrowUpIcon({ className, color, ...props }: IconProps) { + const classNames = iconVariants({ className, color }) + return ( + + + + + + + ) +} diff --git a/components/Icons/index.tsx b/components/Icons/index.tsx index 1bc195717..7ec502ca0 100644 --- a/components/Icons/index.tsx +++ b/components/Icons/index.tsx @@ -6,6 +6,7 @@ export { default as AirIcon } from "./Air" export { default as AirplaneIcon } from "./Airplane" export { default as AllergyIcon } from "./Allergy" export { default as ArrowRightIcon } from "./ArrowRight" +export { default as ArrowUpIcon } from "./ArrowUp" export { default as BarIcon } from "./Bar" export { default as BathtubIcon } from "./Bathtub" export { default as BedDoubleIcon } from "./BedDouble" diff --git a/components/LoginButton/index.tsx b/components/LoginButton/index.tsx index f6b163b38..89cc3a42a 100644 --- a/components/LoginButton/index.tsx +++ b/components/LoginButton/index.tsx @@ -13,19 +13,16 @@ import { trackLoginClick } from "@/utils/tracking" import { TrackingPosition } from "@/types/components/tracking" export default function LoginButton({ - className, position, trackingId, children, - color = "black", - variant = "navigation", -}: PropsWithChildren<{ - className: string - trackingId: string - position: TrackingPosition - color?: LinkProps["color"] - variant?: "navigation" | "signupVerification" -}>) { + ...props +}: PropsWithChildren< + { + trackingId: string + position: TrackingPosition + } & Omit +>) { const lang = useLang() const pathName = useLazyPathname({ includeSearchParams: true }) @@ -34,25 +31,19 @@ export default function LoginButton({ : login[lang] useEffect(() => { - document - .getElementById(trackingId) - ?.addEventListener("click", () => trackLoginClick(position)) + function trackLogin() { + trackLoginClick(position) + } + document.getElementById(trackingId)?.addEventListener("click", trackLogin) return () => { document .getElementById(trackingId) - ?.removeEventListener("click", () => trackLoginClick(position)) + ?.removeEventListener("click", trackLogin) } }, [position, trackingId]) return ( - + {children} ) diff --git a/components/MapModal/index.tsx b/components/MapModal/index.tsx index 2c06a08cc..3f9e3b4b6 100644 --- a/components/MapModal/index.tsx +++ b/components/MapModal/index.tsx @@ -66,7 +66,7 @@ export function MapModal({ children }: { children: React.ReactNode }) { return (
- + svg * { + fill: var(--Base-Button-Tertiary-On-Fill-Hover); + } + .backToTopButton { + padding: calc(var(--Spacing-x1) + 2px) var(--Spacing-x2); + } +} diff --git a/components/TempDesignSystem/BackToTopButton/index.tsx b/components/TempDesignSystem/BackToTopButton/index.tsx new file mode 100644 index 000000000..a50f8329f --- /dev/null +++ b/components/TempDesignSystem/BackToTopButton/index.tsx @@ -0,0 +1,20 @@ +"use client" + +import { Button as ButtonRAC } from "react-aria-components" +import { useIntl } from "react-intl" + +import { ArrowUpIcon } from "@/components/Icons" + +import styles from "./backToTopButton.module.css" + +export function BackToTopButton({ onClick }: { onClick: () => void }) { + const intl = useIntl() + return ( + + + + {intl.formatMessage({ id: "Back to top" })} + + + ) +} diff --git a/components/TempDesignSystem/Form/Checkbox/checkbox.module.css b/components/TempDesignSystem/Form/Checkbox/checkbox.module.css index 2e924b226..11c558af9 100644 --- a/components/TempDesignSystem/Form/Checkbox/checkbox.module.css +++ b/components/TempDesignSystem/Form/Checkbox/checkbox.module.css @@ -2,6 +2,7 @@ display: flex; flex-direction: column; color: var(--text-color); + cursor: pointer; } .container[data-selected] .checkbox { diff --git a/components/TempDesignSystem/Form/Checkbox/index.tsx b/components/TempDesignSystem/Form/Checkbox/index.tsx index 60bd81935..bd4639e39 100644 --- a/components/TempDesignSystem/Form/Checkbox/index.tsx +++ b/components/TempDesignSystem/Form/Checkbox/index.tsx @@ -12,6 +12,7 @@ import styles from "./checkbox.module.css" import { CheckboxProps } from "@/types/components/checkbox" export default function Checkbox({ + className, name, children, registerOptions, @@ -25,16 +26,17 @@ export default function Checkbox({ return ( {({ isSelected }) => ( <> - + {isSelected && } {children} diff --git a/components/TempDesignSystem/Form/ChoiceCard/Checkbox.tsx b/components/TempDesignSystem/Form/ChoiceCard/Checkbox.tsx deleted file mode 100644 index ca32951cb..000000000 --- a/components/TempDesignSystem/Form/ChoiceCard/Checkbox.tsx +++ /dev/null @@ -1,7 +0,0 @@ -import Card from "./_Card" - -import type { CheckboxProps } from "./_Card/card" - -export default function CheckboxCard(props: CheckboxProps) { - return -} diff --git a/components/TempDesignSystem/Form/ChoiceCard/_Card/card.module.css b/components/TempDesignSystem/Form/ChoiceCard/_Card/card.module.css index fa7d6d13a..d50df8a15 100644 --- a/components/TempDesignSystem/Form/ChoiceCard/_Card/card.module.css +++ b/components/TempDesignSystem/Form/ChoiceCard/_Card/card.module.css @@ -1,5 +1,4 @@ .label { - align-self: flex-start; background-color: var(--Base-Surface-Primary-light-Normal); border: 1px solid var(--Base-Border-Subtle); border-radius: var(--Corner-radius-Large); diff --git a/components/TempDesignSystem/Form/Date/date.module.css b/components/TempDesignSystem/Form/Date/date.module.css index fde0b7e03..7f492a293 100644 --- a/components/TempDesignSystem/Form/Date/date.module.css +++ b/components/TempDesignSystem/Form/Date/date.module.css @@ -1,4 +1,8 @@ /* Leaving, will most likely get deleted */ +.datePicker { + container-name: datePickerContainer; + container-type: inline-size; +} .container { display: grid; gap: var(--Spacing-x2); @@ -27,3 +31,10 @@ .year.invalid > div > div { border-color: var(--Scandic-Red-60); } + +@container datePickerContainer (max-width: 350px) { + .container { + display: flex; + flex-direction: column; + } +} diff --git a/components/TempDesignSystem/Form/Date/index.tsx b/components/TempDesignSystem/Form/Date/index.tsx index 4e8f4a7af..fa4b27528 100644 --- a/components/TempDesignSystem/Form/Date/index.tsx +++ b/components/TempDesignSystem/Form/Date/index.tsx @@ -115,6 +115,7 @@ export default function DateSelect({ name, registerOptions = {} }: DateProps) { ref={field.ref} value={dateValue} data-testid={name} + className={styles.datePicker} > diff --git a/components/TempDesignSystem/Form/Phone/index.tsx b/components/TempDesignSystem/Form/Phone/index.tsx index 9c413fc0a..5ff8f3482 100644 --- a/components/TempDesignSystem/Form/Phone/index.tsx +++ b/components/TempDesignSystem/Form/Phone/index.tsx @@ -78,67 +78,69 @@ export default function Phone({ } return ( -
- ( - - )} - /> - - + + )} /> - - + + + + +
) } diff --git a/components/TempDesignSystem/Form/Phone/phone.module.css b/components/TempDesignSystem/Form/Phone/phone.module.css index f2b75136b..31de5be30 100644 --- a/components/TempDesignSystem/Form/Phone/phone.module.css +++ b/components/TempDesignSystem/Form/Phone/phone.module.css @@ -1,3 +1,7 @@ +.wrapper { + container-name: phoneContainer; + container-type: inline-size; +} .phone { display: grid; gap: var(--Spacing-x2); @@ -100,3 +104,10 @@ justify-self: flex-start; padding: 0; } + +@container phoneContainer (max-width: 350px) { + .phone { + display: flex; + flex-direction: column; + } +} diff --git a/components/TempDesignSystem/Link/link.module.css b/components/TempDesignSystem/Link/link.module.css index 138c8094e..3a997bddb 100644 --- a/components/TempDesignSystem/Link/link.module.css +++ b/components/TempDesignSystem/Link/link.module.css @@ -16,7 +16,7 @@ .breadcrumb { font-family: var(--typography-Footnote-Bold-fontFamily); font-size: var(--typography-Footnote-Bold-fontSize); - font-weight: var(--typography-Footnote-Bold-fontWeight); + font-weight: 500; /* var(--typography-Footnote-Bold-fontWeight); */ letter-spacing: var(--typography-Footnote-Bold-letterSpacing); line-height: var(--typography-Footnote-Bold-lineHeight); } @@ -24,7 +24,7 @@ .link.breadcrumb { font-family: var(--typography-Footnote-Bold-fontFamily); font-size: var(--typography-Footnote-Bold-fontSize); - font-weight: var(--typography-Footnote-Bold-fontWeight); + font-weight: 500; /* var(--typography-Footnote-Bold-fontWeight); */ letter-spacing: var(--typography-Footnote-Bold-letterSpacing); line-height: var(--typography-Footnote-Bold-lineHeight); } @@ -128,6 +128,15 @@ color: #000; } +.uiTextPlaceholder { + color: var(--Base-Text-Placeholder); +} + +.uiTextPlaceholder:hover, +.uiTextPlaceholder:active { + color: var(--Base-Text-Medium-contrast); +} + .burgundy { color: var(--Base-Text-High-contrast); } @@ -211,6 +220,14 @@ line-height: var(--typography-Caption-Regular-lineHeight); } +.tiny { + font-family: var(--typography-Footnote-Regular-fontFamily); + font-size: var(--typography-Footnote-Regular-fontSize); + font-weight: var(--typography-Footnote-Regular-fontWeight); + letter-spacing: var(--typography-Footnote-Regular-letterSpacing); + line-height: var(--typography-Footnote-Regular-lineHeight); +} + .activeSmall { font-family: var(--typography-Caption-Bold-fontFamily); font-size: var(--typography-Caption-Bold-fontSize); diff --git a/components/TempDesignSystem/Link/variants.ts b/components/TempDesignSystem/Link/variants.ts index 61c7f8493..0a0c1dd6d 100644 --- a/components/TempDesignSystem/Link/variants.ts +++ b/components/TempDesignSystem/Link/variants.ts @@ -17,10 +17,12 @@ export const linkVariants = cva(styles.link, { peach80: styles.peach80, white: styles.white, red: styles.red, + uiTextPlaceholder: styles.uiTextPlaceholder, }, size: { small: styles.small, regular: styles.regular, + tiny: styles.tiny, }, textDecoration: { none: styles.noDecoration, diff --git a/components/TempDesignSystem/Tooltip/index.tsx b/components/TempDesignSystem/Tooltip/index.tsx index 033fc9a04..d73252161 100644 --- a/components/TempDesignSystem/Tooltip/index.tsx +++ b/components/TempDesignSystem/Tooltip/index.tsx @@ -28,6 +28,7 @@ export function Tooltip

({ role="tooltip" aria-label={text} onClick={handleToggle} + onTouchStart={handleToggle} data-active={isActive} >

diff --git a/components/TempDesignSystem/Tooltip/tooltip.module.css b/components/TempDesignSystem/Tooltip/tooltip.module.css index e25433f7c..b0ae8cf4f 100644 --- a/components/TempDesignSystem/Tooltip/tooltip.module.css +++ b/components/TempDesignSystem/Tooltip/tooltip.module.css @@ -16,6 +16,7 @@ transition: opacity 0.3s; max-width: 200px; min-width: 150px; + height: fit-content; } .tooltipContainer:hover .tooltip { diff --git a/constants/routes/hotelReservation.js b/constants/routes/hotelReservation.js index c7882f159..ed7ae99c5 100644 --- a/constants/routes/hotelReservation.js +++ b/constants/routes/hotelReservation.js @@ -48,7 +48,7 @@ export function selectHotel(lang) { * @param {Lang} lang */ export function selectHotelMap(lang) { - return `${base(lang)}/map` + return `${base(lang)}/select-hotel/map` } /** diff --git a/i18n/dictionaries/da.json b/i18n/dictionaries/da.json index 336f5eef9..a9fdcfae4 100644 --- a/i18n/dictionaries/da.json +++ b/i18n/dictionaries/da.json @@ -47,8 +47,10 @@ "Booking number": "Bookingnummer", "Breakfast": "Morgenmad", "Breakfast buffet": "Morgenbuffet", + "Breakfast deal can be purchased at the hotel.": "Morgenmad kan købes på hotellet.", "Breakfast excluded": "Morgenmad ikke inkluderet", "Breakfast included": "Morgenmad inkluderet", + "Breakfast is included.": "Morgenmad er inkluderet.", "Breakfast restaurant": "Breakfast restaurant", "Breakfast selection in next step.": "Valg af morgenmad i næste trin.", "Bus terminal": "Busstation", @@ -152,6 +154,7 @@ "Hotel": "Hotel", "Hotel facilities": "Hotel faciliteter", "Hotel surroundings": "Hotel omgivelser", + "Hotel(s)": "{amount} {amount, plural, one {hotel} other {hoteller}}", "Hotels": "Hoteller", "How do you want to sleep?": "Hvordan vil du sove?", "How it works": "Hvordan det virker", @@ -236,6 +239,7 @@ "Number of parking spots": "Antal parkeringspladser", "OTHER PAYMENT METHODS": "ANDRE BETALINGSMETODER", "On your journey": "På din rejse", + "Only pay {amount} {currency}": "Betal kun {amount} {currency}", "Open": "Åben", "Open gift(s)": "Åbne {amount, plural, one {gave} other {gaver}}", "Open image gallery": "Åbn billedgalleri", @@ -333,6 +337,7 @@ "Show wellness & exercise": "Vis velvære og motion", "Sign up bonus": "Velkomstbonus", "Sign up to Scandic Friends": "Tilmeld dig Scandic Friends", + "Signing up...": "Tilmelder...", "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 remove your card. Please try again later.": "Noget gik galt, og vi kunne ikke fjerne dit kort. Prøv venligst igen senere.", @@ -456,6 +461,5 @@ "to": "til", "uppercase letter": "stort bogstav", "{amount} out of {total}": "{amount} ud af {total}", - "{amount} {currency}": "{amount} {currency}", - "{difference}{amount} {currency}": "{difference}{amount} {currency}" + "{amount} {currency}": "{amount} {currency}" } diff --git a/i18n/dictionaries/de.json b/i18n/dictionaries/de.json index 78d9fd5cb..96bcef2de 100644 --- a/i18n/dictionaries/de.json +++ b/i18n/dictionaries/de.json @@ -47,8 +47,10 @@ "Booking number": "Buchungsnummer", "Breakfast": "Frühstück", "Breakfast buffet": "Frühstücksbuffet", + "Breakfast deal can be purchased at the hotel.": "Frühstücksangebot kann im Hotel gekauft werden.", "Breakfast excluded": "Frühstück nicht inbegriffen", "Breakfast included": "Frühstück inbegriffen", + "Breakfast is included.": "Frühstück ist inbegriffen.", "Breakfast restaurant": "Breakfast restaurant", "Breakfast selection in next step.": "Frühstücksauswahl in nächsten Schritt.", "Bus terminal": "Busbahnhof", @@ -152,6 +154,7 @@ "Hotel": "Hotel", "Hotel facilities": "Hotel-Infos", "Hotel surroundings": "Umgebung des Hotels", + "Hotel(s)": "{amount} {amount, plural, one {hotel} other {hotels}}", "Hotels": "Hotels", "How do you want to sleep?": "Wie möchtest du schlafen?", "How it works": "Wie es funktioniert", @@ -234,6 +237,7 @@ "Number of parking spots": "Anzahl der Parkplätze", "OTHER PAYMENT METHODS": "ANDERE BEZAHLMETHODE", "On your journey": "Auf deiner Reise", + "Only pay {amount} {currency}": "Nur bezahlen {amount} {currency}", "Open": "Offen", "Open gift(s)": "{amount, plural, one {Geschenk} other {Geschenke}} öffnen", "Open image gallery": "Bildergalerie öffnen", @@ -332,6 +336,7 @@ "Show wellness & exercise": "Zeige Wellness und Bewegung", "Sign up bonus": "Anmelde-Bonus", "Sign up to Scandic Friends": "Treten Sie Scandic Friends bei", + "Signing up...": "Registrierung läuft...", "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 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.", @@ -454,6 +459,5 @@ "to": "zu", "uppercase letter": "großbuchstabe", "{amount} out of {total}": "{amount} von {total}", - "{amount} {currency}": "{amount} {currency}", - "{difference}{amount} {currency}": "{difference}{amount} {currency}" + "{amount} {currency}": "{amount} {currency}" } diff --git a/i18n/dictionaries/en.json b/i18n/dictionaries/en.json index cb8a071d9..385861316 100644 --- a/i18n/dictionaries/en.json +++ b/i18n/dictionaries/en.json @@ -51,8 +51,10 @@ "Booking number": "Booking number", "Breakfast": "Breakfast", "Breakfast buffet": "Breakfast buffet", + "Breakfast deal can be purchased at the hotel.": "Breakfast deal can be purchased at the hotel.", "Breakfast excluded": "Breakfast excluded", "Breakfast included": "Breakfast included", + "Breakfast is included.": "Breakfast is included.", "Breakfast restaurant": "Breakfast restaurant", "Breakfast selection in next step.": "Breakfast selection in next step.", "Bus terminal": "Bus terminal", @@ -164,6 +166,7 @@ "Hotel": "Hotel", "Hotel facilities": "Hotel facilities", "Hotel surroundings": "Hotel surroundings", + "Hotel(s)": "{amount} {amount, plural, one {hotel} other {hotels}}", "Hotels": "Hotels", "How do you want to sleep?": "How do you want to sleep?", "How it works": "How it works", @@ -253,6 +256,7 @@ "Number of parking spots": "Number of parking spots", "OTHER PAYMENT METHODS": "OTHER PAYMENT METHODS", "On your journey": "On your journey", + "Only pay {amount} {currency}": "Only pay {amount} {currency}", "Open": "Open", "Open gift(s)": "Open {amount, plural, one {gift} other {gifts}}", "Open image gallery": "Open image gallery", @@ -362,6 +366,7 @@ "Show wellness & exercise": "Show wellness & exercise", "Sign up bonus": "Sign up bonus", "Sign up to Scandic Friends": "Sign up to Scandic Friends", + "Signing up...": "Signing up...", "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 remove your card. Please try again later.": "Something went wrong and we couldn't remove your card. Please try again later.", @@ -425,7 +430,6 @@ "Which room class suits you the best?": "Which room class suits you the best?", "Year": "Year", "Yes": "Yes", - "Yes, I accept the Terms and conditions for Scandic Friends and understand that Scandic will process my personal data in accordance with": "Yes, I accept the Terms and conditions for Scandic Friends and understand that Scandic will process my personal data in accordance with", "Yes, discard changes": "Yes, discard changes", "Yes, remove my card": "Yes, remove my card", "You can always change your mind later and add breakfast at the hotel.": "You can always change your mind later and add breakfast at the hotel.", @@ -488,12 +492,12 @@ "points": "Points", "room type": "room type", "room types": "room types", + "signup.terms": "By signing up you accept the Scandic Friends Terms and Conditions. Your membership is valid until further notice, and you can terminate your membership at any time by sending an email to Scandic’s customer service", "special character": "special character", "spendable points expiring by": "{points} spendable points expiring by {date}", "to": "to", "uppercase letter": "uppercase letter", "{amount} out of {total}": "{amount} out of {total}", "{amount} {currency}": "{amount} {currency}", - "{card} ending with {cardno}": "{card} ending with {cardno}", - "{difference}{amount} {currency}": "{difference}{amount} {currency}" + "{card} ending with {cardno}": "{card} ending with {cardno}" } diff --git a/i18n/dictionaries/fi.json b/i18n/dictionaries/fi.json index 48931f7f2..09fb8a403 100644 --- a/i18n/dictionaries/fi.json +++ b/i18n/dictionaries/fi.json @@ -47,8 +47,10 @@ "Booking number": "Varausnumero", "Breakfast": "Aamiainen", "Breakfast buffet": "Aamiaisbuffet", + "Breakfast deal can be purchased at the hotel.": "Aamiaisdeali voidaan ostaa hotellissa.", "Breakfast excluded": "Aamiainen ei sisälly", "Breakfast included": "Aamiainen sisältyy", + "Breakfast is included.": "Aamiainen sisältyy.", "Breakfast restaurant": "Breakfast restaurant", "Breakfast selection in next step.": "Aamiaisvalinta seuraavassa vaiheessa.", "Bus terminal": "Bussiasema", @@ -152,6 +154,7 @@ "Hotel": "Hotelli", "Hotel facilities": "Hotellin palvelut", "Hotel surroundings": "Hotellin ympäristö", + "Hotel(s)": "{amount} {amount, plural, one {hotelli} other {hotellit}}", "Hotels": "Hotellit", "How do you want to sleep?": "Kuinka haluat nukkua?", "How it works": "Kuinka se toimii", @@ -236,6 +239,7 @@ "Number of parking spots": "Pysäköintipaikkojen määrä", "OTHER PAYMENT METHODS": "MUISE KORT", "On your journey": "Matkallasi", + "Only pay {amount} {currency}": "Vain maksaa {amount} {currency}", "Open": "Avata", "Open gift(s)": "{amount, plural, one {Avoin lahja} other {Avoimet lahjat}}", "Open image gallery": "Avaa kuvagalleria", @@ -334,6 +338,7 @@ "Show wellness & exercise": "Näytä hyvinvointi ja liikunta", "Sign up bonus": "Liittymisbonus", "Sign up to Scandic Friends": "Liity Scandic Friends -jäseneksi", + "Signing up...": "Rekisteröidytää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 remove your card. Please try again later.": "Jotain meni pieleen, emmekä voineet poistaa korttiasi. Yritä myöhemmin uudelleen.", @@ -454,6 +459,5 @@ "to": "to", "uppercase letter": "iso kirjain", "{amount} out of {total}": "{amount}/{total}", - "{amount} {currency}": "{amount} {currency}", - "{difference}{amount} {currency}": "{difference}{amount} {currency}" + "{amount} {currency}": "{amount} {currency}" } diff --git a/i18n/dictionaries/no.json b/i18n/dictionaries/no.json index 313d0799d..dcbcdddf4 100644 --- a/i18n/dictionaries/no.json +++ b/i18n/dictionaries/no.json @@ -47,8 +47,10 @@ "Booking number": "Bestillingsnummer", "Breakfast": "Frokost", "Breakfast buffet": "Breakfast buffet", + "Breakfast deal can be purchased at the hotel.": "Frokostdeal kan kjøpes på hotellet.", "Breakfast excluded": "Frokost ekskludert", "Breakfast included": "Frokost inkludert", + "Breakfast is included.": "Frokost er inkludert.", "Breakfast restaurant": "Breakfast restaurant", "Breakfast selection in next step.": "Frokostvalg i neste steg.", "Bus terminal": "Bussterminal", @@ -151,6 +153,7 @@ "Hotel": "Hotel", "Hotel facilities": "Hotelfaciliteter", "Hotel surroundings": "Hotellomgivelser", + "Hotel(s)": "{amount} {amount, plural, one {hotell} other {hoteller}}", "Hotels": "Hoteller", "How do you want to sleep?": "Hvordan vil du sove?", "How it works": "Hvordan det fungerer", @@ -234,6 +237,7 @@ "Number of parking spots": "Antall parkeringsplasser", "OTHER PAYMENT METHODS": "ANDRE BETALINGSMETODER", "On your journey": "På reisen din", + "Only pay {amount} {currency}": "Bare betal {amount} {currency}", "Open": "Åpen", "Open gift(s)": "{amount, plural, one {Åpen gave} other {Åpnen gaver}}", "Open image gallery": "Åpne bildegalleri", @@ -331,6 +335,7 @@ "Show wellness & exercise": "Vis velvære og trening", "Sign up bonus": "Velkomstbonus", "Sign up to Scandic Friends": "Bli med i Scandic Friends", + "Signing up...": "Registrerer...", "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 remove your card. Please try again later.": "Noe gikk galt, og vi kunne ikke fjerne kortet ditt. Vennligst prøv igjen senere.", @@ -452,6 +457,5 @@ "to": "til", "uppercase letter": "stor bokstav", "{amount} out of {total}": "{amount} av {total}", - "{amount} {currency}": "{amount} {currency}", - "{difference}{amount} {currency}": "{difference}{amount} {currency}" + "{amount} {currency}": "{amount} {currency}" } diff --git a/i18n/dictionaries/sv.json b/i18n/dictionaries/sv.json index 878bce352..404896e15 100644 --- a/i18n/dictionaries/sv.json +++ b/i18n/dictionaries/sv.json @@ -47,8 +47,10 @@ "Booking number": "Bokningsnummer", "Breakfast": "Frukost", "Breakfast buffet": "Frukostbuffé", + "Breakfast deal can be purchased at the hotel.": "Frukostdeal kan köpas på hotellet.", "Breakfast excluded": "Frukost ingår ej", "Breakfast included": "Frukost ingår", + "Breakfast is included.": "Frukost ingår.", "Breakfast restaurant": "Breakfast restaurant", "Breakfast selection in next step.": "Frukostval i nästa steg.", "Bus terminal": "Bussterminal", @@ -151,6 +153,7 @@ "Hotel": "Hotell", "Hotel facilities": "Hotellfaciliteter", "Hotel surroundings": "Hotellomgivning", + "Hotel(s)": "{amount} hotell", "Hotels": "Hotell", "How do you want to sleep?": "Hur vill du sova?", "How it works": "Hur det fungerar", @@ -234,6 +237,7 @@ "Number of parking spots": "Antal parkeringsplatser", "OTHER PAYMENT METHODS": "ANDRE BETALINGSMETODER", "On your journey": "På din resa", + "Only pay {amount} {currency}": "Betala endast {amount} {currency}", "Open": "Öppna", "Open gift(s)": "Öppna {amount, plural, one {gåva} other {gåvor}}", "Open image gallery": "Öppna bildgalleri", @@ -331,6 +335,7 @@ "Show wellness & exercise": "Visa välbefinnande och träning", "Sign up bonus": "Välkomstbonus", "Sign up to Scandic Friends": "Bli medlem i Scandic Friends", + "Signing up...": "Registrerar...", "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 remove your card. Please try again later.": "Något gick fel och vi kunde inte ta bort ditt kort. Försök igen senare.", @@ -455,6 +460,5 @@ "types": "typer", "uppercase letter": "stor bokstav", "{amount} out of {total}": "{amount} av {total}", - "{amount} {currency}": "{amount} {currency}", - "{difference}{amount} {currency}": "{difference}{amount} {currency}" + "{amount} {currency}": "{amount} {currency}" } diff --git a/next-env.d.ts b/next-env.d.ts index 4f11a03dc..40c3d6809 100644 --- a/next-env.d.ts +++ b/next-env.d.ts @@ -2,4 +2,4 @@ /// // NOTE: This file should not be edited -// see https://nextjs.org/docs/basic-features/typescript for more information. +// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information. diff --git a/package-lock.json b/package-lock.json index a2d63c0f7..e611379b4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -41,7 +41,7 @@ "graphql-tag": "^2.12.6", "immer": "10.1.1", "libphonenumber-js": "^1.10.60", - "next": "^14.2.7", + "next": "^14.2.18", "next-auth": "^5.0.0-beta.19", "react": "^18", "react-day-picker": "^9.0.8", @@ -3425,9 +3425,9 @@ } }, "node_modules/@next/env": { - "version": "14.2.7", - "resolved": "https://registry.npmjs.org/@next/env/-/env-14.2.7.tgz", - "integrity": "sha512-OTx9y6I3xE/eih+qtthppwLytmpJVPM5PPoJxChFsbjIEFXIayG0h/xLzefHGJviAa3Q5+Fd+9uYojKkHDKxoQ==", + "version": "14.2.18", + "resolved": "https://registry.npmjs.org/@next/env/-/env-14.2.18.tgz", + "integrity": "sha512-2vWLOUwIPgoqMJKG6dt35fVXVhgM09tw4tK3/Q34GFXDrfiHlG7iS33VA4ggnjWxjiz9KV5xzfsQzJX6vGAekA==", "license": "MIT" }, "node_modules/@next/eslint-plugin-next": { @@ -3440,9 +3440,9 @@ } }, "node_modules/@next/swc-darwin-arm64": { - "version": "14.2.7", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.2.7.tgz", - "integrity": "sha512-UhZGcOyI9LE/tZL3h9rs/2wMZaaJKwnpAyegUVDGZqwsla6hMfeSj9ssBWQS9yA4UXun3pPhrFLVnw5KXZs3vw==", + "version": "14.2.18", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.2.18.tgz", + "integrity": "sha512-tOBlDHCjGdyLf0ube/rDUs6VtwNOajaWV+5FV/ajPgrvHeisllEdymY/oDgv2cx561+gJksfMUtqf8crug7sbA==", "cpu": [ "arm64" ], @@ -3456,9 +3456,9 @@ } }, "node_modules/@next/swc-darwin-x64": { - "version": "14.2.7", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-14.2.7.tgz", - "integrity": "sha512-ys2cUgZYRc+CbyDeLAaAdZgS7N1Kpyy+wo0b/gAj+SeOeaj0Lw/q+G1hp+DuDiDAVyxLBCJXEY/AkhDmtihUTA==", + "version": "14.2.18", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-14.2.18.tgz", + "integrity": "sha512-uJCEjutt5VeJ30jjrHV1VIHCsbMYnEqytQgvREx+DjURd/fmKy15NaVK4aR/u98S1LGTnjq35lRTnRyygglxoA==", "cpu": [ "x64" ], @@ -3472,9 +3472,9 @@ } }, "node_modules/@next/swc-linux-arm64-gnu": { - "version": "14.2.7", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.2.7.tgz", - "integrity": "sha512-2xoWtE13sUJ3qrC1lwE/HjbDPm+kBQYFkkiVECJWctRASAHQ+NwjMzgrfqqMYHfMxFb5Wws3w9PqzZJqKFdWcQ==", + "version": "14.2.18", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.2.18.tgz", + "integrity": "sha512-IL6rU8vnBB+BAm6YSWZewc+qvdL1EaA+VhLQ6tlUc0xp+kkdxQrVqAnh8Zek1ccKHlTDFRyAft0e60gteYmQ4A==", "cpu": [ "arm64" ], @@ -3488,9 +3488,9 @@ } }, "node_modules/@next/swc-linux-arm64-musl": { - "version": "14.2.7", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.2.7.tgz", - "integrity": "sha512-+zJ1gJdl35BSAGpkCbfyiY6iRTaPrt3KTl4SF/B1NyELkqqnrNX6cp4IjjjxKpd64/7enI0kf6b9O1Uf3cL0pw==", + "version": "14.2.18", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.2.18.tgz", + "integrity": "sha512-RCaENbIZqKKqTlL8KNd+AZV/yAdCsovblOpYFp0OJ7ZxgLNbV5w23CUU1G5On+0fgafrsGcW+GdMKdFjaRwyYA==", "cpu": [ "arm64" ], @@ -3504,9 +3504,9 @@ } }, "node_modules/@next/swc-linux-x64-gnu": { - "version": "14.2.7", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.2.7.tgz", - "integrity": "sha512-m6EBqrskeMUzykBrv0fDX/28lWIBGhMzOYaStp0ihkjzIYJiKUOzVYD1gULHc8XDf5EMSqoH/0/TRAgXqpQwmw==", + "version": "14.2.18", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.2.18.tgz", + "integrity": "sha512-3kmv8DlyhPRCEBM1Vavn8NjyXtMeQ49ID0Olr/Sut7pgzaQTo4h01S7Z8YNE0VtbowyuAL26ibcz0ka6xCTH5g==", "cpu": [ "x64" ], @@ -3520,9 +3520,9 @@ } }, "node_modules/@next/swc-linux-x64-musl": { - "version": "14.2.7", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.2.7.tgz", - "integrity": "sha512-gUu0viOMvMlzFRz1r1eQ7Ql4OE+hPOmA7smfZAhn8vC4+0swMZaZxa9CSIozTYavi+bJNDZ3tgiSdMjmMzRJlQ==", + "version": "14.2.18", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.2.18.tgz", + "integrity": "sha512-mliTfa8seVSpTbVEcKEXGjC18+TDII8ykW4a36au97spm9XMPqQTpdGPNBJ9RySSFw9/hLuaCMByluQIAnkzlw==", "cpu": [ "x64" ], @@ -3536,9 +3536,9 @@ } }, "node_modules/@next/swc-win32-arm64-msvc": { - "version": "14.2.7", - "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.2.7.tgz", - "integrity": "sha512-PGbONHIVIuzWlYmLvuFKcj+8jXnLbx4WrlESYlVnEzDsa3+Q2hI1YHoXaSmbq0k4ZwZ7J6sWNV4UZfx1OeOlbQ==", + "version": "14.2.18", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.2.18.tgz", + "integrity": "sha512-J5g0UFPbAjKYmqS3Cy7l2fetFmWMY9Oao32eUsBPYohts26BdrMUyfCJnZFQkX9npYaHNDOWqZ6uV9hSDPw9NA==", "cpu": [ "arm64" ], @@ -3552,9 +3552,9 @@ } }, "node_modules/@next/swc-win32-ia32-msvc": { - "version": "14.2.7", - "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.7.tgz", - "integrity": "sha512-BiSY5umlx9ed5RQDoHcdbuKTUkuFORDqzYKPHlLeS+STUWQKWziVOn3Ic41LuTBvqE0TRJPKpio9GSIblNR+0w==", + "version": "14.2.18", + "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.18.tgz", + "integrity": "sha512-Ynxuk4ZgIpdcN7d16ivJdjsDG1+3hTvK24Pp8DiDmIa2+A4CfhJSEHHVndCHok6rnLUzAZD+/UOKESQgTsAZGg==", "cpu": [ "ia32" ], @@ -3568,9 +3568,9 @@ } }, "node_modules/@next/swc-win32-x64-msvc": { - "version": "14.2.7", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.2.7.tgz", - "integrity": "sha512-pxsI23gKWRt/SPHFkDEsP+w+Nd7gK37Hpv0ngc5HpWy2e7cKx9zR/+Q2ptAUqICNTecAaGWvmhway7pj/JLEWA==", + "version": "14.2.18", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.2.18.tgz", + "integrity": "sha512-dtRGMhiU9TN5nyhwzce+7c/4CCeykYS+ipY/4mIrGzJ71+7zNo55ZxCB7cAVuNqdwtYniFNR2c9OFQ6UdFIMcg==", "cpu": [ "x64" ], @@ -15460,12 +15460,12 @@ } }, "node_modules/next": { - "version": "14.2.7", - "resolved": "https://registry.npmjs.org/next/-/next-14.2.7.tgz", - "integrity": "sha512-4Qy2aK0LwH4eQiSvQWyKuC7JXE13bIopEQesWE0c/P3uuNRnZCQanI0vsrMLmUQJLAto+A+/8+sve2hd+BQuOQ==", + "version": "14.2.18", + "resolved": "https://registry.npmjs.org/next/-/next-14.2.18.tgz", + "integrity": "sha512-H9qbjDuGivUDEnK6wa+p2XKO+iMzgVgyr9Zp/4Iv29lKa+DYaxJGjOeEA+5VOvJh/M7HLiskehInSa0cWxVXUw==", "license": "MIT", "dependencies": { - "@next/env": "14.2.7", + "@next/env": "14.2.18", "@swc/helpers": "0.5.5", "busboy": "1.6.0", "caniuse-lite": "^1.0.30001579", @@ -15480,15 +15480,15 @@ "node": ">=18.17.0" }, "optionalDependencies": { - "@next/swc-darwin-arm64": "14.2.7", - "@next/swc-darwin-x64": "14.2.7", - "@next/swc-linux-arm64-gnu": "14.2.7", - "@next/swc-linux-arm64-musl": "14.2.7", - "@next/swc-linux-x64-gnu": "14.2.7", - "@next/swc-linux-x64-musl": "14.2.7", - "@next/swc-win32-arm64-msvc": "14.2.7", - "@next/swc-win32-ia32-msvc": "14.2.7", - "@next/swc-win32-x64-msvc": "14.2.7" + "@next/swc-darwin-arm64": "14.2.18", + "@next/swc-darwin-x64": "14.2.18", + "@next/swc-linux-arm64-gnu": "14.2.18", + "@next/swc-linux-arm64-musl": "14.2.18", + "@next/swc-linux-x64-gnu": "14.2.18", + "@next/swc-linux-x64-musl": "14.2.18", + "@next/swc-win32-arm64-msvc": "14.2.18", + "@next/swc-win32-ia32-msvc": "14.2.18", + "@next/swc-win32-x64-msvc": "14.2.18" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", diff --git a/package.json b/package.json index 4afcb9bf1..5871b48b4 100644 --- a/package.json +++ b/package.json @@ -56,7 +56,7 @@ "graphql-tag": "^2.12.6", "immer": "10.1.1", "libphonenumber-js": "^1.10.60", - "next": "^14.2.7", + "next": "^14.2.18", "next-auth": "^5.0.0-beta.19", "react": "^18", "react-day-picker": "^9.0.8", diff --git a/providers/StepsProvider.tsx b/providers/StepsProvider.tsx index 87594be02..9aaf6166f 100644 --- a/providers/StepsProvider.tsx +++ b/providers/StepsProvider.tsx @@ -1,4 +1,5 @@ "use client" +import { useRouter } from "next/navigation" import { useRef } from "react" import { useDetailsStore } from "@/stores/details" @@ -14,6 +15,7 @@ export default function StepsProvider({ breakfastPackages, children, isMember, + searchParams, step, }: StepsProviderProps) { const storeRef = useRef() @@ -21,6 +23,7 @@ export default function StepsProvider({ const updateBreakfast = useDetailsStore( (state) => state.actions.updateBreakfast ) + const router = useRouter() if (!storeRef.current) { const noBedChoices = bedTypes.length === 1 @@ -41,7 +44,9 @@ export default function StepsProvider({ step, isMember, noBedChoices, - noBreakfast + noBreakfast, + searchParams, + router.push ) } diff --git a/server/routers/hotels/output.ts b/server/routers/hotels/output.ts index d4f3d89c9..41f9a6b52 100644 --- a/server/routers/hotels/output.ts +++ b/server/routers/hotels/output.ts @@ -13,7 +13,6 @@ import { AlertTypeEnum } from "@/types/enums/alert" import { CurrencyEnum } from "@/types/enums/currency" import { FacilityEnum } from "@/types/enums/facilities" import { PackageTypeEnum } from "@/types/enums/packages" -import { PointOfInterestCategoryNameEnum } from "@/types/hotel" const ratingsSchema = z .object({ @@ -199,14 +198,12 @@ const rewardNightSchema = z.object({ }), }) -const poiCategoryNames = z.nativeEnum(PointOfInterestCategoryNameEnum) - export const pointOfInterestSchema = z .object({ name: z.string(), distance: z.number(), category: z.object({ - name: poiCategoryNames, + name: z.string(), group: z.string(), }), location: locationSchema, @@ -515,7 +512,16 @@ export const productTypePriceSchema = z.object({ const productSchema = z.object({ productType: z.object({ - public: productTypePriceSchema, + public: productTypePriceSchema.default({ + rateCode: "", + rateType: "", + localPrice: { + currency: "SEK", + pricePerNight: 0, + pricePerStay: 0, + }, + requestedPrice: undefined, + }), member: productTypePriceSchema.optional(), }), }) diff --git a/server/routers/hotels/query.ts b/server/routers/hotels/query.ts index 7f2ef84c0..34a1b3722 100644 --- a/server/routers/hotels/query.ts +++ b/server/routers/hotels/query.ts @@ -731,7 +731,7 @@ export const hotelQueryRouter = router({ const rateTypes = selectedRoom.products.find( (rate) => - rate.productType.public.rateCode === rateCode || + rate.productType.public?.rateCode === rateCode || rate.productType.member?.rateCode === rateCode ) diff --git a/server/routers/hotels/utils.ts b/server/routers/hotels/utils.ts index 6d384b3cf..c3e785a59 100644 --- a/server/routers/hotels/utils.ts +++ b/server/routers/hotels/utils.ts @@ -12,39 +12,34 @@ import { type Countries, } from "./output" -import type { RequestOptionsWithOutBody } from "@/types/fetch" -import { - PointOfInterestCategoryNameEnum, - PointOfInterestGroupEnum, -} from "@/types/hotel" -import { HotelLocation } from "@/types/trpc/routers/hotel/locations" import type { Lang } from "@/constants/languages" import type { Endpoint } from "@/lib/api/endpoints" +import type { RequestOptionsWithOutBody } from "@/types/fetch" +import { PointOfInterestGroupEnum } from "@/types/hotel" +import { HotelLocation } from "@/types/trpc/routers/hotel/locations" -export function getPoiGroupByCategoryName( - category: PointOfInterestCategoryNameEnum -) { +export function getPoiGroupByCategoryName(category: string) { switch (category) { - case PointOfInterestCategoryNameEnum.AIRPORT: - case PointOfInterestCategoryNameEnum.BUS_TERMINAL: - case PointOfInterestCategoryNameEnum.TRANSPORTATIONS: + case "Airport": + case "Bus terminal": + case "Transportations": return PointOfInterestGroupEnum.PUBLIC_TRANSPORT - case PointOfInterestCategoryNameEnum.AMUSEMENT_PARK: - case PointOfInterestCategoryNameEnum.MUSEUM: - case PointOfInterestCategoryNameEnum.SPORTS: - case PointOfInterestCategoryNameEnum.THEATRE: - case PointOfInterestCategoryNameEnum.TOURIST: - case PointOfInterestCategoryNameEnum.ZOO: + case "Amusement park": + case "Museum": + case "Sports": + case "Theatre": + case "Tourist": + case "Zoo": return PointOfInterestGroupEnum.ATTRACTIONS - case PointOfInterestCategoryNameEnum.NEARBY_COMPANIES: - case PointOfInterestCategoryNameEnum.FAIR: + case "Nearby companies": + case "Fair": return PointOfInterestGroupEnum.BUSINESS - case PointOfInterestCategoryNameEnum.PARKING_GARAGE: + case "Parking / Garage": return PointOfInterestGroupEnum.PARKING - case PointOfInterestCategoryNameEnum.SHOPPING: - case PointOfInterestCategoryNameEnum.RESTAURANT: + case "Shopping": + case "Restaurant": return PointOfInterestGroupEnum.SHOPPING_DINING - case PointOfInterestCategoryNameEnum.HOSPITAL: + case "Hospital": default: return PointOfInterestGroupEnum.LOCATION } diff --git a/server/routers/user/input.ts b/server/routers/user/input.ts index d84875ea3..1e9fee267 100644 --- a/server/routers/user/input.ts +++ b/server/routers/user/input.ts @@ -1,5 +1,9 @@ import { z } from "zod" +import { Lang } from "@/constants/languages" + +import { signUpSchema } from "@/components/Forms/Signup/schema" + // Query export const staysInput = z .object({ @@ -35,3 +39,19 @@ export const saveCreditCardInput = z.object({ transactionId: z.string(), 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: "", + }, + })) diff --git a/server/routers/user/mutation.ts b/server/routers/user/mutation.ts index b03e6a68e..689c43a26 100644 --- a/server/routers/user/mutation.ts +++ b/server/routers/user/mutation.ts @@ -1,17 +1,20 @@ import { metrics } from "@opentelemetry/api" +import { signupVerify } from "@/constants/routes/signup" import { env } from "@/env/server" import * as api from "@/lib/api" +import { serverErrorByStatus } from "@/server/errors/trpc" import { initiateSaveCardSchema, subscriberIdSchema, } from "@/server/routers/user/output" -import { protectedProcedure, router } from "@/server/trpc" +import { protectedProcedure, router, serviceProcedure } from "@/server/trpc" import { addCreditCardInput, deleteCreditCardInput, saveCreditCardInput, + signupInput, } from "./input" const meter = metrics.getMeter("trpc.user") @@ -24,6 +27,9 @@ const generatePreferencesLinkSuccessCounter = meter.createCounter( const generatePreferencesLinkFailCounter = meter.createCounter( "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({ creditCard: router({ @@ -208,4 +214,46 @@ export const userMutationRouter = router({ generatePreferencesLinkSuccessCounter.add(1) 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], + } + }), }) diff --git a/server/routers/user/output.ts b/server/routers/user/output.ts index a9e7a0416..616419047 100644 --- a/server/routers/user/output.ts +++ b/server/routers/user/output.ts @@ -1,6 +1,8 @@ import { z } from "zod" import { countriesMap } from "@/components/TempDesignSystem/Form/Country/countries" +import { passwordValidator } from "@/utils/passwordValidator" +import { phoneValidator } from "@/utils/phoneValidator" import { getMembership } from "@/utils/user" export const membershipSchema = z.object({ diff --git a/server/trpc.ts b/server/trpc.ts index 688ea01cf..3fc3a10a6 100644 --- a/server/trpc.ts +++ b/server/trpc.ts @@ -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() if (!access_token) { throw internalServerError(`[serviceProcedure] No service token`) diff --git a/stores/details.ts b/stores/details.ts index 5d23248e5..a382ad7d7 100644 --- a/stores/details.ts +++ b/stores/details.ts @@ -94,7 +94,6 @@ export function createDetailsStore( state.data.membershipNo = data.membershipNo } state.data.phoneNumber = data.phoneNumber - state.data.termsAccepted = data.termsAccepted state.data.zipCode = data.zipCode }) ) diff --git a/stores/steps.ts b/stores/steps.ts index f1e456af2..cf14f6768 100644 --- a/stores/steps.ts +++ b/stores/steps.ts @@ -1,6 +1,7 @@ "use client" import merge from "deepmerge" import { produce } from "immer" +import { AppRouterInstance } from "next/dist/shared/lib/app-router-context.shared-runtime" import { useContext } from "react" import { create, useStore } from "zustand" @@ -18,17 +19,13 @@ import { StepEnum } from "@/types/enums/step" import type { DetailsState } from "@/types/stores/details" import type { StepState } from "@/types/stores/steps" -function push(data: Record, url: string) { - if (typeof window !== "undefined") { - window.history.pushState(data, "", url + window.location.search) - } -} - export function createStepsStore( currentStep: StepEnum, isMember: boolean, noBedChoices: boolean, - noBreakfast: boolean + noBreakfast: boolean, + searchParams: string, + push: AppRouterInstance["push"] ) { const isBrowser = typeof window !== "undefined" const steps = [ @@ -51,14 +48,14 @@ export function createStepsStore( steps.splice(1, 1) if (currentStep === StepEnum.breakfast) { currentStep = steps[1] - push({ step: currentStep }, currentStep) + push(`${currentStep}?${searchParams}`) } } if (noBedChoices) { if (currentStep === StepEnum.selectBed) { currentStep = steps[1] - push({ step: currentStep }, currentStep) + push(`${currentStep}?${searchParams}`) } } @@ -94,7 +91,7 @@ export function createStepsStore( if (!validPaths.includes(currentStep) && isBrowser) { // We will always have at least one valid path currentStep = validPaths.pop()! - push({ step: currentStep }, currentStep) + push(`${currentStep}?${searchParams}`) } } diff --git a/types/components/form/bookingwidget.ts b/types/components/form/bookingwidget.ts index 887c4eae2..bded82de3 100644 --- a/types/components/form/bookingwidget.ts +++ b/types/components/form/bookingwidget.ts @@ -4,7 +4,7 @@ import type { Location, Locations } from "@/types/trpc/routers/hotel/locations" export interface BookingWidgetFormProps { locations: Locations type?: BookingWidgetType - setIsOpen: (isOpen: boolean) => void + onClose: () => void } export interface BookingWidgetFormContentProps { diff --git a/types/components/hotelReservation/enterDetails/details.ts b/types/components/hotelReservation/enterDetails/details.ts index 685fad18a..25004467a 100644 --- a/types/components/hotelReservation/enterDetails/details.ts +++ b/types/components/hotelReservation/enterDetails/details.ts @@ -6,6 +6,14 @@ import type { SafeUser } from "@/types/user" export type DetailsSchema = z.output +type MemberPrice = { price: number; currency: string } + export interface DetailsProps { user: SafeUser + memberPrice?: MemberPrice +} + +export type JoinScandicFriendsCardProps = { + name: string + memberPrice?: MemberPrice } diff --git a/types/components/hotelReservation/selectHotel/hotelFilters.ts b/types/components/hotelReservation/selectHotel/hotelFilters.ts index 200b960e5..fbfd926cf 100644 --- a/types/components/hotelReservation/selectHotel/hotelFilters.ts +++ b/types/components/hotelReservation/selectHotel/hotelFilters.ts @@ -16,3 +16,7 @@ export type Filter = { sortOrder: number filter?: string } + +export type HotelFilterModalProps = { + filters: CategorizedFilters +} diff --git a/types/components/hotelReservation/selectHotel/map.ts b/types/components/hotelReservation/selectHotel/map.ts index 810dba573..490f8d06e 100644 --- a/types/components/hotelReservation/selectHotel/map.ts +++ b/types/components/hotelReservation/selectHotel/map.ts @@ -6,7 +6,7 @@ import { } from "@/server/routers/hotels/schemas/image" import { HotelData } from "./hotelCardListingProps" -import { Filter } from "./hotelFilters" +import { CategorizedFilters, Filter } from "./hotelFilters" import type { Coordinates } from "@/types/components/maps/coordinates" @@ -21,6 +21,7 @@ export interface SelectHotelMapProps { hotelPins: HotelPin[] mapId: string hotels: HotelData[] + filterList: CategorizedFilters } type ImageSizes = z.infer diff --git a/types/components/maps/poiMarker.ts b/types/components/maps/poiMarker.ts index 34fad0e6f..abab6fc9d 100644 --- a/types/components/maps/poiMarker.ts +++ b/types/components/maps/poiMarker.ts @@ -2,14 +2,11 @@ import { poiVariants } from "@/components/Maps/Markers/Poi/variants" import type { VariantProps } from "class-variance-authority" -import { - PointOfInterestCategoryNameEnum, - PointOfInterestGroupEnum, -} from "@/types/hotel" +import type { PointOfInterestGroupEnum } from "@/types/hotel" export interface PoiMarkerProps extends VariantProps { group: PointOfInterestGroupEnum - categoryName?: PointOfInterestCategoryNameEnum + categoryName?: string size?: number className?: string } diff --git a/types/components/tracking.ts b/types/components/tracking.ts index aa42ae5fe..fa6e82a95 100644 --- a/types/components/tracking.ts +++ b/types/components/tracking.ts @@ -80,3 +80,4 @@ export type TrackingPosition = | "hamburger menu" | "join scandic friends sidebar" | "sign up verification" + | "enter details" diff --git a/types/hotel.ts b/types/hotel.ts index 5052caded..aadaab740 100644 --- a/types/hotel.ts +++ b/types/hotel.ts @@ -26,26 +26,6 @@ export type GalleryImage = z.infer export type PointOfInterest = z.output -export enum PointOfInterestCategoryNameEnum { - AIRPORT = "Airport", - AMUSEMENT_PARK = "Amusement park", - BUS_TERMINAL = "Bus terminal", - FAIR = "Fair", - HOSPITAL = "Hospital", - HOTEL = "Hotel", - MARKETING_CITY = "Marketing city", - MUSEUM = "Museum", - NEARBY_COMPANIES = "Nearby companies", - PARKING_GARAGE = "Parking / Garage", - RESTAURANT = "Restaurant", - SHOPPING = "Shopping", - SPORTS = "Sports", - THEATRE = "Theatre", - TOURIST = "Tourist", - TRANSPORTATIONS = "Transportations", - ZOO = "Zoo", -} - export enum PointOfInterestGroupEnum { PUBLIC_TRANSPORT = "Public transport", ATTRACTIONS = "Attractions", diff --git a/types/providers/steps.ts b/types/providers/steps.ts index 8c24fdc8f..9ba0361eb 100644 --- a/types/providers/steps.ts +++ b/types/providers/steps.ts @@ -6,5 +6,6 @@ export interface StepsProviderProps extends React.PropsWithChildren { bedTypes: BedTypeSelection[] breakfastPackages: BreakfastPackage[] | null isMember: boolean + searchParams: string step: StepEnum }