Compare commits
12 Commits
fbdbd35813
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1dffeb6be7 | ||
|
|
1f1ed2e4f3 | ||
|
|
bc9eaf6706 | ||
|
|
549265cd34 | ||
|
|
989b18527e | ||
|
|
0cda37808e | ||
|
|
b3c4761ae5 | ||
|
|
dd65467573 | ||
|
|
eb45e6b294 | ||
|
|
6553fcf685 | ||
|
|
c2cf6b03a7 | ||
|
|
310ad7bc7f |
@@ -1,3 +1,4 @@
|
||||
.layout {
|
||||
font-family: var(--typography-Body-Regular-fontFamily);
|
||||
font-family:
|
||||
var(--Body-Paragraph-Font-family), var(--Body-Paragraph-Font-fallback);
|
||||
}
|
||||
|
||||
@@ -52,3 +52,4 @@ DTMC_ENTRA_ID_CLIENT=""
|
||||
DTMC_ENTRA_ID_ISSUER=""
|
||||
DTMC_ENTRA_ID_SECRET=""
|
||||
|
||||
NEXT_PUBLIC_NEW_POINTCLAIMS="true"
|
||||
|
||||
@@ -4,7 +4,8 @@
|
||||
|
||||
.layout {
|
||||
display: grid;
|
||||
font-family: var(--typography-Body-Regular-fontFamily);
|
||||
font-family:
|
||||
var(--Body-Paragraph-Font-family), var(--Body-Paragraph-Font-fallback);
|
||||
grid-template-rows: auto 1fr;
|
||||
min-height: 100dvh;
|
||||
max-width: var(--max-width-page);
|
||||
|
||||
@@ -1,12 +1,7 @@
|
||||
import { TrackingSDK } from "@scandic-hotels/tracking/TrackingSDK"
|
||||
import {
|
||||
getEurobonusMembership,
|
||||
scandicMembershipTypes,
|
||||
} from "@scandic-hotels/trpc/routers/user/helpers"
|
||||
import { getEurobonusMembership } from "@scandic-hotels/trpc/routers/user/helpers"
|
||||
|
||||
import { env } from "@/env/server"
|
||||
import {
|
||||
getBasicProfileSafely,
|
||||
getProfileSafely,
|
||||
getProfilingConsent,
|
||||
} from "@/lib/trpc/memoizedRequests"
|
||||
@@ -26,15 +21,7 @@ type MyPagesLayoutProps = React.PropsWithChildren<{
|
||||
breadcrumbs: React.ReactNode
|
||||
}>
|
||||
|
||||
export default async function MyPagesLayout(props: MyPagesLayoutProps) {
|
||||
if (env.ENABLE_PROFILE_CONSENT) {
|
||||
return <MyPagesLayoutWithConsent {...props} />
|
||||
}
|
||||
|
||||
return <MyPagesLayoutBase {...props} />
|
||||
}
|
||||
|
||||
async function MyPagesLayoutWithConsent({
|
||||
export default async function MyPagesLayout({
|
||||
breadcrumbs,
|
||||
children,
|
||||
}: MyPagesLayoutProps) {
|
||||
@@ -84,25 +71,3 @@ async function MyPagesLayoutWithConsent({
|
||||
</ProfilingConsentAlertProvider>
|
||||
)
|
||||
}
|
||||
|
||||
async function MyPagesLayoutBase({
|
||||
breadcrumbs,
|
||||
children,
|
||||
}: MyPagesLayoutProps) {
|
||||
const profile = await getBasicProfileSafely()
|
||||
const eurobonusMembership = profile?.loyalty?.memberships?.find(
|
||||
(m) => m.membershipType === scandicMembershipTypes.SAS_EB
|
||||
)
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div className={styles.layout}>
|
||||
{breadcrumbs}
|
||||
<div className={styles.content}>{children}</div>
|
||||
</div>
|
||||
|
||||
{eurobonusMembership && <SASLevelUpgradeCheck />}
|
||||
<Surprises />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,24 +1,13 @@
|
||||
import { redirect } from "next/navigation"
|
||||
|
||||
import { profile } from "@scandic-hotels/common/constants/routes/myPages"
|
||||
import { TrackingSDK } from "@scandic-hotels/tracking/TrackingSDK"
|
||||
|
||||
import { env } from "@/env/server"
|
||||
import { getProfile } from "@/lib/trpc/memoizedRequests"
|
||||
import { serverClient } from "@/lib/trpc/server"
|
||||
|
||||
import { ProfilingConsent } from "@/components/Forms/ProfilingConsent"
|
||||
import { getLang } from "@/i18n/serverContext"
|
||||
|
||||
import styles from "./page.module.css"
|
||||
|
||||
export default async function ProfilingConsentSlot() {
|
||||
const lang = await getLang()
|
||||
|
||||
if (!env.ENABLE_PROFILE_CONSENT) {
|
||||
redirect(profile[lang])
|
||||
}
|
||||
|
||||
const caller = await serverClient()
|
||||
const accountPage = await caller.contentstack.accountPage.get()
|
||||
const user = await getProfile()
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
.layout {
|
||||
display: grid;
|
||||
font-family: var(--typography-Body-Regular-fontFamily);
|
||||
font-family:
|
||||
var(--Body-Paragraph-Font-family), var(--Body-Paragraph-Font-fallback);
|
||||
grid-template-rows: auto 1fr;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
.layout {
|
||||
font-family: var(--typography-Body-Regular-fontFamily);
|
||||
font-family:
|
||||
var(--Body-Paragraph-Font-family), var(--Body-Paragraph-Font-fallback);
|
||||
}
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { redirect } from "next/navigation"
|
||||
import { z } from "zod"
|
||||
|
||||
import Footnote from "@scandic-hotels/design-system/Footnote"
|
||||
import Image from "@scandic-hotels/design-system/Image"
|
||||
import Link from "@scandic-hotels/design-system/OldDSLink"
|
||||
import { TextLink } from "@scandic-hotels/design-system/TextLink"
|
||||
import { Typography } from "@scandic-hotels/design-system/Typography"
|
||||
|
||||
import { env } from "@/env/server"
|
||||
@@ -94,27 +93,24 @@ export default async function SASxScandicLoginPage(
|
||||
{intentDescriptions[parsedParams.intent]}
|
||||
</p>
|
||||
</Typography>
|
||||
<Footnote textAlign="center">
|
||||
{intl.formatMessage(
|
||||
{
|
||||
id: "linkEuroBonusAccount.manualRedirectLinkMessage",
|
||||
defaultMessage:
|
||||
"If you are not redirected automatically, please <loginLink>click here</loginLink>.",
|
||||
},
|
||||
{
|
||||
loginLink: (str) => (
|
||||
<Link
|
||||
href={loginLink}
|
||||
color="red"
|
||||
size="tiny"
|
||||
textDecoration="underline"
|
||||
>
|
||||
{str}
|
||||
</Link>
|
||||
),
|
||||
}
|
||||
)}
|
||||
</Footnote>
|
||||
<Typography variant="Body/Supporting text (caption)/smRegular">
|
||||
<p style={{ textAlign: "center" }}>
|
||||
{intl.formatMessage(
|
||||
{
|
||||
id: "linkEuroBonusAccount.manualRedirectLinkMessage",
|
||||
defaultMessage:
|
||||
"If you are not redirected automatically, please <loginLink>click here</loginLink>.",
|
||||
},
|
||||
{
|
||||
loginLink: (str) => (
|
||||
<TextLink typography="Link/sm" href={loginLink}>
|
||||
{str}
|
||||
</TextLink>
|
||||
),
|
||||
}
|
||||
)}
|
||||
</p>
|
||||
</Typography>
|
||||
</SASModal>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -42,7 +42,8 @@
|
||||
width: 34px;
|
||||
height: 0px;
|
||||
padding: var(--Space-x3) 0;
|
||||
font-family: var(--typography-Body-Regular-fontFamily);
|
||||
font-family:
|
||||
var(--Body-Paragraph-Font-family), var(--Body-Paragraph-Font-fallback);
|
||||
border: 1px solid var(--Base-Border-Normal);
|
||||
border-radius: var(--Corner-Radius-md);
|
||||
text-align: center;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
.layout {
|
||||
background-color: var(--Background-Primary);
|
||||
font-family: var(--typography-Body-Regular-fontFamily);
|
||||
font-family:
|
||||
var(--Body-Paragraph-Font-family), var(--Body-Paragraph-Font-fallback);
|
||||
min-height: 100dvh;
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
.layout {
|
||||
font-family: var(--typography-Body-Regular-fontFamily);
|
||||
font-family:
|
||||
var(--Body-Paragraph-Font-family), var(--Body-Paragraph-Font-fallback);
|
||||
}
|
||||
|
||||
@@ -1,13 +1,29 @@
|
||||
import { Alert } from "@scandic-hotels/design-system/Alert"
|
||||
import { getAlertPhoneContactData } from "@scandic-hotels/trpc/routers/contentstack/base/utils"
|
||||
|
||||
import { serverClient } from "@/lib/trpc/server"
|
||||
|
||||
import type { AlertBlock } from "@scandic-hotels/trpc/types/blocks"
|
||||
|
||||
interface AlertBlockProps extends Pick<AlertBlock, "alert"> {}
|
||||
|
||||
export function AlertBlock({ alert }: AlertBlockProps) {
|
||||
export async function AlertBlock({ alert }: AlertBlockProps) {
|
||||
const caller = await serverClient()
|
||||
const contactConfig = await caller.contentstack.base.contact()
|
||||
if (!alert) {
|
||||
return null
|
||||
}
|
||||
|
||||
return <Alert {...alert} />
|
||||
const phoneContact =
|
||||
alert.phoneContact && contactConfig
|
||||
? getAlertPhoneContactData(alert, contactConfig)
|
||||
: null
|
||||
|
||||
return (
|
||||
<Alert
|
||||
{...alert}
|
||||
phoneContact={phoneContact}
|
||||
sidepeekCtaText={alert.sidepeekButton?.cta_text}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -16,18 +16,14 @@
|
||||
|
||||
.iconTh {
|
||||
padding: var(--Space-x5) var(--Space-x2) var(--Space-x2);
|
||||
font-weight: var(--typography-Caption-Regular-fontWeight);
|
||||
vertical-align: bottom;
|
||||
}
|
||||
|
||||
.summaryTh {
|
||||
font-size: var(--typography-Caption-Regular-fontSize);
|
||||
font-weight: var(--typography-Caption-Regular-fontWeight);
|
||||
padding: 0 var(--Space-x2) var(--Space-x2);
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.select {
|
||||
font-weight: var(--typography-Caption-Regular-fontWeight);
|
||||
padding: 0 var(--Space-x2) var(--Space-x2);
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { Typography } from "@scandic-hotels/design-system/Typography"
|
||||
|
||||
import MembershipLevelIcon from "@/components/Levels/Icon"
|
||||
|
||||
import LevelSummary from "../../LevelSummary"
|
||||
@@ -37,12 +39,14 @@ export default function DesktopHeader({
|
||||
<th />
|
||||
{levels.map((level, idx) => {
|
||||
return (
|
||||
<th
|
||||
key={"summary" + level.level_id + idx}
|
||||
className={styles.summaryTh}
|
||||
<Typography
|
||||
variant="Body/Supporting text (caption)/smRegular"
|
||||
key={"name" + level.level_id + idx}
|
||||
>
|
||||
<LevelSummary level={level} />
|
||||
</th>
|
||||
<th className={styles.summaryTh}>
|
||||
<LevelSummary level={level} />
|
||||
</th>
|
||||
</Typography>
|
||||
)
|
||||
})}
|
||||
</tr>
|
||||
|
||||
@@ -82,10 +82,12 @@ function RewardTableHeader({ name, description }: RewardTableHeaderProps) {
|
||||
</span>
|
||||
</hgroup>
|
||||
</summary>
|
||||
<p
|
||||
className={styles.rewardDescription}
|
||||
dangerouslySetInnerHTML={{ __html: description }}
|
||||
/>
|
||||
<Typography variant="Body/Supporting text (caption)/smRegular">
|
||||
<p
|
||||
className={styles.rewardDescription}
|
||||
dangerouslySetInnerHTML={{ __html: description }}
|
||||
/>
|
||||
</Typography>
|
||||
</details>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -15,14 +15,11 @@
|
||||
}
|
||||
|
||||
.td {
|
||||
font-size: var(--typography-Footnote-Regular-fontSize);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.rewardTh {
|
||||
padding: var(--Space-x3) var(--Space-x2);
|
||||
font-size: var(--typography-Caption-Regular-fontSize);
|
||||
font-weight: var(--typography-Caption-Regular-fontWeight);
|
||||
}
|
||||
|
||||
.details[open] .chevron {
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import { Typography } from "@scandic-hotels/design-system/Typography"
|
||||
|
||||
import styles from "./levelSummary.module.css"
|
||||
|
||||
import type { LevelSummaryProps } from "@/types/components/overviewTable"
|
||||
@@ -32,7 +34,9 @@ export default function LevelSummary({
|
||||
|
||||
return (
|
||||
<div className={styles.levelSummary}>
|
||||
<span className={styles.levelRequirements}>{pointsMsg}</span>
|
||||
<Typography variant="Label/xsRegular">
|
||||
<span className={styles.levelRequirements}>{pointsMsg}</span>
|
||||
</Typography>
|
||||
{showDescription && (
|
||||
<p className={styles.levelSummaryText}>{level.description}</p>
|
||||
)}
|
||||
|
||||
@@ -8,16 +8,14 @@
|
||||
|
||||
.levelRequirements {
|
||||
border-radius: var(--Corner-Radius-md);
|
||||
background-color: var(--Scandic-Brand-Pale-Peach);
|
||||
color: var(--Scandic-Peach-80);
|
||||
background-color: var(--Surface-Brand-Primary-1-Default);
|
||||
color: var(--Text-Interactive-Secondary);
|
||||
padding: var(--Space-x05) var(--Space-x1);
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.levelSummaryText {
|
||||
font-size: var(--typography-Caption-Regular-fontSize);
|
||||
line-height: var(--typography-Body-Regular-lineHeight);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
@@ -26,12 +24,3 @@
|
||||
padding: var(--Space-x05) var(--Space-x1);
|
||||
}
|
||||
}
|
||||
@media screen and (min-width: 1367px) {
|
||||
.levelRequirements {
|
||||
font-size: var(--typography-Footnote-Regular-fontSize);
|
||||
}
|
||||
|
||||
.levelSummaryText {
|
||||
font-size: var(--typography-Caption-Regular-fontSize);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,10 +27,12 @@ export default function RewardCard({
|
||||
</span>
|
||||
</hgroup>
|
||||
</summary>
|
||||
<p
|
||||
className={styles.rewardCardDescription}
|
||||
dangerouslySetInnerHTML={{ __html: description }}
|
||||
/>
|
||||
<Typography variant="Body/Supporting text (caption)/smRegular">
|
||||
<p
|
||||
className={styles.rewardCardDescription}
|
||||
dangerouslySetInnerHTML={{ __html: description }}
|
||||
/>
|
||||
</Typography>
|
||||
</details>
|
||||
</div>
|
||||
<div className={styles.rewardComparison}>
|
||||
|
||||
@@ -12,8 +12,6 @@
|
||||
}
|
||||
|
||||
.rewardCardDescription {
|
||||
font-size: var(--typography-Caption-Regular-fontSize);
|
||||
line-height: 150%;
|
||||
padding-right: var(--Space-x4);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Minus } from "react-feather"
|
||||
|
||||
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
|
||||
import { Typography } from "@scandic-hotels/design-system/Typography"
|
||||
|
||||
import styles from "./rewardValue.module.css"
|
||||
|
||||
@@ -21,8 +22,8 @@ export default function RewardValue({ reward }: RewardValueProps) {
|
||||
)
|
||||
}
|
||||
return (
|
||||
<div className={styles.rewardValueContainer}>
|
||||
<span className={styles.rewardValue}>{reward.value}</span>
|
||||
</div>
|
||||
<Typography variant="Body/Paragraph/mdBold">
|
||||
<div className={styles.rewardValueContainer}>{reward.value}</div>
|
||||
</Typography>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -7,17 +7,6 @@
|
||||
text-wrap: balance;
|
||||
}
|
||||
|
||||
.rewardValue {
|
||||
font-size: var(--typography-Body-Bold-fontSize);
|
||||
font-weight: var(--typography-Body-Bold-fontWeight);
|
||||
}
|
||||
|
||||
.rewardValueDetails {
|
||||
font-size: var(--typography-Footnote-Regular-fontSize);
|
||||
text-align: center;
|
||||
color: var(--UI-Grey-80);
|
||||
}
|
||||
|
||||
.checkIcon {
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,430 @@
|
||||
/* eslint-disable formatjs/no-literal-string-in-jsx */
|
||||
/* TODO remove disable and add i18n */
|
||||
/* TODO add analytics */
|
||||
import { zodResolver } from "@hookform/resolvers/zod"
|
||||
import { cx } from "class-variance-authority"
|
||||
import { useState } from "react"
|
||||
import { FormProvider, useForm, useWatch } from "react-hook-form"
|
||||
import { useIntl } from "react-intl"
|
||||
import z from "zod"
|
||||
|
||||
import { dt } from "@scandic-hotels/common/dt"
|
||||
import { Button } from "@scandic-hotels/design-system/Button"
|
||||
import { FormInput } from "@scandic-hotels/design-system/Form/FormInput"
|
||||
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
|
||||
import { LoadingSpinner } from "@scandic-hotels/design-system/LoadingSpinner"
|
||||
import { MessageBanner } from "@scandic-hotels/design-system/MessageBanner"
|
||||
import { Typography } from "@scandic-hotels/design-system/Typography"
|
||||
import { trpc } from "@scandic-hotels/trpc/client"
|
||||
|
||||
import useLang from "@/hooks/useLang"
|
||||
|
||||
import styles from "./claimPoints.module.css"
|
||||
|
||||
type PointClaimBookingInfo = {
|
||||
from: string
|
||||
to: string
|
||||
city: string
|
||||
hotel: string
|
||||
}
|
||||
export function ClaimPointsWizard({
|
||||
onSuccess,
|
||||
onClose,
|
||||
}: {
|
||||
onSuccess: () => void
|
||||
onClose: () => void
|
||||
}) {
|
||||
const [state, setState] = useState<
|
||||
"initial" | "loading" | "invalid" | "form"
|
||||
>("initial")
|
||||
const [bookingDetails, setBookingDetails] =
|
||||
useState<PointClaimBookingInfo | null>(null)
|
||||
|
||||
const { data, isLoading } = trpc.user.getSafely.useQuery()
|
||||
|
||||
if (state === "invalid") {
|
||||
return <InvalidBooking onClose={onClose} />
|
||||
}
|
||||
|
||||
if (state === "form") {
|
||||
if (isLoading) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<ClaimPointsForm
|
||||
onSuccess={onSuccess}
|
||||
initialData={{
|
||||
...bookingDetails,
|
||||
firstName: data?.firstName ?? "",
|
||||
lastName: data?.lastName ?? "",
|
||||
email: data?.email ?? "",
|
||||
phone: data?.phoneNumber ?? "",
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const handleBookingNumberEvent = (event: BookingNumberEvent) => {
|
||||
switch (event.type) {
|
||||
case "submit":
|
||||
setState("loading")
|
||||
break
|
||||
case "error":
|
||||
setState("initial")
|
||||
break
|
||||
case "invalid":
|
||||
setState("invalid")
|
||||
break
|
||||
case "success":
|
||||
setBookingDetails(event.data)
|
||||
setState("form")
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.introWrapper}>
|
||||
{state === "loading" && (
|
||||
<div
|
||||
className={styles.spinner}
|
||||
aria-live="polite"
|
||||
aria-label="Loading booking details, please wait.."
|
||||
>
|
||||
<LoadingSpinner />
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
className={cx(styles.options, { [styles.hidden]: state === "loading" })}
|
||||
>
|
||||
<section className={styles.sectionCard}>
|
||||
<div className={styles.sectionInfo}>
|
||||
<Typography variant="Body/Paragraph/mdBold">
|
||||
<h4>Claim points with booking number</h4>
|
||||
</Typography>
|
||||
<Typography variant="Body/Supporting text (caption)/smRegular">
|
||||
<p>
|
||||
Enter a valid booking number to load booking details
|
||||
automatically.
|
||||
</p>
|
||||
</Typography>
|
||||
</div>
|
||||
<BookingNumberInput onEvent={handleBookingNumberEvent} />
|
||||
</section>
|
||||
<Divider />
|
||||
<section className={styles.sectionCard}>
|
||||
<div className={styles.sectionInfo}>
|
||||
<Typography variant="Body/Paragraph/mdBold">
|
||||
<h4>Claim points without booking number</h4>
|
||||
</Typography>
|
||||
<Typography variant="Body/Supporting text (caption)/smRegular">
|
||||
<p>You need to add booking details in a form.</p>
|
||||
</Typography>
|
||||
</div>
|
||||
<Button variant="Secondary" onPress={() => setState("form")}>
|
||||
Fill form to claim points
|
||||
</Button>
|
||||
</section>
|
||||
</div>
|
||||
<MessageBanner
|
||||
type="info"
|
||||
text="Points can be claimed up to 6 months back if you were a member at the time of your stay."
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
type BookingNumberFormData = {
|
||||
bookingNumber: string
|
||||
}
|
||||
type BookingNumberEvent =
|
||||
| { type: "submit" }
|
||||
| { type: "success"; data: PointClaimBookingInfo }
|
||||
| { type: "error" }
|
||||
| { type: "invalid" }
|
||||
function BookingNumberInput({
|
||||
onEvent,
|
||||
}: {
|
||||
onEvent: (event: BookingNumberEvent) => void
|
||||
}) {
|
||||
const lang = useLang()
|
||||
const form = useForm<BookingNumberFormData>({
|
||||
resolver: zodResolver(
|
||||
z.object({
|
||||
bookingNumber: z
|
||||
.string()
|
||||
// TODO Check UX for validation as different environments have different lengths
|
||||
.min(9, { message: "Booking number must be 10 digits" })
|
||||
.max(10, { message: "Booking number must be 10 digits" }),
|
||||
})
|
||||
),
|
||||
defaultValues: {
|
||||
bookingNumber: "",
|
||||
},
|
||||
})
|
||||
|
||||
const confirmationNumber = useWatch({
|
||||
name: "bookingNumber",
|
||||
control: form.control,
|
||||
})
|
||||
|
||||
const { refetch, isFetching } =
|
||||
trpc.booking.findBookingForCurrentUser.useQuery(
|
||||
{
|
||||
confirmationNumber,
|
||||
lang,
|
||||
},
|
||||
{ enabled: false }
|
||||
)
|
||||
|
||||
const handleSubmit = async () => {
|
||||
onEvent({ type: "submit" })
|
||||
const result = await refetch()
|
||||
if (!result.data) {
|
||||
onEvent({ type: "error" })
|
||||
form.setError("bookingNumber", {
|
||||
type: "manual",
|
||||
message:
|
||||
"We could not find a booking with this number registered in your name.",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const data = result.data
|
||||
|
||||
// TODO validate if this should be check out or check in date
|
||||
const checkOutDate = dt(data.booking.checkOutDate)
|
||||
const sixMonthsAgo = dt().subtract(6, "months")
|
||||
if (checkOutDate.isBefore(sixMonthsAgo, "day")) {
|
||||
onEvent({ type: "invalid" })
|
||||
return
|
||||
}
|
||||
|
||||
onEvent({
|
||||
type: "success",
|
||||
data: {
|
||||
from: data.booking.checkInDate,
|
||||
to: data.booking.checkOutDate,
|
||||
city: data.hotel.cityName,
|
||||
hotel: data.hotel.name,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<FormProvider {...form}>
|
||||
<form onSubmit={form.handleSubmit(handleSubmit)}>
|
||||
<FormInput
|
||||
name="bookingNumber"
|
||||
label="Booking number"
|
||||
leftIcon={<MaterialIcon icon="edit_document" />}
|
||||
description="Enter your 10-digit booking number"
|
||||
maxLength={10}
|
||||
showClearContentIcon
|
||||
disabled={isFetching}
|
||||
autoFocus
|
||||
autoComplete="off"
|
||||
onChange={(e) => {
|
||||
const value = e.target.value
|
||||
if (value.length !== 10) return
|
||||
|
||||
form.handleSubmit(handleSubmit)()
|
||||
}}
|
||||
/>
|
||||
</form>
|
||||
</FormProvider>
|
||||
)
|
||||
}
|
||||
|
||||
function InvalidBooking({ onClose }: { onClose: () => void }) {
|
||||
return (
|
||||
<div className={styles.invalidWrapper}>
|
||||
<Typography variant="Body/Paragraph/mdRegular">
|
||||
<p>
|
||||
We can’t add these points to your account as it has been longer than 6
|
||||
months since your stay.
|
||||
</p>
|
||||
</Typography>
|
||||
<Button variant="Primary" fullWidth onPress={onClose}>
|
||||
Close
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
type PointClaimUserInfo = {
|
||||
firstName: string
|
||||
lastName: string
|
||||
email: string
|
||||
phone: string
|
||||
}
|
||||
function ClaimPointsForm({
|
||||
onSuccess,
|
||||
initialData,
|
||||
}: {
|
||||
onSuccess: () => void
|
||||
initialData: Partial<PointClaimBookingInfo & PointClaimUserInfo> | null
|
||||
}) {
|
||||
const form = useForm({
|
||||
resolver: zodResolver(
|
||||
z.object({
|
||||
from: z.string().min(1, { message: "Arrival date is required" }),
|
||||
to: z.string().min(1, { message: "Departure date is required" }),
|
||||
city: z.string().min(1, { message: "City is required" }),
|
||||
hotel: z.string().min(1, { message: "Hotel is required" }),
|
||||
firstName: z.string().min(1, { message: "First name is required" }),
|
||||
lastName: z.string().min(1, { message: "Last name is required" }),
|
||||
email: z
|
||||
.string()
|
||||
.email("Enter a valid email")
|
||||
.min(1, { message: "Email is required" }),
|
||||
phone: z.string().min(1, { message: "Phone is required" }),
|
||||
})
|
||||
),
|
||||
defaultValues: {
|
||||
from: initialData?.from || "",
|
||||
to: initialData?.to || "",
|
||||
city: initialData?.city || "",
|
||||
hotel: initialData?.hotel || "",
|
||||
firstName: initialData?.firstName || "",
|
||||
lastName: initialData?.lastName || "",
|
||||
email: initialData?.email || "",
|
||||
phone: initialData?.phone || "",
|
||||
},
|
||||
mode: "all",
|
||||
})
|
||||
|
||||
const { mutate, isPending } = trpc.user.claimPoints.useMutation({
|
||||
onSuccess,
|
||||
})
|
||||
|
||||
const autoFocusField = getAutoFocus(initialData)
|
||||
|
||||
return (
|
||||
<FormProvider {...form}>
|
||||
<form
|
||||
className={styles.form}
|
||||
onSubmit={form.handleSubmit((data) => mutate(data))}
|
||||
>
|
||||
<div className={styles.formInputs}>
|
||||
{!initialData?.firstName && (
|
||||
<FormInput
|
||||
name="firstName"
|
||||
label="First name"
|
||||
autoFocus={autoFocusField === "firstName"}
|
||||
readOnly={isPending}
|
||||
registerOptions={{ required: true }}
|
||||
/>
|
||||
)}
|
||||
{!initialData?.lastName && (
|
||||
<FormInput
|
||||
name="lastName"
|
||||
label="Last name"
|
||||
autoFocus={autoFocusField === "lastName"}
|
||||
readOnly={isPending}
|
||||
registerOptions={{ required: true }}
|
||||
/>
|
||||
)}
|
||||
{!initialData?.email && (
|
||||
<FormInput
|
||||
name="email"
|
||||
label="Email"
|
||||
type="email"
|
||||
autoFocus={autoFocusField === "email"}
|
||||
readOnly={isPending}
|
||||
registerOptions={{ required: true }}
|
||||
/>
|
||||
)}
|
||||
{!initialData?.phone && (
|
||||
<FormInput
|
||||
name="phone"
|
||||
label="Phone"
|
||||
type="tel"
|
||||
autoFocus={autoFocusField === "phone"}
|
||||
readOnly={isPending}
|
||||
registerOptions={{ required: true }}
|
||||
/>
|
||||
)}
|
||||
<FormInput
|
||||
name="from"
|
||||
label="Arrival (YYYY-MM-DD)"
|
||||
leftIcon={<MaterialIcon icon="calendar_today" />}
|
||||
autoFocus={autoFocusField === "from"}
|
||||
readOnly={isPending}
|
||||
registerOptions={{ required: true }}
|
||||
/>
|
||||
<FormInput
|
||||
name="to"
|
||||
label="Departure (YYYY-MM-DD)"
|
||||
leftIcon={<MaterialIcon icon="calendar_today" />}
|
||||
readOnly={isPending}
|
||||
registerOptions={{ required: true }}
|
||||
/>
|
||||
<FormInput
|
||||
name="city"
|
||||
label="City"
|
||||
readOnly={isPending}
|
||||
registerOptions={{ required: true }}
|
||||
/>
|
||||
<FormInput
|
||||
name="hotel"
|
||||
label="Hotel"
|
||||
readOnly={isPending}
|
||||
registerOptions={{ required: true }}
|
||||
/>
|
||||
</div>
|
||||
<MessageBanner
|
||||
type="info"
|
||||
text="Points can be claimed up to 6 months back if you were a member at the time of your stay."
|
||||
/>
|
||||
<Button
|
||||
type="submit"
|
||||
variant="Primary"
|
||||
fullWidth
|
||||
isDisabled={!form.formState.isValid}
|
||||
isPending={isPending}
|
||||
className={styles.formSubmit}
|
||||
>
|
||||
Send points claim
|
||||
</Button>
|
||||
</form>
|
||||
</FormProvider>
|
||||
)
|
||||
}
|
||||
|
||||
function getAutoFocus(userInfo: Partial<PointClaimUserInfo> | null) {
|
||||
if (!userInfo?.firstName) {
|
||||
return "firstName"
|
||||
}
|
||||
|
||||
if (!userInfo?.lastName) {
|
||||
return "lastName"
|
||||
}
|
||||
|
||||
if (!userInfo?.email) {
|
||||
return "email"
|
||||
}
|
||||
|
||||
if (!userInfo?.phone) {
|
||||
return "phone"
|
||||
}
|
||||
|
||||
return "from"
|
||||
}
|
||||
|
||||
function Divider() {
|
||||
const intl = useIntl()
|
||||
|
||||
return (
|
||||
<div className={styles.divider}>
|
||||
<Typography variant="Body/Paragraph/mdRegular">
|
||||
<span>
|
||||
{intl.formatMessage({
|
||||
id: "common.or",
|
||||
defaultMessage: "or",
|
||||
})}
|
||||
</span>
|
||||
</Typography>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -6,3 +6,100 @@
|
||||
gap: var(--Space-x2);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.dialog {
|
||||
max-width: 560px;
|
||||
}
|
||||
|
||||
.introWrapper {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--Space-x3);
|
||||
}
|
||||
|
||||
.options {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--Space-x1);
|
||||
}
|
||||
|
||||
.sectionCard {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--Space-x3);
|
||||
padding: var(--Space-x2);
|
||||
background-color: var(--Surface-Primary-OnSurface-Default);
|
||||
border-radius: var(--Corner-Radius-md);
|
||||
}
|
||||
|
||||
.sectionInfo {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--Space-x1);
|
||||
}
|
||||
|
||||
.spinner {
|
||||
background-color: var(--Base-Surface-Primary-light-Normal);
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.bookingInputDescription {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--Space-x05);
|
||||
}
|
||||
|
||||
.form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--Space-x3);
|
||||
}
|
||||
|
||||
.formInputs {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--Space-x2);
|
||||
}
|
||||
|
||||
.formSubmit {
|
||||
margin-top: auto;
|
||||
}
|
||||
|
||||
.divider {
|
||||
width: 100%;
|
||||
position: relative;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
||||
& > span {
|
||||
position: relative;
|
||||
padding: 0 var(--Space-x2);
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
&::before {
|
||||
position: absolute;
|
||||
bottom: calc(50% - 1px);
|
||||
content: "";
|
||||
display: block;
|
||||
height: 1px;
|
||||
width: 100%;
|
||||
background-color: var(--Border-Default);
|
||||
}
|
||||
}
|
||||
|
||||
.hidden {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.invalidWrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--Space-x3);
|
||||
}
|
||||
|
||||
@@ -1,18 +1,110 @@
|
||||
/* eslint-disable formatjs/no-literal-string-in-jsx */
|
||||
/* TODO remove disable and add i18n */
|
||||
"use client"
|
||||
|
||||
import { useEffect, useState } from "react"
|
||||
import { Dialog } from "react-aria-components"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import { Button } from "@scandic-hotels/design-system/Button"
|
||||
import ButtonLink from "@scandic-hotels/design-system/ButtonLink"
|
||||
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
|
||||
import Modal from "@scandic-hotels/design-system/Modal"
|
||||
import { toast } from "@scandic-hotels/design-system/Toast"
|
||||
import { Typography } from "@scandic-hotels/design-system/Typography"
|
||||
|
||||
import { missingPoints } from "@/constants/missingPointsHrefs"
|
||||
import { env } from "@/env/client"
|
||||
|
||||
import useLang from "@/hooks/useLang"
|
||||
|
||||
import { ClaimPointsWizard } from "./ClaimPointsWizard"
|
||||
|
||||
import styles from "./claimPoints.module.css"
|
||||
|
||||
export default function ClaimPoints() {
|
||||
const intl = useIntl()
|
||||
const [openModal, setOpenModal] = useLinkableModalState("claim-points")
|
||||
|
||||
const useNewFlow = env.NEXT_PUBLIC_NEW_POINTCLAIMS
|
||||
if (!useNewFlow) {
|
||||
return <OldClaimPointsLink />
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={styles.claim}>
|
||||
<Typography variant="Body/Supporting text (caption)/smRegular">
|
||||
<p>
|
||||
{intl.formatMessage({
|
||||
id: "points.claimPoints.missingPreviousStay",
|
||||
defaultMessage: "Missing a previous stay?",
|
||||
})}
|
||||
</p>
|
||||
</Typography>
|
||||
<Button variant="Text" size="sm" onPress={() => setOpenModal(true)}>
|
||||
{intl.formatMessage({
|
||||
id: "points.claimPoints.cta",
|
||||
defaultMessage: "Claim points",
|
||||
})}
|
||||
</Button>
|
||||
</div>
|
||||
<Modal
|
||||
title="Add missing points"
|
||||
isOpen={openModal}
|
||||
onToggle={(open) => setOpenModal(open)}
|
||||
>
|
||||
<Dialog aria-label="TODO" className={styles.dialog}>
|
||||
{({ close }) => (
|
||||
<ClaimPointsWizard
|
||||
onSuccess={() => {
|
||||
toast.info(
|
||||
<>
|
||||
<Typography variant="Body/Paragraph/mdBold">
|
||||
<p>We're on it!</p>
|
||||
</Typography>
|
||||
<Typography variant="Body/Paragraph/mdRegular">
|
||||
<p>
|
||||
If your points have not been added to your account
|
||||
within 2 weeks, please contact us.
|
||||
</p>
|
||||
</Typography>
|
||||
</>,
|
||||
{
|
||||
duration: Infinity,
|
||||
}
|
||||
)
|
||||
close()
|
||||
}}
|
||||
onClose={close}
|
||||
/>
|
||||
)}
|
||||
</Dialog>
|
||||
</Modal>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function useLinkableModalState(target: string) {
|
||||
const [openModal, setOpenModal] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
const params = new URLSearchParams(window.location.search)
|
||||
const claimPoints = params.get("target") === target
|
||||
|
||||
if (claimPoints) {
|
||||
params.delete("target")
|
||||
const newUrl = `${window.location.pathname}?${params.toString()}`
|
||||
window.history.replaceState({}, "", newUrl)
|
||||
// eslint-disable-next-line react-hooks/set-state-in-effect
|
||||
setOpenModal(true)
|
||||
}
|
||||
}, [target])
|
||||
|
||||
return [openModal, setOpenModal] as const
|
||||
}
|
||||
|
||||
function OldClaimPointsLink() {
|
||||
const intl = useIntl()
|
||||
const lang = useLang()
|
||||
|
||||
|
||||
@@ -104,7 +104,7 @@ function getDescription(transaction: Transaction, intl: IntlShape) {
|
||||
|
||||
if (isNonTransactional && transaction.attributes.nights === 0) {
|
||||
return intl.formatMessage({
|
||||
id: "myPoints.pointTransactions.pointsActivity",
|
||||
id: "earnAndBurn.journeyTable.pointsActivity",
|
||||
defaultMessage: "Point activity",
|
||||
})
|
||||
}
|
||||
@@ -113,7 +113,7 @@ function getDescription(transaction: Transaction, intl: IntlShape) {
|
||||
if (hotelInformation?.name) {
|
||||
return intl.formatMessage(
|
||||
{
|
||||
id: "myPoints.pointTransactions.stayAt",
|
||||
id: "earnAndBurn.journeyTable.stayAt",
|
||||
defaultMessage: "Stay at {hotelName}",
|
||||
},
|
||||
{ hotelName: hotelInformation.name }
|
||||
@@ -124,53 +124,53 @@ function getDescription(transaction: Transaction, intl: IntlShape) {
|
||||
case Transactions.rewardType.stayAdj:
|
||||
if (transaction.attributes.hotelOperaId === "ORS") {
|
||||
return intl.formatMessage({
|
||||
id: "myPoints.pointTransactions.formerScandicHotel",
|
||||
id: "earnAndBurn.journeyTable.formerScandicHotel",
|
||||
defaultMessage: "Former Scandic Hotel",
|
||||
})
|
||||
}
|
||||
if (isBalfwd) {
|
||||
return intl.formatMessage({
|
||||
id: "myPoints.pointTransactions.pointsEarnedPriorMay2021",
|
||||
id: "earnAndBurn.journeyTable.pointsEarnedPriorMay2021",
|
||||
defaultMessage: "Points earned prior to May 1, 2021",
|
||||
})
|
||||
}
|
||||
case Transactions.rewardType.redgift:
|
||||
return intl.formatMessage({
|
||||
id: "myPoints.pointTransactions.redGift",
|
||||
id: "earnAndBurn.journeyTable.redGift",
|
||||
defaultMessage: "Reward Gift",
|
||||
})
|
||||
case Transactions.rewardType.rewardNight:
|
||||
return intl.formatMessage({
|
||||
id: "myPoints.pointTransactions.rewardNight",
|
||||
id: "earnAndBurn.journeyTable.rewardNight",
|
||||
defaultMessage: "Reward Night",
|
||||
})
|
||||
case Transactions.rewardType.ancillary:
|
||||
return intl.formatMessage({
|
||||
id: "myPoints.pointTransactions.extrasToBooking",
|
||||
id: "earnAndBurn.journeyTable.extrasToBooking",
|
||||
defaultMessage: "Extras to your booking",
|
||||
})
|
||||
|
||||
case Transactions.rewardType.enrollment:
|
||||
return intl.formatMessage({
|
||||
id: "myPoints.pointTransactions.signUpBonus",
|
||||
id: "earnAndBurn.journeyTable.signUpBonus",
|
||||
defaultMessage: "Sign up bonus",
|
||||
})
|
||||
|
||||
case Transactions.rewardType.mastercard_points:
|
||||
return intl.formatMessage({
|
||||
id: "myPoints.pointTransactions.scandicFriendsMastercard",
|
||||
id: "earnAndBurn.journeyTable.scandicFriendsMastercard",
|
||||
defaultMessage: "Scandic Friends Mastercard",
|
||||
})
|
||||
|
||||
case Transactions.rewardType.tui_points:
|
||||
return intl.formatMessage({
|
||||
id: "myPoints.pointTransactions.tuiPoints",
|
||||
id: "earnAndBurn.journeyTable.tuiPoints",
|
||||
defaultMessage: "TUI Points",
|
||||
})
|
||||
|
||||
case Transactions.rewardType.pointShop:
|
||||
return intl.formatMessage({
|
||||
id: "myPoints.pointTransactions.pointShop",
|
||||
id: "earnAndBurn.journeyTable.pointShop",
|
||||
defaultMessage: "Scandic Friends Point Shop",
|
||||
})
|
||||
default:
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { env } from "@/env/server"
|
||||
|
||||
import SignupForm from "@/components/Forms/Signup"
|
||||
|
||||
import type { SignupFormWrapperProps } from "@/types/components/blocks/dynamicContent"
|
||||
@@ -7,10 +5,5 @@ import type { SignupFormWrapperProps } from "@/types/components/blocks/dynamicCo
|
||||
export default async function SignupFormWrapper({
|
||||
dynamic_content,
|
||||
}: SignupFormWrapperProps) {
|
||||
return (
|
||||
<SignupForm
|
||||
{...dynamic_content}
|
||||
enableProfileConsent={env.ENABLE_PROFILE_CONSENT}
|
||||
/>
|
||||
)
|
||||
return <SignupForm {...dynamic_content} />
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import { useMediaQuery } from "usehooks-ts"
|
||||
|
||||
import { useMarkerHover } from "@scandic-hotels/common/hooks/map/useMarkerHover"
|
||||
import { InfoWindow } from "@scandic-hotels/design-system/Map/InfoWindow"
|
||||
import { Typography } from "@scandic-hotels/design-system/Typography"
|
||||
|
||||
import { useDestinationPageCitiesMapStore } from "@/stores/destination-page-cities-map"
|
||||
|
||||
@@ -79,7 +80,10 @@ export default function CityClusterMarker({
|
||||
})}
|
||||
anchorPoint={AdvancedMarkerAnchorPoint.CENTER}
|
||||
>
|
||||
<span className={styles.count}>{sizeAsText}</span>
|
||||
<Typography variant="Title/Subtitle/md">
|
||||
<span>{sizeAsText}</span>
|
||||
</Typography>
|
||||
|
||||
{isDesktop && isHovered ? (
|
||||
<InfoWindow
|
||||
position={position}
|
||||
|
||||
@@ -20,9 +20,3 @@
|
||||
height: 46px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.count {
|
||||
font-family: var(--typography-Body-Regular-fontFamily);
|
||||
font-size: var(--typography-Subtitle-2-fontSize);
|
||||
font-weight: var(--typography-Subtitle-2-fontWeight);
|
||||
}
|
||||
|
||||
@@ -19,12 +19,13 @@ div.months {
|
||||
td.day,
|
||||
td.rangeEnd,
|
||||
td.rangeStart {
|
||||
font-family: var(--typography-Body-Bold-fontFamily);
|
||||
font-size: var(--typography-Body-Bold-fontSize);
|
||||
font-weight: 500;
|
||||
letter-spacing: var(--typography-Body-Bold-letterSpacing);
|
||||
line-height: var(--typography-Body-Bold-lineHeight);
|
||||
text-decoration: var(--typography-Body-Bold-textDecoration);
|
||||
font-family:
|
||||
var(--Body-Paragraph-Font-family), var(--Body-Paragraph-Font-fallback);
|
||||
font-size: var(--Body-Paragraph-Size);
|
||||
font-weight: var(--Body-Paragraph-Font-weight-2);
|
||||
letter-spacing: var(--Body-Paragraph-Letter-spacing);
|
||||
line-height: 1.5;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
td.rangeEnd,
|
||||
@@ -90,14 +91,16 @@ td.day[data-outside="true"] ~ td.day[data-disabled="true"] button.dayButton,
|
||||
}
|
||||
|
||||
.weekDay {
|
||||
color: var(--UI-Text-Placeholder);
|
||||
font-family: var(--typography-Footnote-Labels-fontFamily);
|
||||
font-size: var(--typography-Footnote-Labels-fontSize);
|
||||
font-weight: var(--typography-Footnote-Labels-fontWeight);
|
||||
letter-spacing: var(--typography-Footnote-Labels-letterSpacing);
|
||||
line-height: var(--typography-Footnote-Labels-lineHeight);
|
||||
text-decoration: var(--typography-Footnote-Labels-textDecoration);
|
||||
text-transform: uppercase;
|
||||
color: var(--Text-Tertiary);
|
||||
font-family:
|
||||
var(--Title-Overline-sm-Font-family), var(--Title-Overline-sm-Font-fallback);
|
||||
font-size: var(--Title-Overline-sm-Size);
|
||||
font-style: normal;
|
||||
font-weight: var(--Title-Overline-sm-Font-weight);
|
||||
line-height: 1.5;
|
||||
letter-spacing: var(--Title-Overline-sm-Letter-spacing);
|
||||
text-transform: var(--Title-Overline-sm-Text-Transform);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.footer {
|
||||
|
||||
@@ -89,12 +89,13 @@ div.months {
|
||||
td.day,
|
||||
td.rangeEnd,
|
||||
td.rangeStart {
|
||||
font-family: var(--typography-Body-Bold-fontFamily);
|
||||
font-size: var(--typography-Body-Bold-fontSize);
|
||||
font-weight: 500;
|
||||
letter-spacing: var(--typography-Body-Bold-letterSpacing);
|
||||
line-height: var(--typography-Body-Bold-lineHeight);
|
||||
text-decoration: var(--typography-Body-Bold-textDecoration);
|
||||
font-family:
|
||||
var(--Body-Paragraph-Font-family), var(--Body-Paragraph-Font-fallback);
|
||||
font-size: var(--Body-Paragraph-Size);
|
||||
font-weight: var(--Body-Paragraph-Font-weight-2);
|
||||
letter-spacing: var(--Body-Paragraph-Letter-spacing);
|
||||
line-height: 1.5;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
td.rangeEnd,
|
||||
@@ -156,14 +157,16 @@ td.day[data-outside="true"] ~ td.day[data-disabled="true"] button.dayButton,
|
||||
}
|
||||
|
||||
.weekDay {
|
||||
color: var(--UI-Text-Placeholder);
|
||||
font-family: var(--typography-Caption-Labels-fontFamily);
|
||||
font-size: var(--typography-Caption-Labels-fontSize);
|
||||
font-weight: var(--typography-Caption-Labels-fontWeight);
|
||||
letter-spacing: var(--typography-Caption-Labels-letterSpacing);
|
||||
line-height: var(--typography-Caption-Labels-lineHeight);
|
||||
text-decoration: var(--typography-Caption-Labels-textDecoration);
|
||||
text-transform: uppercase;
|
||||
color: var(--Text-Tertiary);
|
||||
font-family:
|
||||
var(--Title-Overline-sm-Font-family), var(--Title-Overline-sm-Font-fallback);
|
||||
font-size: var(--Title-Overline-sm-Size);
|
||||
font-style: normal;
|
||||
font-weight: var(--Title-Overline-sm-Font-weight);
|
||||
line-height: 1.5;
|
||||
letter-spacing: var(--Title-Overline-sm-Letter-spacing);
|
||||
text-transform: var(--Title-Overline-sm-Text-Transform);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 1367px) {
|
||||
|
||||
@@ -107,7 +107,6 @@ export default function Form({ user }: EditFormProps) {
|
||||
} else {
|
||||
router.push(profile[lang])
|
||||
}
|
||||
router.refresh() // Can be removed on NextJs 15
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
@@ -48,14 +48,9 @@ import styles from "./form.module.css"
|
||||
|
||||
interface SignUpFormProps {
|
||||
title: string
|
||||
enableProfileConsent?: boolean
|
||||
}
|
||||
|
||||
export default function SignupForm({
|
||||
title,
|
||||
// Handled as a prop rather than a client env var due to limits in Netlify env var size.
|
||||
enableProfileConsent = false,
|
||||
}: SignUpFormProps) {
|
||||
export default function SignupForm({ title }: SignUpFormProps) {
|
||||
const intl = useIntl()
|
||||
const router = useRouter()
|
||||
const lang = useLang()
|
||||
@@ -140,7 +135,7 @@ export default function SignupForm({
|
||||
|
||||
return (
|
||||
<div className={styles.formWrapper}>
|
||||
{enableProfileConsent && <ProfilingConsentModalReadOnly />}
|
||||
<ProfilingConsentModalReadOnly />
|
||||
{title ? (
|
||||
<Typography variant="Title/md">
|
||||
<h2>{title}</h2>
|
||||
@@ -293,41 +288,39 @@ export default function SignupForm({
|
||||
/>
|
||||
</section>
|
||||
|
||||
{enableProfileConsent && (
|
||||
<section className={styles.personalization}>
|
||||
<header>
|
||||
<Typography variant="Title/Subtitle/md">
|
||||
<h3>
|
||||
{intl.formatMessage({
|
||||
id: "signup.UnlockYourPersonalizedExperience",
|
||||
defaultMessage: "Unlock your personalized experience!",
|
||||
})}
|
||||
</h3>
|
||||
</Typography>
|
||||
</header>
|
||||
<Checkbox
|
||||
name="profilingConsent"
|
||||
registerOptions={{ required: true }}
|
||||
>
|
||||
{intl.formatMessage({
|
||||
id: "signup.yesConsent",
|
||||
defaultMessage:
|
||||
"I consent to Scandic using my information to give me even more personalized travel inspiration and offers from Scandic and trusted Scandic Friends partners. This means Scandic may use information about my interactions with Scandic Friends partners, and share details of my interactions with Scandic with those partners, to make the experience even more relevant to me.",
|
||||
})}
|
||||
</Checkbox>
|
||||
<TextLinkButton
|
||||
typography="Link/sm"
|
||||
color="Primary"
|
||||
className={styles.personalizationButton}
|
||||
onClick={openPersonalizationModal}
|
||||
>
|
||||
{intl.formatMessage({
|
||||
id: "signup.ReadMoreAboutPersonalization",
|
||||
defaultMessage: "Read more about personalization at Scandic",
|
||||
})}
|
||||
</TextLinkButton>
|
||||
</section>
|
||||
)}
|
||||
<section className={styles.personalization}>
|
||||
<header>
|
||||
<Typography variant="Title/Subtitle/md">
|
||||
<h3>
|
||||
{intl.formatMessage({
|
||||
id: "signup.UnlockYourPersonalizedExperience",
|
||||
defaultMessage: "Unlock your personalized experience!",
|
||||
})}
|
||||
</h3>
|
||||
</Typography>
|
||||
</header>
|
||||
<Checkbox
|
||||
name="profilingConsent"
|
||||
registerOptions={{ required: true }}
|
||||
>
|
||||
{intl.formatMessage({
|
||||
id: "signup.yesConsent",
|
||||
defaultMessage:
|
||||
"I consent to Scandic using my information to give me even more personalized travel inspiration and offers from Scandic and trusted Scandic Friends partners. This means Scandic may use information about my interactions with Scandic Friends partners, and share details of my interactions with Scandic with those partners, to make the experience even more relevant to me.",
|
||||
})}
|
||||
</Checkbox>
|
||||
<TextLinkButton
|
||||
typography="Link/sm"
|
||||
color="Primary"
|
||||
className={styles.personalizationButton}
|
||||
onClick={openPersonalizationModal}
|
||||
>
|
||||
{intl.formatMessage({
|
||||
id: "signup.ReadMoreAboutPersonalization",
|
||||
defaultMessage: "Read more about personalization at Scandic",
|
||||
})}
|
||||
</TextLinkButton>
|
||||
</section>
|
||||
|
||||
<section className={styles.terms}>
|
||||
<header>
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
text-decoration-skip-ink: none;
|
||||
text-decoration-thickness: auto;
|
||||
text-underline-offset: auto;
|
||||
text-align: center;
|
||||
text-underline-position: from-font;
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useIntl } from "react-intl"
|
||||
|
||||
import { sumPackages } from "@scandic-hotels/booking-flow/utils/SelectRate"
|
||||
import { CurrencyEnum } from "@scandic-hotels/common/constants/currency"
|
||||
import { dt } from "@scandic-hotels/common/dt"
|
||||
import { formatPrice } from "@scandic-hotels/common/utils/numberFormatting"
|
||||
import { trpc } from "@scandic-hotels/trpc/client"
|
||||
|
||||
@@ -66,9 +67,14 @@ export default function Steps({ closeModal }: ChangeDatesStepsProps) {
|
||||
|
||||
setDates({ fromDate, toDate })
|
||||
|
||||
const numberOfNights = dt(toDate).diff(dt(fromDate), "days")
|
||||
|
||||
const pkgsSum = sumPackages(packages)
|
||||
const extraPrice =
|
||||
pkgsSum.price + ((breakfast && breakfast.localPrice.totalPrice) || 0)
|
||||
const breakfastPrice = !!breakfast
|
||||
? breakfast.localPrice.price * numberOfNights
|
||||
: 0
|
||||
|
||||
const extraPrice = pkgsSum.price + breakfastPrice
|
||||
if (isLoggedIn && "member" in data.product && data.product.member) {
|
||||
const { currency, pricePerStay } = data.product.member.localPrice
|
||||
setNewPrice(formatPrice(intl, pricePerStay + extraPrice, currency))
|
||||
|
||||
@@ -4,13 +4,6 @@
|
||||
padding: var(--Space-x3) var(--Space-x2);
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-family: var(--typography-Subtitle-2-fontFamily);
|
||||
font-size: var(--typography-Subtitle-2-Mobile-fontSize);
|
||||
font-weight: var(--typography-Subtitle-2-fontWeight);
|
||||
color: var(--Base-Text-High-contrast);
|
||||
}
|
||||
|
||||
.list {
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
@@ -16,8 +16,8 @@
|
||||
cursor: pointer;
|
||||
height: 32px;
|
||||
width: 32px;
|
||||
font-size: var(--typography-Body-Bold-fontSize);
|
||||
font-weight: var(--typography-Body-Bold-fontWeight);
|
||||
font-size: var(--Body-Paragraph-Size);
|
||||
font-weight: var(--Body-Paragraph-Font-weight-2);
|
||||
padding: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { env } from "@/env/server"
|
||||
|
||||
import { getIntl } from "@/i18n"
|
||||
|
||||
import { Section } from "../Section"
|
||||
@@ -17,7 +15,7 @@ export async function CommunicationSettings() {
|
||||
})}
|
||||
>
|
||||
<EmailSlot />
|
||||
{env.ENABLE_PROFILE_CONSENT && <PersonalizationSlot />}
|
||||
<PersonalizationSlot />
|
||||
</Section>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { Typography } from "@scandic-hotels/design-system/Typography"
|
||||
|
||||
import { env } from "@/env/server"
|
||||
import { getProfile, getProfilingConsent } from "@/lib/trpc/memoizedRequests"
|
||||
|
||||
import { GetMainIconByCSIdentifier, userHasConsent } from "../utils"
|
||||
@@ -9,8 +8,6 @@ import { BannerButton } from "./Button"
|
||||
import styles from "./profilingConsentBanner.module.css"
|
||||
|
||||
export async function ProfilingConsentBanner() {
|
||||
if (!env.ENABLE_PROFILE_CONSENT) return null
|
||||
|
||||
const user = await getProfile()
|
||||
if (!user || userHasConsent(user?.profilingConsent)) return null
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Profiling Consent
|
||||
|
||||
Profiling consent allows users to opt in/out of personalized experiences. The feature is controlled by the `ENABLE_PROFILE_CONSENT` environment variable.
|
||||
Profiling consent allows users to opt in/out of personalized experiences.
|
||||
|
||||
## User Journey
|
||||
|
||||
@@ -121,11 +121,9 @@ Replace `<memberKey>` with the actual `membershipNumber` or `profileId`.
|
||||
Required content for the feature:
|
||||
|
||||
1. **Profiling Consent (config)**
|
||||
|
||||
- Config needs to be created and published in each language
|
||||
|
||||
2. **/consent (account page)**
|
||||
|
||||
- Page needs to be created and published in each language
|
||||
|
||||
3. **/overview (account page)**
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--Space-x05);
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.link {
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
import Footnote from "@scandic-hotels/design-system/Footnote"
|
||||
import {
|
||||
MaterialIcon,
|
||||
type MaterialIconSetIconProps,
|
||||
} from "@scandic-hotels/design-system/Icons/MaterialIcon"
|
||||
import Link from "@scandic-hotels/design-system/OldDSLink"
|
||||
import { TextLink } from "@scandic-hotels/design-system/TextLink"
|
||||
import { Typography } from "@scandic-hotels/design-system/Typography"
|
||||
import { getValueFromContactConfig } from "@scandic-hotels/trpc/utils/contactConfig"
|
||||
|
||||
import { serverClient } from "@/lib/trpc/server"
|
||||
|
||||
// import { getValueFromContactConfig } from "@/utils/contactConfig"
|
||||
import styles from "./contactRow.module.css"
|
||||
|
||||
import type { ContactRowProps } from "@/types/components/sidebar/joinLoyaltyContact"
|
||||
@@ -46,22 +44,27 @@ export default async function ContactRow({ contact }: ContactRowProps) {
|
||||
|
||||
return (
|
||||
<div className={styles.wrapper}>
|
||||
<Typography
|
||||
variant="Body/Paragraph/mdBold"
|
||||
className={styles.displayText}
|
||||
>
|
||||
<p>{contact.display_text}</p>
|
||||
</Typography>
|
||||
<Link
|
||||
{contact.display_text ? (
|
||||
<Typography
|
||||
variant="Body/Paragraph/mdBold"
|
||||
className={styles.displayText}
|
||||
>
|
||||
<p>{contact.display_text}</p>
|
||||
</Typography>
|
||||
) : null}
|
||||
<TextLink
|
||||
typography="Link/sm"
|
||||
className={styles.link}
|
||||
href={openableLink}
|
||||
textDecoration="underline"
|
||||
size="small"
|
||||
>
|
||||
{Icon ? <Icon size={20} color="Icon/Interactive/Default" /> : null}
|
||||
{val}
|
||||
</Link>
|
||||
{footnote && <Footnote color="burgundy">{footnote}</Footnote>}
|
||||
</TextLink>
|
||||
{footnote && (
|
||||
<Typography variant="Body/Supporting text (caption)/smRegular">
|
||||
<p>{footnote}</p>
|
||||
</Typography>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -13,18 +13,9 @@
|
||||
gap: var(--Space-x15);
|
||||
}
|
||||
|
||||
.contact > div {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 1367px) {
|
||||
.contactContainer {
|
||||
align-items: start;
|
||||
padding-top: var(--Space-x2);
|
||||
}
|
||||
|
||||
.contact > div {
|
||||
justify-content: start;
|
||||
}
|
||||
}
|
||||
|
||||
6
apps/scandic-web/env/client.ts
vendored
6
apps/scandic-web/env/client.ts
vendored
@@ -16,6 +16,11 @@ export const env = createEnv({
|
||||
.transform((s) =>
|
||||
getSemver("scandic-web", s, process.env.BRANCH || "development")
|
||||
),
|
||||
NEXT_PUBLIC_NEW_POINTCLAIMS: z
|
||||
.string()
|
||||
.optional()
|
||||
.default("false")
|
||||
.transform((s) => s === "true"),
|
||||
},
|
||||
emptyStringAsUndefined: true,
|
||||
runtimeEnv: {
|
||||
@@ -26,5 +31,6 @@ export const env = createEnv({
|
||||
process.env.NEXT_PUBLIC_SENTRY_CLIENT_SAMPLERATE,
|
||||
NEXT_PUBLIC_PUBLIC_URL: process.env.NEXT_PUBLIC_PUBLIC_URL,
|
||||
NEXT_PUBLIC_RELEASE_TAG: process.env.NEXT_PUBLIC_RELEASE_TAG,
|
||||
NEXT_PUBLIC_NEW_POINTCLAIMS: process.env.NEXT_PUBLIC_NEW_POINTCLAIMS,
|
||||
},
|
||||
})
|
||||
|
||||
6
apps/scandic-web/env/server.ts
vendored
6
apps/scandic-web/env/server.ts
vendored
@@ -96,11 +96,6 @@ export const env = createEnv({
|
||||
.refine((s) => s === "1" || s === "0")
|
||||
.transform((s) => s === "1")
|
||||
.default("0"),
|
||||
ENABLE_PROFILE_CONSENT: z
|
||||
.string()
|
||||
.refine((s) => s === "true" || s === "false")
|
||||
.transform((s) => s === "true")
|
||||
.default("false"),
|
||||
RELEASE_TAG: z
|
||||
.string()
|
||||
.optional()
|
||||
@@ -160,7 +155,6 @@ export const env = createEnv({
|
||||
DTMC_ENTRA_ID_SECRET: process.env.DTMC_ENTRA_ID_SECRET,
|
||||
CHATBOT_LIVE_LANGS: process.env.CHATBOT_LIVE_LANGS,
|
||||
SEO_INERT: process.env.SEO_INERT,
|
||||
ENABLE_PROFILE_CONSENT: process.env.ENABLE_PROFILE_CONSENT,
|
||||
RELEASE_TAG: process.env.NEXT_PUBLIC_RELEASE_TAG,
|
||||
},
|
||||
})
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 847 KiB After Width: | Height: | Size: 301 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 4.4 MiB After Width: | Height: | Size: 208 KiB |
@@ -1,13 +1,11 @@
|
||||
import Footnote from "@scandic-hotels/design-system/Footnote"
|
||||
import { Typography } from "@scandic-hotels/design-system/Typography"
|
||||
|
||||
import styles from "./list.module.css"
|
||||
|
||||
export default function Label({ children }: React.PropsWithChildren) {
|
||||
return (
|
||||
<li className={styles.label}>
|
||||
<Footnote color="uiTextPlaceholder" textTransform="uppercase">
|
||||
{children}
|
||||
</Footnote>
|
||||
</li>
|
||||
<Typography variant="Title/Overline/sm">
|
||||
<li className={styles.label}>{children}</li>
|
||||
</Typography>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -5,5 +5,6 @@
|
||||
}
|
||||
|
||||
.label {
|
||||
padding: 0 var(--Space-x1);
|
||||
padding: 0 var(--Space-x1) var(--Space-x05);
|
||||
color: var(--Text-Tertiary);
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ import { useIntl } from "react-intl"
|
||||
import { useDebounceValue } from "usehooks-ts"
|
||||
|
||||
import { Divider } from "@scandic-hotels/design-system/Divider"
|
||||
import Footnote from "@scandic-hotels/design-system/Footnote"
|
||||
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
|
||||
import { Typography } from "@scandic-hotels/design-system/Typography"
|
||||
import { trpc } from "@scandic-hotels/trpc/client"
|
||||
@@ -192,16 +191,14 @@ export default function SearchList({
|
||||
{typeFilteredSearchHistory && typeFilteredSearchHistory.length > 0 && (
|
||||
<>
|
||||
<Divider className={styles.noResultsDivider} />
|
||||
<Footnote
|
||||
className={styles.text}
|
||||
color="uiTextPlaceholder"
|
||||
textTransform="uppercase"
|
||||
>
|
||||
{intl.formatMessage({
|
||||
id: "bookingWidget.searchList.latestSearches",
|
||||
defaultMessage: "Latest searches",
|
||||
})}
|
||||
</Footnote>
|
||||
<Typography variant="Title/Overline/sm">
|
||||
<p className={styles.text}>
|
||||
{intl.formatMessage({
|
||||
id: "bookingWidget.searchList.latestSearches",
|
||||
defaultMessage: "Latest searches",
|
||||
})}
|
||||
</p>
|
||||
</Typography>
|
||||
<List
|
||||
getItemProps={getItemProps}
|
||||
highlightedIndex={highlightedIndex}
|
||||
@@ -226,12 +223,14 @@ export default function SearchList({
|
||||
if (displaySearchHistory) {
|
||||
return (
|
||||
<Dialog getMenuProps={getMenuProps}>
|
||||
<Footnote color="uiTextPlaceholder" textTransform="uppercase">
|
||||
{intl.formatMessage({
|
||||
id: "bookingWidget.searchList.latestSearches",
|
||||
defaultMessage: "Latest searches",
|
||||
})}
|
||||
</Footnote>
|
||||
<Typography variant="Title/Overline/sm">
|
||||
<p className={styles.text}>
|
||||
{intl.formatMessage({
|
||||
id: "bookingWidget.searchList.latestSearches",
|
||||
defaultMessage: "Latest searches",
|
||||
})}
|
||||
</p>
|
||||
</Typography>
|
||||
<List
|
||||
getItemProps={getItemProps}
|
||||
highlightedIndex={highlightedIndex}
|
||||
|
||||
@@ -33,6 +33,7 @@
|
||||
|
||||
.text {
|
||||
padding: 0 var(--Space-x1);
|
||||
color: var(--Text-Tertiary);
|
||||
white-space: normal;
|
||||
}
|
||||
.textPlaceholderColor {
|
||||
|
||||
@@ -65,7 +65,7 @@ export default function DatePickerRangeDesktop({
|
||||
range_start: styles.rangeStart,
|
||||
root: `${classNames.root} ${styles.container}`,
|
||||
week: styles.week,
|
||||
weekday: `${classNames.weekday} ${styles.weekDay}`,
|
||||
weekday: styles.weekDay,
|
||||
nav: `${classNames.nav} ${styles.nav}`,
|
||||
button_next: `${classNames.button_next} ${styles.button_next}`,
|
||||
button_previous: `${classNames.button_previous} ${styles.button_previous}`,
|
||||
|
||||
@@ -98,7 +98,7 @@ export default function DatePickerRangeMobile({
|
||||
range_start: styles.rangeStart,
|
||||
root: `${classNames.root} ${styles.root}`,
|
||||
week: styles.week,
|
||||
weekday: `${classNames.weekday} ${styles.weekDay}`,
|
||||
weekday: styles.weekDay,
|
||||
}}
|
||||
disabled={[
|
||||
{ from: lastDayOfPreviousMonth, to: yesterday },
|
||||
|
||||
@@ -20,12 +20,12 @@ div.months {
|
||||
td.day,
|
||||
td.rangeEnd,
|
||||
td.rangeStart {
|
||||
font-family: var(--typography-Body-Bold-fontFamily);
|
||||
font-size: var(--typography-Body-Bold-fontSize);
|
||||
font-weight: 500;
|
||||
letter-spacing: var(--typography-Body-Bold-letterSpacing);
|
||||
line-height: var(--typography-Body-Bold-lineHeight);
|
||||
text-decoration: var(--typography-Body-Bold-textDecoration);
|
||||
font-family: var(--Body-Paragraph-Font-family), var(--Body-Paragraph-Font-fallback);
|
||||
font-size: var(--Body-Paragraph-Size);
|
||||
font-weight: var(--Body-Paragraph-Font-weight-2);
|
||||
letter-spacing: var(--Body-Paragraph-Letter-spacing);
|
||||
line-height: 1.5;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
td.rangeEnd,
|
||||
@@ -92,14 +92,15 @@ td.day[data-outside="true"] ~ td.day[data-disabled="true"] button.dayButton,
|
||||
}
|
||||
|
||||
.weekDay {
|
||||
color: var(--UI-Text-Placeholder);
|
||||
font-family: var(--typography-Footnote-Labels-fontFamily);
|
||||
font-size: var(--typography-Footnote-Labels-fontSize);
|
||||
font-weight: var(--typography-Footnote-Labels-fontWeight);
|
||||
letter-spacing: var(--typography-Footnote-Labels-letterSpacing);
|
||||
line-height: var(--typography-Footnote-Labels-lineHeight);
|
||||
text-decoration: var(--typography-Footnote-Labels-textDecoration);
|
||||
text-transform: uppercase;
|
||||
color: var(--Text-Tertiary);
|
||||
font-family: var(--Title-Overline-sm-Font-family), var(--Title-Overline-sm-Font-fallback);
|
||||
font-size: var(--Title-Overline-sm-Size);
|
||||
font-style: normal;
|
||||
font-weight: var(--Title-Overline-sm-Font-weight);
|
||||
line-height: 1.5;
|
||||
letter-spacing: var(--Title-Overline-sm-Letter-spacing);
|
||||
text-transform: var(--Title-Overline-sm-Text-Transform);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.footer {
|
||||
|
||||
@@ -97,12 +97,12 @@ div.months {
|
||||
td.day,
|
||||
td.rangeEnd,
|
||||
td.rangeStart {
|
||||
font-family: var(--typography-Body-Bold-fontFamily);
|
||||
font-size: var(--typography-Body-Bold-fontSize);
|
||||
font-weight: 500;
|
||||
letter-spacing: var(--typography-Body-Bold-letterSpacing);
|
||||
line-height: var(--typography-Body-Bold-lineHeight);
|
||||
text-decoration: var(--typography-Body-Bold-textDecoration);
|
||||
font-family: var(--Body-Paragraph-Font-family), var(--Body-Paragraph-Font-fallback);
|
||||
font-size: var(--Body-Paragraph-Size);
|
||||
font-weight: var(--Body-Paragraph-Font-weight-2);
|
||||
letter-spacing: var(--Body-Paragraph-Letter-spacing);
|
||||
line-height: 1.5;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
td.rangeEnd,
|
||||
@@ -165,15 +165,15 @@ td.day[data-outside="true"] ~ td.day[data-disabled="true"] button.dayButton,
|
||||
}
|
||||
|
||||
.weekDay {
|
||||
color: var(--Base-Text-Medium-contrast);
|
||||
opacity: 1;
|
||||
font-family: var(--typography-Caption-Labels-fontFamily);
|
||||
font-size: var(--typography-Caption-Labels-fontSize);
|
||||
font-weight: var(--typography-Caption-Labels-fontWeight);
|
||||
letter-spacing: var(--typography-Caption-Labels-letterSpacing);
|
||||
line-height: var(--typography-Caption-Labels-lineHeight);
|
||||
text-decoration: var(--typography-Caption-Labels-textDecoration);
|
||||
text-transform: uppercase;
|
||||
color: var(--Text-Tertiary);
|
||||
font-family: var(--Title-Overline-sm-Font-family), var(--Title-Overline-sm-Font-fallback);
|
||||
font-size: var(--Title-Overline-sm-Size);
|
||||
font-style: normal;
|
||||
font-weight: var(--Title-Overline-sm-Font-weight);
|
||||
line-height: 1.5;
|
||||
letter-spacing: var(--Title-Overline-sm-Letter-spacing);
|
||||
text-transform: var(--Title-Overline-sm-Text-Transform);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 1367px) {
|
||||
|
||||
@@ -4,9 +4,8 @@ import { useIntl } from "react-intl"
|
||||
|
||||
import { CurrencyEnum } from "@scandic-hotels/common/constants/currency"
|
||||
import { formatPrice } from "@scandic-hotels/common/utils/numberFormatting"
|
||||
import Footnote from "@scandic-hotels/design-system/Footnote"
|
||||
import Checkbox from "@scandic-hotels/design-system/Form/Checkbox"
|
||||
import Link from "@scandic-hotels/design-system/OldDSLink"
|
||||
import { TextLink } from "@scandic-hotels/design-system/TextLink"
|
||||
import { Typography } from "@scandic-hotels/design-system/Typography"
|
||||
|
||||
import { useBookingFlowConfig } from "../../../../../bookingFlowConfig/bookingFlowConfigContext"
|
||||
@@ -83,8 +82,8 @@ export default function JoinScandicFriendsCard({
|
||||
</Typography>
|
||||
</Checkbox>
|
||||
|
||||
<div className={styles.terms}>
|
||||
<Footnote color="uiTextPlaceholder">
|
||||
<Typography variant="Body/Supporting text (caption)/smRegular">
|
||||
<p className={styles.terms}>
|
||||
{intl.formatMessage(
|
||||
{
|
||||
id: "enterDetails.joinScandicFriendsCard.terms",
|
||||
@@ -93,19 +92,18 @@ export default function JoinScandicFriendsCard({
|
||||
},
|
||||
{
|
||||
termsAndConditionsLink: (str) => (
|
||||
<Link
|
||||
textDecoration="underline"
|
||||
size="tiny"
|
||||
<TextLink
|
||||
typography="Link/sm"
|
||||
target="_blank"
|
||||
href={routes.membershipTermsAndConditions[lang]}
|
||||
>
|
||||
{str}
|
||||
</Link>
|
||||
</TextLink>
|
||||
),
|
||||
}
|
||||
)}
|
||||
</Footnote>
|
||||
</div>
|
||||
</p>
|
||||
</Typography>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
|
||||
.terms {
|
||||
grid-area: terms;
|
||||
color: var(--Text-Secondary);
|
||||
}
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
|
||||
@@ -4,9 +4,8 @@ import { useIntl } from "react-intl"
|
||||
|
||||
import { CurrencyEnum } from "@scandic-hotels/common/constants/currency"
|
||||
import { formatPrice } from "@scandic-hotels/common/utils/numberFormatting"
|
||||
import Footnote from "@scandic-hotels/design-system/Footnote"
|
||||
import Checkbox from "@scandic-hotels/design-system/Form/Checkbox"
|
||||
import Link from "@scandic-hotels/design-system/OldDSLink"
|
||||
import { TextLink } from "@scandic-hotels/design-system/TextLink"
|
||||
import { Typography } from "@scandic-hotels/design-system/Typography"
|
||||
|
||||
import { useBookingFlowConfig } from "../../../../../bookingFlowConfig/bookingFlowConfigContext"
|
||||
@@ -97,8 +96,8 @@ export function PartnerSASJoinScandicFriendsCard({
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={styles.terms}>
|
||||
<Footnote color="uiTextPlaceholder">
|
||||
<Typography variant="Body/Supporting text (caption)/smRegular">
|
||||
<p className={styles.terms}>
|
||||
{intl.formatMessage(
|
||||
{
|
||||
id: "enterDetails.joinScandicFriendsCard.terms",
|
||||
@@ -107,19 +106,18 @@ export function PartnerSASJoinScandicFriendsCard({
|
||||
},
|
||||
{
|
||||
termsAndConditionsLink: (str) => (
|
||||
<Link
|
||||
textDecoration="underline"
|
||||
size="tiny"
|
||||
<TextLink
|
||||
typography="Link/sm"
|
||||
target="_blank"
|
||||
href={routes.membershipTermsAndConditions[lang]}
|
||||
>
|
||||
{str}
|
||||
</Link>
|
||||
</TextLink>
|
||||
),
|
||||
}
|
||||
)}
|
||||
</Footnote>
|
||||
</div>
|
||||
</p>
|
||||
</Typography>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
|
||||
.terms {
|
||||
grid-area: terms;
|
||||
color: var(--Text-Secondary);
|
||||
}
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
|
||||
@@ -4,10 +4,9 @@ import { useIntl } from "react-intl"
|
||||
import { CurrencyEnum } from "@scandic-hotels/common/constants/currency"
|
||||
import { useLazyPathname } from "@scandic-hotels/common/hooks/useLazyPathname"
|
||||
import { formatPrice } from "@scandic-hotels/common/utils/numberFormatting"
|
||||
import Footnote from "@scandic-hotels/design-system/Footnote"
|
||||
import Checkbox from "@scandic-hotels/design-system/Form/Checkbox"
|
||||
import { LoginButton } from "@scandic-hotels/design-system/LoginButton"
|
||||
import Link from "@scandic-hotels/design-system/OldDSLink"
|
||||
import { TextLink } from "@scandic-hotels/design-system/TextLink"
|
||||
import { Typography } from "@scandic-hotels/design-system/Typography"
|
||||
import { trackEvent } from "@scandic-hotels/tracking/base"
|
||||
import { trackLoginClick } from "@scandic-hotels/tracking/navigation"
|
||||
@@ -101,8 +100,8 @@ export function JoinScandicFriendsCard({ name = "join" }: Props) {
|
||||
})}
|
||||
</LoginButton>
|
||||
|
||||
<div className={styles.terms}>
|
||||
<Footnote color="uiTextPlaceholder">
|
||||
<Typography variant="Body/Supporting text (caption)/smRegular">
|
||||
<p className={styles.terms}>
|
||||
{intl.formatMessage(
|
||||
{
|
||||
id: "enterDetails.joinScandicFriendsCard.terms",
|
||||
@@ -111,19 +110,18 @@ export function JoinScandicFriendsCard({ name = "join" }: Props) {
|
||||
},
|
||||
{
|
||||
termsAndConditionsLink: (str) => (
|
||||
<Link
|
||||
textDecoration="underline"
|
||||
size="tiny"
|
||||
<TextLink
|
||||
typography="Link/sm"
|
||||
target="_blank"
|
||||
href={routes.membershipTermsAndConditions[lang]}
|
||||
>
|
||||
{str}
|
||||
</Link>
|
||||
</TextLink>
|
||||
),
|
||||
}
|
||||
)}
|
||||
</Footnote>
|
||||
</div>
|
||||
</p>
|
||||
</Typography>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -34,6 +34,7 @@
|
||||
|
||||
.terms {
|
||||
grid-area: terms;
|
||||
color: var(--Text-Secondary);
|
||||
}
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
|
||||
@@ -4,9 +4,8 @@ import { useIntl } from "react-intl"
|
||||
|
||||
import { CurrencyEnum } from "@scandic-hotels/common/constants/currency"
|
||||
import { formatPrice } from "@scandic-hotels/common/utils/numberFormatting"
|
||||
import Footnote from "@scandic-hotels/design-system/Footnote"
|
||||
import Checkbox from "@scandic-hotels/design-system/Form/Checkbox"
|
||||
import Link from "@scandic-hotels/design-system/OldDSLink"
|
||||
import { TextLink } from "@scandic-hotels/design-system/TextLink"
|
||||
import { Typography } from "@scandic-hotels/design-system/Typography"
|
||||
import { trpc } from "@scandic-hotels/trpc/client"
|
||||
|
||||
@@ -94,8 +93,8 @@ export function PartnerSASJoinScandicFriendsCard({
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={styles.terms}>
|
||||
<Footnote color="uiTextPlaceholder">
|
||||
<Typography variant="Body/Supporting text (caption)/smRegular">
|
||||
<p className={styles.terms}>
|
||||
{intl.formatMessage(
|
||||
{
|
||||
id: "enterDetails.joinScandicFriendsCard.terms",
|
||||
@@ -104,19 +103,18 @@ export function PartnerSASJoinScandicFriendsCard({
|
||||
},
|
||||
{
|
||||
termsAndConditionsLink: (str) => (
|
||||
<Link
|
||||
textDecoration="underline"
|
||||
size="tiny"
|
||||
<TextLink
|
||||
typography="Link/sm"
|
||||
target="_blank"
|
||||
href={routes.membershipTermsAndConditions[lang]}
|
||||
>
|
||||
{str}
|
||||
</Link>
|
||||
</TextLink>
|
||||
),
|
||||
}
|
||||
)}
|
||||
</Footnote>
|
||||
</div>
|
||||
</p>
|
||||
</Typography>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
|
||||
.terms {
|
||||
grid-area: terms;
|
||||
color: var(--Text-Secondary);
|
||||
}
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
|
||||
@@ -6,7 +6,6 @@ import { useIntl } from "react-intl"
|
||||
|
||||
import { selectRate } from "@scandic-hotels/common/constants/routes/hotelReservation"
|
||||
import { Button } from "@scandic-hotels/design-system/Button"
|
||||
import Footnote from "@scandic-hotels/design-system/Footnote"
|
||||
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
|
||||
import { Typography } from "@scandic-hotels/design-system/Typography"
|
||||
import { getHotelRoom } from "@scandic-hotels/trpc/routers/booking/helpers"
|
||||
@@ -47,20 +46,14 @@ export default function SelectedRoom() {
|
||||
<div className={styles.wrapper} data-available={room.isAvailable}>
|
||||
<div className={styles.main}>
|
||||
<div className={styles.headerContainer}>
|
||||
<Footnote
|
||||
className={styles.title}
|
||||
asChild
|
||||
textTransform="uppercase"
|
||||
type="label"
|
||||
color="uiTextHighContrast"
|
||||
>
|
||||
<h2>
|
||||
<Typography variant="Title/Overline/sm">
|
||||
<h2 className={styles.title}>
|
||||
{intl.formatMessage({
|
||||
id: "common.room",
|
||||
defaultMessage: "Room",
|
||||
})}
|
||||
</h2>
|
||||
</Footnote>
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="Title/Subtitle/md"
|
||||
className={styles.description}
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
}
|
||||
|
||||
.facilities {
|
||||
font-family: var(--typography-Body-Bold-fontFamily);
|
||||
font-family: var(--Body-Paragraph-Font-family), var(--Body-Paragraph-Font-fallback);
|
||||
padding-bottom: var(--Space-x3);
|
||||
}
|
||||
|
||||
|
||||
@@ -177,12 +177,12 @@ export function SelectHotelMapContent({
|
||||
>
|
||||
<MaterialIcon icon="close" size={20} color="CurrentColor" />
|
||||
<Typography variant="Body/Supporting text (caption)/smBold">
|
||||
<p>
|
||||
<span>
|
||||
{intl.formatMessage({
|
||||
id: "selectHotel.closeMap",
|
||||
defaultMessage: "Close the map",
|
||||
})}
|
||||
</p>
|
||||
</span>
|
||||
</Typography>
|
||||
</Link>
|
||||
</Button>
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
.link {
|
||||
display: flex;
|
||||
gap: var(--Space-x05);
|
||||
align-items: baseline;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.bookingCodeFilter {
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import { formatPrice } from "@scandic-hotels/common/utils/numberFormatting"
|
||||
import Footnote from "@scandic-hotels/design-system/Footnote"
|
||||
import { Typography } from "@scandic-hotels/design-system/Typography"
|
||||
|
||||
import { useBookingFlowConfig } from "../../bookingFlowConfig/bookingFlowConfigContext"
|
||||
@@ -37,9 +36,11 @@ export default function SignupPromoDesktop({
|
||||
data-testid="signup-promo-desktop"
|
||||
>
|
||||
{badgeContent && <span className={styles.badge}>{badgeContent}</span>}
|
||||
<Footnote color="burgundy">
|
||||
<Message price={price} isEnterDetailsPage={isEnterDetailsPage} />
|
||||
</Footnote>
|
||||
<Typography variant="Body/Supporting text (caption)/smRegular">
|
||||
<p>
|
||||
<Message price={price} isEnterDetailsPage={isEnterDetailsPage} />
|
||||
</p>
|
||||
</Typography>
|
||||
</div>
|
||||
) : null
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import Footnote from "@scandic-hotels/design-system/Footnote"
|
||||
import { Typography } from "@scandic-hotels/design-system/Typography"
|
||||
|
||||
import { useBookingFlowConfig } from "../../bookingFlowConfig/bookingFlowConfigContext"
|
||||
|
||||
@@ -14,9 +14,11 @@ export default function SignupPromoMobile() {
|
||||
data-footer-spacing-signup
|
||||
className={styles.memberDiscountBannerMobile}
|
||||
>
|
||||
<Footnote color="burgundy">
|
||||
<Message />
|
||||
</Footnote>
|
||||
<Typography variant="Body/Supporting text (caption)/smRegular">
|
||||
<p>
|
||||
<Message />
|
||||
</p>
|
||||
</Typography>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--Text-Default);
|
||||
}
|
||||
|
||||
.memberDiscountBannerDesktop {
|
||||
@@ -16,10 +17,11 @@
|
||||
padding: var(--Space-x15) var(--Space-x2);
|
||||
gap: var(--Space-x2);
|
||||
position: relative;
|
||||
color: var(--Text-Default);
|
||||
}
|
||||
|
||||
.red {
|
||||
color: var(--Text-Accent-Primary);
|
||||
color: var(--Scandic-Brand-Scandic-Red);
|
||||
}
|
||||
|
||||
.badge {
|
||||
|
||||
@@ -1,82 +0,0 @@
|
||||
.footnote {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.footnoteFontOnly {
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
.bold {
|
||||
font-family: var(--typography-Footnote-Bold-fontFamily);
|
||||
font-size: var(--typography-Footnote-Bold-fontSize);
|
||||
font-weight: var(--typography-Footnote-Bold-fontWeight);
|
||||
letter-spacing: var(--typography-Footnote-Bold-letterSpacing);
|
||||
line-height: var(--typography-Footnote-Bold-lineHeight);
|
||||
text-decoration: var(--typography-Footnote-Bold-textDecoration);
|
||||
}
|
||||
|
||||
.regular {
|
||||
font-family: var(--typography-Footnote-Regular-fontFamily);
|
||||
font-size: var(--typography-Footnote-Regular-fontSize);
|
||||
font-weight: var(--typography-Footnote-Regular-fontWeight);
|
||||
letter-spacing: var(--typography-Footnote-Regular-letterSpacing);
|
||||
line-height: var(--typography-Footnote-Regular-lineHeight);
|
||||
text-decoration: var(--typography-Footnote-Regular-textDecoration);
|
||||
}
|
||||
|
||||
.labels {
|
||||
font-family: var(--typography-Footnote-Labels-fontFamily);
|
||||
font-size: var(--typography-Footnote-Labels-fontSize);
|
||||
font-weight: var(--typography-Footnote-Labels-fontWeight);
|
||||
letter-spacing: var(--typography-Footnote-Labels-letterSpacing);
|
||||
line-height: var(--typography-Footnote-Labels-lineHeight);
|
||||
text-decoration: var(--typography-Footnote-Labels-textDecoration);
|
||||
}
|
||||
|
||||
.uppercase {
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.left {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.black {
|
||||
color: var(--Main-Grey-100);
|
||||
}
|
||||
|
||||
.burgundy {
|
||||
color: var(--Scandic-Brand-Burgundy);
|
||||
}
|
||||
|
||||
.pale {
|
||||
color: var(--Scandic-Brand-Pale-Peach);
|
||||
}
|
||||
|
||||
.peach50 {
|
||||
color: var(--Scandic-Peach-50);
|
||||
}
|
||||
|
||||
.uiTextMediumContrast {
|
||||
color: var(--UI-Text-Medium-contrast);
|
||||
}
|
||||
|
||||
.uiTextHighContrast {
|
||||
color: var(--UI-Text-High-contrast);
|
||||
}
|
||||
.uiTextPlaceholder {
|
||||
color: var(--UI-Text-Placeholder);
|
||||
}
|
||||
|
||||
.white {
|
||||
color: var(--Main-Grey-White);
|
||||
}
|
||||
|
||||
.baseTextDisabled {
|
||||
color: var(--Base-Text-Disabled);
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
import { Slot } from "@radix-ui/react-slot"
|
||||
|
||||
import { footnoteFontOnlyVariants, footnoteVariants } from "./variants"
|
||||
|
||||
import type { VariantProps } from "class-variance-authority"
|
||||
|
||||
interface FootnoteProps
|
||||
extends
|
||||
Omit<React.HTMLAttributes<HTMLParagraphElement>, "color">,
|
||||
VariantProps<typeof footnoteVariants> {
|
||||
asChild?: boolean
|
||||
fontOnly?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use `@scandic-hotels/design-system/Typography` instead.
|
||||
*/
|
||||
export default function Footnote({
|
||||
asChild = false,
|
||||
className = "",
|
||||
color,
|
||||
fontOnly = false,
|
||||
textAlign,
|
||||
textTransform,
|
||||
type,
|
||||
...props
|
||||
}: FootnoteProps) {
|
||||
const Comp = asChild ? Slot : "p"
|
||||
const classNames = fontOnly
|
||||
? footnoteFontOnlyVariants({
|
||||
className,
|
||||
textAlign,
|
||||
textTransform,
|
||||
type,
|
||||
})
|
||||
: footnoteVariants({
|
||||
className,
|
||||
color,
|
||||
textAlign,
|
||||
textTransform,
|
||||
type,
|
||||
})
|
||||
return <Comp className={classNames} {...props} />
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
import { cva } from "class-variance-authority"
|
||||
|
||||
import styles from "./footnote.module.css"
|
||||
|
||||
const config = {
|
||||
variants: {
|
||||
type: {
|
||||
regular: styles.regular,
|
||||
bold: styles.bold,
|
||||
label: styles.labels,
|
||||
},
|
||||
color: {
|
||||
black: styles.black,
|
||||
burgundy: styles.burgundy,
|
||||
pale: styles.pale,
|
||||
peach50: styles.peach50,
|
||||
uiTextMediumContrast: styles.uiTextMediumContrast,
|
||||
uiTextHighContrast: styles.uiTextHighContrast,
|
||||
uiTextPlaceholder: styles.uiTextPlaceholder,
|
||||
white: styles.white,
|
||||
baseTextDisabled: styles.baseTextDisabled,
|
||||
},
|
||||
textAlign: {
|
||||
center: styles.center,
|
||||
left: styles.left,
|
||||
},
|
||||
textTransform: {
|
||||
uppercase: styles.uppercase,
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
type: "regular",
|
||||
},
|
||||
} as const
|
||||
|
||||
export const footnoteVariants = cva(styles.footnote, config)
|
||||
|
||||
const fontOnlyConfig = {
|
||||
variants: {
|
||||
type: {
|
||||
regular: styles.regular,
|
||||
bold: styles.bold,
|
||||
label: styles.labels,
|
||||
},
|
||||
textAlign: {
|
||||
center: styles.center,
|
||||
left: styles.left,
|
||||
},
|
||||
textTransform: {
|
||||
uppercase: styles.uppercase,
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
type: "regular",
|
||||
},
|
||||
} as const
|
||||
|
||||
export const footnoteFontOnlyVariants = cva(
|
||||
styles.footnoteFontOnly,
|
||||
fontOnlyConfig
|
||||
)
|
||||
@@ -85,7 +85,10 @@ export const FormInput = forwardRef<HTMLInputElement, FormInputProps>(
|
||||
ref={mergeRefs(field.ref, ref)}
|
||||
name={field.name}
|
||||
onBlur={field.onBlur}
|
||||
onChange={field.onChange}
|
||||
onChange={(event) => {
|
||||
field.onChange(event)
|
||||
props.onChange?.(event)
|
||||
}}
|
||||
value={field.value ?? ""}
|
||||
autoComplete={autoComplete}
|
||||
id={id ?? field.name}
|
||||
|
||||
@@ -35,8 +35,3 @@
|
||||
justify-content: start;
|
||||
align-items: baseline;
|
||||
}
|
||||
|
||||
.perNight {
|
||||
font-weight: 400;
|
||||
font-size: var(--typography-Caption-Regular-fontSize);
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { cx } from "class-variance-authority"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import { Divider } from "../../Divider"
|
||||
import { RateTypeEnum } from "@scandic-hotels/common/constants/rateType"
|
||||
import { Divider } from "../../Divider"
|
||||
import { Typography } from "../../Typography"
|
||||
import styles from "./hotelPriceCard.module.css"
|
||||
|
||||
@@ -117,14 +117,16 @@ export function HotelPriceCard({
|
||||
>
|
||||
<p>
|
||||
{productTypePrices.localPrice.currency}
|
||||
{/* eslint-disable-next-line formatjs/no-literal-string-in-jsx */}
|
||||
<span className={styles.perNight}>
|
||||
/
|
||||
{intl.formatMessage({
|
||||
id: "common.night",
|
||||
defaultMessage: "night",
|
||||
})}
|
||||
</span>
|
||||
<Typography variant="Body/Supporting text (caption)/smRegular">
|
||||
{/* eslint-disable-next-line formatjs/no-literal-string-in-jsx */}
|
||||
<span>
|
||||
/
|
||||
{intl.formatMessage({
|
||||
id: "common.night",
|
||||
defaultMessage: "night",
|
||||
})}
|
||||
</span>
|
||||
</Typography>
|
||||
</p>
|
||||
</Typography>
|
||||
</div>
|
||||
|
||||
@@ -160,6 +160,8 @@ import EditOutlined from "./generated/EditOutlined"
|
||||
import EditFilled from "./generated/EditFilled"
|
||||
import EditCalendarOutlined from "./generated/EditCalendarOutlined"
|
||||
import EditCalendarFilled from "./generated/EditCalendarFilled"
|
||||
import EditDocumentOutlined from "./generated/EditDocumentOutlined"
|
||||
import EditDocumentFilled from "./generated/EditDocumentFilled"
|
||||
import EditSquareOutlined from "./generated/EditSquareOutlined"
|
||||
import EditSquareFilled from "./generated/EditSquareFilled"
|
||||
import ElectricBikeOutlined from "./generated/ElectricBikeOutlined"
|
||||
@@ -642,6 +644,9 @@ const _materialIcons = {
|
||||
edit_calendar: {
|
||||
rounded: { outlined: EditCalendarOutlined, filled: EditCalendarFilled },
|
||||
},
|
||||
edit_document: {
|
||||
rounded: { outlined: EditDocumentOutlined, filled: EditDocumentFilled },
|
||||
},
|
||||
edit_square: {
|
||||
rounded: { outlined: EditSquareOutlined, filled: EditSquareFilled },
|
||||
},
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
/* AUTO-GENERATED — DO NOT EDIT */
|
||||
import type { SVGProps } from "react"
|
||||
|
||||
const EditDocumentFilled = (props: SVGProps<SVGSVGElement>) => (
|
||||
<svg viewBox="0 -960 960 960" {...props}>
|
||||
<path d="M220-80q-24 0-42-18t-18-42v-680q0-24 18-42t42-18h315q12 0 23.5 5t19.5 13l204 204q8 8 13 19.5t5 23.5v99q0 8-5 14.5t-13 8.5q-12 5-23 11.5T738-465L518-246q-8 8-13 19.5t-5 23.5v93q0 13-8.5 21.5T470-80zm340-30v-81q0-6 2-11t7-10l212-211q9-9 20-13t22-4q12 0 23 4.5t20 13.5l37 37q9 9 13 20t4 22-4.5 22.5T902-300L692-89q-5 5-10 7t-11 2h-81q-13 0-21.5-8.5T560-110m263-194 37-39-37-37-38 38zM550-600h190L520-820l220 220-220-220v190q0 13 8.5 21.5T550-600" />
|
||||
</svg>
|
||||
)
|
||||
|
||||
export default EditDocumentFilled
|
||||
@@ -0,0 +1,10 @@
|
||||
/* AUTO-GENERATED — DO NOT EDIT */
|
||||
import type { SVGProps } from "react"
|
||||
|
||||
const EditDocumentOutlined = (props: SVGProps<SVGSVGElement>) => (
|
||||
<svg viewBox="0 -960 960 960" {...props}>
|
||||
<path d="M560-110v-81q0-5.57 2-10.78 2-5.22 7-10.22l211.61-210.77q9.11-9.12 20.25-13.18Q812-440 823-440q12 0 23 4.5t20 13.5l37 37q9 9 13 20t4 22-4.5 22.5-13.58 20.62L692-89q-5 5-10.22 7-5.21 2-10.78 2h-81q-12.75 0-21.37-8.63Q560-97.25 560-110m300-233-37-37zM620-140h38l121-122-37-37-122 121zM220-80q-24 0-42-18t-18-42v-680q0-24 18-42t42-18h315q12.44 0 23.72 5T578-862l204 204q8 8 13 19.28t5 23.72v71q0 12.75-8.68 21.37-8.67 8.63-21.5 8.63-12.82 0-21.32-8.63-8.5-8.62-8.5-21.37v-56H550q-12.75 0-21.37-8.63Q520-617.25 520-630v-190H220v680h250q12.75 0 21.38 8.68 8.62 8.67 8.62 21.5 0 12.82-8.62 21.32Q482.75-80 470-80zm0-60v-680zm541-141-19-18 37 37z" />
|
||||
</svg>
|
||||
)
|
||||
|
||||
export default EditDocumentOutlined
|
||||
@@ -4,9 +4,7 @@ import { nodesToHtml } from "./utils"
|
||||
|
||||
import styles from "./jsontohtml.module.css"
|
||||
|
||||
import { AlertTypeEnum } from "@scandic-hotels/common/constants/alert"
|
||||
import { ImageVaultAsset } from "@scandic-hotels/common/utils/imageVault"
|
||||
import { AlertSidepeekContent } from "../../types/sidepeekContent"
|
||||
import { ContentBlockType } from "./types/rte/enums"
|
||||
import type { RTENode } from "./types/rte/node"
|
||||
import type { RenderOptions } from "./types/rte/option"
|
||||
@@ -17,7 +15,7 @@ export type Node<T> = {
|
||||
|
||||
export type Embeds =
|
||||
| {
|
||||
__typename: Exclude<ContentBlockType, "ImageContainer" | "Alert">
|
||||
__typename: Exclude<ContentBlockType, "ImageContainer">
|
||||
system?: { uid: string } | null
|
||||
url?: string | null
|
||||
permanent_url?: string | null
|
||||
@@ -31,25 +29,6 @@ export type Embeds =
|
||||
image_left?: ImageVaultAsset
|
||||
image_right?: ImageVaultAsset
|
||||
}
|
||||
| {
|
||||
__typename: "Alert"
|
||||
system?: { uid: string } | null
|
||||
type: AlertTypeEnum
|
||||
heading: string | null
|
||||
text: string
|
||||
phoneContact?: {
|
||||
displayText: string
|
||||
phoneNumber: string
|
||||
footnote?: string | null
|
||||
} | null
|
||||
sidepeekContent?: AlertSidepeekContent | null
|
||||
sidepeekCtaText?: string | null
|
||||
link?: {
|
||||
url: string
|
||||
title: string
|
||||
keepSearchParams?: boolean
|
||||
} | null
|
||||
}
|
||||
|
||||
export type EmbedByUid = Record<string, Node<Embeds>>
|
||||
|
||||
|
||||
@@ -20,7 +20,6 @@ import {
|
||||
mapImageVaultAssetResponseToImageVaultAsset,
|
||||
mapInsertResponseToImageVaultAsset,
|
||||
} from "@scandic-hotels/common/utils/imageVault"
|
||||
import { Alert } from "../Alert"
|
||||
import { TextLink } from "../TextLink"
|
||||
import type { EmbedByUid } from "./JsonToHtml"
|
||||
import type { Attributes } from "./types/rte/attrs"
|
||||
@@ -459,8 +458,6 @@ export const renderOptions: RenderOptions = {
|
||||
)
|
||||
}
|
||||
return null
|
||||
} else if (entry?.node.__typename === "Alert") {
|
||||
return <Alert key={node.uid} {...entry.node} />
|
||||
} else if (
|
||||
entry?.node.__typename === "AccountPage" ||
|
||||
entry?.node.__typename === "CampaignOverviewPage" ||
|
||||
|
||||
@@ -50,30 +50,15 @@
|
||||
gap: var(--Space-x05);
|
||||
}
|
||||
|
||||
.breadcrumb {
|
||||
font-family: var(--typography-Footnote-Bold-fontFamily);
|
||||
font-size: var(--typography-Footnote-Bold-fontSize);
|
||||
font-weight: 500;
|
||||
letter-spacing: var(--typography-Footnote-Bold-letterSpacing);
|
||||
line-height: var(--typography-Footnote-Bold-lineHeight);
|
||||
}
|
||||
|
||||
.link.breadcrumb {
|
||||
font-family: var(--typography-Footnote-Bold-fontFamily);
|
||||
font-size: var(--typography-Footnote-Bold-fontSize);
|
||||
font-weight: 500;
|
||||
letter-spacing: var(--typography-Footnote-Bold-letterSpacing);
|
||||
line-height: var(--typography-Footnote-Bold-lineHeight);
|
||||
}
|
||||
|
||||
.myPageMobileDropdown {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: var(--Scandic-Brand-Burgundy);
|
||||
font-family: var(--typography-Body-Regular-fontFamily);
|
||||
font-size: var(--typography-Body-Regular-fontSize);
|
||||
line-height: var(--typography-Body-Regular-lineHeight);
|
||||
letter-spacing: var(--typography-Body-Regular-letterSpacing);
|
||||
font-family:
|
||||
var(--Body-Paragraph-Font-family), var(--Body-Paragraph-Font-fallback);
|
||||
font-size: var(--Body-Paragraph-Size);
|
||||
line-height: 1.5;
|
||||
letter-spacing: var(--Body-Paragraph-Letter-spacing);
|
||||
padding: var(--Space-x1);
|
||||
border-radius: var(--Corner-Radius-md);
|
||||
gap: var(--Space-x1);
|
||||
@@ -97,11 +82,12 @@
|
||||
.shortcut {
|
||||
display: grid;
|
||||
align-items: center;
|
||||
font-family: var(--typography-Body-Regular-fontFamily);
|
||||
font-size: var(--typography-Body-Regular-fontSize);
|
||||
font-weight: var(--typography-Body-Regular-fontWeight);
|
||||
letter-spacing: var(--typography-Body-Regular-letterSpacing);
|
||||
line-height: var(--typography-Body-Regular-lineHeight);
|
||||
font-family:
|
||||
var(--Body-Paragraph-Font-family), var(--Body-Paragraph-Font-fallback);
|
||||
font-size: var(--Body-Paragraph-Size);
|
||||
font-weight: var(--Body-Paragraph-Font-weight);
|
||||
letter-spacing: var(--Body-Paragraph-Letter-spacing);
|
||||
line-height: 1.5;
|
||||
gap: var(--Space-x2);
|
||||
grid-template-columns: 1fr auto;
|
||||
padding: var(--Space-x2) var(--Space-x3);
|
||||
@@ -133,22 +119,13 @@
|
||||
line-height: 140%;
|
||||
}
|
||||
|
||||
/* Tiny should be removed, it's not a variant of the Link*/
|
||||
.tiny {
|
||||
font-family: var(--typography-Footnote-Regular-fontFamily);
|
||||
font-size: var(--typography-Footnote-Regular-fontSize);
|
||||
font-weight: var(--typography-Footnote-Regular-fontWeight);
|
||||
letter-spacing: var(--typography-Footnote-Regular-letterSpacing);
|
||||
line-height: var(--typography-Footnote-Regular-lineHeight);
|
||||
}
|
||||
|
||||
.bold {
|
||||
font-family: var(--typography-Body-Bold-fontFamily);
|
||||
font-size: var(--typography-Body-Bold-fontSize);
|
||||
font-weight: 500
|
||||
/* Should be fixed when variables starts working: var(--typography-Body-Bold-fontWeight) */;
|
||||
letter-spacing: var(--typography-Body-Bold-letterSpacing);
|
||||
line-height: var(--typography-Body-Bold-lineHeight);
|
||||
font-family:
|
||||
var(--Body-Paragraph-Font-family), var(--Body-Paragraph-Font-fallback);
|
||||
font-size: var(--Body-Paragraph-Size);
|
||||
font-weight: var(--Body-Paragraph-Font-weight-2);
|
||||
letter-spacing: var(--Body-Paragraph-Letter-spacing);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.menu {
|
||||
|
||||
@@ -18,7 +18,6 @@ export const linkVariants = cva(styles.link, {
|
||||
size: {
|
||||
small: styles.small,
|
||||
large: styles.large,
|
||||
tiny: styles.tiny,
|
||||
none: "",
|
||||
},
|
||||
textDecoration: {
|
||||
@@ -29,7 +28,6 @@ export const linkVariants = cva(styles.link, {
|
||||
},
|
||||
variant: {
|
||||
icon: styles.icon,
|
||||
breadcrumb: styles.breadcrumb,
|
||||
myPageMobileDropdown: styles.myPageMobileDropdown,
|
||||
navigation: styles.navigation,
|
||||
menu: styles.menu,
|
||||
|
||||
@@ -1,131 +1,3 @@
|
||||
:root {
|
||||
--typography-Body-Bold-fontFamily: "fira sans";
|
||||
--typography-Body-Bold-fontSize: 16px;
|
||||
--typography-Body-Bold-fontWeight: "medium";
|
||||
--typography-Body-Bold-letterSpacing: 1.2000000476837158%;
|
||||
--typography-Body-Bold-lineHeight: 150%;
|
||||
--typography-Body-Bold-textDecoration: "none";
|
||||
--typography-Body-Regular-fontFamily: "fira sans";
|
||||
--typography-Body-Regular-fontSize: 16px;
|
||||
--typography-Body-Regular-fontWeight: "regular";
|
||||
--typography-Body-Regular-letterSpacing: 1.2000000476837158%;
|
||||
--typography-Body-Regular-lineHeight: 150%;
|
||||
--typography-Body-Regular-textDecoration: "none";
|
||||
--typography-Body-Underline-fontFamily: "fira sans";
|
||||
--typography-Body-Underline-fontSize: 16px;
|
||||
--typography-Body-Underline-letterSpacing: 1.2000000476837158%;
|
||||
--typography-Body-Underline-lineHeight: 150%;
|
||||
--typography-Body-Underline-textDecoration: "underline";
|
||||
--typography-Caption-Bold-fontFamily: "fira sans";
|
||||
--typography-Caption-Bold-fontSize: 14px;
|
||||
--typography-Caption-Bold-fontWeight: "medium";
|
||||
--typography-Caption-Bold-letterSpacing: 1.399999976158142%;
|
||||
--typography-Caption-Bold-lineHeight: 139.9999976158142%;
|
||||
--typography-Caption-Bold-textDecoration: "none";
|
||||
--typography-Caption-Labels-fontFamily: "brandon text";
|
||||
--typography-Caption-Labels-fontSize: 14px;
|
||||
--typography-Caption-Labels-fontWeight: "bold";
|
||||
--typography-Caption-Labels-letterSpacing: 1.399999976158142%;
|
||||
--typography-Caption-Labels-lineHeight: 150%;
|
||||
--typography-Caption-Labels-textDecoration: "none";
|
||||
--typography-Caption-Regular-fontFamily: "fira sans";
|
||||
--typography-Caption-Regular-fontSize: 14px;
|
||||
--typography-Caption-Regular-fontWeight: "regular";
|
||||
--typography-Caption-Regular-letterSpacing: 1.399999976158142%;
|
||||
--typography-Caption-Regular-lineHeight: 139.9999976158142%;
|
||||
--typography-Caption-Regular-textDecoration: "none";
|
||||
--typography-Caption-Underline-fontFamily: "fira sans";
|
||||
--typography-Caption-Underline-fontSize: 14px;
|
||||
--typography-Caption-Underline-fontWeight: "medium";
|
||||
--typography-Caption-Underline-letterSpacing: 1.399999976158142%;
|
||||
--typography-Caption-Underline-lineHeight: 139.9999976158142%;
|
||||
--typography-Caption-Underline-textDecoration: "underline";
|
||||
--typography-Footnote-Bold-fontFamily: "fira sans";
|
||||
--typography-Footnote-Bold-fontSize: 12px;
|
||||
--typography-Footnote-Bold-fontWeight: "medium";
|
||||
--typography-Footnote-Bold-letterSpacing: 1.399999976158142%;
|
||||
--typography-Footnote-Bold-lineHeight: 150%;
|
||||
--typography-Footnote-Bold-textDecoration: "none";
|
||||
--typography-Footnote-Labels-fontFamily: "brandon text";
|
||||
--typography-Footnote-Labels-fontSize: 12px;
|
||||
--typography-Footnote-Labels-fontWeight: "bold";
|
||||
--typography-Footnote-Labels-letterSpacing: 1.399999976158142%;
|
||||
--typography-Footnote-Labels-lineHeight: 150%;
|
||||
--typography-Footnote-Labels-textDecoration: "none";
|
||||
--typography-Footnote-Regular-fontFamily: "fira sans";
|
||||
--typography-Footnote-Regular-fontSize: 12px;
|
||||
--typography-Footnote-Regular-fontWeight: "regular";
|
||||
--typography-Footnote-Regular-letterSpacing: 1.399999976158142%;
|
||||
--typography-Footnote-Regular-lineHeight: 150%;
|
||||
--typography-Footnote-Regular-textDecoration: "none";
|
||||
--typography-Preamble-Desktop-fontSize: 20px;
|
||||
--typography-Preamble-fontFamily: "fira sans";
|
||||
--typography-Preamble-fontWeight: "regular";
|
||||
--typography-Preamble-letterSpacing: 1%;
|
||||
--typography-Preamble-lineHeight: 139.9999976158142%;
|
||||
--typography-Preamble-Mobile-fontSize: 18px;
|
||||
--typography-Preamble-textDecoration: "none";
|
||||
--typography-Script-1-Desktop-fontSize: 32px;
|
||||
--typography-Script-1-fontFamily: "biro script plus";
|
||||
--typography-Script-1-fontWeight: "regular";
|
||||
--typography-Script-1-letterSpacing: 2%;
|
||||
--typography-Script-1-lineHeight: 110.00000238418579%;
|
||||
--typography-Script-1-Mobile-fontSize: 24px;
|
||||
--typography-Script-2-Desktop-fontSize: 24px;
|
||||
--typography-Script-2-fontWeight: "regular";
|
||||
--typography-Script-2-letterSpacing: 2%;
|
||||
--typography-Script-2-lineHeight: 110.00000238418579%;
|
||||
--typography-Script-2-Mobile-fontSize: 20px;
|
||||
--typography-Subtitle-1-Desktop-fontSize: 24px;
|
||||
--typography-Subtitle-1-fontFamily: "fira sans";
|
||||
--typography-Subtitle-1-letterSpacing: 1%;
|
||||
--typography-Subtitle-1-lineHeight: 120.00000476837158%;
|
||||
--typography-Subtitle-1-Mobile-fontSize: 20px;
|
||||
--typography-Subtitle-2-Desktop-fontSize: 20px;
|
||||
--typography-Subtitle-2-fontFamily: "fira sans";
|
||||
--typography-Subtitle-2-fontSize: 20px;
|
||||
--typography-Subtitle-2-fontWeight: "medium";
|
||||
--typography-Subtitle-2-letterSpacing: 1%;
|
||||
--typography-Subtitle-2-lineHeight: 120.00000476837158%;
|
||||
--typography-Subtitle-2-Mobile-fontSize: 18px;
|
||||
--typography-Title-1-Desktop-fontSize: 64px;
|
||||
--typography-Title-1-fontFamily: "brandon text";
|
||||
--typography-Title-1-fontSize: 64px;
|
||||
--typography-Title-1-fontWeight: "black";
|
||||
--typography-Title-1-letterSpacing: 0.25%;
|
||||
--typography-Title-1-lineHeight: 110.00000238418579%;
|
||||
--typography-Title-1-Mobile-fontSize: 48px;
|
||||
--typography-Title-1-textDecoration: "none";
|
||||
--typography-Title-2-Desktop-fontSize: 48px;
|
||||
--typography-Title-2-fontFamily: "brandon text";
|
||||
--typography-Title-2-fontWeight: "black";
|
||||
--typography-Title-2-letterSpacing: 0.25%;
|
||||
--typography-Title-2-lineHeight: 110.00000238418579%;
|
||||
--typography-Title-2-Mobile-fontSize: 36px;
|
||||
--typography-Title-2-textDecoration: "none";
|
||||
--typography-Title-3-Desktop-fontSize: 36px;
|
||||
--typography-Title-3-fontFamily: "brandon text";
|
||||
--typography-Title-3-fontSize: 36px;
|
||||
--typography-Title-3-fontWeight: "black";
|
||||
--typography-Title-3-letterSpacing: 0.25%;
|
||||
--typography-Title-3-lineHeight: 110.00000238418579%;
|
||||
--typography-Title-3-Mobile-fontSize: 30px;
|
||||
--typography-Title-3-textDecoration: "none";
|
||||
--typography-Title-4-Desktop-fontSize: 28px;
|
||||
--typography-Title-4-fontFamily: "brandon text";
|
||||
--typography-Title-4-fontWeight: "bold";
|
||||
--typography-Title-4-letterSpacing: 0.25%;
|
||||
--typography-Title-4-lineHeight: 110.00000238418579%;
|
||||
--typography-Title-4-Mobile-fontSize: 24px;
|
||||
--typography-Title-4-textDecoration: "none";
|
||||
--typography-Title-5-Desktop-fontSize: 24px;
|
||||
--typography-Title-5-fontFamily: "brandon text";
|
||||
--typography-Title-5-fontWeight: "black";
|
||||
--typography-Title-5-letterSpacing: 0.25%;
|
||||
--typography-Title-5-lineHeight: 110.00000238418579%;
|
||||
--typography-Title-5-Mobile-fontSize: 20px;
|
||||
--typography-Title-5-textDecoration: "none";
|
||||
}
|
||||
:root {
|
||||
--Base-Border-Hover: var(--Scandic-Peach-80);
|
||||
--Base-Border-Inverted: var(--UI-Opacity-White-100);
|
||||
|
||||
@@ -26,7 +26,6 @@
|
||||
"./Divider": "./lib/components/Divider/index.tsx",
|
||||
"./FacilityToIcon": "./lib/components/FacilityToIcon/index.tsx",
|
||||
"./FakeButton": "./lib/components/FakeButton/index.tsx",
|
||||
"./Footnote": "./lib/components/Footnote/index.tsx",
|
||||
"./Form/Checkbox": "./lib/components/Form/Checkbox/index.tsx",
|
||||
"./Form/Country": "./lib/components/Form/Country/index.tsx",
|
||||
"./Form/Date": "./lib/components/Form/Date/index.tsx",
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { gql } from "graphql-tag"
|
||||
|
||||
import { Alert } from "../Alert.graphql"
|
||||
import { ImageContainer } from "../ImageContainer.graphql"
|
||||
import { AccountPageLink } from "../PageLink/AccountPageLink.graphql"
|
||||
import { CampaignOverviewPageLink } from "../PageLink/CampaignOverviewPageLink.graphql"
|
||||
@@ -25,7 +24,6 @@ export const Content_ContentPage = gql`
|
||||
node {
|
||||
__typename
|
||||
...SysAsset
|
||||
...Alert
|
||||
...ImageContainer
|
||||
...AccountPageLink
|
||||
...CampaignOverviewPageLink
|
||||
@@ -47,7 +45,6 @@ export const Content_ContentPage = gql`
|
||||
}
|
||||
}
|
||||
${SysAsset}
|
||||
${Alert}
|
||||
${ImageContainer}
|
||||
${AccountPageLink}
|
||||
${CampaignOverviewPageLink}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { router } from "../.."
|
||||
import { findBookingForCurrentUserRoute } from "./query/findBookingForCurrentUserRoute"
|
||||
import { findBookingRoute } from "./query/findBookingRoute"
|
||||
import { getBookingRoute } from "./query/getBookingRoute"
|
||||
import { getBookingStatusRoute } from "./query/getBookingStatusRoute"
|
||||
@@ -7,6 +8,7 @@ import { getLinkedReservationsRoute } from "./query/getLinkedReservationsRoute"
|
||||
export const bookingQueryRouter = router({
|
||||
get: getBookingRoute,
|
||||
findBooking: findBookingRoute,
|
||||
findBookingForCurrentUser: findBookingForCurrentUserRoute,
|
||||
linkedReservations: getLinkedReservationsRoute,
|
||||
status: getBookingStatusRoute,
|
||||
})
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
import { createCounter } from "@scandic-hotels/common/telemetry"
|
||||
|
||||
import { notFoundError } from "../../../errors"
|
||||
import { safeProtectedServiceProcedure } from "../../../procedures"
|
||||
import { findBooking } from "../../../services/booking/findBooking"
|
||||
import { getHotel } from "../../hotels/services/getHotel"
|
||||
import { findBookingInput } from "../input"
|
||||
|
||||
export const findBookingForCurrentUserRoute = safeProtectedServiceProcedure
|
||||
.input(
|
||||
findBookingInput.omit({ lastName: true, firstName: true, email: true })
|
||||
)
|
||||
.query(async function ({ ctx, input }) {
|
||||
const lang = input.lang ?? ctx.lang
|
||||
const { confirmationNumber } = input
|
||||
const user = await ctx.getScandicUser()
|
||||
const token = await ctx.getScandicUserToken()
|
||||
|
||||
const findBookingCounter = createCounter(
|
||||
"trpc.booking.findBookingForCurrentUser"
|
||||
)
|
||||
const metricsFindBooking = findBookingCounter.init({
|
||||
confirmationNumber,
|
||||
})
|
||||
metricsFindBooking.start()
|
||||
|
||||
if (!user || !token) {
|
||||
metricsFindBooking.dataError(
|
||||
`Fail to find user when finding booking for ${confirmationNumber}`,
|
||||
{ confirmationNumber }
|
||||
)
|
||||
return null
|
||||
}
|
||||
|
||||
const booking = await findBooking(
|
||||
{
|
||||
confirmationNumber,
|
||||
lang,
|
||||
lastName: user.lastName,
|
||||
firstName: user.firstName,
|
||||
email: user.email,
|
||||
},
|
||||
token
|
||||
)
|
||||
|
||||
if (!booking) {
|
||||
metricsFindBooking.dataError(
|
||||
`Fail to find booking data for ${confirmationNumber}`,
|
||||
{ confirmationNumber }
|
||||
)
|
||||
return null
|
||||
}
|
||||
|
||||
const hotelData = await getHotel(
|
||||
{
|
||||
hotelId: booking.hotelId,
|
||||
isCardOnlyPayment: false,
|
||||
language: lang,
|
||||
},
|
||||
ctx.serviceToken
|
||||
)
|
||||
|
||||
if (!hotelData) {
|
||||
metricsFindBooking.dataError(
|
||||
`Failed to find hotel data for ${booking.hotelId}`,
|
||||
{
|
||||
hotelId: booking.hotelId,
|
||||
}
|
||||
)
|
||||
|
||||
throw notFoundError({
|
||||
message: "Hotel data not found",
|
||||
errorDetails: { hotelId: booking.hotelId },
|
||||
})
|
||||
}
|
||||
|
||||
metricsFindBooking.success()
|
||||
|
||||
return {
|
||||
hotel: {
|
||||
name: hotelData.hotel.name,
|
||||
cityName: hotelData.hotel.cityName,
|
||||
},
|
||||
booking,
|
||||
}
|
||||
})
|
||||
@@ -46,7 +46,7 @@ const getContactConfig = cache(async (lang: Lang) => {
|
||||
GetContactConfig,
|
||||
variables,
|
||||
{
|
||||
key: `${lang}:contact`,
|
||||
key: `${lang}:contact_config`,
|
||||
ttl: "max",
|
||||
}
|
||||
)
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import { z } from "zod"
|
||||
|
||||
import { BlocksEnums } from "../../../../types/blocksEnum"
|
||||
import { ContentEnum } from "../../../../types/content"
|
||||
import { alertSchema, transformAlertSchema } from "../alert"
|
||||
import { rawLinkUnionSchema, transformPageLink } from "../pageLinks"
|
||||
import { imageContainerSchema } from "./imageContainer"
|
||||
import { sysAssetSchema } from "./sysAsset"
|
||||
@@ -24,11 +22,7 @@ export const contentSchema = z.object({
|
||||
.discriminatedUnion("__typename", [
|
||||
imageContainerSchema,
|
||||
sysAssetSchema,
|
||||
alertSchema.merge(
|
||||
z.object({
|
||||
__typename: z.literal(ContentEnum.blocks.Alert),
|
||||
})
|
||||
),
|
||||
|
||||
...rawLinkUnionSchema.options,
|
||||
])
|
||||
.transform((data) => {
|
||||
@@ -36,12 +30,6 @@ export const contentSchema = z.object({
|
||||
if (link) {
|
||||
return link
|
||||
}
|
||||
if (data.__typename === ContentEnum.blocks.Alert) {
|
||||
return {
|
||||
__typename: data.__typename,
|
||||
...transformAlertSchema(data),
|
||||
}
|
||||
}
|
||||
return data
|
||||
}),
|
||||
})
|
||||
|
||||
@@ -12,7 +12,7 @@ import type { FilterType, HotelFilter } from "./output"
|
||||
|
||||
export async function getHotelFilters(lang: Lang) {
|
||||
const cacheClient = await getCacheClient()
|
||||
const cacheKey = `${lang}:getHotelFilters`
|
||||
const cacheKey = `${lang}:hotel_filter:outer`
|
||||
|
||||
return await cacheClient.cacheOrGet(
|
||||
cacheKey,
|
||||
@@ -27,7 +27,7 @@ export async function getHotelFilters(lang: Lang) {
|
||||
const response = await request<unknown>(
|
||||
GetHotelFilters,
|
||||
{ locale: lang },
|
||||
{ key: `${lang}:hotel_filters`, ttl: "1d" }
|
||||
{ key: `${lang}:hotel_filter`, ttl: "1d" }
|
||||
)
|
||||
|
||||
if (!response.data) {
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import z from "zod"
|
||||
|
||||
import { signupVerify } from "@scandic-hotels/common/constants/routes/signup"
|
||||
import { createLogger } from "@scandic-hotels/common/logger/createLogger"
|
||||
import { createCounter } from "@scandic-hotels/common/telemetry"
|
||||
@@ -318,4 +320,33 @@ export const userMutationRouter = router({
|
||||
return true
|
||||
}),
|
||||
}),
|
||||
claimPoints: protectedProcedure
|
||||
.input(
|
||||
z.object({
|
||||
from: z.string(),
|
||||
to: z.string(),
|
||||
city: z.string(),
|
||||
hotel: z.string(),
|
||||
firstName: z.string(),
|
||||
lastName: z.string(),
|
||||
email: z.string().email(),
|
||||
phone: z.string(),
|
||||
})
|
||||
)
|
||||
.mutation(async function ({ input, ctx }) {
|
||||
userMutationLogger.info("api.user.claimPoints start")
|
||||
const user = await ctx.getScandicUser()
|
||||
if (!user) {
|
||||
throw "error"
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-console
|
||||
console.log("Claiming points", input, user.membershipNumber)
|
||||
|
||||
// TODO Waiting for API endpoint, simulating delay
|
||||
await new Promise((resolve) => setTimeout(resolve, 2_000))
|
||||
|
||||
userMutationLogger.info("api.user.claimPoints success")
|
||||
return true
|
||||
}),
|
||||
})
|
||||
|
||||
@@ -91,6 +91,7 @@ const ICONS = [
|
||||
"download",
|
||||
"dresser",
|
||||
"edit_calendar",
|
||||
"edit_document",
|
||||
"edit_square",
|
||||
"edit",
|
||||
"electric_bike",
|
||||
|
||||
Reference in New Issue
Block a user