Merge branch 'develop' of bitbucket.org:scandic-swap/web into feature/tracking
This commit is contained in:
@@ -1,14 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import { useEffect } from "react"
|
||||
|
||||
import LoadingSpinner from "@/components/LoadingSpinner"
|
||||
|
||||
export default function LogoutInterceptedRoute() {
|
||||
// Reload the browser on logout in order to flush router cache. This is to make sure we don't show stale user specific data.
|
||||
useEffect(() => {
|
||||
window.location.reload()
|
||||
}, [])
|
||||
|
||||
return <LoadingSpinner />
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
import { serverClient } from "@/lib/trpc/server"
|
||||
import { getHotelDataSchema } from "@/server/routers/hotels/output"
|
||||
import tempHotelData from "@/server/routers/hotels/tempHotelData.json"
|
||||
|
||||
import HotelSelectionHeader from "@/components/HotelReservation/HotelSelectionHeader"
|
||||
import BedSelection from "@/components/HotelReservation/SelectRate/BedSelection"
|
||||
import BreakfastSelection from "@/components/HotelReservation/SelectRate/BreakfastSelection"
|
||||
import Details from "@/components/HotelReservation/SelectRate/Details"
|
||||
@@ -79,7 +81,7 @@ export default async function SectionsPage({
|
||||
setLang(params.lang)
|
||||
|
||||
// TODO: Use real endpoint.
|
||||
const hotel = tempHotelData.data.attributes
|
||||
const hotel = getHotelDataSchema.parse(tempHotelData)
|
||||
|
||||
const rooms = await serverClient().hotel.rates.get({
|
||||
// TODO: pass the correct hotel ID and all other parameters that should be included in the search
|
||||
@@ -107,8 +109,7 @@ export default async function SectionsPage({
|
||||
|
||||
return (
|
||||
<div>
|
||||
{/* TODO: Add Hotel Listing Card */}
|
||||
<div>Hotel Listing Card TBI</div>
|
||||
<HotelSelectionHeader hotel={hotel.data.attributes} />
|
||||
|
||||
<div className={styles.content}>
|
||||
<div className={styles.main}>
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
.main {
|
||||
display: grid;
|
||||
background-color: var(--Scandic-Brand-Warm-White);
|
||||
min-height: 100dvh;
|
||||
grid-template-columns: 420px 1fr;
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
import { env } from "@/env/server"
|
||||
|
||||
import {
|
||||
fetchAvailableHotels,
|
||||
getFiltersFromHotels,
|
||||
} from "@/app/[lang]/(live)/(public)/hotelreservation/select-hotel/utils"
|
||||
import SelectHotelMap from "@/components/HotelReservation/SelectHotel/SelectHotelMap"
|
||||
import { setLang } from "@/i18n/serverContext"
|
||||
|
||||
import styles from "./page.module.css"
|
||||
|
||||
import {
|
||||
PointOfInterest,
|
||||
PointOfInterestCategoryNameEnum,
|
||||
PointOfInterestGroupEnum,
|
||||
} from "@/types/hotel"
|
||||
import { LangParams, PageArgs } from "@/types/params"
|
||||
|
||||
export default async function SelectHotelMapPage({
|
||||
params,
|
||||
}: PageArgs<LangParams, {}>) {
|
||||
const googleMapId = env.GOOGLE_DYNAMIC_MAP_ID
|
||||
const googleMapsApiKey = env.GOOGLE_STATIC_MAP_KEY
|
||||
setLang(params.lang)
|
||||
|
||||
const hotels = await fetchAvailableHotels({
|
||||
cityId: "8ec4bba3-1c38-4606-82d1-bbe3f6738e54",
|
||||
roomStayStartDate: "2024-11-02",
|
||||
roomStayEndDate: "2024-11-03",
|
||||
adults: 1,
|
||||
})
|
||||
|
||||
const filters = getFiltersFromHotels(hotels)
|
||||
|
||||
// TODO: this is just a quick transformation to get something there. May need rework
|
||||
const pointOfInterests: PointOfInterest[] = hotels.map((hotel) => ({
|
||||
coordinates: {
|
||||
lat: hotel.hotelData.location.latitude,
|
||||
lng: hotel.hotelData.location.longitude,
|
||||
},
|
||||
name: hotel.hotelData.name,
|
||||
distance: hotel.hotelData.location.distanceToCentre,
|
||||
categoryName: PointOfInterestCategoryNameEnum.HOTEL,
|
||||
group: PointOfInterestGroupEnum.LOCATION,
|
||||
}))
|
||||
|
||||
return (
|
||||
<main className={styles.main}>
|
||||
<SelectHotelMap
|
||||
apiKey={googleMapsApiKey}
|
||||
// TODO: use correct coordinates. The city center?
|
||||
coordinates={{ lat: 59.32, lng: 18.01 }}
|
||||
pointsOfInterest={pointOfInterests}
|
||||
mapId={googleMapId}
|
||||
/>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
@@ -1,45 +1,21 @@
|
||||
import { serverClient } from "@/lib/trpc/server"
|
||||
import { selectHotelMap } from "@/constants/routes/hotelReservation"
|
||||
|
||||
import {
|
||||
fetchAvailableHotels,
|
||||
getFiltersFromHotels,
|
||||
} from "@/app/[lang]/(live)/(public)/hotelreservation/select-hotel/utils"
|
||||
import HotelCardListing from "@/components/HotelReservation/HotelCardListing"
|
||||
import HotelFilter from "@/components/HotelReservation/SelectHotel/HotelFilter"
|
||||
import { ChevronRightIcon } from "@/components/Icons"
|
||||
import StaticMap from "@/components/Maps/StaticMap"
|
||||
import Link from "@/components/TempDesignSystem/Link"
|
||||
import { getIntl } from "@/i18n"
|
||||
import { getLang, setLang } from "@/i18n/serverContext"
|
||||
import { setLang } from "@/i18n/serverContext"
|
||||
|
||||
import styles from "./page.module.css"
|
||||
|
||||
import { AvailabilityInput } from "@/types/components/hotelReservation/selectHotel/availabilityInput"
|
||||
import { HotelData } from "@/types/components/hotelReservation/selectHotel/hotelCardListingProps"
|
||||
import { LangParams, PageArgs } from "@/types/params"
|
||||
|
||||
async function getAvailableHotels(
|
||||
input: AvailabilityInput
|
||||
): Promise<HotelData[]> {
|
||||
const getAvailableHotels = await serverClient().hotel.availability.get(input)
|
||||
|
||||
if (!getAvailableHotels) throw new Error()
|
||||
|
||||
const { availability } = getAvailableHotels
|
||||
|
||||
const hotels = availability.map(async (hotel) => {
|
||||
const hotelData = await serverClient().hotel.hotelData.get({
|
||||
hotelId: hotel.hotelId.toString(),
|
||||
language: getLang(),
|
||||
})
|
||||
|
||||
if (!hotelData) throw new Error()
|
||||
|
||||
return {
|
||||
hotelData: hotelData.data.attributes,
|
||||
price: hotel.bestPricePerNight,
|
||||
}
|
||||
})
|
||||
|
||||
return await Promise.all(hotels)
|
||||
}
|
||||
|
||||
export default async function SelectHotelPage({
|
||||
params,
|
||||
}: PageArgs<LangParams>) {
|
||||
@@ -48,54 +24,34 @@ export default async function SelectHotelPage({
|
||||
const tempSearchTerm = "Stockholm"
|
||||
const intl = await getIntl()
|
||||
|
||||
const hotels = await getAvailableHotels({
|
||||
const hotels = await fetchAvailableHotels({
|
||||
cityId: "8ec4bba3-1c38-4606-82d1-bbe3f6738e54",
|
||||
roomStayStartDate: "2024-11-02",
|
||||
roomStayEndDate: "2024-11-03",
|
||||
adults: 1,
|
||||
})
|
||||
|
||||
const filters = hotels.flatMap((data) => data.hotelData.detailedFacilities)
|
||||
|
||||
const filterIds = [...new Set(filters.map((data) => data.id))]
|
||||
const filterList: {
|
||||
name: string
|
||||
id: number
|
||||
applyToAllHotels: boolean
|
||||
public: boolean
|
||||
icon: string
|
||||
sortOrder: number
|
||||
code?: string
|
||||
iconName?: string
|
||||
}[] = filterIds
|
||||
.map((id) => filters.find((find) => find.id === id))
|
||||
.filter(
|
||||
(
|
||||
filter
|
||||
): filter is {
|
||||
name: string
|
||||
id: number
|
||||
applyToAllHotels: boolean
|
||||
public: boolean
|
||||
icon: string
|
||||
sortOrder: number
|
||||
code?: string
|
||||
iconName?: string
|
||||
} => filter !== undefined
|
||||
)
|
||||
const filterList = getFiltersFromHotels(hotels)
|
||||
|
||||
return (
|
||||
<main className={styles.main}>
|
||||
<section className={styles.section}>
|
||||
<StaticMap
|
||||
city={tempSearchTerm}
|
||||
width={340}
|
||||
height={180}
|
||||
zoomLevel={11}
|
||||
mapType="roadmap"
|
||||
altText={`Map of ${tempSearchTerm} city center`}
|
||||
/>
|
||||
<Link className={styles.link} color="burgundy" href="#">
|
||||
<Link href={selectHotelMap[params.lang]} keepSearchParams>
|
||||
<StaticMap
|
||||
city={tempSearchTerm}
|
||||
width={340}
|
||||
height={180}
|
||||
zoomLevel={11}
|
||||
mapType="roadmap"
|
||||
altText={`Map of ${tempSearchTerm} city center`}
|
||||
/>
|
||||
</Link>
|
||||
<Link
|
||||
className={styles.link}
|
||||
color="burgundy"
|
||||
href={selectHotelMap[params.lang]}
|
||||
keepSearchParams
|
||||
>
|
||||
{intl.formatMessage({ id: "Show map" })}
|
||||
<ChevronRightIcon color="burgundy" />
|
||||
</Link>
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
import { serverClient } from "@/lib/trpc/server"
|
||||
|
||||
import { getLang } from "@/i18n/serverContext"
|
||||
|
||||
import { AvailabilityInput } from "@/types/components/hotelReservation/selectHotel/availabilityInput"
|
||||
import { HotelData } from "@/types/components/hotelReservation/selectHotel/hotelCardListingProps"
|
||||
import { Filter } from "@/types/components/hotelReservation/selectHotel/hotelFilters"
|
||||
|
||||
export async function fetchAvailableHotels(
|
||||
input: AvailabilityInput
|
||||
): Promise<HotelData[]> {
|
||||
const availableHotels = await serverClient().hotel.availability.get(input)
|
||||
|
||||
if (!availableHotels) throw new Error()
|
||||
|
||||
const language = getLang()
|
||||
const hotels = availableHotels.availability.map(async (hotel) => {
|
||||
const hotelData = await serverClient().hotel.hotelData.get({
|
||||
hotelId: hotel.hotelId.toString(),
|
||||
language,
|
||||
})
|
||||
|
||||
if (!hotelData) throw new Error()
|
||||
|
||||
return {
|
||||
hotelData: hotelData.data.attributes,
|
||||
price: hotel.bestPricePerNight,
|
||||
}
|
||||
})
|
||||
|
||||
return await Promise.all(hotels)
|
||||
}
|
||||
|
||||
export function getFiltersFromHotels(hotels: HotelData[]) {
|
||||
const filters = hotels.flatMap((hotel) => hotel.hotelData.detailedFacilities)
|
||||
|
||||
const uniqueFilterIds = [...new Set(filters.map((filter) => filter.id))]
|
||||
const filterList: Filter[] = uniqueFilterIds
|
||||
.map((filterId) => filters.find((filter) => filter.id === filterId))
|
||||
.filter((filter): filter is Filter => filter !== undefined)
|
||||
|
||||
return filterList
|
||||
}
|
||||
@@ -11,7 +11,7 @@ import { generateLoyaltyConfigTag } from "@/utils/generateTag"
|
||||
|
||||
enum LoyaltyConfigContentTypes {
|
||||
loyalty_level = "loyalty_level",
|
||||
rewards = "rewards",
|
||||
reward = "reward",
|
||||
}
|
||||
|
||||
const validateJsonBody = z.object({
|
||||
@@ -65,7 +65,7 @@ export async function POST(request: NextRequest) {
|
||||
entry.level_id
|
||||
)
|
||||
} else if (
|
||||
content_type.uid === LoyaltyConfigContentTypes.rewards &&
|
||||
content_type.uid === LoyaltyConfigContentTypes.reward &&
|
||||
entry.reward_id
|
||||
) {
|
||||
tag = generateLoyaltyConfigTag(
|
||||
|
||||
@@ -63,7 +63,7 @@ async function PointsColumn({
|
||||
</Title>
|
||||
{subtitle ? (
|
||||
<Body color="white" textAlign="center">
|
||||
{subtitle}
|
||||
{formatMessage({ id: subtitle })}
|
||||
</Body>
|
||||
) : null}
|
||||
</article>
|
||||
|
||||
@@ -11,7 +11,7 @@ export default function Stats({ user }: UserProps) {
|
||||
return (
|
||||
<section className={styles.stats}>
|
||||
<Points user={user} />
|
||||
<Divider variant="default" color="pale" />
|
||||
<Divider color="pale" />
|
||||
<ExpiringPoints user={user} />
|
||||
</section>
|
||||
)
|
||||
|
||||
107
components/BookingWidget/Client.tsx
Normal file
107
components/BookingWidget/Client.tsx
Normal file
@@ -0,0 +1,107 @@
|
||||
"use client"
|
||||
import { zodResolver } from "@hookform/resolvers/zod"
|
||||
import { useEffect, useState } from "react"
|
||||
import { FormProvider, useForm } from "react-hook-form"
|
||||
|
||||
import { dt } from "@/lib/dt"
|
||||
|
||||
import Form from "@/components/Forms/BookingWidget"
|
||||
import { bookingWidgetSchema } from "@/components/Forms/BookingWidget/schema"
|
||||
import { CloseLarge } from "@/components/Icons"
|
||||
import { debounce } from "@/utils/debounce"
|
||||
|
||||
import MobileToggleButton from "./MobileToggleButton"
|
||||
|
||||
import styles from "./bookingWidget.module.css"
|
||||
|
||||
import type {
|
||||
BookingWidgetClientProps,
|
||||
BookingWidgetSchema,
|
||||
} from "@/types/components/bookingWidget"
|
||||
import type { Location } from "@/types/trpc/routers/hotel/locations"
|
||||
|
||||
export default function BookingWidgetClient({
|
||||
locations,
|
||||
}: BookingWidgetClientProps) {
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
|
||||
const sessionStorageSearchData =
|
||||
typeof window !== "undefined"
|
||||
? sessionStorage.getItem("searchData")
|
||||
: undefined
|
||||
const initialSelectedLocation: Location | undefined = sessionStorageSearchData
|
||||
? JSON.parse(sessionStorageSearchData)
|
||||
: undefined
|
||||
const methods = useForm<BookingWidgetSchema>({
|
||||
defaultValues: {
|
||||
search: initialSelectedLocation?.name ?? "",
|
||||
location: sessionStorageSearchData
|
||||
? encodeURIComponent(sessionStorageSearchData)
|
||||
: undefined,
|
||||
date: {
|
||||
// UTC is required to handle requests from far away timezones https://scandichotels.atlassian.net/browse/SWAP-6375 & PET-507
|
||||
// This is specifically to handle timezones falling in different dates.
|
||||
from: dt().utc().format("YYYY-MM-DD"),
|
||||
to: dt().utc().add(1, "day").format("YYYY-MM-DD"),
|
||||
},
|
||||
bookingCode: "",
|
||||
redemption: false,
|
||||
voucher: false,
|
||||
rooms: [
|
||||
{
|
||||
adults: 1,
|
||||
children: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
shouldFocusError: false,
|
||||
mode: "all",
|
||||
resolver: zodResolver(bookingWidgetSchema),
|
||||
reValidateMode: "onChange",
|
||||
})
|
||||
|
||||
function closeMobileSearch() {
|
||||
setIsOpen(false)
|
||||
document.body.style.overflowY = "visible"
|
||||
}
|
||||
|
||||
function openMobileSearch() {
|
||||
setIsOpen(true)
|
||||
document.body.style.overflowY = "hidden"
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const debouncedResizeHandler = debounce(function ([
|
||||
entry,
|
||||
]: ResizeObserverEntry[]) {
|
||||
if (entry.contentRect.width > 1366) {
|
||||
closeMobileSearch()
|
||||
}
|
||||
})
|
||||
const observer = new ResizeObserver(debouncedResizeHandler)
|
||||
|
||||
observer.observe(document.body)
|
||||
|
||||
return () => {
|
||||
if (observer) {
|
||||
observer.unobserve(document.body)
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<FormProvider {...methods}>
|
||||
<section className={styles.container} data-open={isOpen}>
|
||||
<button
|
||||
className={styles.close}
|
||||
onClick={closeMobileSearch}
|
||||
type="button"
|
||||
>
|
||||
<CloseLarge />
|
||||
</button>
|
||||
<Form locations={locations} />
|
||||
</section>
|
||||
<MobileToggleButton openMobileSearch={openMobileSearch} />
|
||||
</FormProvider>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
.complete,
|
||||
.partial {
|
||||
align-items: center;
|
||||
box-shadow: 0px 8px 24px 0px rgba(0, 0, 0, 0.16);
|
||||
cursor: pointer;
|
||||
display: grid;
|
||||
gap: var(--Spacing-x-one-and-half);
|
||||
padding: var(--Spacing-x2);
|
||||
}
|
||||
|
||||
.complete {
|
||||
grid-template-columns: 1fr 36px;
|
||||
}
|
||||
|
||||
.partial {
|
||||
grid-template-columns: min(1fr, 150px) min-content min(1fr, 150px) 1fr;
|
||||
}
|
||||
|
||||
.icon {
|
||||
align-items: center;
|
||||
background-color: var(--Base-Button-Primary-Fill-Normal);
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
height: 36px;
|
||||
justify-content: center;
|
||||
justify-self: flex-end;
|
||||
width: 36px;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
.complete,
|
||||
.partial {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
102
components/BookingWidget/MobileToggleButton/index.tsx
Normal file
102
components/BookingWidget/MobileToggleButton/index.tsx
Normal file
@@ -0,0 +1,102 @@
|
||||
"use client"
|
||||
import { useEffect, useState } from "react"
|
||||
import { useWatch } from "react-hook-form"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import { dt } from "@/lib/dt"
|
||||
|
||||
import { EditIcon, SearchIcon } from "@/components/Icons"
|
||||
import Divider from "@/components/TempDesignSystem/Divider"
|
||||
import Body from "@/components/TempDesignSystem/Text/Body"
|
||||
import Caption from "@/components/TempDesignSystem/Text/Caption"
|
||||
import useLang from "@/hooks/useLang"
|
||||
|
||||
import styles from "./button.module.css"
|
||||
|
||||
import type {
|
||||
BookingWidgetSchema,
|
||||
BookingWidgetToggleButtonProps,
|
||||
} from "@/types/components/bookingWidget"
|
||||
import type { Location } from "@/types/trpc/routers/hotel/locations"
|
||||
|
||||
export default function MobileToggleButton({
|
||||
openMobileSearch,
|
||||
}: BookingWidgetToggleButtonProps) {
|
||||
const [hasMounted, setHasMounted] = useState(false)
|
||||
const intl = useIntl()
|
||||
const lang = useLang()
|
||||
const d = useWatch({ name: "date" })
|
||||
const location = useWatch({ name: "location" })
|
||||
const rooms: BookingWidgetSchema["rooms"] = useWatch({ name: "rooms" })
|
||||
|
||||
const parsedLocation: Location | null = location
|
||||
? JSON.parse(decodeURIComponent(location))
|
||||
: null
|
||||
|
||||
const nights = dt(d.to).diff(dt(d.from), "days")
|
||||
|
||||
const selectedFromDate = dt(d.from).locale(lang).format("D MMM")
|
||||
const selectedToDate = dt(d.to).locale(lang).format("D MMM")
|
||||
|
||||
useEffect(() => {
|
||||
setHasMounted(true)
|
||||
}, [])
|
||||
|
||||
if (!hasMounted) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (parsedLocation && d) {
|
||||
const totalRooms = rooms.length
|
||||
const totalAdults = rooms.reduce((acc, room) => {
|
||||
if (room.adults) {
|
||||
acc = acc + room.adults
|
||||
}
|
||||
return acc
|
||||
}, 0)
|
||||
return (
|
||||
<div className={styles.complete} onClick={openMobileSearch} role="button">
|
||||
<div>
|
||||
<Caption color="red">{parsedLocation.name}</Caption>
|
||||
<Caption>
|
||||
{`${selectedFromDate} - ${selectedToDate} (${intl.formatMessage(
|
||||
{ id: "booking.nights" },
|
||||
{ totalNights: nights }
|
||||
)}) ${intl.formatMessage({ id: "booking.adults" }, { totalAdults })}, ${intl.formatMessage({ id: "booking.rooms" }, { totalRooms })}`}
|
||||
</Caption>
|
||||
</div>
|
||||
<div className={styles.icon}>
|
||||
<EditIcon color="white" />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.partial} onClick={openMobileSearch} role="button">
|
||||
<div>
|
||||
<Caption color="red">{intl.formatMessage({ id: "Where to" })}</Caption>
|
||||
<Body color="uiTextPlaceholder">
|
||||
{parsedLocation
|
||||
? parsedLocation.name
|
||||
: intl.formatMessage({ id: "Destination" })}
|
||||
</Body>
|
||||
</div>
|
||||
<Divider color="baseSurfaceSubtleNormal" variant="vertical" />
|
||||
<div>
|
||||
<Caption color="red">
|
||||
{intl.formatMessage(
|
||||
{ id: "booking.nights" },
|
||||
{ totalNights: nights }
|
||||
)}
|
||||
</Caption>
|
||||
<Body>
|
||||
{selectedFromDate} - {selectedToDate}
|
||||
</Body>
|
||||
</div>
|
||||
<div className={styles.icon}>
|
||||
<SearchIcon color="white" />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,12 +1,38 @@
|
||||
.container {
|
||||
background-color: var(--Base-Surface-Primary-light-Normal);
|
||||
border-top: 1px solid var(--Base-Border-Subtle);
|
||||
border-bottom: 1px solid var(--Base-Border-Subtle);
|
||||
padding: var(--Spacing-x2) var(--Spacing-x5);
|
||||
@media screen and (max-width: 1366px) {
|
||||
.container {
|
||||
background-color: var(--UI-Input-Controls-Surface-Normal);
|
||||
bottom: -100%;
|
||||
display: grid;
|
||||
gap: var(--Spacing-x3);
|
||||
grid-template-rows: 36px 1fr;
|
||||
height: 100dvh;
|
||||
padding: var(--Spacing-x3) var(--Spacing-x2) var(--Spacing-x7);
|
||||
position: fixed;
|
||||
transition: bottom 300ms ease;
|
||||
width: 100%;
|
||||
z-index: 10000;
|
||||
}
|
||||
|
||||
.container[data-open="true"] {
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
.close {
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
justify-self: flex-end;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 1367px) {
|
||||
@media screen and (min-width: 1367px) {
|
||||
.container {
|
||||
border-bottom: 1px solid var(--Base-Border-Subtle);
|
||||
border-top: 1px solid var(--Base-Border-Subtle);
|
||||
display: block;
|
||||
}
|
||||
|
||||
.close {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import { getLocations } from "@/lib/trpc/memoizedRequests"
|
||||
|
||||
import Form from "@/components/Forms/BookingWidget"
|
||||
|
||||
import styles from "./bookingWidget.module.css"
|
||||
import BookingWidgetClient from "./Client"
|
||||
|
||||
export function preload() {
|
||||
void getLocations()
|
||||
@@ -15,9 +13,5 @@ export default async function BookingWidget() {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<section className={styles.container}>
|
||||
<Form locations={locations.data} />
|
||||
</section>
|
||||
)
|
||||
return <BookingWidgetClient locations={locations.data} />
|
||||
}
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
/*nav:has(+ .contentPage) {*/
|
||||
/* background-color: var(--Base-Surface-Subtle-Normal);*/
|
||||
/*}*/
|
||||
|
||||
.contentPage {
|
||||
padding-bottom: var(--Spacing-x9);
|
||||
container-name: content-page;
|
||||
container-type: inline-size;
|
||||
}
|
||||
|
||||
.header {
|
||||
@@ -17,21 +15,32 @@
|
||||
max-width: var(--max-width-content);
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.headerIntro {
|
||||
display: grid;
|
||||
max-width: var(--max-width-text-block);
|
||||
gap: var(--Spacing-x3);
|
||||
}
|
||||
|
||||
.content {
|
||||
.heroContainer {
|
||||
width: 100%;
|
||||
padding: var(--Spacing-x4) var(--Spacing-x2);
|
||||
display: grid;
|
||||
justify-items: center;
|
||||
}
|
||||
|
||||
.innerContent {
|
||||
width: 100%;
|
||||
.heroContainer img {
|
||||
max-width: var(--max-width-content);
|
||||
margin: 0 auto;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.contentContainer {
|
||||
display: grid;
|
||||
padding: var(--Spacing-x4) var(--Spacing-x2) 0;
|
||||
gap: var(--Spacing-x4);
|
||||
}
|
||||
|
||||
.mainContent {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
@@ -39,3 +48,16 @@
|
||||
gap: var(--Spacing-x3);
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1367px) {
|
||||
.heroContainer {
|
||||
padding: var(--Spacing-x4) 0;
|
||||
}
|
||||
.contentContainer {
|
||||
grid-template-columns: var(--max-width-text-block) 1fr;
|
||||
gap: var(--Spacing-x9);
|
||||
max-width: var(--max-width-content);
|
||||
margin: 0 auto;
|
||||
padding: var(--Spacing-x4) 0 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,31 +23,38 @@ export default async function ContentPage() {
|
||||
return (
|
||||
<>
|
||||
<section className={styles.contentPage}>
|
||||
{sidebar?.length ? <Sidebar blocks={sidebar} /> : null}
|
||||
|
||||
<header className={styles.header}>
|
||||
<div className={styles.headerContent}>
|
||||
<div className={styles.headerIntro}>
|
||||
<Title as="h2">{header.heading}</Title>
|
||||
<Preamble>{header.preamble}</Preamble>
|
||||
</div>
|
||||
{header.navigation_links ? (
|
||||
<LinkChips chips={header.navigation_links} />
|
||||
{header ? (
|
||||
<>
|
||||
<div className={styles.headerIntro}>
|
||||
<Title as="h2">{header.heading}</Title>
|
||||
<Preamble>{header.preamble}</Preamble>
|
||||
</div>
|
||||
{header.navigation_links ? (
|
||||
<LinkChips chips={header.navigation_links} />
|
||||
) : null}
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main className={styles.content}>
|
||||
<div className={styles.innerContent}>
|
||||
{hero_image ? (
|
||||
<Hero
|
||||
alt={hero_image.meta.alt || hero_image.meta.caption || ""}
|
||||
src={hero_image.url}
|
||||
/>
|
||||
) : null}
|
||||
{blocks ? <Blocks blocks={blocks} /> : null}
|
||||
{hero_image ? (
|
||||
<div className={styles.heroContainer}>
|
||||
<Hero
|
||||
alt={hero_image.meta.alt || hero_image.meta.caption || ""}
|
||||
src={hero_image.url}
|
||||
/>
|
||||
</div>
|
||||
</main>
|
||||
) : null}
|
||||
|
||||
<div className={styles.contentContainer}>
|
||||
<main className={styles.mainContent}>
|
||||
{blocks ? <Blocks blocks={blocks} /> : null}
|
||||
</main>
|
||||
|
||||
{sidebar?.length ? <Sidebar blocks={sidebar} /> : null}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<TrackingSDK pageData={tracking} />
|
||||
|
||||
@@ -14,3 +14,9 @@
|
||||
top: var(--main-menu-desktop-height);
|
||||
}
|
||||
}
|
||||
|
||||
.closeButton {
|
||||
pointer-events: initial;
|
||||
box-shadow: var(--button-box-shadow);
|
||||
gap: var(--Spacing-x-half);
|
||||
}
|
||||
|
||||
@@ -6,9 +6,11 @@ import { useIntl } from "react-intl"
|
||||
|
||||
import useHotelPageStore from "@/stores/hotel-page"
|
||||
|
||||
import CloseLargeIcon from "@/components/Icons/CloseLarge"
|
||||
import InteractiveMap from "@/components/Maps/InteractiveMap"
|
||||
import Button from "@/components/TempDesignSystem/Button"
|
||||
import { useHandleKeyUp } from "@/hooks/useHandleKeyUp"
|
||||
|
||||
import MapContent from "./Map"
|
||||
import Sidebar from "./Sidebar"
|
||||
|
||||
import styles from "./dynamicMap.module.css"
|
||||
@@ -52,6 +54,20 @@ export default function DynamicMap({
|
||||
}
|
||||
}, [isDynamicMapOpen, scrollHeightWhenOpened])
|
||||
|
||||
const closeButton = (
|
||||
<Button
|
||||
theme="base"
|
||||
intent="inverted"
|
||||
variant="icon"
|
||||
size="small"
|
||||
className={styles.closeButton}
|
||||
onClick={closeDynamicMap}
|
||||
>
|
||||
<CloseLargeIcon color="burgundy" />
|
||||
<span>{intl.formatMessage({ id: "Close the map" })}</span>
|
||||
</Button>
|
||||
)
|
||||
|
||||
return (
|
||||
<APIProvider apiKey={apiKey}>
|
||||
<Modal isOpen={isDynamicMapOpen}>
|
||||
@@ -68,7 +84,8 @@ export default function DynamicMap({
|
||||
pointsOfInterest={pointsOfInterest}
|
||||
onActivePoiChange={setActivePoi}
|
||||
/>
|
||||
<MapContent
|
||||
<InteractiveMap
|
||||
closeButton={closeButton}
|
||||
coordinates={coordinates}
|
||||
pointsOfInterest={pointsOfInterest}
|
||||
activePoi={activePoi}
|
||||
|
||||
@@ -55,6 +55,7 @@ export default function MapCard({ hotelName, pois }: MapCardProps) {
|
||||
theme="base"
|
||||
intent="secondary"
|
||||
size="small"
|
||||
fullWidth
|
||||
className={styles.ctaButton}
|
||||
onClick={openDynamicMap}
|
||||
>
|
||||
|
||||
@@ -6,6 +6,8 @@
|
||||
position: relative;
|
||||
justify-content: center;
|
||||
align-items: flex-start;
|
||||
container-name: loyalty-page;
|
||||
container-type: inline-size;
|
||||
}
|
||||
|
||||
.blocks {
|
||||
|
||||
@@ -1,62 +0,0 @@
|
||||
"use client"
|
||||
import { da, de, fi, nb, sv } from "date-fns/locale"
|
||||
import { useState } from "react"
|
||||
import { type DateRange, DayPicker } from "react-day-picker"
|
||||
|
||||
import { Lang } from "@/constants/languages"
|
||||
import { dt } from "@/lib/dt"
|
||||
|
||||
import useLang from "@/hooks/useLang"
|
||||
|
||||
import classNames from "react-day-picker/style.module.css"
|
||||
|
||||
const locales = {
|
||||
[Lang.da]: da,
|
||||
[Lang.de]: de,
|
||||
[Lang.fi]: fi,
|
||||
[Lang.no]: nb,
|
||||
[Lang.sv]: sv,
|
||||
}
|
||||
|
||||
export interface DatePickerProps {
|
||||
handleOnSelect: (selected: DateRange) => void
|
||||
initialSelected?: DateRange
|
||||
}
|
||||
|
||||
export default function DatePicker({
|
||||
handleOnSelect,
|
||||
initialSelected = {
|
||||
from: undefined,
|
||||
to: undefined,
|
||||
},
|
||||
}: DatePickerProps) {
|
||||
const lang = useLang()
|
||||
const [selectedDate, setSelectedDate] = useState<DateRange>(initialSelected)
|
||||
|
||||
function handleSelectDate(selected: DateRange) {
|
||||
handleOnSelect(selected)
|
||||
setSelectedDate(selected)
|
||||
}
|
||||
|
||||
/** English is default language and doesn't need to be imported */
|
||||
const locale = lang === Lang.en ? undefined : locales[lang]
|
||||
|
||||
const currentDate = dt().toDate()
|
||||
const startOfMonth = dt(currentDate).set("date", 1).toDate()
|
||||
const yesterday = dt(currentDate).subtract(1, "day").toDate()
|
||||
return (
|
||||
<DayPicker
|
||||
classNames={classNames}
|
||||
disabled={{ from: startOfMonth, to: yesterday }}
|
||||
excludeDisabled
|
||||
locale={locale}
|
||||
mode="range"
|
||||
onSelect={handleSelectDate}
|
||||
pagedNavigation
|
||||
required
|
||||
selected={selectedDate}
|
||||
showWeekNumber
|
||||
startMonth={currentDate}
|
||||
/>
|
||||
)
|
||||
}
|
||||
127
components/DatePicker/Screen/Desktop.tsx
Normal file
127
components/DatePicker/Screen/Desktop.tsx
Normal file
@@ -0,0 +1,127 @@
|
||||
"use client"
|
||||
import { DayPicker } from "react-day-picker"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import { Lang } from "@/constants/languages"
|
||||
import { dt } from "@/lib/dt"
|
||||
|
||||
import { ChevronLeftIcon } from "@/components/Icons"
|
||||
import Button from "@/components/TempDesignSystem/Button"
|
||||
import Divider from "@/components/TempDesignSystem/Divider"
|
||||
import Caption from "@/components/TempDesignSystem/Text/Caption"
|
||||
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
|
||||
import useLang from "@/hooks/useLang"
|
||||
|
||||
import styles from "./desktop.module.css"
|
||||
import classNames from "react-day-picker/style.module.css"
|
||||
|
||||
import type { DatePickerProps } from "@/types/components/datepicker"
|
||||
|
||||
export default function DatePickerDesktop({
|
||||
close,
|
||||
handleOnSelect,
|
||||
locales,
|
||||
selectedDate,
|
||||
}: DatePickerProps) {
|
||||
const lang = useLang()
|
||||
const intl = useIntl()
|
||||
|
||||
/** English is default language and doesn't need to be imported */
|
||||
const locale = lang === Lang.en ? undefined : locales[lang]
|
||||
const currentDate = dt().toDate()
|
||||
const startOfMonth = dt(currentDate).set("date", 1).toDate()
|
||||
const yesterday = dt(currentDate).subtract(1, "day").toDate()
|
||||
return (
|
||||
<DayPicker
|
||||
classNames={{
|
||||
...classNames,
|
||||
caption_label: `${classNames.caption_label} ${styles.captionLabel}`,
|
||||
day: `${classNames.day} ${styles.day}`,
|
||||
day_button: `${classNames.day_button} ${styles.dayButton}`,
|
||||
footer: styles.footer,
|
||||
month_caption: `${classNames.month_caption} ${styles.monthCaption}`,
|
||||
months: `${classNames.months} ${styles.months}`,
|
||||
range_end: styles.rangeEnd,
|
||||
range_middle: styles.rangeMiddle,
|
||||
range_start: styles.rangeStart,
|
||||
root: `${classNames.root} ${styles.container}`,
|
||||
week: styles.week,
|
||||
weekday: `${classNames.weekday} ${styles.weekDay}`,
|
||||
}}
|
||||
disabled={{ from: startOfMonth, to: yesterday }}
|
||||
excludeDisabled
|
||||
footer
|
||||
formatters={{
|
||||
formatWeekdayName(weekday) {
|
||||
return dt(weekday).locale(lang).format("ddd")
|
||||
},
|
||||
}}
|
||||
lang={lang}
|
||||
locale={locale}
|
||||
mode="range"
|
||||
numberOfMonths={2}
|
||||
onSelect={handleOnSelect}
|
||||
pagedNavigation
|
||||
required
|
||||
selected={selectedDate}
|
||||
startMonth={currentDate}
|
||||
weekStartsOn={1}
|
||||
components={{
|
||||
Chevron(props) {
|
||||
return <ChevronLeftIcon {...props} height={20} width={20} />
|
||||
},
|
||||
Footer(props) {
|
||||
return (
|
||||
<>
|
||||
<Divider className={styles.divider} color="primaryLightSubtle" />
|
||||
<footer className={props.className}>
|
||||
<Button
|
||||
intent="tertiary"
|
||||
onPress={close}
|
||||
size="small"
|
||||
theme="base"
|
||||
>
|
||||
<Caption color="white" textTransform="bold">
|
||||
{intl.formatMessage({ id: "Select dates" })}
|
||||
</Caption>
|
||||
</Button>
|
||||
</footer>
|
||||
</>
|
||||
)
|
||||
},
|
||||
MonthCaption(props) {
|
||||
return (
|
||||
<div className={props.className}>
|
||||
<Subtitle asChild type="two">
|
||||
{props.children}
|
||||
</Subtitle>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
Nav(props) {
|
||||
if (Array.isArray(props.children)) {
|
||||
const prevButton = props.children?.[0]
|
||||
const nextButton = props.children?.[1]
|
||||
return (
|
||||
<>
|
||||
{prevButton ? (
|
||||
<nav
|
||||
className={`${props.className} ${styles.previousButton}`}
|
||||
>
|
||||
{prevButton}
|
||||
</nav>
|
||||
) : null}
|
||||
{nextButton ? (
|
||||
<nav className={`${props.className} ${styles.nextButton}`}>
|
||||
{nextButton}
|
||||
</nav>
|
||||
) : null}
|
||||
</>
|
||||
)
|
||||
}
|
||||
return <></>
|
||||
},
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
140
components/DatePicker/Screen/Mobile.tsx
Normal file
140
components/DatePicker/Screen/Mobile.tsx
Normal file
@@ -0,0 +1,140 @@
|
||||
"use client"
|
||||
import { type ChangeEvent, useState } from "react"
|
||||
import { DayPicker } from "react-day-picker"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import { Lang } from "@/constants/languages"
|
||||
import { dt } from "@/lib/dt"
|
||||
|
||||
import { CloseLarge } from "@/components/Icons"
|
||||
import Button from "@/components/TempDesignSystem/Button"
|
||||
import Body from "@/components/TempDesignSystem/Text/Body"
|
||||
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
|
||||
import useLang from "@/hooks/useLang"
|
||||
|
||||
import styles from "./mobile.module.css"
|
||||
import classNames from "react-day-picker/style.module.css"
|
||||
|
||||
import type { DatePickerProps } from "@/types/components/datepicker"
|
||||
|
||||
function addOneYear(_: undefined, i: number) {
|
||||
return new Date().getFullYear() + i
|
||||
}
|
||||
|
||||
const fiftyYearsAhead = Array.from({ length: 50 }, addOneYear)
|
||||
|
||||
export default function DatePickerMobile({
|
||||
close,
|
||||
handleOnSelect,
|
||||
locales,
|
||||
selectedDate,
|
||||
}: DatePickerProps) {
|
||||
const [selectedYear, setSelectedYear] = useState(() => dt().year())
|
||||
const lang = useLang()
|
||||
const intl = useIntl()
|
||||
|
||||
function handleSelectYear(evt: ChangeEvent<HTMLSelectElement>) {
|
||||
setSelectedYear(Number(evt.currentTarget.value))
|
||||
}
|
||||
|
||||
/** English is default language and doesn't need to be imported */
|
||||
const locale = lang === Lang.en ? undefined : locales[lang]
|
||||
const currentDate = dt().toDate()
|
||||
const startOfCurrentMonth = dt(currentDate).set("date", 1).toDate()
|
||||
const yesterday = dt(currentDate).subtract(1, "day").toDate()
|
||||
|
||||
const startMonth = dt().set("year", selectedYear).startOf("year").toDate()
|
||||
const decemberOfYear = dt().set("year", selectedYear).endOf("year").toDate()
|
||||
return (
|
||||
<DayPicker
|
||||
classNames={{
|
||||
...classNames,
|
||||
caption_label: `${classNames.caption_label} ${styles.captionLabel}`,
|
||||
day: `${classNames.day} ${styles.day}`,
|
||||
day_button: `${classNames.day_button} ${styles.dayButton}`,
|
||||
footer: styles.footer,
|
||||
month: styles.month,
|
||||
month_caption: `${classNames.month_caption} ${styles.monthCaption}`,
|
||||
months: styles.months,
|
||||
range_end: styles.rangeEnd,
|
||||
range_middle: styles.rangeMiddle,
|
||||
range_start: styles.rangeStart,
|
||||
root: `${classNames.root} ${styles.container}`,
|
||||
week: styles.week,
|
||||
weekday: `${classNames.weekday} ${styles.weekDay}`,
|
||||
}}
|
||||
disabled={{ from: startOfCurrentMonth, to: yesterday }}
|
||||
endMonth={decemberOfYear}
|
||||
excludeDisabled
|
||||
footer
|
||||
formatters={{
|
||||
formatWeekdayName(weekday) {
|
||||
return dt(weekday).locale(lang).format("ddd")
|
||||
},
|
||||
}}
|
||||
hideNavigation
|
||||
lang={lang}
|
||||
locale={locale}
|
||||
mode="range"
|
||||
/** Showing full year or what's left of it */
|
||||
numberOfMonths={12}
|
||||
onSelect={handleOnSelect}
|
||||
required
|
||||
selected={selectedDate}
|
||||
startMonth={startMonth}
|
||||
weekStartsOn={1}
|
||||
components={{
|
||||
Footer(props) {
|
||||
return (
|
||||
<footer className={props.className}>
|
||||
<Button
|
||||
className={styles.button}
|
||||
intent="tertiary"
|
||||
onPress={close}
|
||||
size="large"
|
||||
theme="base"
|
||||
>
|
||||
<Body color="white" textTransform="bold">
|
||||
{intl.formatMessage({ id: "Select dates" })}
|
||||
</Body>
|
||||
</Button>
|
||||
<div className={styles.backdrop} />
|
||||
</footer>
|
||||
)
|
||||
},
|
||||
MonthCaption(props) {
|
||||
return (
|
||||
<div className={props.className}>
|
||||
<Subtitle asChild type="two">
|
||||
{props.children}
|
||||
</Subtitle>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
Root({ children, ...props }) {
|
||||
return (
|
||||
<div {...props}>
|
||||
<header className={styles.header}>
|
||||
<select
|
||||
className={styles.select}
|
||||
defaultValue={selectedYear}
|
||||
onChange={handleSelectYear}
|
||||
>
|
||||
{fiftyYearsAhead.map((year) => (
|
||||
<option key={year} value={year}>
|
||||
{year}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<button className={styles.close} onClick={close} type="button">
|
||||
<CloseLarge />
|
||||
</button>
|
||||
</header>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
},
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
120
components/DatePicker/Screen/desktop.module.css
Normal file
120
components/DatePicker/Screen/desktop.module.css
Normal file
@@ -0,0 +1,120 @@
|
||||
@media screen and (max-width: 1366px) {
|
||||
.container {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
div.months {
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
|
||||
.monthCaption {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.captionLabel {
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
td.day,
|
||||
td.rangeEnd,
|
||||
td.rangeStart {
|
||||
font-family: var(--typography-Body-Bold-fontFamily);
|
||||
font-size: var(--typography-Body-Bold-fontSize);
|
||||
font-weight: 500;
|
||||
letter-spacing: var(--typography-Body-Bold-letterSpacing);
|
||||
line-height: var(--typography-Body-Bold-lineHeight);
|
||||
text-decoration: var(--typography-Body-Bold-textDecoration);
|
||||
}
|
||||
|
||||
td.rangeEnd,
|
||||
td.rangeStart {
|
||||
background: var(--Base-Background-Primary-Normal);
|
||||
}
|
||||
|
||||
td.rangeEnd[aria-selected="true"]:not([data-outside="true"]) {
|
||||
border-radius: 0 50% 50% 0;
|
||||
}
|
||||
|
||||
td.rangeStart[aria-selected="true"] {
|
||||
border-radius: 50% 0 0 50%;
|
||||
}
|
||||
|
||||
td.rangeEnd[aria-selected="true"] button.dayButton:hover,
|
||||
td.rangeStart[aria-selected="true"] button.dayButton:hover {
|
||||
background: var(--Primary-Light-On-Surface-Accent);
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
td.rangeEnd[aria-selected="true"]:not([data-outside="true"]) button.dayButton,
|
||||
td.rangeStart[aria-selected="true"]:not([data-outside="true"])
|
||||
button.dayButton {
|
||||
background: var(--Primary-Light-On-Surface-Accent);
|
||||
border: none;
|
||||
color: var(--Base-Button-Inverted-Fill-Normal);
|
||||
}
|
||||
|
||||
td.day,
|
||||
td.day[data-today="true"] {
|
||||
color: var(--UI-Text-High-contrast);
|
||||
height: 40px;
|
||||
padding: var(--Spacing-x-half);
|
||||
width: 40px;
|
||||
}
|
||||
|
||||
td.day button.dayButton:hover {
|
||||
background: var(--Base-Surface-Secondary-light-Hover);
|
||||
}
|
||||
|
||||
td.day[data-outside="true"] button.dayButton {
|
||||
border: none;
|
||||
}
|
||||
|
||||
td.day:not(td.rangeEnd, td.rangeStart)[aria-selected="true"],
|
||||
td.rangeMiddle[aria-selected="true"] button.dayButton {
|
||||
background: var(--Base-Background-Primary-Normal);
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
td.day[data-disabled="true"],
|
||||
td.day[data-disabled="true"] button.dayButton,
|
||||
td.day[data-outside="true"] ~ td.day[data-disabled="true"],
|
||||
td.day[data-outside="true"] ~ td.day[data-disabled="true"] button.dayButton,
|
||||
.week:has(td.day[data-outside="true"] ~ td.day[data-disabled="true"])
|
||||
td.day[data-outside="true"]
|
||||
button.dayButton {
|
||||
background: none;
|
||||
color: var(--Base-Text-Disabled);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.weekDay {
|
||||
color: var(--UI-Text-Placeholder);
|
||||
font-family: var(--typography-Footnote-Labels-fontFamily);
|
||||
font-size: var(--typography-Footnote-Labels-fontSize);
|
||||
font-weight: var(--typography-Footnote-Labels-fontWeight);
|
||||
letter-spacing: var(--typography-Footnote-Labels-letterSpacing);
|
||||
line-height: var(--typography-Footnote-Labels-lineHeight);
|
||||
text-decoration: var(--typography-Footnote-Labels-textDecoration);
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: var(--Spacing-x2);
|
||||
}
|
||||
|
||||
.divider {
|
||||
margin-top: var(--Spacing-x2);
|
||||
}
|
||||
|
||||
.nextButton {
|
||||
transform: rotate(180deg);
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.previousButton {
|
||||
left: 0;
|
||||
}
|
||||
173
components/DatePicker/Screen/mobile.module.css
Normal file
173
components/DatePicker/Screen/mobile.module.css
Normal file
@@ -0,0 +1,173 @@
|
||||
.container {
|
||||
--header-height: 68px;
|
||||
--sticky-button-height: 120px;
|
||||
|
||||
display: grid;
|
||||
grid-template-areas:
|
||||
"header"
|
||||
"content";
|
||||
grid-template-rows: var(--header-height) calc(100dvh - var(--header-height));
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.header {
|
||||
align-self: flex-start;
|
||||
background-color: var(--Main-Grey-White);
|
||||
display: grid;
|
||||
grid-area: header;
|
||||
grid-template-columns: 1fr 24px;
|
||||
padding: var(--Spacing-x3) var(--Spacing-x2) var(--Spacing-x2);
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.select {
|
||||
justify-self: center;
|
||||
min-width: 100px;
|
||||
transform: translateX(24px);
|
||||
}
|
||||
|
||||
.close {
|
||||
align-items: center;
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
justify-self: flex-end;
|
||||
}
|
||||
|
||||
div.months {
|
||||
display: grid;
|
||||
grid-area: content;
|
||||
overflow-y: scroll;
|
||||
scroll-snap-type: y mandatory;
|
||||
}
|
||||
|
||||
.month {
|
||||
display: grid;
|
||||
justify-items: center;
|
||||
scroll-snap-align: start;
|
||||
}
|
||||
|
||||
.month:last-of-type {
|
||||
padding-bottom: var(--sticky-button-height);
|
||||
}
|
||||
|
||||
.monthCaption {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.captionLabel {
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
.footer {
|
||||
align-self: flex-start;
|
||||
background: linear-gradient(
|
||||
180deg,
|
||||
rgba(255, 255, 255, 0) 7.5%,
|
||||
#ffffff 82.5%
|
||||
);
|
||||
display: flex;
|
||||
grid-area: content;
|
||||
padding: var(--Spacing-x1) var(--Spacing-x2) var(--Spacing-x7);
|
||||
position: sticky;
|
||||
top: calc(100vh - var(--sticky-button-height));
|
||||
width: 100%;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.footer .button {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
td.day,
|
||||
td.rangeEnd,
|
||||
td.rangeStart {
|
||||
font-family: var(--typography-Body-Bold-fontFamily);
|
||||
font-size: var(--typography-Body-Bold-fontSize);
|
||||
font-weight: 500;
|
||||
letter-spacing: var(--typography-Body-Bold-letterSpacing);
|
||||
line-height: var(--typography-Body-Bold-lineHeight);
|
||||
text-decoration: var(--typography-Body-Bold-textDecoration);
|
||||
}
|
||||
|
||||
td.rangeEnd,
|
||||
td.rangeStart {
|
||||
background: var(--Base-Background-Primary-Normal);
|
||||
}
|
||||
|
||||
td.rangeEnd[aria-selected="true"]:not([data-outside="true"]) {
|
||||
border-radius: 0 50% 50% 0;
|
||||
}
|
||||
|
||||
td.rangeStart[aria-selected="true"] {
|
||||
border-radius: 50% 0 0 50%;
|
||||
}
|
||||
|
||||
td.rangeEnd[aria-selected="true"] button.dayButton:hover,
|
||||
td.rangeStart[aria-selected="true"] button.dayButton:hover {
|
||||
background: var(--Primary-Light-On-Surface-Accent);
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
td.rangeEnd[aria-selected="true"]:not([data-outside="true"]) button.dayButton,
|
||||
td.rangeStart[aria-selected="true"]:not([data-outside="true"])
|
||||
button.dayButton {
|
||||
background: var(--Primary-Light-On-Surface-Accent);
|
||||
border: none;
|
||||
color: var(--Base-Button-Inverted-Fill-Normal);
|
||||
}
|
||||
|
||||
td.day,
|
||||
td.day[data-today="true"] {
|
||||
color: var(--UI-Text-High-contrast);
|
||||
height: 40px;
|
||||
padding: var(--Spacing-x-half);
|
||||
width: 40px;
|
||||
}
|
||||
|
||||
td.day button.dayButton:hover {
|
||||
background: var(--Base-Surface-Secondary-light-Hover);
|
||||
}
|
||||
|
||||
td.day[data-outside="true"] button.dayButton {
|
||||
border: none;
|
||||
}
|
||||
|
||||
td.day:not(td.rangeEnd, td.rangeStart)[aria-selected="true"],
|
||||
td.rangeMiddle[aria-selected="true"] button.dayButton {
|
||||
background: var(--Base-Background-Primary-Normal);
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
td.day[data-disabled="true"],
|
||||
td.day[data-disabled="true"] button.dayButton,
|
||||
td.day[data-outside="true"] ~ td.day[data-disabled="true"],
|
||||
td.day[data-outside="true"] ~ td.day[data-disabled="true"] button.dayButton,
|
||||
.week:has(td.day[data-outside="true"] ~ td.day[data-disabled="true"])
|
||||
td.day[data-outside="true"]
|
||||
button.dayButton {
|
||||
background: none;
|
||||
color: var(--Base-Text-Disabled);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.weekDay {
|
||||
color: var(--Base-Text-Medium-contrast);
|
||||
font-family: var(--typography-Footnote-Labels-fontFamily);
|
||||
font-size: var(--typography-Footnote-Labels-fontSize);
|
||||
font-weight: var(--typography-Footnote-Labels-fontWeight);
|
||||
letter-spacing: var(--typography-Footnote-Labels-letterSpacing);
|
||||
line-height: var(--typography-Footnote-Labels-lineHeight);
|
||||
text-decoration: var(--typography-Footnote-Labels-textDecoration);
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 1367px) {
|
||||
.container {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
@@ -7,16 +7,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
.hideWrapper {
|
||||
background-color: var(--Main-Grey-White);
|
||||
border-radius: var(--Corner-radius-Medium);
|
||||
box-shadow: 0px 16px 24px 0px rgba(0, 0, 0, 0.08);
|
||||
padding: var(--Spacing-x-one-and-half);
|
||||
position: absolute;
|
||||
/** BookingWidget padding + border-width */
|
||||
top: calc(100% + var(--Spacing-x2) + 1px);
|
||||
}
|
||||
|
||||
.btn {
|
||||
background: none;
|
||||
border: none;
|
||||
@@ -29,3 +19,43 @@
|
||||
.body {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.hideWrapper {
|
||||
background-color: var(--Main-Grey-White);
|
||||
}
|
||||
|
||||
@media screen and (max-width: 1366px) {
|
||||
.container {
|
||||
z-index: 10001;
|
||||
}
|
||||
|
||||
.hideWrapper {
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
overflow: auto;
|
||||
position: fixed;
|
||||
right: 0;
|
||||
top: 100%;
|
||||
transition: top 300ms ease;
|
||||
z-index: 10001;
|
||||
}
|
||||
|
||||
.container[data-isopen="true"] .hideWrapper {
|
||||
top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (min-width: 1367px) {
|
||||
.hideWrapper {
|
||||
border-radius: var(--Corner-radius-Large);
|
||||
box-shadow: 0px 0px 14px 6px rgba(0, 0, 0, 0.1);
|
||||
padding: var(--Spacing-x2) var(--Spacing-x3);
|
||||
position: absolute;
|
||||
/**
|
||||
BookingWidget padding +
|
||||
border-width +
|
||||
wanted space below booking widget
|
||||
*/
|
||||
top: calc(100% + var(--Spacing-x2) + 1px + var(--Spacing-x4));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
"use client"
|
||||
import { da, de, fi, nb, sv } from "date-fns/locale"
|
||||
import { useEffect, useRef, useState } from "react"
|
||||
import { useFormContext, useWatch } from "react-hook-form"
|
||||
|
||||
import { Lang } from "@/constants/languages"
|
||||
import { dt } from "@/lib/dt"
|
||||
|
||||
import Body from "@/components/TempDesignSystem/Text/Body"
|
||||
import useLang from "@/hooks/useLang"
|
||||
|
||||
import DatePicker from "./DatePicker"
|
||||
import DatePickerDesktop from "./Screen/Desktop"
|
||||
import DatePickerMobile from "./Screen/Mobile"
|
||||
|
||||
import styles from "./date-picker.module.css"
|
||||
|
||||
@@ -15,6 +18,14 @@ import type { DateRange } from "react-day-picker"
|
||||
|
||||
import type { DatePickerFormProps } from "@/types/components/datepicker"
|
||||
|
||||
const locales = {
|
||||
[Lang.da]: da,
|
||||
[Lang.de]: de,
|
||||
[Lang.fi]: fi,
|
||||
[Lang.no]: nb,
|
||||
[Lang.sv]: sv,
|
||||
}
|
||||
|
||||
export default function DatePickerForm({ name = "date" }: DatePickerFormProps) {
|
||||
const lang = useLang()
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
@@ -22,6 +33,10 @@ export default function DatePickerForm({ name = "date" }: DatePickerFormProps) {
|
||||
const { register, setValue } = useFormContext()
|
||||
const ref = useRef<HTMLDivElement | null>(null)
|
||||
|
||||
function close() {
|
||||
setIsOpen(false)
|
||||
}
|
||||
|
||||
function handleOnClick() {
|
||||
setIsOpen((prevIsOpen) => !prevIsOpen)
|
||||
}
|
||||
@@ -40,11 +55,9 @@ export default function DatePickerForm({ name = "date" }: DatePickerFormProps) {
|
||||
setIsOpen(false)
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener("click", handleClickOutside)
|
||||
|
||||
document.body.addEventListener("click", handleClickOutside)
|
||||
return () => {
|
||||
document.removeEventListener("click", handleClickOutside)
|
||||
document.body.removeEventListener("click", handleClickOutside)
|
||||
}
|
||||
}, [setIsOpen])
|
||||
|
||||
@@ -63,9 +76,17 @@ export default function DatePickerForm({ name = "date" }: DatePickerFormProps) {
|
||||
<input {...register("date.from")} type="hidden" />
|
||||
<input {...register("date.to")} type="hidden" />
|
||||
<div aria-modal className={styles.hideWrapper} role="dialog">
|
||||
<DatePicker
|
||||
<DatePickerDesktop
|
||||
close={close}
|
||||
handleOnSelect={handleSelectDate}
|
||||
initialSelected={selectedDate}
|
||||
locales={locales}
|
||||
selectedDate={selectedDate}
|
||||
/>
|
||||
<DatePickerMobile
|
||||
close={close}
|
||||
handleOnSelect={handleSelectDate}
|
||||
locales={locales}
|
||||
selectedDate={selectedDate}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
height: 24px;
|
||||
outline: none;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,45 +1,80 @@
|
||||
.input {
|
||||
display: flex;
|
||||
gap: var(--Spacing-x2);
|
||||
}
|
||||
|
||||
.input input[type="text"] {
|
||||
border: none;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
.rooms,
|
||||
.vouchers,
|
||||
.when,
|
||||
.where {
|
||||
border-right: 1px solid var(--Base-Surface-Subtle-Normal);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.rooms,
|
||||
.when {
|
||||
max-width: 240px;
|
||||
padding: var(--Spacing-x1) var(--Spacing-x-one-and-half);
|
||||
}
|
||||
|
||||
.vouchers {
|
||||
max-width: 200px;
|
||||
padding: var(--Spacing-x1) 0;
|
||||
}
|
||||
|
||||
.where {
|
||||
max-width: 280px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.options {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
max-width: 158px;
|
||||
}
|
||||
|
||||
.option {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 1366px) {
|
||||
.input {
|
||||
display: grid;
|
||||
gap: var(--Spacing-x2);
|
||||
}
|
||||
|
||||
.rooms,
|
||||
.vouchers,
|
||||
.when,
|
||||
.where {
|
||||
background-color: var(--Base-Background-Primary-Normal);
|
||||
border-radius: var(--Corner-radius-Medium);
|
||||
}
|
||||
|
||||
.rooms,
|
||||
.vouchers,
|
||||
.when {
|
||||
padding: var(--Spacing-x1) var(--Spacing-x-one-and-half);
|
||||
}
|
||||
|
||||
.options {
|
||||
gap: var(--Spacing-x2);
|
||||
margin-top: var(--Spacing-x2);
|
||||
}
|
||||
|
||||
.option {
|
||||
gap: var(--Spacing-x2);
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (min-width: 1367px) {
|
||||
.input {
|
||||
display: flex;
|
||||
gap: var(--Spacing-x2);
|
||||
}
|
||||
|
||||
.rooms,
|
||||
.vouchers,
|
||||
.when,
|
||||
.where {
|
||||
border-right: 1px solid var(--Base-Surface-Subtle-Normal);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.input input[type="text"] {
|
||||
border: none;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
.rooms,
|
||||
.when {
|
||||
max-width: 240px;
|
||||
padding: var(--Spacing-x1) var(--Spacing-x-one-and-half);
|
||||
}
|
||||
|
||||
.vouchers {
|
||||
max-width: 200px;
|
||||
padding: var(--Spacing-x1) 0;
|
||||
}
|
||||
|
||||
.where {
|
||||
max-width: 280px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.options {
|
||||
max-width: 158px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,8 +19,8 @@ export default function FormContent({
|
||||
const intl = useIntl()
|
||||
const selectedDate = useWatch({ name: "date" })
|
||||
|
||||
const rooms = intl.formatMessage({ id: "Rooms & Guests" })
|
||||
const vouchers = intl.formatMessage({ id: "Booking codes and vouchers" })
|
||||
const rooms = intl.formatMessage({ id: "Guests & Rooms" })
|
||||
const vouchers = intl.formatMessage({ id: "Code / Voucher" })
|
||||
const bonus = intl.formatMessage({ id: "Use bonus cheque" })
|
||||
const reward = intl.formatMessage({ id: "Book reward night" })
|
||||
|
||||
@@ -33,10 +33,10 @@ export default function FormContent({
|
||||
</div>
|
||||
<div className={styles.when}>
|
||||
<Caption color="red" textTransform="bold">
|
||||
{nights}{" "}
|
||||
{nights > 1
|
||||
? intl.formatMessage({ id: "nights" })
|
||||
: intl.formatMessage({ id: "night" })}
|
||||
{intl.formatMessage(
|
||||
{ id: "booking.nights" },
|
||||
{ totalNights: nights }
|
||||
)}
|
||||
</Caption>
|
||||
<DatePicker />
|
||||
</div>
|
||||
@@ -47,20 +47,20 @@ export default function FormContent({
|
||||
<input type="text" placeholder={rooms} />
|
||||
</div>
|
||||
<div className={styles.vouchers}>
|
||||
<Caption color="textMediumContrast" textTransform="bold">
|
||||
<Caption color="uiTextMediumContrast" textTransform="bold">
|
||||
{vouchers}
|
||||
</Caption>
|
||||
<input type="text" placeholder={vouchers} />
|
||||
</div>
|
||||
<div className={styles.options}>
|
||||
<div className={styles.option}>
|
||||
<label className={styles.option}>
|
||||
<input type="checkbox" />
|
||||
<Caption color="textMediumContrast">{bonus}</Caption>
|
||||
</div>
|
||||
<div className={styles.option}>
|
||||
</label>
|
||||
<label className={styles.option}>
|
||||
<input type="checkbox" />
|
||||
<Caption color="textMediumContrast">{reward}</Caption>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -1,15 +1,36 @@
|
||||
.section {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
display: grid;
|
||||
margin: 0 auto;
|
||||
max-width: var(--max-width-navigation);
|
||||
}
|
||||
|
||||
.form {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.button {
|
||||
width: 118px;
|
||||
justify-content: center;
|
||||
.form {
|
||||
display: grid;
|
||||
gap: var(--Spacing-x2);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 1366px) {
|
||||
.form {
|
||||
align-self: flex-start;
|
||||
}
|
||||
|
||||
.button {
|
||||
align-self: flex-end;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (min-width: 1367px) {
|
||||
.section {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.button {
|
||||
justify-content: center;
|
||||
width: 118px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,22 +1,17 @@
|
||||
"use client"
|
||||
import { zodResolver } from "@hookform/resolvers/zod"
|
||||
import { useRouter } from "next/navigation"
|
||||
import { FormProvider, useForm } from "react-hook-form"
|
||||
import { useFormContext } from "react-hook-form"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import { dt } from "@/lib/dt"
|
||||
|
||||
import Button from "@/components/TempDesignSystem/Button"
|
||||
import Caption from "@/components/TempDesignSystem/Text/Caption"
|
||||
|
||||
import FormContent from "./FormContent"
|
||||
import { bookingWidgetSchema } from "./schema"
|
||||
|
||||
import styles from "./form.module.css"
|
||||
|
||||
import type { BookingWidgetSchema } from "@/types/components/bookingWidget"
|
||||
import type { BookingWidgetFormProps } from "@/types/components/form/bookingwidget"
|
||||
import type { Location } from "@/types/trpc/routers/hotel/locations"
|
||||
|
||||
const formId = "booking-widget"
|
||||
|
||||
@@ -24,40 +19,8 @@ export default function Form({ locations }: BookingWidgetFormProps) {
|
||||
const intl = useIntl()
|
||||
const router = useRouter()
|
||||
|
||||
const sessionStorageSearchData =
|
||||
typeof window !== "undefined"
|
||||
? sessionStorage.getItem("searchData")
|
||||
: undefined
|
||||
const initialSelectedLocation: Location | undefined = sessionStorageSearchData
|
||||
? JSON.parse(sessionStorageSearchData)
|
||||
: undefined
|
||||
const methods = useForm<BookingWidgetSchema>({
|
||||
defaultValues: {
|
||||
search: initialSelectedLocation?.name ?? "",
|
||||
location: sessionStorageSearchData
|
||||
? encodeURIComponent(sessionStorageSearchData)
|
||||
: undefined,
|
||||
date: {
|
||||
// UTC is required to handle requests from far away timezones https://scandichotels.atlassian.net/browse/SWAP-6375 & PET-507
|
||||
// This is specifically to handle timezones falling in different dates.
|
||||
from: dt().utc().format("YYYY-MM-DD"),
|
||||
to: dt().utc().add(1, "day").format("YYYY-MM-DD"),
|
||||
},
|
||||
bookingCode: "",
|
||||
redemption: false,
|
||||
voucher: false,
|
||||
rooms: [
|
||||
{
|
||||
adults: 1,
|
||||
childs: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
shouldFocusError: false,
|
||||
mode: "all",
|
||||
resolver: zodResolver(bookingWidgetSchema),
|
||||
reValidateMode: "onChange",
|
||||
})
|
||||
const { formState, handleSubmit, register } =
|
||||
useFormContext<BookingWidgetSchema>()
|
||||
|
||||
function onSubmit(data: BookingWidgetSchema) {
|
||||
data.location = JSON.parse(decodeURIComponent(data.location))
|
||||
@@ -70,25 +33,24 @@ export default function Form({ locations }: BookingWidgetFormProps) {
|
||||
return (
|
||||
<section className={styles.section}>
|
||||
<form
|
||||
onSubmit={methods.handleSubmit(onSubmit)}
|
||||
onSubmit={handleSubmit(onSubmit)}
|
||||
className={styles.form}
|
||||
id={formId}
|
||||
>
|
||||
<FormProvider {...methods}>
|
||||
<input {...methods.register("location")} type="hidden" />
|
||||
<FormContent locations={locations} />
|
||||
</FormProvider>
|
||||
<input {...register("location")} type="hidden" />
|
||||
<FormContent locations={locations} />
|
||||
</form>
|
||||
<Button
|
||||
type="submit"
|
||||
className={styles.button}
|
||||
disabled={!formState.isValid}
|
||||
form={formId}
|
||||
intent="primary"
|
||||
size="small"
|
||||
theme="base"
|
||||
intent="primary"
|
||||
className={styles.button}
|
||||
type="submit"
|
||||
>
|
||||
<Caption color="white" textTransform="bold">
|
||||
{intl.formatMessage({ id: "Find hotels" })}
|
||||
{intl.formatMessage({ id: "Search" })}
|
||||
</Caption>
|
||||
</Button>
|
||||
</section>
|
||||
|
||||
@@ -3,8 +3,9 @@ import { z } from "zod"
|
||||
import type { Location } from "@/types/trpc/routers/hotel/locations"
|
||||
|
||||
export const bookingWidgetSchema = z.object({
|
||||
search: z.string({ coerce: true }).min(1, "Required"),
|
||||
bookingCode: z.string(), // Update this as required when working with booking codes component
|
||||
date: z.object({
|
||||
// Update this as required once started working with Date picker in Nights component
|
||||
from: z.string(),
|
||||
to: z.string(),
|
||||
}),
|
||||
@@ -23,14 +24,12 @@ export const bookingWidgetSchema = z.object({
|
||||
},
|
||||
{ message: "Required" }
|
||||
),
|
||||
bookingCode: z.string(), // Update this as required when working with booking codes component
|
||||
redemption: z.boolean().default(false),
|
||||
voucher: z.boolean().default(false),
|
||||
rooms: z.array(
|
||||
// This will be updated when working in guests component
|
||||
z.object({
|
||||
adults: z.number().default(1),
|
||||
childs: z.array(
|
||||
children: z.array(
|
||||
z.object({
|
||||
age: z.number(),
|
||||
bed: z.number(),
|
||||
@@ -38,4 +37,6 @@ export const bookingWidgetSchema = z.object({
|
||||
),
|
||||
})
|
||||
),
|
||||
search: z.string({ coerce: true }).min(1, "Required"),
|
||||
voucher: z.boolean().default(false),
|
||||
})
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { Link } from "react-feather"
|
||||
|
||||
import { myPages } from "@/constants/routes/myPages"
|
||||
import { serverClient } from "@/lib/trpc/server"
|
||||
|
||||
import Link from "@/components/TempDesignSystem/Link"
|
||||
import { getIntl } from "@/i18n"
|
||||
import { getLang } from "@/i18n/serverContext"
|
||||
|
||||
|
||||
@@ -48,12 +48,6 @@
|
||||
gap: var(--Spacing-x-half);
|
||||
}
|
||||
|
||||
.link {
|
||||
display: flex;
|
||||
padding: var(--Spacing-x2) var(--Spacing-x0);
|
||||
border-bottom: 1px solid var(--Base-Border-Subtle);
|
||||
}
|
||||
|
||||
.prices {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -115,7 +109,7 @@
|
||||
padding-bottom: var(--Spacing-x2);
|
||||
}
|
||||
|
||||
.link {
|
||||
.detailsButton {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,5 @@
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import { mapFacilityToIcon } from "@/components/ContentType/HotelPage/data"
|
||||
import {
|
||||
ChevronRightIcon,
|
||||
PriceTagIcon,
|
||||
ScandicLogoIcon,
|
||||
} from "@/components/Icons"
|
||||
import { PriceTagIcon, ScandicLogoIcon } from "@/components/Icons"
|
||||
import TripAdvisorIcon from "@/components/Icons/TripAdvisor"
|
||||
import Image from "@/components/Image"
|
||||
import Button from "@/components/TempDesignSystem/Button"
|
||||
@@ -14,13 +8,16 @@ import Link from "@/components/TempDesignSystem/Link"
|
||||
import Caption from "@/components/TempDesignSystem/Text/Caption"
|
||||
import Footnote from "@/components/TempDesignSystem/Text/Footnote"
|
||||
import Title from "@/components/TempDesignSystem/Text/Title"
|
||||
import { getIntl } from "@/i18n"
|
||||
|
||||
import ReadMore from "../ReadMore"
|
||||
|
||||
import styles from "./hotelCard.module.css"
|
||||
|
||||
import { HotelCardProps } from "@/types/components/hotelReservation/selectHotel/hotelCardProps"
|
||||
|
||||
export default function HotelCard({ hotel }: HotelCardProps) {
|
||||
const intl = useIntl()
|
||||
export default async function HotelCard({ hotel }: HotelCardProps) {
|
||||
const intl = await getIntl()
|
||||
|
||||
const { hotelData } = hotel
|
||||
const { price } = hotel
|
||||
@@ -51,7 +48,7 @@ export default function HotelCard({ hotel }: HotelCardProps) {
|
||||
<Title as="h4" textTransform="capitalize">
|
||||
{hotelData.name}
|
||||
</Title>
|
||||
<Footnote color="textMediumContrast" className={styles.adress}>
|
||||
<Footnote color="textMediumContrast">
|
||||
{`${hotelData.address.streetAddress}, ${hotelData.address.city}`}
|
||||
</Footnote>
|
||||
<Footnote color="textMediumContrast">
|
||||
@@ -70,10 +67,7 @@ export default function HotelCard({ hotel }: HotelCardProps) {
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
<Link href="#" color="burgundy" className={styles.link}>
|
||||
{intl.formatMessage({ id: "See hotel details" })}
|
||||
<ChevronRightIcon color="burgundy" />
|
||||
</Link>
|
||||
<ReadMore hotelId={hotelData.operaId} hotel={hotelData} />
|
||||
</section>
|
||||
<section className={styles.prices}>
|
||||
<div>
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
"use client"
|
||||
|
||||
import Title from "@/components/TempDesignSystem/Text/Title"
|
||||
|
||||
import HotelCard from "../HotelCard"
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
.buttons {
|
||||
display: flex;
|
||||
gap: var(--Spacing-x3);
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import ChevronRightSmallIcon from "@/components/Icons/ChevronRightSmall"
|
||||
import Button from "@/components/TempDesignSystem/Button"
|
||||
import SidePeek from "@/components/TempDesignSystem/SidePeek"
|
||||
|
||||
import styles from "./hotelDetailSidePeek.module.css"
|
||||
|
||||
export default function HotelDetailSidePeek() {
|
||||
const intl = useIntl()
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
|
||||
function toggleSidePeek() {
|
||||
setIsOpen(!isOpen)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={styles.buttons}>
|
||||
<Button
|
||||
variant="icon"
|
||||
theme="base"
|
||||
intent="text"
|
||||
wrapping
|
||||
onClick={toggleSidePeek}
|
||||
>
|
||||
{intl.formatMessage({
|
||||
id: "See hotel details",
|
||||
})}
|
||||
<ChevronRightSmallIcon aria-hidden="true" color="burgundy" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="icon"
|
||||
theme="base"
|
||||
intent="text"
|
||||
wrapping
|
||||
onClick={toggleSidePeek}
|
||||
>
|
||||
{intl.formatMessage({
|
||||
id: "Show all amenities",
|
||||
})}
|
||||
<ChevronRightSmallIcon aria-hidden="true" color="burgundy" />
|
||||
</Button>
|
||||
</div>
|
||||
<SidePeek
|
||||
contentKey="hotel-detail-side-peek"
|
||||
title="Hotel Details"
|
||||
isOpen={isOpen}
|
||||
handleClose={() => setIsOpen(false)}
|
||||
>
|
||||
<div>TBD</div>
|
||||
</SidePeek>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
.hotelSelectionHeader {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: var(--Base-Surface-Subtle-Normal);
|
||||
padding: var(--Spacing-x3) var(--Spacing-x2);
|
||||
justify-content: center;
|
||||
gap: var(--Spacing-x3);
|
||||
}
|
||||
|
||||
.titleContainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
justify-content: center;
|
||||
gap: var(--Spacing-x1);
|
||||
}
|
||||
|
||||
.descriptionContainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--Spacing-x-one-and-half);
|
||||
}
|
||||
|
||||
.address {
|
||||
display: flex;
|
||||
gap: var(--Spacing-x-one-and-half);
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
.dividerContainer {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.hotelSelectionHeader {
|
||||
flex-direction: row;
|
||||
padding: var(--Spacing-x4) var(--Spacing-x5);
|
||||
gap: var(--Spacing-x6);
|
||||
}
|
||||
|
||||
.titleContainer > h1 {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.dividerContainer {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.address {
|
||||
gap: var(--Spacing-x3);
|
||||
}
|
||||
}
|
||||
52
components/HotelReservation/HotelSelectionHeader/index.tsx
Normal file
52
components/HotelReservation/HotelSelectionHeader/index.tsx
Normal file
@@ -0,0 +1,52 @@
|
||||
import Divider from "@/components/TempDesignSystem/Divider"
|
||||
import Body from "@/components/TempDesignSystem/Text/Body"
|
||||
import Caption from "@/components/TempDesignSystem/Text/Caption"
|
||||
import Title from "@/components/TempDesignSystem/Text/Title"
|
||||
import { getIntl } from "@/i18n"
|
||||
|
||||
import HotelDetailSidePeek from "./HotelDetailSidePeek"
|
||||
|
||||
import styles from "./hotelSelectionHeader.module.css"
|
||||
|
||||
import { HotelSelectionHeaderProps } from "@/types/components/hotelReservation/selectRate/hotelSelectionHeader"
|
||||
|
||||
export default async function HotelSelectionHeader({
|
||||
hotel,
|
||||
}: HotelSelectionHeaderProps) {
|
||||
const intl = await getIntl()
|
||||
|
||||
return (
|
||||
<header className={styles.hotelSelectionHeader}>
|
||||
<div className={styles.titleContainer}>
|
||||
<Title as="h3" level="h1">
|
||||
{hotel.name}
|
||||
</Title>
|
||||
<address className={styles.address}>
|
||||
<Caption color="textMediumContrast">
|
||||
{hotel.address.streetAddress}, {hotel.address.city}
|
||||
</Caption>
|
||||
<div>
|
||||
<Divider variant="vertical" color="subtle" />
|
||||
</div>
|
||||
<Caption color="textMediumContrast">
|
||||
{intl.formatMessage(
|
||||
{
|
||||
id: "Distance to city centre",
|
||||
},
|
||||
{ number: hotel.location.distanceToCentre }
|
||||
)}
|
||||
</Caption>
|
||||
</address>
|
||||
</div>
|
||||
<div className={styles.dividerContainer}>
|
||||
<Divider variant="vertical" color="subtle" />
|
||||
</div>
|
||||
<div className={styles.descriptionContainer}>
|
||||
<Body color="textHighContrast">
|
||||
{hotel.hotelContent.texts.descriptions.short}
|
||||
</Body>
|
||||
<HotelDetailSidePeek />
|
||||
</div>
|
||||
</header>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
.wrapper {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
grid-template-rows: auto;
|
||||
gap: var(--Spacing-x2);
|
||||
font-family: var(--typography-Body-Regular-fontFamily);
|
||||
}
|
||||
|
||||
.address,
|
||||
.contactInfo {
|
||||
display: grid;
|
||||
grid-template-columns: subgrid;
|
||||
grid-template-rows: subgrid;
|
||||
grid-column: 1 / 3;
|
||||
grid-row: 1 / 4;
|
||||
}
|
||||
|
||||
.contactInfo > li {
|
||||
font-style: normal;
|
||||
list-style-type: none;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.heading {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.soMeIcons {
|
||||
display: flex;
|
||||
gap: var(--Spacing-x-one-and-half);
|
||||
}
|
||||
|
||||
.ecoLabel {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr;
|
||||
column-gap: var(--Spacing-x-one-and-half);
|
||||
grid-column: 2 / 3;
|
||||
grid-row: 3 / 4;
|
||||
font-size: var(--typography-Footnote-Regular-fontSize);
|
||||
line-height: ();
|
||||
}
|
||||
|
||||
.ecoLabelText {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
85
components/HotelReservation/ReadMore/Contact/index.tsx
Normal file
85
components/HotelReservation/ReadMore/Contact/index.tsx
Normal file
@@ -0,0 +1,85 @@
|
||||
"use client"
|
||||
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import FacebookIcon from "@/components/Icons/Facebook"
|
||||
import InstagramIcon from "@/components/Icons/Instagram"
|
||||
import Image from "@/components/Image"
|
||||
import Link from "@/components/TempDesignSystem/Link"
|
||||
import useLang from "@/hooks/useLang"
|
||||
|
||||
import styles from "./contact.module.css"
|
||||
|
||||
import { ContactProps } from "@/types/components/hotelReservation/selectHotel/selectHotel"
|
||||
|
||||
export default function Contact({ hotel }: ContactProps) {
|
||||
const lang = useLang()
|
||||
const intl = useIntl()
|
||||
|
||||
return (
|
||||
<section className={styles.wrapper}>
|
||||
<address className={styles.address}>
|
||||
<ul className={styles.contactInfo}>
|
||||
<li>
|
||||
<span className={styles.heading}>
|
||||
{intl.formatMessage({ id: "Address" })}
|
||||
</span>
|
||||
<span>{hotel.address.streetAddress}</span>
|
||||
<span>{hotel.address.city}</span>
|
||||
</li>
|
||||
<li>
|
||||
<span className={styles.heading}>
|
||||
{intl.formatMessage({ id: "Driving directions" })}
|
||||
</span>
|
||||
<Link href="#">{intl.formatMessage({ id: "Google Maps" })}</Link>
|
||||
</li>
|
||||
<li>
|
||||
<span className={styles.heading}>
|
||||
{intl.formatMessage({ id: "Email" })}
|
||||
</span>
|
||||
<Link href={`mailto:${hotel.contactInformation.email}`}>
|
||||
{hotel.contactInformation.email}
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<span className={styles.heading}>
|
||||
{intl.formatMessage({ id: "Contact us" })}
|
||||
</span>
|
||||
<Link href={`tel:${hotel.contactInformation.phoneNumber}`}>
|
||||
{hotel.contactInformation.phoneNumber}
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<span className={styles.heading}>
|
||||
{intl.formatMessage({ id: "Follow us" })}
|
||||
</span>
|
||||
<div className={styles.soMeIcons}>
|
||||
<Link href="#" target="_blank">
|
||||
<InstagramIcon color="burgundy" />
|
||||
</Link>
|
||||
<Link href="#" target="_blank">
|
||||
<FacebookIcon color="burgundy" />
|
||||
</Link>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</address>
|
||||
{hotel.hotelFacts.ecoLabels.nordicEcoLabel ? (
|
||||
<div className={styles.ecoLabel}>
|
||||
<Image
|
||||
height={38}
|
||||
width={38}
|
||||
alt={intl.formatMessage({ id: "Nordic Swan Ecolabel" })}
|
||||
src={`/_static/img/icons/swan-eco/swan_eco_dark_${lang}.png`}
|
||||
/>
|
||||
<div className={styles.ecoLabelText}>
|
||||
<span>{intl.formatMessage({ id: "Nordic Swan Ecolabel" })}</span>
|
||||
<span>
|
||||
{hotel.hotelFacts.ecoLabels.svanenEcoLabelCertificateNumber}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</section>
|
||||
)
|
||||
}
|
||||
119
components/HotelReservation/ReadMore/index.tsx
Normal file
119
components/HotelReservation/ReadMore/index.tsx
Normal file
@@ -0,0 +1,119 @@
|
||||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import { ChevronRightIcon } from "@/components/Icons"
|
||||
import Accordion from "@/components/TempDesignSystem/Accordion"
|
||||
import AccordionItem from "@/components/TempDesignSystem/Accordion/AccordionItem"
|
||||
import Button from "@/components/TempDesignSystem/Button"
|
||||
import SidePeek from "@/components/TempDesignSystem/SidePeek"
|
||||
import Body from "@/components/TempDesignSystem/Text/Body"
|
||||
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
|
||||
|
||||
import Contact from "./Contact"
|
||||
|
||||
import styles from "./readMore.module.css"
|
||||
|
||||
import {
|
||||
DetailedAmenity,
|
||||
ParkingProps,
|
||||
ReadMoreProps,
|
||||
} from "@/types/components/hotelReservation/selectHotel/selectHotel"
|
||||
import { Hotel } from "@/types/hotel"
|
||||
|
||||
function getAmenitiesList(hotel: Hotel) {
|
||||
const detailedAmenities: DetailedAmenity[] = Object.entries(
|
||||
hotel.hotelFacts.hotelFacilityDetail
|
||||
).map(([key, value]) => ({ name: key, ...value }))
|
||||
|
||||
// Remove Parking facilities since parking accordion is based on hotel.parking
|
||||
const simpleAmenities = hotel.detailedFacilities.filter(
|
||||
(facility) => !facility.name.startsWith("Parking")
|
||||
)
|
||||
return [...detailedAmenities, ...simpleAmenities]
|
||||
}
|
||||
|
||||
export default function ReadMore({ hotel, hotelId }: ReadMoreProps) {
|
||||
const intl = useIntl()
|
||||
|
||||
const [sidePeekOpen, setSidePeekOpen] = useState(false)
|
||||
|
||||
const amenitiesList = getAmenitiesList(hotel)
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
onPress={() => {
|
||||
setSidePeekOpen(true)
|
||||
}}
|
||||
intent={"text"}
|
||||
color="burgundy"
|
||||
className={styles.detailsButton}
|
||||
>
|
||||
{intl.formatMessage({ id: "See hotel details" })}
|
||||
<ChevronRightIcon color="burgundy" />
|
||||
</Button>
|
||||
<SidePeek
|
||||
title={hotel.name}
|
||||
isOpen={sidePeekOpen}
|
||||
contentKey={`${hotelId}`}
|
||||
handleClose={() => {
|
||||
setSidePeekOpen(false)
|
||||
}}
|
||||
>
|
||||
<div className={styles.content}>
|
||||
<Subtitle>
|
||||
{intl.formatMessage({ id: "Practical information" })}
|
||||
</Subtitle>
|
||||
<Contact hotel={hotel} />
|
||||
<Accordion>
|
||||
{/* parking */}
|
||||
{hotel.parking.length ? (
|
||||
<AccordionItem title={intl.formatMessage({ id: "Parking" })}>
|
||||
{hotel.parking.map((p) => (
|
||||
<Parking key={p.name} parking={p} />
|
||||
))}
|
||||
</AccordionItem>
|
||||
) : null}
|
||||
<AccordionItem title={intl.formatMessage({ id: "Accessibility" })}>
|
||||
TODO: What content should be in the accessibility section?
|
||||
</AccordionItem>
|
||||
{amenitiesList.map((amenity) => {
|
||||
return "description" in amenity ? (
|
||||
<AccordionItem key={amenity.name} title={amenity.heading}>
|
||||
{amenity.description}
|
||||
</AccordionItem>
|
||||
) : (
|
||||
<div key={amenity.id} className={styles.amenity}>
|
||||
{amenity.name}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</Accordion>
|
||||
{/* TODO: handle linking to Hotel Page */}
|
||||
<Button theme={"base"}>To the hotel</Button>
|
||||
</div>
|
||||
</SidePeek>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function Parking({ parking }: ParkingProps) {
|
||||
const intl = useIntl()
|
||||
return (
|
||||
<div>
|
||||
<Body>{`${intl.formatMessage({ id: parking.type })} (${parking.name})`}</Body>
|
||||
<ul className={styles.list}>
|
||||
<li>
|
||||
{`${intl.formatMessage({
|
||||
id: "Number of charging points for electric cars",
|
||||
})}: ${parking.numberOfChargingSpaces}`}
|
||||
</li>
|
||||
<li>{`${intl.formatMessage({ id: "Parking can be reserved in advance" })}: ${parking.canMakeReservation ? intl.formatMessage({ id: "Yes" }) : intl.formatMessage({ id: "No" })}`}</li>
|
||||
<li>{`${intl.formatMessage({ id: "Number of parking spots" })}: ${parking.numberOfParkingSpots}`}</li>
|
||||
<li>{`${intl.formatMessage({ id: "Distance to hotel" })}: ${parking.distanceToHotel}`}</li>
|
||||
<li>{`${intl.formatMessage({ id: "Address" })}: ${parking.address}`}</li>
|
||||
</ul>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
25
components/HotelReservation/ReadMore/readMore.module.css
Normal file
25
components/HotelReservation/ReadMore/readMore.module.css
Normal file
@@ -0,0 +1,25 @@
|
||||
.detailsButton {
|
||||
align-self: start;
|
||||
border-radius: 0;
|
||||
height: auto;
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
.content {
|
||||
display: grid;
|
||||
gap: var(--Spacing-x2);
|
||||
}
|
||||
|
||||
.amenity {
|
||||
font-family: var(--typography-Body-Regular-fontFamily);
|
||||
border-bottom: 1px solid var(--Base-Border-Subtle);
|
||||
/* padding set to align with AccordionItem which has a different composition */
|
||||
padding: var(--Spacing-x2)
|
||||
calc(var(--Spacing-x1) + var(--Spacing-x-one-and-half));
|
||||
}
|
||||
|
||||
.list {
|
||||
font-family: var(--typography-Body-Regular-fontFamily);
|
||||
list-style: inside;
|
||||
}
|
||||
@@ -4,7 +4,7 @@ import { useIntl } from "react-intl"
|
||||
|
||||
import styles from "./hotelFilter.module.css"
|
||||
|
||||
import { HotelFiltersProps } from "@/types/components/hotelReservation/selectHotel/hotelFiltersProps"
|
||||
import { HotelFiltersProps } from "@/types/components/hotelReservation/selectHotel/hotelFilters"
|
||||
|
||||
export default function HotelFilter({ filters }: HotelFiltersProps) {
|
||||
const intl = useIntl()
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
"use client"
|
||||
|
||||
import { HotelListingProps } from "@/types/components/hotelReservation/selectHotel/map"
|
||||
|
||||
// TODO: This component is copied from
|
||||
// components/ContentType/HotelPage/Map/DynamicMap/Sidebar.
|
||||
// Look at that for inspiration on how to do the interaction with the map.
|
||||
|
||||
export default function HotelListing({}: HotelListingProps) {
|
||||
return <section>Hotel listing TBI</section>
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
"use client"
|
||||
import { APIProvider } from "@vis.gl/react-google-maps"
|
||||
import { useState } from "react"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import { selectHotel } from "@/constants/routes/hotelReservation"
|
||||
|
||||
import { CloseIcon } from "@/components/Icons"
|
||||
import InteractiveMap from "@/components/Maps/InteractiveMap"
|
||||
import Button from "@/components/TempDesignSystem/Button"
|
||||
import Link from "@/components/TempDesignSystem/Link"
|
||||
import useLang from "@/hooks/useLang"
|
||||
|
||||
import HotelListing from "./HotelListing"
|
||||
|
||||
import styles from "./selectHotelMap.module.css"
|
||||
|
||||
import { SelectHotelMapProps } from "@/types/components/hotelReservation/selectHotel/map"
|
||||
|
||||
export default function SelectHotelMap({
|
||||
apiKey,
|
||||
coordinates,
|
||||
pointsOfInterest,
|
||||
mapId,
|
||||
}: SelectHotelMapProps) {
|
||||
const lang = useLang()
|
||||
const intl = useIntl()
|
||||
const [activePoi, setActivePoi] = useState<string | null>(null)
|
||||
|
||||
const closeButton = (
|
||||
<Button
|
||||
asChild
|
||||
intent="inverted"
|
||||
size="small"
|
||||
theme="base"
|
||||
className={styles.closeButton}
|
||||
>
|
||||
<Link href={selectHotel[lang]} keepSearchParams color="burgundy">
|
||||
<CloseIcon color="burgundy" />
|
||||
{intl.formatMessage({ id: "Close the map" })}
|
||||
</Link>
|
||||
</Button>
|
||||
)
|
||||
return (
|
||||
<APIProvider apiKey={apiKey}>
|
||||
<HotelListing />
|
||||
<InteractiveMap
|
||||
closeButton={closeButton}
|
||||
coordinates={coordinates}
|
||||
pointsOfInterest={pointsOfInterest}
|
||||
activePoi={activePoi}
|
||||
onActivePoiChange={setActivePoi}
|
||||
mapId={mapId}
|
||||
/>
|
||||
</APIProvider>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
.closeButton {
|
||||
pointer-events: initial;
|
||||
box-shadow: var(--button-box-shadow);
|
||||
gap: var(--Spacing-x-half);
|
||||
}
|
||||
36
components/Icons/Edit.tsx
Normal file
36
components/Icons/Edit.tsx
Normal file
@@ -0,0 +1,36 @@
|
||||
import { iconVariants } from "./variants"
|
||||
|
||||
import type { IconProps } from "@/types/components/icon"
|
||||
|
||||
export default function EditIcon({ 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
|
||||
height="20"
|
||||
id="mask0_162_2666"
|
||||
maskUnits="userSpaceOnUse"
|
||||
style={{ maskType: "alpha" }}
|
||||
width="20"
|
||||
x="0"
|
||||
y="0"
|
||||
>
|
||||
<rect width="20" height="20" fill="#D9D9D9" />
|
||||
</mask>
|
||||
<g mask="url(#mask0_162_2666)">
|
||||
<path
|
||||
d="M4.58333 16.8125C4.19949 16.8125 3.87088 16.6758 3.59752 16.4025C3.32417 16.1291 3.1875 15.8005 3.1875 15.4167V4.58333C3.1875 4.19375 3.32313 3.86024 3.5944 3.58281C3.86566 3.30538 4.19531 3.17361 4.58333 3.1875H11.7292L10.3333 4.58333H4.58333V15.4167H15.4167V9.65625L16.8125 8.26042V15.4167C16.8125 15.8005 16.6758 16.1291 16.4025 16.4025C16.1291 16.6758 15.8005 16.8125 15.4167 16.8125H4.58333ZM8.05208 11.9479V8.83333L15.4583 1.42708C15.6042 1.28125 15.7569 1.17882 15.9167 1.11979C16.0764 1.06076 16.245 1.03125 16.4224 1.03125C16.6116 1.03125 16.7899 1.06076 16.9573 1.11979C17.1247 1.17882 17.2808 1.27974 17.4259 1.42256L18.5625 2.5625C18.7083 2.70833 18.8125 2.86585 18.875 3.03504C18.9375 3.20424 18.9688 3.38006 18.9688 3.5625C18.9688 3.74529 18.9373 3.91971 18.8745 4.08575C18.8118 4.25179 18.7077 4.40724 18.5625 4.55208L11.1667 11.9479H8.05208ZM9.44792 10.5521H10.5938L15.4583 5.67708L14.8958 5.09375L14.3229 4.54167L9.44792 9.40625V10.5521Z"
|
||||
fill="#26201E"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
@@ -23,6 +23,7 @@ export { default as CrossCircle } from "./CrossCircle"
|
||||
export { default as CulturalIcon } from "./Cultural"
|
||||
export { default as DeleteIcon } from "./Delete"
|
||||
export { default as DoorOpenIcon } from "./DoorOpen"
|
||||
export { default as EditIcon } from "./Edit"
|
||||
export { default as ElectricBikeIcon } from "./ElectricBike"
|
||||
export { default as EmailIcon } from "./Email"
|
||||
export { default as ErrorCircleIcon } from "./ErrorCircle"
|
||||
|
||||
@@ -18,16 +18,12 @@
|
||||
color: var(--Primary-Light-On-Surface-Accent);
|
||||
}
|
||||
|
||||
.li:has(.heart),
|
||||
.li:has(.check) {
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.li {
|
||||
margin-left: var(--Spacing-x3);
|
||||
}
|
||||
|
||||
.li:has(.heart):before {
|
||||
.heart > .li::before,
|
||||
.li:has(.heart)::before {
|
||||
content: url("/_static/icons/heart.svg");
|
||||
position: relative;
|
||||
height: 8px;
|
||||
@@ -36,6 +32,14 @@
|
||||
margin-left: calc(var(--Spacing-x3) * -1);
|
||||
}
|
||||
|
||||
.heart > .li,
|
||||
.check > .li,
|
||||
.li:has(.check),
|
||||
.li:has(.heart) {
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.check > .li::before,
|
||||
.li:has(.check)::before {
|
||||
content: url("/_static/icons/check-ring.svg");
|
||||
position: relative;
|
||||
@@ -59,6 +63,7 @@
|
||||
max-width: 100%;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
.ol:has(li:nth-last-child(n + 5)),
|
||||
.ul:has(li:nth-last-child(n + 5)) {
|
||||
@@ -66,3 +71,11 @@
|
||||
grid-auto-flow: column;
|
||||
}
|
||||
}
|
||||
|
||||
@container sidebar (max-width: 360px) {
|
||||
.ol,
|
||||
.ul {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,11 @@ import Caption from "../TempDesignSystem/Text/Caption"
|
||||
import Footnote from "../TempDesignSystem/Text/Footnote"
|
||||
import Subtitle from "../TempDesignSystem/Text/Subtitle"
|
||||
import Title from "../TempDesignSystem/Text/Title"
|
||||
import { hasAvailableParagraphFormat, hasAvailableULFormat } from "./utils"
|
||||
import {
|
||||
hasAvailableParagraphFormat,
|
||||
hasAvailableULFormat,
|
||||
makeCssModuleCompatibleClassName,
|
||||
} from "./utils"
|
||||
|
||||
import styles from "./jsontohtml.module.css"
|
||||
|
||||
@@ -217,8 +221,16 @@ export const renderOptions: RenderOptions = {
|
||||
fullRenderOptions: RenderOptions
|
||||
) => {
|
||||
const props = extractPossibleAttributes(node.attrs)
|
||||
const compatibleClassName = makeCssModuleCompatibleClassName(
|
||||
props.className,
|
||||
"ul"
|
||||
)
|
||||
return (
|
||||
<li key={node.uid} {...props} className={styles.li}>
|
||||
<li
|
||||
key={node.uid}
|
||||
{...props}
|
||||
className={`${styles.li} ${compatibleClassName}`}
|
||||
>
|
||||
{next(node.children, embeds, fullRenderOptions)}
|
||||
</li>
|
||||
)
|
||||
@@ -525,6 +537,10 @@ export const renderOptions: RenderOptions = {
|
||||
fullRenderOptions: RenderOptions
|
||||
) => {
|
||||
const props = extractPossibleAttributes(node.attrs)
|
||||
const compatibleClassName = makeCssModuleCompatibleClassName(
|
||||
props.className,
|
||||
"ul"
|
||||
)
|
||||
|
||||
// Set the number of rows dynamically to create even rows for each column. We want the li:s
|
||||
// to flow with the column, so therefore this is needed.
|
||||
@@ -538,7 +554,7 @@ export const renderOptions: RenderOptions = {
|
||||
<ul
|
||||
key={node.uid}
|
||||
{...props}
|
||||
className={styles.ul}
|
||||
className={`${styles.ul} ${compatibleClassName}`}
|
||||
style={
|
||||
numberOfRows
|
||||
? {
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { renderOptions } from "./renderOptions"
|
||||
|
||||
import styles from "./jsontohtml.module.css"
|
||||
|
||||
import type { Node } from "@/types/requests/utils/edges"
|
||||
import type { EmbedByUid } from "@/types/transitionTypes/jsontohtml"
|
||||
import {
|
||||
@@ -136,3 +138,19 @@ export function nodesToHtml(
|
||||
const fullRenderOptions = { ...renderOptions, ...overrideRenderOptions }
|
||||
return nodes.map((node) => nodeToHtml(node, embeds, fullRenderOptions))
|
||||
}
|
||||
|
||||
export function makeCssModuleCompatibleClassName(
|
||||
className: string | undefined,
|
||||
formatType: "ul"
|
||||
): string {
|
||||
if (!className) return ""
|
||||
|
||||
if (formatType === "ul" && hasAvailableULFormat(className)) {
|
||||
// @ts-ignore: We want to set css modules classNames even if it does not correspond
|
||||
// to an existing class in the module style sheet. Due to our css modules plugin for
|
||||
// typescript, we cannot do this without the ts-ignore
|
||||
return styles[className] || className
|
||||
}
|
||||
|
||||
return className
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ import type { LanguageSwitcherContentProps } from "@/types/components/languageSw
|
||||
|
||||
export default function LanguageSwitcherContent({
|
||||
urls,
|
||||
onLanguageSwitch,
|
||||
}: LanguageSwitcherContentProps) {
|
||||
const intl = useIntl()
|
||||
const currentLanguage = useLang()
|
||||
@@ -39,6 +40,7 @@ export default function LanguageSwitcherContent({
|
||||
<Link
|
||||
className={`${styles.link} ${isActive ? styles.active : ""}`}
|
||||
href={url}
|
||||
onClick={onLanguageSwitch}
|
||||
>
|
||||
{languages[key]}
|
||||
{isActive ? <CheckIcon color="burgundy" /> : null}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"use client"
|
||||
|
||||
import { useEffect, useRef } from "react"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import { languages } from "@/constants/languages"
|
||||
@@ -28,6 +29,7 @@ export default function LanguageSwitcher({
|
||||
const intl = useIntl()
|
||||
const currentLanguage = useLang()
|
||||
const toggleDropdown = useDropdownStore((state) => state.toggleDropdown)
|
||||
const languageSwitcherRef = useRef<HTMLDivElement>(null)
|
||||
const isFooterLanguageSwitcherOpen = useDropdownStore(
|
||||
(state) => state.isFooterLanguageSwitcherOpen
|
||||
)
|
||||
@@ -70,10 +72,37 @@ export default function LanguageSwitcher({
|
||||
})
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
function handleClickOutside(evt: Event) {
|
||||
const target = evt.target as HTMLElement
|
||||
if (
|
||||
languageSwitcherRef.current &&
|
||||
target &&
|
||||
!languageSwitcherRef.current.contains(target) &&
|
||||
isLanguageSwitcherOpen &&
|
||||
!isHeaderLanguageSwitcherMobileOpen
|
||||
) {
|
||||
toggleDropdown(dropdownType)
|
||||
}
|
||||
}
|
||||
|
||||
if (languageSwitcherRef.current) {
|
||||
document.addEventListener("click", handleClickOutside)
|
||||
}
|
||||
return () => {
|
||||
document.removeEventListener("click", handleClickOutside)
|
||||
}
|
||||
}, [
|
||||
dropdownType,
|
||||
toggleDropdown,
|
||||
isLanguageSwitcherOpen,
|
||||
isHeaderLanguageSwitcherMobileOpen,
|
||||
])
|
||||
|
||||
const classNames = languageSwitcherVariants({ color, position })
|
||||
|
||||
return (
|
||||
<div className={classNames}>
|
||||
<div className={classNames} ref={languageSwitcherRef}>
|
||||
<button
|
||||
type="button"
|
||||
className={styles.button}
|
||||
@@ -99,7 +128,10 @@ export default function LanguageSwitcher({
|
||||
>
|
||||
{isLanguageSwitcherOpen ? (
|
||||
<LanguageSwitcherContainer type={type}>
|
||||
<LanguageSwitcherContent urls={urls} />
|
||||
<LanguageSwitcherContent
|
||||
urls={urls}
|
||||
onLanguageSwitch={() => toggleDropdown(dropdownType)}
|
||||
/>
|
||||
</LanguageSwitcherContainer>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
@@ -8,29 +8,26 @@ import {
|
||||
} from "@vis.gl/react-google-maps"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import useHotelPageStore from "@/stores/hotel-page"
|
||||
|
||||
import { MinusIcon, PlusIcon } from "@/components/Icons"
|
||||
import CloseLargeIcon from "@/components/Icons/CloseLarge"
|
||||
import PoiMarker from "@/components/Maps/Markers/Poi"
|
||||
import ScandicMarker from "@/components/Maps/Markers/Scandic"
|
||||
import Button from "@/components/TempDesignSystem/Button"
|
||||
import Body from "@/components/TempDesignSystem/Text/Body"
|
||||
import Caption from "@/components/TempDesignSystem/Text/Caption"
|
||||
|
||||
import styles from "./map.module.css"
|
||||
import styles from "./interactiveMap.module.css"
|
||||
|
||||
import type { MapContentProps } from "@/types/components/hotelPage/map/mapContent"
|
||||
import type { InteractiveMapProps } from "@/types/components/hotelPage/map/interactiveMap"
|
||||
|
||||
export default function MapContent({
|
||||
export default function InteractiveMap({
|
||||
coordinates,
|
||||
pointsOfInterest,
|
||||
activePoi,
|
||||
mapId,
|
||||
onActivePoiChange,
|
||||
}: MapContentProps) {
|
||||
closeButton,
|
||||
}: InteractiveMapProps) {
|
||||
const intl = useIntl()
|
||||
const { closeDynamicMap } = useHotelPageStore()
|
||||
const map = useMap()
|
||||
|
||||
const mapOptions: MapProps = {
|
||||
@@ -98,17 +95,7 @@ export default function MapContent({
|
||||
))}
|
||||
</Map>
|
||||
<div className={styles.ctaButtons}>
|
||||
<Button
|
||||
theme="base"
|
||||
intent="inverted"
|
||||
variant="icon"
|
||||
size="small"
|
||||
className={styles.closeButton}
|
||||
onClick={closeDynamicMap}
|
||||
>
|
||||
<CloseLargeIcon color="burgundy" />
|
||||
<span>{intl.formatMessage({ id: "Close the map" })}</span>
|
||||
</Button>
|
||||
{closeButton}
|
||||
<div className={styles.zoomButtons}>
|
||||
<Button
|
||||
theme="base"
|
||||
@@ -1,15 +1,23 @@
|
||||
.aside {
|
||||
display: none;
|
||||
display: grid;
|
||||
container-name: sidebar;
|
||||
container-type: inline-size;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: var(--Spacing-x0) var(--Spacing-x2);
|
||||
}
|
||||
|
||||
@media screen and (min-width: 1366px) {
|
||||
@media screen and (min-width: 1367px) {
|
||||
.aside {
|
||||
align-content: flex-start;
|
||||
display: grid;
|
||||
gap: var(--Spacing-x4);
|
||||
}
|
||||
}
|
||||
|
||||
@container loyalty-page (max-width: 1366px) {
|
||||
.aside {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
.btn {
|
||||
background: none;
|
||||
/* No variable yet for radius 50px */
|
||||
border-radius: 50px;
|
||||
border-radius: var(--Corner-radius-Rounded);
|
||||
cursor: pointer;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
@@ -10,12 +9,12 @@
|
||||
background-color 300ms ease,
|
||||
color 300ms ease;
|
||||
|
||||
/* TODO: Waiting for variables for buttons from Design team */
|
||||
font-family: var(--typography-Body-Bold-fontFamily);
|
||||
font-size: var(--typography-Body-Bold-fontSize);
|
||||
font-weight: 500;
|
||||
line-height: 24px;
|
||||
line-height: var(--typography-Body-Bold-lineHeight);
|
||||
letter-spacing: 0.6%;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.wrapping {
|
||||
@@ -23,6 +22,10 @@
|
||||
padding-right: 0 !important;
|
||||
}
|
||||
|
||||
.fullWidth {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* INTENT */
|
||||
.primary,
|
||||
a.primary {
|
||||
@@ -69,21 +72,33 @@ a.default {
|
||||
}
|
||||
|
||||
/* SIZES */
|
||||
.small {
|
||||
.btn.small {
|
||||
font-size: var(--typography-Caption-Bold-fontSize);
|
||||
line-height: var(--typography-Caption-Bold-lineHeight);
|
||||
gap: var(--Spacing-x-quarter);
|
||||
height: 40px;
|
||||
padding: calc(var(--Spacing-x1) + 2px) var(--Spacing-x2); /* Special case padding to adjust the missing border */
|
||||
}
|
||||
|
||||
.btn.small.secondary {
|
||||
padding: var(--Spacing-x1) var(--Spacing-x2);
|
||||
}
|
||||
|
||||
.medium {
|
||||
.btn.medium {
|
||||
gap: var(--Spacing-x-half);
|
||||
height: 48px;
|
||||
padding: calc(var(--Spacing-x-one-and-half) + 2px) var(--Spacing-x2); /* Special case padding to adjust the missing border */
|
||||
}
|
||||
|
||||
.medium.secondary {
|
||||
padding: var(--Spacing-x-one-and-half) var(--Spacing-x2);
|
||||
}
|
||||
|
||||
.large {
|
||||
.btn.large {
|
||||
gap: var(--Spacing-x-half);
|
||||
padding: calc(var(--Spacing-x2) + 2px) var(--Spacing-x3); /* Special case padding to adjust the missing border */
|
||||
}
|
||||
|
||||
.large.secondary {
|
||||
gap: var(--Spacing-x-half);
|
||||
height: 56px;
|
||||
padding: var(--Spacing-x2) var(--Spacing-x3);
|
||||
}
|
||||
|
||||
@@ -170,19 +185,19 @@ a.default {
|
||||
fill: var(--Base-Button-Secondary-On-Fill-Disabled);
|
||||
}
|
||||
|
||||
.baseTertiary {
|
||||
.btn.baseTertiary {
|
||||
background-color: var(--Base-Button-Tertiary-Fill-Normal);
|
||||
color: var(--Base-Button-Tertiary-On-Fill-Normal);
|
||||
}
|
||||
|
||||
.baseTertiary:active,
|
||||
.baseTertiary:focus,
|
||||
.baseTertiary:hover {
|
||||
.btn.baseTertiary:active,
|
||||
.btn.baseTertiary:focus,
|
||||
.btn.baseTertiary:hover {
|
||||
background-color: var(--Base-Button-Tertiary-Fill-Hover);
|
||||
color: var(--Base-Button-Tertiary-On-Fill-Hover);
|
||||
}
|
||||
|
||||
.baseTertiary:disabled {
|
||||
.btn.baseTertiary:disabled {
|
||||
background-color: var(--Base-Button-Tertiary-Fill-Disabled);
|
||||
color: var(--Base-Button-Tertiary-On-Fill-Disabled);
|
||||
}
|
||||
@@ -800,4 +815,4 @@ a.default {
|
||||
.icon.tertiaryLightSecondary:disabled svg,
|
||||
.icon.tertiaryLightSecondary:disabled svg * {
|
||||
fill: var(--Tertiary-Light-Button-Secondary-On-Fill-Disabled);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,14 +8,23 @@ import { buttonVariants } from "./variants"
|
||||
import type { ButtonProps } from "./button"
|
||||
|
||||
export default function Button(props: ButtonProps) {
|
||||
const { className, intent, size, theme, wrapping, variant, ...restProps } =
|
||||
props
|
||||
const {
|
||||
className,
|
||||
intent,
|
||||
size,
|
||||
theme,
|
||||
fullWidth,
|
||||
wrapping,
|
||||
variant,
|
||||
...restProps
|
||||
} = props
|
||||
|
||||
const classNames = buttonVariants({
|
||||
className,
|
||||
intent,
|
||||
size,
|
||||
theme,
|
||||
fullWidth,
|
||||
wrapping,
|
||||
variant,
|
||||
})
|
||||
|
||||
@@ -33,6 +33,9 @@ export const buttonVariants = cva(styles.btn, {
|
||||
wrapping: {
|
||||
true: styles.wrapping,
|
||||
},
|
||||
fullWidth: {
|
||||
true: styles.fullWidth,
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
intent: "primary",
|
||||
|
||||
@@ -23,6 +23,8 @@ export interface CardProps
|
||||
heading?: string | null
|
||||
bodyText?: string | null
|
||||
backgroundImage?: ImageVaultAsset
|
||||
imageHeight?: number
|
||||
imageWidth?: number
|
||||
onPrimaryButtonClick?: () => void
|
||||
onSecondaryButtonClick?: () => void
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import Link from "next/link"
|
||||
|
||||
import Image from "@/components/Image"
|
||||
import Button from "@/components/TempDesignSystem/Button"
|
||||
import Link from "@/components/TempDesignSystem/Link"
|
||||
import BiroScript from "@/components/TempDesignSystem/Text/BiroScript"
|
||||
import Body from "@/components/TempDesignSystem/Text/Body"
|
||||
import Title from "@/components/TempDesignSystem/Text/Title"
|
||||
@@ -21,10 +22,19 @@ export default function Card({
|
||||
className,
|
||||
theme,
|
||||
backgroundImage,
|
||||
imageHeight,
|
||||
imageWidth,
|
||||
onPrimaryButtonClick,
|
||||
onSecondaryButtonClick,
|
||||
}: CardProps) {
|
||||
const { buttonTheme, primaryLinkColor, secondaryLinkColor } = getTheme(theme)
|
||||
const buttonTheme = getTheme(theme)
|
||||
|
||||
imageHeight = imageHeight || 320
|
||||
imageWidth =
|
||||
imageWidth ||
|
||||
(backgroundImage
|
||||
? backgroundImage.dimensions.aspectRatio * imageHeight
|
||||
: 420)
|
||||
|
||||
return (
|
||||
<article
|
||||
@@ -39,8 +49,8 @@ export default function Card({
|
||||
src={backgroundImage.url}
|
||||
className={styles.image}
|
||||
alt={backgroundImage.meta.alt || backgroundImage.title}
|
||||
width={backgroundImage.dimensions.width || 420}
|
||||
height={backgroundImage.dimensions.height || 320}
|
||||
width={imageWidth}
|
||||
height={imageHeight}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
@@ -76,7 +86,6 @@ export default function Card({
|
||||
<Link
|
||||
href={primaryButton.href}
|
||||
target={primaryButton.openInNewTab ? "_blank" : undefined}
|
||||
color={primaryLinkColor}
|
||||
onClick={onPrimaryButtonClick}
|
||||
>
|
||||
{primaryButton.title}
|
||||
@@ -94,7 +103,6 @@ export default function Card({
|
||||
<Link
|
||||
href={secondaryButton.href}
|
||||
target={secondaryButton.openInNewTab ? "_blank" : undefined}
|
||||
color={secondaryLinkColor}
|
||||
onClick={onSecondaryButtonClick}
|
||||
>
|
||||
{secondaryButton.title}
|
||||
|
||||
@@ -1,36 +1,47 @@
|
||||
.divider {
|
||||
border-bottom-style: solid;
|
||||
border-bottom-width: 1px;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.horizontal {
|
||||
height: 1px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.dotted {
|
||||
border-bottom-style: dotted;
|
||||
.vertical {
|
||||
height: 100%;
|
||||
width: 1px;
|
||||
}
|
||||
|
||||
.burgundy {
|
||||
border-bottom-color: var(--Scandic-Brand-Burgundy);
|
||||
background-color: var(--Scandic-Brand-Burgundy);
|
||||
}
|
||||
|
||||
.pale {
|
||||
border-bottom-color: var(--Primary-Dark-On-Surface-Text);
|
||||
background-color: var(--Primary-Dark-On-Surface-Text);
|
||||
}
|
||||
|
||||
.peach {
|
||||
border-bottom-color: var(--Primary-Light-On-Surface-Divider);
|
||||
background-color: var(--Primary-Light-On-Surface-Divider);
|
||||
}
|
||||
|
||||
.beige {
|
||||
border-bottom-color: var(--Scandic-Beige-20);
|
||||
background-color: var(--Scandic-Beige-20);
|
||||
}
|
||||
|
||||
.white {
|
||||
border-bottom-color: var(--UI-Opacity-White-100);
|
||||
background-color: var(--UI-Opacity-White-100);
|
||||
}
|
||||
|
||||
.subtle {
|
||||
border-bottom-color: var(--Base-Border-Subtle);
|
||||
background-color: var(--Base-Border-Subtle);
|
||||
}
|
||||
|
||||
.primaryLightSubtle {
|
||||
background-color: var(--Primary-Light-On-Surface-Divider-subtle);
|
||||
}
|
||||
|
||||
.baseSurfaceSubtleNormal {
|
||||
background-color: var(--Base-Surface-Subtle-Normal);
|
||||
}
|
||||
|
||||
.opacity100 {
|
||||
|
||||
@@ -5,25 +5,27 @@ import styles from "./divider.module.css"
|
||||
export const dividerVariants = cva(styles.divider, {
|
||||
variants: {
|
||||
color: {
|
||||
burgundy: styles.burgundy,
|
||||
peach: styles.peach,
|
||||
baseSurfaceSubtleNormal: styles.baseSurfaceSubtleNormal,
|
||||
beige: styles.beige,
|
||||
white: styles.white,
|
||||
subtle: styles.subtle,
|
||||
burgundy: styles.burgundy,
|
||||
pale: styles.pale,
|
||||
peach: styles.peach,
|
||||
primaryLightSubtle: styles.primaryLightSubtle,
|
||||
subtle: styles.subtle,
|
||||
white: styles.white,
|
||||
},
|
||||
opacity: {
|
||||
100: styles.opacity100,
|
||||
8: styles.opacity8,
|
||||
},
|
||||
variant: {
|
||||
default: styles.default,
|
||||
dotted: styles.dotted,
|
||||
horizontal: styles.horizontal,
|
||||
vertical: styles.vertical,
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
color: "burgundy",
|
||||
opacity: 100,
|
||||
variant: "default",
|
||||
variant: "horizontal",
|
||||
},
|
||||
})
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client"
|
||||
import NextLink from "next/link"
|
||||
import { usePathname, useRouter } from "next/navigation"
|
||||
import { startTransition, useCallback } from "react"
|
||||
import { usePathname, useRouter, useSearchParams } from "next/navigation"
|
||||
import { startTransition, useCallback, useMemo } from "react"
|
||||
|
||||
import { trackClick } from "@/utils/tracking"
|
||||
|
||||
@@ -22,9 +22,14 @@ export default function Link({
|
||||
variant,
|
||||
trackingId,
|
||||
onClick,
|
||||
/**
|
||||
* Decides if the link should include the current search params in the URL
|
||||
*/
|
||||
keepSearchParams,
|
||||
...props
|
||||
}: LinkProps) {
|
||||
const currentPageSlug = usePathname()
|
||||
const searchParams = useSearchParams()
|
||||
let isActive = active || currentPageSlug === href
|
||||
|
||||
if (partialMatch && !isActive) {
|
||||
@@ -42,6 +47,12 @@ export default function Link({
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
const fullUrl = useMemo(() => {
|
||||
const search =
|
||||
keepSearchParams && searchParams.size ? `?${searchParams}` : ""
|
||||
return `${href}${search}`
|
||||
}, [href, searchParams, keepSearchParams])
|
||||
|
||||
const trackClickById = useCallback(() => {
|
||||
if (trackingId) {
|
||||
trackClick(trackingId)
|
||||
@@ -65,12 +76,17 @@ export default function Link({
|
||||
// track navigation nor start a router transition.
|
||||
return
|
||||
}
|
||||
if (href.startsWith("tel:") || href.startsWith("mailto:")) {
|
||||
// If href contains tel or mailto protocols we don't want to
|
||||
// track navigation nor start a router transition.
|
||||
return
|
||||
}
|
||||
e.preventDefault()
|
||||
startTransition(() => {
|
||||
router.push(href, { scroll })
|
||||
router.push(fullUrl, { scroll })
|
||||
})
|
||||
}}
|
||||
href={href}
|
||||
href={fullUrl}
|
||||
id={trackingId}
|
||||
{...props}
|
||||
/>
|
||||
|
||||
@@ -10,4 +10,5 @@ export interface LinkProps
|
||||
partialMatch?: boolean
|
||||
prefetch?: boolean
|
||||
trackingId?: string
|
||||
keepSearchParams?: boolean
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client"
|
||||
|
||||
import { useIsSSR } from "@react-aria/ssr"
|
||||
import { useContext } from "react"
|
||||
import { useContext, useState } from "react"
|
||||
import {
|
||||
Dialog,
|
||||
DialogTrigger,
|
||||
@@ -29,6 +29,15 @@ function SidePeek({
|
||||
}: React.PropsWithChildren<SidePeekProps>) {
|
||||
const isSSR = useIsSSR()
|
||||
const intl = useIntl()
|
||||
|
||||
const [rootDiv, setRootDiv] = useState<HTMLDivElement | undefined>(undefined)
|
||||
|
||||
function setRef(node: HTMLDivElement | null) {
|
||||
if (node) {
|
||||
setRootDiv(node)
|
||||
}
|
||||
}
|
||||
|
||||
const context = useContext(SidePeekContext)
|
||||
function onClose() {
|
||||
const closeHandler = handleClose || context?.handleClose
|
||||
@@ -44,42 +53,45 @@ function SidePeek({
|
||||
)
|
||||
}
|
||||
return (
|
||||
<DialogTrigger>
|
||||
<ModalOverlay
|
||||
className={styles.overlay}
|
||||
isOpen={isOpen || contentKey === context?.activeSidePeek}
|
||||
onOpenChange={onClose}
|
||||
isDismissable
|
||||
>
|
||||
<Modal className={styles.modal}>
|
||||
<Dialog className={styles.dialog}>
|
||||
<aside className={styles.sidePeek}>
|
||||
<header className={styles.header}>
|
||||
{title ? (
|
||||
<Title
|
||||
color="burgundy"
|
||||
textTransform="uppercase"
|
||||
level="h2"
|
||||
as="h3"
|
||||
<div ref={setRef}>
|
||||
<DialogTrigger>
|
||||
<ModalOverlay
|
||||
UNSTABLE_portalContainer={rootDiv}
|
||||
className={styles.overlay}
|
||||
isOpen={isOpen || contentKey === context?.activeSidePeek}
|
||||
onOpenChange={onClose}
|
||||
isDismissable
|
||||
>
|
||||
<Modal className={styles.modal}>
|
||||
<Dialog className={styles.dialog}>
|
||||
<aside className={styles.sidePeek}>
|
||||
<header className={styles.header}>
|
||||
{title ? (
|
||||
<Title
|
||||
color="burgundy"
|
||||
textTransform="uppercase"
|
||||
level="h2"
|
||||
as="h3"
|
||||
>
|
||||
{title}
|
||||
</Title>
|
||||
) : null}
|
||||
<Button
|
||||
aria-label={intl.formatMessage({ id: "Close" })}
|
||||
className={styles.closeButton}
|
||||
intent="text"
|
||||
onPress={onClose}
|
||||
>
|
||||
{title}
|
||||
</Title>
|
||||
) : null}
|
||||
<Button
|
||||
aria-label={intl.formatMessage({ id: "Close" })}
|
||||
className={styles.closeButton}
|
||||
intent="text"
|
||||
onPress={onClose}
|
||||
>
|
||||
<CloseIcon color="burgundy" height={32} width={32} />
|
||||
</Button>
|
||||
</header>
|
||||
<div className={styles.sidePeekContent}>{children}</div>
|
||||
</aside>
|
||||
</Dialog>
|
||||
</Modal>
|
||||
</ModalOverlay>
|
||||
</DialogTrigger>
|
||||
<CloseIcon color="burgundy" height={32} width={32} />
|
||||
</Button>
|
||||
</header>
|
||||
<div className={styles.sidePeekContent}>{children}</div>
|
||||
</aside>
|
||||
</Dialog>
|
||||
</Modal>
|
||||
</ModalOverlay>
|
||||
</DialogTrigger>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -78,6 +78,7 @@
|
||||
|
||||
.sidePeekContent {
|
||||
padding: var(--Spacing-x4);
|
||||
overflow-y: auto;
|
||||
}
|
||||
@media screen and (min-width: 1367px) {
|
||||
.modal {
|
||||
@@ -94,8 +95,4 @@
|
||||
.modal[data-exiting] {
|
||||
animation: slide-in 250ms reverse;
|
||||
}
|
||||
|
||||
.overlay {
|
||||
top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,6 +76,10 @@
|
||||
color: var(--UI-Text-Medium-contrast);
|
||||
}
|
||||
|
||||
.textHighContrast {
|
||||
color: var(--UI-Text-High-contrast);
|
||||
}
|
||||
|
||||
.white {
|
||||
color: var(--UI-Opacity-White-100);
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ const config = {
|
||||
pale: styles.pale,
|
||||
red: styles.red,
|
||||
textMediumContrast: styles.textMediumContrast,
|
||||
textHighContrast: styles.textHighContrast,
|
||||
white: styles.white,
|
||||
peach50: styles.peach50,
|
||||
peach80: styles.peach80,
|
||||
|
||||
@@ -63,6 +63,10 @@ p.caption {
|
||||
color: var(--UI-Text-Active);
|
||||
}
|
||||
|
||||
.uiTextMediumContrast {
|
||||
color: var(--UI-Text-Medium-contrast);
|
||||
}
|
||||
|
||||
.center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ const config = {
|
||||
red: styles.red,
|
||||
white: styles.white,
|
||||
uiTextActive: styles.uiTextActive,
|
||||
uiTextMediumContrast: styles.uiTextMediumContrast,
|
||||
},
|
||||
textTransform: {
|
||||
bold: styles.bold,
|
||||
|
||||
@@ -8,4 +8,24 @@ export const hotelReservation = {
|
||||
de: "/de/hotelreservierung",
|
||||
}
|
||||
|
||||
// TODO: Translate paths
|
||||
export const selectHotel = {
|
||||
en: `${hotelReservation.en}/select-hotel`,
|
||||
sv: `${hotelReservation.sv}/select-hotel`,
|
||||
no: `${hotelReservation.no}/select-hotel`,
|
||||
fi: `${hotelReservation.fi}/select-hotel`,
|
||||
da: `${hotelReservation.da}/select-hotel`,
|
||||
de: `${hotelReservation.de}/select-hotel`,
|
||||
}
|
||||
|
||||
// TODO: Translate paths
|
||||
export const selectHotelMap = {
|
||||
en: `${selectHotel.en}/map`,
|
||||
sv: `${selectHotel.sv}/map`,
|
||||
no: `${selectHotel.no}/map`,
|
||||
fi: `${selectHotel.fi}/map`,
|
||||
da: `${selectHotel.da}/map`,
|
||||
de: `${selectHotel.de}/map`,
|
||||
}
|
||||
|
||||
export const bookingFlow = [...Object.values(hotelReservation)]
|
||||
|
||||
@@ -14,7 +14,6 @@
|
||||
"Any changes you've made will be lost.": "Alle ændringer, du har foretaget, går tabt.",
|
||||
"Are you sure you want to remove the card ending with {lastFourDigits} from your member profile?": "Er du sikker på, at du vil fjerne kortet, der slutter me {lastFourDigits} fra din medlemsprofil?",
|
||||
"Arrival date": "Ankomstdato",
|
||||
"as of today": "fra idag",
|
||||
"As our": "Som vores {level}",
|
||||
"As our Close Friend": "Som vores nære ven",
|
||||
"At latest": "Senest",
|
||||
@@ -24,16 +23,14 @@
|
||||
"Bed type": "Seng type",
|
||||
"Book": "Book",
|
||||
"Book reward night": "Book bonusnat",
|
||||
"Booking codes and vouchers": "Bookingkoder og vouchers",
|
||||
"Code / Voucher": "Bookingkoder / voucher",
|
||||
"Booking number": "Bookingnummer",
|
||||
"Breakfast": "Morgenmad",
|
||||
"Breakfast excluded": "Morgenmad ikke inkluderet",
|
||||
"Breakfast included": "Morgenmad inkluderet",
|
||||
"Bus terminal": "Busstation",
|
||||
"Business": "Forretning",
|
||||
"by": "inden",
|
||||
"Cancel": "Afbestille",
|
||||
"characters": "tegn",
|
||||
"Check in": "Check ind",
|
||||
"Check out": "Check ud",
|
||||
"Check out the credit cards saved to your profile. Pay with a saved card when signed in for a smoother web experience.": "Tjek de kreditkort, der er gemt på din profil. Betal med et gemt kort, når du er logget ind for en mere jævn weboplevelse.",
|
||||
@@ -75,9 +72,9 @@
|
||||
"Explore all levels and benefits": "Udforsk alle niveauer og fordele",
|
||||
"Explore nearby": "Udforsk i nærheden",
|
||||
"Extras to your booking": "Tillæg til din booking",
|
||||
"FAQ": "Ofte stillede spørgsmål",
|
||||
"Failed to delete credit card, please try again later.": "Kunne ikke slette kreditkort. Prøv venligst igen senere.",
|
||||
"Fair": "Messe",
|
||||
"FAQ": "Ofte stillede spørgsmål",
|
||||
"Find booking": "Find booking",
|
||||
"Find hotels": "Find hotel",
|
||||
"Flexibility": "Fleksibilitet",
|
||||
@@ -94,15 +91,11 @@
|
||||
"Hotel": "Hotel",
|
||||
"Hotel facilities": "Hotel faciliteter",
|
||||
"Hotel surroundings": "Hotel omgivelser",
|
||||
"hotelPages.rooms.roomCard.person": "person",
|
||||
"hotelPages.rooms.roomCard.persons": "personer",
|
||||
"hotelPages.rooms.roomCard.seeRoomDetails": "Se værelsesdetaljer",
|
||||
"Hotels": "Hoteller",
|
||||
"How do you want to sleep?": "Hvordan vil du sove?",
|
||||
"How it works": "Hvordan det virker",
|
||||
"Image gallery": "Billedgalleri",
|
||||
"Join Scandic Friends": "Tilmeld dig Scandic Friends",
|
||||
"km to city center": "km til byens centrum",
|
||||
"Language": "Sprog",
|
||||
"Latest searches": "Seneste søgninger",
|
||||
"Level": "Niveau",
|
||||
@@ -129,9 +122,9 @@
|
||||
"Member price": "Medlemspris",
|
||||
"Member price from": "Medlemspris fra",
|
||||
"Members": "Medlemmer",
|
||||
"Membership cards": "Medlemskort",
|
||||
"Membership ID": "Medlems-id",
|
||||
"Membership ID copied to clipboard": "Medlems-ID kopieret til udklipsholder",
|
||||
"Membership cards": "Medlemskort",
|
||||
"Menu": "Menu",
|
||||
"Modify": "Ændre",
|
||||
"Month": "Måned",
|
||||
@@ -146,9 +139,6 @@
|
||||
"Nearby companies": "Nærliggende virksomheder",
|
||||
"New password": "Nyt kodeord",
|
||||
"Next": "Næste",
|
||||
"next level:": "Næste niveau:",
|
||||
"night": "nat",
|
||||
"nights": "nætter",
|
||||
"Nights needed to level up": "Nætter nødvendige for at komme i niveau",
|
||||
"No content published": "Intet indhold offentliggjort",
|
||||
"No matching location found": "Der blev ikke fundet nogen matchende placering",
|
||||
@@ -159,13 +149,11 @@
|
||||
"Non-refundable": "Ikke-refunderbart",
|
||||
"Not found": "Ikke fundet",
|
||||
"Nr night, nr adult": "{nights, number} nat, {adults, number} voksen",
|
||||
"number": "nummer",
|
||||
"On your journey": "På din rejse",
|
||||
"Open": "Åben",
|
||||
"Open language menu": "Åbn sprogmenuen",
|
||||
"Open menu": "Åbn menuen",
|
||||
"Open my pages menu": "Åbn mine sider menuen",
|
||||
"or": "eller",
|
||||
"Overview": "Oversigt",
|
||||
"Parking": "Parkering",
|
||||
"Parking / Garage": "Parkering / Garage",
|
||||
@@ -177,7 +165,6 @@
|
||||
"Phone is required": "Telefonnummer er påkrævet",
|
||||
"Phone number": "Telefonnummer",
|
||||
"Please enter a valid phone number": "Indtast venligst et gyldigt telefonnummer",
|
||||
"points": "Point",
|
||||
"Points": "Point",
|
||||
"Points being calculated": "Point udregnes",
|
||||
"Points earned prior to May 1, 2021": "Point optjent inden 1. maj 2021",
|
||||
@@ -196,10 +183,11 @@
|
||||
"Room & Terms": "Værelse & Vilkår",
|
||||
"Room facilities": "Værelsesfaciliteter",
|
||||
"Rooms": "Værelser",
|
||||
"Rooms & Guests": "Værelser & gæster",
|
||||
"Guests & Rooms": "Gæster & værelser",
|
||||
"Save": "Gemme",
|
||||
"Scandic Friends Mastercard": "Scandic Friends Mastercard",
|
||||
"Scandic Friends Point Shop": "Scandic Friends Point Shop",
|
||||
"Search": "Søge",
|
||||
"See all photos": "Se alle billeder",
|
||||
"See hotel details": "Se hoteloplysninger",
|
||||
"See room details": "Se værelsesdetaljer",
|
||||
@@ -220,29 +208,25 @@
|
||||
"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!": "Noget gik galt!",
|
||||
"special character": "speciel karakter",
|
||||
"spendable points expiring by": "{points} Brugbare point udløber den {date}",
|
||||
"Sports": "Sport",
|
||||
"Standard price": "Standardpris",
|
||||
"Street": "Gade",
|
||||
"Successfully updated profile!": "Profilen er opdateret med succes!",
|
||||
"Summary": "Opsummering",
|
||||
"TUI Points": "TUI Points",
|
||||
"Tell us what information and updates you'd like to receive, and how, by clicking the link below.": "Fortæl os, hvilke oplysninger og opdateringer du gerne vil modtage, og hvordan, ved at klikke på linket nedenfor.",
|
||||
"Thank you": "Tak",
|
||||
"Theatre": "Teater",
|
||||
"There are no transactions to display": "Der er ingen transaktioner at vise",
|
||||
"Things nearby HOTEL_NAME": "Ting i nærheden af {hotelName}",
|
||||
"to": "til",
|
||||
"Total Points": "Samlet antal point",
|
||||
"Tourist": "Turist",
|
||||
"Transaction date": "Overførselsdato",
|
||||
"Transactions": "Transaktioner",
|
||||
"Transportations": "Transport",
|
||||
"Tripadvisor reviews": "{rating} ({count} anmeldelser på Tripadvisor)",
|
||||
"TUI Points": "TUI Points",
|
||||
"Type of bed": "Sengtype",
|
||||
"Type of room": "Værelsestype",
|
||||
"uppercase letter": "stort bogstav",
|
||||
"Use bonus cheque": "Brug Bonus Cheque",
|
||||
"User information": "Brugeroplysninger",
|
||||
"View as list": "Vis som liste",
|
||||
@@ -268,9 +252,9 @@
|
||||
"You canceled adding a new credit card.": "Du har annulleret tilføjelsen af et nyt kreditkort.",
|
||||
"You have no previous stays.": "Du har ingen tidligere ophold.",
|
||||
"You have no upcoming stays.": "Du har ingen kommende ophold.",
|
||||
"Your Challenges Conquer & Earn!": "Dine udfordringer Overvind og tjen!",
|
||||
"Your card was successfully removed!": "Dit kort blev fjernet!",
|
||||
"Your card was successfully saved!": "Dit kort blev gemt!",
|
||||
"Your Challenges Conquer & Earn!": "Dine udfordringer Overvind og tjen!",
|
||||
"Your current level": "Dit nuværende niveau",
|
||||
"Your details": "Dine oplysninger",
|
||||
"Your level": "Dit niveau",
|
||||
@@ -278,5 +262,23 @@
|
||||
"Zip code": "Postnummer",
|
||||
"Zoo": "Zoo",
|
||||
"Zoom in": "Zoom ind",
|
||||
"Zoom out": "Zoom ud"
|
||||
"Zoom out": "Zoom ud",
|
||||
"as of today": "pr. dags dato",
|
||||
"booking.nights": "{totalNights, plural, one {# nat} other {# nætter}}",
|
||||
"by": "inden",
|
||||
"characters": "tegn",
|
||||
"hotelPages.rooms.roomCard.person": "person",
|
||||
"hotelPages.rooms.roomCard.persons": "personer",
|
||||
"hotelPages.rooms.roomCard.seeRoomDetails": "Se værelsesdetaljer",
|
||||
"km to city center": "km til byens centrum",
|
||||
"next level:": "Næste niveau:",
|
||||
"night": "nat",
|
||||
"nights": "nætter",
|
||||
"number": "nummer",
|
||||
"or": "eller",
|
||||
"points": "Point",
|
||||
"special character": "speciel karakter",
|
||||
"spendable points expiring by": "{points} Brugbare point udløber den {date}",
|
||||
"to": "til",
|
||||
"uppercase letter": "stort bogstav"
|
||||
}
|
||||
|
||||
@@ -14,7 +14,6 @@
|
||||
"Any changes you've made will be lost.": "Alle Änderungen, die Sie vorgenommen haben, gehen verloren.",
|
||||
"Are you sure you want to remove the card ending with {lastFourDigits} from your member profile?": "Möchten Sie die Karte mit der Endung {lastFourDigits} wirklich aus Ihrem Mitgliedsprofil entfernen?",
|
||||
"Arrival date": "Ankunftsdatum",
|
||||
"as of today": "Stand heute",
|
||||
"As our": "Als unser {level}",
|
||||
"As our Close Friend": "Als unser enger Freund",
|
||||
"At latest": "Spätestens",
|
||||
@@ -24,16 +23,14 @@
|
||||
"Bed type": "Bettentyp",
|
||||
"Book": "Buchen",
|
||||
"Book reward night": "Bonusnacht buchen",
|
||||
"Booking codes and vouchers": "Buchungscodes und Gutscheine",
|
||||
"Code / Voucher": "Buchungscodes / Gutscheine",
|
||||
"Booking number": "Buchungsnummer",
|
||||
"Breakfast": "Frühstück",
|
||||
"Breakfast excluded": "Frühstück nicht inbegriffen",
|
||||
"Breakfast included": "Frühstück inbegriffen",
|
||||
"Bus terminal": "Busbahnhof",
|
||||
"Business": "Geschäft",
|
||||
"by": "bis",
|
||||
"Cancel": "Stornieren",
|
||||
"characters": "figuren",
|
||||
"Check in": "Einchecken",
|
||||
"Check out": "Auschecken",
|
||||
"Check out the credit cards saved to your profile. Pay with a saved card when signed in for a smoother web experience.": "Sehen Sie sich die in Ihrem Profil gespeicherten Kreditkarten an. Bezahlen Sie mit einer gespeicherten Karte, wenn Sie angemeldet sind, für ein reibungsloseres Web-Erlebnis.",
|
||||
@@ -75,9 +72,9 @@
|
||||
"Explore all levels and benefits": "Entdecken Sie alle Levels und Vorteile",
|
||||
"Explore nearby": "Erkunden Sie die Umgebung",
|
||||
"Extras to your booking": "Extras zu Ihrer Buchung",
|
||||
"FAQ": "Häufig gestellte Fragen",
|
||||
"Failed to delete credit card, please try again later.": "Kreditkarte konnte nicht gelöscht werden. Bitte versuchen Sie es später noch einmal.",
|
||||
"Fair": "Messe",
|
||||
"FAQ": "Häufig gestellte Fragen",
|
||||
"Find booking": "Buchung finden",
|
||||
"Find hotels": "Hotels finden",
|
||||
"Flexibility": "Flexibilität",
|
||||
@@ -94,15 +91,11 @@
|
||||
"Hotel": "Hotel",
|
||||
"Hotel facilities": "Hotel-Infos",
|
||||
"Hotel surroundings": "Umgebung des Hotels",
|
||||
"hotelPages.rooms.roomCard.person": "person",
|
||||
"hotelPages.rooms.roomCard.persons": "personen",
|
||||
"hotelPages.rooms.roomCard.seeRoomDetails": "Zimmerdetails ansehen",
|
||||
"Hotels": "Hotels",
|
||||
"How do you want to sleep?": "Wie möchtest du schlafen?",
|
||||
"How it works": "Wie es funktioniert",
|
||||
"Image gallery": "Bildergalerie",
|
||||
"Join Scandic Friends": "Treten Sie Scandic Friends bei",
|
||||
"km to city center": "km bis zum Stadtzentrum",
|
||||
"Language": "Sprache",
|
||||
"Latest searches": "Letzte Suchanfragen",
|
||||
"Level": "Level",
|
||||
@@ -129,9 +122,9 @@
|
||||
"Member price": "Mitgliederpreis",
|
||||
"Member price from": "Mitgliederpreis ab",
|
||||
"Members": "Mitglieder",
|
||||
"Membership cards": "Mitgliedskarten",
|
||||
"Membership ID": "Mitglieds-ID",
|
||||
"Membership ID copied to clipboard": "Mitglieds-ID in die Zwischenablage kopiert",
|
||||
"Membership cards": "Mitgliedskarten",
|
||||
"Menu": "Menu",
|
||||
"Modify": "Ändern",
|
||||
"Month": "Monat",
|
||||
@@ -146,9 +139,6 @@
|
||||
"Nearby companies": "Nahe gelegene Unternehmen",
|
||||
"New password": "Neues Kennwort",
|
||||
"Next": "Nächste",
|
||||
"next level:": "Nächstes Level:",
|
||||
"night": "nacht",
|
||||
"nights": "Nächte",
|
||||
"Nights needed to level up": "Nächte, die zum Levelaufstieg benötigt werden",
|
||||
"No content published": "Kein Inhalt veröffentlicht",
|
||||
"No matching location found": "Kein passender Standort gefunden",
|
||||
@@ -159,13 +149,11 @@
|
||||
"Non-refundable": "Nicht erstattungsfähig",
|
||||
"Not found": "Nicht gefunden",
|
||||
"Nr night, nr adult": "{nights, number} Nacht, {adults, number} Erwachsener",
|
||||
"number": "nummer",
|
||||
"On your journey": "Auf deiner Reise",
|
||||
"Open": "Offen",
|
||||
"Open language menu": "Sprachmenü öffnen",
|
||||
"Open menu": "Menü öffnen",
|
||||
"Open my pages menu": "Meine Seiten Menü öffnen",
|
||||
"or": "oder",
|
||||
"Overview": "Übersicht",
|
||||
"Parking": "Parken",
|
||||
"Parking / Garage": "Parken / Garage",
|
||||
@@ -177,7 +165,6 @@
|
||||
"Phone number": "Telefonnummer",
|
||||
"Please enter a valid phone number": "Bitte geben Sie eine gültige Telefonnummer ein",
|
||||
"Points": "Punkte",
|
||||
"points": "Punkte",
|
||||
"Points being calculated": "Punkte werden berechnet",
|
||||
"Points earned prior to May 1, 2021": "Zusammengeführte Punkte vor dem 1. Mai 2021",
|
||||
"Points may take up to 10 days to be displayed.": "Es kann bis zu 10 Tage dauern, bis Punkte angezeigt werden.",
|
||||
@@ -195,10 +182,11 @@
|
||||
"Room & Terms": "Zimmer & Bedingungen",
|
||||
"Room facilities": "Zimmerausstattung",
|
||||
"Rooms": "Räume",
|
||||
"Rooms & Guests": "Zimmer & Gäste",
|
||||
"Guests & Rooms": "Gäste & Zimmer",
|
||||
"Save": "Speichern",
|
||||
"Scandic Friends Mastercard": "Scandic Friends Mastercard",
|
||||
"Scandic Friends Point Shop": "Scandic Friends Point Shop",
|
||||
"Search": "Suchen",
|
||||
"See all photos": "Alle Fotos ansehen",
|
||||
"See hotel details": "Hotelinformationen ansehen",
|
||||
"See room details": "Zimmerdetails ansehen",
|
||||
@@ -219,29 +207,25 @@
|
||||
"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!": "Etwas ist schief gelaufen!",
|
||||
"special character": "sonderzeichen",
|
||||
"spendable points expiring by": "{points} Einlösbare punkte verfallen bis zum {date}",
|
||||
"Sports": "Sport",
|
||||
"Standard price": "Standardpreis",
|
||||
"Street": "Straße",
|
||||
"Successfully updated profile!": "Profil erfolgreich aktualisiert!",
|
||||
"Summary": "Zusammenfassung",
|
||||
"TUI Points": "TUI Points",
|
||||
"Tell us what information and updates you'd like to receive, and how, by clicking the link below.": "Teilen Sie uns mit, welche Informationen und Updates Sie wie erhalten möchten, indem Sie auf den unten stehenden Link klicken.",
|
||||
"Thank you": "Danke",
|
||||
"Theatre": "Theater",
|
||||
"There are no transactions to display": "Es sind keine Transaktionen zum Anzeigen vorhanden",
|
||||
"Things nearby HOTEL_NAME": "Dinge in der Nähe von {hotelName}",
|
||||
"to": "zu",
|
||||
"Total Points": "Gesamtpunktzahl",
|
||||
"Tourist": "Tourist",
|
||||
"Transaction date": "Transaktionsdatum",
|
||||
"Transactions": "Transaktionen",
|
||||
"Transportations": "Transportmittel",
|
||||
"Tripadvisor reviews": "{rating} ({count} Bewertungen auf Tripadvisor)",
|
||||
"TUI Points": "TUI Points",
|
||||
"Type of bed": "Bettentyp",
|
||||
"Type of room": "Zimmerart",
|
||||
"uppercase letter": "großbuchstabe",
|
||||
"Use bonus cheque": "Bonusscheck nutzen",
|
||||
"User information": "Nutzerinformation",
|
||||
"View as list": "Als Liste anzeigen",
|
||||
@@ -267,9 +251,9 @@
|
||||
"You canceled adding a new credit card.": "Sie haben das Hinzufügen einer neuen Kreditkarte abgebrochen.",
|
||||
"You have no previous stays.": "Sie haben keine vorherigen Aufenthalte.",
|
||||
"You have no upcoming stays.": "Sie haben keine bevorstehenden Aufenthalte.",
|
||||
"Your Challenges Conquer & Earn!": "Meistern Sie Ihre Herausforderungen und verdienen Sie Geld!",
|
||||
"Your card was successfully removed!": "Ihre Karte wurde erfolgreich entfernt!",
|
||||
"Your card was successfully saved!": "Ihre Karte wurde erfolgreich gespeichert!",
|
||||
"Your Challenges Conquer & Earn!": "Meistern Sie Ihre Herausforderungen und verdienen Sie Geld!",
|
||||
"Your current level": "Ihr aktuelles Level",
|
||||
"Your details": "Ihre Angaben",
|
||||
"Your level": "Dein level",
|
||||
@@ -277,5 +261,23 @@
|
||||
"Zip code": "PLZ",
|
||||
"Zoo": "Zoo",
|
||||
"Zoom in": "Vergrößern",
|
||||
"Zoom out": "Verkleinern"
|
||||
"Zoom out": "Verkleinern",
|
||||
"as of today": "Stand heute",
|
||||
"booking.nights": "{totalNights, plural, one {# nacht} other {# Nächte}}",
|
||||
"by": "bis",
|
||||
"characters": "figuren",
|
||||
"hotelPages.rooms.roomCard.person": "person",
|
||||
"hotelPages.rooms.roomCard.persons": "personen",
|
||||
"hotelPages.rooms.roomCard.seeRoomDetails": "Zimmerdetails ansehen",
|
||||
"km to city center": "km bis zum Stadtzentrum",
|
||||
"next level:": "Nächstes Level:",
|
||||
"night": "nacht",
|
||||
"nights": "Nächte",
|
||||
"number": "nummer",
|
||||
"or": "oder",
|
||||
"points": "Punkte",
|
||||
"special character": "sonderzeichen",
|
||||
"spendable points expiring by": "{points} Einlösbare punkte verfallen bis zum {date}",
|
||||
"to": "zu",
|
||||
"uppercase letter": "großbuchstabe"
|
||||
}
|
||||
|
||||
@@ -14,7 +14,6 @@
|
||||
"Any changes you've made will be lost.": "Any changes you've made will be lost.",
|
||||
"Are you sure you want to remove the card ending with {lastFourDigits} from your member profile?": "Are you sure you want to remove the card ending with {lastFourDigits} from your member profile?",
|
||||
"Arrival date": "Arrival date",
|
||||
"as of today": "as of today",
|
||||
"As our": "As our {level}",
|
||||
"As our Close Friend": "As our Close Friend",
|
||||
"At latest": "At latest",
|
||||
@@ -24,16 +23,14 @@
|
||||
"Bed type": "Bed type",
|
||||
"Book": "Book",
|
||||
"Book reward night": "Book reward night",
|
||||
"Booking codes and vouchers": "Booking codes and vouchers",
|
||||
"Code / Voucher": "Code / Voucher",
|
||||
"Booking number": "Booking number",
|
||||
"Breakfast": "Breakfast",
|
||||
"Breakfast excluded": "Breakfast excluded",
|
||||
"Breakfast included": "Breakfast included",
|
||||
"Bus terminal": "Bus terminal",
|
||||
"Business": "Business",
|
||||
"by": "by",
|
||||
"Cancel": "Cancel",
|
||||
"characters": "characters",
|
||||
"Check in": "Check in",
|
||||
"Check out": "Check out",
|
||||
"Check out the credit cards saved to your profile. Pay with a saved card when signed in for a smoother web experience.": "Check out the credit cards saved to your profile. Pay with a saved card when signed in for a smoother web experience.",
|
||||
@@ -63,6 +60,7 @@
|
||||
"Day": "Day",
|
||||
"Description": "Description",
|
||||
"Destinations & hotels": "Destinations & hotels",
|
||||
"Destination": "Destination",
|
||||
"Discard changes": "Discard changes",
|
||||
"Discard unsaved changes?": "Discard unsaved changes?",
|
||||
"Distance to city centre": "{number}km to city centre",
|
||||
@@ -75,9 +73,9 @@
|
||||
"Explore all levels and benefits": "Explore all levels and benefits",
|
||||
"Explore nearby": "Explore nearby",
|
||||
"Extras to your booking": "Extras to your booking",
|
||||
"FAQ": "FAQ",
|
||||
"Failed to delete credit card, please try again later.": "Failed to delete credit card, please try again later.",
|
||||
"Fair": "Fair",
|
||||
"FAQ": "FAQ",
|
||||
"Find booking": "Find booking",
|
||||
"Find hotels": "Find hotels",
|
||||
"Flexibility": "Flexibility",
|
||||
@@ -94,15 +92,11 @@
|
||||
"Hotel": "Hotel",
|
||||
"Hotel facilities": "Hotel facilities",
|
||||
"Hotel surroundings": "Hotel surroundings",
|
||||
"hotelPages.rooms.roomCard.person": "person",
|
||||
"hotelPages.rooms.roomCard.persons": "persons",
|
||||
"hotelPages.rooms.roomCard.seeRoomDetails": "See room details",
|
||||
"Hotels": "Hotels",
|
||||
"How do you want to sleep?": "How do you want to sleep?",
|
||||
"How it works": "How it works",
|
||||
"Image gallery": "Image gallery",
|
||||
"Join Scandic Friends": "Join Scandic Friends",
|
||||
"km to city center": "km to city center",
|
||||
"Language": "Language",
|
||||
"Latest searches": "Latest searches",
|
||||
"Level": "Level",
|
||||
@@ -129,9 +123,9 @@
|
||||
"Member price": "Member price",
|
||||
"Member price from": "Member price from",
|
||||
"Members": "Members",
|
||||
"Membership cards": "Membership cards",
|
||||
"Membership ID": "Membership ID",
|
||||
"Membership ID copied to clipboard": "Membership ID copied to clipboard",
|
||||
"Membership cards": "Membership cards",
|
||||
"Menu": "Menu",
|
||||
"Modify": "Modify",
|
||||
"Month": "Month",
|
||||
@@ -146,9 +140,6 @@
|
||||
"Nearby companies": "Nearby companies",
|
||||
"New password": "New password",
|
||||
"Next": "Next",
|
||||
"next level:": "next level:",
|
||||
"night": "night",
|
||||
"nights": "nights",
|
||||
"Nights needed to level up": "Nights needed to level up",
|
||||
"No content published": "No content published",
|
||||
"No matching location found": "No matching location found",
|
||||
@@ -159,13 +150,11 @@
|
||||
"Non-refundable": "Non-refundable",
|
||||
"Not found": "Not found",
|
||||
"Nr night, nr adult": "{nights, number} night, {adults, number} adult",
|
||||
"number": "number",
|
||||
"On your journey": "On your journey",
|
||||
"Open": "Open",
|
||||
"Open language menu": "Open language menu",
|
||||
"Open menu": "Open menu",
|
||||
"Open my pages menu": "Open my pages menu",
|
||||
"or": "or",
|
||||
"Overview": "Overview",
|
||||
"Parking": "Parking",
|
||||
"Parking / Garage": "Parking / Garage",
|
||||
@@ -178,7 +167,6 @@
|
||||
"Phone number": "Phone number",
|
||||
"Please enter a valid phone number": "Please enter a valid phone number",
|
||||
"Points": "Points",
|
||||
"points": "Points",
|
||||
"Points being calculated": "Points being calculated",
|
||||
"Points earned prior to May 1, 2021": "Points earned prior to May 1, 2021",
|
||||
"Points may take up to 10 days to be displayed.": "Points may take up to 10 days to be displayed.",
|
||||
@@ -196,17 +184,19 @@
|
||||
"Room & Terms": "Room & Terms",
|
||||
"Room facilities": "Room facilities",
|
||||
"Rooms": "Rooms",
|
||||
"Rooms & Guests": "Rooms & Guests",
|
||||
"Guests & Rooms": "Guests & Rooms",
|
||||
"Save": "Save",
|
||||
"Scandic Friends Mastercard": "Scandic Friends Mastercard",
|
||||
"Scandic Friends Point Shop": "Scandic Friends Point Shop",
|
||||
"See all photos": "See all photos",
|
||||
"Search": "Search",
|
||||
"See hotel details": "See hotel details",
|
||||
"See room details": "See room details",
|
||||
"See rooms": "See rooms",
|
||||
"Select a country": "Select a country",
|
||||
"Select country of residence": "Select country of residence",
|
||||
"Select date of birth": "Select date of birth",
|
||||
"Select dates": "Select dates",
|
||||
"Select language": "Select language",
|
||||
"Select your language": "Select your language",
|
||||
"Shopping": "Shopping",
|
||||
@@ -220,29 +210,25 @@
|
||||
"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!": "Something went wrong!",
|
||||
"special character": "special character",
|
||||
"spendable points expiring by": "{points} spendable points expiring by {date}",
|
||||
"Sports": "Sports",
|
||||
"Standard price": "Standard price",
|
||||
"Street": "Street",
|
||||
"Successfully updated profile!": "Successfully updated profile!",
|
||||
"Summary": "Summary",
|
||||
"TUI Points": "TUI Points",
|
||||
"Tell us what information and updates you'd like to receive, and how, by clicking the link below.": "Tell us what information and updates you'd like to receive, and how, by clicking the link below.",
|
||||
"Thank you": "Thank you",
|
||||
"Theatre": "Theatre",
|
||||
"There are no transactions to display": "There are no transactions to display",
|
||||
"Things nearby HOTEL_NAME": "Things nearby {hotelName}",
|
||||
"to": "to",
|
||||
"Total Points": "Total Points",
|
||||
"Tourist": "Tourist",
|
||||
"Transaction date": "Transaction date",
|
||||
"Transactions": "Transactions",
|
||||
"Transportations": "Transportations",
|
||||
"Tripadvisor reviews": "{rating} ({count} reviews on Tripadvisor)",
|
||||
"TUI Points": "TUI Points",
|
||||
"Type of bed": "Type of bed",
|
||||
"Type of room": "Type of room",
|
||||
"uppercase letter": "uppercase letter",
|
||||
"Use bonus cheque": "Use bonus cheque",
|
||||
"User information": "User information",
|
||||
"View as list": "View as list",
|
||||
@@ -268,9 +254,9 @@
|
||||
"You canceled adding a new credit card.": "You canceled adding a new credit card.",
|
||||
"You have no previous stays.": "You have no previous stays.",
|
||||
"You have no upcoming stays.": "You have no upcoming stays.",
|
||||
"Your Challenges Conquer & Earn!": "Your Challenges Conquer & Earn!",
|
||||
"Your card was successfully removed!": "Your card was successfully removed!",
|
||||
"Your card was successfully saved!": "Your card was successfully saved!",
|
||||
"Your Challenges Conquer & Earn!": "Your Challenges Conquer & Earn!",
|
||||
"Your current level": "Your current level",
|
||||
"Your details": "Your details",
|
||||
"Your level": "Your level",
|
||||
@@ -278,5 +264,25 @@
|
||||
"Zip code": "Zip code",
|
||||
"Zoo": "Zoo",
|
||||
"Zoom in": "Zoom in",
|
||||
"Zoom out": "Zoom out"
|
||||
"Zoom out": "Zoom out",
|
||||
"as of today": "as of today",
|
||||
"booking.adults": "{totalAdults, plural, one {# adult} other {# adults}}",
|
||||
"booking.nights": "{totalNights, plural, one {# night} other {# nights}}",
|
||||
"booking.rooms": "{totalRooms, plural, one {# room} other {# rooms}}",
|
||||
"by": "by",
|
||||
"characters": "characters",
|
||||
"hotelPages.rooms.roomCard.person": "person",
|
||||
"hotelPages.rooms.roomCard.persons": "persons",
|
||||
"hotelPages.rooms.roomCard.seeRoomDetails": "See room details",
|
||||
"km to city center": "km to city center",
|
||||
"next level:": "next level:",
|
||||
"night": "night",
|
||||
"nights": "nights",
|
||||
"number": "number",
|
||||
"or": "or",
|
||||
"points": "Points",
|
||||
"special character": "special character",
|
||||
"spendable points expiring by": "{points} spendable points expiring by {date}",
|
||||
"to": "to",
|
||||
"uppercase letter": "uppercase letter"
|
||||
}
|
||||
|
||||
@@ -14,7 +14,6 @@
|
||||
"Any changes you've made will be lost.": "Kaikki tekemäsi muutokset menetetään.",
|
||||
"Are you sure you want to remove the card ending with {lastFourDigits} from your member profile?": "Haluatko varmasti poistaa kortin, joka päättyy numeroon {lastFourDigits} jäsenprofiilistasi?",
|
||||
"Arrival date": "Saapumispäivä",
|
||||
"as of today": "tänään",
|
||||
"As our": "{level}-etu",
|
||||
"As our Close Friend": "Läheisenä ystävänämme",
|
||||
"At latest": "Viimeistään",
|
||||
@@ -24,16 +23,13 @@
|
||||
"Bed type": "Vuodetyyppi",
|
||||
"Book": "Varaa",
|
||||
"Book reward night": "Kirjapalkinto-ilta",
|
||||
"Booking codes and vouchers": "Varauskoodit ja kupongit",
|
||||
"Booking number": "Varausnumero",
|
||||
"Breakfast": "Aamiainen",
|
||||
"Breakfast excluded": "Aamiainen ei sisälly",
|
||||
"Breakfast included": "Aamiainen sisältyy",
|
||||
"Bus terminal": "Bussiasema",
|
||||
"Business": "Business",
|
||||
"by": "mennessä",
|
||||
"Cancel": "Peruuttaa",
|
||||
"characters": "hahmoja",
|
||||
"Check in": "Sisäänkirjautuminen",
|
||||
"Check out": "Uloskirjautuminen",
|
||||
"Check out the credit cards saved to your profile. Pay with a saved card when signed in for a smoother web experience.": "Tarkista profiiliisi tallennetut luottokortit. Maksa tallennetulla kortilla kirjautuneena, jotta verkkokokemus on sujuvampi.",
|
||||
@@ -48,6 +44,7 @@
|
||||
"Close menu": "Sulje valikko",
|
||||
"Close my pages menu": "Sulje omat sivut -valikko",
|
||||
"Close the map": "Sulje kartta",
|
||||
"Code / Voucher": "Varauskoodit / kupongit",
|
||||
"Coming up": "Tulossa",
|
||||
"Compare all levels": "Vertaa kaikkia tasoja",
|
||||
"Contact us": "Ota meihin yhteyttä",
|
||||
@@ -75,9 +72,9 @@
|
||||
"Explore all levels and benefits": "Tutustu kaikkiin tasoihin ja etuihin",
|
||||
"Explore nearby": "Tutustu lähialueeseen",
|
||||
"Extras to your booking": "Varauksessa lisäpalveluita",
|
||||
"FAQ": "UKK",
|
||||
"Failed to delete credit card, please try again later.": "Luottokortin poistaminen epäonnistui, yritä myöhemmin uudelleen.",
|
||||
"Fair": "Messukeskus",
|
||||
"FAQ": "UKK",
|
||||
"Find booking": "Etsi varaus",
|
||||
"Find hotels": "Etsi hotelleja",
|
||||
"Flexibility": "Joustavuus",
|
||||
@@ -88,21 +85,18 @@
|
||||
"Get inspired": "Inspiroidu",
|
||||
"Go back to edit": "Palaa muokkaamaan",
|
||||
"Go back to overview": "Palaa yleiskatsaukseen",
|
||||
"Guests & Rooms": "Vieraat & Huoneet",
|
||||
"Hi": "Hi",
|
||||
"Highest level": "Korkein taso",
|
||||
"Hospital": "Sairaala",
|
||||
"Hotel": "Hotelli",
|
||||
"Hotel facilities": "Hotellin palvelut",
|
||||
"Hotel surroundings": "Hotellin ympäristö",
|
||||
"hotelPages.rooms.roomCard.person": "henkilö",
|
||||
"hotelPages.rooms.roomCard.persons": "Henkilöä",
|
||||
"hotelPages.rooms.roomCard.seeRoomDetails": "Katso huoneen tiedot",
|
||||
"Hotels": "Hotellit",
|
||||
"How do you want to sleep?": "Kuinka haluat nukkua?",
|
||||
"How it works": "Kuinka se toimii",
|
||||
"Image gallery": "Kuvagalleria",
|
||||
"Join Scandic Friends": "Liity jäseneksi",
|
||||
"km to city center": "km keskustaan",
|
||||
"Language": "Kieli",
|
||||
"Latest searches": "Viimeisimmät haut",
|
||||
"Level": "Level",
|
||||
@@ -129,9 +123,9 @@
|
||||
"Member price": "Jäsenhinta",
|
||||
"Member price from": "Jäsenhinta alkaen",
|
||||
"Members": "Jäsenet",
|
||||
"Membership cards": "Jäsenkortit",
|
||||
"Membership ID": "Jäsentunnus",
|
||||
"Membership ID copied to clipboard": "Jäsenyystunnus kopioitu leikepöydälle",
|
||||
"Membership cards": "Jäsenkortit",
|
||||
"Menu": "Valikko",
|
||||
"Modify": "Muokkaa",
|
||||
"Month": "Kuukausi",
|
||||
@@ -146,9 +140,6 @@
|
||||
"Nearby companies": "Läheiset yritykset",
|
||||
"New password": "Uusi salasana",
|
||||
"Next": "Seuraava",
|
||||
"next level:": "pistettä tasolle:",
|
||||
"night": "yö",
|
||||
"nights": "yötä",
|
||||
"Nights needed to level up": "Yöt, joita tarvitaan tasolle",
|
||||
"No content published": "Ei julkaistua sisältöä",
|
||||
"No matching location found": "Vastaavaa sijaintia ei löytynyt",
|
||||
@@ -159,13 +150,11 @@
|
||||
"Non-refundable": "Ei palautettavissa",
|
||||
"Not found": "Ei löydetty",
|
||||
"Nr night, nr adult": "{nights, number} yö, {adults, number} aikuinen",
|
||||
"number": "määrä",
|
||||
"On your journey": "Matkallasi",
|
||||
"Open": "Avata",
|
||||
"Open language menu": "Avaa kielivalikko",
|
||||
"Open menu": "Avaa valikko",
|
||||
"Open my pages menu": "Avaa omat sivut -valikko",
|
||||
"or": "tai",
|
||||
"Overview": "Yleiskatsaus",
|
||||
"Parking": "Pysäköinti",
|
||||
"Parking / Garage": "Pysäköinti / Autotalli",
|
||||
@@ -177,7 +166,6 @@
|
||||
"Phone is required": "Puhelin vaaditaan",
|
||||
"Phone number": "Puhelinnumero",
|
||||
"Please enter a valid phone number": "Ole hyvä ja näppäile voimassaoleva puhelinnumero",
|
||||
"points": "pistettä",
|
||||
"Points": "Pisteet",
|
||||
"Points being calculated": "Pisteitä lasketaan",
|
||||
"Points earned prior to May 1, 2021": "Pisteet, jotka ansaittu ennen 1.5.2021",
|
||||
@@ -196,11 +184,10 @@
|
||||
"Room & Terms": "Huone & Ehdot",
|
||||
"Room facilities": "Huoneen varustelu",
|
||||
"Rooms": "Huoneet",
|
||||
"Rooms & Guests": "Huoneet & Vieraat",
|
||||
"Rooms & Guestss": "Huoneet & Vieraat",
|
||||
"Save": "Tallentaa",
|
||||
"Save": "Tallenna",
|
||||
"Scandic Friends Mastercard": "Scandic Friends Mastercard",
|
||||
"Scandic Friends Point Shop": "Scandic Friends Point Shop",
|
||||
"Search": "Haku",
|
||||
"See all photos": "Katso kaikki kuvat",
|
||||
"See hotel details": "Katso hotellin tiedot",
|
||||
"See room details": "Katso huoneen tiedot",
|
||||
@@ -221,29 +208,25 @@
|
||||
"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!": "Jotain meni pieleen!",
|
||||
"special character": "erikoishahmo",
|
||||
"spendable points expiring by": "{points} pistettä vanhenee {date} mennessä",
|
||||
"Sports": "Urheilu",
|
||||
"Standard price": "Normaali hinta",
|
||||
"Street": "Katu",
|
||||
"Successfully updated profile!": "Profiilin päivitys onnistui!",
|
||||
"Summary": "Yhteenveto",
|
||||
"TUI Points": "TUI Points",
|
||||
"Tell us what information and updates you'd like to receive, and how, by clicking the link below.": "Kerro meille, mitä tietoja ja päivityksiä haluat saada ja miten, napsauttamalla alla olevaa linkkiä.",
|
||||
"Thank you": "Kiitos",
|
||||
"Theatre": "Teatteri",
|
||||
"There are no transactions to display": "Näytettäviä tapahtumia ei ole",
|
||||
"Things nearby HOTEL_NAME": "Lähellä olevia asioita {hotelName}",
|
||||
"to": "to",
|
||||
"Total Points": "Kokonaispisteet",
|
||||
"Tourist": "Turisti",
|
||||
"Transaction date": "Tapahtuman päivämäärä",
|
||||
"Transactions": "Tapahtumat",
|
||||
"Transportations": "Kuljetukset",
|
||||
"Tripadvisor reviews": "{rating} ({count} arvostelua TripAdvisorissa)",
|
||||
"TUI Points": "TUI Points",
|
||||
"Type of bed": "Vuodetyyppi",
|
||||
"Type of room": "Huonetyyppi",
|
||||
"uppercase letter": "iso kirjain",
|
||||
"Use bonus cheque": "Käytä bonussekkiä",
|
||||
"User information": "Käyttäjän tiedot",
|
||||
"View as list": "Näytä listana",
|
||||
@@ -269,9 +252,9 @@
|
||||
"You canceled adding a new credit card.": "Peruutit uuden luottokortin lisäämisen.",
|
||||
"You have no previous stays.": "Sinulla ei ole aiempia majoituksia.",
|
||||
"You have no upcoming stays.": "Sinulla ei ole tulevia majoituksia.",
|
||||
"Your Challenges Conquer & Earn!": "Voita ja ansaitse haasteesi!",
|
||||
"Your card was successfully removed!": "Korttisi poistettiin onnistuneesti!",
|
||||
"Your card was successfully saved!": "Korttisi tallennettu onnistuneesti!",
|
||||
"Your Challenges Conquer & Earn!": "Voita ja ansaitse haasteesi!",
|
||||
"Your current level": "Nykyinen tasosi",
|
||||
"Your details": "Tietosi",
|
||||
"Your level": "Tasosi",
|
||||
@@ -279,5 +262,23 @@
|
||||
"Zip code": "Postinumero",
|
||||
"Zoo": "Eläintarha",
|
||||
"Zoom in": "Lähennä",
|
||||
"Zoom out": "Loitonna"
|
||||
"Zoom out": "Loitonna",
|
||||
"as of today": "tänään",
|
||||
"booking.nights": "{totalNights, plural, one {# yö} other {# yötä}}",
|
||||
"by": "mennessä",
|
||||
"characters": "hahmoja",
|
||||
"hotelPages.rooms.roomCard.person": "henkilö",
|
||||
"hotelPages.rooms.roomCard.persons": "Henkilöä",
|
||||
"hotelPages.rooms.roomCard.seeRoomDetails": "Katso huoneen tiedot",
|
||||
"km to city center": "km keskustaan",
|
||||
"next level:": "pistettä tasolle:",
|
||||
"night": "yö",
|
||||
"nights": "yötä",
|
||||
"number": "määrä",
|
||||
"or": "tai",
|
||||
"points": "pistettä",
|
||||
"special character": "erikoishahmo",
|
||||
"spendable points expiring by": "{points} pistettä vanhenee {date} mennessä",
|
||||
"to": "to",
|
||||
"uppercase letter": "iso kirjain"
|
||||
}
|
||||
|
||||
@@ -14,7 +14,6 @@
|
||||
"Any changes you've made will be lost.": "Eventuelle endringer du har gjort, går tapt.",
|
||||
"Are you sure you want to remove the card ending with {lastFourDigits} from your member profile?": "Er du sikker på at du vil fjerne kortet som slutter på {lastFourDigits} fra medlemsprofilen din?",
|
||||
"Arrival date": "Ankomstdato",
|
||||
"as of today": "per idag",
|
||||
"As our": "Som vår {level}",
|
||||
"As our Close Friend": "Som vår nære venn",
|
||||
"At latest": "Senest",
|
||||
@@ -24,16 +23,14 @@
|
||||
"Bed type": "Seng type",
|
||||
"Book": "Bestill",
|
||||
"Book reward night": "Bestill belønningskveld",
|
||||
"Booking codes and vouchers": "Bestillingskoder og kuponger",
|
||||
"Code / Voucher": "Bestillingskoder / kuponger",
|
||||
"Booking number": "Bestillingsnummer",
|
||||
"Breakfast": "Frokost",
|
||||
"Breakfast excluded": "Frokost ekskludert",
|
||||
"Breakfast included": "Frokost inkludert",
|
||||
"Bus terminal": "Bussterminal",
|
||||
"Business": "Forretnings",
|
||||
"by": "innen",
|
||||
"Cancel": "Avbryt",
|
||||
"characters": "tegn",
|
||||
"Check in": "Sjekk inn",
|
||||
"Check out": "Sjekk ut",
|
||||
"Check out the credit cards saved to your profile. Pay with a saved card when signed in for a smoother web experience.": "Sjekk ut kredittkortene som er lagret på profilen din. Betal med et lagret kort når du er pålogget for en jevnere nettopplevelse.",
|
||||
@@ -75,9 +72,9 @@
|
||||
"Explore all levels and benefits": "Utforsk alle nivåer og fordeler",
|
||||
"Explore nearby": "Utforsk i nærheten",
|
||||
"Extras to your booking": "Tilvalg til bestillingen din",
|
||||
"FAQ": "FAQ",
|
||||
"Failed to delete credit card, please try again later.": "Kunne ikke slette kredittkortet, prøv igjen senere.",
|
||||
"Fair": "Messe",
|
||||
"FAQ": "FAQ",
|
||||
"Find booking": "Finn booking",
|
||||
"Find hotels": "Finn hotell",
|
||||
"Flexibility": "Fleksibilitet",
|
||||
@@ -94,15 +91,11 @@
|
||||
"Hotel": "Hotel",
|
||||
"Hotel facilities": "Hotelfaciliteter",
|
||||
"Hotel surroundings": "Hotellomgivelser",
|
||||
"hotelPages.rooms.roomCard.person": "person",
|
||||
"hotelPages.rooms.roomCard.persons": "personer",
|
||||
"hotelPages.rooms.roomCard.seeRoomDetails": "Se detaljer om rommet",
|
||||
"Hotels": "Hoteller",
|
||||
"How do you want to sleep?": "Hvordan vil du sove?",
|
||||
"How it works": "Hvordan det fungerer",
|
||||
"Image gallery": "Bildegalleri",
|
||||
"Join Scandic Friends": "Bli med i Scandic Friends",
|
||||
"km to city center": "km til sentrum",
|
||||
"Language": "Språk",
|
||||
"Latest searches": "Siste søk",
|
||||
"Level": "Nivå",
|
||||
@@ -129,9 +122,9 @@
|
||||
"Member price": "Medlemspris",
|
||||
"Member price from": "Medlemspris fra",
|
||||
"Members": "Medlemmer",
|
||||
"Membership cards": "Medlemskort",
|
||||
"Membership ID": "Medlems-ID",
|
||||
"Membership ID copied to clipboard": "Medlems-ID kopiert til utklippstavlen",
|
||||
"Membership cards": "Medlemskort",
|
||||
"Menu": "Menu",
|
||||
"Modify": "Endre",
|
||||
"Month": "Måned",
|
||||
@@ -146,9 +139,6 @@
|
||||
"Nearby companies": "Nærliggende selskaper",
|
||||
"New password": "Nytt passord",
|
||||
"Next": "Neste",
|
||||
"next level:": "Neste nivå:",
|
||||
"night": "natt",
|
||||
"nights": "netter",
|
||||
"Nights needed to level up": "Netter som trengs for å komme opp i nivå",
|
||||
"No content published": "Ingen innhold publisert",
|
||||
"No matching location found": "Fant ingen samsvarende plassering",
|
||||
@@ -159,13 +149,11 @@
|
||||
"Non-refundable": "Ikke-refunderbart",
|
||||
"Not found": "Ikke funnet",
|
||||
"Nr night, nr adult": "{nights, number} natt, {adults, number} voksen",
|
||||
"number": "antall",
|
||||
"On your journey": "På reisen din",
|
||||
"Open": "Åpen",
|
||||
"Open language menu": "Åpne språkmenyen",
|
||||
"Open menu": "Åpne menyen",
|
||||
"Open my pages menu": "Åpne mine sider menyen",
|
||||
"or": "eller",
|
||||
"Overview": "Oversikt",
|
||||
"Parking": "Parkering",
|
||||
"Parking / Garage": "Parkering / Garasje",
|
||||
@@ -177,7 +165,6 @@
|
||||
"Phone is required": "Telefon kreves",
|
||||
"Phone number": "Telefonnummer",
|
||||
"Please enter a valid phone number": "Vennligst oppgi et gyldig telefonnummer",
|
||||
"points": "poeng",
|
||||
"Points": "Poeng",
|
||||
"Points being calculated": "Poeng beregnes",
|
||||
"Points earned prior to May 1, 2021": "Opptjente poeng før 1. mai 2021",
|
||||
@@ -196,10 +183,11 @@
|
||||
"Room & Terms": "Rom & Vilkår",
|
||||
"Room facilities": "Romfasiliteter",
|
||||
"Rooms": "Rom",
|
||||
"Rooms & Guests": "Rom og gjester",
|
||||
"Guests & Rooms": "Gjester & rom",
|
||||
"Save": "Lagre",
|
||||
"Scandic Friends Mastercard": "Scandic Friends Mastercard",
|
||||
"Scandic Friends Point Shop": "Scandic Friends Point Shop",
|
||||
"Search": "Søk",
|
||||
"See all photos": "Se alle bilder",
|
||||
"See hotel details": "Se hotellinformasjon",
|
||||
"See room details": "Se detaljer om rommet",
|
||||
@@ -220,29 +208,25 @@
|
||||
"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!": "Noe gikk galt!",
|
||||
"special character": "spesiell karakter",
|
||||
"spendable points expiring by": "{points} Brukbare poeng utløper innen {date}",
|
||||
"Sports": "Sport",
|
||||
"Standard price": "Standardpris",
|
||||
"Street": "Gate",
|
||||
"Successfully updated profile!": "Vellykket oppdatert profil!",
|
||||
"Summary": "Sammendrag",
|
||||
"TUI Points": "TUI Points",
|
||||
"Tell us what information and updates you'd like to receive, and how, by clicking the link below.": "Fortell oss hvilken informasjon og hvilke oppdateringer du ønsker å motta, og hvordan, ved å klikke på lenken nedenfor.",
|
||||
"Thank you": "Takk",
|
||||
"Theatre": "Teater",
|
||||
"There are no transactions to display": "Det er ingen transaksjoner å vise",
|
||||
"Things nearby HOTEL_NAME": "Ting i nærheten av {hotelName}",
|
||||
"to": "til",
|
||||
"Total Points": "Totale poeng",
|
||||
"Tourist": "Turist",
|
||||
"Transaction date": "Transaksjonsdato",
|
||||
"Transactions": "Transaksjoner",
|
||||
"Transportations": "Transport",
|
||||
"Tripadvisor reviews": "{rating} ({count} anmeldelser på Tripadvisor)",
|
||||
"TUI Points": "TUI Points",
|
||||
"Type of bed": "Sengtype",
|
||||
"Type of room": "Romtype",
|
||||
"uppercase letter": "stor bokstav",
|
||||
"Use bonus cheque": "Bruk bonussjekk",
|
||||
"User information": "Brukerinformasjon",
|
||||
"View as list": "Vis som liste",
|
||||
@@ -268,9 +252,9 @@
|
||||
"You canceled adding a new credit card.": "Du kansellerte å legge til et nytt kredittkort.",
|
||||
"You have no previous stays.": "Du har ingen tidligere opphold.",
|
||||
"You have no upcoming stays.": "Du har ingen kommende opphold.",
|
||||
"Your Challenges Conquer & Earn!": "Dine utfordringer Erobre og tjen!",
|
||||
"Your card was successfully removed!": "Kortet ditt ble fjernet!",
|
||||
"Your card was successfully saved!": "Kortet ditt ble lagret!",
|
||||
"Your Challenges Conquer & Earn!": "Dine utfordringer Erobre og tjen!",
|
||||
"Your current level": "Ditt nåværende nivå",
|
||||
"Your details": "Dine detaljer",
|
||||
"Your level": "Ditt nivå",
|
||||
@@ -278,5 +262,23 @@
|
||||
"Zip code": "Post kode",
|
||||
"Zoo": "Dyrehage",
|
||||
"Zoom in": "Zoom inn",
|
||||
"Zoom out": "Zoom ut"
|
||||
"Zoom out": "Zoom ut",
|
||||
"as of today": "per i dag",
|
||||
"booking.nights": "{totalNights, plural, one {# natt} other {# netter}}",
|
||||
"by": "innen",
|
||||
"characters": "tegn",
|
||||
"hotelPages.rooms.roomCard.person": "person",
|
||||
"hotelPages.rooms.roomCard.persons": "personer",
|
||||
"hotelPages.rooms.roomCard.seeRoomDetails": "Se detaljer om rommet",
|
||||
"km to city center": "km til sentrum",
|
||||
"next level:": "Neste nivå:",
|
||||
"night": "natt",
|
||||
"nights": "netter",
|
||||
"number": "antall",
|
||||
"or": "eller",
|
||||
"points": "poeng",
|
||||
"special character": "spesiell karakter",
|
||||
"spendable points expiring by": "{points} Brukbare poeng utløper innen {date}",
|
||||
"to": "til",
|
||||
"uppercase letter": "stor bokstav"
|
||||
}
|
||||
|
||||
@@ -14,7 +14,6 @@
|
||||
"Any changes you've made will be lost.": "Alla ändringar du har gjort kommer att gå förlorade.",
|
||||
"Are you sure you want to remove the card ending with {lastFourDigits} from your member profile?": "Är du säker på att du vill ta bort kortet som slutar med {lastFourDigits} från din medlemsprofil?",
|
||||
"Arrival date": "Ankomstdatum",
|
||||
"as of today": "från och med idag",
|
||||
"As our": "Som vår {level}",
|
||||
"As our Close Friend": "Som vår nära vän",
|
||||
"At latest": "Senast",
|
||||
@@ -24,16 +23,14 @@
|
||||
"Bed type": "Sängtyp",
|
||||
"Book": "Boka",
|
||||
"Book reward night": "Boka frinatt",
|
||||
"Booking codes and vouchers": "Bokningskoder och kuponger",
|
||||
"Code / Voucher": "Bokningskoder / kuponger",
|
||||
"Booking number": "Bokningsnummer",
|
||||
"Breakfast": "Frukost",
|
||||
"Breakfast excluded": "Frukost ingår ej",
|
||||
"Breakfast included": "Frukost ingår",
|
||||
"Bus terminal": "Bussterminal",
|
||||
"Business": "Business",
|
||||
"by": "innan",
|
||||
"Cancel": "Avbryt",
|
||||
"characters": "tecken",
|
||||
"Check in": "Checka in",
|
||||
"Check out": "Checka ut",
|
||||
"Check out the credit cards saved to your profile. Pay with a saved card when signed in for a smoother web experience.": "Kolla in kreditkorten som sparats i din profil. Betala med ett sparat kort när du är inloggad för en smidigare webbupplevelse.",
|
||||
@@ -75,9 +72,9 @@
|
||||
"Explore all levels and benefits": "Utforska alla nivåer och fördelar",
|
||||
"Explore nearby": "Utforska i närheten",
|
||||
"Extras to your booking": "Extra tillval till din bokning",
|
||||
"FAQ": "FAQ",
|
||||
"Failed to delete credit card, please try again later.": "Det gick inte att ta bort kreditkortet, försök igen senare.",
|
||||
"Fair": "Mässa",
|
||||
"FAQ": "FAQ",
|
||||
"Find booking": "Hitta bokning",
|
||||
"Find hotels": "Hitta hotell",
|
||||
"Flexibility": "Flexibilitet",
|
||||
@@ -94,15 +91,11 @@
|
||||
"Hotel": "Hotell",
|
||||
"Hotel facilities": "Hotellfaciliteter",
|
||||
"Hotel surroundings": "Hotellomgivning",
|
||||
"hotelPages.rooms.roomCard.person": "person",
|
||||
"hotelPages.rooms.roomCard.persons": "personer",
|
||||
"hotelPages.rooms.roomCard.seeRoomDetails": "Se information om rummet",
|
||||
"Hotels": "Hotell",
|
||||
"How do you want to sleep?": "Hur vill du sova?",
|
||||
"How it works": "Hur det fungerar",
|
||||
"Image gallery": "Bildgalleri",
|
||||
"Join Scandic Friends": "Gå med i Scandic Friends",
|
||||
"km to city center": "km till stadens centrum",
|
||||
"Language": "Språk",
|
||||
"Latest searches": "Senaste sökningarna",
|
||||
"Level": "Nivå",
|
||||
@@ -129,9 +122,9 @@
|
||||
"Member price": "Medlemspris",
|
||||
"Member price from": "Medlemspris från",
|
||||
"Members": "Medlemmar",
|
||||
"Membership cards": "Medlemskort",
|
||||
"Membership ID": "Medlems-ID",
|
||||
"Membership ID copied to clipboard": "Medlems-ID kopierat till urklipp",
|
||||
"Membership cards": "Medlemskort",
|
||||
"Menu": "Meny",
|
||||
"Modify": "Ändra",
|
||||
"Month": "Månad",
|
||||
@@ -146,9 +139,6 @@
|
||||
"Nearby companies": "Närliggande företag",
|
||||
"New password": "Nytt lösenord",
|
||||
"Next": "Nästa",
|
||||
"next level:": "Nästa nivå:",
|
||||
"night": "natt",
|
||||
"nights": "nätter",
|
||||
"Nights needed to level up": "Nätter som behövs för att gå upp i nivå",
|
||||
"No content published": "Inget innehåll publicerat",
|
||||
"No matching location found": "Ingen matchande plats hittades",
|
||||
@@ -159,13 +149,11 @@
|
||||
"Non-refundable": "Ej återbetalningsbar",
|
||||
"Not found": "Hittades inte",
|
||||
"Nr night, nr adult": "{nights, number} natt, {adults, number} vuxen",
|
||||
"number": "nummer",
|
||||
"On your journey": "På din resa",
|
||||
"Open": "Öppna",
|
||||
"Open language menu": "Öppna språkmenyn",
|
||||
"Open menu": "Öppna menyn",
|
||||
"Open my pages menu": "Öppna mina sidor menyn",
|
||||
"or": "eller",
|
||||
"Overview": "Översikt",
|
||||
"Parking": "Parkering",
|
||||
"Parking / Garage": "Parkering / Garage",
|
||||
@@ -177,7 +165,6 @@
|
||||
"Phone is required": "Telefonnummer är obligatorisk",
|
||||
"Phone number": "Telefonnummer",
|
||||
"Please enter a valid phone number": "Var vänlig och ange ett giltigt telefonnummer",
|
||||
"points": "poäng",
|
||||
"Points": "Poäng",
|
||||
"Points being calculated": "Poäng beräknas",
|
||||
"Points earned prior to May 1, 2021": "Intjänade poäng före den 1 maj 2021",
|
||||
@@ -196,10 +183,11 @@
|
||||
"Room & Terms": "Rum & Villkor",
|
||||
"Room facilities": "Rumfaciliteter",
|
||||
"Rooms": "Rum",
|
||||
"Rooms & Guests": "Rum och gäster",
|
||||
"Guests & Rooms": "Gäster & rum",
|
||||
"Save": "Spara",
|
||||
"Scandic Friends Mastercard": "Scandic Friends Mastercard",
|
||||
"Scandic Friends Point Shop": "Scandic Friends Point Shop",
|
||||
"Search": "Sök",
|
||||
"See all photos": "Se alla foton",
|
||||
"See hotel details": "Se hotellinformation",
|
||||
"See room details": "Se rumsdetaljer",
|
||||
@@ -220,29 +208,25 @@
|
||||
"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!": "Något gick fel!",
|
||||
"special character": "speciell karaktär",
|
||||
"spendable points expiring by": "{points} poäng förfaller {date}",
|
||||
"Sports": "Sport",
|
||||
"Standard price": "Standardpris",
|
||||
"Street": "Gata",
|
||||
"Successfully updated profile!": "Profilen har uppdaterats framgångsrikt!",
|
||||
"Summary": "Sammanfattning",
|
||||
"TUI Points": "TUI Points",
|
||||
"Tell us what information and updates you'd like to receive, and how, by clicking the link below.": "Berätta för oss vilken information och vilka uppdateringar du vill få och hur genom att klicka på länken nedan.",
|
||||
"Thank you": "Tack",
|
||||
"Theatre": "Teater",
|
||||
"There are no transactions to display": "Det finns inga transaktioner att visa",
|
||||
"Things nearby HOTEL_NAME": "Saker i närheten av {hotelName}",
|
||||
"to": "till",
|
||||
"Total Points": "Poäng totalt",
|
||||
"Tourist": "Turist",
|
||||
"Transaction date": "Transaktionsdatum",
|
||||
"Transactions": "Transaktioner",
|
||||
"Transportations": "Transport",
|
||||
"Tripadvisor reviews": "{rating} ({count} recensioner på Tripadvisor)",
|
||||
"TUI Points": "TUI Points",
|
||||
"Type of bed": "Sängtyp",
|
||||
"Type of room": "Rumstyp",
|
||||
"uppercase letter": "stor bokstav",
|
||||
"Use bonus cheque": "Use bonus cheque",
|
||||
"User information": "Användarinformation",
|
||||
"View as list": "Visa som lista",
|
||||
@@ -268,9 +252,9 @@
|
||||
"You canceled adding a new credit card.": "Du avbröt att lägga till ett nytt kreditkort.",
|
||||
"You have no previous stays.": "Du har inga tidigare vistelser.",
|
||||
"You have no upcoming stays.": "Du har inga planerade resor.",
|
||||
"Your Challenges Conquer & Earn!": "Dina utmaningar Erövra och tjäna!",
|
||||
"Your card was successfully removed!": "Ditt kort har tagits bort!",
|
||||
"Your card was successfully saved!": "Ditt kort har sparats!",
|
||||
"Your Challenges Conquer & Earn!": "Dina utmaningar Erövra och tjäna!",
|
||||
"Your current level": "Din nuvarande nivå",
|
||||
"Your details": "Dina uppgifter",
|
||||
"Your level": "Din nivå",
|
||||
@@ -278,5 +262,23 @@
|
||||
"Zip code": "Postnummer",
|
||||
"Zoo": "Djurpark",
|
||||
"Zoom in": "Zooma in",
|
||||
"Zoom out": "Zooma ut"
|
||||
"Zoom out": "Zooma ut",
|
||||
"as of today": "per idag",
|
||||
"booking.nights": "{totalNights, plural, one {# natt} other {# nätter}}",
|
||||
"by": "innan",
|
||||
"characters": "tecken",
|
||||
"hotelPages.rooms.roomCard.person": "person",
|
||||
"hotelPages.rooms.roomCard.persons": "personer",
|
||||
"hotelPages.rooms.roomCard.seeRoomDetails": "Se information om rummet",
|
||||
"km to city center": "km till stadens centrum",
|
||||
"next level:": "Nästa nivå:",
|
||||
"night": "natt",
|
||||
"nights": "nätter",
|
||||
"number": "nummer",
|
||||
"or": "eller",
|
||||
"points": "poäng",
|
||||
"special character": "speciell karaktär",
|
||||
"spendable points expiring by": "{points} poäng förfaller {date}",
|
||||
"to": "till",
|
||||
"uppercase letter": "stor bokstav"
|
||||
}
|
||||
|
||||
@@ -4,9 +4,9 @@
|
||||
export namespace endpoints {
|
||||
export const enum v0 {
|
||||
profile = "profile/v0/Profile",
|
||||
availability = "availability/v0/availabilities/city",
|
||||
}
|
||||
export const enum v1 {
|
||||
availability = "availability/v1/availabilities/city",
|
||||
profile = "profile/v1/Profile",
|
||||
booking = "booking/v1/Bookings",
|
||||
creditCards = `${profile}/creditCards`,
|
||||
|
||||
@@ -105,9 +105,14 @@ export const imageVaultAssetTransformedSchema = imageVaultAssetSchema.transform(
|
||||
const caption = rawData.Metadata?.find((meta) =>
|
||||
meta.Name.includes("Title_")
|
||||
)?.Value
|
||||
const mediaConversion = rawData.MediaConversions[0]
|
||||
const aspectRatio =
|
||||
mediaConversion.FormatAspectRatio ||
|
||||
mediaConversion.AspectRatio ||
|
||||
mediaConversion.Width / mediaConversion.Height
|
||||
|
||||
return {
|
||||
url: rawData.MediaConversions[0].Url,
|
||||
url: mediaConversion.Url,
|
||||
id: rawData.Id,
|
||||
meta: {
|
||||
alt,
|
||||
@@ -115,9 +120,9 @@ export const imageVaultAssetTransformedSchema = imageVaultAssetSchema.transform(
|
||||
},
|
||||
title: rawData.Name,
|
||||
dimensions: {
|
||||
width: rawData.MediaConversions[0].Width,
|
||||
height: rawData.MediaConversions[0].Height,
|
||||
aspectRatio: rawData.MediaConversions[0].FormatAspectRatio,
|
||||
width: mediaConversion.Width,
|
||||
height: mediaConversion.Height,
|
||||
aspectRatio,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -298,7 +298,7 @@ const parkingPricingSchema = z.object({
|
||||
.optional(),
|
||||
})
|
||||
|
||||
const parkingSchema = z.object({
|
||||
export const parkingSchema = z.object({
|
||||
type: z.string(),
|
||||
name: z.string(),
|
||||
address: z.string().optional(),
|
||||
|
||||
@@ -271,7 +271,7 @@ export const hotelQueryRouter = router({
|
||||
JSON.stringify({ query: { cityId, params } })
|
||||
)
|
||||
const apiResponse = await api.get(
|
||||
`${api.endpoints.v0.availability}/${cityId}`,
|
||||
`${api.endpoints.v1.availability}/${cityId}`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${ctx.serviceToken}`,
|
||||
|
||||
@@ -9,19 +9,11 @@
|
||||
"cityName": "Stockholm",
|
||||
"ratings": {
|
||||
"tripAdvisor": {
|
||||
"numberOfReviews": 2776,
|
||||
"rating": 4.0,
|
||||
"ratingImageUrl": "https://www.tripadvisor.com/img/cdsi/img2/ratings/traveler/4.5-15458-5.svg",
|
||||
"webUrl": "https://www.tripadvisor.com/Hotel_Review-g189852-d229383-Reviews-Scandic_Continental-Stockholm.html",
|
||||
"numberOfReviews": 2793,
|
||||
"rating": 4,
|
||||
"ratingImageUrl": "https://www.tripadvisor.com/img/cdsi/img2/ratings/traveler/4.0-15458-5.svg",
|
||||
"webUrl": "https://www.tripadvisor.com/Hotel_Review-g189852-d229383-Reviews-Scandic_Continental-Stockholm.html?m=15458",
|
||||
"awards": [
|
||||
{
|
||||
"displayName": "Travelers Choice",
|
||||
"images": {
|
||||
"small": "https://static.tacdn.com/img2/travelers_choice/widgets/tchotel_2024_L.png",
|
||||
"medium": "https://static.tacdn.com/img2/travelers_choice/widgets/tchotel_2024_L.png",
|
||||
"large": "https://static.tacdn.com/img2/travelers_choice/widgets/tchotel_2024_L.png"
|
||||
}
|
||||
},
|
||||
{
|
||||
"displayName": "Travelers Choice",
|
||||
"images": {
|
||||
@@ -30,12 +22,7 @@
|
||||
"large": "https://static.tacdn.com/img2/travelers_choice/widgets/tchotel_2023_L.png"
|
||||
}
|
||||
}
|
||||
],
|
||||
"reviews": {
|
||||
"widgetHtmlTagId": "TA_cdspropertydetail",
|
||||
"widgetScriptEmbedUrlIframe": "//www.tripadvisor.com/WidgetEmbed-cdspropertydetail?locationId=12441627&partnerId=FDF3F5CC73C349C0A5AB94C0DD86BB76&lang=en&display=true",
|
||||
"widgetScriptEmbedUrlJavaScript": "//www.tripadvisor.com/WidgetEmbed-cdspropertydetail?locationId=12441627&partnerId=FDF3F5CC73C349C0A5AB94C0DD86BB76&lang=en&display=false"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"address": {
|
||||
@@ -157,7 +144,7 @@
|
||||
"facilityInformation": "Relax with your favorite mixed drink in the bar on our roof terrace. Work out in our gym or relax in our sauna. At Scandic Continental we offer meeting rooms for up to 900 participants.",
|
||||
"surroundingInformation": "Explore popular attractions such as the Old Town, the Opera, City Hall and Royal Palace. You are close to public transport, buses and trams and within walking distance of all that the city centre has to offer.",
|
||||
"descriptions": {
|
||||
"short": "Scandic Continental enjoys a fantastic location in Stockholm city centre. Close to shopping areas, culture, attractions and restaurants. ",
|
||||
"short": "Scandic Continental enjoys a fantastic location in Stockholm city centre. Close to shopping areas, culture, attractions and restaurants.",
|
||||
"medium": "Scandic Continental enjoys a fantastic location in Stockholm city centre. Close to shopping areas, culture, attractions and restaurants. Popular attractions such as the Old Town, the Opera, City Hall and Royal Palace are all close by."
|
||||
}
|
||||
},
|
||||
@@ -172,190 +159,149 @@
|
||||
{
|
||||
"id": 5550,
|
||||
"name": "Bikes for loan",
|
||||
"applyToAllHotels": false,
|
||||
"public": true,
|
||||
"icon": "BikesForLoan",
|
||||
"sortOrder": 700
|
||||
"sortOrder": 700,
|
||||
"filter": "None"
|
||||
},
|
||||
{
|
||||
"id": 1829,
|
||||
"name": "Gym",
|
||||
"code": "HEA - TRAI",
|
||||
"applyToAllHotels": false,
|
||||
"public": true,
|
||||
"icon": "Gym",
|
||||
"iconName": "Gym",
|
||||
"sortOrder": 1700
|
||||
"sortOrder": 1700,
|
||||
"filter": "Hotel facilities"
|
||||
},
|
||||
{
|
||||
"id": 1833,
|
||||
"name": "Free WiFi",
|
||||
"code": "IHF",
|
||||
"applyToAllHotels": false,
|
||||
"public": true,
|
||||
"icon": "FreeWiFi",
|
||||
"sortOrder": 1900
|
||||
"sortOrder": 1900,
|
||||
"filter": "None"
|
||||
},
|
||||
{
|
||||
"id": 1834,
|
||||
"name": "Laundry service",
|
||||
"code": "LAU",
|
||||
"applyToAllHotels": false,
|
||||
"public": true,
|
||||
"icon": "LaundryService",
|
||||
"sortOrder": 200
|
||||
"sortOrder": 200,
|
||||
"filter": "None"
|
||||
},
|
||||
{
|
||||
"id": 1406,
|
||||
"name": "Parking - additional cost",
|
||||
"code": "PAR",
|
||||
"applyToAllHotels": false,
|
||||
"public": true,
|
||||
"icon": "None",
|
||||
"sortOrder": 0
|
||||
"sortOrder": 0,
|
||||
"filter": "None"
|
||||
},
|
||||
{
|
||||
"id": 2665,
|
||||
"name": "Parking - garage",
|
||||
"code": "GAR",
|
||||
"applyToAllHotels": false,
|
||||
"public": true,
|
||||
"icon": "Parking",
|
||||
"iconName": "Garage",
|
||||
"sortOrder": 1400
|
||||
"sortOrder": 1400,
|
||||
"filter": "Hotel facilities"
|
||||
},
|
||||
{
|
||||
"id": 1835,
|
||||
"name": "Pet-friendly rooms",
|
||||
"code": "PET",
|
||||
"applyToAllHotels": false,
|
||||
"public": true,
|
||||
"icon": "None",
|
||||
"sortOrder": 0
|
||||
"sortOrder": 0,
|
||||
"filter": "Hotel facilities"
|
||||
},
|
||||
{
|
||||
"id": 1379,
|
||||
"name": "Sauna",
|
||||
"code": "SAU - RELX",
|
||||
"applyToAllHotels": false,
|
||||
"public": true,
|
||||
"icon": "Sauna",
|
||||
"sortOrder": 2000
|
||||
"sortOrder": 2000,
|
||||
"filter": "Hotel facilities"
|
||||
},
|
||||
{
|
||||
"id": 1017,
|
||||
"name": "Meeting rooms",
|
||||
"code": "MEE",
|
||||
"applyToAllHotels": false,
|
||||
"public": true,
|
||||
"icon": "Meeting",
|
||||
"sortOrder": 9000
|
||||
"sortOrder": 9000,
|
||||
"filter": "None"
|
||||
},
|
||||
{
|
||||
"id": 1382,
|
||||
"name": "Outdoor terrace",
|
||||
"code": "-",
|
||||
"applyToAllHotels": false,
|
||||
"public": true,
|
||||
"icon": "OutdoorTerrace",
|
||||
"sortOrder": 1000
|
||||
"sortOrder": 1000,
|
||||
"filter": "Hotel facilities"
|
||||
},
|
||||
{
|
||||
"id": 1408,
|
||||
"name": "Scandic Shop 24 hrs",
|
||||
"code": "SHOP",
|
||||
"applyToAllHotels": false,
|
||||
"public": true,
|
||||
"icon": "Shop",
|
||||
"sortOrder": 100
|
||||
"sortOrder": 100,
|
||||
"filter": "None"
|
||||
},
|
||||
{
|
||||
"id": 1606,
|
||||
"name": "Sky bar",
|
||||
"code": "-",
|
||||
"applyToAllHotels": false,
|
||||
"public": true,
|
||||
"icon": "Skybar",
|
||||
"iconName": "Sky Bar / Rooftop Bar",
|
||||
"sortOrder": 1100
|
||||
"sortOrder": 1100,
|
||||
"filter": "Hotel facilities"
|
||||
},
|
||||
{
|
||||
"id": 5806,
|
||||
"name": "Meeting / conference facilities",
|
||||
"code": "MEE - MEETING ",
|
||||
"applyToAllHotels": false,
|
||||
"public": true,
|
||||
"icon": "None",
|
||||
"sortOrder": 1500
|
||||
"sortOrder": 1500,
|
||||
"filter": "None"
|
||||
},
|
||||
{
|
||||
"id": 1607,
|
||||
"name": "Golf course (0-30 km)",
|
||||
"code": "GOLF",
|
||||
"applyToAllHotels": false,
|
||||
"public": false,
|
||||
"icon": "None",
|
||||
"sortOrder": 0
|
||||
"sortOrder": 0,
|
||||
"filter": "Hotel surroundings"
|
||||
},
|
||||
{
|
||||
"id": 971,
|
||||
"name": "Shopping",
|
||||
"code": "-",
|
||||
"applyToAllHotels": false,
|
||||
"public": false,
|
||||
"icon": "None",
|
||||
"sortOrder": 0
|
||||
"sortOrder": 0,
|
||||
"filter": "Hotel surroundings"
|
||||
},
|
||||
{
|
||||
"id": 1911,
|
||||
"name": "24 hours security",
|
||||
"applyToAllHotels": false,
|
||||
"public": true,
|
||||
"icon": "None",
|
||||
"sortOrder": 0
|
||||
"sortOrder": 0,
|
||||
"filter": "None"
|
||||
},
|
||||
{
|
||||
"id": 1913,
|
||||
"name": "Overnight security",
|
||||
"applyToAllHotels": false,
|
||||
"public": true,
|
||||
"icon": "None",
|
||||
"sortOrder": 0
|
||||
"sortOrder": 0,
|
||||
"filter": "None"
|
||||
},
|
||||
{
|
||||
"id": 162583,
|
||||
"name": "Laundry service - express",
|
||||
"applyToAllHotels": false,
|
||||
"public": true,
|
||||
"icon": "ExpressLaundryService",
|
||||
"iconName": "Express dry cleaning",
|
||||
"sortOrder": 300
|
||||
"sortOrder": 300,
|
||||
"filter": "None"
|
||||
},
|
||||
{
|
||||
"id": 229144,
|
||||
"name": "TV with Chromecast",
|
||||
"applyToAllHotels": false,
|
||||
"public": true,
|
||||
"icon": "None",
|
||||
"sortOrder": 0
|
||||
"sortOrder": 0,
|
||||
"filter": "None"
|
||||
},
|
||||
{
|
||||
"id": 1407,
|
||||
"name": "Serves breakfast (always included)",
|
||||
"code": "-",
|
||||
"applyToAllHotels": false,
|
||||
"public": true,
|
||||
"icon": "None",
|
||||
"sortOrder": 0
|
||||
"sortOrder": 0,
|
||||
"filter": "None"
|
||||
},
|
||||
{
|
||||
"id": 1378,
|
||||
"name": "Room service",
|
||||
"code": "ROO - R/S",
|
||||
"applyToAllHotels": false,
|
||||
"public": true,
|
||||
"icon": "RoomService",
|
||||
"sortOrder": 400
|
||||
"sortOrder": 400,
|
||||
"filter": "None"
|
||||
}
|
||||
],
|
||||
"healthFacilities": [
|
||||
@@ -1067,7 +1013,617 @@
|
||||
"instagram": "https://www.instagram.com/scandiccontinental/",
|
||||
"facebook": "https://www.facebook.com/scandiccontinental/"
|
||||
},
|
||||
"isActive": true
|
||||
}
|
||||
"isActive": true,
|
||||
"gallery": {
|
||||
"heroImages": [
|
||||
{
|
||||
"metaData": {
|
||||
"title": "Superior plus, room",
|
||||
"altText": "Superior plus, room",
|
||||
"altText_En": "Superior plus, room",
|
||||
"copyRight": "Elin Strömberg © 2016"
|
||||
},
|
||||
"imageSizes": {
|
||||
"tiny": "https://test3.scandichotels.com/imagevault/publishedmedia/5rrxa1aq23taddapu11q/scandic-continental-room-superiorplus.jpg",
|
||||
"small": "https://test3.scandichotels.com/imagevault/publishedmedia/66hti5xhqzmr8jhab213/scandic-continental-room-superiorplus.jpg",
|
||||
"medium": "https://test3.scandichotels.com/imagevault/publishedmedia/9lplgtij1oyjwaz93lrs/scandic-continental-room-superiorplus.jpg",
|
||||
"large": "https://test3.scandichotels.com/imagevault/publishedmedia/f5bdchr9zeat67jri8kw/scandic-continental-room-superiorplus.jpg"
|
||||
}
|
||||
},
|
||||
{
|
||||
"metaData": {
|
||||
"title": "Entrance, restaurant Market",
|
||||
"altText": "Entrance, restaurant Market",
|
||||
"altText_En": "Entrance, restaurant Market",
|
||||
"copyRight": "Elin Sylwan © 2016"
|
||||
},
|
||||
"imageSizes": {
|
||||
"tiny": "https://test3.scandichotels.com/imagevault/publishedmedia/9qgydydjtavu128kv2xt/scandic-continental-entrance-themarket.jpg",
|
||||
"small": "https://test3.scandichotels.com/imagevault/publishedmedia/0c0sz9g0r9hhejw62ez9/scandic-continental-entrance-themarket.jpg",
|
||||
"medium": "https://test3.scandichotels.com/imagevault/publishedmedia/ka3ku5zqp5uuqpfjddot/scandic-continental-entrance-themarket.jpg",
|
||||
"large": "https://test3.scandichotels.com/imagevault/publishedmedia/ksa5jgagck9sbj2uarz5/scandic-continental-entrance-themarket.jpg"
|
||||
}
|
||||
},
|
||||
{
|
||||
"metaData": {
|
||||
"title": "Terrace",
|
||||
"altText": "Terrace",
|
||||
"altText_En": "Terrace",
|
||||
"copyRight": "Björn Enström"
|
||||
},
|
||||
"imageSizes": {
|
||||
"tiny": "https://test3.scandichotels.com/imagevault/publishedmedia/m9yj4g160snutij4sivk/Scandic_Continental_Capitol_Terrace.jpg",
|
||||
"small": "https://test3.scandichotels.com/imagevault/publishedmedia/fqqakeunmamm3uetf47w/Scandic_Continental_Capitol_Terrace.jpg",
|
||||
"medium": "https://test3.scandichotels.com/imagevault/publishedmedia/6fp0avu8yjrh38zv1as8/Scandic_Continental_Capitol_Terrace.jpg",
|
||||
"large": "https://test3.scandichotels.com/imagevault/publishedmedia/j8dveyubybb4f7or9qay/Scandic_Continental_Capitol_Terrace.jpg"
|
||||
}
|
||||
}
|
||||
],
|
||||
"smallerImages": [
|
||||
{
|
||||
"metaData": {
|
||||
"title": "Entrance",
|
||||
"altText": "Entrance from Vasagatan",
|
||||
"altText_En": "Entrance from Vasagatan",
|
||||
"copyRight": "Elin Sylwan © 2016"
|
||||
},
|
||||
"imageSizes": {
|
||||
"tiny": "https://test3.scandichotels.com/imagevault/publishedmedia/jlnmjycubt2ks3oiaoee/scandic-continental-entrance-vasagatan.jpg",
|
||||
"small": "https://test3.scandichotels.com/imagevault/publishedmedia/izgmvkaesxw9jynunuyo/scandic-continental-entrance-vasagatan.jpg",
|
||||
"medium": "https://test3.scandichotels.com/imagevault/publishedmedia/sfasl29ijndz2ez6lywn/scandic-continental-entrance-vasagatan.jpg",
|
||||
"large": "https://test3.scandichotels.com/imagevault/publishedmedia/9vcvf9x9exykkl2yf0bg/scandic-continental-entrance-vasagatan.jpg"
|
||||
}
|
||||
},
|
||||
{
|
||||
"metaData": {
|
||||
"title": "Standard room",
|
||||
"altText": "Standard room",
|
||||
"altText_En": "Standard room",
|
||||
"copyRight": "Elin Strömberg © 2016"
|
||||
},
|
||||
"imageSizes": {
|
||||
"tiny": "https://test3.scandichotels.com/imagevault/publishedmedia/qj2en3cqvvadxe5y5b6y/scandic-continental-room-standard.jpg",
|
||||
"small": "https://test3.scandichotels.com/imagevault/publishedmedia/16kz0cyxngrwe7880gjh/scandic-continental-room-standard.jpg",
|
||||
"medium": "https://test3.scandichotels.com/imagevault/publishedmedia/x8r0of336j2rsrhvwlqy/scandic-continental-room-standard.jpg",
|
||||
"large": "https://test3.scandichotels.com/imagevault/publishedmedia/co3h6nr4p28j2tn641sz/scandic-continental-room-standard.jpg"
|
||||
}
|
||||
},
|
||||
{
|
||||
"metaData": {
|
||||
"title": "Junior suite",
|
||||
"altText": "Junior suite, detail",
|
||||
"altText_En": "Junior suite, detail",
|
||||
"copyRight": "Elin Strömberg © 2016"
|
||||
},
|
||||
"imageSizes": {
|
||||
"tiny": "https://test3.scandichotels.com/imagevault/publishedmedia/tijveokv1qmnmy964iv9/scandic-continental-room-juniorsuite-detail-1.jpg",
|
||||
"small": "https://test3.scandichotels.com/imagevault/publishedmedia/i7h3u2x6a9ta46jxb2tg/scandic-continental-room-juniorsuite-detail-1.jpg",
|
||||
"medium": "https://test3.scandichotels.com/imagevault/publishedmedia/hv4on6ec8qlcxmcd0enl/scandic-continental-room-juniorsuite-detail-1.jpg",
|
||||
"large": "https://test3.scandichotels.com/imagevault/publishedmedia/3eakprn9n0zwyjpzpy6p/scandic-continental-room-juniorsuite-detail-1.jpg"
|
||||
}
|
||||
},
|
||||
{
|
||||
"metaData": {
|
||||
"title": "Junior Suite",
|
||||
"altText": "Junior Suite",
|
||||
"altText_En": "Junior Suite",
|
||||
"copyRight": "Elin Strömberg © 2016"
|
||||
},
|
||||
"imageSizes": {
|
||||
"tiny": "https://test3.scandichotels.com/imagevault/publishedmedia/7h4bonsezjsjz1lpg6hd/scandic-continental-room-juniorsuite.jpg",
|
||||
"small": "https://test3.scandichotels.com/imagevault/publishedmedia/gge47e5ach2e0kuakggx/scandic-continental-room-juniorsuite.jpg",
|
||||
"medium": "https://test3.scandichotels.com/imagevault/publishedmedia/cts4y9u9w02ockyyoze7/scandic-continental-room-juniorsuite.jpg",
|
||||
"large": "https://test3.scandichotels.com/imagevault/publishedmedia/f8aojtx38vb7ywv33cf6/scandic-continental-room-juniorsuite.jpg"
|
||||
}
|
||||
},
|
||||
{
|
||||
"metaData": {
|
||||
"title": "Superior plus, room",
|
||||
"altText": "Superior plus, room",
|
||||
"altText_En": "Superior plus, room",
|
||||
"copyRight": "Elin Strömberg © 2016"
|
||||
},
|
||||
"imageSizes": {
|
||||
"tiny": "https://test3.scandichotels.com/imagevault/publishedmedia/5rrxa1aq23taddapu11q/scandic-continental-room-superiorplus.jpg",
|
||||
"small": "https://test3.scandichotels.com/imagevault/publishedmedia/66hti5xhqzmr8jhab213/scandic-continental-room-superiorplus.jpg",
|
||||
"medium": "https://test3.scandichotels.com/imagevault/publishedmedia/9lplgtij1oyjwaz93lrs/scandic-continental-room-superiorplus.jpg",
|
||||
"large": "https://test3.scandichotels.com/imagevault/publishedmedia/f5bdchr9zeat67jri8kw/scandic-continental-room-superiorplus.jpg"
|
||||
}
|
||||
},
|
||||
{
|
||||
"metaData": {
|
||||
"title": "Superior room, detail",
|
||||
"altText": "Superior room, detail",
|
||||
"altText_En": "Superior room, detail",
|
||||
"copyRight": "Elin Strömberg © 2016"
|
||||
},
|
||||
"imageSizes": {
|
||||
"tiny": "https://test3.scandichotels.com/imagevault/publishedmedia/6jjitns74k9nutn9v9tz/scandic-continental-room-superior-detail.jpg",
|
||||
"small": "https://test3.scandichotels.com/imagevault/publishedmedia/ft3se6waag20lx6hxtn9/scandic-continental-room-superior-detail.jpg",
|
||||
"medium": "https://test3.scandichotels.com/imagevault/publishedmedia/wsfl5usd8qbdm929dvxu/scandic-continental-room-superior-detail.jpg",
|
||||
"large": "https://test3.scandichotels.com/imagevault/publishedmedia/duedba0io5xew6almv7l/scandic-continental-room-superior-detail.jpg"
|
||||
}
|
||||
},
|
||||
{
|
||||
"metaData": {
|
||||
"title": "Junior suite, bathroom",
|
||||
"altText": "Junior suite, bathroom",
|
||||
"altText_En": "Junior suite, bathroom",
|
||||
"copyRight": "Elin Strömberg © 2016"
|
||||
},
|
||||
"imageSizes": {
|
||||
"tiny": "https://test3.scandichotels.com/imagevault/publishedmedia/2cpdxrywepicqnp9eoob/scandic-continental-room-juniorsuite-bathroom-1.jpg",
|
||||
"small": "https://test3.scandichotels.com/imagevault/publishedmedia/ynkzimfaldgef0ukj7q5/scandic-continental-room-juniorsuite-bathroom-1.jpg",
|
||||
"medium": "https://test3.scandichotels.com/imagevault/publishedmedia/jrhqe8vbm91vegd02bjh/scandic-continental-room-juniorsuite-bathroom-1.jpg",
|
||||
"large": "https://test3.scandichotels.com/imagevault/publishedmedia/93ag11udgjefz6qwa8st/scandic-continental-room-juniorsuite-bathroom-1.jpg"
|
||||
}
|
||||
},
|
||||
{
|
||||
"metaData": {
|
||||
"title": "Entrance, restaurant Market",
|
||||
"altText": "Entrance, restaurant Market",
|
||||
"altText_En": "Entrance, restaurant Market",
|
||||
"copyRight": "Elin Sylwan © 2016"
|
||||
},
|
||||
"imageSizes": {
|
||||
"tiny": "https://test3.scandichotels.com/imagevault/publishedmedia/9qgydydjtavu128kv2xt/scandic-continental-entrance-themarket.jpg",
|
||||
"small": "https://test3.scandichotels.com/imagevault/publishedmedia/0c0sz9g0r9hhejw62ez9/scandic-continental-entrance-themarket.jpg",
|
||||
"medium": "https://test3.scandichotels.com/imagevault/publishedmedia/ka3ku5zqp5uuqpfjddot/scandic-continental-entrance-themarket.jpg",
|
||||
"large": "https://test3.scandichotels.com/imagevault/publishedmedia/ksa5jgagck9sbj2uarz5/scandic-continental-entrance-themarket.jpg"
|
||||
}
|
||||
},
|
||||
{
|
||||
"metaData": {
|
||||
"title": "Dining room, the Market",
|
||||
"altText": "Dining room, the Market",
|
||||
"altText_En": "Dining room, the Market",
|
||||
"copyRight": "Karl Gabor"
|
||||
},
|
||||
"imageSizes": {
|
||||
"tiny": "https://test3.scandichotels.com/imagevault/publishedmedia/bywlg4nwp11hmebfb1wg/scandic-continental-diningroom-themarket.jpg",
|
||||
"small": "https://test3.scandichotels.com/imagevault/publishedmedia/fdvp429hm1rfpg2le6jo/scandic-continental-diningroom-themarket.jpg",
|
||||
"medium": "https://test3.scandichotels.com/imagevault/publishedmedia/wm24ma9ngk8gijotm7va/scandic-continental-diningroom-themarket.jpg",
|
||||
"large": "https://test3.scandichotels.com/imagevault/publishedmedia/7xiz9p0f254yl9oef7bj/scandic-continental-diningroom-themarket.jpg"
|
||||
}
|
||||
},
|
||||
{
|
||||
"metaData": {
|
||||
"title": "Dining room, the Market",
|
||||
"altText": "Dining room, the Market",
|
||||
"altText_En": "Dining room, the Market",
|
||||
"copyRight": "Karl Gabor"
|
||||
},
|
||||
"imageSizes": {
|
||||
"tiny": "https://test3.scandichotels.com/imagevault/publishedmedia/zd9rkkm383ctblciw0np/scandic-continental-diningroom-themarket-2.jpg",
|
||||
"small": "https://test3.scandichotels.com/imagevault/publishedmedia/i9giiq3ijihm172b5bbw/scandic-continental-diningroom-themarket-2.jpg",
|
||||
"medium": "https://test3.scandichotels.com/imagevault/publishedmedia/9baixkat8wjdsjn1lo34/scandic-continental-diningroom-themarket-2.jpg",
|
||||
"large": "https://test3.scandichotels.com/imagevault/publishedmedia/kwemrr8fohnnwo0uzd3s/scandic-continental-diningroom-themarket-2.jpg"
|
||||
}
|
||||
},
|
||||
{
|
||||
"metaData": {
|
||||
"title": "Restaurant, the Market, detail",
|
||||
"altText": "Restaurant, the Market, detail",
|
||||
"altText_En": "Restaurant, the Market, detail",
|
||||
"copyRight": "Karl Gabor"
|
||||
},
|
||||
"imageSizes": {
|
||||
"tiny": "https://test3.scandichotels.com/imagevault/publishedmedia/uo7e32pi5cmf06as55ad/scandic-continental-themarket-detail.jpg",
|
||||
"small": "https://test3.scandichotels.com/imagevault/publishedmedia/1qhpesv8d407j00b1y5x/scandic-continental-themarket-detail.jpg",
|
||||
"medium": "https://test3.scandichotels.com/imagevault/publishedmedia/ldenlssse8595b4iavjj/scandic-continental-themarket-detail.jpg",
|
||||
"large": "https://test3.scandichotels.com/imagevault/publishedmedia/yhs5jgy8mgw9xvzazjze/scandic-continental-themarket-detail.jpg"
|
||||
}
|
||||
},
|
||||
{
|
||||
"metaData": {
|
||||
"title": "Breakfast, the Market",
|
||||
"altText": "Breakfast, the Market",
|
||||
"altText_En": "Breakfast, the Market",
|
||||
"copyRight": "Karl Gabor"
|
||||
},
|
||||
"imageSizes": {
|
||||
"tiny": "https://test3.scandichotels.com/imagevault/publishedmedia/vsj1725jfae5eqx9urua/scandic-continental-breakfast-themarket-15.jpg",
|
||||
"small": "https://test3.scandichotels.com/imagevault/publishedmedia/ajqxe5obci19sw4g08ub/scandic-continental-breakfast-themarket-15.jpg",
|
||||
"medium": "https://test3.scandichotels.com/imagevault/publishedmedia/wi3xa47bwtb5s5kbaxq8/scandic-continental-breakfast-themarket-15.jpg",
|
||||
"large": "https://test3.scandichotels.com/imagevault/publishedmedia/1igemz65ffu4nw7bg64l/scandic-continental-breakfast-themarket-15.jpg"
|
||||
}
|
||||
},
|
||||
{
|
||||
"metaData": {
|
||||
"title": "Breakfast, the Market",
|
||||
"altText": "Breakfast, the Market",
|
||||
"altText_En": "Breakfast, the Market",
|
||||
"copyRight": "Karl Gabor"
|
||||
},
|
||||
"imageSizes": {
|
||||
"tiny": "https://test3.scandichotels.com/imagevault/publishedmedia/1clcvo8e86itpus3xy99/scandic-continental-breakfast-themarket-9.jpg",
|
||||
"small": "https://test3.scandichotels.com/imagevault/publishedmedia/on5ckcmwxvs47l11natn/scandic-continental-breakfast-themarket-9.jpg",
|
||||
"medium": "https://test3.scandichotels.com/imagevault/publishedmedia/l5g6dmumzh9duseaa9zw/scandic-continental-breakfast-themarket-9.jpg",
|
||||
"large": "https://test3.scandichotels.com/imagevault/publishedmedia/pl45n94xmu19bo0fokwf/scandic-continental-breakfast-themarket-9.jpg"
|
||||
}
|
||||
},
|
||||
{
|
||||
"metaData": {
|
||||
"title": "Restaurant Caldo",
|
||||
"altText": "Restaurant Caldo",
|
||||
"altText_En": "Restaurant Caldo",
|
||||
"copyRight": "Elin Sylwan © 2016"
|
||||
},
|
||||
"imageSizes": {
|
||||
"tiny": "https://test3.scandichotels.com/imagevault/publishedmedia/q5cj5qehiq03ey38ppen/scandic-continental-caldo.jpg",
|
||||
"small": "https://test3.scandichotels.com/imagevault/publishedmedia/na08m2sippkfzd3yf9le/scandic-continental-caldo.jpg",
|
||||
"medium": "https://test3.scandichotels.com/imagevault/publishedmedia/wxh6w02ujd5wqyr9bvg8/scandic-continental-caldo.jpg",
|
||||
"large": "https://test3.scandichotels.com/imagevault/publishedmedia/bem9c6dxappvlwxg7s1g/scandic-continental-caldo.jpg"
|
||||
}
|
||||
},
|
||||
{
|
||||
"metaData": {
|
||||
"title": "Roof top bar",
|
||||
"altText": "Roof top bar at Scandic Continental",
|
||||
"altText_En": "Roof top bar at Scandic Continental",
|
||||
"copyRight": "Scandic"
|
||||
},
|
||||
"imageSizes": {
|
||||
"tiny": "https://test3.scandichotels.com/imagevault/publishedmedia/cxf1z9sgr9152ls1kdmg/Scandic_Continental_Capitol_The_View_13.jpg",
|
||||
"small": "https://test3.scandichotels.com/imagevault/publishedmedia/pd43w13z5dx8l7kzhsyg/Scandic_Continental_Capitol_The_View_13.jpg",
|
||||
"medium": "https://test3.scandichotels.com/imagevault/publishedmedia/7ryqztmdqg0mam62oyf5/Scandic_Continental_Capitol_The_View_13.jpg",
|
||||
"large": "https://test3.scandichotels.com/imagevault/publishedmedia/hwpsq70dbln0l052cjqa/Scandic_Continental_Capitol_The_View_13.jpg"
|
||||
}
|
||||
},
|
||||
{
|
||||
"metaData": {
|
||||
"title": "Roof top bar",
|
||||
"altText": "Roof top bar at Scandic Continental",
|
||||
"altText_En": "Roof top bar at Scandic Continental",
|
||||
"copyRight": "Scandic"
|
||||
},
|
||||
"imageSizes": {
|
||||
"tiny": "https://test3.scandichotels.com/imagevault/publishedmedia/hd1wk1wvtwsz4fuvf0rb/Scandic_Continental_Capitol_The_View_6.jpg",
|
||||
"small": "https://test3.scandichotels.com/imagevault/publishedmedia/ioi4ir6ijpy0f5dvu56n/Scandic_Continental_Capitol_The_View_6.jpg",
|
||||
"medium": "https://test3.scandichotels.com/imagevault/publishedmedia/ydbhbjj3ulju2bxh7ebj/Scandic_Continental_Capitol_The_View_6.jpg",
|
||||
"large": "https://test3.scandichotels.com/imagevault/publishedmedia/kom0ttrq6xazxxuodrr1/Scandic_Continental_Capitol_The_View_6.jpg"
|
||||
}
|
||||
},
|
||||
{
|
||||
"metaData": {
|
||||
"title": "Terrace",
|
||||
"altText": "Terrace",
|
||||
"altText_En": "Terrace",
|
||||
"copyRight": "Björn Enström"
|
||||
},
|
||||
"imageSizes": {
|
||||
"tiny": "https://test3.scandichotels.com/imagevault/publishedmedia/m9yj4g160snutij4sivk/Scandic_Continental_Capitol_Terrace.jpg",
|
||||
"small": "https://test3.scandichotels.com/imagevault/publishedmedia/fqqakeunmamm3uetf47w/Scandic_Continental_Capitol_Terrace.jpg",
|
||||
"medium": "https://test3.scandichotels.com/imagevault/publishedmedia/6fp0avu8yjrh38zv1as8/Scandic_Continental_Capitol_Terrace.jpg",
|
||||
"large": "https://test3.scandichotels.com/imagevault/publishedmedia/j8dveyubybb4f7or9qay/Scandic_Continental_Capitol_Terrace.jpg"
|
||||
}
|
||||
},
|
||||
{
|
||||
"metaData": {
|
||||
"title": "Terrace",
|
||||
"altText": "Terrace",
|
||||
"altText_En": "Terrace",
|
||||
"copyRight": "Björn Enström"
|
||||
},
|
||||
"imageSizes": {
|
||||
"tiny": "https://test3.scandichotels.com/imagevault/publishedmedia/dsm1ief68fx4b6j5673x/Scandic_Continental_Capitol_Terrace.jpg",
|
||||
"small": "https://test3.scandichotels.com/imagevault/publishedmedia/pm1lxtm8zf1ejd0qtko4/Scandic_Continental_Capitol_Terrace.jpg",
|
||||
"medium": "https://test3.scandichotels.com/imagevault/publishedmedia/wwgkqa2fr5z223ybdemr/Scandic_Continental_Capitol_Terrace.jpg",
|
||||
"large": "https://test3.scandichotels.com/imagevault/publishedmedia/aztwaanmwiwk5s0tc36w/Scandic_Continental_Capitol_Terrace.jpg"
|
||||
}
|
||||
},
|
||||
{
|
||||
"metaData": {
|
||||
"title": "Rooftop terrace",
|
||||
"altText": "Rooftop terrace",
|
||||
"altText_En": "Rooftop terrace",
|
||||
"copyRight": "Karl Gabor"
|
||||
},
|
||||
"imageSizes": {
|
||||
"tiny": "https://test3.scandichotels.com/imagevault/publishedmedia/m4mhmwyynuf65ls1cybu/scandic-continental-rooftop-terrace-capital-4.jpg",
|
||||
"small": "https://test3.scandichotels.com/imagevault/publishedmedia/phsnpd122oq7e6hszsc7/scandic-continental-rooftop-terrace-capital-4.jpg",
|
||||
"medium": "https://test3.scandichotels.com/imagevault/publishedmedia/ty9gb8ogkq5iqszke7pe/scandic-continental-rooftop-terrace-capital-4.jpg",
|
||||
"large": "https://test3.scandichotels.com/imagevault/publishedmedia/ug0co0ks7v25avze1vi3/scandic-continental-rooftop-terrace-capital-4.jpg"
|
||||
}
|
||||
},
|
||||
{
|
||||
"metaData": {
|
||||
"title": "View",
|
||||
"altText": "View",
|
||||
"altText_En": "View",
|
||||
"copyRight": "Elin Strömberg © 2016"
|
||||
},
|
||||
"imageSizes": {
|
||||
"tiny": "https://test3.scandichotels.com/imagevault/publishedmedia/gs1lqt13ptscg4audxgo/scandic-continental-theview-detail-1.jpg",
|
||||
"small": "https://test3.scandichotels.com/imagevault/publishedmedia/otr9i3f4q8084ggfwnm1/scandic-continental-theview-detail-1.jpg",
|
||||
"medium": "https://test3.scandichotels.com/imagevault/publishedmedia/w8pkgf9igqibmlzp9egl/scandic-continental-theview-detail-1.jpg",
|
||||
"large": "https://test3.scandichotels.com/imagevault/publishedmedia/9yujqdk34x6juk6fstks/scandic-continental-theview-detail-1.jpg"
|
||||
}
|
||||
},
|
||||
{
|
||||
"metaData": {
|
||||
"title": "View",
|
||||
"altText": "View",
|
||||
"altText_En": "View",
|
||||
"copyRight": "Elin Strömberg © 2016"
|
||||
},
|
||||
"imageSizes": {
|
||||
"tiny": "https://test3.scandichotels.com/imagevault/publishedmedia/autuf074xvc2xr3zmxyf/scandic-continental-theview-detail-2.jpg",
|
||||
"small": "https://test3.scandichotels.com/imagevault/publishedmedia/hyf3fjkreqw2rlx4wm4e/scandic-continental-theview-detail-2.jpg",
|
||||
"medium": "https://test3.scandichotels.com/imagevault/publishedmedia/3wezw0ftkli48oloekdb/scandic-continental-theview-detail-2.jpg",
|
||||
"large": "https://test3.scandichotels.com/imagevault/publishedmedia/ygxz2q45b4gcgscqdzaw/scandic-continental-theview-detail-2.jpg"
|
||||
}
|
||||
},
|
||||
{
|
||||
"metaData": {
|
||||
"title": "Work out equipment at the terrace",
|
||||
"altText": "Work out equipment at the terrace",
|
||||
"altText_En": "Work out equipment at the terrace",
|
||||
"copyRight": "Scandic"
|
||||
},
|
||||
"imageSizes": {
|
||||
"tiny": "https://test3.scandichotels.com/imagevault/publishedmedia/oe32sgg1pudiwfd9hjpa/Scandic_Continental_Terrace_Workout_1.jpg",
|
||||
"small": "https://test3.scandichotels.com/imagevault/publishedmedia/1g9d62gqkyvfky85kgag/Scandic_Continental_Terrace_Workout_1.jpg",
|
||||
"medium": "https://test3.scandichotels.com/imagevault/publishedmedia/eb9n7j8s9scwg89ac4lk/Scandic_Continental_Terrace_Workout_1.jpg",
|
||||
"large": "https://test3.scandichotels.com/imagevault/publishedmedia/cyrtt6bmy11y1wti2d9h/Scandic_Continental_Terrace_Workout_1.jpg"
|
||||
}
|
||||
},
|
||||
{
|
||||
"metaData": {
|
||||
"title": "Meeting room",
|
||||
"altText": "",
|
||||
"altText_En": "",
|
||||
"copyRight": "Elin Sylwan © 2016"
|
||||
},
|
||||
"imageSizes": {
|
||||
"tiny": "https://test3.scandichotels.com/imagevault/publishedmedia/zbc3bhdzs59sqsvm9v29/scandic-continental-conference-9.jpg",
|
||||
"small": "https://test3.scandichotels.com/imagevault/publishedmedia/m2nh4f57ys3bcxuq27hb/scandic-continental-conference-9.jpg",
|
||||
"medium": "https://test3.scandichotels.com/imagevault/publishedmedia/wk39k84g3nrivntzuww2/scandic-continental-conference-9.jpg",
|
||||
"large": "https://test3.scandichotels.com/imagevault/publishedmedia/yn1mzw6nbyl90dreiz6b/scandic-continental-conference-9.jpg"
|
||||
}
|
||||
},
|
||||
{
|
||||
"metaData": {
|
||||
"title": "Meeting room",
|
||||
"altText": "",
|
||||
"altText_En": "",
|
||||
"copyRight": "Elin Sylwan © 2016"
|
||||
},
|
||||
"imageSizes": {
|
||||
"tiny": "https://test3.scandichotels.com/imagevault/publishedmedia/mfq1ayjplo7keqxm88ng/scandic-continental-conference-3.jpg",
|
||||
"small": "https://test3.scandichotels.com/imagevault/publishedmedia/wzjl4n8qk4imfgla7r73/scandic-continental-conference-3.jpg",
|
||||
"medium": "https://test3.scandichotels.com/imagevault/publishedmedia/wf6lg2u4bz1f9ntkszuz/scandic-continental-conference-3.jpg",
|
||||
"large": "https://test3.scandichotels.com/imagevault/publishedmedia/uh83sqrope55u6q4hbhl/scandic-continental-conference-3.jpg"
|
||||
}
|
||||
},
|
||||
{
|
||||
"metaData": {
|
||||
"title": "Meeting room ",
|
||||
"altText": "Meeting room ",
|
||||
"altText_En": "Meeting room ",
|
||||
"copyRight": "Karl Gabor"
|
||||
},
|
||||
"imageSizes": {
|
||||
"tiny": "https://test3.scandichotels.com/imagevault/publishedmedia/z00ei7j974hx7c9vp8a6/Scandic_Continental_meetingroom_15-16-17-18-0357.jpg",
|
||||
"small": "https://test3.scandichotels.com/imagevault/publishedmedia/v64n5ne3ayfo8v3q9x2s/Scandic_Continental_meetingroom_15-16-17-18-0357.jpg",
|
||||
"medium": "https://test3.scandichotels.com/imagevault/publishedmedia/mtx54l82hr3o3m7k0vqi/Scandic_Continental_meetingroom_15-16-17-18-0357.jpg",
|
||||
"large": "https://test3.scandichotels.com/imagevault/publishedmedia/3xr7ovgdjxq5taz8nzma/Scandic_Continental_meetingroom_15-16-17-18-0357.jpg"
|
||||
}
|
||||
},
|
||||
{
|
||||
"metaData": {
|
||||
"title": "Breakout area ",
|
||||
"altText": "Breakout area ",
|
||||
"altText_En": "Breakout area ",
|
||||
"copyRight": "Karl Gabor "
|
||||
},
|
||||
"imageSizes": {
|
||||
"tiny": "https://test3.scandichotels.com/imagevault/publishedmedia/e84l4tyafvmx6epe0c3d/Scandic_Continental_Breakout_3-0432.jpg",
|
||||
"small": "https://test3.scandichotels.com/imagevault/publishedmedia/60im9a4yre1kg5me9riz/Scandic_Continental_Breakout_3-0432.jpg",
|
||||
"medium": "https://test3.scandichotels.com/imagevault/publishedmedia/nsfh80un5l7qv9y9i1iw/Scandic_Continental_Breakout_3-0432.jpg",
|
||||
"large": "https://test3.scandichotels.com/imagevault/publishedmedia/pxe0dozr1joaqig3cbf2/Scandic_Continental_Breakout_3-0432.jpg"
|
||||
}
|
||||
},
|
||||
{
|
||||
"metaData": {
|
||||
"title": "Breakout area ",
|
||||
"altText": "Breakout area ",
|
||||
"altText_En": "Breakout area ",
|
||||
"copyRight": "Karl Gabor"
|
||||
},
|
||||
"imageSizes": {
|
||||
"tiny": "https://test3.scandichotels.com/imagevault/publishedmedia/fpyspgc9op7wb4ay3083/Scandic_Continental_breakout2-0391.jpg",
|
||||
"small": "https://test3.scandichotels.com/imagevault/publishedmedia/2ueu1qe77bdqkj682xvq/Scandic_Continental_breakout2-0391.jpg",
|
||||
"medium": "https://test3.scandichotels.com/imagevault/publishedmedia/xmaaxzi1qrlwkm2gbl0z/Scandic_Continental_breakout2-0391.jpg",
|
||||
"large": "https://test3.scandichotels.com/imagevault/publishedmedia/px12tarpgqq31cuz512z/Scandic_Continental_breakout2-0391.jpg"
|
||||
}
|
||||
},
|
||||
{
|
||||
"metaData": {
|
||||
"title": "Meeting room ",
|
||||
"altText": "Meeting room ",
|
||||
"altText_En": "Meeting room ",
|
||||
"copyRight": "Karl Gabor"
|
||||
},
|
||||
"imageSizes": {
|
||||
"tiny": "https://test3.scandichotels.com/imagevault/publishedmedia/ssrltn03vl7layrr5wgk/Scandic_Continental_Meetingroom_15_16_17_18.jpg",
|
||||
"small": "https://test3.scandichotels.com/imagevault/publishedmedia/jozqcjuo1dpd9gyqjcyo/Scandic_Continental_Meetingroom_15_16_17_18.jpg",
|
||||
"medium": "https://test3.scandichotels.com/imagevault/publishedmedia/vdinjqa1j09av33z6wss/Scandic_Continental_Meetingroom_15_16_17_18.jpg",
|
||||
"large": "https://test3.scandichotels.com/imagevault/publishedmedia/gq5lhbwrjvvo6otkgi9z/Scandic_Continental_Meetingroom_15_16_17_18.jpg"
|
||||
}
|
||||
},
|
||||
{
|
||||
"metaData": {
|
||||
"title": "Gym",
|
||||
"altText": "Gym of Scandic Continental in Stockholm",
|
||||
"altText_En": "Gym of Scandic Continental in Stockholm",
|
||||
"copyRight": "Elin Sylwan © 2016"
|
||||
},
|
||||
"imageSizes": {
|
||||
"tiny": "https://test3.scandichotels.com/imagevault/publishedmedia/ic6077yovsoxdmj9545r/scandic_continental_gym.jpg",
|
||||
"small": "https://test3.scandichotels.com/imagevault/publishedmedia/uhhqrnb8wzy9xlr42zn8/scandic_continental_gym.jpg",
|
||||
"medium": "https://test3.scandichotels.com/imagevault/publishedmedia/iqor5zd7owsll42z59vh/scandic_continental_gym.jpg",
|
||||
"large": "https://test3.scandichotels.com/imagevault/publishedmedia/5v5o0bbopf9t7r95sp2o/scandic_continental_gym.jpg"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"conferencesAndMeetings": {
|
||||
"headingText": "Meetings, conferences & events",
|
||||
"pageUrl": "https://test3.scandichotels.com/hotels/sweden/stockholm/scandic-continental/meetings-conferences-events",
|
||||
"heroImages": [
|
||||
{
|
||||
"metaData": {
|
||||
"title": "Roof top bar",
|
||||
"altText": "Roof bar of Scandic Continental in Stockholm",
|
||||
"altText_En": "Roof bar of Scandic Continental in Stockholm",
|
||||
"copyRight": "Scandic"
|
||||
},
|
||||
"imageSizes": {
|
||||
"tiny": "https://test3.scandichotels.com/imagevault/publishedmedia/086xasvs4lq29x4oqvbq/Scandic_Continental_Capitol_The_View_2.jpg",
|
||||
"small": "https://test3.scandichotels.com/imagevault/publishedmedia/rgrcj8ed9mhecp6hpcuq/Scandic_Continental_Capitol_The_View_2.jpg",
|
||||
"medium": "https://test3.scandichotels.com/imagevault/publishedmedia/gklysd2pkx9gi0nx1mqu/Scandic_Continental_Capitol_The_View_2.jpg",
|
||||
"large": "https://test3.scandichotels.com/imagevault/publishedmedia/sjkd7r6mw6xxhwzkkqvk/Scandic_Continental_Capitol_The_View_2.jpg"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"healthAndWellness": {
|
||||
"headingText": "Gym and health",
|
||||
"heroImages": [
|
||||
{
|
||||
"metaData": {
|
||||
"title": "Gym",
|
||||
"altText": "Gym of Scandic Continental in Stockholm",
|
||||
"altText_En": "Gym of Scandic Continental in Stockholm",
|
||||
"copyRight": "Elin Sylwan © 2016"
|
||||
},
|
||||
"imageSizes": {
|
||||
"tiny": "https://test3.scandichotels.com/imagevault/publishedmedia/ic6077yovsoxdmj9545r/scandic_continental_gym.jpg",
|
||||
"small": "https://test3.scandichotels.com/imagevault/publishedmedia/uhhqrnb8wzy9xlr42zn8/scandic_continental_gym.jpg",
|
||||
"medium": "https://test3.scandichotels.com/imagevault/publishedmedia/iqor5zd7owsll42z59vh/scandic_continental_gym.jpg",
|
||||
"large": "https://test3.scandichotels.com/imagevault/publishedmedia/5v5o0bbopf9t7r95sp2o/scandic_continental_gym.jpg"
|
||||
}
|
||||
},
|
||||
{
|
||||
"metaData": {
|
||||
"title": "Work out equipment at the terrace",
|
||||
"altText": "Work out equipment at the terrace",
|
||||
"altText_En": "Work out equipment at the terrace",
|
||||
"copyRight": "Scandic"
|
||||
},
|
||||
"imageSizes": {
|
||||
"tiny": "https://test3.scandichotels.com/imagevault/publishedmedia/oe32sgg1pudiwfd9hjpa/Scandic_Continental_Terrace_Workout_1.jpg",
|
||||
"small": "https://test3.scandichotels.com/imagevault/publishedmedia/1g9d62gqkyvfky85kgag/Scandic_Continental_Terrace_Workout_1.jpg",
|
||||
"medium": "https://test3.scandichotels.com/imagevault/publishedmedia/eb9n7j8s9scwg89ac4lk/Scandic_Continental_Terrace_Workout_1.jpg",
|
||||
"large": "https://test3.scandichotels.com/imagevault/publishedmedia/cyrtt6bmy11y1wti2d9h/Scandic_Continental_Terrace_Workout_1.jpg"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"accessibilityElevatorPitchText": "Find the information you might need, before visiting us. You are always welcome to our hotel - completely without barriers. Regardless of impairment, sight, hearing, allergies or wheelchair, we have made sure that you enjoy your stay.",
|
||||
"merchantInformationData": {
|
||||
"webMerchantId": "1110009031",
|
||||
"cards": {
|
||||
"americanExpress": true,
|
||||
"dankort": false,
|
||||
"dinersClub": true,
|
||||
"jcb": true,
|
||||
"masterCard": true,
|
||||
"visa": true,
|
||||
"maestro": false,
|
||||
"chinaUnionPay": true,
|
||||
"discover": true
|
||||
},
|
||||
"alternatePaymentOptions": {
|
||||
"swish": true,
|
||||
"vipps": false,
|
||||
"mobilePay": true,
|
||||
"applePay": true,
|
||||
"alipayPlus": false,
|
||||
"googlePay": true,
|
||||
"klarna": false,
|
||||
"payPal": false,
|
||||
"weChatPay": false
|
||||
}
|
||||
},
|
||||
"restaurantImages": {
|
||||
"headingText": "Bar and breakfast",
|
||||
"heroImages": [
|
||||
{
|
||||
"metaData": {
|
||||
"title": "Dining room, the Market",
|
||||
"altText": "Dining room, the Market",
|
||||
"altText_En": "Dining room, the Market",
|
||||
"copyRight": "Karl Gabor"
|
||||
},
|
||||
"imageSizes": {
|
||||
"tiny": "https://test3.scandichotels.com/imagevault/publishedmedia/bywlg4nwp11hmebfb1wg/scandic-continental-diningroom-themarket.jpg",
|
||||
"small": "https://test3.scandichotels.com/imagevault/publishedmedia/fdvp429hm1rfpg2le6jo/scandic-continental-diningroom-themarket.jpg",
|
||||
"medium": "https://test3.scandichotels.com/imagevault/publishedmedia/wm24ma9ngk8gijotm7va/scandic-continental-diningroom-themarket.jpg",
|
||||
"large": "https://test3.scandichotels.com/imagevault/publishedmedia/7xiz9p0f254yl9oef7bj/scandic-continental-diningroom-themarket.jpg"
|
||||
}
|
||||
},
|
||||
{
|
||||
"metaData": {
|
||||
"title": "Rooftop bar",
|
||||
"altText": "Rooftop bar",
|
||||
"altText_En": "Rooftop bar",
|
||||
"copyRight": "Karl Gabor"
|
||||
},
|
||||
"imageSizes": {
|
||||
"tiny": "https://test3.scandichotels.com/imagevault/publishedmedia/ntwor0b4i90sp9sh5qbj/scandic-continental-rooftopbar-capital.jpg",
|
||||
"small": "https://test3.scandichotels.com/imagevault/publishedmedia/ivxowx9eunbyz68qkstd/scandic-continental-rooftopbar-capital.jpg",
|
||||
"medium": "https://test3.scandichotels.com/imagevault/publishedmedia/6pi2262z6lo0un7x7iik/scandic-continental-rooftopbar-capital.jpg",
|
||||
"large": "https://test3.scandichotels.com/imagevault/publishedmedia/6rlwgo7lq198xg7ifii6/scandic-continental-rooftopbar-capital.jpg"
|
||||
}
|
||||
},
|
||||
{
|
||||
"metaData": {
|
||||
"title": "Terrace",
|
||||
"altText": "Bar of Scandic Continental in Stockholm",
|
||||
"altText_En": "Bar of Scandic Continental in Stockholm",
|
||||
"copyRight": "Björn Enström"
|
||||
},
|
||||
"imageSizes": {
|
||||
"tiny": "https://test3.scandichotels.com/imagevault/publishedmedia/69jl573jtmw8g4jjtugo/Scandic_Continental_Capitol_Terrace.jpg",
|
||||
"small": "https://test3.scandichotels.com/imagevault/publishedmedia/td5tz4ld37kwws6rpnye/Scandic_Continental_Capitol_Terrace.jpg",
|
||||
"medium": "https://test3.scandichotels.com/imagevault/publishedmedia/mlrdylyxf3m9fr5r9l7x/Scandic_Continental_Capitol_Terrace.jpg",
|
||||
"large": "https://test3.scandichotels.com/imagevault/publishedmedia/dywcmnaamp0altjge9qx/Scandic_Continental_Capitol_Terrace.jpg"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"relationships": {
|
||||
"restaurants": {
|
||||
"links": {
|
||||
"related": "http://tstapi.scandichotels.com/hotel/v1/Hotels/811/restaurants?language=En"
|
||||
}
|
||||
},
|
||||
"nearbyHotels": {
|
||||
"links": {
|
||||
"related": "http://tstapi.scandichotels.com/hotel/v1/Hotels/811/nearbyHotels?language=En"
|
||||
}
|
||||
},
|
||||
"roomCategories": {
|
||||
"links": {
|
||||
"related": "http://tstapi.scandichotels.com/hotel/v1/Hotels/811/roomCategories?language=En"
|
||||
}
|
||||
},
|
||||
"meetingRooms": {
|
||||
"links": {
|
||||
"related": "http://tstapi.scandichotels.com/hotel/v1/Hotels/811/meetingRooms?language=En"
|
||||
}
|
||||
},
|
||||
"merchantInformation": {
|
||||
"links": {
|
||||
"related": "http://tstapi.scandichotels.com/hotel/v1/Hotels/811/merchantInformation?language=En"
|
||||
}
|
||||
}
|
||||
},
|
||||
"id": "811",
|
||||
"language": "En",
|
||||
"type": "hotels"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ export async function fetchServiceToken(
|
||||
grant_type: "client_credentials",
|
||||
client_id: env.CURITY_CLIENT_ID_SERVICE,
|
||||
client_secret: env.CURITY_CLIENT_SECRET_SERVICE,
|
||||
scope: scopes.join(","),
|
||||
scope: scopes.join(" "),
|
||||
}),
|
||||
next: {
|
||||
revalidate: SERVICE_TOKEN_REVALIDATE_SECONDS,
|
||||
|
||||
@@ -2,4 +2,14 @@ import { z } from "zod"
|
||||
|
||||
import { bookingWidgetSchema } from "@/components/Forms/BookingWidget/schema"
|
||||
|
||||
import type { Locations } from "@/types/trpc/routers/hotel/locations"
|
||||
|
||||
export type BookingWidgetSchema = z.output<typeof bookingWidgetSchema>
|
||||
|
||||
export interface BookingWidgetClientProps {
|
||||
locations: Locations
|
||||
}
|
||||
|
||||
export interface BookingWidgetToggleButtonProps {
|
||||
openMobileSearch: () => void
|
||||
}
|
||||
|
||||
@@ -1,3 +1,17 @@
|
||||
import { Lang } from "@/constants/languages"
|
||||
|
||||
import type { Locale } from "date-fns"
|
||||
import type { DateRange } from "react-day-picker"
|
||||
|
||||
export interface DatePickerFormProps {
|
||||
name?: string
|
||||
}
|
||||
|
||||
type LangWithoutEn = Lang.da | Lang.de | Lang.fi | Lang.no | Lang.sv
|
||||
|
||||
export interface DatePickerProps {
|
||||
close: () => void
|
||||
handleOnSelect: (selected: DateRange) => void
|
||||
locales: Record<LangWithoutEn, Locale>
|
||||
selectedDate: DateRange
|
||||
}
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
import { ReactElement } from "react"
|
||||
|
||||
import type { Coordinates } from "@/types/components/maps/coordinates"
|
||||
import type { PointOfInterest } from "@/types/hotel"
|
||||
|
||||
export interface MapContentProps {
|
||||
export interface InteractiveMapProps {
|
||||
coordinates: Coordinates
|
||||
pointsOfInterest: PointOfInterest[]
|
||||
activePoi: PointOfInterest["name"] | null
|
||||
mapId: string
|
||||
onActivePoiChange: (poi: PointOfInterest["name"] | null) => void
|
||||
closeButton: ReactElement
|
||||
}
|
||||
@@ -3,3 +3,11 @@ import { Hotel } from "@/types/hotel"
|
||||
export type HotelFiltersProps = {
|
||||
filters: Hotel["detailedFacilities"]
|
||||
}
|
||||
|
||||
export type Filter = {
|
||||
name: string
|
||||
id: number
|
||||
public: boolean
|
||||
sortOrder: number
|
||||
filter?: string
|
||||
}
|
||||
15
types/components/hotelReservation/selectHotel/map.ts
Normal file
15
types/components/hotelReservation/selectHotel/map.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { Coordinates } from "@/types/components/maps/coordinates"
|
||||
import type { PointOfInterest } from "@/types/hotel"
|
||||
|
||||
export interface HotelListingProps {
|
||||
// pointsOfInterest: PointOfInterest[]
|
||||
// activePoi: PointOfInterest["name"] | null
|
||||
// onActivePoiChange: (poi: PointOfInterest["name"] | null) => void
|
||||
}
|
||||
|
||||
export interface SelectHotelMapProps {
|
||||
apiKey: string
|
||||
coordinates: Coordinates
|
||||
pointsOfInterest: PointOfInterest[]
|
||||
mapId: string
|
||||
}
|
||||
@@ -1,4 +1,25 @@
|
||||
import { Hotel, ParkingData } from "@/types/hotel"
|
||||
|
||||
export enum AvailabilityEnum {
|
||||
Available = "Available",
|
||||
NotAvailable = "NotAvailable",
|
||||
}
|
||||
|
||||
export interface DetailedAmenity {
|
||||
name: string
|
||||
heading: string
|
||||
description: string
|
||||
}
|
||||
|
||||
export interface ReadMoreProps {
|
||||
hotelId: string
|
||||
hotel: Hotel
|
||||
}
|
||||
|
||||
export interface ContactProps {
|
||||
hotel: Hotel
|
||||
}
|
||||
|
||||
export interface ParkingProps {
|
||||
parking: ParkingData
|
||||
}
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
import { Hotel } from "@/types/hotel"
|
||||
|
||||
export type HotelSelectionHeaderProps = {
|
||||
hotel: Hotel
|
||||
}
|
||||
@@ -17,6 +17,7 @@ export interface LanguageSwitcherProps {
|
||||
|
||||
export interface LanguageSwitcherContentProps {
|
||||
urls: LanguageSwitcherData
|
||||
onLanguageSwitch: () => void
|
||||
}
|
||||
|
||||
export interface LanguageSwitcherContainerProps {
|
||||
|
||||
@@ -2,6 +2,7 @@ import { z } from "zod"
|
||||
|
||||
import {
|
||||
getHotelDataSchema,
|
||||
parkingSchema,
|
||||
pointOfInterestSchema,
|
||||
roomSchema,
|
||||
} from "@/server/routers/hotels/output"
|
||||
@@ -49,3 +50,5 @@ export enum PointOfInterestGroupEnum {
|
||||
PARKING = "Parking",
|
||||
SHOPPING_DINING = "Shopping & Dining",
|
||||
}
|
||||
|
||||
export type ParkingData = z.infer<typeof parkingSchema>
|
||||
|
||||
@@ -1,53 +1,21 @@
|
||||
import type { ButtonProps } from "@/components/TempDesignSystem/Button/button"
|
||||
import type { CardProps } from "@/components/TempDesignSystem/Card/card"
|
||||
import type { LinkProps } from "@/components/TempDesignSystem/Link/link"
|
||||
|
||||
export function getTheme(theme: CardProps["theme"]) {
|
||||
let buttonTheme: ButtonProps["theme"] = "primaryLight"
|
||||
let primaryLinkColor: LinkProps["color"] = "pale"
|
||||
let secondaryLinkColor: LinkProps["color"] = "burgundy"
|
||||
|
||||
export function getTheme(theme: CardProps["theme"]): ButtonProps["theme"] {
|
||||
switch (theme) {
|
||||
case "one":
|
||||
buttonTheme = "primaryLight"
|
||||
primaryLinkColor = "pale"
|
||||
secondaryLinkColor = "burgundy"
|
||||
break
|
||||
case "two":
|
||||
buttonTheme = "secondaryLight"
|
||||
primaryLinkColor = "pale"
|
||||
secondaryLinkColor = "burgundy"
|
||||
break
|
||||
return "secondaryLight"
|
||||
case "three":
|
||||
buttonTheme = "tertiaryLight"
|
||||
primaryLinkColor = "pale"
|
||||
secondaryLinkColor = "burgundy"
|
||||
break
|
||||
return "tertiaryLight"
|
||||
case "primaryDark":
|
||||
buttonTheme = "primaryDark"
|
||||
primaryLinkColor = "burgundy"
|
||||
secondaryLinkColor = "pale"
|
||||
break
|
||||
case "primaryDim":
|
||||
buttonTheme = "primaryLight"
|
||||
primaryLinkColor = "pale"
|
||||
secondaryLinkColor = "burgundy"
|
||||
break
|
||||
case "primaryInverted":
|
||||
buttonTheme = "primaryLight"
|
||||
primaryLinkColor = "pale"
|
||||
secondaryLinkColor = "burgundy"
|
||||
break
|
||||
return "primaryDark"
|
||||
case "primaryStrong":
|
||||
case "image":
|
||||
buttonTheme = "primaryStrong"
|
||||
primaryLinkColor = "red"
|
||||
secondaryLinkColor = "white"
|
||||
}
|
||||
|
||||
return {
|
||||
buttonTheme: buttonTheme,
|
||||
primaryLinkColor: primaryLinkColor,
|
||||
secondaryLinkColor: secondaryLinkColor,
|
||||
return "primaryStrong"
|
||||
case "one":
|
||||
case "primaryDim":
|
||||
case "primaryInverted":
|
||||
default:
|
||||
return "primaryLight"
|
||||
}
|
||||
}
|
||||
|
||||
10
utils/debounce.ts
Normal file
10
utils/debounce.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
export function debounce(func: Function, delay = 300) {
|
||||
let debounceTimer: ReturnType<typeof setTimeout>
|
||||
return function () {
|
||||
// @ts-expect-error this in TypeScript
|
||||
const context = this
|
||||
const args = arguments
|
||||
clearTimeout(debounceTimer)
|
||||
debounceTimer = setTimeout(() => func.apply(context, args), delay)
|
||||
}
|
||||
}
|
||||
@@ -14,8 +14,14 @@ export function insertResponseToImageVaultAsset(
|
||||
meta.Name.includes("Title_")
|
||||
)?.Value
|
||||
|
||||
const mediaConversion = response.MediaConversions[0]
|
||||
const aspectRatio =
|
||||
mediaConversion.FormatAspectRatio ||
|
||||
mediaConversion.AspectRatio ||
|
||||
mediaConversion.Width / mediaConversion.Height
|
||||
|
||||
return {
|
||||
url: response.MediaConversions[0].Url,
|
||||
url: mediaConversion.Url,
|
||||
id: response.Id,
|
||||
meta: {
|
||||
alt,
|
||||
@@ -23,9 +29,9 @@ export function insertResponseToImageVaultAsset(
|
||||
},
|
||||
title: response.Name,
|
||||
dimensions: {
|
||||
width: response.MediaConversions[0].Width,
|
||||
height: response.MediaConversions[0].Height,
|
||||
aspectRatio: response.MediaConversions[0].FormatAspectRatio,
|
||||
width: mediaConversion.Width,
|
||||
height: mediaConversion.Height,
|
||||
aspectRatio,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user