fix(i18n): prepare for Lokalise
This commit is contained in:
@@ -5,11 +5,13 @@ import { overview } from "@/constants/routes/myPages"
|
||||
import { getProfile } from "@/lib/trpc/memoizedRequests"
|
||||
|
||||
import { auth } from "@/auth"
|
||||
import { getIntl } from "@/i18n"
|
||||
import { getLang } from "@/i18n/serverContext"
|
||||
|
||||
export default async function ProtectedLayout({
|
||||
children,
|
||||
}: React.PropsWithChildren) {
|
||||
const intl = await getIntl()
|
||||
const session = await auth()
|
||||
/**
|
||||
* Fallback to make sure every route nested in the
|
||||
@@ -54,7 +56,7 @@ export default async function ProtectedLayout({
|
||||
console.error(`[layout:protected] unhandled user loading error`)
|
||||
break
|
||||
}
|
||||
return <p>Something went wrong!</p>
|
||||
return <p>{intl.formatMessage({ id: "Something went wrong!" })}</p>
|
||||
}
|
||||
|
||||
if (!user) {
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
"use client"
|
||||
|
||||
import * as Sentry from "@sentry/nextjs"
|
||||
import { useEffect } from "react"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
export default function Error({
|
||||
error,
|
||||
}: {
|
||||
error: Error & { digest?: string }
|
||||
}) {
|
||||
const intl = useIntl()
|
||||
|
||||
useEffect(() => {
|
||||
if (!error) return
|
||||
|
||||
@@ -17,7 +21,12 @@ export default function Error({
|
||||
return (
|
||||
<p>
|
||||
<strong>
|
||||
Breadcrumbs failed for this page ({error.digest}@{Date.now()})
|
||||
{intl.formatMessage(
|
||||
{ id: "Breadcrumbs failed for this page ({errorId})" },
|
||||
{
|
||||
errorId: `${error.digest}@${Date.now()}`,
|
||||
}
|
||||
)}
|
||||
</strong>
|
||||
</p>
|
||||
)
|
||||
|
||||
@@ -2,12 +2,15 @@
|
||||
|
||||
import * as Sentry from "@sentry/nextjs"
|
||||
import { useEffect } from "react"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
export default function Error({
|
||||
error,
|
||||
}: {
|
||||
error: Error & { digest?: string }
|
||||
}) {
|
||||
const intl = useIntl()
|
||||
|
||||
useEffect(() => {
|
||||
if (!error) return
|
||||
|
||||
@@ -18,7 +21,12 @@ export default function Error({
|
||||
return (
|
||||
<p>
|
||||
<strong>
|
||||
Error loading this page ({error.digest}@{Date.now()})
|
||||
{intl.formatMessage(
|
||||
{ id: "Error loading this page ({errorId})" },
|
||||
{
|
||||
errorId: `${error.digest}@${Date.now()}`,
|
||||
}
|
||||
)}
|
||||
</strong>
|
||||
</p>
|
||||
)
|
||||
|
||||
@@ -30,12 +30,43 @@ export default async function MembershipCardSlot({
|
||||
membershipCards.map((card, idx) => (
|
||||
<div className={styles.card} key={idx}>
|
||||
<Subtitle className={styles.subTitle}>
|
||||
Name: {card.membershipType}
|
||||
{intl.formatMessage(
|
||||
{ id: "Name: {cardMembershipType}" },
|
||||
{
|
||||
cardMembershipType: card.membershipType,
|
||||
}
|
||||
)}
|
||||
</Subtitle>
|
||||
<span> Current Points: {card.currentPoints} </span>
|
||||
<span> Member Since: {card.memberSince}</span>
|
||||
<span> Number: {card.membershipNumber}</span>
|
||||
<span>Expiration Date: {card.expirationDate.split("T")[0]}</span>
|
||||
<span>
|
||||
{intl.formatMessage(
|
||||
{ id: "Current Points {points, number}" },
|
||||
{ points: card.currentPoints }
|
||||
)}
|
||||
</span>
|
||||
<span>
|
||||
{intl.formatMessage(
|
||||
{ id: "Member Since: {value}" },
|
||||
{
|
||||
value: card.memberSince,
|
||||
}
|
||||
)}
|
||||
</span>
|
||||
<span>
|
||||
{intl.formatMessage(
|
||||
{ id: "Number: {membershipNumber}" },
|
||||
{
|
||||
membershipNumber: card.membershipNumber,
|
||||
}
|
||||
)}
|
||||
</span>
|
||||
<span>
|
||||
{intl.formatMessage(
|
||||
{ id: "Expiration Date: {expirationDate}" },
|
||||
{
|
||||
expirationDate: card.expirationDate.split("T")[0],
|
||||
}
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
<Link href="#" variant="icon">
|
||||
|
||||
@@ -2,10 +2,13 @@
|
||||
|
||||
import * as Sentry from "@sentry/nextjs"
|
||||
import { useEffect } from "react"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import type { ErrorPage } from "@/types/next/error"
|
||||
|
||||
export default function ProfileError({ error }: ErrorPage) {
|
||||
const intl = useIntl()
|
||||
|
||||
useEffect(() => {
|
||||
if (!error) return
|
||||
|
||||
@@ -13,5 +16,5 @@ export default function ProfileError({ error }: ErrorPage) {
|
||||
Sentry.captureException(error)
|
||||
}, [error])
|
||||
|
||||
return <h1>Error happened, Profile</h1>
|
||||
return <h1>{intl.formatMessage({ id: "Error happened, Profile" })}</h1>
|
||||
}
|
||||
|
||||
@@ -30,6 +30,24 @@ export default async function Profile({ params }: PageArgs<LangParams>) {
|
||||
return null
|
||||
}
|
||||
|
||||
const addressParts = []
|
||||
if (user.address.streetAddress) {
|
||||
addressParts.push(user.address.streetAddress)
|
||||
}
|
||||
|
||||
if (user.address.city) {
|
||||
addressParts.push(user.address.city)
|
||||
}
|
||||
|
||||
if (user.address.country) {
|
||||
addressParts.push(user.address.country)
|
||||
}
|
||||
|
||||
const addressOutput =
|
||||
addressParts.length > 0
|
||||
? addressParts.join(", ")
|
||||
: intl.formatMessage({ id: "N/A" })
|
||||
|
||||
const defaultLanguage = languages[params.lang]
|
||||
const language = languageSelect.find((l) => l.value === user.language)
|
||||
return (
|
||||
@@ -90,18 +108,7 @@ export default async function Profile({ params }: PageArgs<LangParams>) {
|
||||
<Body color="burgundy" textTransform="bold">
|
||||
{intl.formatMessage({ id: "Address" })}
|
||||
</Body>
|
||||
<Body color="burgundy">
|
||||
{user.address.streetAddress
|
||||
? `${user.address.streetAddress}, `
|
||||
: ""}
|
||||
{user.address.city ? `${user.address.city}, ` : ""}
|
||||
{user.address.country ? `${user.address.country}` : ""}
|
||||
{!user.address.streetAddress &&
|
||||
!user.address.city &&
|
||||
!user.address.country
|
||||
? "N/A"
|
||||
: null}
|
||||
</Body>
|
||||
<Body color="burgundy">{addressOutput}</Body>
|
||||
</div>
|
||||
<div className={styles.item}>
|
||||
<LockIcon color="burgundy" />
|
||||
|
||||
@@ -1,19 +1,13 @@
|
||||
"use client" // Error components must be Client Components
|
||||
"use client"
|
||||
|
||||
import * as Sentry from "@sentry/nextjs"
|
||||
import {
|
||||
useParams,
|
||||
usePathname,
|
||||
useRouter,
|
||||
useSearchParams,
|
||||
} from "next/navigation"
|
||||
import { useParams, useRouter, useSearchParams } from "next/navigation"
|
||||
import { startTransition, useEffect, useRef } from "react"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import { login } from "@/constants/routes/handleAuth"
|
||||
import { SESSION_EXPIRED } from "@/server/errors/trpc"
|
||||
|
||||
import { findLang } from "@/utils/languages"
|
||||
|
||||
import styles from "./error.module.css"
|
||||
|
||||
import type { LangParams } from "@/types/params"
|
||||
@@ -63,13 +57,10 @@ export default function Error({
|
||||
currentSearchParamsRef.current = currentSearchParams
|
||||
}, [searchParams, reset, router])
|
||||
|
||||
const pathname = usePathname()
|
||||
const lang = findLang(pathname)
|
||||
|
||||
return (
|
||||
<section className={styles.layout}>
|
||||
<div className={styles.content}>
|
||||
{lang}: {intl.formatMessage({ id: "Something went wrong!" })}
|
||||
{intl.formatMessage({ id: "Something went wrong!" })}
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
|
||||
@@ -1,17 +1,26 @@
|
||||
import { getIntl } from "@/i18n"
|
||||
import { setLang } from "@/i18n/serverContext"
|
||||
|
||||
import styles from "./page.module.css"
|
||||
|
||||
import type { LangParams, LayoutArgs, StatusParams } from "@/types/params"
|
||||
|
||||
export default function MiddlewareError({
|
||||
export default async function MiddlewareError({
|
||||
params,
|
||||
}: LayoutArgs<LangParams & StatusParams>) {
|
||||
setLang(params.lang)
|
||||
|
||||
const intl = await getIntl()
|
||||
|
||||
return (
|
||||
<div className={styles.layout}>
|
||||
Middleware Error {params.lang}: {params.status}
|
||||
{intl.formatMessage(
|
||||
{ id: "Middleware error {lang}: {status}" },
|
||||
{
|
||||
lang: params.lang,
|
||||
status: params.status,
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import { getProfile } from "@/lib/trpc/memoizedRequests"
|
||||
|
||||
import AccountPage from "@/components/Webviews/AccountPage"
|
||||
import LoyaltyPage from "@/components/Webviews/LoyaltyPage"
|
||||
import { getIntl } from "@/i18n"
|
||||
import { setLang } from "@/i18n/serverContext"
|
||||
|
||||
import type {
|
||||
@@ -18,11 +19,12 @@ export default async function ContentTypePage({
|
||||
params,
|
||||
}: PageArgs<LangParams & ContentTypeWebviewParams & UIDParams, {}>) {
|
||||
setLang(params.lang)
|
||||
const intl = await getIntl()
|
||||
const user = await getProfile()
|
||||
|
||||
if (!user) {
|
||||
console.log(`[webview:page] unable to load user`)
|
||||
return <p>Error: No user could be loaded</p>
|
||||
return <p>{intl.formatMessage({ id: "Error: No user could be loaded" })}</p>
|
||||
}
|
||||
|
||||
if ("error" in user) {
|
||||
@@ -36,9 +38,13 @@ export default async function ContentTypePage({
|
||||
console.log(`[webview:page] user error, redirecting to: ${redirectURL}`)
|
||||
redirect(redirectURL)
|
||||
case "notfound":
|
||||
return <p>Error: user not found</p>
|
||||
return <p>{intl.formatMessage({ id: "Error: user not found" })}</p>
|
||||
case "unknown":
|
||||
return <p>Unknown error occurred loading user</p>
|
||||
return (
|
||||
<p>
|
||||
{intl.formatMessage({ id: "Unknown error occurred loading user" })}
|
||||
</p>
|
||||
)
|
||||
default:
|
||||
const u: never = user
|
||||
console.log(`[webview:page] unhandled user loading error`)
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import * as Sentry from "@sentry/nextjs"
|
||||
import { useEffect } from "react"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import styles from "./global-error.module.css"
|
||||
|
||||
@@ -12,6 +13,8 @@ export default function GlobalError({
|
||||
}) {
|
||||
console.log({ global_error: error })
|
||||
|
||||
const intl = useIntl()
|
||||
|
||||
useEffect(() => {
|
||||
Sentry.captureException(error)
|
||||
}, [error])
|
||||
@@ -20,7 +23,7 @@ export default function GlobalError({
|
||||
<html>
|
||||
<body>
|
||||
<div className={styles.layout}>
|
||||
<h1>Something went really wrong!</h1>
|
||||
<h1>{intl.formatMessage({ id: "Something went really wrong!" })}</h1>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import JsonToHtml from "@/components/JsonToHtml"
|
||||
import SectionContainer from "@/components/Section/Container"
|
||||
@@ -14,6 +16,8 @@ import type { AccordionProps } from "@/types/components/blocks/Accordion"
|
||||
import { HotelHashValues } from "@/types/components/hotelPage/tabNavigation"
|
||||
|
||||
export default function AccordionSection({ accordion, title }: AccordionProps) {
|
||||
const intl = useIntl()
|
||||
|
||||
const showToggleButton = accordion.length > 5
|
||||
const [allAccordionsVisible, setAllAccordionsVisible] =
|
||||
useState(!showToggleButton)
|
||||
@@ -44,8 +48,12 @@ export default function AccordionSection({ accordion, title }: AccordionProps) {
|
||||
<ShowMoreButton
|
||||
loadMoreData={toggleAccordions}
|
||||
showLess={allAccordionsVisible}
|
||||
textShowMore="See all FAQ"
|
||||
textShowLess="See less FAQ"
|
||||
textShowMore={intl.formatMessage({
|
||||
id: "See all FAQ",
|
||||
})}
|
||||
textShowLess={intl.formatMessage({
|
||||
id: "See less FAQ",
|
||||
})}
|
||||
/>
|
||||
) : null}
|
||||
</SectionContainer>
|
||||
|
||||
@@ -6,7 +6,6 @@ import BiroScript from "@/components/TempDesignSystem/Text/BiroScript"
|
||||
import Caption from "@/components/TempDesignSystem/Text/Caption"
|
||||
import Title from "@/components/TempDesignSystem/Text/Title"
|
||||
import { getIntl } from "@/i18n"
|
||||
import { getLang } from "@/i18n/serverContext"
|
||||
|
||||
import SectionWrapper from "../SectionWrapper"
|
||||
|
||||
@@ -35,9 +34,25 @@ export default async function LoyaltyLevels({
|
||||
}
|
||||
|
||||
async function LevelCard({ level }: LevelCardProps) {
|
||||
const lang = getLang()
|
||||
const intl = await getIntl()
|
||||
const pointsString = `${level.required_points.toLocaleString(lang)} ${intl.formatMessage({ id: "points" })} `
|
||||
|
||||
let pointsMsg: React.ReactNode = intl.formatMessage(
|
||||
{ id: "{pointsAmount, number} points" },
|
||||
{ pointsAmount: level.required_points }
|
||||
)
|
||||
|
||||
if (level.required_nights) {
|
||||
pointsMsg = intl.formatMessage<React.ReactNode>(
|
||||
{
|
||||
id: "{pointsAmount, number} points <highlight>or {nightsAmount, number} nights</highlight>",
|
||||
},
|
||||
{
|
||||
pointsAmount: level.required_points,
|
||||
nightsAmount: level.required_nights,
|
||||
highlight: (str) => <span className={styles.redText}>{str}</span>,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<article className={styles.card}>
|
||||
@@ -47,18 +62,15 @@ async function LevelCard({ level }: LevelCardProps) {
|
||||
color="primaryLightOnSurfaceAccent"
|
||||
tilted="large"
|
||||
>
|
||||
{intl.formatMessage({ id: "Level" })} {level.user_facing_tag}
|
||||
{intl.formatMessage(
|
||||
{ id: "Level {level}" },
|
||||
{ level: level.user_facing_tag }
|
||||
)}
|
||||
</BiroScript>
|
||||
<MembershipLevelIcon level={level.level_id} color="red" />
|
||||
</header>
|
||||
<Title textAlign="center" level="h5">
|
||||
{pointsString}
|
||||
{level.required_nights ? (
|
||||
<span className={styles.redText}>
|
||||
{intl.formatMessage({ id: "or" })} {level.required_nights}{" "}
|
||||
{intl.formatMessage({ id: "nights" })}
|
||||
</span>
|
||||
) : null}
|
||||
{pointsMsg}
|
||||
</Title>
|
||||
<div className={styles.textContainer}>
|
||||
{level.rewards.map((reward) => (
|
||||
|
||||
@@ -19,14 +19,15 @@ export default async function MembershipNumber({
|
||||
return (
|
||||
<div className={classNames}>
|
||||
<Caption color="pale">
|
||||
{intl.formatMessage({ id: "Membership ID" })}
|
||||
{": "}
|
||||
{intl.formatMessage({ id: "Membership ID: " })}
|
||||
</Caption>
|
||||
<span className={styles.icon}>
|
||||
<Caption className={styles.icon} color="pale" asChild>
|
||||
<code>{membership?.membershipNumber ?? "N/A"}</code>
|
||||
<code>
|
||||
{membership?.membershipNumber ?? intl.formatMessage({ id: "N/A" })}
|
||||
</code>
|
||||
</Caption>
|
||||
{membership && (
|
||||
{membership?.membershipNumber && (
|
||||
<CopyButton membershipNumber={membership.membershipNumber} />
|
||||
)}
|
||||
</span>
|
||||
|
||||
@@ -24,15 +24,18 @@ export default async function Friend({
|
||||
}
|
||||
const isHighestLevel = isHighestMembership(membership.membershipLevel)
|
||||
|
||||
const lvlMessageHighest = intl.formatMessage({ id: "Highest level" })
|
||||
|
||||
const lvlMessageLevel = intl.formatMessage(
|
||||
{ id: "Level {level}" },
|
||||
{ level: membershipLevels[membership.membershipLevel] }
|
||||
)
|
||||
|
||||
return (
|
||||
<section className={styles.friend}>
|
||||
<header className={styles.header}>
|
||||
<Body color="white" textTransform="bold" textAlign="center">
|
||||
{intl.formatMessage(
|
||||
isHighestLevel
|
||||
? { id: "Highest floor" }
|
||||
: { id: `Level ${membershipLevels[membership.membershipLevel]}` }
|
||||
)}
|
||||
{isHighestLevel ? lvlMessageHighest : lvlMessageLevel}
|
||||
</Body>
|
||||
<MembershipLevelIcon
|
||||
level={MembershipLevelEnum[membership.membershipLevel]}
|
||||
|
||||
@@ -24,7 +24,7 @@ export default async function ExpiringPoints({ user }: UserProps) {
|
||||
<section>
|
||||
<Body color="white" textTransform="bold" textAlign="center">
|
||||
{intl.formatMessage(
|
||||
{ id: "spendable points expiring by" },
|
||||
{ id: "{points} spendable points expiring by {date}" },
|
||||
{
|
||||
points: intl.formatNumber(membership.pointsToExpire),
|
||||
date: d.format(dateFormat),
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import useLang from "@/hooks/useLang"
|
||||
|
||||
import styles from "./levelSummary.module.css"
|
||||
|
||||
import type { LevelSummaryProps } from "@/types/components/overviewTable"
|
||||
@@ -10,17 +8,29 @@ export default function LevelSummary({
|
||||
level,
|
||||
showDescription = true,
|
||||
}: LevelSummaryProps) {
|
||||
const lang = useLang()
|
||||
const intl = useIntl()
|
||||
|
||||
let pointsMsg: React.ReactNode = intl.formatMessage(
|
||||
{ id: "{pointsAmount, number} points" },
|
||||
{ pointsAmount: level.required_points }
|
||||
)
|
||||
|
||||
if (level.required_nights) {
|
||||
pointsMsg = intl.formatMessage<React.ReactNode>(
|
||||
{
|
||||
id: "{pointsAmount, number} points or {nightsAmount, number} nights",
|
||||
},
|
||||
{
|
||||
pointsAmount: level.required_points,
|
||||
nightsAmount: level.required_nights,
|
||||
highlight: (str) => <span className={styles.redText}>{str}</span>,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.levelSummary}>
|
||||
<span className={styles.levelRequirements}>
|
||||
{level.required_points.toLocaleString(lang)}
|
||||
{"p "}
|
||||
{level.required_nights
|
||||
? `${intl.formatMessage({ id: "or" })} ${level.required_nights} ${intl.formatMessage({ id: "nights" })}`
|
||||
: ""}
|
||||
</span>
|
||||
<span className={styles.levelRequirements}>{pointsMsg}</span>
|
||||
{showDescription && (
|
||||
<p className={styles.levelSummaryText}>{level.description}</p>
|
||||
)}
|
||||
|
||||
@@ -22,12 +22,19 @@ export default function Row({ transaction }: RowProps) {
|
||||
const pathName = usePathname()
|
||||
const isWebview = webviews.includes(pathName)
|
||||
|
||||
const nightString = `${transaction.nights} ${transaction.nights === 1 ? intl.formatMessage({ id: "night" }) : intl.formatMessage({ id: "nights" })}`
|
||||
const nightsMsg = intl.formatMessage(
|
||||
{
|
||||
id: "{nightsAmount, plural, one {# night} other {# nights}}",
|
||||
},
|
||||
{
|
||||
nightsAmount: transaction.nights,
|
||||
}
|
||||
)
|
||||
|
||||
let description =
|
||||
transaction.hotelName && transaction.city
|
||||
? `${transaction.hotelName}, ${transaction.city} ${nightString}`
|
||||
: `${nightString}`
|
||||
? `${transaction.hotelName}, ${transaction.city} ${nightsMsg}`
|
||||
: `${nightsMsg}`
|
||||
|
||||
switch (transaction.type) {
|
||||
case Transactions.rewardType.stay:
|
||||
|
||||
@@ -11,16 +11,16 @@ import styles from "./clientTable.module.css"
|
||||
|
||||
import type { ClientTableProps } from "@/types/components/myPages/myPage/earnAndBurn"
|
||||
|
||||
const tableHeadings = [
|
||||
"Points",
|
||||
"Description",
|
||||
"Booking number",
|
||||
"Arrival date",
|
||||
]
|
||||
|
||||
export default function ClientTable({ transactions }: ClientTableProps) {
|
||||
const intl = useIntl()
|
||||
|
||||
const tableHeadings = [
|
||||
intl.formatMessage({ id: "Points" }),
|
||||
intl.formatMessage({ id: "Description" }),
|
||||
intl.formatMessage({ id: "Booking number" }),
|
||||
intl.formatMessage({ id: "Arrival date" }),
|
||||
]
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<Table>
|
||||
@@ -28,9 +28,7 @@ export default function ClientTable({ transactions }: ClientTableProps) {
|
||||
<Table.TR>
|
||||
{tableHeadings.map((heading) => (
|
||||
<Table.TH key={heading}>
|
||||
<Body textTransform="bold">
|
||||
{intl.formatMessage({ id: heading })}
|
||||
</Body>
|
||||
<Body textTransform="bold">{heading}</Body>
|
||||
</Table.TH>
|
||||
))}
|
||||
</Table.TR>
|
||||
|
||||
@@ -10,8 +10,6 @@ import useLang from "@/hooks/useLang"
|
||||
|
||||
import AwardPoints from "../../EarnAndBurn/AwardPoints"
|
||||
|
||||
const tableHeadings = ["Points", "Expiration Date"]
|
||||
|
||||
export default function ExpiringPointsTable({
|
||||
points,
|
||||
expirationDate,
|
||||
@@ -23,15 +21,18 @@ export default function ExpiringPointsTable({
|
||||
const lang = useLang()
|
||||
const expiration = dt(expirationDate).locale(lang).format("DD MMM YYYY")
|
||||
|
||||
const tableHeadings = [
|
||||
intl.formatMessage({ id: "Points" }),
|
||||
intl.formatMessage({ id: "Expiration Date" }),
|
||||
]
|
||||
|
||||
return (
|
||||
<Table>
|
||||
<Table.THead>
|
||||
<Table.TR>
|
||||
{tableHeadings.map((heading) => (
|
||||
<Table.TH key={heading}>
|
||||
<Body textTransform="bold">
|
||||
{intl.formatMessage({ id: heading })}
|
||||
</Body>
|
||||
<Body textTransform="bold">{heading}</Body>
|
||||
</Table.TH>
|
||||
))}
|
||||
</Table.TR>
|
||||
|
||||
@@ -52,7 +52,7 @@ export default async function NextLevelRewardsBlock({
|
||||
<div className={styles.textContainer}>
|
||||
<Body color="peach50" textAlign="center">
|
||||
{intl.formatMessage(
|
||||
{ id: "As our" },
|
||||
{ id: "As our {level}" },
|
||||
{ level: nextLevelRewards.level?.name }
|
||||
)}
|
||||
</Body>
|
||||
|
||||
@@ -14,7 +14,10 @@ export default function MembershipNumberBadge({
|
||||
return (
|
||||
<div className={styles.rewardBadge}>
|
||||
<Caption textAlign="center" color="uiTextHighContrast">
|
||||
{intl.formatMessage({ id: "Membership ID" })}: {membershipNumber}
|
||||
{intl.formatMessage(
|
||||
{ id: "Membership ID: {id}" },
|
||||
{ id: membershipNumber }
|
||||
)}
|
||||
</Caption>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import { dt } from "@/lib/dt"
|
||||
|
||||
@@ -18,6 +19,7 @@ import type { StayCardProps } from "@/types/components/myPages/stays/stayCard"
|
||||
|
||||
export default function StayCard({ stay }: StayCardProps) {
|
||||
const lang = useLang()
|
||||
const intl = useIntl()
|
||||
|
||||
// TODO: Temporary loading. Remove when current web is deleted.
|
||||
const [loading, setLoading] = useState(false)
|
||||
@@ -55,7 +57,7 @@ export default function StayCard({ stay }: StayCardProps) {
|
||||
<Caption asChild>
|
||||
<time dateTime={arrivalDateTime}>{arrivalDate}</time>
|
||||
</Caption>
|
||||
{" - "}
|
||||
{intl.formatMessage({ id: " - " })}
|
||||
<Caption asChild>
|
||||
<time dateTime={departDateTime}>{departDate}</time>
|
||||
</Caption>
|
||||
|
||||
@@ -51,7 +51,7 @@ export default async function HotelListingItem({
|
||||
</div>
|
||||
<Caption color="uiTextPlaceholder">
|
||||
{intl.formatMessage(
|
||||
{ id: "Distance in km to city centre" },
|
||||
{ id: "{number} km to city centre" },
|
||||
{
|
||||
number: getSingleDecimal(
|
||||
hotel.location.distanceToCentre / 1000
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
"use client"
|
||||
|
||||
import { useWatch } from "react-hook-form"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
@@ -34,13 +35,12 @@ export default function MobileToggleButton({
|
||||
? JSON.parse(decodeURIComponent(location))
|
||||
: null
|
||||
|
||||
const nights = dt(d.toDate).diff(dt(d.fromDate), "days")
|
||||
|
||||
const selectedFromDate = dt(d.fromDate).locale(lang).format("D MMM")
|
||||
const selectedToDate = dt(d.toDate).locale(lang).format("D MMM")
|
||||
|
||||
const locationAndDateIsSet = parsedLocation && d
|
||||
|
||||
const totalNights = dt(d.toDate).diff(dt(d.fromDate), "days")
|
||||
const totalRooms = rooms.length
|
||||
const totalAdults = rooms.reduce((acc, room) => {
|
||||
if (room.adults) {
|
||||
@@ -55,6 +55,32 @@ export default function MobileToggleButton({
|
||||
return acc
|
||||
}, 0)
|
||||
|
||||
const totalNightsMsg = intl.formatMessage(
|
||||
{ id: "{totalNights, plural, one {# night} other {# nights}}" },
|
||||
{ totalNights }
|
||||
)
|
||||
|
||||
const totalAdultsMsg = intl.formatMessage(
|
||||
{ id: "{totalAdults, plural, one {# adult} other {# adults}}" },
|
||||
{ totalAdults }
|
||||
)
|
||||
|
||||
const totalChildrenMsg = intl.formatMessage(
|
||||
{ id: "{totalChildren, plural, one {# child} other {# children}}" },
|
||||
{ totalChildren }
|
||||
)
|
||||
|
||||
const totalRoomsMsg = intl.formatMessage(
|
||||
{ id: "{totalRooms, plural, one {# room} other {# rooms}}" },
|
||||
{ totalRooms }
|
||||
)
|
||||
|
||||
const totalDetails = [totalAdultsMsg]
|
||||
if (totalChildren > 0) {
|
||||
totalDetails.push(totalChildrenMsg)
|
||||
}
|
||||
totalDetails.push(totalRoomsMsg)
|
||||
|
||||
return (
|
||||
<div
|
||||
className={locationAndDateIsSet ? styles.complete : styles.partial}
|
||||
@@ -76,13 +102,16 @@ export default function MobileToggleButton({
|
||||
<Divider color="baseSurfaceSubtleNormal" variant="vertical" />
|
||||
<div>
|
||||
<Caption type="bold" color="red">
|
||||
{intl.formatMessage(
|
||||
{ id: "booking.nights" },
|
||||
{ totalNights: nights }
|
||||
)}
|
||||
{totalNightsMsg}
|
||||
</Caption>
|
||||
<Body>
|
||||
{selectedFromDate} - {selectedToDate}
|
||||
{intl.formatMessage(
|
||||
{ id: "{selectedFromDate} - {selectedToDate}" },
|
||||
{
|
||||
selectedFromDate,
|
||||
selectedToDate,
|
||||
}
|
||||
)}
|
||||
</Body>
|
||||
</div>
|
||||
<div className={styles.icon}>
|
||||
@@ -96,17 +125,17 @@ export default function MobileToggleButton({
|
||||
<div>
|
||||
<Caption color="red">{parsedLocation?.name}</Caption>
|
||||
<Caption>
|
||||
{`${selectedFromDate} - ${selectedToDate} (${intl.formatMessage(
|
||||
{ id: "booking.nights" },
|
||||
{ totalNights: nights }
|
||||
)}) ${intl.formatMessage({ id: "booking.adults" }, { totalAdults })}, ${
|
||||
totalChildren > 0
|
||||
? intl.formatMessage(
|
||||
{ id: "booking.children" },
|
||||
{ totalChildren }
|
||||
) + ", "
|
||||
: ""
|
||||
}${intl.formatMessage({ id: "booking.rooms" }, { totalRooms })}`}
|
||||
{intl.formatMessage(
|
||||
{
|
||||
id: "{selectedFromDate} - {selectedToDate} ({totalNights}) {details}",
|
||||
},
|
||||
{
|
||||
selectedFromDate,
|
||||
selectedToDate,
|
||||
totalNights: totalNightsMsg,
|
||||
details: totalDetails.join(", "),
|
||||
}
|
||||
)}
|
||||
</Caption>
|
||||
</div>
|
||||
<div className={styles.icon}>
|
||||
@@ -132,7 +161,10 @@ export function MobileToggleButtonSkeleton() {
|
||||
<Divider color="baseSurfaceSubtleNormal" variant="vertical" />
|
||||
<div>
|
||||
<Caption type="bold" color="red">
|
||||
{intl.formatMessage({ id: "booking.nights" }, { totalNights: 0 })}
|
||||
{intl.formatMessage(
|
||||
{ id: "{totalNights, plural, one {# night} other {# nights}}" },
|
||||
{ totalNights: 0 }
|
||||
)}
|
||||
</Caption>
|
||||
<SkeletonShimmer height="24px" />
|
||||
</div>
|
||||
|
||||
@@ -25,7 +25,7 @@ export default async function IntroSection({
|
||||
const { streetAddress, city } = address
|
||||
const { distanceToCentre } = location
|
||||
const formattedDistanceText = intl.formatMessage(
|
||||
{ id: "Distance in km to city centre" },
|
||||
{ id: "{number} km to city centre" },
|
||||
{ number: getSingleDecimal(distanceToCentre / 1000) }
|
||||
)
|
||||
const lang = getLang()
|
||||
@@ -37,7 +37,7 @@ export default async function IntroSection({
|
||||
)
|
||||
const formattedTripAdvisorText = hasTripAdvisorData
|
||||
? intl.formatMessage(
|
||||
{ id: "Tripadvisor reviews" },
|
||||
{ id: "{rating} ({count} reviews on Tripadvisor)" },
|
||||
{ rating: tripAdvisor.rating, count: tripAdvisor.numberOfReviews }
|
||||
)
|
||||
: ""
|
||||
|
||||
@@ -83,6 +83,9 @@ export default function Sidebar({
|
||||
}, 200)
|
||||
}
|
||||
|
||||
const viewAsMapMsg = intl.formatMessage({ id: "View as map" })
|
||||
const viewAsListMsg = intl.formatMessage({ id: "View as list" })
|
||||
|
||||
return (
|
||||
<>
|
||||
<aside
|
||||
@@ -98,17 +101,13 @@ export default function Sidebar({
|
||||
onClick={toggleFullScreenSidebar}
|
||||
>
|
||||
<Body textTransform="bold" color="textMediumContrast" asChild>
|
||||
<span>
|
||||
{intl.formatMessage({
|
||||
id: isFullScreenSidebar ? "View as map" : "View as list",
|
||||
})}
|
||||
</span>
|
||||
<span>{isFullScreenSidebar ? viewAsMapMsg : viewAsListMsg}</span>
|
||||
</Body>
|
||||
</Button>
|
||||
<div className={styles.sidebarContent}>
|
||||
<Title as="h4" level="h2" textTransform="regular">
|
||||
{intl.formatMessage(
|
||||
{ id: "Things nearby HOTEL_NAME" },
|
||||
{ id: "Things nearby {hotelName}" },
|
||||
{ hotelName }
|
||||
)}
|
||||
</Title>
|
||||
@@ -138,7 +137,14 @@ export default function Sidebar({
|
||||
}
|
||||
>
|
||||
<span>{poi.name}</span>
|
||||
<span>{poi.distance} km</span>
|
||||
<span>
|
||||
{intl.formatMessage(
|
||||
{ id: "{distanceInKm} km" },
|
||||
{
|
||||
distanceInKm: poi.distance,
|
||||
}
|
||||
)}
|
||||
</span>
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
|
||||
@@ -105,7 +105,7 @@ export default function DynamicMap({
|
||||
className={styles.dynamicMap}
|
||||
style={{ "--hotel-map-height": mapHeight } as React.CSSProperties}
|
||||
aria-label={intl.formatMessage(
|
||||
{ id: "Things nearby HOTEL_NAME" },
|
||||
{ id: "Things nearby {hotelName}" },
|
||||
{ hotelName }
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -52,7 +52,12 @@ export default function MapCard({ hotelName, pois }: MapCardProps) {
|
||||
size={20}
|
||||
/>
|
||||
<Body color="black">{poi.name}</Body>
|
||||
<Caption>{poi.distance} km</Caption>
|
||||
<Caption>
|
||||
{intl.formatMessage(
|
||||
{ id: "{distanceInKm} km" },
|
||||
{ distanceInKm: poi.distance }
|
||||
)}
|
||||
</Caption>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
@@ -33,7 +33,10 @@ export default async function StaticMap({
|
||||
width={380}
|
||||
height={mapHeight}
|
||||
zoomLevel={zoomLevel}
|
||||
altText={intl.formatMessage({ id: "Map of HOTEL_NAME" }, { hotelName })}
|
||||
altText={intl.formatMessage(
|
||||
{ id: "Map of {hotelName}" },
|
||||
{ hotelName }
|
||||
)}
|
||||
mapId={mapId}
|
||||
/>
|
||||
<ScandicMarker
|
||||
|
||||
@@ -46,8 +46,8 @@ export default function PreviewImages({
|
||||
<Lightbox
|
||||
images={images}
|
||||
dialogTitle={intl.formatMessage(
|
||||
{ id: "Image gallery" },
|
||||
{ name: hotelName }
|
||||
{ id: "{title} - Image gallery" },
|
||||
{ title: hotelName }
|
||||
)}
|
||||
isOpen={lightboxIsOpen}
|
||||
onClose={() => setLightboxIsOpen(false)}
|
||||
|
||||
@@ -29,7 +29,10 @@ export function RoomCard({ room }: RoomCardProps) {
|
||||
<div className={styles.imageContainer}>
|
||||
<ImageGallery
|
||||
images={images}
|
||||
title={intl.formatMessage({ id: "Image gallery" }, { name })}
|
||||
title={intl.formatMessage(
|
||||
{ id: "{title} - Image gallery" },
|
||||
{ title: name }
|
||||
)}
|
||||
height={200}
|
||||
/>
|
||||
</div>
|
||||
@@ -45,7 +48,9 @@ export function RoomCard({ room }: RoomCardProps) {
|
||||
</Subtitle>
|
||||
<Body color="grey">
|
||||
{intl.formatMessage(
|
||||
{ id: "hotelPages.rooms.roomCard.persons" },
|
||||
{
|
||||
id: "{size} ({max, plural, one {{range} person} other {{range} persons}})",
|
||||
},
|
||||
{
|
||||
size,
|
||||
max: totalOccupancy.max,
|
||||
|
||||
@@ -54,8 +54,12 @@ export function Rooms({ rooms }: RoomsProps) {
|
||||
<ShowMoreButton
|
||||
loadMoreData={handleShowMore}
|
||||
showLess={allRoomsVisible}
|
||||
textShowMore="Show more rooms"
|
||||
textShowLess="Show less rooms"
|
||||
textShowMore={intl.formatMessage({
|
||||
id: "Show more rooms",
|
||||
})}
|
||||
textShowLess={intl.formatMessage({
|
||||
id: "Show less rooms",
|
||||
})}
|
||||
/>
|
||||
) : null}
|
||||
</SectionContainer>
|
||||
|
||||
@@ -49,7 +49,7 @@ export default async function ContactInformation({
|
||||
color="peach80"
|
||||
textDecoration="underline"
|
||||
>
|
||||
Google Maps
|
||||
{intl.formatMessage({ id: "Google Maps" })}
|
||||
</Link>
|
||||
</div>
|
||||
<div className={styles.contact}>
|
||||
|
||||
@@ -18,8 +18,18 @@ export default async function CheckInAmenity({
|
||||
trackingId="amenities:check-in"
|
||||
>
|
||||
<Body textTransform="bold">{intl.formatMessage({ id: "Times" })}</Body>
|
||||
<Body color="uiTextHighContrast">{`${intl.formatMessage({ id: "Check in from" })}: ${checkInTime}`}</Body>
|
||||
<Body color="uiTextHighContrast">{`${intl.formatMessage({ id: "Check out at latest" })}: ${checkOutTime}`}</Body>
|
||||
<Body color="uiTextHighContrast">
|
||||
{intl.formatMessage(
|
||||
{ id: "Check in from: {checkInTime}" },
|
||||
{ checkInTime }
|
||||
)}
|
||||
</Body>
|
||||
<Body color="uiTextHighContrast">
|
||||
{intl.formatMessage(
|
||||
{ id: "Check out at latest: {checkOutTime}" },
|
||||
{ checkOutTime }
|
||||
)}
|
||||
</Body>
|
||||
</AccordionItem>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -13,22 +13,34 @@ export default async function ParkingList({
|
||||
address,
|
||||
}: ParkingListProps) {
|
||||
const intl = await getIntl()
|
||||
|
||||
const canMakeReservationYesMsg = intl.formatMessage({
|
||||
id: "Parking can be reserved in advance: Yes",
|
||||
})
|
||||
const canMakeReservationNoMsg = intl.formatMessage({
|
||||
id: "Parking can be reserved in advance: No",
|
||||
})
|
||||
|
||||
return (
|
||||
<Body color="uiTextHighContrast" asChild>
|
||||
<ul className={styles.listStyling}>
|
||||
{numberOfChargingSpaces ? (
|
||||
<li>
|
||||
{intl.formatMessage(
|
||||
{ id: "Number of charging points for electric cars" },
|
||||
{ id: "Number of charging points for electric cars: {number}" },
|
||||
{ number: numberOfChargingSpaces }
|
||||
)}
|
||||
</li>
|
||||
) : null}
|
||||
<li>{`${intl.formatMessage({ id: "Parking can be reserved in advance" })}: ${canMakeReservation ? intl.formatMessage({ id: "Yes" }) : intl.formatMessage({ id: "No" })}`}</li>
|
||||
<li>
|
||||
{canMakeReservation
|
||||
? canMakeReservationYesMsg
|
||||
: canMakeReservationNoMsg}
|
||||
</li>
|
||||
{numberOfParkingSpots ? (
|
||||
<li>
|
||||
{intl.formatMessage(
|
||||
{ id: "Number of parking spots" },
|
||||
{ id: "Number of parking spots: {number}" },
|
||||
{ number: numberOfParkingSpots }
|
||||
)}
|
||||
</li>
|
||||
@@ -36,13 +48,15 @@ export default async function ParkingList({
|
||||
{distanceToHotel ? (
|
||||
<li>
|
||||
{intl.formatMessage(
|
||||
{ id: "Distance to hotel" },
|
||||
{ distance: distanceToHotel }
|
||||
{ id: "Distance to hotel: {distanceInM} m" },
|
||||
{ distanceInM: distanceToHotel }
|
||||
)}
|
||||
</li>
|
||||
) : null}
|
||||
{address ? (
|
||||
<li>{`${intl.formatMessage({ id: "Address" })}: ${address}`}</li>
|
||||
<li>
|
||||
{intl.formatMessage({ id: "Address: {address}" }, { address })}
|
||||
</li>
|
||||
) : null}
|
||||
</ul>
|
||||
</Body>
|
||||
|
||||
@@ -47,7 +47,7 @@ export default async function ParkingPrices({
|
||||
? freeParking
|
||||
? intl.formatMessage({ id: "Free parking" })
|
||||
: formatPrice(intl, parking.amount, currency)
|
||||
: "N/A"}
|
||||
: intl.formatMessage({ id: "N/A" })}
|
||||
</Body>
|
||||
</div>
|
||||
{parking.startTime &&
|
||||
@@ -58,7 +58,13 @@ export default async function ParkingPrices({
|
||||
{intl.formatMessage({ id: "From" })}
|
||||
</Body>
|
||||
<Body color="uiTextHighContrast">
|
||||
{parking.startTime}-{parking.endTime}
|
||||
{intl.formatMessage(
|
||||
{ id: "{parkingStartTime}-{parkingEndTime}" },
|
||||
{
|
||||
parkingStartTime: parking.startTime,
|
||||
parkingEndTime: parking.endTime,
|
||||
}
|
||||
)}
|
||||
</Body>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -29,14 +29,14 @@ export async function getRoomText(roomSizes: number[]) {
|
||||
let roomText
|
||||
if (largestRoom === smallestRoom) {
|
||||
roomText = intl.formatMessage(
|
||||
{ id: "{number} square meters" },
|
||||
{ number: largestRoom }
|
||||
{ id: "{value} square meters" },
|
||||
{ value: largestRoom }
|
||||
)
|
||||
} else if (smallestRoom != null && largestRoom) {
|
||||
{
|
||||
roomText = intl.formatMessage(
|
||||
{
|
||||
id: "{number} to {number} square meters",
|
||||
id: "{smallest} to {largest} square meters",
|
||||
},
|
||||
{ largest: largestRoom, smallest: smallestRoom }
|
||||
)
|
||||
@@ -53,14 +53,14 @@ export async function getSeatingText(roomSeating: number[]) {
|
||||
let seatingText
|
||||
if (biggestSeating === smallestSeating) {
|
||||
seatingText = intl.formatMessage(
|
||||
{ id: "{number} persons" },
|
||||
{ id: "{value} persons" },
|
||||
{ number: biggestSeating }
|
||||
)
|
||||
} else if (smallestSeating != null && biggestSeating) {
|
||||
{
|
||||
seatingText = intl.formatMessage(
|
||||
{
|
||||
id: "{number} to {number} persons",
|
||||
id: "{lowest} to {highest} persons",
|
||||
},
|
||||
{ highest: biggestSeating, lowest: smallestSeating }
|
||||
)
|
||||
|
||||
@@ -5,8 +5,6 @@ import { getIntl } from "@/i18n"
|
||||
import styles from "./openingHours.module.css"
|
||||
|
||||
import type { OpeningHoursProps } from "@/types/components/hotelPage/sidepeek/openingHours"
|
||||
import { DaysEnum } from "@/types/components/hotelPage/sidepeek/restaurantBar"
|
||||
import type { RestaurantOpeningHoursDay } from "@/types/hotel"
|
||||
|
||||
export default async function OpeningHours({
|
||||
openingHours,
|
||||
@@ -16,45 +14,83 @@ export default async function OpeningHours({
|
||||
}: OpeningHoursProps) {
|
||||
const intl = await getIntl()
|
||||
|
||||
const closed = intl.formatMessage({ id: "Closed" })
|
||||
const alwaysOpen = intl.formatMessage({ id: "Always open" })
|
||||
const closedMsg = intl.formatMessage({ id: "Closed" })
|
||||
const alwaysOpenMsg = intl.formatMessage({ id: "Always open" })
|
||||
|
||||
const days: (keyof typeof openingHours)[] = [
|
||||
DaysEnum.Monday,
|
||||
DaysEnum.Tuesday,
|
||||
DaysEnum.Wednesday,
|
||||
DaysEnum.Thursday,
|
||||
DaysEnum.Friday,
|
||||
DaysEnum.Saturday,
|
||||
DaysEnum.Sunday,
|
||||
]
|
||||
// In order
|
||||
const weekdayDefinitions = [
|
||||
{
|
||||
key: "monday",
|
||||
label: intl.formatMessage({ id: "monday" }),
|
||||
},
|
||||
{
|
||||
key: "tuesday",
|
||||
label: intl.formatMessage({ id: "tuesday" }),
|
||||
},
|
||||
{
|
||||
key: "wednesday",
|
||||
label: intl.formatMessage({ id: "wednesday" }),
|
||||
},
|
||||
{
|
||||
key: "thursday",
|
||||
label: intl.formatMessage({ id: "thursday" }),
|
||||
},
|
||||
{
|
||||
key: "friday",
|
||||
label: intl.formatMessage({ id: "friday" }),
|
||||
},
|
||||
{
|
||||
key: "saturday",
|
||||
label: intl.formatMessage({ id: "saturday" }),
|
||||
},
|
||||
{
|
||||
key: "sunday",
|
||||
label: intl.formatMessage({ id: "sunday" }),
|
||||
},
|
||||
] as const
|
||||
|
||||
const groupedOpeningHours: { [key: string]: string[] } = {}
|
||||
const groupedOpeningHours: string[] = []
|
||||
|
||||
days.forEach((day) => {
|
||||
const today = openingHours[day] as RestaurantOpeningHoursDay
|
||||
let key: string
|
||||
let rangeWeekdays: string[] = []
|
||||
let rangeValue = ""
|
||||
for (let i = 0, n = weekdayDefinitions.length; i < n; ++i) {
|
||||
const weekdayDefinition = weekdayDefinitions[i]
|
||||
const weekday = openingHours[weekdayDefinition.key]
|
||||
const label = weekdayDefinition.label
|
||||
if (weekday) {
|
||||
let newValue = null
|
||||
|
||||
if (today.isClosed) {
|
||||
key = closed
|
||||
} else if (today.alwaysOpen) {
|
||||
key = alwaysOpen
|
||||
} else {
|
||||
key = `${today.openingTime}-${today.closingTime}`
|
||||
if (weekday.alwaysOpen) {
|
||||
newValue = alwaysOpenMsg
|
||||
} else if (weekday.isClosed) {
|
||||
newValue = closedMsg
|
||||
} else if (weekday.openingTime && weekday.closingTime) {
|
||||
newValue = `${weekday.openingTime}-${weekday.closingTime}`
|
||||
}
|
||||
|
||||
if (newValue !== null) {
|
||||
if (rangeValue === newValue) {
|
||||
if (rangeWeekdays.length > 1) {
|
||||
rangeWeekdays.splice(-1, 1, label) // Replace last element
|
||||
} else {
|
||||
rangeWeekdays.push(label)
|
||||
}
|
||||
} else {
|
||||
if (rangeValue) {
|
||||
groupedOpeningHours.push(
|
||||
`${rangeWeekdays.join("-")}: ${rangeValue}`
|
||||
)
|
||||
}
|
||||
rangeValue = newValue
|
||||
rangeWeekdays = [label]
|
||||
}
|
||||
}
|
||||
|
||||
if (rangeValue && i === n - 1) {
|
||||
// Flush everything at the end
|
||||
groupedOpeningHours.push(`${rangeWeekdays.join("-")}: ${rangeValue}`)
|
||||
}
|
||||
}
|
||||
|
||||
if (!groupedOpeningHours[key]) {
|
||||
groupedOpeningHours[key] = []
|
||||
}
|
||||
const formattedDay = day.charAt(0).toUpperCase() + day.slice(1)
|
||||
groupedOpeningHours[key].push(intl.formatMessage({ id: formattedDay }))
|
||||
})
|
||||
|
||||
function formatDayInterval(days: string[]) {
|
||||
if (days.length === 1) {
|
||||
return days[0]
|
||||
}
|
||||
return `${days[0]}-${days[days.length - 1]}`
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -62,10 +98,10 @@ export default async function OpeningHours({
|
||||
<Body textTransform="bold" asChild>
|
||||
<h5>{heading ?? openingHours.name}</h5>
|
||||
</Body>
|
||||
{Object.entries(groupedOpeningHours).map(([time, days]) => {
|
||||
{groupedOpeningHours.map((groupedOpeningHour) => {
|
||||
return (
|
||||
<Body color="uiTextHighContrast" key={time}>
|
||||
{`${formatDayInterval(days)}: ${time}`}
|
||||
<Body color="uiTextHighContrast" key={groupedOpeningHour}>
|
||||
{groupedOpeningHour}
|
||||
</Body>
|
||||
)
|
||||
})}
|
||||
|
||||
@@ -98,7 +98,7 @@ export default async function RestaurantBarItem({
|
||||
) : null}
|
||||
{ctaUrl ? (
|
||||
<ButtonLink fullWidth theme="base" intent="secondary" href={ctaUrl}>
|
||||
{`${intl.formatMessage({ id: "Discover" })} ${name}`}
|
||||
{intl.formatMessage({ id: "Discover {name}" }, { name })}
|
||||
</ButtonLink>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
@@ -28,13 +28,27 @@ export default async function RoomSidePeek({ room }: RoomSidePeekProps) {
|
||||
<div className={styles.innerContent}>
|
||||
<Body color="baseTextMediumContrast">
|
||||
{roomSize.min === roomSize.max
|
||||
? roomSize.min
|
||||
: `${roomSize.min} - ${roomSize.max}`}
|
||||
m².{" "}
|
||||
{intl.formatMessage(
|
||||
{ id: "booking.accommodatesUpTo" },
|
||||
{ range: totalOccupancy.range, max: totalOccupancy.max }
|
||||
)}
|
||||
? intl.formatMessage(
|
||||
{
|
||||
id: "{roomSize} m². Accommodates up to {max, plural, one {{range} person} other {{range} people}}",
|
||||
},
|
||||
{
|
||||
roomSize: roomSize.min,
|
||||
max: totalOccupancy.max,
|
||||
range: totalOccupancy.range,
|
||||
}
|
||||
)
|
||||
: intl.formatMessage(
|
||||
{
|
||||
id: "{roomSizeMin} - {roomSizeMax} m². Accommodates up to {max, plural, one {{range} person} other {{range} people}}",
|
||||
},
|
||||
{
|
||||
roomSizeMin: roomSize.min,
|
||||
roomSizeMax: roomSize.max,
|
||||
max: totalOccupancy.max,
|
||||
range: totalOccupancy.range,
|
||||
}
|
||||
)}
|
||||
</Body>
|
||||
<div className={styles.imageContainer}>
|
||||
<ImageGallery images={images} title={room.name} height={280} />
|
||||
@@ -44,9 +58,7 @@ export default async function RoomSidePeek({ room }: RoomSidePeekProps) {
|
||||
|
||||
<div className={styles.innerContent}>
|
||||
<Subtitle type="two" color="uiTextHighContrast" asChild>
|
||||
<h3>
|
||||
{intl.formatMessage({ id: "booking.thisRoomIsEquippedWith" })}
|
||||
</h3>
|
||||
<h3>{intl.formatMessage({ id: "This room is equipped with" })}</h3>
|
||||
</Subtitle>
|
||||
<ul className={styles.facilityList}>
|
||||
{room.roomFacilities
|
||||
@@ -77,10 +89,10 @@ export default async function RoomSidePeek({ room }: RoomSidePeekProps) {
|
||||
|
||||
<div className={styles.innerContent}>
|
||||
<Subtitle type="two" color="uiTextHighContrast" asChild>
|
||||
<h3>{intl.formatMessage({ id: "booking.bedOptions" })}</h3>
|
||||
<h3>{intl.formatMessage({ id: "Bed options" })}</h3>
|
||||
</Subtitle>
|
||||
<Body color="grey">
|
||||
{intl.formatMessage({ id: "booking.basedOnAvailability" })}
|
||||
{intl.formatMessage({ id: "Based on availability" })}
|
||||
</Body>
|
||||
<ul className={styles.bedOptions}>
|
||||
{room.roomTypes.map((roomType) => {
|
||||
@@ -107,7 +119,7 @@ export default async function RoomSidePeek({ room }: RoomSidePeekProps) {
|
||||
<div className={styles.buttonContainer}>
|
||||
<Button fullWidth theme="base" intent="primary" asChild>
|
||||
<Link href={ctaUrl}>
|
||||
{intl.formatMessage({ id: "booking.selectRoom" })}
|
||||
{intl.formatMessage({ id: "Select room" })}
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -27,7 +27,7 @@ export default async function Facility({ data }: FacilityProps) {
|
||||
)}
|
||||
<div className={styles.information}>
|
||||
<Subtitle color="burgundy" asChild type="one">
|
||||
<Title level="h3">{intl.formatMessage({ id: `${data.type}` })}</Title>
|
||||
<Title level="h3">{intl.formatMessage({ id: data.type })}</Title>
|
||||
</Subtitle>
|
||||
<div>
|
||||
<Subtitle type="two" color="uiTextHighContrast">
|
||||
@@ -36,13 +36,25 @@ export default async function Facility({ data }: FacilityProps) {
|
||||
<div className={styles.openingHours}>
|
||||
<Body color="uiTextHighContrast">
|
||||
{ordinaryOpeningTimes.alwaysOpen
|
||||
? `${intl.formatMessage({ id: "Mon-Fri" })} ${intl.formatMessage({ id: "Always open" })}`
|
||||
: `${intl.formatMessage({ id: "Mon-Fri" })} ${ordinaryOpeningTimes.openingTime}-${ordinaryOpeningTimes.closingTime}`}
|
||||
? intl.formatMessage({ id: "Mon-Fri Always open" })
|
||||
: intl.formatMessage(
|
||||
{ id: "Mon-Fri {openingTime}-${closingTime}" },
|
||||
{
|
||||
openingTime: ordinaryOpeningTimes.openingTime,
|
||||
closingTime: ordinaryOpeningTimes.closingTime,
|
||||
}
|
||||
)}
|
||||
</Body>
|
||||
<Body color="uiTextHighContrast">
|
||||
{weekendOpeningTimes.alwaysOpen
|
||||
? `${intl.formatMessage({ id: "Sat-Sun" })} ${intl.formatMessage({ id: "Always open" })}`
|
||||
: `${intl.formatMessage({ id: "Sat-Sun" })} ${weekendOpeningTimes.openingTime}-${weekendOpeningTimes.closingTime}`}
|
||||
? intl.formatMessage({ id: "Sat-Sun Always open" })
|
||||
: intl.formatMessage(
|
||||
{ id: "Sat-Sun {openingTime}-${closingTime}" },
|
||||
{
|
||||
openingTime: weekendOpeningTimes.openingTime,
|
||||
closingTime: weekendOpeningTimes.closingTime,
|
||||
}
|
||||
)}
|
||||
</Body>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -39,7 +39,10 @@ export default function TabNavigation({
|
||||
hash: HotelHashValues.overview,
|
||||
text: intl.formatMessage({ id: "Overview" }),
|
||||
},
|
||||
{ hash: HotelHashValues.rooms, text: intl.formatMessage({ id: "Rooms" }) },
|
||||
{
|
||||
hash: HotelHashValues.rooms,
|
||||
text: intl.formatMessage({ id: "Rooms" }),
|
||||
},
|
||||
{
|
||||
hash: HotelHashValues.restaurant,
|
||||
text: intl.formatMessage({ id: restaurantTitle }, { count: 1 }),
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
"use client"
|
||||
|
||||
import { da, de, fi, nb, sv } from "date-fns/locale"
|
||||
import { useCallback, useEffect, useRef, useState } from "react"
|
||||
import { useFormContext, useWatch } from "react-hook-form"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import { Lang } from "@/constants/languages"
|
||||
import { dt } from "@/lib/dt"
|
||||
@@ -26,6 +28,8 @@ const locales = {
|
||||
|
||||
export default function DatePickerForm({ name = "date" }: DatePickerFormProps) {
|
||||
const lang = useLang()
|
||||
const intl = useIntl()
|
||||
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
const selectedDate = useWatch({ name })
|
||||
const { register, setValue } = useFormContext()
|
||||
@@ -131,7 +135,13 @@ export default function DatePickerForm({ name = "date" }: DatePickerFormProps) {
|
||||
>
|
||||
<Body className={styles.body} asChild color="uiTextHighContrast">
|
||||
<span>
|
||||
{selectedFromDate} - {selectedToDate}
|
||||
{intl.formatMessage(
|
||||
{ id: "{selectedFromDate} - {selectedToDate}" },
|
||||
{
|
||||
selectedFromDate,
|
||||
selectedToDate,
|
||||
}
|
||||
)}
|
||||
</span>
|
||||
</Body>
|
||||
</button>
|
||||
|
||||
@@ -40,8 +40,10 @@ export default async function FooterDetails() {
|
||||
<div className={styles.bottomContainer}>
|
||||
<div className={styles.copyrightContainer}>
|
||||
<Footnote type="label" textTransform="uppercase">
|
||||
© {currentYear}{" "}
|
||||
{intl.formatMessage({ id: "Copyright all rights reserved" })}
|
||||
{intl.formatMessage(
|
||||
{ id: "© {currentYear} Scandic AB All rights reserved" },
|
||||
{ currentYear }
|
||||
)}
|
||||
</Footnote>
|
||||
</div>
|
||||
<div className={styles.navigationContainer}>
|
||||
@@ -99,8 +101,10 @@ export async function FooterDetailsSkeleton() {
|
||||
<div className={styles.bottomContainer}>
|
||||
<div className={styles.copyrightContainer}>
|
||||
<Footnote type="label" textTransform="uppercase">
|
||||
© {currentYear}{" "}
|
||||
{intl.formatMessage({ id: "Copyright all rights reserved" })}
|
||||
{intl.formatMessage(
|
||||
{ id: "© {currentYear} Scandic AB All rights reserved" },
|
||||
{ currentYear }
|
||||
)}
|
||||
</Footnote>
|
||||
</div>
|
||||
<div className={styles.navigationContainer}>
|
||||
|
||||
@@ -19,10 +19,10 @@ export default function Voucher() {
|
||||
const bonus = intl.formatMessage({ id: "Use bonus cheque" })
|
||||
const reward = intl.formatMessage({ id: "Book reward night" })
|
||||
const disabledBookingOptionsHeader = intl.formatMessage({
|
||||
id: "Disabled booking options header",
|
||||
id: "We're sorry",
|
||||
})
|
||||
const disabledBookingOptionsText = intl.formatMessage({
|
||||
id: "Disabled booking options text",
|
||||
id: "Codes, cheques and reward nights aren't available on the new website yet.",
|
||||
})
|
||||
|
||||
return (
|
||||
|
||||
@@ -41,7 +41,9 @@ export default function FormContent({
|
||||
<Caption color="red" type="bold">
|
||||
{nights > 0
|
||||
? intl.formatMessage(
|
||||
{ id: "booking.nights" },
|
||||
{
|
||||
id: "{totalNights, plural, one {# night} other {# nights}}",
|
||||
},
|
||||
{ totalNights: nights }
|
||||
)
|
||||
: intl.formatMessage({
|
||||
@@ -102,7 +104,10 @@ export function BookingWidgetFormContentSkeleton() {
|
||||
</div>
|
||||
<div className={styles.when}>
|
||||
<Caption color="red" type="bold">
|
||||
{intl.formatMessage({ id: "booking.nights" }, { totalNights: 0 })}
|
||||
{intl.formatMessage(
|
||||
{ id: "{totalNights, plural, one {# night} other {# nights}}" },
|
||||
{ totalNights: 0 }
|
||||
)}
|
||||
</Caption>
|
||||
<SkeletonShimmer width={"100%"} />
|
||||
</div>
|
||||
|
||||
@@ -164,9 +164,11 @@ export default function SignupForm({ link, subtitle, title }: SignUpFormProps) {
|
||||
{/* TODO: Update copy once ready */}
|
||||
<Body>
|
||||
{intl.formatMessage<React.ReactNode>(
|
||||
{ id: "signupPage.terms" },
|
||||
{
|
||||
termsAndConditions: (str) => (
|
||||
id: "By accepting the <termsAndConditionsLink>Terms and Conditions for Scandic Friends</termsAndConditionsLink> I understand that my personal data will be processed in accordance with <privacyPolicy>Scandic's Privacy Policy</privacyPolicy>.",
|
||||
},
|
||||
{
|
||||
termsAndConditionsLink: (str) => (
|
||||
<Link
|
||||
variant="underscored"
|
||||
color="peach80"
|
||||
|
||||
@@ -33,13 +33,13 @@ export default function GuestsRoomsPickerDialog({
|
||||
const { getFieldState, trigger, setValue } =
|
||||
useFormContext<BookingWidgetSchema>()
|
||||
const roomsValue = useWatch<BookingWidgetSchema, "rooms">({ name: "rooms" })
|
||||
const addRoomLabel = intl.formatMessage({ id: "Add Room" })
|
||||
const addRoomLabel = intl.formatMessage({ id: "Add room" })
|
||||
const doneLabel = intl.formatMessage({ id: "Done" })
|
||||
const disabledBookingOptionsHeader = intl.formatMessage({
|
||||
id: "Disabled booking options header",
|
||||
id: "We're sorry",
|
||||
})
|
||||
const disabledBookingOptionsText = intl.formatMessage({
|
||||
id: "Disabled adding room",
|
||||
id: "Adding room is not available on the new website yet.",
|
||||
})
|
||||
|
||||
const handleClose = useCallback(async () => {
|
||||
|
||||
@@ -137,6 +137,38 @@ function Trigger({
|
||||
}) {
|
||||
const intl = useIntl()
|
||||
|
||||
const parts = []
|
||||
|
||||
parts.push(
|
||||
intl.formatMessage(
|
||||
{ id: "{totalRooms, plural, one {# room} other {# rooms}}" },
|
||||
{ totalRooms: rooms.length }
|
||||
)
|
||||
)
|
||||
|
||||
parts.push(
|
||||
intl.formatMessage(
|
||||
{ id: "{totalAdults, plural, one {# adult} other {# adults}}" },
|
||||
{ totalAdults: rooms.reduce((acc, room) => acc + room.adults, 0) }
|
||||
)
|
||||
)
|
||||
|
||||
if (rooms.some((room) => room.child.length > 0)) {
|
||||
parts.push(
|
||||
intl.formatMessage(
|
||||
{
|
||||
id: "{totalChildren, plural, one {# child} other {# children}}",
|
||||
},
|
||||
{
|
||||
totalChildren: rooms.reduce(
|
||||
(acc, room) => acc + room.child.length,
|
||||
0
|
||||
),
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Button
|
||||
className={`${className} ${styles.btn}`}
|
||||
@@ -144,29 +176,7 @@ function Trigger({
|
||||
onPress={triggerFn}
|
||||
>
|
||||
<Body color="uiTextHighContrast">
|
||||
<span>
|
||||
{intl.formatMessage(
|
||||
{ id: "booking.rooms" },
|
||||
{ totalRooms: rooms.length }
|
||||
)}
|
||||
{", "}
|
||||
{intl.formatMessage(
|
||||
{ id: "booking.adults" },
|
||||
{ totalAdults: rooms.reduce((acc, room) => acc + room.adults, 0) }
|
||||
)}
|
||||
{rooms.some((room) => room.child.length > 0)
|
||||
? ", " +
|
||||
intl.formatMessage(
|
||||
{ id: "booking.children" },
|
||||
{
|
||||
totalChildren: rooms.reduce(
|
||||
(acc, room) => acc + room.child.length,
|
||||
0
|
||||
),
|
||||
}
|
||||
)
|
||||
: null}
|
||||
</span>
|
||||
<span>{parts.join(", ")}</span>
|
||||
</Body>
|
||||
</Button>
|
||||
)
|
||||
|
||||
@@ -60,14 +60,19 @@ export default function MobileMenu({
|
||||
}
|
||||
}, [isHamburgerMenuOpen])
|
||||
|
||||
const closeMsg = intl.formatMessage({
|
||||
id: "Close menu",
|
||||
})
|
||||
const openMsg = intl.formatMessage({
|
||||
id: "Open menu",
|
||||
})
|
||||
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
className={`${styles.hamburger} ${isHamburgerExtended ? styles.isExpanded : ""}`}
|
||||
aria-label={intl.formatMessage({
|
||||
id: isHamburgerExtended ? "Close menu" : "Open menu",
|
||||
})}
|
||||
aria-label={isHamburgerExtended ? closeMsg : openMsg}
|
||||
onClick={() => toggleDropdown(DropdownTypeEnum.HamburgerMenu)}
|
||||
>
|
||||
<span className={styles.bar} />
|
||||
|
||||
@@ -50,7 +50,10 @@ export default function MyPagesMenu({
|
||||
<Avatar initials={getInitials(user.firstName, user.lastName)} />
|
||||
<Body textTransform="bold" color="baseTextHighContrast" asChild>
|
||||
<span>
|
||||
{intl.formatMessage({ id: "Hi" })} {user.firstName}!
|
||||
{intl.formatMessage(
|
||||
{ id: "Hi {firstName}!" },
|
||||
{ firstName: user.firstName }
|
||||
)}
|
||||
</span>
|
||||
</Body>
|
||||
<ChevronDownSmallIcon
|
||||
|
||||
@@ -40,13 +40,19 @@ export default function MyPagesMenuContent({
|
||||
<nav className={styles.myPagesMenuContent} ref={myPagesMenuContentRef}>
|
||||
<div className={introClassName}>
|
||||
<Subtitle type="two" className={styles.userName}>
|
||||
{intl.formatMessage({ id: "Hi" })} {user.firstName}!
|
||||
{intl.formatMessage(
|
||||
{ id: "Hi {firstName}!" },
|
||||
{ firstName: user.firstName }
|
||||
)}
|
||||
</Subtitle>
|
||||
{membershipLevel && membershipPoints ? (
|
||||
<Caption className={styles.friendTypeWrapper}>
|
||||
<span className={styles.friendType}>{membershipLevel.name}</span>
|
||||
<span>
|
||||
{membershipPoints} {intl.formatMessage({ id: "points" })}
|
||||
{intl.formatMessage(
|
||||
{ id: "{pointsAmount, number} points" },
|
||||
{ pointsAmount: membershipPoints }
|
||||
)}
|
||||
</span>
|
||||
</Caption>
|
||||
) : null}
|
||||
|
||||
@@ -36,10 +36,10 @@ export default function Confirmation({
|
||||
<Alert
|
||||
type={AlertTypeEnum.Info}
|
||||
heading={intl.formatMessage({
|
||||
id: "booking.confirmation.membershipInfo.heading",
|
||||
id: "Failed to verify membership",
|
||||
})}
|
||||
text={intl.formatMessage({
|
||||
id: "booking.confirmation.membershipInfo.text",
|
||||
id: "Your booking(s) is confirmed but we could not verify your membership. If you have booked with a member discount, you'll either need to present your existing membership number upon check-in, become a member or pay the price difference at the hotel. Signing up is preferably done online before the stay.",
|
||||
})}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -24,7 +24,9 @@ export default function Header({
|
||||
const intl = useIntl()
|
||||
|
||||
const text = intl.formatMessage<React.ReactNode>(
|
||||
{ id: "booking.confirmation.text" },
|
||||
{
|
||||
id: "Thank you for booking with us! We look forward to welcoming you and hope you have a pleasant stay. If you have any questions or need to make changes to your reservation, please <emailLink>contact us.</emailLink>",
|
||||
},
|
||||
{
|
||||
emailLink: (str) => (
|
||||
<Link color="burgundy" href="#" textDecoration="underline">
|
||||
@@ -57,7 +59,7 @@ export default function Header({
|
||||
<header className={styles.header}>
|
||||
<hgroup className={styles.hgroup}>
|
||||
<Title as="h2" color="red" textTransform="uppercase" type="h2">
|
||||
{intl.formatMessage({ id: "booking.confirmation.title" })}
|
||||
{intl.formatMessage({ id: "Booking confirmation" })}
|
||||
</Title>
|
||||
<Title as="h2" color="burgundy" textTransform="uppercase" type="h1">
|
||||
{hotel.name}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
"use client"
|
||||
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import Link from "@/components/TempDesignSystem/Link"
|
||||
@@ -23,8 +24,14 @@ export default function HotelDetails({
|
||||
<div className={styles.hotel}>
|
||||
<Body color="uiTextHighContrast">{hotel.name}</Body>
|
||||
<Body color="uiTextHighContrast">
|
||||
{hotel.address.streetAddress}, {hotel.address.zipCode}{" "}
|
||||
{hotel.address.city}
|
||||
{intl.formatMessage(
|
||||
{ id: "{streetAddress}, {zipCode} {city}" },
|
||||
{
|
||||
streetAddress: hotel.address.streetAddress,
|
||||
zipCode: hotel.address.zipCode,
|
||||
city: hotel.address.city,
|
||||
}
|
||||
)}
|
||||
</Body>
|
||||
<Body asChild color="uiTextHighContrast">
|
||||
<Link
|
||||
@@ -64,7 +71,7 @@ export default function HotelDetails({
|
||||
<div className={styles.toast}>
|
||||
<Toast variant="info">
|
||||
<ul className={styles.list}>
|
||||
<li>N/A</li>
|
||||
<li>{intl.formatMessage({ id: "N/A" })}</li>
|
||||
</ul>
|
||||
</Toast>
|
||||
</div>
|
||||
|
||||
@@ -26,8 +26,16 @@ export default function PaymentDetails({
|
||||
</Subtitle>
|
||||
<div className={styles.payment}>
|
||||
<Body color="uiTextHighContrast">
|
||||
{formatPrice(intl, booking.totalPrice, booking.currencyCode)}{" "}
|
||||
{intl.formatMessage({ id: "has been paid" })}
|
||||
{intl.formatMessage(
|
||||
{ id: "{amount} has been paid" },
|
||||
{
|
||||
amount: formatPrice(
|
||||
intl,
|
||||
booking.totalPrice,
|
||||
booking.currencyCode
|
||||
),
|
||||
}
|
||||
)}
|
||||
</Body>
|
||||
<Body color="uiTextHighContrast">
|
||||
{dt(booking.createDateTime)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
"use client"
|
||||
|
||||
import { notFound } from "next/navigation"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
@@ -47,7 +48,7 @@ export default function Receipt({
|
||||
{booking.rateDefinition.isMemberRate ? (
|
||||
<div className={styles.memberPrice}>
|
||||
<Body color="uiTextPlaceholder">
|
||||
<s>N/A</s>
|
||||
<s>{intl.formatMessage({ id: "N/A" })}</s>
|
||||
</Body>
|
||||
<Body color="red">
|
||||
{formatPrice(intl, booking.roomPrice, booking.currencyCode)}
|
||||
@@ -60,7 +61,7 @@ export default function Receipt({
|
||||
)}
|
||||
<Caption color="uiTextMediumContrast">
|
||||
{intl.formatMessage(
|
||||
{ id: "booking.adults" },
|
||||
{ id: "{totalAdults, plural, one {# adult} other {# adults}}" },
|
||||
{
|
||||
totalAdults: booking.adults,
|
||||
}
|
||||
@@ -156,7 +157,12 @@ export default function Receipt({
|
||||
<ChevronRightSmallIcon />
|
||||
</Button>
|
||||
<Caption color="uiTextMediumContrast">
|
||||
{intl.formatMessage({ id: "Approx." })} N/A EUR
|
||||
{intl.formatMessage(
|
||||
{ id: "Approx. {value}" },
|
||||
{
|
||||
value: "N/A EUR",
|
||||
}
|
||||
)}
|
||||
</Caption>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
"use client"
|
||||
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import { dt } from "@/lib/dt"
|
||||
@@ -23,6 +24,7 @@ export default function Room({ booking, img, roomName }: RoomProps) {
|
||||
const intl = useIntl()
|
||||
const lang = useLang()
|
||||
|
||||
const guestName = `${booking.guest.firstName} ${booking.guest.lastName}`
|
||||
const fromDate = dt(booking.checkInDate).locale(lang)
|
||||
const toDate = dt(booking.checkOutDate).locale(lang)
|
||||
return (
|
||||
@@ -33,7 +35,12 @@ export default function Room({ booking, img, roomName }: RoomProps) {
|
||||
{intl.formatMessage({ id: "Room" })} 1
|
||||
</Subtitle> */}
|
||||
<Subtitle color="uiTextHighContrast" type="two">
|
||||
{`${intl.formatMessage({ id: "Reservation number" })} ${booking.confirmationNumber}`}
|
||||
{intl.formatMessage(
|
||||
{ id: "Reservation number {value}" },
|
||||
{
|
||||
value: booking.confirmationNumber,
|
||||
}
|
||||
)}
|
||||
</Subtitle>
|
||||
</div>
|
||||
<div className={styles.benefits}>
|
||||
@@ -81,7 +88,13 @@ export default function Room({ booking, img, roomName }: RoomProps) {
|
||||
{intl.formatMessage({ id: "Check-in" })}
|
||||
</Body>
|
||||
<Body color="uiTextHighContrast">
|
||||
{`${fromDate.format("ddd, D MMM")} ${intl.formatMessage({ id: "from" })} ${fromDate.format("HH:mm")}`}
|
||||
{intl.formatMessage(
|
||||
{ id: "{checkInDate} from {checkInTime}" },
|
||||
{
|
||||
checkInDate: fromDate.format("ddd, D MMM"),
|
||||
checkInTime: fromDate.format("HH:mm"),
|
||||
}
|
||||
)}
|
||||
</Body>
|
||||
</li>
|
||||
<li className={styles.listItem}>
|
||||
@@ -89,14 +102,22 @@ export default function Room({ booking, img, roomName }: RoomProps) {
|
||||
{intl.formatMessage({ id: "Check-out" })}
|
||||
</Body>
|
||||
<Body color="uiTextHighContrast">
|
||||
{`${toDate.format("ddd, D MMM")} ${intl.formatMessage({ id: "from" })} ${toDate.format("HH:mm")}`}
|
||||
{intl.formatMessage(
|
||||
{ id: "{checkOutDate} from {checkOutTime}" },
|
||||
{
|
||||
checkOutDate: toDate.format("ddd, D MMM"),
|
||||
checkOutTime: toDate.format("HH:mm"),
|
||||
}
|
||||
)}
|
||||
</Body>
|
||||
</li>
|
||||
<li className={styles.listItem}>
|
||||
<Body color="uiTextPlaceholder">
|
||||
{intl.formatMessage({ id: "Breakfast" })}
|
||||
</Body>
|
||||
<Body color="uiTextHighContrast">N/A</Body>
|
||||
<Body color="uiTextHighContrast">
|
||||
{intl.formatMessage({ id: "N/A" })}
|
||||
</Body>
|
||||
</li>
|
||||
<li className={styles.listItem}>
|
||||
<Body color="uiTextPlaceholder">
|
||||
@@ -110,19 +131,24 @@ export default function Room({ booking, img, roomName }: RoomProps) {
|
||||
<Body color="uiTextPlaceholder">
|
||||
{intl.formatMessage({ id: "Rebooking" })}
|
||||
</Body>
|
||||
<Body color="uiTextHighContrast">N/A</Body>
|
||||
<Body color="uiTextHighContrast">
|
||||
{intl.formatMessage({ id: "N/A" })}
|
||||
</Body>
|
||||
</li>
|
||||
</ul>
|
||||
<div className={styles.guest}>
|
||||
<Body color="uiTextPlaceholder">
|
||||
{intl.formatMessage({ id: "Main guest" })}
|
||||
</Body>
|
||||
<Body color="uiTextHighContrast">
|
||||
{`${booking.guest.firstName} ${booking.guest.lastName}`}
|
||||
</Body>
|
||||
<Body color="uiTextHighContrast">{guestName}</Body>
|
||||
{booking.guest.membershipNumber ? (
|
||||
<Body color="uiTextHighContrast">
|
||||
{`${intl.formatMessage({ id: "Friend no." })} ${booking.guest.membershipNumber}`}
|
||||
{intl.formatMessage(
|
||||
{ id: "Friend no. {value}" },
|
||||
{
|
||||
value: booking.guest.membershipNumber,
|
||||
}
|
||||
)}
|
||||
</Body>
|
||||
) : null}
|
||||
{booking.guest.phoneNumber ? (
|
||||
|
||||
@@ -16,6 +16,9 @@ export default function Contact({ hotel }: ContactProps) {
|
||||
const lang = useLang()
|
||||
const intl = useIntl()
|
||||
|
||||
const addressStr = `${hotel.address.streetAddress}, `
|
||||
const cityStr = hotel.address.city
|
||||
|
||||
return (
|
||||
<section className={styles.wrapper}>
|
||||
<address className={styles.address}>
|
||||
@@ -24,8 +27,11 @@ export default function Contact({ hotel }: ContactProps) {
|
||||
<Body textTransform="bold">
|
||||
{intl.formatMessage({ id: "Address" })}
|
||||
</Body>
|
||||
<Body>{`${hotel.address.streetAddress}, `}</Body>
|
||||
<Body>{hotel.address.city}</Body>
|
||||
<Body>
|
||||
{addressStr}
|
||||
<br />
|
||||
{cityStr}
|
||||
</Body>
|
||||
</li>
|
||||
<li>
|
||||
<Body textTransform="bold">
|
||||
@@ -34,7 +40,9 @@ export default function Contact({ hotel }: ContactProps) {
|
||||
<Link
|
||||
href={`https://www.google.com/maps/dir/?api=1&destination=${hotel.location.latitude},${hotel.location.longitude}`}
|
||||
>
|
||||
<span className={styles.link}>Google Maps</span>
|
||||
<span className={styles.link}>
|
||||
{intl.formatMessage({ id: "Google Maps" })}
|
||||
</span>
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
|
||||
@@ -24,12 +24,10 @@ export default function BedTypeInfo({ hasMultipleBedTypes }: BedTypeInfoProps) {
|
||||
id: "Extra bed will be provided additionally",
|
||||
})
|
||||
|
||||
const combinedStr = `${availabilityText}. ${extraBedText}`
|
||||
|
||||
if (hasMultipleBedTypes && hasChildWithExtraBed) {
|
||||
return (
|
||||
<Body>
|
||||
{availabilityText}. {extraBedText}
|
||||
</Body>
|
||||
)
|
||||
return <Body>{combinedStr}</Body>
|
||||
}
|
||||
|
||||
if (hasMultipleBedTypes) {
|
||||
|
||||
@@ -90,7 +90,9 @@ export default function Breakfast({ packages }: BreakfastProps) {
|
||||
subtitle={
|
||||
pkg.code === BreakfastPackageEnum.FREE_MEMBER_BREAKFAST
|
||||
? intl.formatMessage<React.ReactNode>(
|
||||
{ id: "breakfast.price.free" },
|
||||
{
|
||||
id: "<strikethrough>{amount}</strikethrough> <free>0 {currency}</free>/night per adult",
|
||||
},
|
||||
{
|
||||
amount: formatPrice(
|
||||
intl,
|
||||
@@ -103,7 +105,7 @@ export default function Breakfast({ packages }: BreakfastProps) {
|
||||
}
|
||||
)
|
||||
: intl.formatMessage(
|
||||
{ id: "breakfast.price" },
|
||||
{ id: "{amount}/night per adult" },
|
||||
{
|
||||
amount: formatPrice(
|
||||
intl,
|
||||
|
||||
@@ -89,10 +89,10 @@ export default function JoinScandicFriendsCard({
|
||||
<Footnote color="uiTextPlaceholder">
|
||||
{intl.formatMessage<React.ReactNode>(
|
||||
{
|
||||
id: "signup.terms",
|
||||
id: "By signing up you accept the Scandic Friends <termsAndConditionsLink>Terms and Conditions</termsAndConditionsLink>. Your membership is valid until further notice, and you can terminate your membership at any time by sending an email to Scandic's customer service",
|
||||
},
|
||||
{
|
||||
termsLink: (str) => (
|
||||
termsAndConditionsLink: (str) => (
|
||||
<Link
|
||||
variant="default"
|
||||
textDecoration="underline"
|
||||
|
||||
@@ -57,7 +57,7 @@ export default function MemberPriceModal({
|
||||
)}
|
||||
</div>
|
||||
<Button intent="primary" theme="base" onClick={() => setIsOpen(false)}>
|
||||
OK
|
||||
{intl.formatMessage({ id: "OK" })}
|
||||
</Button>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
@@ -12,7 +12,6 @@ import useLang from "@/hooks/useLang"
|
||||
import styles from "./signup.module.css"
|
||||
|
||||
export default function Signup({ name }: { name: string }) {
|
||||
const lang = useLang()
|
||||
const intl = useIntl()
|
||||
|
||||
const [isJoinChecked, setIsJoinChecked] = useState(false)
|
||||
@@ -35,7 +34,9 @@ export default function Signup({ name }: { name: string }) {
|
||||
<div className={styles.dateField}>
|
||||
<header>
|
||||
<Caption type="bold">
|
||||
{intl.formatMessage({ id: "Birth date" })} *
|
||||
<span className={styles.required}>
|
||||
{intl.formatMessage({ id: "Birth date" })}
|
||||
</span>
|
||||
</Caption>
|
||||
</header>
|
||||
<DateSelect name="dateOfBirth" registerOptions={{ required: true }} />
|
||||
|
||||
@@ -13,3 +13,7 @@
|
||||
display: grid;
|
||||
gap: var(--Spacing-x1);
|
||||
}
|
||||
|
||||
.required:after {
|
||||
content: " *";
|
||||
}
|
||||
|
||||
@@ -15,6 +15,9 @@ export default async function HotelHeader({ hotelData }: HotelHeaderProps) {
|
||||
const hotel = hotelData.data.attributes
|
||||
|
||||
const image = hotel.hotelContent?.images
|
||||
|
||||
const addressStr = `${hotel.address.streetAddress}, ${hotel.address.city}`
|
||||
|
||||
return (
|
||||
<header className={styles.header}>
|
||||
<Image
|
||||
@@ -30,14 +33,12 @@ export default async function HotelHeader({ hotelData }: HotelHeaderProps) {
|
||||
<Title as="h1" level="h1" color="white" className={styles.title}>
|
||||
{hotel.name}
|
||||
</Title>
|
||||
<address className={styles.address}>
|
||||
<Caption color="white">
|
||||
{hotel.address.streetAddress}, {hotel.address.city}
|
||||
</Caption>
|
||||
<div className={styles.address}>
|
||||
<Caption color="white">{addressStr}</Caption>
|
||||
<Caption color="white">∙</Caption>
|
||||
<Caption color="white">
|
||||
{intl.formatMessage(
|
||||
{ id: "Distance in km to city centre" },
|
||||
{ id: "{number} km to city centre" },
|
||||
{
|
||||
number: getSingleDecimal(
|
||||
hotel.location.distanceToCentre / 1000
|
||||
@@ -45,7 +46,7 @@ export default async function HotelHeader({ hotelData }: HotelHeaderProps) {
|
||||
}
|
||||
)}
|
||||
</Caption>
|
||||
</address>
|
||||
</div>
|
||||
</div>
|
||||
<ToggleSidePeek hotelId={hotel.operaId} />
|
||||
</div>
|
||||
|
||||
@@ -160,7 +160,7 @@ export default function PaymentClient({
|
||||
(errorMessage: string) => {
|
||||
toast.error(
|
||||
intl.formatMessage({
|
||||
id: "payment.error.failed",
|
||||
id: "We had an issue processing your booking. Please try again. No charges have been made.",
|
||||
})
|
||||
)
|
||||
const currentPaymentMethod = methods.getValues("paymentMethod")
|
||||
@@ -312,10 +312,6 @@ export default function PaymentClient({
|
||||
return <LoadingSpinner />
|
||||
}
|
||||
|
||||
const guaranteeing = intl.formatMessage({ id: "guaranteeing" })
|
||||
const paying = intl.formatMessage({ id: "paying" })
|
||||
const paymentVerb = mustBeGuaranteed ? guaranteeing : paying
|
||||
|
||||
return (
|
||||
<>
|
||||
<FormProvider {...methods}>
|
||||
@@ -387,11 +383,10 @@ export default function PaymentClient({
|
||||
<Caption>
|
||||
{intl.formatMessage<React.ReactNode>(
|
||||
{
|
||||
id: "booking.terms",
|
||||
id: "By paying with any of the payment methods available, I accept the terms for this booking and the general <termsAndConditionsLink>Terms & Conditions</termsAndConditionsLink>, and understand that Scandic will process my personal data for this booking in accordance with <privacyPolicyLink>Scandic's Privacy policy</privacyPolicyLink>. I also accept that Scandic require a valid credit card during my visit in case anything is left unpaid.",
|
||||
},
|
||||
{
|
||||
paymentVerb,
|
||||
termsLink: (str) => (
|
||||
termsAndConditionsLink: (str) => (
|
||||
<Link
|
||||
className={styles.link}
|
||||
variant="underscored"
|
||||
@@ -401,7 +396,7 @@ export default function PaymentClient({
|
||||
{str}
|
||||
</Link>
|
||||
),
|
||||
privacyLink: (str) => (
|
||||
privacyPolicyLink: (str) => (
|
||||
<Link
|
||||
className={styles.link}
|
||||
variant="underscored"
|
||||
|
||||
@@ -53,7 +53,7 @@ export default function PriceChangeDialog({
|
||||
<br />
|
||||
<span className={styles.oldPrice}>
|
||||
{formatPrice(intl, oldPrice, currency)}
|
||||
</span>{" "}
|
||||
</span>
|
||||
<strong className={styles.newPrice}>
|
||||
{formatPrice(intl, newPrice, currency)}
|
||||
</strong>
|
||||
|
||||
@@ -28,7 +28,7 @@ export default function ToggleSidePeek({
|
||||
intent="text"
|
||||
wrapping
|
||||
>
|
||||
{intl.formatMessage({ id: "See room details" })}{" "}
|
||||
{intl.formatMessage({ id: "See room details" })}
|
||||
<ChevronRight height="14" />
|
||||
</Button>
|
||||
)
|
||||
|
||||
@@ -47,8 +47,16 @@ export default function SelectedRoom({
|
||||
className={styles.description}
|
||||
color="uiTextHighContrast"
|
||||
>
|
||||
{room.roomType}{" "}
|
||||
<span className={styles.rate}>{rateDescription}</span>
|
||||
{intl.formatMessage<React.ReactNode>(
|
||||
{ id: "{roomType} <rate>{rateDescription}</rate>" },
|
||||
{
|
||||
roomType: room.roomType,
|
||||
rateDescription,
|
||||
rate: (str) => {
|
||||
return <span className={styles.rate}>{str}</span>
|
||||
},
|
||||
}
|
||||
)}
|
||||
</Subtitle>
|
||||
<Link
|
||||
className={styles.button}
|
||||
|
||||
@@ -55,7 +55,7 @@ export default function SummaryBottomSheet({ children }: PropsWithChildren) {
|
||||
onClick={toggleSummaryOpen}
|
||||
className={styles.priceDetailsButton}
|
||||
>
|
||||
<Caption>{intl.formatMessage({ id: "Total price" })}:</Caption>
|
||||
<Caption>{intl.formatMessage({ id: "Total price" })}</Caption>
|
||||
<Subtitle>
|
||||
{formatPrice(
|
||||
intl,
|
||||
|
||||
@@ -104,10 +104,17 @@ export default function PriceDetailsTable({
|
||||
<TableSection>
|
||||
<TableSectionHeader title={intl.formatMessage({ id: "Breakfast" })} />
|
||||
<Row
|
||||
label={intl.formatMessage(
|
||||
{ id: "booking.adults.breakfasts" },
|
||||
{ totalAdults: adults, totalBreakfasts: nights.length }
|
||||
)}
|
||||
label={`${intl.formatMessage(
|
||||
{
|
||||
id: "{totalAdults, plural, one {# voksen} other {# voksne}}",
|
||||
},
|
||||
{ totalAdults: adults }
|
||||
)}, ${intl.formatMessage(
|
||||
{
|
||||
id: "{totalBreakfasts, plural, one {# morgenmad} other {# morgenmad}}",
|
||||
},
|
||||
{ totalBreakfasts: nights.length }
|
||||
)}`}
|
||||
value={formatPrice(
|
||||
intl,
|
||||
parseInt(breakfast.localPrice.totalPrice),
|
||||
@@ -116,13 +123,17 @@ export default function PriceDetailsTable({
|
||||
/>
|
||||
{children?.length ? (
|
||||
<Row
|
||||
label={intl.formatMessage(
|
||||
{ id: "booking.children.breakfasts" },
|
||||
label={`${intl.formatMessage(
|
||||
{
|
||||
totalChildren: children.length,
|
||||
totalBreakfasts: nights.length,
|
||||
}
|
||||
)}
|
||||
id: "{totalChildren, plural, one {# child} other {# children}}",
|
||||
},
|
||||
{ totalChildren: children.length }
|
||||
)}, ${intl.formatMessage(
|
||||
{
|
||||
id: "{totalBreakfasts, plural, one {# breakfast} other {# breakfasts}}",
|
||||
},
|
||||
{ totalBreakfasts: nights.length }
|
||||
)}`}
|
||||
value={formatPrice(intl, 0, breakfast.localPrice.currency)}
|
||||
/>
|
||||
) : null}
|
||||
@@ -131,17 +142,17 @@ export default function PriceDetailsTable({
|
||||
<TableSection>
|
||||
<TableSectionHeader title={intl.formatMessage({ id: "Total" })} />
|
||||
<Row
|
||||
label={intl.formatMessage({ id: "booking.vat.excl" })}
|
||||
label={intl.formatMessage({ id: "Price excluding VAT" })}
|
||||
value={formatPrice(intl, priceExclVat, totalPrice.local.currency)}
|
||||
/>
|
||||
<Row
|
||||
label={intl.formatMessage({ id: "booking.vat" }, { vat })}
|
||||
label={intl.formatMessage({ id: "VAT {vat}%" }, { vat })}
|
||||
value={formatPrice(intl, vatAmount, totalPrice.local.currency)}
|
||||
/>
|
||||
<tr className={styles.row}>
|
||||
<td>
|
||||
<Body textTransform="bold">
|
||||
{intl.formatMessage({ id: "booking.vat.incl" })}
|
||||
{intl.formatMessage({ id: "Price including VAT" })}
|
||||
</Body>
|
||||
</td>
|
||||
<td className={styles.price}>
|
||||
|
||||
@@ -107,7 +107,7 @@ export default function SummaryUI({
|
||||
const diff = dt(booking.toDate).diff(booking.fromDate, "days")
|
||||
|
||||
const nights = intl.formatMessage(
|
||||
{ id: "booking.nights" },
|
||||
{ id: "{totalNights, plural, one {# night} other {# nights}}" },
|
||||
{ totalNights: diff }
|
||||
)
|
||||
|
||||
@@ -123,6 +123,22 @@ export default function SummaryUI({
|
||||
}
|
||||
}
|
||||
|
||||
const adultsMsg = intl.formatMessage(
|
||||
{ id: "{totalAdults, plural, one {# adult} other {# adults}}" },
|
||||
{ totalAdults: adults }
|
||||
)
|
||||
|
||||
const guestsParts = [adultsMsg]
|
||||
if (children?.length) {
|
||||
const childrenMsg = intl.formatMessage(
|
||||
{
|
||||
id: "{totalChildren, plural, one {# child} other {# children}}",
|
||||
},
|
||||
{ totalChildren: children.length }
|
||||
)
|
||||
guestsParts.push(childrenMsg)
|
||||
}
|
||||
|
||||
return (
|
||||
<section className={styles.summary}>
|
||||
<header className={styles.header}>
|
||||
@@ -157,17 +173,7 @@ export default function SummaryUI({
|
||||
</Body>
|
||||
</div>
|
||||
<Caption color="uiTextMediumContrast">
|
||||
{`${intl.formatMessage(
|
||||
{ id: "booking.adults" },
|
||||
{ totalAdults: adults }
|
||||
)}${
|
||||
children?.length
|
||||
? `, ${intl.formatMessage(
|
||||
{ id: "booking.children" },
|
||||
{ totalChildren: children.length }
|
||||
)}`
|
||||
: ""
|
||||
}`}
|
||||
{guestsParts.join(", ")}
|
||||
</Caption>
|
||||
<Caption color="uiTextMediumContrast">{cancellationText}</Caption>
|
||||
<Modal
|
||||
@@ -231,7 +237,10 @@ export default function SummaryUI({
|
||||
<div className={styles.entry}>
|
||||
<div>
|
||||
<Body color="uiTextHighContrast">
|
||||
{`${intl.formatMessage({ id: "Crib (child)" })} × ${childBedCrib}`}
|
||||
{intl.formatMessage(
|
||||
{ id: "Crib (child) × {count}" },
|
||||
{ count: childBedCrib }
|
||||
)}
|
||||
</Body>
|
||||
<Caption color="uiTextMediumContrast">
|
||||
{intl.formatMessage({ id: "Based on availability" })}
|
||||
@@ -246,7 +255,12 @@ export default function SummaryUI({
|
||||
<div className={styles.entry}>
|
||||
<div>
|
||||
<Body color="uiTextHighContrast">
|
||||
{`${intl.formatMessage({ id: "Extra bed (child)" })} × ${childBedExtraBed}`}
|
||||
{intl.formatMessage(
|
||||
{ id: "Extra bed (child) × {count}" },
|
||||
{
|
||||
count: childBedExtraBed,
|
||||
}
|
||||
)}
|
||||
</Body>
|
||||
</div>
|
||||
<Body color="uiTextHighContrast">
|
||||
@@ -278,7 +292,9 @@ export default function SummaryUI({
|
||||
<div className={styles.entry}>
|
||||
<Caption color="uiTextMediumContrast">
|
||||
{intl.formatMessage(
|
||||
{ id: "booking.adults" },
|
||||
{
|
||||
id: "{totalAdults, plural, one {# adult} other {# adults}}",
|
||||
},
|
||||
{ totalAdults: adults }
|
||||
)}
|
||||
</Caption>
|
||||
@@ -294,7 +310,9 @@ export default function SummaryUI({
|
||||
<div className={styles.entry}>
|
||||
<Caption color="uiTextMediumContrast">
|
||||
{intl.formatMessage(
|
||||
{ id: "booking.children" },
|
||||
{
|
||||
id: "{totalChildren, plural, one {# child} other {# children}}",
|
||||
},
|
||||
{ totalChildren: children.length }
|
||||
)}
|
||||
</Caption>
|
||||
@@ -345,11 +363,15 @@ export default function SummaryUI({
|
||||
</Body>
|
||||
{totalPrice.requested && (
|
||||
<Caption color="uiTextMediumContrast">
|
||||
{intl.formatMessage({ id: "Approx." })}{" "}
|
||||
{formatPrice(
|
||||
intl,
|
||||
totalPrice.requested.price,
|
||||
totalPrice.requested.currency
|
||||
{intl.formatMessage(
|
||||
{ id: "Approx. {value}" },
|
||||
{
|
||||
value: formatPrice(
|
||||
intl,
|
||||
totalPrice.requested.price,
|
||||
totalPrice.requested.currency
|
||||
),
|
||||
}
|
||||
)}
|
||||
</Caption>
|
||||
)}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
"use client"
|
||||
|
||||
import { useParams } from "next/dist/client/components/navigation"
|
||||
import { memo, useCallback } from "react"
|
||||
import { useIntl } from "react-intl"
|
||||
@@ -64,6 +65,8 @@ function HotelCard({
|
||||
state,
|
||||
})
|
||||
|
||||
const addressStr = `${hotelData.address.streetAddress}, ${hotelData.address.city}`
|
||||
|
||||
return (
|
||||
<article
|
||||
className={classNames}
|
||||
@@ -94,9 +97,7 @@ function HotelCard({
|
||||
</Subtitle>
|
||||
<div className={styles.addressContainer}>
|
||||
<address className={styles.address}>
|
||||
<Caption color="uiTextPlaceholder">
|
||||
{hotelData.address.streetAddress}, {hotelData.address.city}
|
||||
</Caption>
|
||||
<Caption color="uiTextPlaceholder">{addressStr}</Caption>
|
||||
</address>
|
||||
<address className={styles.addressMobile}>
|
||||
<Caption color="burgundy" type="underline" asChild>
|
||||
@@ -112,7 +113,7 @@ function HotelCard({
|
||||
color="burgundy"
|
||||
size="small"
|
||||
>
|
||||
{hotelData.address.streetAddress}, {hotelData.address.city}
|
||||
{addressStr}
|
||||
</Link>
|
||||
</Caption>
|
||||
</address>
|
||||
@@ -121,7 +122,7 @@ function HotelCard({
|
||||
</div>
|
||||
<Caption color="uiTextPlaceholder">
|
||||
{intl.formatMessage(
|
||||
{ id: "Distance in km to city centre" },
|
||||
{ id: "{number} km to city centre" },
|
||||
{
|
||||
number: getSingleDecimal(
|
||||
hotelData.location.distanceToCentre / 1000
|
||||
|
||||
@@ -93,7 +93,13 @@ export default function ListingHotelCardDialog({
|
||||
{publicPrice && memberPrice && <Caption>/</Caption>}
|
||||
{memberPrice && (
|
||||
<Subtitle type="two" color="red" className={styles.memberPrice}>
|
||||
{memberPrice} {currency}
|
||||
{intl.formatMessage(
|
||||
{ id: "{price} {currency}" },
|
||||
{
|
||||
price: memberPrice,
|
||||
currency,
|
||||
}
|
||||
)}
|
||||
</Subtitle>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -88,7 +88,13 @@ export default function StandaloneHotelCardDialog({
|
||||
</Caption>
|
||||
{publicPrice && (
|
||||
<Subtitle type="two">
|
||||
{publicPrice} {currency}
|
||||
{intl.formatMessage(
|
||||
{ id: "{price} {currency}" },
|
||||
{
|
||||
price: publicPrice,
|
||||
currency,
|
||||
}
|
||||
)}
|
||||
<Body asChild>
|
||||
<span>/{intl.formatMessage({ id: "night" })}</span>
|
||||
</Body>
|
||||
@@ -100,7 +106,13 @@ export default function StandaloneHotelCardDialog({
|
||||
color="red"
|
||||
className={styles.memberPrice}
|
||||
>
|
||||
{memberPrice} {currency}
|
||||
{intl.formatMessage(
|
||||
{ id: "{price} {currency}" },
|
||||
{
|
||||
price: memberPrice,
|
||||
currency,
|
||||
}
|
||||
)}
|
||||
<Body asChild color="red">
|
||||
<span>/{intl.formatMessage({ id: "night" })}</span>
|
||||
</Body>
|
||||
|
||||
@@ -81,8 +81,10 @@ export default function HotelCardListing({
|
||||
) : activeFilters ? (
|
||||
<Alert
|
||||
type={AlertTypeEnum.Info}
|
||||
heading={intl.formatMessage({ id: "filters.nohotel.heading" })}
|
||||
text={intl.formatMessage({ id: "filters.nohotel.text" })}
|
||||
heading={intl.formatMessage({ id: "No hotels match your filters" })}
|
||||
text={intl.formatMessage({
|
||||
id: "It looks like no hotels match your filters. Try adjusting your search to find the perfect stay.",
|
||||
})}
|
||||
/>
|
||||
) : null}
|
||||
{showBackToTop && (
|
||||
|
||||
@@ -139,8 +139,10 @@ export default function FilterAndSortModal({
|
||||
onClick={() => handleApplyFiltersAndSorting(close)}
|
||||
>
|
||||
{intl.formatMessage(
|
||||
{ id: "See results" },
|
||||
{ count: resultCount }
|
||||
{ id: "See results ({ count })" },
|
||||
{
|
||||
count: resultCount,
|
||||
}
|
||||
)}
|
||||
</Button>
|
||||
<Button
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
"use client"
|
||||
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import { useHotelFilterStore } from "@/stores/hotel-filters"
|
||||
@@ -13,7 +14,7 @@ export default function HotelCount() {
|
||||
<Preamble>
|
||||
{intl.formatMessage(
|
||||
{
|
||||
id: "Hotel(s)",
|
||||
id: "{amount, plural, one {# hotel} other {# hotels}}",
|
||||
},
|
||||
{ amount: resultCount }
|
||||
)}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
"use client"
|
||||
|
||||
import { useRouter, useSearchParams } from "next/navigation"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import SelectionCard from "../SelectionCard"
|
||||
|
||||
@@ -11,6 +13,7 @@ export default function BedSelection({
|
||||
alternatives,
|
||||
nextPath,
|
||||
}: BedSelectionProps) {
|
||||
const intl = useIntl()
|
||||
const router = useRouter()
|
||||
const searchParams = useSearchParams()
|
||||
|
||||
@@ -46,7 +49,7 @@ export default function BedSelection({
|
||||
</ul>
|
||||
|
||||
<button type="submit" hidden>
|
||||
Submit
|
||||
{intl.formatMessage({ id: "Submit" })}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
"use client"
|
||||
|
||||
import { useRouter, useSearchParams } from "next/navigation"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import SelectionCard from "../SelectionCard"
|
||||
|
||||
@@ -11,6 +13,7 @@ export default function BreakfastSelection({
|
||||
alternatives,
|
||||
nextPath,
|
||||
}: BreakfastSelectionProps) {
|
||||
const intl = useIntl()
|
||||
const router = useRouter()
|
||||
const searchParams = useSearchParams()
|
||||
|
||||
@@ -49,7 +52,7 @@ export default function BreakfastSelection({
|
||||
</ul>
|
||||
|
||||
<button type="submit" hidden>
|
||||
Submit
|
||||
{intl.formatMessage({ id: "Submit" })}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
"use client"
|
||||
|
||||
import { useSearchParams } from "next/navigation"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import Button from "@/components/TempDesignSystem/Button"
|
||||
|
||||
@@ -8,12 +10,13 @@ import styles from "./details.module.css"
|
||||
import type { DetailsProps } from "@/types/components/hotelReservation/selectRate/section"
|
||||
|
||||
export default function Details({ nextPath }: DetailsProps) {
|
||||
const intl = useIntl()
|
||||
const searchParams = useSearchParams()
|
||||
|
||||
return (
|
||||
<div className={styles.wrapper}>
|
||||
<form method="GET" action={`${nextPath}?${searchParams}`}>
|
||||
<Button type="submit">Submit</Button>
|
||||
<Button type="submit">{intl.formatMessage({ id: "Submit" })}</Button>
|
||||
</form>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -70,7 +70,18 @@ export default async function HotelInfoCard({
|
||||
</Title>
|
||||
<div className={styles.hotelAddressDescription}>
|
||||
<Caption color="uiTextMediumContrast">
|
||||
{`${hotelAttributes.address.streetAddress}, ${hotelAttributes.address.city} ∙ ${getSingleDecimal(hotelAttributes.location.distanceToCentre / 1000)} ${intl.formatMessage({ id: "km to city center" })}`}
|
||||
{intl.formatMessage(
|
||||
{
|
||||
id: "{address}, {city} ∙ {distanceToCityCentreInKm} km to city center",
|
||||
},
|
||||
{
|
||||
address: hotelAttributes.address.streetAddress,
|
||||
city: hotelAttributes.address.city,
|
||||
distanceToCityCentreInKm: getSingleDecimal(
|
||||
hotelAttributes.location.distanceToCentre / 1000
|
||||
),
|
||||
}
|
||||
)}
|
||||
</Caption>
|
||||
<Body color="uiTextHighContrast">
|
||||
{hotelAttributes.hotelContent.texts.descriptions.medium}
|
||||
|
||||
@@ -84,8 +84,12 @@ export default function RoomFilter({
|
||||
<div className={styles.infoDesktop}>
|
||||
<Body color="uiTextHighContrast">
|
||||
{intl.formatMessage(
|
||||
{ id: "Room types available" },
|
||||
{ numberOfRooms }
|
||||
{
|
||||
id: "{numberOfRooms, plural, one {# room type} other {# room types}} available",
|
||||
},
|
||||
{
|
||||
numberOfRooms,
|
||||
}
|
||||
)}
|
||||
</Body>
|
||||
</div>
|
||||
@@ -117,8 +121,12 @@ export default function RoomFilter({
|
||||
</div>
|
||||
<Caption color="uiTextHighContrast">
|
||||
{intl.formatMessage(
|
||||
{ id: "Room types available" },
|
||||
{ numberOfRooms }
|
||||
{
|
||||
id: "{numberOfRooms, plural, one {# room type} other {# room types}} available",
|
||||
},
|
||||
{
|
||||
numberOfRooms,
|
||||
}
|
||||
)}
|
||||
</Caption>
|
||||
</div>
|
||||
@@ -138,7 +146,7 @@ export default function RoomFilter({
|
||||
<CheckboxChip
|
||||
name={code}
|
||||
key={code}
|
||||
label={intl.formatMessage({ id: description })}
|
||||
label={description}
|
||||
disabled={isDisabled}
|
||||
selected={getValues(code)}
|
||||
Icon={getIconForFeatureCode(code)}
|
||||
|
||||
@@ -85,7 +85,7 @@ export default function PriceList({
|
||||
</div>
|
||||
) : (
|
||||
<Subtitle type="two" color="baseTextDisabled">
|
||||
{intl.formatMessage({ id: "n/a" })}
|
||||
{intl.formatMessage({ id: "N/A" })}
|
||||
</Subtitle>
|
||||
)}
|
||||
</dd>
|
||||
@@ -112,7 +112,7 @@ export default function PriceList({
|
||||
</div>
|
||||
) : (
|
||||
<Body textTransform="bold" color="disabled">
|
||||
- {intl.formatMessage({ id: "Currency Code" })}
|
||||
-
|
||||
</Body>
|
||||
)}
|
||||
</dd>
|
||||
@@ -126,9 +126,14 @@ export default function PriceList({
|
||||
</dt>
|
||||
<dd>
|
||||
<Caption color="uiTextMediumContrast">
|
||||
{totalPublicRequestedPricePerNight}/
|
||||
{totalMemberRequestedPricePerNight}{" "}
|
||||
{publicRequestedPrice.currency}
|
||||
{intl.formatMessage(
|
||||
{ id: "{publicPrice}/{memberPrice} {currency}" },
|
||||
{
|
||||
publicPrice: totalPublicRequestedPricePerNight,
|
||||
memberPrice: totalMemberRequestedPricePerNight,
|
||||
currency: publicRequestedPrice.currency,
|
||||
}
|
||||
)}
|
||||
</Caption>
|
||||
</dd>
|
||||
</div>
|
||||
|
||||
@@ -80,15 +80,15 @@ export default function RateSummary({
|
||||
const showMemberDiscountBanner = member && !isUserLoggedIn
|
||||
|
||||
const summaryPriceText = `${intl.formatMessage(
|
||||
{ id: "booking.nights" },
|
||||
{ id: "{totalNights, plural, one {# night} other {# nights}}" },
|
||||
{ totalNights: nights }
|
||||
)}, ${intl.formatMessage(
|
||||
{ id: "booking.adults" },
|
||||
{ id: "{totalAdults, plural, one {# adult} other {# adults}}" },
|
||||
{ totalAdults: roomsAvailability.occupancy?.adults }
|
||||
)}${
|
||||
roomsAvailability.occupancy?.children?.length
|
||||
? `, ${intl.formatMessage(
|
||||
{ id: "booking.children" },
|
||||
{ id: "{totalChildren, plural, one {# child} other {# children}}" },
|
||||
{ totalChildren: roomsAvailability.occupancy.children.length }
|
||||
)}`
|
||||
: ""
|
||||
@@ -136,11 +136,15 @@ export default function RateSummary({
|
||||
</Subtitle>
|
||||
{totalPriceToShow?.requestedPrice ? (
|
||||
<Body color="uiTextMediumContrast">
|
||||
{intl.formatMessage({ id: "Approx." })}{" "}
|
||||
{formatPrice(
|
||||
intl,
|
||||
totalPriceToShow.requestedPrice.price,
|
||||
totalPriceToShow.requestedPrice.currency
|
||||
{intl.formatMessage(
|
||||
{ id: "Approx. {value}" },
|
||||
{
|
||||
value: formatPrice(
|
||||
intl,
|
||||
totalPriceToShow.requestedPrice.price,
|
||||
totalPriceToShow.requestedPrice.currency
|
||||
),
|
||||
}
|
||||
)}
|
||||
</Body>
|
||||
) : null}
|
||||
|
||||
@@ -118,10 +118,12 @@ export default function RoomCard({
|
||||
{roomConfiguration.roomsLeft > 0 &&
|
||||
roomConfiguration.roomsLeft < 5 && (
|
||||
<span className={styles.chip}>
|
||||
<Footnote
|
||||
color="burgundy"
|
||||
textTransform="uppercase"
|
||||
>{`${roomConfiguration.roomsLeft} ${intl.formatMessage({ id: "Left" })}`}</Footnote>
|
||||
<Footnote color="burgundy" textTransform="uppercase">
|
||||
{intl.formatMessage(
|
||||
{ id: "{amount, number} left" },
|
||||
{ amount: roomConfiguration.roomsLeft }
|
||||
)}
|
||||
</Footnote>
|
||||
</span>
|
||||
)}
|
||||
{roomConfiguration.features
|
||||
@@ -150,7 +152,7 @@ export default function RoomCard({
|
||||
<Caption color="uiTextMediumContrast" className={styles.guests}>
|
||||
{intl.formatMessage(
|
||||
{
|
||||
id: "booking.guests",
|
||||
id: "Max {max, plural, one {{range} guest} other {{range} guests}}",
|
||||
},
|
||||
{ max: totalOccupancy.max, range: totalOccupancy.range }
|
||||
)}
|
||||
@@ -159,9 +161,21 @@ export default function RoomCard({
|
||||
{roomSize && (
|
||||
<Caption color="uiTextMediumContrast">
|
||||
{roomSize.min === roomSize.max
|
||||
? roomSize.min
|
||||
: `${roomSize.min}-${roomSize.max}`}
|
||||
m²
|
||||
? intl.formatMessage(
|
||||
{ id: "{roomSize} m²" },
|
||||
{
|
||||
roomSize: roomSize.min,
|
||||
}
|
||||
)
|
||||
: intl.formatMessage(
|
||||
{
|
||||
id: "{roomSizeMin} - {roomSizeMax} m²",
|
||||
},
|
||||
{
|
||||
roomSizeMin: roomSize.min,
|
||||
roomSizeMax: roomSize.max,
|
||||
}
|
||||
)}
|
||||
</Caption>
|
||||
)}
|
||||
<div className={styles.toggleSidePeek}>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"use client"
|
||||
|
||||
import { useCallback, useEffect, useMemo, useState } from "react"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import RoomFilter from "../RoomFilter"
|
||||
import RoomSelection from "../RoomSelection"
|
||||
@@ -27,6 +28,8 @@ export default function Rooms({
|
||||
hotelType,
|
||||
isUserLoggedIn,
|
||||
}: SelectRateProps) {
|
||||
const intl = useIntl()
|
||||
|
||||
const visibleRooms: RoomConfiguration[] = useMemo(() => {
|
||||
const deduped = filterDuplicateRoomTypesByLowestPrice(
|
||||
roomsAvailability.roomConfigurations
|
||||
@@ -59,27 +62,27 @@ export default function Rooms({
|
||||
() => [
|
||||
{
|
||||
code: RoomPackageCodeEnum.ACCESSIBILITY_ROOM,
|
||||
description: "Accessible Room",
|
||||
description: intl.formatMessage({ id: "Accessible Room" }),
|
||||
itemCode: availablePackages.find(
|
||||
(pkg) => pkg.code === RoomPackageCodeEnum.ACCESSIBILITY_ROOM
|
||||
)?.itemCode,
|
||||
},
|
||||
{
|
||||
code: RoomPackageCodeEnum.ALLERGY_ROOM,
|
||||
description: "Allergy Room",
|
||||
description: intl.formatMessage({ id: "Allergy Room" }),
|
||||
itemCode: availablePackages.find(
|
||||
(pkg) => pkg.code === RoomPackageCodeEnum.ALLERGY_ROOM
|
||||
)?.itemCode,
|
||||
},
|
||||
{
|
||||
code: RoomPackageCodeEnum.PET_ROOM,
|
||||
description: "Pet Room",
|
||||
description: intl.formatMessage({ id: "Pet Room" }),
|
||||
itemCode: availablePackages.find(
|
||||
(pkg) => pkg.code === RoomPackageCodeEnum.PET_ROOM
|
||||
)?.itemCode,
|
||||
},
|
||||
],
|
||||
[availablePackages]
|
||||
[availablePackages, intl]
|
||||
)
|
||||
|
||||
const handleFilter = useCallback(
|
||||
|
||||
@@ -30,15 +30,20 @@ export default function SelectionCard({
|
||||
|
||||
<div>
|
||||
<Caption color="burgundy" className={styles.price}>
|
||||
{formatPrice(intl, price, currency)}/
|
||||
{intl.formatMessage({ id: "night" })}
|
||||
{intl.formatMessage(
|
||||
{ id: "{price}/night" },
|
||||
{
|
||||
price: formatPrice(intl, price, currency),
|
||||
}
|
||||
)}
|
||||
</Caption>
|
||||
|
||||
{membersPrice && (
|
||||
<Caption color="burgundy" className={styles.membersPrice}>
|
||||
{intl.formatMessage({ id: "Members" })}{" "}
|
||||
{formatPrice(intl, membersPrice, currency)}/
|
||||
{intl.formatMessage({ id: "night" })}
|
||||
{intl.formatMessage(
|
||||
{ id: "Members {price}/night" },
|
||||
{ price: formatPrice(intl, membersPrice, currency) }
|
||||
)}
|
||||
</Caption>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -77,16 +77,15 @@ export default function LanguageSwitcher({
|
||||
|
||||
const classNames = languageSwitcherVariants({ position })
|
||||
|
||||
const closeMsg = intl.formatMessage({ id: "Close language menu" })
|
||||
const openMsg = intl.formatMessage({ id: "Open language menu" })
|
||||
|
||||
return (
|
||||
<div className={classNames} ref={languageSwitcherRef}>
|
||||
<button
|
||||
type="button"
|
||||
className={styles.button}
|
||||
aria-label={intl.formatMessage({
|
||||
id: isLanguageSwitcherOpen
|
||||
? "Close language menu"
|
||||
: "Open language menu",
|
||||
})}
|
||||
aria-label={isLanguageSwitcherOpen ? closeMsg : openMsg}
|
||||
onClick={handleClick}
|
||||
>
|
||||
<GlobeIcon width={globeIconSize} height={globeIconSize} />
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
"use client"
|
||||
|
||||
import { AnimatePresence, motion } from "framer-motion"
|
||||
import { useState } from "react"
|
||||
|
||||
|
||||
@@ -100,7 +100,7 @@ function HotelListingMapContent({ hotelPins }: HotelListingMapContentProps) {
|
||||
{/* TODO: Handle when no price is available */}
|
||||
{hotelPrice
|
||||
? formatPrice(intl, hotelPrice, pin.currency)
|
||||
: "N/A"}
|
||||
: intl.formatMessage({ id: "N/A" })}
|
||||
</span>
|
||||
</Body>
|
||||
</span>
|
||||
|
||||
@@ -2,6 +2,7 @@ import {
|
||||
AdvancedMarker,
|
||||
AdvancedMarkerAnchorPoint,
|
||||
} from "@vis.gl/react-google-maps"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import Body from "@/components/TempDesignSystem/Text/Body"
|
||||
import Caption from "@/components/TempDesignSystem/Text/Caption"
|
||||
@@ -19,6 +20,8 @@ export default function HotelMapContent({
|
||||
onActivePoiChange,
|
||||
activePoi,
|
||||
}: HotelMapContentProps) {
|
||||
const intl = useIntl()
|
||||
|
||||
function toggleActivePoi(poiName: string) {
|
||||
onActivePoiChange?.(activePoi === poiName ? null : poiName)
|
||||
}
|
||||
@@ -51,7 +54,14 @@ export default function HotelMapContent({
|
||||
<span>
|
||||
{poi.name}
|
||||
<Caption asChild>
|
||||
<span>{poi.distance} km</span>
|
||||
<span>
|
||||
{intl.formatMessage(
|
||||
{ id: "{distanceInKm} km" },
|
||||
{
|
||||
distanceInKm: poi.distance,
|
||||
}
|
||||
)}
|
||||
</span>
|
||||
</Caption>
|
||||
</span>
|
||||
</Body>
|
||||
|
||||
@@ -38,6 +38,7 @@ function InnerModal({
|
||||
subtitle,
|
||||
}: PropsWithChildren<InnerModalProps>) {
|
||||
const intl = useIntl()
|
||||
|
||||
function modalStateHandler(newAnimationState: AnimationState) {
|
||||
setAnimation((currentAnimationState) =>
|
||||
newAnimationState === AnimationStateEnum.hidden &&
|
||||
@@ -72,7 +73,10 @@ function InnerModal({
|
||||
animate={animation}
|
||||
initial={"hidden"}
|
||||
>
|
||||
<Dialog className={styles.dialog} aria-label="Dialog">
|
||||
<Dialog
|
||||
className={styles.dialog}
|
||||
aria-label={intl.formatMessage({ id: "Dialog" })}
|
||||
>
|
||||
{({ close }) => (
|
||||
<>
|
||||
<header className={styles.header}>
|
||||
|
||||
@@ -42,7 +42,9 @@ export default function SurprisesNotification({
|
||||
toast.success(
|
||||
<>
|
||||
{intl.formatMessage(
|
||||
{ id: "Gift(s) added to your benefits" },
|
||||
{
|
||||
id: "{amount, plural, one {Gift} other {Gifts}} added to your benefits",
|
||||
},
|
||||
{ amount: surprises.length }
|
||||
)}
|
||||
<br />
|
||||
@@ -117,7 +119,10 @@ export default function SurprisesNotification({
|
||||
}}
|
||||
onAnimationComplete={confetti}
|
||||
>
|
||||
<Dialog aria-label="Surprises" className={styles.dialog}>
|
||||
<Dialog
|
||||
aria-label={intl.formatMessage({ id: "Surprises" })}
|
||||
className={styles.dialog}
|
||||
>
|
||||
{({ close }) => {
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -18,7 +18,7 @@ export default function Initial({ totalSurprises, onOpen }: InitialProps) {
|
||||
<>
|
||||
{intl.formatMessage<React.ReactNode>(
|
||||
{
|
||||
id: "You have <b>#</b> gifts waiting for you!",
|
||||
id: "You have <b>{amount}</b> gifts waiting for you!",
|
||||
},
|
||||
{
|
||||
amount: totalSurprises,
|
||||
@@ -52,7 +52,7 @@ export default function Initial({ totalSurprises, onOpen }: InitialProps) {
|
||||
>
|
||||
{intl.formatMessage(
|
||||
{
|
||||
id: "Open gift(s)",
|
||||
id: "Open {amount, plural, one {gift} other {gifts}}",
|
||||
},
|
||||
{ amount: totalSurprises }
|
||||
)}
|
||||
|
||||
@@ -23,25 +23,30 @@ export default function Slide({ surprise, membershipNumber }: SlideProps) {
|
||||
},
|
||||
dt()
|
||||
)
|
||||
|
||||
return (
|
||||
<Card title={surprise.label}>
|
||||
<Body textAlign="center">{surprise.description}</Body>
|
||||
<div className={styles.badge}>
|
||||
<Caption>
|
||||
{intl.formatMessage(
|
||||
{ id: "Expires at the earliest" },
|
||||
{ id: "Expires at the earliest {expirationDate}" },
|
||||
{
|
||||
date: dt(earliestExpirationDate)
|
||||
expirationDate: dt(earliestExpirationDate)
|
||||
.locale(lang)
|
||||
.format("D MMM YYYY"),
|
||||
}
|
||||
)}
|
||||
</Caption>
|
||||
<Caption>
|
||||
{intl.formatMessage({
|
||||
id: "Membership ID",
|
||||
})}{" "}
|
||||
{membershipNumber}
|
||||
{intl.formatMessage(
|
||||
{
|
||||
id: "Membership ID {id}",
|
||||
},
|
||||
{
|
||||
id: membershipNumber,
|
||||
}
|
||||
)}
|
||||
</Caption>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
@@ -16,8 +16,22 @@ export default function CheckinCheckOut({ checkin }: CheckInCheckOutProps) {
|
||||
variant="sidepeek"
|
||||
>
|
||||
<Body textTransform="bold">{intl.formatMessage({ id: "Hours" })}</Body>
|
||||
<Body>{`${intl.formatMessage({ id: "Check in from" })}: ${checkin.checkInTime}`}</Body>
|
||||
<Body>{`${intl.formatMessage({ id: "Check out at latest" })}: ${checkin.checkOutTime}`}</Body>
|
||||
<Body>
|
||||
{intl.formatMessage(
|
||||
{ id: "Check in from: {checkInTime}" },
|
||||
{
|
||||
checkInTime: checkin.checkInTime,
|
||||
}
|
||||
)}
|
||||
</Body>
|
||||
<Body>
|
||||
{intl.formatMessage(
|
||||
{ id: "Check out at latest: {checkOutTime}" },
|
||||
{
|
||||
checkOutTime: checkin.checkOutTime,
|
||||
}
|
||||
)}
|
||||
</Body>
|
||||
</AccordionItem>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import { IconName } from "@/types/components/icon"
|
||||
|
||||
export default function Parking({ parking }: ParkingProps) {
|
||||
const intl = useIntl()
|
||||
|
||||
return (
|
||||
<AccordionItem
|
||||
title={intl.formatMessage({ id: "Parking" })}
|
||||
@@ -18,39 +19,48 @@ export default function Parking({ parking }: ParkingProps) {
|
||||
className={styles.parking}
|
||||
variant="sidepeek"
|
||||
>
|
||||
{parking.map((p) => (
|
||||
<div key={p.name}>
|
||||
<Subtitle type="two">
|
||||
{`${intl.formatMessage({ id: p.type })} ${p?.name ? ` (${p.name})` : ""}`}
|
||||
</Subtitle>
|
||||
<ul className={styles.list}>
|
||||
{p?.address && (
|
||||
<li>
|
||||
<FilledHeartIcon color="baseIconLowContrast" />
|
||||
{`${intl.formatMessage({ id: "Address" })}: ${p.address}`}
|
||||
</li>
|
||||
)}
|
||||
{p?.numberOfParkingSpots !== undefined && (
|
||||
<li>
|
||||
<FilledHeartIcon color="baseIconLowContrast" />
|
||||
{intl.formatMessage(
|
||||
{ id: "Number of parking spots" },
|
||||
{ number: p.numberOfParkingSpots }
|
||||
)}
|
||||
</li>
|
||||
)}
|
||||
{p?.numberOfChargingSpaces !== undefined && (
|
||||
<li>
|
||||
<FilledHeartIcon color="baseIconLowContrast" />
|
||||
{intl.formatMessage(
|
||||
{ id: "Number of charging points for electric cars" },
|
||||
{ number: p.numberOfChargingSpaces }
|
||||
)}
|
||||
</li>
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
))}
|
||||
{parking.map((p) => {
|
||||
const title = `${p.type}${p.name ? ` (${p.name})` : ""}`
|
||||
|
||||
return (
|
||||
<div key={p.name}>
|
||||
<Subtitle type="two">{title}</Subtitle>
|
||||
<ul className={styles.list}>
|
||||
{p.address !== undefined && (
|
||||
<li>
|
||||
<FilledHeartIcon color="baseIconLowContrast" />$
|
||||
{intl.formatMessage(
|
||||
{ id: "Address: {address}" },
|
||||
{
|
||||
address: p.address,
|
||||
}
|
||||
)}
|
||||
</li>
|
||||
)}
|
||||
{p.numberOfParkingSpots !== undefined && (
|
||||
<li>
|
||||
<FilledHeartIcon color="baseIconLowContrast" />
|
||||
{intl.formatMessage(
|
||||
{ id: "Number of parking spots: {number}" },
|
||||
{ number: p.numberOfParkingSpots }
|
||||
)}
|
||||
</li>
|
||||
)}
|
||||
{p.numberOfChargingSpaces !== undefined && (
|
||||
<li>
|
||||
<FilledHeartIcon color="baseIconLowContrast" />
|
||||
{intl.formatMessage(
|
||||
{
|
||||
id: "Number of charging points for electric cars: {number}",
|
||||
},
|
||||
{ number: p.numberOfChargingSpaces }
|
||||
)}
|
||||
</li>
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</AccordionItem>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -13,7 +13,12 @@ export default function Restaurant({
|
||||
|
||||
return (
|
||||
<AccordionItem
|
||||
title={intl.formatMessage({ id: "Restaurant" }, { count: 1 })}
|
||||
title={intl.formatMessage(
|
||||
{
|
||||
id: "{totalRestaurants, plural, one {Restaurant} other {Restaurants}}",
|
||||
},
|
||||
{ totalRestaurants: 1 }
|
||||
)}
|
||||
icon={IconName.Restaurant}
|
||||
variant="sidepeek"
|
||||
>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user