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)/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/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/Signup/index.tsx b/components/Forms/Signup/index.tsx index d6d5fc7da..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: { @@ -56,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 ( @@ -79,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} > @@ -186,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} ) : ( - {intl.formatMessage({ id: "Sign up to Scandic Friends" })} + {methods.formState.isSubmitting || signup.isPending + ? signingUpPendingText + : signupButtonText} )} diff --git a/components/GuestsRoomsPicker/ChildSelector/ChildInfoSelector.tsx b/components/GuestsRoomsPicker/ChildSelector/ChildInfoSelector.tsx index 8252f2d2a..b39821483 100644 --- a/components/GuestsRoomsPicker/ChildSelector/ChildInfoSelector.tsx +++ b/components/GuestsRoomsPicker/ChildSelector/ChildInfoSelector.tsx @@ -94,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/details.module.css b/components/HotelReservation/EnterDetails/Details/details.module.css index c6571ea9e..8781a9be2 100644 --- a/components/HotelReservation/EnterDetails/Details/details.module.css +++ b/components/HotelReservation/EnterDetails/Details/details.module.css @@ -1,7 +1,6 @@ .form { display: grid; gap: var(--Spacing-x3); - margin-bottom: var(--Spacing-x3); } .container { diff --git a/components/HotelReservation/EnterDetails/SectionAccordion/index.tsx b/components/HotelReservation/EnterDetails/SectionAccordion/index.tsx index b96ef8ef8..488d941b5 100644 --- a/components/HotelReservation/EnterDetails/SectionAccordion/index.tsx +++ b/components/HotelReservation/EnterDetails/SectionAccordion/index.tsx @@ -75,7 +75,11 @@ export default function SectionAccordion({ - + - {children} + + {children} + ) } diff --git a/components/HotelReservation/EnterDetails/SectionAccordion/sectionAccordion.module.css b/components/HotelReservation/EnterDetails/SectionAccordion/sectionAccordion.module.css index 0bbcf851c..125905317 100644 --- a/components/HotelReservation/EnterDetails/SectionAccordion/sectionAccordion.module.css +++ b/components/HotelReservation/EnterDetails/SectionAccordion/sectionAccordion.module.css @@ -33,6 +33,10 @@ padding: 0; } +.modifyButton:disabled { + cursor: default; +} + .title { grid-area: title; text-align: start; @@ -79,7 +83,10 @@ .accordion[data-open="true"] { grid-template-rows: var(--header-height) 1fr; - gap: var(--Spacing-x3); +} + +.contentWrapper { + padding-bottom: var(--Spacing-x3); } .content { @@ -90,7 +97,7 @@ @media screen and (min-width: 768px) { .accordion { - gap: var(--Spacing-x3); + column-gap: var(--Spacing-x3); grid-template-areas: "circle header" "circle content"; } 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 ce3d2173d..4fe186988 100644 --- a/components/HotelReservation/SelectHotel/SelectHotelMap/index.tsx +++ b/components/HotelReservation/SelectHotel/SelectHotelMap/index.tsx @@ -13,6 +13,7 @@ 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" @@ -25,6 +26,7 @@ export default function SelectHotelMap({ hotelPins, mapId, hotels, + filterList, }: SelectHotelMapProps) { const searchParams = useSearchParams() const router = useRouter() @@ -102,8 +104,7 @@ export default function SelectHotelMap({ > - Filter and sort - {/* TODO: Add filter and sort button */} + { const breakfastIncluded = getRateDefinitionForRate(rate)?.breakfastIncluded - switch (breakfastIncluded) { case true: return intl.formatMessage({ id: "Breakfast is included." }) @@ -83,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" }) @@ -115,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 && ( - + ({ 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/i18n/dictionaries/da.json b/i18n/dictionaries/da.json index 7ec920d05..a9fdcfae4 100644 --- a/i18n/dictionaries/da.json +++ b/i18n/dictionaries/da.json @@ -154,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", @@ -336,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.", diff --git a/i18n/dictionaries/de.json b/i18n/dictionaries/de.json index 80b3b6116..96bcef2de 100644 --- a/i18n/dictionaries/de.json +++ b/i18n/dictionaries/de.json @@ -154,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", @@ -335,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.", diff --git a/i18n/dictionaries/en.json b/i18n/dictionaries/en.json index 6080fc36b..385861316 100644 --- a/i18n/dictionaries/en.json +++ b/i18n/dictionaries/en.json @@ -166,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", @@ -365,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.", diff --git a/i18n/dictionaries/fi.json b/i18n/dictionaries/fi.json index 0c1a08d35..09fb8a403 100644 --- a/i18n/dictionaries/fi.json +++ b/i18n/dictionaries/fi.json @@ -154,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", @@ -337,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.", diff --git a/i18n/dictionaries/no.json b/i18n/dictionaries/no.json index 2aca61a85..dcbcdddf4 100644 --- a/i18n/dictionaries/no.json +++ b/i18n/dictionaries/no.json @@ -153,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", @@ -334,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.", diff --git a/i18n/dictionaries/sv.json b/i18n/dictionaries/sv.json index db300b7c2..404896e15 100644 --- a/i18n/dictionaries/sv.json +++ b/i18n/dictionaries/sv.json @@ -153,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", @@ -334,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.", 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/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