Merge branch 'master' of bitbucket.org:scandic-swap/web into fix/loading-rooms-separately
This commit is contained in:
@@ -1,89 +0,0 @@
|
|||||||
"use server"
|
|
||||||
|
|
||||||
import { parsePhoneNumber } from "libphonenumber-js"
|
|
||||||
import { redirect } from "next/navigation"
|
|
||||||
import { z } from "zod"
|
|
||||||
|
|
||||||
import { signupVerify } from "@/constants/routes/signup"
|
|
||||||
import * as api from "@/lib/api"
|
|
||||||
import { serviceServerActionProcedure } from "@/server/trpc"
|
|
||||||
|
|
||||||
import { signUpSchema } from "@/components/Forms/Signup/schema"
|
|
||||||
import { passwordValidator } from "@/utils/passwordValidator"
|
|
||||||
import { phoneValidator } from "@/utils/phoneValidator"
|
|
||||||
|
|
||||||
const registerUserPayload = z.object({
|
|
||||||
language: z.string(),
|
|
||||||
firstName: z.string(),
|
|
||||||
lastName: z.string(),
|
|
||||||
email: z.string(),
|
|
||||||
phoneNumber: phoneValidator("Phone is required"),
|
|
||||||
dateOfBirth: z.string(),
|
|
||||||
address: z.object({
|
|
||||||
city: z.string().default(""),
|
|
||||||
country: z.string().default(""),
|
|
||||||
countryCode: z.string().default(""),
|
|
||||||
zipCode: z.string().default(""),
|
|
||||||
streetAddress: z.string().default(""),
|
|
||||||
}),
|
|
||||||
password: passwordValidator("Password is required"),
|
|
||||||
})
|
|
||||||
|
|
||||||
export const registerUser = serviceServerActionProcedure
|
|
||||||
.input(signUpSchema)
|
|
||||||
.mutation(async function ({ ctx, input }) {
|
|
||||||
const payload = {
|
|
||||||
...input,
|
|
||||||
language: ctx.lang,
|
|
||||||
phoneNumber: input.phoneNumber.replace(/\s+/g, ""),
|
|
||||||
}
|
|
||||||
|
|
||||||
const parsedPayload = registerUserPayload.safeParse(payload)
|
|
||||||
if (!parsedPayload.success) {
|
|
||||||
console.error(
|
|
||||||
"registerUser payload validation error",
|
|
||||||
JSON.stringify({
|
|
||||||
query: input,
|
|
||||||
error: parsedPayload.error,
|
|
||||||
})
|
|
||||||
)
|
|
||||||
|
|
||||||
return { success: false, error: "Validation error" }
|
|
||||||
}
|
|
||||||
|
|
||||||
let apiResponse
|
|
||||||
try {
|
|
||||||
apiResponse = await api.post(api.endpoints.v1.Profile.profile, {
|
|
||||||
body: parsedPayload.data,
|
|
||||||
headers: {
|
|
||||||
Authorization: `Bearer ${ctx.serviceToken}`,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Unexpected error", error)
|
|
||||||
return { success: false, error: "Unexpected error" }
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!apiResponse.ok) {
|
|
||||||
const text = await apiResponse.text()
|
|
||||||
console.error(
|
|
||||||
"registerUser api error",
|
|
||||||
JSON.stringify({
|
|
||||||
query: input,
|
|
||||||
error: {
|
|
||||||
status: apiResponse.status,
|
|
||||||
statusText: apiResponse.statusText,
|
|
||||||
error: text,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
)
|
|
||||||
return { success: false, error: "API error" }
|
|
||||||
}
|
|
||||||
|
|
||||||
const json = await apiResponse.json()
|
|
||||||
console.log("registerUser: json", json)
|
|
||||||
|
|
||||||
// Note: The redirect needs to be called after the try/catch block.
|
|
||||||
// See: https://nextjs.org/docs/app/api-reference/functions/redirect
|
|
||||||
redirect(signupVerify[ctx.lang])
|
|
||||||
})
|
|
||||||
@@ -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 <CurrentLoadingSpinner />
|
||||||
|
}
|
||||||
|
return <LoadingSpinner />
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import LoadingSpinner from "@/components/LoadingSpinner"
|
||||||
|
|
||||||
|
export default function Loading() {
|
||||||
|
return <LoadingSpinner fullPage />
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import LoadingSpinner from "@/components/LoadingSpinner"
|
||||||
|
|
||||||
|
export default function Loading() {
|
||||||
|
return <LoadingSpinner fullPage />
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import LoadingSpinner from "@/components/LoadingSpinner"
|
||||||
|
|
||||||
|
export default function Loading() {
|
||||||
|
return <LoadingSpinner fullPage />
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import LoadingSpinner from "@/components/LoadingSpinner"
|
||||||
|
|
||||||
|
export default function LoadingModal() {
|
||||||
|
return <LoadingSpinner />
|
||||||
|
}
|
||||||
@@ -12,7 +12,7 @@ import {
|
|||||||
import { MapModal } from "@/components/MapModal"
|
import { MapModal } from "@/components/MapModal"
|
||||||
import { setLang } from "@/i18n/serverContext"
|
import { setLang } from "@/i18n/serverContext"
|
||||||
|
|
||||||
import { fetchAvailableHotels } from "../../utils"
|
import { fetchAvailableHotels, getFiltersFromHotels } from "../../utils"
|
||||||
|
|
||||||
import type { SelectHotelSearchParams } from "@/types/components/hotelReservation/selectHotel/selectHotelSearchParams"
|
import type { SelectHotelSearchParams } from "@/types/components/hotelReservation/selectHotel/selectHotelSearchParams"
|
||||||
import type { LangParams, PageArgs } from "@/types/params"
|
import type { LangParams, PageArgs } from "@/types/params"
|
||||||
@@ -57,6 +57,7 @@ export default async function SelectHotelMapPage({
|
|||||||
})
|
})
|
||||||
|
|
||||||
const hotelPins = getHotelPins(hotels)
|
const hotelPins = getHotelPins(hotels)
|
||||||
|
const filterList = getFiltersFromHotels(hotels)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MapModal>
|
<MapModal>
|
||||||
@@ -65,6 +66,7 @@ export default async function SelectHotelMapPage({
|
|||||||
hotelPins={hotelPins}
|
hotelPins={hotelPins}
|
||||||
mapId={googleMapId}
|
mapId={googleMapId}
|
||||||
hotels={hotels}
|
hotels={hotels}
|
||||||
|
filterList={filterList}
|
||||||
/>
|
/>
|
||||||
</MapModal>
|
</MapModal>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
getFiltersFromHotels,
|
getFiltersFromHotels,
|
||||||
} from "@/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/utils"
|
} from "@/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/utils"
|
||||||
import HotelCardListing from "@/components/HotelReservation/HotelCardListing"
|
import HotelCardListing from "@/components/HotelReservation/HotelCardListing"
|
||||||
|
import HotelCount from "@/components/HotelReservation/SelectHotel/HotelCount"
|
||||||
import HotelFilter from "@/components/HotelReservation/SelectHotel/HotelFilter"
|
import HotelFilter from "@/components/HotelReservation/SelectHotel/HotelFilter"
|
||||||
import HotelSorter from "@/components/HotelReservation/SelectHotel/HotelSorter"
|
import HotelSorter from "@/components/HotelReservation/SelectHotel/HotelSorter"
|
||||||
import MobileMapButtonContainer from "@/components/HotelReservation/SelectHotel/MobileMapButtonContainer"
|
import MobileMapButtonContainer from "@/components/HotelReservation/SelectHotel/MobileMapButtonContainer"
|
||||||
@@ -20,7 +21,6 @@ import StaticMap from "@/components/Maps/StaticMap"
|
|||||||
import Alert from "@/components/TempDesignSystem/Alert"
|
import Alert from "@/components/TempDesignSystem/Alert"
|
||||||
import Button from "@/components/TempDesignSystem/Button"
|
import Button from "@/components/TempDesignSystem/Button"
|
||||||
import Link from "@/components/TempDesignSystem/Link"
|
import Link from "@/components/TempDesignSystem/Link"
|
||||||
import Preamble from "@/components/TempDesignSystem/Text/Preamble"
|
|
||||||
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
|
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
|
||||||
import { getIntl } from "@/i18n"
|
import { getIntl } from "@/i18n"
|
||||||
import { setLang } from "@/i18n/serverContext"
|
import { setLang } from "@/i18n/serverContext"
|
||||||
@@ -66,13 +66,15 @@ export default async function SelectHotelPage({
|
|||||||
|
|
||||||
const filterList = getFiltersFromHotels(hotels)
|
const filterList = getFiltersFromHotels(hotels)
|
||||||
|
|
||||||
|
const isAllUnavailable = hotels.every((hotel) => hotel.price === undefined)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<header className={styles.header}>
|
<header className={styles.header}>
|
||||||
<div className={styles.title}>
|
<div className={styles.title}>
|
||||||
<div className={styles.cityInformation}>
|
<div className={styles.cityInformation}>
|
||||||
<Subtitle>{city.name}</Subtitle>
|
<Subtitle>{city.name}</Subtitle>
|
||||||
<Preamble>{hotels.length} hotels</Preamble>
|
<HotelCount />
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.sorter}>
|
<div className={styles.sorter}>
|
||||||
<HotelSorter discreet />
|
<HotelSorter discreet />
|
||||||
@@ -123,7 +125,7 @@ export default async function SelectHotelPage({
|
|||||||
<HotelFilter filters={filterList} className={styles.filter} />
|
<HotelFilter filters={filterList} className={styles.filter} />
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.hotelList}>
|
<div className={styles.hotelList}>
|
||||||
{!hotels.length && (
|
{isAllUnavailable && (
|
||||||
<Alert
|
<Alert
|
||||||
type={AlertTypeEnum.Info}
|
type={AlertTypeEnum.Info}
|
||||||
heading={intl.formatMessage({ id: "No availability" })}
|
heading={intl.formatMessage({ id: "No availability" })}
|
||||||
|
|||||||
@@ -19,6 +19,15 @@ const hotelSurroundingsFilterNames = [
|
|||||||
"Omgivningar",
|
"Omgivningar",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
const hotelFacilitiesFilterNames = [
|
||||||
|
"Hotel facilities",
|
||||||
|
"Hotellfaciliteter",
|
||||||
|
"Hotelfaciliteter",
|
||||||
|
"Hotel faciliteter",
|
||||||
|
"Hotel-Infos",
|
||||||
|
"Hotellin palvelut",
|
||||||
|
]
|
||||||
|
|
||||||
export async function fetchAvailableHotels(
|
export async function fetchAvailableHotels(
|
||||||
input: AvailabilityInput
|
input: AvailabilityInput
|
||||||
): Promise<HotelData[]> {
|
): Promise<HotelData[]> {
|
||||||
@@ -52,6 +61,7 @@ export function getFiltersFromHotels(hotels: HotelData[]): CategorizedFilters {
|
|||||||
const filterList: Filter[] = uniqueFilterIds
|
const filterList: Filter[] = uniqueFilterIds
|
||||||
.map((filterId) => filters.find((filter) => filter.id === filterId))
|
.map((filterId) => filters.find((filter) => filter.id === filterId))
|
||||||
.filter((filter): filter is Filter => filter !== undefined)
|
.filter((filter): filter is Filter => filter !== undefined)
|
||||||
|
.sort((a, b) => b.sortOrder - a.sortOrder)
|
||||||
|
|
||||||
return filterList.reduce<CategorizedFilters>(
|
return filterList.reduce<CategorizedFilters>(
|
||||||
(acc, filter) => {
|
(acc, filter) => {
|
||||||
@@ -61,10 +71,13 @@ export function getFiltersFromHotels(hotels: HotelData[]): CategorizedFilters {
|
|||||||
surroundingsFilters: [...acc.surroundingsFilters, filter],
|
surroundingsFilters: [...acc.surroundingsFilters, filter],
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
if (filter.filter && hotelFacilitiesFilterNames.includes(filter.filter))
|
||||||
facilityFilters: [...acc.facilityFilters, filter],
|
return {
|
||||||
surroundingsFilters: acc.surroundingsFilters,
|
facilityFilters: [...acc.facilityFilters, filter],
|
||||||
}
|
surroundingsFilters: acc.surroundingsFilters,
|
||||||
|
}
|
||||||
|
|
||||||
|
return acc
|
||||||
},
|
},
|
||||||
{ facilityFilters: [], surroundingsFilters: [] }
|
{ facilityFilters: [], surroundingsFilters: [] }
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import LoadingSpinner from "@/components/LoadingSpinner"
|
||||||
|
|
||||||
|
export default function LoadingSummaryHeader() {
|
||||||
|
return <LoadingSpinner />
|
||||||
|
}
|
||||||
@@ -83,10 +83,10 @@ export default async function SummaryPage({
|
|||||||
price: availability.publicRate.localPrice.pricePerStay,
|
price: availability.publicRate.localPrice.pricePerStay,
|
||||||
currency: availability.publicRate.localPrice.currency,
|
currency: availability.publicRate.localPrice.currency,
|
||||||
},
|
},
|
||||||
euro: availability.publicRate.requestedPrice
|
euro: availability.publicRate?.requestedPrice
|
||||||
? {
|
? {
|
||||||
price: availability.publicRate.requestedPrice.pricePerStay,
|
price: availability.publicRate?.requestedPrice.pricePerStay,
|
||||||
currency: availability.publicRate.requestedPrice.currency,
|
currency: availability.publicRate?.requestedPrice.currency,
|
||||||
}
|
}
|
||||||
: undefined,
|
: undefined,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import "./enterDetailsLayout.css"
|
import "./enterDetailsLayout.css"
|
||||||
|
|
||||||
import { notFound, redirect, RedirectType } from "next/navigation"
|
import { notFound } from "next/navigation"
|
||||||
|
|
||||||
import {
|
import {
|
||||||
getBreakfastPackages,
|
getBreakfastPackages,
|
||||||
@@ -38,6 +38,8 @@ export default async function StepPage({
|
|||||||
}: PageArgs<LangParams, SelectRateSearchParams & { step: StepEnum }>) {
|
}: PageArgs<LangParams, SelectRateSearchParams & { step: StepEnum }>) {
|
||||||
const intl = await getIntl()
|
const intl = await getIntl()
|
||||||
const selectRoomParams = new URLSearchParams(searchParams)
|
const selectRoomParams = new URLSearchParams(searchParams)
|
||||||
|
selectRoomParams.delete("step")
|
||||||
|
const searchParamsString = selectRoomParams.toString()
|
||||||
const {
|
const {
|
||||||
hotel: hotelId,
|
hotel: hotelId,
|
||||||
rooms,
|
rooms,
|
||||||
@@ -111,11 +113,19 @@ export default async function StepPage({
|
|||||||
publicPrice: roomAvailability.publicRate!.localPrice.pricePerStay,
|
publicPrice: roomAvailability.publicRate!.localPrice.pricePerStay,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const memberPrice = roomAvailability.memberRate
|
||||||
|
? {
|
||||||
|
price: roomAvailability.memberRate.localPrice.pricePerStay,
|
||||||
|
currency: roomAvailability.memberRate.localPrice.currency,
|
||||||
|
}
|
||||||
|
: undefined
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StepsProvider
|
<StepsProvider
|
||||||
bedTypes={roomAvailability.bedTypes}
|
bedTypes={roomAvailability.bedTypes}
|
||||||
breakfastPackages={breakfastPackages}
|
breakfastPackages={breakfastPackages}
|
||||||
isMember={!!user}
|
isMember={!!user}
|
||||||
|
searchParams={searchParamsString}
|
||||||
step={searchParams.step}
|
step={searchParams.step}
|
||||||
>
|
>
|
||||||
<section>
|
<section>
|
||||||
@@ -152,7 +162,7 @@ export default async function StepPage({
|
|||||||
step={StepEnum.details}
|
step={StepEnum.details}
|
||||||
label={intl.formatMessage({ id: "Enter your details" })}
|
label={intl.formatMessage({ id: "Enter your details" })}
|
||||||
>
|
>
|
||||||
<Details user={user} />
|
<Details user={user} memberPrice={memberPrice} />
|
||||||
</SectionAccordion>
|
</SectionAccordion>
|
||||||
|
|
||||||
<SectionAccordion
|
<SectionAccordion
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
export { default } from "../../page"
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export { default } from "./page"
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export { default } from "../../page"
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export { default } from "../../page"
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export { default } from "../../page"
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export { default } from "./page"
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export { default } from "../../page"
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export { default } from "../page"
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export { default } from "../../page"
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export { default } from "./page"
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export { default } from "../../page"
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export { default } from "../page"
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export { default } from "../../page"
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export { default } from "./page"
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export { default } from "../../page"
|
|
||||||
@@ -13,7 +13,7 @@ export default function SitewideAlertPage({ params }: PageArgs<LangParams>) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setLang(params.lang)
|
setLang(params.lang)
|
||||||
preload()
|
void preload()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Suspense>
|
<Suspense>
|
||||||
|
|||||||
@@ -177,7 +177,7 @@ export default function BookingWidgetClient({
|
|||||||
>
|
>
|
||||||
<CloseLargeIcon />
|
<CloseLargeIcon />
|
||||||
</button>
|
</button>
|
||||||
<Form locations={locations} type={type} setIsOpen={setIsOpen} />
|
<Form locations={locations} type={type} onClose={closeMobileSearch} />
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
<div className={styles.backdrop} onClick={closeMobileSearch} />
|
<div className={styles.backdrop} onClick={closeMobileSearch} />
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { getLocations } from "@/lib/trpc/memoizedRequests"
|
import { getLocations, getSiteConfig } from "@/lib/trpc/memoizedRequests"
|
||||||
|
|
||||||
import BookingWidgetClient from "./Client"
|
import BookingWidgetClient from "./Client"
|
||||||
|
|
||||||
@@ -13,8 +13,9 @@ export default async function BookingWidget({
|
|||||||
searchParams,
|
searchParams,
|
||||||
}: BookingWidgetProps) {
|
}: BookingWidgetProps) {
|
||||||
const locations = await getLocations()
|
const locations = await getLocations()
|
||||||
|
const siteConfig = await getSiteConfig()
|
||||||
|
|
||||||
if (!locations || "error" in locations) {
|
if (!locations || "error" in locations || siteConfig?.bookingWidgetDisabled) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -206,11 +206,12 @@ export default function Search({ locations }: SearchProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function SearchSkeleton() {
|
export function SearchSkeleton() {
|
||||||
|
const intl = useIntl()
|
||||||
return (
|
return (
|
||||||
<div className={styles.container}>
|
<div className={styles.container}>
|
||||||
<div className={styles.label}>
|
<div className={styles.label}>
|
||||||
<Caption type="bold" color="red" asChild>
|
<Caption type="bold" color="red" asChild>
|
||||||
<span>Where to</span>
|
<span>{intl.formatMessage({ id: "Where to" })}</span>
|
||||||
</Caption>
|
</Caption>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.input}>
|
<div className={styles.input}>
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ const formId = "booking-widget"
|
|||||||
export default function Form({
|
export default function Form({
|
||||||
locations,
|
locations,
|
||||||
type,
|
type,
|
||||||
setIsOpen,
|
onClose,
|
||||||
}: BookingWidgetFormProps) {
|
}: BookingWidgetFormProps) {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const lang = useLang()
|
const lang = useLang()
|
||||||
@@ -56,7 +56,7 @@ export default function Form({
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
setIsOpen(false)
|
onClose()
|
||||||
router.push(`${bookingFlowPage}?${bookingWidgetParams.toString()}`)
|
router.push(`${bookingFlowPage}?${bookingWidgetParams.toString()}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,12 +3,14 @@
|
|||||||
align-self: flex-start;
|
align-self: flex-start;
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: var(--Spacing-x2);
|
gap: var(--Spacing-x2);
|
||||||
|
container-name: addressContainer;
|
||||||
|
container-type: inline-size;
|
||||||
}
|
}
|
||||||
|
|
||||||
.container {
|
.container {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: var(--Spacing-x2);
|
gap: var(--Spacing-x2);
|
||||||
grid-template-columns: max(164px) 1fr;
|
grid-template-columns: minmax(100px, 164px) 1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 768px) {
|
@media (min-width: 768px) {
|
||||||
@@ -16,3 +18,9 @@
|
|||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@container addressContainer (max-width: 350px) {
|
||||||
|
.container {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { zodResolver } from "@hookform/resolvers/zod"
|
import { zodResolver } from "@hookform/resolvers/zod"
|
||||||
|
import { useRouter } from "next/navigation"
|
||||||
import { FormProvider, useForm } from "react-hook-form"
|
import { FormProvider, useForm } from "react-hook-form"
|
||||||
import { useIntl } from "react-intl"
|
import { useIntl } from "react-intl"
|
||||||
|
|
||||||
import { privacyPolicy } from "@/constants/currentWebHrefs"
|
import { privacyPolicy } from "@/constants/currentWebHrefs"
|
||||||
|
import { trpc } from "@/lib/trpc/client"
|
||||||
|
|
||||||
import { registerUser } from "@/actions/registerUser"
|
|
||||||
import Button from "@/components/TempDesignSystem/Button"
|
import Button from "@/components/TempDesignSystem/Button"
|
||||||
import Checkbox from "@/components/TempDesignSystem/Form/Checkbox"
|
import Checkbox from "@/components/TempDesignSystem/Form/Checkbox"
|
||||||
import CountrySelect from "@/components/TempDesignSystem/Form/Country"
|
import CountrySelect from "@/components/TempDesignSystem/Form/Country"
|
||||||
@@ -30,11 +31,28 @@ import type { SignUpFormProps } from "@/types/components/form/signupForm"
|
|||||||
|
|
||||||
export default function SignupForm({ link, subtitle, title }: SignUpFormProps) {
|
export default function SignupForm({ link, subtitle, title }: SignUpFormProps) {
|
||||||
const intl = useIntl()
|
const intl = useIntl()
|
||||||
|
const router = useRouter()
|
||||||
const lang = useLang()
|
const lang = useLang()
|
||||||
const country = intl.formatMessage({ id: "Country" })
|
const country = intl.formatMessage({ id: "Country" })
|
||||||
const email = intl.formatMessage({ id: "Email address" })
|
const email = intl.formatMessage({ id: "Email address" })
|
||||||
const phoneNumber = intl.formatMessage({ id: "Phone number" })
|
const phoneNumber = intl.formatMessage({ id: "Phone number" })
|
||||||
const zipCode = intl.formatMessage({ id: "Zip code" })
|
const zipCode = intl.formatMessage({ id: "Zip code" })
|
||||||
|
const signupButtonText = intl.formatMessage({
|
||||||
|
id: "Sign up to Scandic Friends",
|
||||||
|
})
|
||||||
|
const signingUpPendingText = intl.formatMessage({ id: "Signing up..." })
|
||||||
|
|
||||||
|
const signup = trpc.user.signup.useMutation({
|
||||||
|
onSuccess: (data) => {
|
||||||
|
if (data.success && data.redirectUrl) {
|
||||||
|
router.push(data.redirectUrl)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
toast.error(intl.formatMessage({ id: "Something went wrong!" }))
|
||||||
|
console.error("Component Signup error:", error)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
const methods = useForm<SignUpSchema>({
|
const methods = useForm<SignUpSchema>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
@@ -48,7 +66,6 @@ export default function SignupForm({ link, subtitle, title }: SignUpFormProps) {
|
|||||||
zipCode: "",
|
zipCode: "",
|
||||||
},
|
},
|
||||||
password: "",
|
password: "",
|
||||||
termsAccepted: false,
|
|
||||||
},
|
},
|
||||||
mode: "all",
|
mode: "all",
|
||||||
criteriaMode: "all",
|
criteriaMode: "all",
|
||||||
@@ -57,19 +74,7 @@ export default function SignupForm({ link, subtitle, title }: SignUpFormProps) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
async function onSubmit(data: SignUpSchema) {
|
async function onSubmit(data: SignUpSchema) {
|
||||||
try {
|
signup.mutate({ ...data, language: lang })
|
||||||
const result = await registerUser(data)
|
|
||||||
if (result && !result.success) {
|
|
||||||
toast.error(intl.formatMessage({ id: "Something went wrong!" }))
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
// The server-side redirect will throw an error, which we can ignore
|
|
||||||
// as it's handled by Next.js.
|
|
||||||
if (error instanceof Error && error.message.includes("NEXT_REDIRECT")) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
toast.error(intl.formatMessage({ id: "Something went wrong!" }))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -80,11 +85,6 @@ export default function SignupForm({ link, subtitle, title }: SignUpFormProps) {
|
|||||||
className={styles.form}
|
className={styles.form}
|
||||||
id="register"
|
id="register"
|
||||||
onSubmit={methods.handleSubmit(onSubmit)}
|
onSubmit={methods.handleSubmit(onSubmit)}
|
||||||
/**
|
|
||||||
* Ignoring since ts doesn't recognize that tRPC
|
|
||||||
* parses FormData before reaching the route
|
|
||||||
* @ts-ignore */
|
|
||||||
action={registerUser}
|
|
||||||
>
|
>
|
||||||
<section className={styles.userInfo}>
|
<section className={styles.userInfo}>
|
||||||
<div className={styles.container}>
|
<div className={styles.container}>
|
||||||
@@ -187,7 +187,7 @@ export default function SignupForm({ link, subtitle, title }: SignUpFormProps) {
|
|||||||
onClick={() => methods.trigger()}
|
onClick={() => methods.trigger()}
|
||||||
data-testid="trigger-validation"
|
data-testid="trigger-validation"
|
||||||
>
|
>
|
||||||
{intl.formatMessage({ id: "Sign up to Scandic Friends" })}
|
{signupButtonText}
|
||||||
</Button>
|
</Button>
|
||||||
) : (
|
) : (
|
||||||
<Button
|
<Button
|
||||||
@@ -195,10 +195,12 @@ export default function SignupForm({ link, subtitle, title }: SignUpFormProps) {
|
|||||||
type="submit"
|
type="submit"
|
||||||
theme="base"
|
theme="base"
|
||||||
intent="primary"
|
intent="primary"
|
||||||
disabled={methods.formState.isSubmitting}
|
disabled={methods.formState.isSubmitting || signup.isPending}
|
||||||
data-testid="submit"
|
data-testid="submit"
|
||||||
>
|
>
|
||||||
{intl.formatMessage({ id: "Sign up to Scandic Friends" })}
|
{methods.formState.isSubmitting || signup.isPending
|
||||||
|
? signingUpPendingText
|
||||||
|
: signupButtonText}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -33,11 +33,10 @@ export default function ChildInfoSelector({
|
|||||||
const ageLabel = intl.formatMessage({ id: "Age" })
|
const ageLabel = intl.formatMessage({ id: "Age" })
|
||||||
const bedLabel = intl.formatMessage({ id: "Bed" })
|
const bedLabel = intl.formatMessage({ id: "Bed" })
|
||||||
const errorMessage = intl.formatMessage({ id: "Child age is required" })
|
const errorMessage = intl.formatMessage({ id: "Child age is required" })
|
||||||
const { setValue, formState, register, trigger } = useFormContext()
|
const { setValue, formState, register } = useFormContext()
|
||||||
|
|
||||||
function updateSelectedBed(bed: number) {
|
function updateSelectedBed(bed: number) {
|
||||||
setValue(`rooms.${roomIndex}.child.${index}.bed`, bed)
|
setValue(`rooms.${roomIndex}.child.${index}.bed`, bed)
|
||||||
trigger()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateSelectedAge(age: number) {
|
function updateSelectedAge(age: number) {
|
||||||
@@ -95,7 +94,7 @@ export default function ChildInfoSelector({
|
|||||||
updateSelectedAge(key as number)
|
updateSelectedAge(key as number)
|
||||||
}}
|
}}
|
||||||
placeholder={ageLabel}
|
placeholder={ageLabel}
|
||||||
maxHeight={150}
|
maxHeight={180}
|
||||||
{...register(ageFieldName, {
|
{...register(ageFieldName, {
|
||||||
required: true,
|
required: true,
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -99,7 +99,7 @@ export default function GuestsRoomsPickerDialog({
|
|||||||
<Tooltip
|
<Tooltip
|
||||||
heading={disabledBookingOptionsHeader}
|
heading={disabledBookingOptionsHeader}
|
||||||
text={disabledBookingOptionsText}
|
text={disabledBookingOptionsText}
|
||||||
position="top"
|
position="bottom"
|
||||||
arrow="left"
|
arrow="left"
|
||||||
>
|
>
|
||||||
{rooms.length < 4 ? (
|
{rooms.length < 4 ? (
|
||||||
@@ -124,7 +124,7 @@ export default function GuestsRoomsPickerDialog({
|
|||||||
<Tooltip
|
<Tooltip
|
||||||
heading={disabledBookingOptionsHeader}
|
heading={disabledBookingOptionsHeader}
|
||||||
text={disabledBookingOptionsText}
|
text={disabledBookingOptionsText}
|
||||||
position="top"
|
position="bottom"
|
||||||
arrow="left"
|
arrow="left"
|
||||||
>
|
>
|
||||||
{rooms.length < 4 ? (
|
{rooms.length < 4 ? (
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
<div className={styles.cardContainer}>
|
||||||
|
<Checkbox name={name} className={styles.checkBox}>
|
||||||
|
<div>
|
||||||
|
{memberPrice ? (
|
||||||
|
<Caption type="label" textTransform="uppercase" color="red">
|
||||||
|
{saveOnJoiningLabel}
|
||||||
|
</Caption>
|
||||||
|
) : null}
|
||||||
|
<Caption
|
||||||
|
type="label"
|
||||||
|
textTransform="uppercase"
|
||||||
|
color="uiTextHighContrast"
|
||||||
|
>
|
||||||
|
{intl.formatMessage({ id: "Join Scandic Friends" })}
|
||||||
|
</Caption>
|
||||||
|
</div>
|
||||||
|
</Checkbox>
|
||||||
|
|
||||||
|
<Footnote color="uiTextHighContrast" className={styles.login}>
|
||||||
|
{intl.formatMessage({ id: "Already a friend?" })}{" "}
|
||||||
|
<LoginButton
|
||||||
|
color="burgundy"
|
||||||
|
position="enter details"
|
||||||
|
trackingId="join-scandic-friends-enter-details"
|
||||||
|
variant="breadcrumb"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
{intl.formatMessage({ id: "Log in" })}
|
||||||
|
</LoginButton>
|
||||||
|
</Footnote>
|
||||||
|
|
||||||
|
<div className={styles.list}>
|
||||||
|
{list.map((item) => (
|
||||||
|
<Caption
|
||||||
|
key={item.title}
|
||||||
|
color="uiTextPlaceholder"
|
||||||
|
className={styles.listItem}
|
||||||
|
>
|
||||||
|
<CheckIcon color="uiTextPlaceholder" height="20" /> {item.title}
|
||||||
|
</Caption>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<Footnote color="uiTextPlaceholder" className={styles.terms}>
|
||||||
|
{intl.formatMessage<React.ReactNode>(
|
||||||
|
{
|
||||||
|
id: "signup.terms",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
termsLink: (str) => (
|
||||||
|
<Link
|
||||||
|
variant="default"
|
||||||
|
textDecoration="underline"
|
||||||
|
size="tiny"
|
||||||
|
target="_blank"
|
||||||
|
color="uiTextPlaceholder"
|
||||||
|
href={privacyPolicy[lang]}
|
||||||
|
>
|
||||||
|
{str}
|
||||||
|
</Link>
|
||||||
|
),
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
</Footnote>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,14 +4,8 @@ import { useEffect, useState } from "react"
|
|||||||
import { useWatch } from "react-hook-form"
|
import { useWatch } from "react-hook-form"
|
||||||
import { useIntl } from "react-intl"
|
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 DateSelect from "@/components/TempDesignSystem/Form/Date"
|
||||||
import Input from "@/components/TempDesignSystem/Form/Input"
|
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 Caption from "@/components/TempDesignSystem/Text/Caption"
|
||||||
import useLang from "@/hooks/useLang"
|
import useLang from "@/hooks/useLang"
|
||||||
|
|
||||||
@@ -31,67 +25,27 @@ export default function Signup({ name }: { name: string }) {
|
|||||||
setIsJoinChecked(joinValue)
|
setIsJoinChecked(joinValue)
|
||||||
}, [joinValue])
|
}, [joinValue])
|
||||||
|
|
||||||
const list = [
|
return isJoinChecked ? (
|
||||||
{ title: intl.formatMessage({ id: "Earn bonus nights & points" }) },
|
<div className={styles.additionalFormData}>
|
||||||
{ title: intl.formatMessage({ id: "Get member benefits & offers" }) },
|
<Input
|
||||||
{ title: intl.formatMessage({ id: "Join at no cost" }) },
|
name="zipCode"
|
||||||
]
|
label={intl.formatMessage({ id: "Zip code" })}
|
||||||
|
registerOptions={{ required: true }}
|
||||||
return (
|
|
||||||
<div className={styles.container}>
|
|
||||||
<CheckboxCard
|
|
||||||
highlightSubtitle
|
|
||||||
list={list}
|
|
||||||
name={name}
|
|
||||||
subtitle={intl.formatMessage(
|
|
||||||
{
|
|
||||||
id: "{difference}{amount} {currency}",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
amount: "491",
|
|
||||||
currency: "SEK",
|
|
||||||
difference: "-",
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
title={intl.formatMessage({ id: "Join Scandic Friends" })}
|
|
||||||
/>
|
/>
|
||||||
{isJoinChecked ? (
|
<div className={styles.dateField}>
|
||||||
<div className={styles.additionalFormData}>
|
<header>
|
||||||
<div className={styles.dateField}>
|
<Caption type="bold">
|
||||||
<header>
|
{intl.formatMessage({ id: "Birth date" })} *
|
||||||
<Caption type="bold">
|
</Caption>
|
||||||
{intl.formatMessage({ id: "Birth date" })} *
|
</header>
|
||||||
</Caption>
|
<DateSelect name="dateOfBirth" registerOptions={{ required: true }} />
|
||||||
</header>
|
</div>
|
||||||
<DateSelect
|
|
||||||
name="dateOfBirth"
|
|
||||||
registerOptions={{ required: true }}
|
|
||||||
/>
|
|
||||||
<Input
|
|
||||||
name="zipCode"
|
|
||||||
label={intl.formatMessage({ id: "Zip code" })}
|
|
||||||
registerOptions={{ required: true }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<Checkbox name="termsAccepted" registerOptions={{ required: true }}>
|
|
||||||
<Body>
|
|
||||||
{intl.formatMessage({
|
|
||||||
id: "Yes, I accept the Terms and conditions for Scandic Friends and understand that Scandic will process my personal data in accordance with",
|
|
||||||
})}{" "}
|
|
||||||
<Link
|
|
||||||
variant="underscored"
|
|
||||||
color="peach80"
|
|
||||||
target="_blank"
|
|
||||||
href={privacyPolicy[lang]}
|
|
||||||
>
|
|
||||||
{intl.formatMessage({ id: "Scandic's Privacy Policy." })}
|
|
||||||
</Link>
|
|
||||||
</Body>
|
|
||||||
</Checkbox>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
</div>
|
</div>
|
||||||
|
) : (
|
||||||
|
<Input
|
||||||
|
label={intl.formatMessage({ id: "Membership no" })}
|
||||||
|
name="membershipNo"
|
||||||
|
type="tel"
|
||||||
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,25 +1,34 @@
|
|||||||
.form {
|
.form {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: var(--Spacing-x2);
|
gap: var(--Spacing-x3);
|
||||||
}
|
}
|
||||||
|
|
||||||
.container {
|
.container {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: var(--Spacing-x2);
|
gap: var(--Spacing-x2);
|
||||||
grid-template-columns: 1fr 1fr;
|
|
||||||
width: min(100%, 600px);
|
width: min(100%, 600px);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.header,
|
||||||
.country,
|
.country,
|
||||||
.email,
|
.email,
|
||||||
.membershipNo,
|
.signup,
|
||||||
.phone {
|
.phone {
|
||||||
grid-column: 1/-1;
|
grid-column: 1/-1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.footer {
|
.footer {
|
||||||
display: grid;
|
|
||||||
gap: var(--Spacing-x3);
|
|
||||||
justify-items: flex-start;
|
|
||||||
margin-top: var(--Spacing-x1);
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import Input from "@/components/TempDesignSystem/Form/Input"
|
|||||||
import Phone from "@/components/TempDesignSystem/Form/Phone"
|
import Phone from "@/components/TempDesignSystem/Form/Phone"
|
||||||
import Footnote from "@/components/TempDesignSystem/Text/Footnote"
|
import Footnote from "@/components/TempDesignSystem/Text/Footnote"
|
||||||
|
|
||||||
|
import JoinScandicFriendsCard from "./JoinScandicFriendsCard"
|
||||||
import { guestDetailsSchema, signedInDetailsSchema } from "./schema"
|
import { guestDetailsSchema, signedInDetailsSchema } from "./schema"
|
||||||
import Signup from "./Signup"
|
import Signup from "./Signup"
|
||||||
|
|
||||||
@@ -24,7 +25,7 @@ import type {
|
|||||||
} from "@/types/components/hotelReservation/enterDetails/details"
|
} from "@/types/components/hotelReservation/enterDetails/details"
|
||||||
|
|
||||||
const formID = "enter-details"
|
const formID = "enter-details"
|
||||||
export default function Details({ user }: DetailsProps) {
|
export default function Details({ user, memberPrice }: DetailsProps) {
|
||||||
const intl = useIntl()
|
const intl = useIntl()
|
||||||
const initialData = useDetailsStore((state) => ({
|
const initialData = useDetailsStore((state) => ({
|
||||||
countryCode: state.data.countryCode,
|
countryCode: state.data.countryCode,
|
||||||
@@ -35,7 +36,6 @@ export default function Details({ user }: DetailsProps) {
|
|||||||
join: state.data.join,
|
join: state.data.join,
|
||||||
dateOfBirth: state.data.dateOfBirth,
|
dateOfBirth: state.data.dateOfBirth,
|
||||||
zipCode: state.data.zipCode,
|
zipCode: state.data.zipCode,
|
||||||
termsAccepted: state.data.termsAccepted,
|
|
||||||
membershipNo: state.data.membershipNo,
|
membershipNo: state.data.membershipNo,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
@@ -52,7 +52,6 @@ export default function Details({ user }: DetailsProps) {
|
|||||||
join: initialData.join,
|
join: initialData.join,
|
||||||
dateOfBirth: initialData.dateOfBirth,
|
dateOfBirth: initialData.dateOfBirth,
|
||||||
zipCode: initialData.zipCode,
|
zipCode: initialData.zipCode,
|
||||||
termsAccepted: initialData.termsAccepted,
|
|
||||||
membershipNo: initialData.membershipNo,
|
membershipNo: initialData.membershipNo,
|
||||||
},
|
},
|
||||||
criteriaMode: "all",
|
criteriaMode: "all",
|
||||||
@@ -76,15 +75,18 @@ export default function Details({ user }: DetailsProps) {
|
|||||||
id={formID}
|
id={formID}
|
||||||
onSubmit={methods.handleSubmit(onSubmit)}
|
onSubmit={methods.handleSubmit(onSubmit)}
|
||||||
>
|
>
|
||||||
{user ? null : <Signup name="join" />}
|
{user ? null : (
|
||||||
<Footnote
|
<JoinScandicFriendsCard name="join" memberPrice={memberPrice} />
|
||||||
color="uiTextHighContrast"
|
)}
|
||||||
textTransform="uppercase"
|
|
||||||
type="label"
|
|
||||||
>
|
|
||||||
{intl.formatMessage({ id: "Guest information" })}
|
|
||||||
</Footnote>
|
|
||||||
<div className={styles.container}>
|
<div className={styles.container}>
|
||||||
|
<Footnote
|
||||||
|
color="uiTextHighContrast"
|
||||||
|
textTransform="uppercase"
|
||||||
|
type="label"
|
||||||
|
className={styles.header}
|
||||||
|
>
|
||||||
|
{intl.formatMessage({ id: "Guest information" })}
|
||||||
|
</Footnote>
|
||||||
<Input
|
<Input
|
||||||
label={intl.formatMessage({ id: "First name" })}
|
label={intl.formatMessage({ id: "First name" })}
|
||||||
name="firstName"
|
name="firstName"
|
||||||
@@ -118,13 +120,10 @@ export default function Details({ user }: DetailsProps) {
|
|||||||
readOnly={!!user}
|
readOnly={!!user}
|
||||||
registerOptions={{ required: true }}
|
registerOptions={{ required: true }}
|
||||||
/>
|
/>
|
||||||
{user || methods.watch("join") ? null : (
|
{user ? null : (
|
||||||
<Input
|
<div className={styles.signup}>
|
||||||
className={styles.membershipNo}
|
<Signup name="join" />
|
||||||
label={intl.formatMessage({ id: "Membership no" })}
|
</div>
|
||||||
name="membershipNo"
|
|
||||||
type="tel"
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<footer className={styles.footer}>
|
<footer className={styles.footer}>
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ export const notJoinDetailsSchema = baseDetailsSchema.merge(
|
|||||||
join: z.literal<boolean>(false),
|
join: z.literal<boolean>(false),
|
||||||
zipCode: z.string().optional(),
|
zipCode: z.string().optional(),
|
||||||
dateOfBirth: z.string().optional(),
|
dateOfBirth: z.string().optional(),
|
||||||
termsAccepted: z.boolean().default(false),
|
|
||||||
membershipNo: z
|
membershipNo: z
|
||||||
.string()
|
.string()
|
||||||
.optional()
|
.optional()
|
||||||
@@ -39,15 +38,6 @@ export const joinDetailsSchema = baseDetailsSchema.merge(
|
|||||||
join: z.literal<boolean>(true),
|
join: z.literal<boolean>(true),
|
||||||
zipCode: z.string().min(1, { message: "Zip code is required" }),
|
zipCode: z.string().min(1, { message: "Zip code is required" }),
|
||||||
dateOfBirth: z.string().min(1, { message: "Date of birth is required" }),
|
dateOfBirth: z.string().min(1, { message: "Date of birth is required" }),
|
||||||
termsAccepted: z.literal<boolean>(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(),
|
membershipNo: z.string().optional(),
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ export default function SectionAccordion({
|
|||||||
const textColor =
|
const textColor =
|
||||||
isComplete || isOpen ? "uiTextHighContrast" : "baseTextDisabled"
|
isComplete || isOpen ? "uiTextHighContrast" : "baseTextDisabled"
|
||||||
return (
|
return (
|
||||||
<section className={styles.wrapper} data-open={isOpen} data-step={step}>
|
<div className={styles.accordion} data-open={isOpen} data-step={step}>
|
||||||
<div className={styles.iconWrapper}>
|
<div className={styles.iconWrapper}>
|
||||||
<div className={styles.circle} data-checked={isComplete}>
|
<div className={styles.circle} data-checked={isComplete}>
|
||||||
{isComplete ? (
|
{isComplete ? (
|
||||||
@@ -74,31 +74,33 @@ export default function SectionAccordion({
|
|||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.main}>
|
<header className={styles.header}>
|
||||||
<header>
|
<button
|
||||||
<button onClick={onModify} className={styles.modifyButton}>
|
onClick={onModify}
|
||||||
<Footnote
|
disabled={!isComplete}
|
||||||
className={styles.title}
|
className={styles.modifyButton}
|
||||||
asChild
|
>
|
||||||
textTransform="uppercase"
|
<Footnote
|
||||||
type="label"
|
className={styles.title}
|
||||||
color={textColor}
|
asChild
|
||||||
>
|
textTransform="uppercase"
|
||||||
<h2>{header}</h2>
|
type="label"
|
||||||
</Footnote>
|
color={textColor}
|
||||||
<Subtitle className={styles.selection} type="two" color={textColor}>
|
>
|
||||||
{title}
|
<h2>{header}</h2>
|
||||||
</Subtitle>
|
</Footnote>
|
||||||
|
<Subtitle className={styles.selection} type="two" color={textColor}>
|
||||||
|
{title}
|
||||||
|
</Subtitle>
|
||||||
|
|
||||||
{isComplete && !isOpen && (
|
{isComplete && !isOpen && (
|
||||||
<ChevronDownIcon className={styles.button} color="burgundy" />
|
<ChevronDownIcon className={styles.button} color="burgundy" />
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
</header>
|
</header>
|
||||||
<div className={styles.content}>
|
<div className={styles.content}>
|
||||||
<div className={styles.contentWrapper}>{children}</div>
|
<div className={styles.contentWrapper}>{children}</div>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,28 @@
|
|||||||
.wrapper {
|
.accordion {
|
||||||
position: relative;
|
--header-height: 2.4em;
|
||||||
display: flex;
|
--circle-height: 24px;
|
||||||
flex-direction: row;
|
|
||||||
gap: var(--Spacing-x-one-and-half);
|
gap: var(--Spacing-x3);
|
||||||
|
width: 100%;
|
||||||
padding-top: var(--Spacing-x3);
|
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;
|
border-bottom: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
grid-area: header;
|
||||||
|
}
|
||||||
|
|
||||||
.modifyButton {
|
.modifyButton {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-areas: "title button" "selection button";
|
grid-template-areas: "title button" "selection button";
|
||||||
@@ -17,6 +30,11 @@
|
|||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
border: none;
|
border: none;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modifyButton:disabled {
|
||||||
|
cursor: default;
|
||||||
}
|
}
|
||||||
|
|
||||||
.title {
|
.title {
|
||||||
@@ -29,15 +47,6 @@
|
|||||||
justify-self: flex-end;
|
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 {
|
.selection {
|
||||||
font-weight: 450;
|
font-weight: 450;
|
||||||
font-size: var(--typography-Title-4-fontSize);
|
font-size: var(--typography-Title-4-fontSize);
|
||||||
@@ -46,11 +55,12 @@
|
|||||||
|
|
||||||
.iconWrapper {
|
.iconWrapper {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
grid-area: circle;
|
||||||
}
|
}
|
||||||
|
|
||||||
.circle {
|
.circle {
|
||||||
width: 24px;
|
width: var(--circle-height);
|
||||||
height: 24px;
|
height: var(--circle-height);
|
||||||
border-radius: 100px;
|
border-radius: 100px;
|
||||||
transition: background-color 0.4s;
|
transition: background-color 0.4s;
|
||||||
border: 2px solid var(--Base-Border-Inverted);
|
border: 2px solid var(--Base-Border-Inverted);
|
||||||
@@ -63,42 +73,45 @@
|
|||||||
background-color: var(--UI-Input-Controls-Fill-Selected);
|
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);
|
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);
|
background-color: var(--Base-Surface-Subtle-Hover);
|
||||||
}
|
}
|
||||||
|
|
||||||
.wrapper[data-open="true"] .main {
|
.accordion[data-open="true"] {
|
||||||
grid-template-rows: 2em 1fr;
|
grid-template-rows: var(--header-height) 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contentWrapper {
|
||||||
|
padding-bottom: var(--Spacing-x3);
|
||||||
}
|
}
|
||||||
|
|
||||||
.content {
|
.content {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
grid-area: content;
|
||||||
|
border-bottom: 1px solid var(--Primary-Light-On-Surface-Divider-subtle);
|
||||||
}
|
}
|
||||||
|
|
||||||
.contentWrapper {
|
@media screen and (min-width: 768px) {
|
||||||
padding-top: var(--Spacing-x3);
|
.accordion {
|
||||||
}
|
column-gap: var(--Spacing-x3);
|
||||||
|
grid-template-areas: "circle header" "circle content";
|
||||||
@media screen and (min-width: 1367px) {
|
|
||||||
.wrapper {
|
|
||||||
gap: var(--Spacing-x3);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.iconWrapper {
|
.iconWrapper {
|
||||||
top: var(--Spacing-x1);
|
top: var(--Spacing-x1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.wrapper:not(:last-child)::after {
|
.accordion:not(:last-child) .iconWrapper::after {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 12px;
|
left: 12px;
|
||||||
bottom: 0;
|
bottom: calc(0px - var(--Spacing-x7));
|
||||||
top: var(--Spacing-x7);
|
top: var(--circle-height);
|
||||||
height: 100%;
|
|
||||||
content: "";
|
content: "";
|
||||||
border-left: 1px solid var(--Primary-Light-On-Surface-Divider-subtle);
|
border-left: 1px solid var(--Primary-Light-On-Surface-Divider-subtle);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -63,7 +63,7 @@
|
|||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (min-width: 1367px) {
|
@media screen and (min-width: 768px) {
|
||||||
.wrapper {
|
.wrapper {
|
||||||
gap: var(--Spacing-x3);
|
gap: var(--Spacing-x3);
|
||||||
padding-top: var(--Spacing-x3);
|
padding-top: var(--Spacing-x3);
|
||||||
|
|||||||
@@ -81,7 +81,6 @@ export default function Summary({ showMemberPrice, room }: SummaryProps) {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setChosenBed(bedType)
|
setChosenBed(bedType)
|
||||||
setChosenBreakfast(breakfast)
|
|
||||||
|
|
||||||
if (breakfast || breakfast === false) {
|
if (breakfast || breakfast === false) {
|
||||||
setChosenBreakfast(breakfast)
|
setChosenBreakfast(breakfast)
|
||||||
@@ -94,9 +93,9 @@ export default function Summary({ showMemberPrice, room }: SummaryProps) {
|
|||||||
euro:
|
euro:
|
||||||
room.euroPrice && roomsPriceEuro
|
room.euroPrice && roomsPriceEuro
|
||||||
? {
|
? {
|
||||||
price: roomsPriceEuro,
|
price: roomsPriceEuro,
|
||||||
currency: room.euroPrice.currency,
|
currency: room.euroPrice.currency,
|
||||||
}
|
}
|
||||||
: undefined,
|
: undefined,
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
@@ -108,11 +107,11 @@ export default function Summary({ showMemberPrice, room }: SummaryProps) {
|
|||||||
euro:
|
euro:
|
||||||
room.euroPrice && roomsPriceEuro
|
room.euroPrice && roomsPriceEuro
|
||||||
? {
|
? {
|
||||||
price:
|
price:
|
||||||
roomsPriceEuro +
|
roomsPriceEuro +
|
||||||
parseInt(breakfast.requestedPrice.totalPrice),
|
parseInt(breakfast.requestedPrice.totalPrice),
|
||||||
currency: room.euroPrice.currency,
|
currency: room.euroPrice.currency,
|
||||||
}
|
}
|
||||||
: undefined,
|
: undefined,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -199,24 +198,24 @@ export default function Summary({ showMemberPrice, room }: SummaryProps) {
|
|||||||
</div>
|
</div>
|
||||||
{room.packages
|
{room.packages
|
||||||
? room.packages.map((roomPackage) => (
|
? room.packages.map((roomPackage) => (
|
||||||
<div className={styles.entry} key={roomPackage.code}>
|
<div className={styles.entry} key={roomPackage.code}>
|
||||||
<div>
|
<div>
|
||||||
<Body color="uiTextHighContrast">
|
<Body color="uiTextHighContrast">
|
||||||
{roomPackage.description}
|
{roomPackage.description}
|
||||||
</Body>
|
</Body>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Caption color="uiTextHighContrast">
|
<Caption color="uiTextHighContrast">
|
||||||
{intl.formatMessage(
|
{intl.formatMessage(
|
||||||
{ id: "{amount} {currency}" },
|
{ id: "{amount} {currency}" },
|
||||||
{
|
{
|
||||||
amount: roomPackage.localPrice.price,
|
amount: roomPackage.localPrice.price,
|
||||||
currency: roomPackage.localPrice.currency,
|
currency: roomPackage.localPrice.currency,
|
||||||
}
|
}
|
||||||
)}
|
)}
|
||||||
</Caption>
|
</Caption>
|
||||||
</div>
|
</div>
|
||||||
))
|
))
|
||||||
: null}
|
: null}
|
||||||
{chosenBed ? (
|
{chosenBed ? (
|
||||||
<div className={styles.entry}>
|
<div className={styles.entry}>
|
||||||
@@ -263,9 +262,8 @@ export default function Summary({ showMemberPrice, room }: SummaryProps) {
|
|||||||
)}
|
)}
|
||||||
</Caption>
|
</Caption>
|
||||||
</div>
|
</div>
|
||||||
) : null
|
) : null}
|
||||||
}
|
</div>
|
||||||
</div >
|
|
||||||
<Divider color="primaryLightSubtle" />
|
<Divider color="primaryLightSubtle" />
|
||||||
<div className={styles.total}>
|
<div className={styles.total}>
|
||||||
<div className={styles.entry}>
|
<div className={styles.entry}>
|
||||||
@@ -306,6 +304,6 @@ export default function Summary({ showMemberPrice, room }: SummaryProps) {
|
|||||||
</div>
|
</div>
|
||||||
<Divider className={styles.bottomDivider} color="primaryLightSubtle" />
|
<Divider className={styles.bottomDivider} color="primaryLightSubtle" />
|
||||||
</div>
|
</div>
|
||||||
</section >
|
</section>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,6 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: var(--Spacing-x-one-and-half);
|
gap: var(--Spacing-x-one-and-half);
|
||||||
max-width: 260px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.divider {
|
.divider {
|
||||||
@@ -38,3 +37,9 @@
|
|||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
font-size: var(--typography-Caption-Regular-fontSize);
|
font-size: var(--typography-Caption-Regular-fontSize);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media screen and (min-width: 1367px) {
|
||||||
|
.prices {
|
||||||
|
max-width: 260px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import { selectHotelMap, selectRate } from "@/constants/routes/hotelReservation"
|
|||||||
|
|
||||||
import { mapFacilityToIcon } from "@/components/ContentType/HotelPage/data"
|
import { mapFacilityToIcon } from "@/components/ContentType/HotelPage/data"
|
||||||
import ImageGallery from "@/components/ImageGallery"
|
import ImageGallery from "@/components/ImageGallery"
|
||||||
import Alert from "@/components/TempDesignSystem/Alert"
|
|
||||||
import Button from "@/components/TempDesignSystem/Button"
|
import Button from "@/components/TempDesignSystem/Button"
|
||||||
import Divider from "@/components/TempDesignSystem/Divider"
|
import Divider from "@/components/TempDesignSystem/Divider"
|
||||||
import Link from "@/components/TempDesignSystem/Link"
|
import Link from "@/components/TempDesignSystem/Link"
|
||||||
@@ -133,13 +132,6 @@ export default function HotelCard({
|
|||||||
hotel={hotelData}
|
hotel={hotelData}
|
||||||
showCTA={true}
|
showCTA={true}
|
||||||
/>
|
/>
|
||||||
{hotelData.specialAlerts.length > 0 && (
|
|
||||||
<div className={styles.specialAlerts}>
|
|
||||||
{hotelData.specialAlerts.map((alert) => (
|
|
||||||
<Alert key={alert.id} type={alert.type} text={alert.text} />
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</section>
|
</section>
|
||||||
<HotelPriceList price={price} hotelId={hotel.hotelData.operaId} />
|
<HotelPriceList price={price} hotelId={hotel.hotelData.operaId} />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
"use client"
|
"use client"
|
||||||
import { useSearchParams } from "next/navigation"
|
import { useSearchParams } from "next/navigation"
|
||||||
import { useMemo } from "react"
|
import { useEffect, useMemo, useState } from "react"
|
||||||
|
|
||||||
import { useHotelFilterStore } from "@/stores/hotel-filters"
|
import { useHotelFilterStore } from "@/stores/hotel-filters"
|
||||||
|
|
||||||
|
import { BackToTopButton } from "@/components/TempDesignSystem/BackToTopButton"
|
||||||
|
|
||||||
import HotelCard from "../HotelCard"
|
import HotelCard from "../HotelCard"
|
||||||
import { DEFAULT_SORT } from "../SelectHotel/HotelSorter"
|
import { DEFAULT_SORT } from "../SelectHotel/HotelSorter"
|
||||||
|
|
||||||
@@ -25,6 +27,7 @@ export default function HotelCardListing({
|
|||||||
const searchParams = useSearchParams()
|
const searchParams = useSearchParams()
|
||||||
const activeFilters = useHotelFilterStore((state) => state.activeFilters)
|
const activeFilters = useHotelFilterStore((state) => state.activeFilters)
|
||||||
const setResultCount = useHotelFilterStore((state) => state.setResultCount)
|
const setResultCount = useHotelFilterStore((state) => state.setResultCount)
|
||||||
|
const [showBackToTop, setShowBackToTop] = useState<boolean>(false)
|
||||||
|
|
||||||
const sortBy = useMemo(
|
const sortBy = useMemo(
|
||||||
() => searchParams.get("sort") ?? DEFAULT_SORT,
|
() => searchParams.get("sort") ?? DEFAULT_SORT,
|
||||||
@@ -48,7 +51,7 @@ export default function HotelCardListing({
|
|||||||
return (
|
return (
|
||||||
hotel.price?.member?.localPrice?.pricePerNight ??
|
hotel.price?.member?.localPrice?.pricePerNight ??
|
||||||
hotel.price?.public?.localPrice?.pricePerNight ??
|
hotel.price?.public?.localPrice?.pricePerNight ??
|
||||||
0
|
Infinity
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
return [...hotelData].sort(
|
return [...hotelData].sort(
|
||||||
@@ -82,6 +85,20 @@ export default function HotelCardListing({
|
|||||||
return filteredHotels
|
return filteredHotels
|
||||||
}, [activeFilters, sortedHotels, setResultCount])
|
}, [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 (
|
return (
|
||||||
<section className={styles.hotelCards}>
|
<section className={styles.hotelCards}>
|
||||||
{hotels?.length
|
{hotels?.length
|
||||||
@@ -95,6 +112,7 @@ export default function HotelCardListing({
|
|||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
: null}
|
: null}
|
||||||
|
{showBackToTop && <BackToTopButton onClick={scrollToTop} />}
|
||||||
</section>
|
</section>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -81,6 +81,10 @@
|
|||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
.close {
|
.close {
|
||||||
background: none;
|
background: none;
|
||||||
border: none;
|
border: none;
|
||||||
@@ -97,3 +101,80 @@
|
|||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
border-top: 1px solid var(--Base-Border-Subtle);
|
border-top: 1px solid var(--Base-Border-Subtle);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media screen and (min-width: 768px) {
|
||||||
|
.modal {
|
||||||
|
left: 50%;
|
||||||
|
bottom: 50%;
|
||||||
|
height: min(80dvh, 680px);
|
||||||
|
width: min(80dvw, 960px);
|
||||||
|
translate: -50% 50%;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: auto 1fr;
|
||||||
|
padding: var(--Spacing-x2) var(--Spacing-x3);
|
||||||
|
align-items: center;
|
||||||
|
border-bottom: 1px solid var(--Base-Border-Subtle);
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
background: var(--Base-Surface-Primary-light-Normal);
|
||||||
|
z-index: 1;
|
||||||
|
border-top-left-radius: var(--Corner-radius-large);
|
||||||
|
border-top-right-radius: var(--Corner-radius-large);
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
gap: var(--Spacing-x4);
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filters {
|
||||||
|
overflow-y: unset;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sorter,
|
||||||
|
.filters,
|
||||||
|
.footer,
|
||||||
|
.divider {
|
||||||
|
padding: 0 var(--Spacing-x3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
flex-direction: row-reverse;
|
||||||
|
justify-content: space-between;
|
||||||
|
position: sticky;
|
||||||
|
bottom: 0;
|
||||||
|
background: var(--Base-Surface-Primary-light-Normal);
|
||||||
|
z-index: 1;
|
||||||
|
border-bottom-left-radius: var(--Corner-radius-large);
|
||||||
|
border-bottom-right-radius: var(--Corner-radius-large);
|
||||||
|
padding: var(--Spacing-x2) var(--Spacing-x3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.filters aside h1 {
|
||||||
|
margin-bottom: var(--Spacing-x2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.filters aside > div:last-child {
|
||||||
|
margin-top: var(--Spacing-x4);
|
||||||
|
padding-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filters aside ul {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
margin-top: var(--Spacing-x3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@media screen and (min-width: 1024) {
|
||||||
|
.facilities ul {
|
||||||
|
grid-template-columns: 1fr 1fr 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ import { useHotelFilterStore } from "@/stores/hotel-filters"
|
|||||||
|
|
||||||
import { CloseLargeIcon, FilterIcon } from "@/components/Icons"
|
import { CloseLargeIcon, FilterIcon } from "@/components/Icons"
|
||||||
import Button from "@/components/TempDesignSystem/Button"
|
import Button from "@/components/TempDesignSystem/Button"
|
||||||
|
import Divider from "@/components/TempDesignSystem/Divider"
|
||||||
|
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
|
||||||
|
|
||||||
import HotelFilter from "../HotelFilter"
|
import HotelFilter from "../HotelFilter"
|
||||||
import HotelSorter from "../HotelSorter"
|
import HotelSorter from "../HotelSorter"
|
||||||
@@ -47,10 +49,18 @@ export default function FilterAndSortModal({
|
|||||||
>
|
>
|
||||||
<CloseLargeIcon />
|
<CloseLargeIcon />
|
||||||
</button>
|
</button>
|
||||||
|
<Subtitle
|
||||||
|
type="two"
|
||||||
|
textAlign="center"
|
||||||
|
className={styles.title}
|
||||||
|
>
|
||||||
|
{intl.formatMessage({ id: "Filter and sort" })}
|
||||||
|
</Subtitle>
|
||||||
</header>
|
</header>
|
||||||
<div className={styles.sorter}>
|
<div className={styles.sorter}>
|
||||||
<HotelSorter />
|
<HotelSorter />
|
||||||
</div>
|
</div>
|
||||||
|
<Divider color="subtle" className="divider" />
|
||||||
<div className={styles.filters}>
|
<div className={styles.filters}>
|
||||||
<HotelFilter filters={filters} />
|
<HotelFilter filters={filters} />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
22
components/HotelReservation/SelectHotel/HotelCount/index.tsx
Normal file
22
components/HotelReservation/SelectHotel/HotelCount/index.tsx
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
"use client"
|
||||||
|
import { useIntl } from "react-intl"
|
||||||
|
|
||||||
|
import { useHotelFilterStore } from "@/stores/hotel-filters"
|
||||||
|
|
||||||
|
import Preamble from "@/components/TempDesignSystem/Text/Preamble"
|
||||||
|
|
||||||
|
export default function HotelCount() {
|
||||||
|
const intl = useIntl()
|
||||||
|
const resultCount = useHotelFilterStore((state) => state.resultCount)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Preamble>
|
||||||
|
{intl.formatMessage(
|
||||||
|
{
|
||||||
|
id: "Hotel(s)",
|
||||||
|
},
|
||||||
|
{ amount: resultCount }
|
||||||
|
)}
|
||||||
|
</Preamble>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -7,11 +7,13 @@ import { useMediaQuery } from "usehooks-ts"
|
|||||||
|
|
||||||
import { selectHotel } from "@/constants/routes/hotelReservation"
|
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 InteractiveMap from "@/components/Maps/InteractiveMap"
|
||||||
|
import { BackToTopButton } from "@/components/TempDesignSystem/BackToTopButton"
|
||||||
import Button from "@/components/TempDesignSystem/Button"
|
import Button from "@/components/TempDesignSystem/Button"
|
||||||
import useLang from "@/hooks/useLang"
|
import useLang from "@/hooks/useLang"
|
||||||
|
|
||||||
|
import FilterAndSortModal from "../FilterAndSortModal"
|
||||||
import HotelListing from "./HotelListing"
|
import HotelListing from "./HotelListing"
|
||||||
import { getCentralCoordinates } from "./utils"
|
import { getCentralCoordinates } from "./utils"
|
||||||
|
|
||||||
@@ -24,6 +26,7 @@ export default function SelectHotelMap({
|
|||||||
hotelPins,
|
hotelPins,
|
||||||
mapId,
|
mapId,
|
||||||
hotels,
|
hotels,
|
||||||
|
filterList,
|
||||||
}: SelectHotelMapProps) {
|
}: SelectHotelMapProps) {
|
||||||
const searchParams = useSearchParams()
|
const searchParams = useSearchParams()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
@@ -101,25 +104,14 @@ export default function SelectHotelMap({
|
|||||||
>
|
>
|
||||||
<CloseLargeIcon />
|
<CloseLargeIcon />
|
||||||
</Button>
|
</Button>
|
||||||
<span>Filter and sort</span>
|
<FilterAndSortModal filters={filterList} />
|
||||||
{/* TODO: Add filter and sort button */}
|
|
||||||
</div>
|
</div>
|
||||||
<HotelListing
|
<HotelListing
|
||||||
hotels={hotels}
|
hotels={hotels}
|
||||||
activeHotelPin={activeHotelPin}
|
activeHotelPin={activeHotelPin}
|
||||||
setActiveHotelPin={setActiveHotelPin}
|
setActiveHotelPin={setActiveHotelPin}
|
||||||
/>
|
/>
|
||||||
{showBackToTop && (
|
{showBackToTop && <BackToTopButton onClick={scrollToTop} />}
|
||||||
<Button
|
|
||||||
intent="inverted"
|
|
||||||
size="small"
|
|
||||||
theme="base"
|
|
||||||
className={styles.backToTopButton}
|
|
||||||
onClick={scrollToTop}
|
|
||||||
>
|
|
||||||
{intl.formatMessage({ id: "Back to top" })}
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
<InteractiveMap
|
<InteractiveMap
|
||||||
closeButton={closeButton}
|
closeButton={closeButton}
|
||||||
|
|||||||
@@ -23,10 +23,6 @@
|
|||||||
height: 44px;
|
height: 44px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.backToTopButton {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 768px) {
|
@media (min-width: 768px) {
|
||||||
.container .closeButton {
|
.container .closeButton {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -34,12 +30,7 @@
|
|||||||
.container .listingContainer .filterContainer .filterContainerCloseButton {
|
.container .listingContainer .filterContainer .filterContainerCloseButton {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
.backToTopButton {
|
|
||||||
position: fixed;
|
|
||||||
bottom: 24px;
|
|
||||||
left: 32px;
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
.listingContainer {
|
.listingContainer {
|
||||||
background-color: var(--Base-Surface-Secondary-light-Normal);
|
background-color: var(--Base-Surface-Secondary-light-Normal);
|
||||||
padding: var(--Spacing-x3) var(--Spacing-x4);
|
padding: var(--Spacing-x3) var(--Spacing-x4);
|
||||||
@@ -50,4 +41,9 @@
|
|||||||
.container {
|
.container {
|
||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.filterContainer {
|
||||||
|
justify-content: flex-end;
|
||||||
|
padding: 0 0 var(--Spacing-x1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,9 +54,22 @@ export default function RoomCard({
|
|||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
function getPriceInformationForRate(rate: RateDefinition | undefined) {
|
function getRateDefinitionForRate(rate: RateDefinition | undefined) {
|
||||||
return rateDefinitions.find((def) => def.rateCode === rate?.rateCode)
|
return rateDefinitions.find((def) => 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 =
|
const petRoomPackage =
|
||||||
@@ -69,7 +82,6 @@ export default function RoomCard({
|
|||||||
)
|
)
|
||||||
|
|
||||||
const { roomSize, occupancy, images } = selectedRoom || {}
|
const { roomSize, occupancy, images } = selectedRoom || {}
|
||||||
const mainImage = images?.[0]
|
|
||||||
|
|
||||||
const freeCancelation = intl.formatMessage({ id: "Free cancellation" })
|
const freeCancelation = intl.formatMessage({ id: "Free cancellation" })
|
||||||
const nonRefundable = intl.formatMessage({ id: "Non-refundable" })
|
const nonRefundable = intl.formatMessage({ id: "Non-refundable" })
|
||||||
@@ -101,53 +113,56 @@ export default function RoomCard({
|
|||||||
return (
|
return (
|
||||||
<div className={classNames}>
|
<div className={classNames}>
|
||||||
<div>
|
<div>
|
||||||
{mainImage && (
|
<div className={styles.imageContainer}>
|
||||||
<div className={styles.imageContainer}>
|
<div className={styles.chipContainer}>
|
||||||
<div className={styles.chipContainer}>
|
{roomConfiguration.roomsLeft < 5 && (
|
||||||
{roomConfiguration.roomsLeft < 5 && (
|
<span className={styles.chip}>
|
||||||
<span className={styles.chip}>
|
<Footnote
|
||||||
<Footnote
|
color="burgundy"
|
||||||
color="burgundy"
|
textTransform="uppercase"
|
||||||
textTransform="uppercase"
|
>{`${roomConfiguration.roomsLeft} ${intl.formatMessage({ id: "Left" })}`}</Footnote>
|
||||||
>{`${roomConfiguration.roomsLeft} ${intl.formatMessage({ id: "Left" })}`}</Footnote>
|
</span>
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
{roomConfiguration.features
|
|
||||||
.filter((feature) => selectedPackages.includes(feature.code))
|
|
||||||
.map((feature) => (
|
|
||||||
<span className={styles.chip} key={feature.code}>
|
|
||||||
{createElement(getIconForFeatureCode(feature.code), {
|
|
||||||
width: 16,
|
|
||||||
height: 16,
|
|
||||||
color: "burgundy",
|
|
||||||
})}
|
|
||||||
</span>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
{/*NOTE: images from the test API are hosted on test3.scandichotels.com,
|
|
||||||
which can't be accessed unless on Scandic's Wifi or using Citrix. */}
|
|
||||||
<ImageGallery
|
|
||||||
images={images}
|
|
||||||
title={roomConfiguration.roomType}
|
|
||||||
fill
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div className={styles.specification}>
|
|
||||||
<Caption color="uiTextMediumContrast" className={styles.guests}>
|
|
||||||
{intl.formatMessage(
|
|
||||||
{
|
|
||||||
id: "booking.guests",
|
|
||||||
},
|
|
||||||
{ nrOfGuests: occupancy?.total }
|
|
||||||
)}
|
)}
|
||||||
</Caption>
|
{roomConfiguration.features
|
||||||
<Caption color="uiTextMediumContrast">
|
.filter((feature) => selectedPackages.includes(feature.code))
|
||||||
{roomSize?.min === roomSize?.max
|
.map((feature) => (
|
||||||
? roomSize?.min
|
<span className={styles.chip} key={feature.code}>
|
||||||
: `${roomSize?.min}-${roomSize?.max}`}
|
{createElement(getIconForFeatureCode(feature.code), {
|
||||||
m²
|
width: 16,
|
||||||
</Caption>
|
height: 16,
|
||||||
|
color: "burgundy",
|
||||||
|
})}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
{/*NOTE: images from the test API are hosted on test3.scandichotels.com,
|
||||||
|
which can't be accessed unless on Scandic's Wifi or using Citrix. */}
|
||||||
|
<ImageGallery
|
||||||
|
images={images}
|
||||||
|
title={roomConfiguration.roomType}
|
||||||
|
fill
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.specification}>
|
||||||
|
{occupancy && (
|
||||||
|
<Caption color="uiTextMediumContrast" className={styles.guests}>
|
||||||
|
{intl.formatMessage(
|
||||||
|
{
|
||||||
|
id: "booking.guests",
|
||||||
|
},
|
||||||
|
{ nrOfGuests: occupancy?.total }
|
||||||
|
)}
|
||||||
|
</Caption>
|
||||||
|
)}
|
||||||
|
{roomSize && (
|
||||||
|
<Caption color="uiTextMediumContrast">
|
||||||
|
{roomSize.min === roomSize.max
|
||||||
|
? roomSize.min
|
||||||
|
: `${roomSize.min}-${roomSize.max}`}
|
||||||
|
m²
|
||||||
|
</Caption>
|
||||||
|
)}
|
||||||
<div className={styles.toggleSidePeek}>
|
<div className={styles.toggleSidePeek}>
|
||||||
{roomConfiguration.roomTypeCode && (
|
{roomConfiguration.roomTypeCode && (
|
||||||
<ToggleSidePeek
|
<ToggleSidePeek
|
||||||
@@ -168,9 +183,7 @@ export default function RoomCard({
|
|||||||
</div>
|
</div>
|
||||||
<div className={styles.container}>
|
<div className={styles.container}>
|
||||||
<Caption color="uiTextHighContrast" type="bold">
|
<Caption color="uiTextHighContrast" type="bold">
|
||||||
{intl.formatMessage({
|
{getBreakfastMessage(rates.flexRate)}
|
||||||
id: "Breakfast selection in next step.",
|
|
||||||
})}
|
|
||||||
</Caption>
|
</Caption>
|
||||||
{roomConfiguration.status === "NotAvailable" ? (
|
{roomConfiguration.status === "NotAvailable" ? (
|
||||||
<div className={styles.noRoomsContainer}>
|
<div className={styles.noRoomsContainer}>
|
||||||
@@ -192,7 +205,7 @@ export default function RoomCard({
|
|||||||
value={key.toLowerCase()}
|
value={key.toLowerCase()}
|
||||||
paymentTerm={key === "flexRate" ? payLater : payNow}
|
paymentTerm={key === "flexRate" ? payLater : payNow}
|
||||||
product={findProductForRate(rate)}
|
product={findProductForRate(rate)}
|
||||||
priceInformation={getPriceInformationForRate(rate)}
|
priceInformation={getRateDefinitionForRate(rate)?.generalTerms}
|
||||||
handleSelectRate={handleSelectRate}
|
handleSelectRate={handleSelectRate}
|
||||||
roomType={roomConfiguration.roomType}
|
roomType={roomConfiguration.roomType}
|
||||||
roomTypeCode={roomConfiguration.roomTypeCode}
|
roomTypeCode={roomConfiguration.roomTypeCode}
|
||||||
|
|||||||
@@ -7,23 +7,13 @@
|
|||||||
border: 1px solid var(--Base-Border-Subtle);
|
border: 1px solid var(--Base-Border-Subtle);
|
||||||
position: relative;
|
position: relative;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
min-height: 730px;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card.noAvailability {
|
.card.noAvailability {
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
}
|
opacity: 0.6;
|
||||||
|
|
||||||
.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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.specification {
|
.specification {
|
||||||
|
|||||||
@@ -31,7 +31,10 @@ export function filterDuplicateRoomTypesByLowestPrice(
|
|||||||
|
|
||||||
products.forEach((product) => {
|
products.forEach((product) => {
|
||||||
const { productType } = product
|
const { productType } = product
|
||||||
const publicProduct = productType.public
|
const publicProduct = productType.public || {
|
||||||
|
requestedPrice: null,
|
||||||
|
localPrice: null,
|
||||||
|
}
|
||||||
const memberProduct = productType.member || {
|
const memberProduct = productType.member || {
|
||||||
requestedPrice: null,
|
requestedPrice: null,
|
||||||
localPrice: null,
|
localPrice: null,
|
||||||
@@ -53,7 +56,7 @@ export function filterDuplicateRoomTypesByLowestPrice(
|
|||||||
Number(memberRequestedPrice?.pricePerNight) ?? Infinity
|
Number(memberRequestedPrice?.pricePerNight) ?? Infinity
|
||||||
)
|
)
|
||||||
const currentLocalPrice = Math.min(
|
const currentLocalPrice = Math.min(
|
||||||
Number(publicLocalPrice.pricePerNight) ?? Infinity,
|
Number(publicLocalPrice?.pricePerNight) ?? Infinity,
|
||||||
Number(memberLocalPrice?.pricePerNight) ?? Infinity
|
Number(memberLocalPrice?.pricePerNight) ?? Infinity
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -63,7 +66,7 @@ export function filterDuplicateRoomTypesByLowestPrice(
|
|||||||
Math.min(
|
Math.min(
|
||||||
Number(
|
Number(
|
||||||
previousLowest.products[0].productType.public.requestedPrice
|
previousLowest.products[0].productType.public.requestedPrice
|
||||||
.pricePerNight
|
?.pricePerNight
|
||||||
) ?? Infinity,
|
) ?? Infinity,
|
||||||
Number(
|
Number(
|
||||||
previousLowest.products[0].productType.member?.requestedPrice
|
previousLowest.products[0].productType.member?.requestedPrice
|
||||||
@@ -74,7 +77,7 @@ export function filterDuplicateRoomTypesByLowestPrice(
|
|||||||
Math.min(
|
Math.min(
|
||||||
Number(
|
Number(
|
||||||
previousLowest.products[0].productType.public.requestedPrice
|
previousLowest.products[0].productType.public.requestedPrice
|
||||||
.pricePerNight
|
?.pricePerNight
|
||||||
) ?? Infinity,
|
) ?? Infinity,
|
||||||
Number(
|
Number(
|
||||||
previousLowest.products[0].productType.member?.requestedPrice
|
previousLowest.products[0].productType.member?.requestedPrice
|
||||||
@@ -85,7 +88,7 @@ export function filterDuplicateRoomTypesByLowestPrice(
|
|||||||
Math.min(
|
Math.min(
|
||||||
Number(
|
Number(
|
||||||
previousLowest.products[0].productType.public.localPrice
|
previousLowest.products[0].productType.public.localPrice
|
||||||
.pricePerNight
|
?.pricePerNight
|
||||||
) ?? Infinity,
|
) ?? Infinity,
|
||||||
Number(
|
Number(
|
||||||
previousLowest.products[0].productType.member?.localPrice
|
previousLowest.products[0].productType.member?.localPrice
|
||||||
|
|||||||
33
components/Icons/ArrowUp.tsx
Normal file
33
components/Icons/ArrowUp.tsx
Normal file
@@ -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 (
|
||||||
|
<svg
|
||||||
|
className={classNames}
|
||||||
|
fill="none"
|
||||||
|
height="20"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
width="20"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<mask
|
||||||
|
id="a"
|
||||||
|
width="20"
|
||||||
|
height="20"
|
||||||
|
x="0"
|
||||||
|
y="0"
|
||||||
|
maskUnits="userSpaceOnUse"
|
||||||
|
>
|
||||||
|
<path fill="#D9D9D9" d="M0 0h20v20H0z" />
|
||||||
|
</mask>
|
||||||
|
<path
|
||||||
|
fill="#4D001B"
|
||||||
|
d="m9.219 6.541-4.021 4.021a.74.74 0 0 1-.552.235.778.778 0 0 1-.552-.245.796.796 0 0 1-.235-.552.74.74 0 0 1 .235-.552l5.354-5.355a.77.77 0 0 1 .849-.171.77.77 0 0 1 .255.171l5.354 5.355a.782.782 0 0 1 0 1.104.764.764 0 0 1-1.114 0l-4.01-4.01v9.135c0 .215-.077.4-.23.552a.752.752 0 0 1-.552.229.752.752 0 0 1-.552-.23.752.752 0 0 1-.23-.551V6.54Z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@ export { default as AirIcon } from "./Air"
|
|||||||
export { default as AirplaneIcon } from "./Airplane"
|
export { default as AirplaneIcon } from "./Airplane"
|
||||||
export { default as AllergyIcon } from "./Allergy"
|
export { default as AllergyIcon } from "./Allergy"
|
||||||
export { default as ArrowRightIcon } from "./ArrowRight"
|
export { default as ArrowRightIcon } from "./ArrowRight"
|
||||||
|
export { default as ArrowUpIcon } from "./ArrowUp"
|
||||||
export { default as BarIcon } from "./Bar"
|
export { default as BarIcon } from "./Bar"
|
||||||
export { default as BathtubIcon } from "./Bathtub"
|
export { default as BathtubIcon } from "./Bathtub"
|
||||||
export { default as BedDoubleIcon } from "./BedDouble"
|
export { default as BedDoubleIcon } from "./BedDouble"
|
||||||
|
|||||||
@@ -13,19 +13,16 @@ import { trackLoginClick } from "@/utils/tracking"
|
|||||||
import { TrackingPosition } from "@/types/components/tracking"
|
import { TrackingPosition } from "@/types/components/tracking"
|
||||||
|
|
||||||
export default function LoginButton({
|
export default function LoginButton({
|
||||||
className,
|
|
||||||
position,
|
position,
|
||||||
trackingId,
|
trackingId,
|
||||||
children,
|
children,
|
||||||
color = "black",
|
...props
|
||||||
variant = "navigation",
|
}: PropsWithChildren<
|
||||||
}: PropsWithChildren<{
|
{
|
||||||
className: string
|
trackingId: string
|
||||||
trackingId: string
|
position: TrackingPosition
|
||||||
position: TrackingPosition
|
} & Omit<LinkProps, "href">
|
||||||
color?: LinkProps["color"]
|
>) {
|
||||||
variant?: "navigation" | "signupVerification"
|
|
||||||
}>) {
|
|
||||||
const lang = useLang()
|
const lang = useLang()
|
||||||
const pathName = useLazyPathname({ includeSearchParams: true })
|
const pathName = useLazyPathname({ includeSearchParams: true })
|
||||||
|
|
||||||
@@ -34,25 +31,19 @@ export default function LoginButton({
|
|||||||
: login[lang]
|
: login[lang]
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
document
|
function trackLogin() {
|
||||||
.getElementById(trackingId)
|
trackLoginClick(position)
|
||||||
?.addEventListener("click", () => trackLoginClick(position))
|
}
|
||||||
|
document.getElementById(trackingId)?.addEventListener("click", trackLogin)
|
||||||
return () => {
|
return () => {
|
||||||
document
|
document
|
||||||
.getElementById(trackingId)
|
.getElementById(trackingId)
|
||||||
?.removeEventListener("click", () => trackLoginClick(position))
|
?.removeEventListener("click", trackLogin)
|
||||||
}
|
}
|
||||||
}, [position, trackingId])
|
}, [position, trackingId])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Link
|
<Link id={trackingId} prefetch={false} {...props} href={href}>
|
||||||
className={className}
|
|
||||||
id={trackingId}
|
|
||||||
color={color}
|
|
||||||
href={href}
|
|
||||||
prefetch={false}
|
|
||||||
variant={variant}
|
|
||||||
>
|
|
||||||
{children}
|
{children}
|
||||||
</Link>
|
</Link>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ export function MapModal({ children }: { children: React.ReactNode }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.wrapper} ref={rootDiv}>
|
<div className={styles.wrapper} ref={rootDiv}>
|
||||||
<Modal isDismissable isOpen={isOpen} onOpenChange={handleOnOpenChange}>
|
<Modal isOpen={isOpen} onOpenChange={handleOnOpenChange}>
|
||||||
<Dialog
|
<Dialog
|
||||||
style={
|
style={
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,22 +1,15 @@
|
|||||||
import { IconName } from "@/types/components/icon"
|
import { IconName } from "@/types/components/icon"
|
||||||
import {
|
import { PointOfInterestGroupEnum } from "@/types/hotel"
|
||||||
PointOfInterestCategoryNameEnum,
|
|
||||||
PointOfInterestGroupEnum,
|
|
||||||
} from "@/types/hotel"
|
|
||||||
|
|
||||||
export function getIconByPoiGroupAndCategory(
|
export function getIconByPoiGroupAndCategory(
|
||||||
group: PointOfInterestGroupEnum,
|
group: PointOfInterestGroupEnum,
|
||||||
category?: PointOfInterestCategoryNameEnum
|
category?: string
|
||||||
) {
|
) {
|
||||||
switch (group) {
|
switch (group) {
|
||||||
case PointOfInterestGroupEnum.PUBLIC_TRANSPORT:
|
case PointOfInterestGroupEnum.PUBLIC_TRANSPORT:
|
||||||
return category === PointOfInterestCategoryNameEnum.AIRPORT
|
return category === "Airport" ? IconName.Airplane : IconName.Train
|
||||||
? IconName.Airplane
|
|
||||||
: IconName.Train
|
|
||||||
case PointOfInterestGroupEnum.ATTRACTIONS:
|
case PointOfInterestGroupEnum.ATTRACTIONS:
|
||||||
return category === PointOfInterestCategoryNameEnum.MUSEUM
|
return category === "Museum" ? IconName.Museum : IconName.Camera
|
||||||
? IconName.Museum
|
|
||||||
: IconName.Camera
|
|
||||||
case PointOfInterestGroupEnum.BUSINESS:
|
case PointOfInterestGroupEnum.BUSINESS:
|
||||||
return IconName.Business
|
return IconName.Business
|
||||||
case PointOfInterestGroupEnum.PARKING:
|
case PointOfInterestGroupEnum.PARKING:
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
.backToTopButton {
|
||||||
|
border-radius: var(--Corner-radius-Rounded);
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-end;
|
||||||
|
position: fixed;
|
||||||
|
bottom: 20px;
|
||||||
|
right: 20px;
|
||||||
|
z-index: 1000;
|
||||||
|
background-color: var(--Base-Surface-Primary-light-Normal);
|
||||||
|
color: var(--Base-Button-Secondary-On-Fill-Normal);
|
||||||
|
border: 2px solid var(--Base-Button-Secondary-On-Fill-Normal);
|
||||||
|
gap: var(--Spacing-x-half);
|
||||||
|
padding: var(--Spacing-x1);
|
||||||
|
text-align: center;
|
||||||
|
transition:
|
||||||
|
background-color 300ms ease,
|
||||||
|
color 300ms ease;
|
||||||
|
font-family: var(--typography-Body-Bold-fontFamily);
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: var(--typography-Caption-Bold-fontSize);
|
||||||
|
line-height: var(--typography-Caption-Bold-lineHeight);
|
||||||
|
letter-spacing: 0.6%;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.backToTopButtonText {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
.backToTopButtonText {
|
||||||
|
display: initial;
|
||||||
|
}
|
||||||
|
.backToTopButton:hover {
|
||||||
|
background-color: var(--Base-Button-Tertiary-Fill-Normal);
|
||||||
|
color: var(--Base-Button-Tertiary-On-Fill-Hover);
|
||||||
|
}
|
||||||
|
.backToTopButton:hover > svg * {
|
||||||
|
fill: var(--Base-Button-Tertiary-On-Fill-Hover);
|
||||||
|
}
|
||||||
|
.backToTopButton {
|
||||||
|
padding: calc(var(--Spacing-x1) + 2px) var(--Spacing-x2);
|
||||||
|
}
|
||||||
|
}
|
||||||
20
components/TempDesignSystem/BackToTopButton/index.tsx
Normal file
20
components/TempDesignSystem/BackToTopButton/index.tsx
Normal file
@@ -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 (
|
||||||
|
<ButtonRAC className={styles.backToTopButton} onPress={onClick}>
|
||||||
|
<ArrowUpIcon color="burgundy" />
|
||||||
|
<span className={styles.backToTopButtonText}>
|
||||||
|
{intl.formatMessage({ id: "Back to top" })}
|
||||||
|
</span>
|
||||||
|
</ButtonRAC>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
color: var(--text-color);
|
color: var(--text-color);
|
||||||
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.container[data-selected] .checkbox {
|
.container[data-selected] .checkbox {
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import styles from "./checkbox.module.css"
|
|||||||
import { CheckboxProps } from "@/types/components/checkbox"
|
import { CheckboxProps } from "@/types/components/checkbox"
|
||||||
|
|
||||||
export default function Checkbox({
|
export default function Checkbox({
|
||||||
|
className,
|
||||||
name,
|
name,
|
||||||
children,
|
children,
|
||||||
registerOptions,
|
registerOptions,
|
||||||
@@ -25,16 +26,17 @@ export default function Checkbox({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<AriaCheckbox
|
<AriaCheckbox
|
||||||
className={styles.container}
|
className={`${styles.container} ${className}`}
|
||||||
isSelected={field.value}
|
isSelected={field.value}
|
||||||
onChange={field.onChange}
|
onChange={field.onChange}
|
||||||
data-testid={name}
|
data-testid={name}
|
||||||
isDisabled={registerOptions?.disabled}
|
isDisabled={registerOptions?.disabled}
|
||||||
|
excludeFromTabOrder
|
||||||
>
|
>
|
||||||
{({ isSelected }) => (
|
{({ isSelected }) => (
|
||||||
<>
|
<>
|
||||||
<span className={styles.checkboxContainer}>
|
<span className={styles.checkboxContainer}>
|
||||||
<span className={styles.checkbox}>
|
<span className={styles.checkbox} tabIndex={0}>
|
||||||
{isSelected && <CheckIcon color="white" />}
|
{isSelected && <CheckIcon color="white" />}
|
||||||
</span>
|
</span>
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
import Card from "./_Card"
|
|
||||||
|
|
||||||
import type { CheckboxProps } from "./_Card/card"
|
|
||||||
|
|
||||||
export default function CheckboxCard(props: CheckboxProps) {
|
|
||||||
return <Card {...props} type="checkbox" />
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
.label {
|
.label {
|
||||||
align-self: flex-start;
|
|
||||||
background-color: var(--Base-Surface-Primary-light-Normal);
|
background-color: var(--Base-Surface-Primary-light-Normal);
|
||||||
border: 1px solid var(--Base-Border-Subtle);
|
border: 1px solid var(--Base-Border-Subtle);
|
||||||
border-radius: var(--Corner-radius-Large);
|
border-radius: var(--Corner-radius-Large);
|
||||||
|
|||||||
@@ -1,4 +1,8 @@
|
|||||||
/* Leaving, will most likely get deleted */
|
/* Leaving, will most likely get deleted */
|
||||||
|
.datePicker {
|
||||||
|
container-name: datePickerContainer;
|
||||||
|
container-type: inline-size;
|
||||||
|
}
|
||||||
.container {
|
.container {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: var(--Spacing-x2);
|
gap: var(--Spacing-x2);
|
||||||
@@ -27,3 +31,10 @@
|
|||||||
.year.invalid > div > div {
|
.year.invalid > div > div {
|
||||||
border-color: var(--Scandic-Red-60);
|
border-color: var(--Scandic-Red-60);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@container datePickerContainer (max-width: 350px) {
|
||||||
|
.container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -115,6 +115,7 @@ export default function DateSelect({ name, registerOptions = {} }: DateProps) {
|
|||||||
ref={field.ref}
|
ref={field.ref}
|
||||||
value={dateValue}
|
value={dateValue}
|
||||||
data-testid={name}
|
data-testid={name}
|
||||||
|
className={styles.datePicker}
|
||||||
>
|
>
|
||||||
<Group>
|
<Group>
|
||||||
<DateInput className={styles.container}>
|
<DateInput className={styles.container}>
|
||||||
|
|||||||
@@ -78,67 +78,69 @@ export default function Phone({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`${styles.phone} ${className}`}>
|
<div className={`${styles.wrapper} ${className}`}>
|
||||||
<CountrySelector
|
<div className={styles.phone}>
|
||||||
disabled={readOnly}
|
<CountrySelector
|
||||||
dropdownArrowClassName={styles.arrow}
|
disabled={readOnly}
|
||||||
flagClassName={styles.flag}
|
dropdownArrowClassName={styles.arrow}
|
||||||
onSelect={handleSelectCountry}
|
flagClassName={styles.flag}
|
||||||
preferredCountries={["de", "dk", "fi", "no", "se", "gb"]}
|
onSelect={handleSelectCountry}
|
||||||
selectedCountry={country.iso2}
|
preferredCountries={["de", "dk", "fi", "no", "se", "gb"]}
|
||||||
renderButtonWrapper={(props) => (
|
selectedCountry={country.iso2}
|
||||||
<button
|
renderButtonWrapper={(props) => (
|
||||||
{...props.rootProps}
|
<button
|
||||||
className={styles.select}
|
{...props.rootProps}
|
||||||
tabIndex={0}
|
className={styles.select}
|
||||||
type="button"
|
tabIndex={0}
|
||||||
data-testid="country-selector"
|
type="button"
|
||||||
>
|
data-testid="country-selector"
|
||||||
<Label required={!!registerOptions.required} size="small">
|
>
|
||||||
{intl.formatMessage({ id: "Country code" })}
|
<Label required={!!registerOptions.required} size="small">
|
||||||
</Label>
|
{intl.formatMessage({ id: "Country code" })}
|
||||||
<span className={styles.selectContainer}>
|
</Label>
|
||||||
{props.children}
|
<span className={styles.selectContainer}>
|
||||||
<Body asChild fontOnly>
|
{props.children}
|
||||||
<DialCodePreview
|
<Body asChild fontOnly>
|
||||||
className={styles.dialCode}
|
<DialCodePreview
|
||||||
dialCode={country.dialCode}
|
className={styles.dialCode}
|
||||||
prefix="+"
|
dialCode={country.dialCode}
|
||||||
|
prefix="+"
|
||||||
|
/>
|
||||||
|
</Body>
|
||||||
|
<ChevronDownIcon
|
||||||
|
className={styles.chevron}
|
||||||
|
color="grey80"
|
||||||
|
height={18}
|
||||||
|
width={18}
|
||||||
/>
|
/>
|
||||||
</Body>
|
</span>
|
||||||
<ChevronDownIcon
|
</button>
|
||||||
className={styles.chevron}
|
)}
|
||||||
color="grey80"
|
|
||||||
height={18}
|
|
||||||
width={18}
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<TextField
|
|
||||||
aria-label={ariaLabel}
|
|
||||||
defaultValue={field.value}
|
|
||||||
isDisabled={disabled ?? field.disabled}
|
|
||||||
isInvalid={fieldState.invalid}
|
|
||||||
isRequired={!!registerOptions?.required}
|
|
||||||
isReadOnly={readOnly}
|
|
||||||
name={field.name}
|
|
||||||
type="tel"
|
|
||||||
>
|
|
||||||
<AriaInputWithLabel
|
|
||||||
{...field}
|
|
||||||
id={field.name}
|
|
||||||
label={label}
|
|
||||||
onChange={handleChange}
|
|
||||||
placeholder={placeholder}
|
|
||||||
readOnly={readOnly}
|
|
||||||
required={!!registerOptions.required}
|
|
||||||
type="tel"
|
|
||||||
value={inputValue}
|
|
||||||
/>
|
/>
|
||||||
<ErrorMessage errors={formState.errors} name={field.name} />
|
<TextField
|
||||||
</TextField>
|
aria-label={ariaLabel}
|
||||||
|
defaultValue={field.value}
|
||||||
|
isDisabled={disabled ?? field.disabled}
|
||||||
|
isInvalid={fieldState.invalid}
|
||||||
|
isRequired={!!registerOptions?.required}
|
||||||
|
isReadOnly={readOnly}
|
||||||
|
name={field.name}
|
||||||
|
type="tel"
|
||||||
|
>
|
||||||
|
<AriaInputWithLabel
|
||||||
|
{...field}
|
||||||
|
id={field.name}
|
||||||
|
label={label}
|
||||||
|
onChange={handleChange}
|
||||||
|
placeholder={placeholder}
|
||||||
|
readOnly={readOnly}
|
||||||
|
required={!!registerOptions.required}
|
||||||
|
type="tel"
|
||||||
|
value={inputValue}
|
||||||
|
/>
|
||||||
|
<ErrorMessage errors={formState.errors} name={field.name} />
|
||||||
|
</TextField>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
|
.wrapper {
|
||||||
|
container-name: phoneContainer;
|
||||||
|
container-type: inline-size;
|
||||||
|
}
|
||||||
.phone {
|
.phone {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: var(--Spacing-x2);
|
gap: var(--Spacing-x2);
|
||||||
@@ -100,3 +104,10 @@
|
|||||||
justify-self: flex-start;
|
justify-self: flex-start;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@container phoneContainer (max-width: 350px) {
|
||||||
|
.phone {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
.breadcrumb {
|
.breadcrumb {
|
||||||
font-family: var(--typography-Footnote-Bold-fontFamily);
|
font-family: var(--typography-Footnote-Bold-fontFamily);
|
||||||
font-size: var(--typography-Footnote-Bold-fontSize);
|
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);
|
letter-spacing: var(--typography-Footnote-Bold-letterSpacing);
|
||||||
line-height: var(--typography-Footnote-Bold-lineHeight);
|
line-height: var(--typography-Footnote-Bold-lineHeight);
|
||||||
}
|
}
|
||||||
@@ -24,7 +24,7 @@
|
|||||||
.link.breadcrumb {
|
.link.breadcrumb {
|
||||||
font-family: var(--typography-Footnote-Bold-fontFamily);
|
font-family: var(--typography-Footnote-Bold-fontFamily);
|
||||||
font-size: var(--typography-Footnote-Bold-fontSize);
|
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);
|
letter-spacing: var(--typography-Footnote-Bold-letterSpacing);
|
||||||
line-height: var(--typography-Footnote-Bold-lineHeight);
|
line-height: var(--typography-Footnote-Bold-lineHeight);
|
||||||
}
|
}
|
||||||
@@ -128,6 +128,15 @@
|
|||||||
color: #000;
|
color: #000;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.uiTextPlaceholder {
|
||||||
|
color: var(--Base-Text-Placeholder);
|
||||||
|
}
|
||||||
|
|
||||||
|
.uiTextPlaceholder:hover,
|
||||||
|
.uiTextPlaceholder:active {
|
||||||
|
color: var(--Base-Text-Medium-contrast);
|
||||||
|
}
|
||||||
|
|
||||||
.burgundy {
|
.burgundy {
|
||||||
color: var(--Base-Text-High-contrast);
|
color: var(--Base-Text-High-contrast);
|
||||||
}
|
}
|
||||||
@@ -211,6 +220,14 @@
|
|||||||
line-height: var(--typography-Caption-Regular-lineHeight);
|
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 {
|
.activeSmall {
|
||||||
font-family: var(--typography-Caption-Bold-fontFamily);
|
font-family: var(--typography-Caption-Bold-fontFamily);
|
||||||
font-size: var(--typography-Caption-Bold-fontSize);
|
font-size: var(--typography-Caption-Bold-fontSize);
|
||||||
|
|||||||
@@ -17,10 +17,12 @@ export const linkVariants = cva(styles.link, {
|
|||||||
peach80: styles.peach80,
|
peach80: styles.peach80,
|
||||||
white: styles.white,
|
white: styles.white,
|
||||||
red: styles.red,
|
red: styles.red,
|
||||||
|
uiTextPlaceholder: styles.uiTextPlaceholder,
|
||||||
},
|
},
|
||||||
size: {
|
size: {
|
||||||
small: styles.small,
|
small: styles.small,
|
||||||
regular: styles.regular,
|
regular: styles.regular,
|
||||||
|
tiny: styles.tiny,
|
||||||
},
|
},
|
||||||
textDecoration: {
|
textDecoration: {
|
||||||
none: styles.noDecoration,
|
none: styles.noDecoration,
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ export function Tooltip<P extends TooltipPosition>({
|
|||||||
role="tooltip"
|
role="tooltip"
|
||||||
aria-label={text}
|
aria-label={text}
|
||||||
onClick={handleToggle}
|
onClick={handleToggle}
|
||||||
|
onTouchStart={handleToggle}
|
||||||
data-active={isActive}
|
data-active={isActive}
|
||||||
>
|
>
|
||||||
<div className={className}>
|
<div className={className}>
|
||||||
|
|||||||
@@ -16,6 +16,7 @@
|
|||||||
transition: opacity 0.3s;
|
transition: opacity 0.3s;
|
||||||
max-width: 200px;
|
max-width: 200px;
|
||||||
min-width: 150px;
|
min-width: 150px;
|
||||||
|
height: fit-content;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tooltipContainer:hover .tooltip {
|
.tooltipContainer:hover .tooltip {
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ export function selectHotel(lang) {
|
|||||||
* @param {Lang} lang
|
* @param {Lang} lang
|
||||||
*/
|
*/
|
||||||
export function selectHotelMap(lang) {
|
export function selectHotelMap(lang) {
|
||||||
return `${base(lang)}/map`
|
return `${base(lang)}/select-hotel/map`
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -47,8 +47,10 @@
|
|||||||
"Booking number": "Bookingnummer",
|
"Booking number": "Bookingnummer",
|
||||||
"Breakfast": "Morgenmad",
|
"Breakfast": "Morgenmad",
|
||||||
"Breakfast buffet": "Morgenbuffet",
|
"Breakfast buffet": "Morgenbuffet",
|
||||||
|
"Breakfast deal can be purchased at the hotel.": "Morgenmad kan købes på hotellet.",
|
||||||
"Breakfast excluded": "Morgenmad ikke inkluderet",
|
"Breakfast excluded": "Morgenmad ikke inkluderet",
|
||||||
"Breakfast included": "Morgenmad inkluderet",
|
"Breakfast included": "Morgenmad inkluderet",
|
||||||
|
"Breakfast is included.": "Morgenmad er inkluderet.",
|
||||||
"Breakfast restaurant": "Breakfast restaurant",
|
"Breakfast restaurant": "Breakfast restaurant",
|
||||||
"Breakfast selection in next step.": "Valg af morgenmad i næste trin.",
|
"Breakfast selection in next step.": "Valg af morgenmad i næste trin.",
|
||||||
"Bus terminal": "Busstation",
|
"Bus terminal": "Busstation",
|
||||||
@@ -152,6 +154,7 @@
|
|||||||
"Hotel": "Hotel",
|
"Hotel": "Hotel",
|
||||||
"Hotel facilities": "Hotel faciliteter",
|
"Hotel facilities": "Hotel faciliteter",
|
||||||
"Hotel surroundings": "Hotel omgivelser",
|
"Hotel surroundings": "Hotel omgivelser",
|
||||||
|
"Hotel(s)": "{amount} {amount, plural, one {hotel} other {hoteller}}",
|
||||||
"Hotels": "Hoteller",
|
"Hotels": "Hoteller",
|
||||||
"How do you want to sleep?": "Hvordan vil du sove?",
|
"How do you want to sleep?": "Hvordan vil du sove?",
|
||||||
"How it works": "Hvordan det virker",
|
"How it works": "Hvordan det virker",
|
||||||
@@ -236,6 +239,7 @@
|
|||||||
"Number of parking spots": "Antal parkeringspladser",
|
"Number of parking spots": "Antal parkeringspladser",
|
||||||
"OTHER PAYMENT METHODS": "ANDRE BETALINGSMETODER",
|
"OTHER PAYMENT METHODS": "ANDRE BETALINGSMETODER",
|
||||||
"On your journey": "På din rejse",
|
"On your journey": "På din rejse",
|
||||||
|
"Only pay {amount} {currency}": "Betal kun {amount} {currency}",
|
||||||
"Open": "Åben",
|
"Open": "Åben",
|
||||||
"Open gift(s)": "Åbne {amount, plural, one {gave} other {gaver}}",
|
"Open gift(s)": "Åbne {amount, plural, one {gave} other {gaver}}",
|
||||||
"Open image gallery": "Åbn billedgalleri",
|
"Open image gallery": "Åbn billedgalleri",
|
||||||
@@ -333,6 +337,7 @@
|
|||||||
"Show wellness & exercise": "Vis velvære og motion",
|
"Show wellness & exercise": "Vis velvære og motion",
|
||||||
"Sign up bonus": "Velkomstbonus",
|
"Sign up bonus": "Velkomstbonus",
|
||||||
"Sign up to Scandic Friends": "Tilmeld dig Scandic Friends",
|
"Sign up to Scandic Friends": "Tilmeld dig Scandic Friends",
|
||||||
|
"Signing up...": "Tilmelder...",
|
||||||
"Skip to main content": "Spring over og gå til hovedindhold",
|
"Skip to main content": "Spring over og gå til hovedindhold",
|
||||||
"Something went wrong and we couldn't add your card. Please try again later.": "Noget gik galt, og vi kunne ikke tilføje dit kort. Prøv venligst igen senere.",
|
"Something went wrong and we couldn't add your card. Please try again later.": "Noget gik galt, og vi kunne ikke tilføje dit kort. Prøv venligst igen senere.",
|
||||||
"Something went wrong and we couldn't remove your card. Please try again later.": "Noget gik galt, og vi kunne ikke fjerne dit kort. Prøv venligst igen senere.",
|
"Something went wrong and we couldn't remove your card. Please try again later.": "Noget gik galt, og vi kunne ikke fjerne dit kort. Prøv venligst igen senere.",
|
||||||
@@ -456,6 +461,5 @@
|
|||||||
"to": "til",
|
"to": "til",
|
||||||
"uppercase letter": "stort bogstav",
|
"uppercase letter": "stort bogstav",
|
||||||
"{amount} out of {total}": "{amount} ud af {total}",
|
"{amount} out of {total}": "{amount} ud af {total}",
|
||||||
"{amount} {currency}": "{amount} {currency}",
|
"{amount} {currency}": "{amount} {currency}"
|
||||||
"{difference}{amount} {currency}": "{difference}{amount} {currency}"
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,8 +47,10 @@
|
|||||||
"Booking number": "Buchungsnummer",
|
"Booking number": "Buchungsnummer",
|
||||||
"Breakfast": "Frühstück",
|
"Breakfast": "Frühstück",
|
||||||
"Breakfast buffet": "Frühstücksbuffet",
|
"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 excluded": "Frühstück nicht inbegriffen",
|
||||||
"Breakfast included": "Frühstück inbegriffen",
|
"Breakfast included": "Frühstück inbegriffen",
|
||||||
|
"Breakfast is included.": "Frühstück ist inbegriffen.",
|
||||||
"Breakfast restaurant": "Breakfast restaurant",
|
"Breakfast restaurant": "Breakfast restaurant",
|
||||||
"Breakfast selection in next step.": "Frühstücksauswahl in nächsten Schritt.",
|
"Breakfast selection in next step.": "Frühstücksauswahl in nächsten Schritt.",
|
||||||
"Bus terminal": "Busbahnhof",
|
"Bus terminal": "Busbahnhof",
|
||||||
@@ -152,6 +154,7 @@
|
|||||||
"Hotel": "Hotel",
|
"Hotel": "Hotel",
|
||||||
"Hotel facilities": "Hotel-Infos",
|
"Hotel facilities": "Hotel-Infos",
|
||||||
"Hotel surroundings": "Umgebung des Hotels",
|
"Hotel surroundings": "Umgebung des Hotels",
|
||||||
|
"Hotel(s)": "{amount} {amount, plural, one {hotel} other {hotels}}",
|
||||||
"Hotels": "Hotels",
|
"Hotels": "Hotels",
|
||||||
"How do you want to sleep?": "Wie möchtest du schlafen?",
|
"How do you want to sleep?": "Wie möchtest du schlafen?",
|
||||||
"How it works": "Wie es funktioniert",
|
"How it works": "Wie es funktioniert",
|
||||||
@@ -234,6 +237,7 @@
|
|||||||
"Number of parking spots": "Anzahl der Parkplätze",
|
"Number of parking spots": "Anzahl der Parkplätze",
|
||||||
"OTHER PAYMENT METHODS": "ANDERE BEZAHLMETHODE",
|
"OTHER PAYMENT METHODS": "ANDERE BEZAHLMETHODE",
|
||||||
"On your journey": "Auf deiner Reise",
|
"On your journey": "Auf deiner Reise",
|
||||||
|
"Only pay {amount} {currency}": "Nur bezahlen {amount} {currency}",
|
||||||
"Open": "Offen",
|
"Open": "Offen",
|
||||||
"Open gift(s)": "{amount, plural, one {Geschenk} other {Geschenke}} öffnen",
|
"Open gift(s)": "{amount, plural, one {Geschenk} other {Geschenke}} öffnen",
|
||||||
"Open image gallery": "Bildergalerie öffnen",
|
"Open image gallery": "Bildergalerie öffnen",
|
||||||
@@ -332,6 +336,7 @@
|
|||||||
"Show wellness & exercise": "Zeige Wellness und Bewegung",
|
"Show wellness & exercise": "Zeige Wellness und Bewegung",
|
||||||
"Sign up bonus": "Anmelde-Bonus",
|
"Sign up bonus": "Anmelde-Bonus",
|
||||||
"Sign up to Scandic Friends": "Treten Sie Scandic Friends bei",
|
"Sign up to Scandic Friends": "Treten Sie Scandic Friends bei",
|
||||||
|
"Signing up...": "Registrierung läuft...",
|
||||||
"Skip to main content": "Direkt zum Inhalt",
|
"Skip to main content": "Direkt zum Inhalt",
|
||||||
"Something went wrong and we couldn't add your card. Please try again later.": "Ein Fehler ist aufgetreten und wir konnten Ihre Karte nicht hinzufügen. Bitte versuchen Sie es später erneut.",
|
"Something went wrong and we couldn't add your card. Please try again later.": "Ein Fehler ist aufgetreten und wir konnten Ihre Karte nicht hinzufügen. Bitte versuchen Sie es später erneut.",
|
||||||
"Something went wrong and we couldn't remove your card. Please try again later.": "Ein Fehler ist aufgetreten und wir konnten Ihre Karte nicht entfernen. Bitte versuchen Sie es später noch einmal.",
|
"Something went wrong and we couldn't remove your card. Please try again later.": "Ein Fehler ist aufgetreten und wir konnten Ihre Karte nicht entfernen. Bitte versuchen Sie es später noch einmal.",
|
||||||
@@ -454,6 +459,5 @@
|
|||||||
"to": "zu",
|
"to": "zu",
|
||||||
"uppercase letter": "großbuchstabe",
|
"uppercase letter": "großbuchstabe",
|
||||||
"{amount} out of {total}": "{amount} von {total}",
|
"{amount} out of {total}": "{amount} von {total}",
|
||||||
"{amount} {currency}": "{amount} {currency}",
|
"{amount} {currency}": "{amount} {currency}"
|
||||||
"{difference}{amount} {currency}": "{difference}{amount} {currency}"
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,8 +51,10 @@
|
|||||||
"Booking number": "Booking number",
|
"Booking number": "Booking number",
|
||||||
"Breakfast": "Breakfast",
|
"Breakfast": "Breakfast",
|
||||||
"Breakfast buffet": "Breakfast buffet",
|
"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 excluded": "Breakfast excluded",
|
||||||
"Breakfast included": "Breakfast included",
|
"Breakfast included": "Breakfast included",
|
||||||
|
"Breakfast is included.": "Breakfast is included.",
|
||||||
"Breakfast restaurant": "Breakfast restaurant",
|
"Breakfast restaurant": "Breakfast restaurant",
|
||||||
"Breakfast selection in next step.": "Breakfast selection in next step.",
|
"Breakfast selection in next step.": "Breakfast selection in next step.",
|
||||||
"Bus terminal": "Bus terminal",
|
"Bus terminal": "Bus terminal",
|
||||||
@@ -164,6 +166,7 @@
|
|||||||
"Hotel": "Hotel",
|
"Hotel": "Hotel",
|
||||||
"Hotel facilities": "Hotel facilities",
|
"Hotel facilities": "Hotel facilities",
|
||||||
"Hotel surroundings": "Hotel surroundings",
|
"Hotel surroundings": "Hotel surroundings",
|
||||||
|
"Hotel(s)": "{amount} {amount, plural, one {hotel} other {hotels}}",
|
||||||
"Hotels": "Hotels",
|
"Hotels": "Hotels",
|
||||||
"How do you want to sleep?": "How do you want to sleep?",
|
"How do you want to sleep?": "How do you want to sleep?",
|
||||||
"How it works": "How it works",
|
"How it works": "How it works",
|
||||||
@@ -253,6 +256,7 @@
|
|||||||
"Number of parking spots": "Number of parking spots",
|
"Number of parking spots": "Number of parking spots",
|
||||||
"OTHER PAYMENT METHODS": "OTHER PAYMENT METHODS",
|
"OTHER PAYMENT METHODS": "OTHER PAYMENT METHODS",
|
||||||
"On your journey": "On your journey",
|
"On your journey": "On your journey",
|
||||||
|
"Only pay {amount} {currency}": "Only pay {amount} {currency}",
|
||||||
"Open": "Open",
|
"Open": "Open",
|
||||||
"Open gift(s)": "Open {amount, plural, one {gift} other {gifts}}",
|
"Open gift(s)": "Open {amount, plural, one {gift} other {gifts}}",
|
||||||
"Open image gallery": "Open image gallery",
|
"Open image gallery": "Open image gallery",
|
||||||
@@ -362,6 +366,7 @@
|
|||||||
"Show wellness & exercise": "Show wellness & exercise",
|
"Show wellness & exercise": "Show wellness & exercise",
|
||||||
"Sign up bonus": "Sign up bonus",
|
"Sign up bonus": "Sign up bonus",
|
||||||
"Sign up to Scandic Friends": "Sign up to Scandic Friends",
|
"Sign up to Scandic Friends": "Sign up to Scandic Friends",
|
||||||
|
"Signing up...": "Signing up...",
|
||||||
"Skip to main content": "Skip to main content",
|
"Skip to main content": "Skip to main content",
|
||||||
"Something went wrong and we couldn't add your card. Please try again later.": "Something went wrong and we couldn't add your card. Please try again later.",
|
"Something went wrong and we couldn't add your card. Please try again later.": "Something went wrong and we couldn't add your card. Please try again later.",
|
||||||
"Something went wrong and we couldn't remove your card. Please try again later.": "Something went wrong and we couldn't remove your card. Please try again later.",
|
"Something went wrong and we couldn't remove your card. Please try again later.": "Something went wrong and we couldn't remove your card. Please try again later.",
|
||||||
@@ -425,7 +430,6 @@
|
|||||||
"Which room class suits you the best?": "Which room class suits you the best?",
|
"Which room class suits you the best?": "Which room class suits you the best?",
|
||||||
"Year": "Year",
|
"Year": "Year",
|
||||||
"Yes": "Yes",
|
"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, discard changes": "Yes, discard changes",
|
||||||
"Yes, remove my card": "Yes, remove my card",
|
"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.",
|
"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",
|
"points": "Points",
|
||||||
"room type": "room type",
|
"room type": "room type",
|
||||||
"room types": "room types",
|
"room types": "room types",
|
||||||
|
"signup.terms": "By signing up you accept the Scandic Friends <termsLink>Terms and Conditions</termsLink>. 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",
|
"special character": "special character",
|
||||||
"spendable points expiring by": "{points} spendable points expiring by {date}",
|
"spendable points expiring by": "{points} spendable points expiring by {date}",
|
||||||
"to": "to",
|
"to": "to",
|
||||||
"uppercase letter": "uppercase letter",
|
"uppercase letter": "uppercase letter",
|
||||||
"{amount} out of {total}": "{amount} out of {total}",
|
"{amount} out of {total}": "{amount} out of {total}",
|
||||||
"{amount} {currency}": "{amount} {currency}",
|
"{amount} {currency}": "{amount} {currency}",
|
||||||
"{card} ending with {cardno}": "{card} ending with {cardno}",
|
"{card} ending with {cardno}": "{card} ending with {cardno}"
|
||||||
"{difference}{amount} {currency}": "{difference}{amount} {currency}"
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,8 +47,10 @@
|
|||||||
"Booking number": "Varausnumero",
|
"Booking number": "Varausnumero",
|
||||||
"Breakfast": "Aamiainen",
|
"Breakfast": "Aamiainen",
|
||||||
"Breakfast buffet": "Aamiaisbuffet",
|
"Breakfast buffet": "Aamiaisbuffet",
|
||||||
|
"Breakfast deal can be purchased at the hotel.": "Aamiaisdeali voidaan ostaa hotellissa.",
|
||||||
"Breakfast excluded": "Aamiainen ei sisälly",
|
"Breakfast excluded": "Aamiainen ei sisälly",
|
||||||
"Breakfast included": "Aamiainen sisältyy",
|
"Breakfast included": "Aamiainen sisältyy",
|
||||||
|
"Breakfast is included.": "Aamiainen sisältyy.",
|
||||||
"Breakfast restaurant": "Breakfast restaurant",
|
"Breakfast restaurant": "Breakfast restaurant",
|
||||||
"Breakfast selection in next step.": "Aamiaisvalinta seuraavassa vaiheessa.",
|
"Breakfast selection in next step.": "Aamiaisvalinta seuraavassa vaiheessa.",
|
||||||
"Bus terminal": "Bussiasema",
|
"Bus terminal": "Bussiasema",
|
||||||
@@ -152,6 +154,7 @@
|
|||||||
"Hotel": "Hotelli",
|
"Hotel": "Hotelli",
|
||||||
"Hotel facilities": "Hotellin palvelut",
|
"Hotel facilities": "Hotellin palvelut",
|
||||||
"Hotel surroundings": "Hotellin ympäristö",
|
"Hotel surroundings": "Hotellin ympäristö",
|
||||||
|
"Hotel(s)": "{amount} {amount, plural, one {hotelli} other {hotellit}}",
|
||||||
"Hotels": "Hotellit",
|
"Hotels": "Hotellit",
|
||||||
"How do you want to sleep?": "Kuinka haluat nukkua?",
|
"How do you want to sleep?": "Kuinka haluat nukkua?",
|
||||||
"How it works": "Kuinka se toimii",
|
"How it works": "Kuinka se toimii",
|
||||||
@@ -236,6 +239,7 @@
|
|||||||
"Number of parking spots": "Pysäköintipaikkojen määrä",
|
"Number of parking spots": "Pysäköintipaikkojen määrä",
|
||||||
"OTHER PAYMENT METHODS": "MUISE KORT",
|
"OTHER PAYMENT METHODS": "MUISE KORT",
|
||||||
"On your journey": "Matkallasi",
|
"On your journey": "Matkallasi",
|
||||||
|
"Only pay {amount} {currency}": "Vain maksaa {amount} {currency}",
|
||||||
"Open": "Avata",
|
"Open": "Avata",
|
||||||
"Open gift(s)": "{amount, plural, one {Avoin lahja} other {Avoimet lahjat}}",
|
"Open gift(s)": "{amount, plural, one {Avoin lahja} other {Avoimet lahjat}}",
|
||||||
"Open image gallery": "Avaa kuvagalleria",
|
"Open image gallery": "Avaa kuvagalleria",
|
||||||
@@ -334,6 +338,7 @@
|
|||||||
"Show wellness & exercise": "Näytä hyvinvointi ja liikunta",
|
"Show wellness & exercise": "Näytä hyvinvointi ja liikunta",
|
||||||
"Sign up bonus": "Liittymisbonus",
|
"Sign up bonus": "Liittymisbonus",
|
||||||
"Sign up to Scandic Friends": "Liity Scandic Friends -jäseneksi",
|
"Sign up to Scandic Friends": "Liity Scandic Friends -jäseneksi",
|
||||||
|
"Signing up...": "Rekisteröidytään...",
|
||||||
"Skip to main content": "Siirry pääsisältöön",
|
"Skip to main content": "Siirry pääsisältöön",
|
||||||
"Something went wrong and we couldn't add your card. Please try again later.": "Jotain meni pieleen, emmekä voineet lisätä korttiasi. Yritä myöhemmin uudelleen.",
|
"Something went wrong and we couldn't add your card. Please try again later.": "Jotain meni pieleen, emmekä voineet lisätä korttiasi. Yritä myöhemmin uudelleen.",
|
||||||
"Something went wrong and we couldn't remove your card. Please try again later.": "Jotain meni pieleen, emmekä voineet poistaa korttiasi. Yritä myöhemmin uudelleen.",
|
"Something went wrong and we couldn't remove your card. Please try again later.": "Jotain meni pieleen, emmekä voineet poistaa korttiasi. Yritä myöhemmin uudelleen.",
|
||||||
@@ -454,6 +459,5 @@
|
|||||||
"to": "to",
|
"to": "to",
|
||||||
"uppercase letter": "iso kirjain",
|
"uppercase letter": "iso kirjain",
|
||||||
"{amount} out of {total}": "{amount}/{total}",
|
"{amount} out of {total}": "{amount}/{total}",
|
||||||
"{amount} {currency}": "{amount} {currency}",
|
"{amount} {currency}": "{amount} {currency}"
|
||||||
"{difference}{amount} {currency}": "{difference}{amount} {currency}"
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,8 +47,10 @@
|
|||||||
"Booking number": "Bestillingsnummer",
|
"Booking number": "Bestillingsnummer",
|
||||||
"Breakfast": "Frokost",
|
"Breakfast": "Frokost",
|
||||||
"Breakfast buffet": "Breakfast buffet",
|
"Breakfast buffet": "Breakfast buffet",
|
||||||
|
"Breakfast deal can be purchased at the hotel.": "Frokostdeal kan kjøpes på hotellet.",
|
||||||
"Breakfast excluded": "Frokost ekskludert",
|
"Breakfast excluded": "Frokost ekskludert",
|
||||||
"Breakfast included": "Frokost inkludert",
|
"Breakfast included": "Frokost inkludert",
|
||||||
|
"Breakfast is included.": "Frokost er inkludert.",
|
||||||
"Breakfast restaurant": "Breakfast restaurant",
|
"Breakfast restaurant": "Breakfast restaurant",
|
||||||
"Breakfast selection in next step.": "Frokostvalg i neste steg.",
|
"Breakfast selection in next step.": "Frokostvalg i neste steg.",
|
||||||
"Bus terminal": "Bussterminal",
|
"Bus terminal": "Bussterminal",
|
||||||
@@ -151,6 +153,7 @@
|
|||||||
"Hotel": "Hotel",
|
"Hotel": "Hotel",
|
||||||
"Hotel facilities": "Hotelfaciliteter",
|
"Hotel facilities": "Hotelfaciliteter",
|
||||||
"Hotel surroundings": "Hotellomgivelser",
|
"Hotel surroundings": "Hotellomgivelser",
|
||||||
|
"Hotel(s)": "{amount} {amount, plural, one {hotell} other {hoteller}}",
|
||||||
"Hotels": "Hoteller",
|
"Hotels": "Hoteller",
|
||||||
"How do you want to sleep?": "Hvordan vil du sove?",
|
"How do you want to sleep?": "Hvordan vil du sove?",
|
||||||
"How it works": "Hvordan det fungerer",
|
"How it works": "Hvordan det fungerer",
|
||||||
@@ -234,6 +237,7 @@
|
|||||||
"Number of parking spots": "Antall parkeringsplasser",
|
"Number of parking spots": "Antall parkeringsplasser",
|
||||||
"OTHER PAYMENT METHODS": "ANDRE BETALINGSMETODER",
|
"OTHER PAYMENT METHODS": "ANDRE BETALINGSMETODER",
|
||||||
"On your journey": "På reisen din",
|
"On your journey": "På reisen din",
|
||||||
|
"Only pay {amount} {currency}": "Bare betal {amount} {currency}",
|
||||||
"Open": "Åpen",
|
"Open": "Åpen",
|
||||||
"Open gift(s)": "{amount, plural, one {Åpen gave} other {Åpnen gaver}}",
|
"Open gift(s)": "{amount, plural, one {Åpen gave} other {Åpnen gaver}}",
|
||||||
"Open image gallery": "Åpne bildegalleri",
|
"Open image gallery": "Åpne bildegalleri",
|
||||||
@@ -331,6 +335,7 @@
|
|||||||
"Show wellness & exercise": "Vis velvære og trening",
|
"Show wellness & exercise": "Vis velvære og trening",
|
||||||
"Sign up bonus": "Velkomstbonus",
|
"Sign up bonus": "Velkomstbonus",
|
||||||
"Sign up to Scandic Friends": "Bli med i Scandic Friends",
|
"Sign up to Scandic Friends": "Bli med i Scandic Friends",
|
||||||
|
"Signing up...": "Registrerer...",
|
||||||
"Skip to main content": "Gå videre til hovedsiden",
|
"Skip to main content": "Gå videre til hovedsiden",
|
||||||
"Something went wrong and we couldn't add your card. Please try again later.": "Noe gikk galt, og vi kunne ikke legge til kortet ditt. Prøv igjen senere.",
|
"Something went wrong and we couldn't add your card. Please try again later.": "Noe gikk galt, og vi kunne ikke legge til kortet ditt. Prøv igjen senere.",
|
||||||
"Something went wrong and we couldn't remove your card. Please try again later.": "Noe gikk galt, og vi kunne ikke fjerne kortet ditt. Vennligst prøv igjen senere.",
|
"Something went wrong and we couldn't remove your card. Please try again later.": "Noe gikk galt, og vi kunne ikke fjerne kortet ditt. Vennligst prøv igjen senere.",
|
||||||
@@ -452,6 +457,5 @@
|
|||||||
"to": "til",
|
"to": "til",
|
||||||
"uppercase letter": "stor bokstav",
|
"uppercase letter": "stor bokstav",
|
||||||
"{amount} out of {total}": "{amount} av {total}",
|
"{amount} out of {total}": "{amount} av {total}",
|
||||||
"{amount} {currency}": "{amount} {currency}",
|
"{amount} {currency}": "{amount} {currency}"
|
||||||
"{difference}{amount} {currency}": "{difference}{amount} {currency}"
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,8 +47,10 @@
|
|||||||
"Booking number": "Bokningsnummer",
|
"Booking number": "Bokningsnummer",
|
||||||
"Breakfast": "Frukost",
|
"Breakfast": "Frukost",
|
||||||
"Breakfast buffet": "Frukostbuffé",
|
"Breakfast buffet": "Frukostbuffé",
|
||||||
|
"Breakfast deal can be purchased at the hotel.": "Frukostdeal kan köpas på hotellet.",
|
||||||
"Breakfast excluded": "Frukost ingår ej",
|
"Breakfast excluded": "Frukost ingår ej",
|
||||||
"Breakfast included": "Frukost ingår",
|
"Breakfast included": "Frukost ingår",
|
||||||
|
"Breakfast is included.": "Frukost ingår.",
|
||||||
"Breakfast restaurant": "Breakfast restaurant",
|
"Breakfast restaurant": "Breakfast restaurant",
|
||||||
"Breakfast selection in next step.": "Frukostval i nästa steg.",
|
"Breakfast selection in next step.": "Frukostval i nästa steg.",
|
||||||
"Bus terminal": "Bussterminal",
|
"Bus terminal": "Bussterminal",
|
||||||
@@ -151,6 +153,7 @@
|
|||||||
"Hotel": "Hotell",
|
"Hotel": "Hotell",
|
||||||
"Hotel facilities": "Hotellfaciliteter",
|
"Hotel facilities": "Hotellfaciliteter",
|
||||||
"Hotel surroundings": "Hotellomgivning",
|
"Hotel surroundings": "Hotellomgivning",
|
||||||
|
"Hotel(s)": "{amount} hotell",
|
||||||
"Hotels": "Hotell",
|
"Hotels": "Hotell",
|
||||||
"How do you want to sleep?": "Hur vill du sova?",
|
"How do you want to sleep?": "Hur vill du sova?",
|
||||||
"How it works": "Hur det fungerar",
|
"How it works": "Hur det fungerar",
|
||||||
@@ -234,6 +237,7 @@
|
|||||||
"Number of parking spots": "Antal parkeringsplatser",
|
"Number of parking spots": "Antal parkeringsplatser",
|
||||||
"OTHER PAYMENT METHODS": "ANDRE BETALINGSMETODER",
|
"OTHER PAYMENT METHODS": "ANDRE BETALINGSMETODER",
|
||||||
"On your journey": "På din resa",
|
"On your journey": "På din resa",
|
||||||
|
"Only pay {amount} {currency}": "Betala endast {amount} {currency}",
|
||||||
"Open": "Öppna",
|
"Open": "Öppna",
|
||||||
"Open gift(s)": "Öppna {amount, plural, one {gåva} other {gåvor}}",
|
"Open gift(s)": "Öppna {amount, plural, one {gåva} other {gåvor}}",
|
||||||
"Open image gallery": "Öppna bildgalleri",
|
"Open image gallery": "Öppna bildgalleri",
|
||||||
@@ -331,6 +335,7 @@
|
|||||||
"Show wellness & exercise": "Visa välbefinnande och träning",
|
"Show wellness & exercise": "Visa välbefinnande och träning",
|
||||||
"Sign up bonus": "Välkomstbonus",
|
"Sign up bonus": "Välkomstbonus",
|
||||||
"Sign up to Scandic Friends": "Bli medlem i Scandic Friends",
|
"Sign up to Scandic Friends": "Bli medlem i Scandic Friends",
|
||||||
|
"Signing up...": "Registrerar...",
|
||||||
"Skip to main content": "Fortsätt till huvudinnehåll",
|
"Skip to main content": "Fortsätt till huvudinnehåll",
|
||||||
"Something went wrong and we couldn't add your card. Please try again later.": "Något gick fel och vi kunde inte lägga till ditt kort. Försök igen senare.",
|
"Something went wrong and we couldn't add your card. Please try again later.": "Något gick fel och vi kunde inte lägga till ditt kort. Försök igen senare.",
|
||||||
"Something went wrong and we couldn't remove your card. Please try again later.": "Något gick fel och vi kunde inte ta bort ditt kort. Försök igen senare.",
|
"Something went wrong and we couldn't remove your card. Please try again later.": "Något gick fel och vi kunde inte ta bort ditt kort. Försök igen senare.",
|
||||||
@@ -455,6 +460,5 @@
|
|||||||
"types": "typer",
|
"types": "typer",
|
||||||
"uppercase letter": "stor bokstav",
|
"uppercase letter": "stor bokstav",
|
||||||
"{amount} out of {total}": "{amount} av {total}",
|
"{amount} out of {total}": "{amount} av {total}",
|
||||||
"{amount} {currency}": "{amount} {currency}",
|
"{amount} {currency}": "{amount} {currency}"
|
||||||
"{difference}{amount} {currency}": "{difference}{amount} {currency}"
|
|
||||||
}
|
}
|
||||||
|
|||||||
2
next-env.d.ts
vendored
2
next-env.d.ts
vendored
@@ -2,4 +2,4 @@
|
|||||||
/// <reference types="next/image-types/global" />
|
/// <reference types="next/image-types/global" />
|
||||||
|
|
||||||
// NOTE: This file should not be edited
|
// 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.
|
||||||
|
|||||||
88
package-lock.json
generated
88
package-lock.json
generated
@@ -41,7 +41,7 @@
|
|||||||
"graphql-tag": "^2.12.6",
|
"graphql-tag": "^2.12.6",
|
||||||
"immer": "10.1.1",
|
"immer": "10.1.1",
|
||||||
"libphonenumber-js": "^1.10.60",
|
"libphonenumber-js": "^1.10.60",
|
||||||
"next": "^14.2.7",
|
"next": "^14.2.18",
|
||||||
"next-auth": "^5.0.0-beta.19",
|
"next-auth": "^5.0.0-beta.19",
|
||||||
"react": "^18",
|
"react": "^18",
|
||||||
"react-day-picker": "^9.0.8",
|
"react-day-picker": "^9.0.8",
|
||||||
@@ -3425,9 +3425,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@next/env": {
|
"node_modules/@next/env": {
|
||||||
"version": "14.2.7",
|
"version": "14.2.18",
|
||||||
"resolved": "https://registry.npmjs.org/@next/env/-/env-14.2.7.tgz",
|
"resolved": "https://registry.npmjs.org/@next/env/-/env-14.2.18.tgz",
|
||||||
"integrity": "sha512-OTx9y6I3xE/eih+qtthppwLytmpJVPM5PPoJxChFsbjIEFXIayG0h/xLzefHGJviAa3Q5+Fd+9uYojKkHDKxoQ==",
|
"integrity": "sha512-2vWLOUwIPgoqMJKG6dt35fVXVhgM09tw4tK3/Q34GFXDrfiHlG7iS33VA4ggnjWxjiz9KV5xzfsQzJX6vGAekA==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@next/eslint-plugin-next": {
|
"node_modules/@next/eslint-plugin-next": {
|
||||||
@@ -3440,9 +3440,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@next/swc-darwin-arm64": {
|
"node_modules/@next/swc-darwin-arm64": {
|
||||||
"version": "14.2.7",
|
"version": "14.2.18",
|
||||||
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.2.7.tgz",
|
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.2.18.tgz",
|
||||||
"integrity": "sha512-UhZGcOyI9LE/tZL3h9rs/2wMZaaJKwnpAyegUVDGZqwsla6hMfeSj9ssBWQS9yA4UXun3pPhrFLVnw5KXZs3vw==",
|
"integrity": "sha512-tOBlDHCjGdyLf0ube/rDUs6VtwNOajaWV+5FV/ajPgrvHeisllEdymY/oDgv2cx561+gJksfMUtqf8crug7sbA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -3456,9 +3456,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@next/swc-darwin-x64": {
|
"node_modules/@next/swc-darwin-x64": {
|
||||||
"version": "14.2.7",
|
"version": "14.2.18",
|
||||||
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-14.2.7.tgz",
|
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-14.2.18.tgz",
|
||||||
"integrity": "sha512-ys2cUgZYRc+CbyDeLAaAdZgS7N1Kpyy+wo0b/gAj+SeOeaj0Lw/q+G1hp+DuDiDAVyxLBCJXEY/AkhDmtihUTA==",
|
"integrity": "sha512-uJCEjutt5VeJ30jjrHV1VIHCsbMYnEqytQgvREx+DjURd/fmKy15NaVK4aR/u98S1LGTnjq35lRTnRyygglxoA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -3472,9 +3472,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@next/swc-linux-arm64-gnu": {
|
"node_modules/@next/swc-linux-arm64-gnu": {
|
||||||
"version": "14.2.7",
|
"version": "14.2.18",
|
||||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.2.7.tgz",
|
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.2.18.tgz",
|
||||||
"integrity": "sha512-2xoWtE13sUJ3qrC1lwE/HjbDPm+kBQYFkkiVECJWctRASAHQ+NwjMzgrfqqMYHfMxFb5Wws3w9PqzZJqKFdWcQ==",
|
"integrity": "sha512-IL6rU8vnBB+BAm6YSWZewc+qvdL1EaA+VhLQ6tlUc0xp+kkdxQrVqAnh8Zek1ccKHlTDFRyAft0e60gteYmQ4A==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -3488,9 +3488,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@next/swc-linux-arm64-musl": {
|
"node_modules/@next/swc-linux-arm64-musl": {
|
||||||
"version": "14.2.7",
|
"version": "14.2.18",
|
||||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.2.7.tgz",
|
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.2.18.tgz",
|
||||||
"integrity": "sha512-+zJ1gJdl35BSAGpkCbfyiY6iRTaPrt3KTl4SF/B1NyELkqqnrNX6cp4IjjjxKpd64/7enI0kf6b9O1Uf3cL0pw==",
|
"integrity": "sha512-RCaENbIZqKKqTlL8KNd+AZV/yAdCsovblOpYFp0OJ7ZxgLNbV5w23CUU1G5On+0fgafrsGcW+GdMKdFjaRwyYA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -3504,9 +3504,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@next/swc-linux-x64-gnu": {
|
"node_modules/@next/swc-linux-x64-gnu": {
|
||||||
"version": "14.2.7",
|
"version": "14.2.18",
|
||||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.2.7.tgz",
|
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.2.18.tgz",
|
||||||
"integrity": "sha512-m6EBqrskeMUzykBrv0fDX/28lWIBGhMzOYaStp0ihkjzIYJiKUOzVYD1gULHc8XDf5EMSqoH/0/TRAgXqpQwmw==",
|
"integrity": "sha512-3kmv8DlyhPRCEBM1Vavn8NjyXtMeQ49ID0Olr/Sut7pgzaQTo4h01S7Z8YNE0VtbowyuAL26ibcz0ka6xCTH5g==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -3520,9 +3520,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@next/swc-linux-x64-musl": {
|
"node_modules/@next/swc-linux-x64-musl": {
|
||||||
"version": "14.2.7",
|
"version": "14.2.18",
|
||||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.2.7.tgz",
|
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.2.18.tgz",
|
||||||
"integrity": "sha512-gUu0viOMvMlzFRz1r1eQ7Ql4OE+hPOmA7smfZAhn8vC4+0swMZaZxa9CSIozTYavi+bJNDZ3tgiSdMjmMzRJlQ==",
|
"integrity": "sha512-mliTfa8seVSpTbVEcKEXGjC18+TDII8ykW4a36au97spm9XMPqQTpdGPNBJ9RySSFw9/hLuaCMByluQIAnkzlw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -3536,9 +3536,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@next/swc-win32-arm64-msvc": {
|
"node_modules/@next/swc-win32-arm64-msvc": {
|
||||||
"version": "14.2.7",
|
"version": "14.2.18",
|
||||||
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.2.7.tgz",
|
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.2.18.tgz",
|
||||||
"integrity": "sha512-PGbONHIVIuzWlYmLvuFKcj+8jXnLbx4WrlESYlVnEzDsa3+Q2hI1YHoXaSmbq0k4ZwZ7J6sWNV4UZfx1OeOlbQ==",
|
"integrity": "sha512-J5g0UFPbAjKYmqS3Cy7l2fetFmWMY9Oao32eUsBPYohts26BdrMUyfCJnZFQkX9npYaHNDOWqZ6uV9hSDPw9NA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -3552,9 +3552,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@next/swc-win32-ia32-msvc": {
|
"node_modules/@next/swc-win32-ia32-msvc": {
|
||||||
"version": "14.2.7",
|
"version": "14.2.18",
|
||||||
"resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.7.tgz",
|
"resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.18.tgz",
|
||||||
"integrity": "sha512-BiSY5umlx9ed5RQDoHcdbuKTUkuFORDqzYKPHlLeS+STUWQKWziVOn3Ic41LuTBvqE0TRJPKpio9GSIblNR+0w==",
|
"integrity": "sha512-Ynxuk4ZgIpdcN7d16ivJdjsDG1+3hTvK24Pp8DiDmIa2+A4CfhJSEHHVndCHok6rnLUzAZD+/UOKESQgTsAZGg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"ia32"
|
"ia32"
|
||||||
],
|
],
|
||||||
@@ -3568,9 +3568,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@next/swc-win32-x64-msvc": {
|
"node_modules/@next/swc-win32-x64-msvc": {
|
||||||
"version": "14.2.7",
|
"version": "14.2.18",
|
||||||
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.2.7.tgz",
|
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.2.18.tgz",
|
||||||
"integrity": "sha512-pxsI23gKWRt/SPHFkDEsP+w+Nd7gK37Hpv0ngc5HpWy2e7cKx9zR/+Q2ptAUqICNTecAaGWvmhway7pj/JLEWA==",
|
"integrity": "sha512-dtRGMhiU9TN5nyhwzce+7c/4CCeykYS+ipY/4mIrGzJ71+7zNo55ZxCB7cAVuNqdwtYniFNR2c9OFQ6UdFIMcg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -15460,12 +15460,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/next": {
|
"node_modules/next": {
|
||||||
"version": "14.2.7",
|
"version": "14.2.18",
|
||||||
"resolved": "https://registry.npmjs.org/next/-/next-14.2.7.tgz",
|
"resolved": "https://registry.npmjs.org/next/-/next-14.2.18.tgz",
|
||||||
"integrity": "sha512-4Qy2aK0LwH4eQiSvQWyKuC7JXE13bIopEQesWE0c/P3uuNRnZCQanI0vsrMLmUQJLAto+A+/8+sve2hd+BQuOQ==",
|
"integrity": "sha512-H9qbjDuGivUDEnK6wa+p2XKO+iMzgVgyr9Zp/4Iv29lKa+DYaxJGjOeEA+5VOvJh/M7HLiskehInSa0cWxVXUw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@next/env": "14.2.7",
|
"@next/env": "14.2.18",
|
||||||
"@swc/helpers": "0.5.5",
|
"@swc/helpers": "0.5.5",
|
||||||
"busboy": "1.6.0",
|
"busboy": "1.6.0",
|
||||||
"caniuse-lite": "^1.0.30001579",
|
"caniuse-lite": "^1.0.30001579",
|
||||||
@@ -15480,15 +15480,15 @@
|
|||||||
"node": ">=18.17.0"
|
"node": ">=18.17.0"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"@next/swc-darwin-arm64": "14.2.7",
|
"@next/swc-darwin-arm64": "14.2.18",
|
||||||
"@next/swc-darwin-x64": "14.2.7",
|
"@next/swc-darwin-x64": "14.2.18",
|
||||||
"@next/swc-linux-arm64-gnu": "14.2.7",
|
"@next/swc-linux-arm64-gnu": "14.2.18",
|
||||||
"@next/swc-linux-arm64-musl": "14.2.7",
|
"@next/swc-linux-arm64-musl": "14.2.18",
|
||||||
"@next/swc-linux-x64-gnu": "14.2.7",
|
"@next/swc-linux-x64-gnu": "14.2.18",
|
||||||
"@next/swc-linux-x64-musl": "14.2.7",
|
"@next/swc-linux-x64-musl": "14.2.18",
|
||||||
"@next/swc-win32-arm64-msvc": "14.2.7",
|
"@next/swc-win32-arm64-msvc": "14.2.18",
|
||||||
"@next/swc-win32-ia32-msvc": "14.2.7",
|
"@next/swc-win32-ia32-msvc": "14.2.18",
|
||||||
"@next/swc-win32-x64-msvc": "14.2.7"
|
"@next/swc-win32-x64-msvc": "14.2.18"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@opentelemetry/api": "^1.1.0",
|
"@opentelemetry/api": "^1.1.0",
|
||||||
|
|||||||
@@ -56,7 +56,7 @@
|
|||||||
"graphql-tag": "^2.12.6",
|
"graphql-tag": "^2.12.6",
|
||||||
"immer": "10.1.1",
|
"immer": "10.1.1",
|
||||||
"libphonenumber-js": "^1.10.60",
|
"libphonenumber-js": "^1.10.60",
|
||||||
"next": "^14.2.7",
|
"next": "^14.2.18",
|
||||||
"next-auth": "^5.0.0-beta.19",
|
"next-auth": "^5.0.0-beta.19",
|
||||||
"react": "^18",
|
"react": "^18",
|
||||||
"react-day-picker": "^9.0.8",
|
"react-day-picker": "^9.0.8",
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
import { useRouter } from "next/navigation"
|
||||||
import { useRef } from "react"
|
import { useRef } from "react"
|
||||||
|
|
||||||
import { useDetailsStore } from "@/stores/details"
|
import { useDetailsStore } from "@/stores/details"
|
||||||
@@ -14,6 +15,7 @@ export default function StepsProvider({
|
|||||||
breakfastPackages,
|
breakfastPackages,
|
||||||
children,
|
children,
|
||||||
isMember,
|
isMember,
|
||||||
|
searchParams,
|
||||||
step,
|
step,
|
||||||
}: StepsProviderProps) {
|
}: StepsProviderProps) {
|
||||||
const storeRef = useRef<StepsStore>()
|
const storeRef = useRef<StepsStore>()
|
||||||
@@ -21,6 +23,7 @@ export default function StepsProvider({
|
|||||||
const updateBreakfast = useDetailsStore(
|
const updateBreakfast = useDetailsStore(
|
||||||
(state) => state.actions.updateBreakfast
|
(state) => state.actions.updateBreakfast
|
||||||
)
|
)
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
if (!storeRef.current) {
|
if (!storeRef.current) {
|
||||||
const noBedChoices = bedTypes.length === 1
|
const noBedChoices = bedTypes.length === 1
|
||||||
@@ -41,7 +44,9 @@ export default function StepsProvider({
|
|||||||
step,
|
step,
|
||||||
isMember,
|
isMember,
|
||||||
noBedChoices,
|
noBedChoices,
|
||||||
noBreakfast
|
noBreakfast,
|
||||||
|
searchParams,
|
||||||
|
router.push
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ import { AlertTypeEnum } from "@/types/enums/alert"
|
|||||||
import { CurrencyEnum } from "@/types/enums/currency"
|
import { CurrencyEnum } from "@/types/enums/currency"
|
||||||
import { FacilityEnum } from "@/types/enums/facilities"
|
import { FacilityEnum } from "@/types/enums/facilities"
|
||||||
import { PackageTypeEnum } from "@/types/enums/packages"
|
import { PackageTypeEnum } from "@/types/enums/packages"
|
||||||
import { PointOfInterestCategoryNameEnum } from "@/types/hotel"
|
|
||||||
|
|
||||||
const ratingsSchema = z
|
const ratingsSchema = z
|
||||||
.object({
|
.object({
|
||||||
@@ -199,14 +198,12 @@ const rewardNightSchema = z.object({
|
|||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
|
|
||||||
const poiCategoryNames = z.nativeEnum(PointOfInterestCategoryNameEnum)
|
|
||||||
|
|
||||||
export const pointOfInterestSchema = z
|
export const pointOfInterestSchema = z
|
||||||
.object({
|
.object({
|
||||||
name: z.string(),
|
name: z.string(),
|
||||||
distance: z.number(),
|
distance: z.number(),
|
||||||
category: z.object({
|
category: z.object({
|
||||||
name: poiCategoryNames,
|
name: z.string(),
|
||||||
group: z.string(),
|
group: z.string(),
|
||||||
}),
|
}),
|
||||||
location: locationSchema,
|
location: locationSchema,
|
||||||
@@ -515,7 +512,16 @@ export const productTypePriceSchema = z.object({
|
|||||||
|
|
||||||
const productSchema = z.object({
|
const productSchema = z.object({
|
||||||
productType: z.object({
|
productType: z.object({
|
||||||
public: productTypePriceSchema,
|
public: productTypePriceSchema.default({
|
||||||
|
rateCode: "",
|
||||||
|
rateType: "",
|
||||||
|
localPrice: {
|
||||||
|
currency: "SEK",
|
||||||
|
pricePerNight: 0,
|
||||||
|
pricePerStay: 0,
|
||||||
|
},
|
||||||
|
requestedPrice: undefined,
|
||||||
|
}),
|
||||||
member: productTypePriceSchema.optional(),
|
member: productTypePriceSchema.optional(),
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -731,7 +731,7 @@ export const hotelQueryRouter = router({
|
|||||||
|
|
||||||
const rateTypes = selectedRoom.products.find(
|
const rateTypes = selectedRoom.products.find(
|
||||||
(rate) =>
|
(rate) =>
|
||||||
rate.productType.public.rateCode === rateCode ||
|
rate.productType.public?.rateCode === rateCode ||
|
||||||
rate.productType.member?.rateCode === rateCode
|
rate.productType.member?.rateCode === rateCode
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -12,39 +12,34 @@ import {
|
|||||||
type Countries,
|
type Countries,
|
||||||
} from "./output"
|
} 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 { Lang } from "@/constants/languages"
|
||||||
import type { Endpoint } from "@/lib/api/endpoints"
|
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(
|
export function getPoiGroupByCategoryName(category: string) {
|
||||||
category: PointOfInterestCategoryNameEnum
|
|
||||||
) {
|
|
||||||
switch (category) {
|
switch (category) {
|
||||||
case PointOfInterestCategoryNameEnum.AIRPORT:
|
case "Airport":
|
||||||
case PointOfInterestCategoryNameEnum.BUS_TERMINAL:
|
case "Bus terminal":
|
||||||
case PointOfInterestCategoryNameEnum.TRANSPORTATIONS:
|
case "Transportations":
|
||||||
return PointOfInterestGroupEnum.PUBLIC_TRANSPORT
|
return PointOfInterestGroupEnum.PUBLIC_TRANSPORT
|
||||||
case PointOfInterestCategoryNameEnum.AMUSEMENT_PARK:
|
case "Amusement park":
|
||||||
case PointOfInterestCategoryNameEnum.MUSEUM:
|
case "Museum":
|
||||||
case PointOfInterestCategoryNameEnum.SPORTS:
|
case "Sports":
|
||||||
case PointOfInterestCategoryNameEnum.THEATRE:
|
case "Theatre":
|
||||||
case PointOfInterestCategoryNameEnum.TOURIST:
|
case "Tourist":
|
||||||
case PointOfInterestCategoryNameEnum.ZOO:
|
case "Zoo":
|
||||||
return PointOfInterestGroupEnum.ATTRACTIONS
|
return PointOfInterestGroupEnum.ATTRACTIONS
|
||||||
case PointOfInterestCategoryNameEnum.NEARBY_COMPANIES:
|
case "Nearby companies":
|
||||||
case PointOfInterestCategoryNameEnum.FAIR:
|
case "Fair":
|
||||||
return PointOfInterestGroupEnum.BUSINESS
|
return PointOfInterestGroupEnum.BUSINESS
|
||||||
case PointOfInterestCategoryNameEnum.PARKING_GARAGE:
|
case "Parking / Garage":
|
||||||
return PointOfInterestGroupEnum.PARKING
|
return PointOfInterestGroupEnum.PARKING
|
||||||
case PointOfInterestCategoryNameEnum.SHOPPING:
|
case "Shopping":
|
||||||
case PointOfInterestCategoryNameEnum.RESTAURANT:
|
case "Restaurant":
|
||||||
return PointOfInterestGroupEnum.SHOPPING_DINING
|
return PointOfInterestGroupEnum.SHOPPING_DINING
|
||||||
case PointOfInterestCategoryNameEnum.HOSPITAL:
|
case "Hospital":
|
||||||
default:
|
default:
|
||||||
return PointOfInterestGroupEnum.LOCATION
|
return PointOfInterestGroupEnum.LOCATION
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
import { z } from "zod"
|
import { z } from "zod"
|
||||||
|
|
||||||
|
import { Lang } from "@/constants/languages"
|
||||||
|
|
||||||
|
import { signUpSchema } from "@/components/Forms/Signup/schema"
|
||||||
|
|
||||||
// Query
|
// Query
|
||||||
export const staysInput = z
|
export const staysInput = z
|
||||||
.object({
|
.object({
|
||||||
@@ -35,3 +39,19 @@ export const saveCreditCardInput = z.object({
|
|||||||
transactionId: z.string(),
|
transactionId: z.string(),
|
||||||
merchantId: z.string().optional(),
|
merchantId: z.string().optional(),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
export const signupInput = signUpSchema
|
||||||
|
.extend({
|
||||||
|
language: z.nativeEnum(Lang),
|
||||||
|
})
|
||||||
|
.omit({ termsAccepted: true })
|
||||||
|
.transform((data) => ({
|
||||||
|
...data,
|
||||||
|
phoneNumber: data.phoneNumber.replace(/\s+/g, ""),
|
||||||
|
address: {
|
||||||
|
...data.address,
|
||||||
|
city: "",
|
||||||
|
country: "",
|
||||||
|
streetAddress: "",
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|||||||
@@ -1,17 +1,20 @@
|
|||||||
import { metrics } from "@opentelemetry/api"
|
import { metrics } from "@opentelemetry/api"
|
||||||
|
|
||||||
|
import { signupVerify } from "@/constants/routes/signup"
|
||||||
import { env } from "@/env/server"
|
import { env } from "@/env/server"
|
||||||
import * as api from "@/lib/api"
|
import * as api from "@/lib/api"
|
||||||
|
import { serverErrorByStatus } from "@/server/errors/trpc"
|
||||||
import {
|
import {
|
||||||
initiateSaveCardSchema,
|
initiateSaveCardSchema,
|
||||||
subscriberIdSchema,
|
subscriberIdSchema,
|
||||||
} from "@/server/routers/user/output"
|
} from "@/server/routers/user/output"
|
||||||
import { protectedProcedure, router } from "@/server/trpc"
|
import { protectedProcedure, router, serviceProcedure } from "@/server/trpc"
|
||||||
|
|
||||||
import {
|
import {
|
||||||
addCreditCardInput,
|
addCreditCardInput,
|
||||||
deleteCreditCardInput,
|
deleteCreditCardInput,
|
||||||
saveCreditCardInput,
|
saveCreditCardInput,
|
||||||
|
signupInput,
|
||||||
} from "./input"
|
} from "./input"
|
||||||
|
|
||||||
const meter = metrics.getMeter("trpc.user")
|
const meter = metrics.getMeter("trpc.user")
|
||||||
@@ -24,6 +27,9 @@ const generatePreferencesLinkSuccessCounter = meter.createCounter(
|
|||||||
const generatePreferencesLinkFailCounter = meter.createCounter(
|
const generatePreferencesLinkFailCounter = meter.createCounter(
|
||||||
"trpc.user.generatePreferencesLink-fail"
|
"trpc.user.generatePreferencesLink-fail"
|
||||||
)
|
)
|
||||||
|
const signupCounter = meter.createCounter("trpc.user.signup")
|
||||||
|
const signupSuccessCounter = meter.createCounter("trpc.user.signup-success")
|
||||||
|
const signupFailCounter = meter.createCounter("trpc.user.signup-fail")
|
||||||
|
|
||||||
export const userMutationRouter = router({
|
export const userMutationRouter = router({
|
||||||
creditCard: router({
|
creditCard: router({
|
||||||
@@ -208,4 +214,46 @@ export const userMutationRouter = router({
|
|||||||
generatePreferencesLinkSuccessCounter.add(1)
|
generatePreferencesLinkSuccessCounter.add(1)
|
||||||
return preferencesLink.toString()
|
return preferencesLink.toString()
|
||||||
}),
|
}),
|
||||||
|
signup: serviceProcedure.input(signupInput).mutation(async function ({
|
||||||
|
ctx,
|
||||||
|
input,
|
||||||
|
}) {
|
||||||
|
signupCounter.add(1)
|
||||||
|
|
||||||
|
const apiResponse = await api.post(api.endpoints.v1.Profile.profile, {
|
||||||
|
body: input,
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${ctx.serviceToken}`,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!apiResponse.ok) {
|
||||||
|
const text = await apiResponse.text()
|
||||||
|
signupFailCounter.add(1, {
|
||||||
|
error_type: "http_error",
|
||||||
|
error: JSON.stringify({
|
||||||
|
status: apiResponse.status,
|
||||||
|
statusText: apiResponse.statusText,
|
||||||
|
error: text,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
console.error(
|
||||||
|
"api.user.signup api error",
|
||||||
|
JSON.stringify({
|
||||||
|
error: {
|
||||||
|
status: apiResponse.status,
|
||||||
|
statusText: apiResponse.statusText,
|
||||||
|
error: text,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
)
|
||||||
|
throw serverErrorByStatus(apiResponse.status, text)
|
||||||
|
}
|
||||||
|
signupSuccessCounter.add(1)
|
||||||
|
console.info("api.user.signup success")
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
redirectUrl: signupVerify[input.language],
|
||||||
|
}
|
||||||
|
}),
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { z } from "zod"
|
import { z } from "zod"
|
||||||
|
|
||||||
import { countriesMap } from "@/components/TempDesignSystem/Form/Country/countries"
|
import { countriesMap } from "@/components/TempDesignSystem/Form/Country/countries"
|
||||||
|
import { passwordValidator } from "@/utils/passwordValidator"
|
||||||
|
import { phoneValidator } from "@/utils/phoneValidator"
|
||||||
import { getMembership } from "@/utils/user"
|
import { getMembership } from "@/utils/user"
|
||||||
|
|
||||||
export const membershipSchema = z.object({
|
export const membershipSchema = z.object({
|
||||||
|
|||||||
@@ -121,7 +121,7 @@ export const safeProtectedProcedure = t.procedure.use(async function (opts) {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
export const serviceProcedure = t.procedure.use(async (opts) => {
|
export const serviceProcedure = t.procedure.use(async function (opts) {
|
||||||
const { access_token } = await getServiceToken()
|
const { access_token } = await getServiceToken()
|
||||||
if (!access_token) {
|
if (!access_token) {
|
||||||
throw internalServerError(`[serviceProcedure] No service token`)
|
throw internalServerError(`[serviceProcedure] No service token`)
|
||||||
|
|||||||
@@ -94,7 +94,6 @@ export function createDetailsStore(
|
|||||||
state.data.membershipNo = data.membershipNo
|
state.data.membershipNo = data.membershipNo
|
||||||
}
|
}
|
||||||
state.data.phoneNumber = data.phoneNumber
|
state.data.phoneNumber = data.phoneNumber
|
||||||
state.data.termsAccepted = data.termsAccepted
|
|
||||||
state.data.zipCode = data.zipCode
|
state.data.zipCode = data.zipCode
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
"use client"
|
"use client"
|
||||||
import merge from "deepmerge"
|
import merge from "deepmerge"
|
||||||
import { produce } from "immer"
|
import { produce } from "immer"
|
||||||
|
import { AppRouterInstance } from "next/dist/shared/lib/app-router-context.shared-runtime"
|
||||||
import { useContext } from "react"
|
import { useContext } from "react"
|
||||||
import { create, useStore } from "zustand"
|
import { create, useStore } from "zustand"
|
||||||
|
|
||||||
@@ -18,17 +19,13 @@ import { StepEnum } from "@/types/enums/step"
|
|||||||
import type { DetailsState } from "@/types/stores/details"
|
import type { DetailsState } from "@/types/stores/details"
|
||||||
import type { StepState } from "@/types/stores/steps"
|
import type { StepState } from "@/types/stores/steps"
|
||||||
|
|
||||||
function push(data: Record<string, string>, url: string) {
|
|
||||||
if (typeof window !== "undefined") {
|
|
||||||
window.history.pushState(data, "", url + window.location.search)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createStepsStore(
|
export function createStepsStore(
|
||||||
currentStep: StepEnum,
|
currentStep: StepEnum,
|
||||||
isMember: boolean,
|
isMember: boolean,
|
||||||
noBedChoices: boolean,
|
noBedChoices: boolean,
|
||||||
noBreakfast: boolean
|
noBreakfast: boolean,
|
||||||
|
searchParams: string,
|
||||||
|
push: AppRouterInstance["push"]
|
||||||
) {
|
) {
|
||||||
const isBrowser = typeof window !== "undefined"
|
const isBrowser = typeof window !== "undefined"
|
||||||
const steps = [
|
const steps = [
|
||||||
@@ -51,14 +48,14 @@ export function createStepsStore(
|
|||||||
steps.splice(1, 1)
|
steps.splice(1, 1)
|
||||||
if (currentStep === StepEnum.breakfast) {
|
if (currentStep === StepEnum.breakfast) {
|
||||||
currentStep = steps[1]
|
currentStep = steps[1]
|
||||||
push({ step: currentStep }, currentStep)
|
push(`${currentStep}?${searchParams}`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (noBedChoices) {
|
if (noBedChoices) {
|
||||||
if (currentStep === StepEnum.selectBed) {
|
if (currentStep === StepEnum.selectBed) {
|
||||||
currentStep = steps[1]
|
currentStep = steps[1]
|
||||||
push({ step: currentStep }, currentStep)
|
push(`${currentStep}?${searchParams}`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -94,7 +91,7 @@ export function createStepsStore(
|
|||||||
if (!validPaths.includes(currentStep) && isBrowser) {
|
if (!validPaths.includes(currentStep) && isBrowser) {
|
||||||
// We will always have at least one valid path
|
// We will always have at least one valid path
|
||||||
currentStep = validPaths.pop()!
|
currentStep = validPaths.pop()!
|
||||||
push({ step: currentStep }, currentStep)
|
push(`${currentStep}?${searchParams}`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user