12 Commits

Author SHA1 Message Date
Anton Gunnarsson
1dffeb6be7 Merged in fix/loy-603-fix-edit-profile-redirect (pull request #3543)
fix(LOY-603): Remove refresh preventing redirect

* Remove refresh preventing redirect


Approved-by: Matilda Landström
2026-02-05 07:45:19 +00:00
Anton Gunnarsson
1f1ed2e4f3 Merged in chore/update-sas-banner-image (pull request #3545)
fix(LOY-621): Replace scandic banner image and reduce size

* Replace scandic banner image and reduce size


Approved-by: Emma Zettervall
Approved-by: Matilda Landström
2026-02-05 07:44:54 +00:00
Linus Flood
bc9eaf6706 Merged in fix/robots-txt (pull request #3547)
feat(robots): fixed robots.txt file

* feat(robots): fixed robots.txt file
2026-02-05 07:38:15 +00:00
Matilda Landström
549265cd34 Merged in feat/LOY-615-cleanup-env-prof-consent (pull request #3537)
feat(LOY-615): cleanup profiling consent env var

* feat(LOY-615): cleanup profiling consent env var


Approved-by: Anton Gunnarsson
2026-02-04 16:51:06 +00:00
Matilda Landström
989b18527e Merged in fix/STAY-138-center-text (pull request #3538)
fix(STAY-138): center text

* fix(STAY-138): center text


Approved-by: Emma Zettervall
2026-02-04 13:09:12 +00:00
Erik Tiekstra
0cda37808e Merged in fix/BOOK-755-alert-content (pull request #3523)
fix(BOOK-755, BOOK-787): Fixed issue for phone number and sidepeeks not showing

* fix(BOOK-755): Fixed issue for phone number and sidepeeks not showing

* fix(BOOK-755): fix issue phonenumber alert

* fix(BOOK-755): fix issue phonenumber


Approved-by: Matilda Landström
2026-02-03 15:28:23 +00:00
Erik Tiekstra
b3c4761ae5 Merged in chore/BOOK-773-replace-old-typography-variables (pull request #3515)
Chore/BOOK-773 replace old typography variables

* chore(BOOK-773): Replaced body typography

* chore(BOOK-773): Replaced caption typography

* chore(BOOK-773): Replaced footnote typography

* chore(BOOK-773): Replaced subtitle typography


Approved-by: Bianca Widstam
2026-02-03 15:07:18 +00:00
Linus Flood
dd65467573 Merged in fix/close-map-text (pull request #3536)
feat(map): fixed close map text alignment

* feat(map): fixed close map text alignment
2026-02-03 14:07:40 +00:00
Joakim Jäderberg
eb45e6b294 Merged in fix/LOY-606 (pull request #3535)
fix(LOY-606): breakfast price now considers number of nights in MyStay/ChangeDates

* fix(LOY-606): breakfast price now considers number of nights in MyStay/ChangeDates


Approved-by: Linus Flood
2026-02-03 13:51:45 +00:00
Emma Zettervall
6553fcf685 Merged in fix/use-old-loacalize-keys (pull request #3534)
fix(LOY-391): changed back localize keys to the old ones to make less work for content

* fix(LOY-391): changed back localize keys to the old ones to make less work for content


Approved-by: Anton Gunnarsson
2026-02-03 13:42:37 +00:00
Anton Gunnarsson
c2cf6b03a7 Merged in feat/loy-291-new-claim-points-flow (pull request #3508)
feat(LOY-291): New claim points flow for logged in users

* wip new flow

* More wip

* More wip

* Wip styling

* wip with a mutation

* Actually fetch booking data

* More styling wip

* Fix toast duration

* fix loading a11y maybe

* More stuff

* Add feature flag

* Add invalid state

* Clean up

* Add fields for missing user info

* Restructure files

* Add todos

* Disable warning

* Fix icon and border radius


Approved-by: Emma Zettervall
Approved-by: Matilda Landström
2026-02-03 13:27:24 +00:00
Linus Flood
310ad7bc7f Merged in fix/book-785-hotelfilters-cache (pull request #3533)
fix(BOOK-785): fix incorrect cache keys for contact config and hotel filters

* fix(BOOK-785): fix incorrect cache keys for contact config and hotel filters
2026-02-03 13:18:23 +00:00
94 changed files with 1122 additions and 811 deletions

View File

@@ -1,3 +1,4 @@
.layout { .layout {
font-family: var(--typography-Body-Regular-fontFamily); font-family:
var(--Body-Paragraph-Font-family), var(--Body-Paragraph-Font-fallback);
} }

View File

@@ -52,3 +52,4 @@ DTMC_ENTRA_ID_CLIENT=""
DTMC_ENTRA_ID_ISSUER="" DTMC_ENTRA_ID_ISSUER=""
DTMC_ENTRA_ID_SECRET="" DTMC_ENTRA_ID_SECRET=""
NEXT_PUBLIC_NEW_POINTCLAIMS="true"

View File

@@ -4,7 +4,8 @@
.layout { .layout {
display: grid; 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; grid-template-rows: auto 1fr;
min-height: 100dvh; min-height: 100dvh;
max-width: var(--max-width-page); max-width: var(--max-width-page);

View File

@@ -1,12 +1,7 @@
import { TrackingSDK } from "@scandic-hotels/tracking/TrackingSDK" import { TrackingSDK } from "@scandic-hotels/tracking/TrackingSDK"
import { import { getEurobonusMembership } from "@scandic-hotels/trpc/routers/user/helpers"
getEurobonusMembership,
scandicMembershipTypes,
} from "@scandic-hotels/trpc/routers/user/helpers"
import { env } from "@/env/server"
import { import {
getBasicProfileSafely,
getProfileSafely, getProfileSafely,
getProfilingConsent, getProfilingConsent,
} from "@/lib/trpc/memoizedRequests" } from "@/lib/trpc/memoizedRequests"
@@ -26,15 +21,7 @@ type MyPagesLayoutProps = React.PropsWithChildren<{
breadcrumbs: React.ReactNode breadcrumbs: React.ReactNode
}> }>
export default async function MyPagesLayout(props: MyPagesLayoutProps) { export default async function MyPagesLayout({
if (env.ENABLE_PROFILE_CONSENT) {
return <MyPagesLayoutWithConsent {...props} />
}
return <MyPagesLayoutBase {...props} />
}
async function MyPagesLayoutWithConsent({
breadcrumbs, breadcrumbs,
children, children,
}: MyPagesLayoutProps) { }: MyPagesLayoutProps) {
@@ -84,25 +71,3 @@ async function MyPagesLayoutWithConsent({
</ProfilingConsentAlertProvider> </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>
)
}

View File

@@ -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 { TrackingSDK } from "@scandic-hotels/tracking/TrackingSDK"
import { env } from "@/env/server"
import { getProfile } from "@/lib/trpc/memoizedRequests" import { getProfile } from "@/lib/trpc/memoizedRequests"
import { serverClient } from "@/lib/trpc/server" import { serverClient } from "@/lib/trpc/server"
import { ProfilingConsent } from "@/components/Forms/ProfilingConsent" import { ProfilingConsent } from "@/components/Forms/ProfilingConsent"
import { getLang } from "@/i18n/serverContext"
import styles from "./page.module.css" import styles from "./page.module.css"
export default async function ProfilingConsentSlot() { export default async function ProfilingConsentSlot() {
const lang = await getLang()
if (!env.ENABLE_PROFILE_CONSENT) {
redirect(profile[lang])
}
const caller = await serverClient() const caller = await serverClient()
const accountPage = await caller.contentstack.accountPage.get() const accountPage = await caller.contentstack.accountPage.get()
const user = await getProfile() const user = await getProfile()

View File

@@ -1,6 +1,7 @@
.layout { .layout {
display: grid; 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; grid-template-rows: auto 1fr;
position: relative; position: relative;
} }

View File

@@ -1,3 +1,4 @@
.layout { .layout {
font-family: var(--typography-Body-Regular-fontFamily); font-family:
var(--Body-Paragraph-Font-family), var(--Body-Paragraph-Font-fallback);
} }

View File

@@ -1,9 +1,8 @@
import { redirect } from "next/navigation" import { redirect } from "next/navigation"
import { z } from "zod" import { z } from "zod"
import Footnote from "@scandic-hotels/design-system/Footnote"
import Image from "@scandic-hotels/design-system/Image" 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 { Typography } from "@scandic-hotels/design-system/Typography"
import { env } from "@/env/server" import { env } from "@/env/server"
@@ -94,27 +93,24 @@ export default async function SASxScandicLoginPage(
{intentDescriptions[parsedParams.intent]} {intentDescriptions[parsedParams.intent]}
</p> </p>
</Typography> </Typography>
<Footnote textAlign="center"> <Typography variant="Body/Supporting text (caption)/smRegular">
{intl.formatMessage( <p style={{ textAlign: "center" }}>
{ {intl.formatMessage(
id: "linkEuroBonusAccount.manualRedirectLinkMessage", {
defaultMessage: id: "linkEuroBonusAccount.manualRedirectLinkMessage",
"If you are not redirected automatically, please <loginLink>click here</loginLink>.", defaultMessage:
}, "If you are not redirected automatically, please <loginLink>click here</loginLink>.",
{ },
loginLink: (str) => ( {
<Link loginLink: (str) => (
href={loginLink} <TextLink typography="Link/sm" href={loginLink}>
color="red" {str}
size="tiny" </TextLink>
textDecoration="underline" ),
> }
{str} )}
</Link> </p>
), </Typography>
}
)}
</Footnote>
</SASModal> </SASModal>
) )
} }

View File

@@ -42,7 +42,8 @@
width: 34px; width: 34px;
height: 0px; height: 0px;
padding: var(--Space-x3) 0; 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: 1px solid var(--Base-Border-Normal);
border-radius: var(--Corner-Radius-md); border-radius: var(--Corner-Radius-md);
text-align: center; text-align: center;

View File

@@ -1,5 +1,6 @@
.layout { .layout {
background-color: var(--Background-Primary); 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; min-height: 100dvh;
} }

View File

@@ -1,3 +1,4 @@
.layout { .layout {
font-family: var(--typography-Body-Regular-fontFamily); font-family:
var(--Body-Paragraph-Font-family), var(--Body-Paragraph-Font-fallback);
} }

View File

@@ -1,13 +1,29 @@
import { Alert } from "@scandic-hotels/design-system/Alert" 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" import type { AlertBlock } from "@scandic-hotels/trpc/types/blocks"
interface AlertBlockProps extends Pick<AlertBlock, "alert"> {} 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) { if (!alert) {
return null return null
} }
return <Alert {...alert} /> const phoneContact =
alert.phoneContact && contactConfig
? getAlertPhoneContactData(alert, contactConfig)
: null
return (
<Alert
{...alert}
phoneContact={phoneContact}
sidepeekCtaText={alert.sidepeekButton?.cta_text}
/>
)
} }

View File

@@ -16,18 +16,14 @@
.iconTh { .iconTh {
padding: var(--Space-x5) var(--Space-x2) var(--Space-x2); padding: var(--Space-x5) var(--Space-x2) var(--Space-x2);
font-weight: var(--typography-Caption-Regular-fontWeight);
vertical-align: bottom; vertical-align: bottom;
} }
.summaryTh { .summaryTh {
font-size: var(--typography-Caption-Regular-fontSize);
font-weight: var(--typography-Caption-Regular-fontWeight);
padding: 0 var(--Space-x2) var(--Space-x2); padding: 0 var(--Space-x2) var(--Space-x2);
vertical-align: top; vertical-align: top;
} }
.select { .select {
font-weight: var(--typography-Caption-Regular-fontWeight);
padding: 0 var(--Space-x2) var(--Space-x2); padding: 0 var(--Space-x2) var(--Space-x2);
} }

View File

@@ -1,3 +1,5 @@
import { Typography } from "@scandic-hotels/design-system/Typography"
import MembershipLevelIcon from "@/components/Levels/Icon" import MembershipLevelIcon from "@/components/Levels/Icon"
import LevelSummary from "../../LevelSummary" import LevelSummary from "../../LevelSummary"
@@ -37,12 +39,14 @@ export default function DesktopHeader({
<th /> <th />
{levels.map((level, idx) => { {levels.map((level, idx) => {
return ( return (
<th <Typography
key={"summary" + level.level_id + idx} variant="Body/Supporting text (caption)/smRegular"
className={styles.summaryTh} key={"name" + level.level_id + idx}
> >
<LevelSummary level={level} /> <th className={styles.summaryTh}>
</th> <LevelSummary level={level} />
</th>
</Typography>
) )
})} })}
</tr> </tr>

View File

@@ -82,10 +82,12 @@ function RewardTableHeader({ name, description }: RewardTableHeaderProps) {
</span> </span>
</hgroup> </hgroup>
</summary> </summary>
<p <Typography variant="Body/Supporting text (caption)/smRegular">
className={styles.rewardDescription} <p
dangerouslySetInnerHTML={{ __html: description }} className={styles.rewardDescription}
/> dangerouslySetInnerHTML={{ __html: description }}
/>
</Typography>
</details> </details>
) )
} }

View File

@@ -15,14 +15,11 @@
} }
.td { .td {
font-size: var(--typography-Footnote-Regular-fontSize);
text-align: center; text-align: center;
} }
.rewardTh { .rewardTh {
padding: var(--Space-x3) var(--Space-x2); padding: var(--Space-x3) var(--Space-x2);
font-size: var(--typography-Caption-Regular-fontSize);
font-weight: var(--typography-Caption-Regular-fontWeight);
} }
.details[open] .chevron { .details[open] .chevron {

View File

@@ -1,5 +1,7 @@
import { useIntl } from "react-intl" import { useIntl } from "react-intl"
import { Typography } from "@scandic-hotels/design-system/Typography"
import styles from "./levelSummary.module.css" import styles from "./levelSummary.module.css"
import type { LevelSummaryProps } from "@/types/components/overviewTable" import type { LevelSummaryProps } from "@/types/components/overviewTable"
@@ -32,7 +34,9 @@ export default function LevelSummary({
return ( return (
<div className={styles.levelSummary}> <div className={styles.levelSummary}>
<span className={styles.levelRequirements}>{pointsMsg}</span> <Typography variant="Label/xsRegular">
<span className={styles.levelRequirements}>{pointsMsg}</span>
</Typography>
{showDescription && ( {showDescription && (
<p className={styles.levelSummaryText}>{level.description}</p> <p className={styles.levelSummaryText}>{level.description}</p>
)} )}

View File

@@ -8,16 +8,14 @@
.levelRequirements { .levelRequirements {
border-radius: var(--Corner-Radius-md); border-radius: var(--Corner-Radius-md);
background-color: var(--Scandic-Brand-Pale-Peach); background-color: var(--Surface-Brand-Primary-1-Default);
color: var(--Scandic-Peach-80); color: var(--Text-Interactive-Secondary);
padding: var(--Space-x05) var(--Space-x1); padding: var(--Space-x05) var(--Space-x1);
text-align: center; text-align: center;
width: 100%; width: 100%;
} }
.levelSummaryText { .levelSummaryText {
font-size: var(--typography-Caption-Regular-fontSize);
line-height: var(--typography-Body-Regular-lineHeight);
margin: 0; margin: 0;
} }
@@ -26,12 +24,3 @@
padding: var(--Space-x05) var(--Space-x1); 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);
}
}

View File

@@ -27,10 +27,12 @@ export default function RewardCard({
</span> </span>
</hgroup> </hgroup>
</summary> </summary>
<p <Typography variant="Body/Supporting text (caption)/smRegular">
className={styles.rewardCardDescription} <p
dangerouslySetInnerHTML={{ __html: description }} className={styles.rewardCardDescription}
/> dangerouslySetInnerHTML={{ __html: description }}
/>
</Typography>
</details> </details>
</div> </div>
<div className={styles.rewardComparison}> <div className={styles.rewardComparison}>

View File

@@ -12,8 +12,6 @@
} }
.rewardCardDescription { .rewardCardDescription {
font-size: var(--typography-Caption-Regular-fontSize);
line-height: 150%;
padding-right: var(--Space-x4); padding-right: var(--Space-x4);
} }

View File

@@ -1,6 +1,7 @@
import { Minus } from "react-feather" import { Minus } from "react-feather"
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon" import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
import { Typography } from "@scandic-hotels/design-system/Typography"
import styles from "./rewardValue.module.css" import styles from "./rewardValue.module.css"
@@ -21,8 +22,8 @@ export default function RewardValue({ reward }: RewardValueProps) {
) )
} }
return ( return (
<div className={styles.rewardValueContainer}> <Typography variant="Body/Paragraph/mdBold">
<span className={styles.rewardValue}>{reward.value}</span> <div className={styles.rewardValueContainer}>{reward.value}</div>
</div> </Typography>
) )
} }

View File

@@ -7,17 +7,6 @@
text-wrap: balance; 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 { .checkIcon {
display: inline-flex; display: inline-flex;
} }

View File

@@ -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 cant 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>
)
}

View File

@@ -6,3 +6,100 @@
gap: var(--Space-x2); gap: var(--Space-x2);
white-space: nowrap; 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);
}

View File

@@ -1,18 +1,110 @@
/* eslint-disable formatjs/no-literal-string-in-jsx */
/* TODO remove disable and add i18n */
"use client" "use client"
import { useEffect, useState } from "react"
import { Dialog } from "react-aria-components"
import { useIntl } from "react-intl" import { useIntl } from "react-intl"
import { Button } from "@scandic-hotels/design-system/Button"
import ButtonLink from "@scandic-hotels/design-system/ButtonLink" import ButtonLink from "@scandic-hotels/design-system/ButtonLink"
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon" 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 { Typography } from "@scandic-hotels/design-system/Typography"
import { missingPoints } from "@/constants/missingPointsHrefs" import { missingPoints } from "@/constants/missingPointsHrefs"
import { env } from "@/env/client"
import useLang from "@/hooks/useLang" import useLang from "@/hooks/useLang"
import { ClaimPointsWizard } from "./ClaimPointsWizard"
import styles from "./claimPoints.module.css" import styles from "./claimPoints.module.css"
export default function ClaimPoints() { 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&apos;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 intl = useIntl()
const lang = useLang() const lang = useLang()

View File

@@ -104,7 +104,7 @@ function getDescription(transaction: Transaction, intl: IntlShape) {
if (isNonTransactional && transaction.attributes.nights === 0) { if (isNonTransactional && transaction.attributes.nights === 0) {
return intl.formatMessage({ return intl.formatMessage({
id: "myPoints.pointTransactions.pointsActivity", id: "earnAndBurn.journeyTable.pointsActivity",
defaultMessage: "Point activity", defaultMessage: "Point activity",
}) })
} }
@@ -113,7 +113,7 @@ function getDescription(transaction: Transaction, intl: IntlShape) {
if (hotelInformation?.name) { if (hotelInformation?.name) {
return intl.formatMessage( return intl.formatMessage(
{ {
id: "myPoints.pointTransactions.stayAt", id: "earnAndBurn.journeyTable.stayAt",
defaultMessage: "Stay at {hotelName}", defaultMessage: "Stay at {hotelName}",
}, },
{ hotelName: hotelInformation.name } { hotelName: hotelInformation.name }
@@ -124,53 +124,53 @@ function getDescription(transaction: Transaction, intl: IntlShape) {
case Transactions.rewardType.stayAdj: case Transactions.rewardType.stayAdj:
if (transaction.attributes.hotelOperaId === "ORS") { if (transaction.attributes.hotelOperaId === "ORS") {
return intl.formatMessage({ return intl.formatMessage({
id: "myPoints.pointTransactions.formerScandicHotel", id: "earnAndBurn.journeyTable.formerScandicHotel",
defaultMessage: "Former Scandic Hotel", defaultMessage: "Former Scandic Hotel",
}) })
} }
if (isBalfwd) { if (isBalfwd) {
return intl.formatMessage({ return intl.formatMessage({
id: "myPoints.pointTransactions.pointsEarnedPriorMay2021", id: "earnAndBurn.journeyTable.pointsEarnedPriorMay2021",
defaultMessage: "Points earned prior to May 1, 2021", defaultMessage: "Points earned prior to May 1, 2021",
}) })
} }
case Transactions.rewardType.redgift: case Transactions.rewardType.redgift:
return intl.formatMessage({ return intl.formatMessage({
id: "myPoints.pointTransactions.redGift", id: "earnAndBurn.journeyTable.redGift",
defaultMessage: "Reward Gift", defaultMessage: "Reward Gift",
}) })
case Transactions.rewardType.rewardNight: case Transactions.rewardType.rewardNight:
return intl.formatMessage({ return intl.formatMessage({
id: "myPoints.pointTransactions.rewardNight", id: "earnAndBurn.journeyTable.rewardNight",
defaultMessage: "Reward Night", defaultMessage: "Reward Night",
}) })
case Transactions.rewardType.ancillary: case Transactions.rewardType.ancillary:
return intl.formatMessage({ return intl.formatMessage({
id: "myPoints.pointTransactions.extrasToBooking", id: "earnAndBurn.journeyTable.extrasToBooking",
defaultMessage: "Extras to your booking", defaultMessage: "Extras to your booking",
}) })
case Transactions.rewardType.enrollment: case Transactions.rewardType.enrollment:
return intl.formatMessage({ return intl.formatMessage({
id: "myPoints.pointTransactions.signUpBonus", id: "earnAndBurn.journeyTable.signUpBonus",
defaultMessage: "Sign up bonus", defaultMessage: "Sign up bonus",
}) })
case Transactions.rewardType.mastercard_points: case Transactions.rewardType.mastercard_points:
return intl.formatMessage({ return intl.formatMessage({
id: "myPoints.pointTransactions.scandicFriendsMastercard", id: "earnAndBurn.journeyTable.scandicFriendsMastercard",
defaultMessage: "Scandic Friends Mastercard", defaultMessage: "Scandic Friends Mastercard",
}) })
case Transactions.rewardType.tui_points: case Transactions.rewardType.tui_points:
return intl.formatMessage({ return intl.formatMessage({
id: "myPoints.pointTransactions.tuiPoints", id: "earnAndBurn.journeyTable.tuiPoints",
defaultMessage: "TUI Points", defaultMessage: "TUI Points",
}) })
case Transactions.rewardType.pointShop: case Transactions.rewardType.pointShop:
return intl.formatMessage({ return intl.formatMessage({
id: "myPoints.pointTransactions.pointShop", id: "earnAndBurn.journeyTable.pointShop",
defaultMessage: "Scandic Friends Point Shop", defaultMessage: "Scandic Friends Point Shop",
}) })
default: default:

View File

@@ -1,5 +1,3 @@
import { env } from "@/env/server"
import SignupForm from "@/components/Forms/Signup" import SignupForm from "@/components/Forms/Signup"
import type { SignupFormWrapperProps } from "@/types/components/blocks/dynamicContent" 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({ export default async function SignupFormWrapper({
dynamic_content, dynamic_content,
}: SignupFormWrapperProps) { }: SignupFormWrapperProps) {
return ( return <SignupForm {...dynamic_content} />
<SignupForm
{...dynamic_content}
enableProfileConsent={env.ENABLE_PROFILE_CONSENT}
/>
)
} }

View File

@@ -10,6 +10,7 @@ import { useMediaQuery } from "usehooks-ts"
import { useMarkerHover } from "@scandic-hotels/common/hooks/map/useMarkerHover" import { useMarkerHover } from "@scandic-hotels/common/hooks/map/useMarkerHover"
import { InfoWindow } from "@scandic-hotels/design-system/Map/InfoWindow" 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" import { useDestinationPageCitiesMapStore } from "@/stores/destination-page-cities-map"
@@ -79,7 +80,10 @@ export default function CityClusterMarker({
})} })}
anchorPoint={AdvancedMarkerAnchorPoint.CENTER} anchorPoint={AdvancedMarkerAnchorPoint.CENTER}
> >
<span className={styles.count}>{sizeAsText}</span> <Typography variant="Title/Subtitle/md">
<span>{sizeAsText}</span>
</Typography>
{isDesktop && isHovered ? ( {isDesktop && isHovered ? (
<InfoWindow <InfoWindow
position={position} position={position}

View File

@@ -20,9 +20,3 @@
height: 46px !important; 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);
}

View File

@@ -19,12 +19,13 @@ div.months {
td.day, td.day,
td.rangeEnd, td.rangeEnd,
td.rangeStart { td.rangeStart {
font-family: var(--typography-Body-Bold-fontFamily); font-family:
font-size: var(--typography-Body-Bold-fontSize); var(--Body-Paragraph-Font-family), var(--Body-Paragraph-Font-fallback);
font-weight: 500; font-size: var(--Body-Paragraph-Size);
letter-spacing: var(--typography-Body-Bold-letterSpacing); font-weight: var(--Body-Paragraph-Font-weight-2);
line-height: var(--typography-Body-Bold-lineHeight); letter-spacing: var(--Body-Paragraph-Letter-spacing);
text-decoration: var(--typography-Body-Bold-textDecoration); line-height: 1.5;
text-decoration: none;
} }
td.rangeEnd, td.rangeEnd,
@@ -90,14 +91,16 @@ td.day[data-outside="true"] ~ td.day[data-disabled="true"] button.dayButton,
} }
.weekDay { .weekDay {
color: var(--UI-Text-Placeholder); color: var(--Text-Tertiary);
font-family: var(--typography-Footnote-Labels-fontFamily); font-family:
font-size: var(--typography-Footnote-Labels-fontSize); var(--Title-Overline-sm-Font-family), var(--Title-Overline-sm-Font-fallback);
font-weight: var(--typography-Footnote-Labels-fontWeight); font-size: var(--Title-Overline-sm-Size);
letter-spacing: var(--typography-Footnote-Labels-letterSpacing); font-style: normal;
line-height: var(--typography-Footnote-Labels-lineHeight); font-weight: var(--Title-Overline-sm-Font-weight);
text-decoration: var(--typography-Footnote-Labels-textDecoration); line-height: 1.5;
text-transform: uppercase; letter-spacing: var(--Title-Overline-sm-Letter-spacing);
text-transform: var(--Title-Overline-sm-Text-Transform);
text-decoration: none;
} }
.footer { .footer {

View File

@@ -89,12 +89,13 @@ div.months {
td.day, td.day,
td.rangeEnd, td.rangeEnd,
td.rangeStart { td.rangeStart {
font-family: var(--typography-Body-Bold-fontFamily); font-family:
font-size: var(--typography-Body-Bold-fontSize); var(--Body-Paragraph-Font-family), var(--Body-Paragraph-Font-fallback);
font-weight: 500; font-size: var(--Body-Paragraph-Size);
letter-spacing: var(--typography-Body-Bold-letterSpacing); font-weight: var(--Body-Paragraph-Font-weight-2);
line-height: var(--typography-Body-Bold-lineHeight); letter-spacing: var(--Body-Paragraph-Letter-spacing);
text-decoration: var(--typography-Body-Bold-textDecoration); line-height: 1.5;
text-decoration: none;
} }
td.rangeEnd, td.rangeEnd,
@@ -156,14 +157,16 @@ td.day[data-outside="true"] ~ td.day[data-disabled="true"] button.dayButton,
} }
.weekDay { .weekDay {
color: var(--UI-Text-Placeholder); color: var(--Text-Tertiary);
font-family: var(--typography-Caption-Labels-fontFamily); font-family:
font-size: var(--typography-Caption-Labels-fontSize); var(--Title-Overline-sm-Font-family), var(--Title-Overline-sm-Font-fallback);
font-weight: var(--typography-Caption-Labels-fontWeight); font-size: var(--Title-Overline-sm-Size);
letter-spacing: var(--typography-Caption-Labels-letterSpacing); font-style: normal;
line-height: var(--typography-Caption-Labels-lineHeight); font-weight: var(--Title-Overline-sm-Font-weight);
text-decoration: var(--typography-Caption-Labels-textDecoration); line-height: 1.5;
text-transform: uppercase; 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) { @media screen and (min-width: 1367px) {

View File

@@ -107,7 +107,6 @@ export default function Form({ user }: EditFormProps) {
} else { } else {
router.push(profile[lang]) router.push(profile[lang])
} }
router.refresh() // Can be removed on NextJs 15
} }
break break
} }

View File

@@ -48,14 +48,9 @@ import styles from "./form.module.css"
interface SignUpFormProps { interface SignUpFormProps {
title: string title: string
enableProfileConsent?: boolean
} }
export default function SignupForm({ export default function SignupForm({ title }: SignUpFormProps) {
title,
// Handled as a prop rather than a client env var due to limits in Netlify env var size.
enableProfileConsent = false,
}: SignUpFormProps) {
const intl = useIntl() const intl = useIntl()
const router = useRouter() const router = useRouter()
const lang = useLang() const lang = useLang()
@@ -140,7 +135,7 @@ export default function SignupForm({
return ( return (
<div className={styles.formWrapper}> <div className={styles.formWrapper}>
{enableProfileConsent && <ProfilingConsentModalReadOnly />} <ProfilingConsentModalReadOnly />
{title ? ( {title ? (
<Typography variant="Title/md"> <Typography variant="Title/md">
<h2>{title}</h2> <h2>{title}</h2>
@@ -293,41 +288,39 @@ export default function SignupForm({
/> />
</section> </section>
{enableProfileConsent && ( <section className={styles.personalization}>
<section className={styles.personalization}> <header>
<header> <Typography variant="Title/Subtitle/md">
<Typography variant="Title/Subtitle/md"> <h3>
<h3> {intl.formatMessage({
{intl.formatMessage({ id: "signup.UnlockYourPersonalizedExperience",
id: "signup.UnlockYourPersonalizedExperience", defaultMessage: "Unlock your personalized experience!",
defaultMessage: "Unlock your personalized experience!", })}
})} </h3>
</h3> </Typography>
</Typography> </header>
</header> <Checkbox
<Checkbox name="profilingConsent"
name="profilingConsent" registerOptions={{ required: true }}
registerOptions={{ required: true }} >
> {intl.formatMessage({
{intl.formatMessage({ id: "signup.yesConsent",
id: "signup.yesConsent", defaultMessage:
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.",
"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>
</Checkbox> <TextLinkButton
<TextLinkButton typography="Link/sm"
typography="Link/sm" color="Primary"
color="Primary" className={styles.personalizationButton}
className={styles.personalizationButton} onClick={openPersonalizationModal}
onClick={openPersonalizationModal} >
> {intl.formatMessage({
{intl.formatMessage({ id: "signup.ReadMoreAboutPersonalization",
id: "signup.ReadMoreAboutPersonalization", defaultMessage: "Read more about personalization at Scandic",
defaultMessage: "Read more about personalization at Scandic", })}
})} </TextLinkButton>
</TextLinkButton> </section>
</section>
)}
<section className={styles.terms}> <section className={styles.terms}>
<header> <header>

View File

@@ -23,6 +23,7 @@
text-decoration-skip-ink: none; text-decoration-skip-ink: none;
text-decoration-thickness: auto; text-decoration-thickness: auto;
text-underline-offset: auto; text-underline-offset: auto;
text-align: center;
text-underline-position: from-font; text-underline-position: from-font;
} }

View File

@@ -4,6 +4,7 @@ import { useIntl } from "react-intl"
import { sumPackages } from "@scandic-hotels/booking-flow/utils/SelectRate" import { sumPackages } from "@scandic-hotels/booking-flow/utils/SelectRate"
import { CurrencyEnum } from "@scandic-hotels/common/constants/currency" import { CurrencyEnum } from "@scandic-hotels/common/constants/currency"
import { dt } from "@scandic-hotels/common/dt"
import { formatPrice } from "@scandic-hotels/common/utils/numberFormatting" import { formatPrice } from "@scandic-hotels/common/utils/numberFormatting"
import { trpc } from "@scandic-hotels/trpc/client" import { trpc } from "@scandic-hotels/trpc/client"
@@ -66,9 +67,14 @@ export default function Steps({ closeModal }: ChangeDatesStepsProps) {
setDates({ fromDate, toDate }) setDates({ fromDate, toDate })
const numberOfNights = dt(toDate).diff(dt(fromDate), "days")
const pkgsSum = sumPackages(packages) const pkgsSum = sumPackages(packages)
const extraPrice = const breakfastPrice = !!breakfast
pkgsSum.price + ((breakfast && breakfast.localPrice.totalPrice) || 0) ? breakfast.localPrice.price * numberOfNights
: 0
const extraPrice = pkgsSum.price + breakfastPrice
if (isLoggedIn && "member" in data.product && data.product.member) { if (isLoggedIn && "member" in data.product && data.product.member) {
const { currency, pricePerStay } = data.product.member.localPrice const { currency, pricePerStay } = data.product.member.localPrice
setNewPrice(formatPrice(intl, pricePerStay + extraPrice, currency)) setNewPrice(formatPrice(intl, pricePerStay + extraPrice, currency))

View File

@@ -4,13 +4,6 @@
padding: var(--Space-x3) var(--Space-x2); 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 {
list-style: none; list-style: none;
} }

View File

@@ -16,8 +16,8 @@
cursor: pointer; cursor: pointer;
height: 32px; height: 32px;
width: 32px; width: 32px;
font-size: var(--typography-Body-Bold-fontSize); font-size: var(--Body-Paragraph-Size);
font-weight: var(--typography-Body-Bold-fontWeight); font-weight: var(--Body-Paragraph-Font-weight-2);
padding: 0; padding: 0;
display: flex; display: flex;
align-items: center; align-items: center;

View File

@@ -1,5 +1,3 @@
import { env } from "@/env/server"
import { getIntl } from "@/i18n" import { getIntl } from "@/i18n"
import { Section } from "../Section" import { Section } from "../Section"
@@ -17,7 +15,7 @@ export async function CommunicationSettings() {
})} })}
> >
<EmailSlot /> <EmailSlot />
{env.ENABLE_PROFILE_CONSENT && <PersonalizationSlot />} <PersonalizationSlot />
</Section> </Section>
) )
} }

View File

@@ -1,6 +1,5 @@
import { Typography } from "@scandic-hotels/design-system/Typography" import { Typography } from "@scandic-hotels/design-system/Typography"
import { env } from "@/env/server"
import { getProfile, getProfilingConsent } from "@/lib/trpc/memoizedRequests" import { getProfile, getProfilingConsent } from "@/lib/trpc/memoizedRequests"
import { GetMainIconByCSIdentifier, userHasConsent } from "../utils" import { GetMainIconByCSIdentifier, userHasConsent } from "../utils"
@@ -9,8 +8,6 @@ import { BannerButton } from "./Button"
import styles from "./profilingConsentBanner.module.css" import styles from "./profilingConsentBanner.module.css"
export async function ProfilingConsentBanner() { export async function ProfilingConsentBanner() {
if (!env.ENABLE_PROFILE_CONSENT) return null
const user = await getProfile() const user = await getProfile()
if (!user || userHasConsent(user?.profilingConsent)) return null if (!user || userHasConsent(user?.profilingConsent)) return null

View File

@@ -1,6 +1,6 @@
# Profiling Consent # 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 ## User Journey
@@ -121,11 +121,9 @@ Replace `<memberKey>` with the actual `membershipNumber` or `profileId`.
Required content for the feature: Required content for the feature:
1. **Profiling Consent (config)** 1. **Profiling Consent (config)**
- Config needs to be created and published in each language - Config needs to be created and published in each language
2. **/consent (account page)** 2. **/consent (account page)**
- Page needs to be created and published in each language - Page needs to be created and published in each language
3. **/overview (account page)** 3. **/overview (account page)**

View File

@@ -2,6 +2,7 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: var(--Space-x05); gap: var(--Space-x05);
align-items: flex-start;
} }
.link { .link {

View File

@@ -1,15 +1,13 @@
import Footnote from "@scandic-hotels/design-system/Footnote"
import { import {
MaterialIcon, MaterialIcon,
type MaterialIconSetIconProps, type MaterialIconSetIconProps,
} from "@scandic-hotels/design-system/Icons/MaterialIcon" } 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 { Typography } from "@scandic-hotels/design-system/Typography"
import { getValueFromContactConfig } from "@scandic-hotels/trpc/utils/contactConfig" import { getValueFromContactConfig } from "@scandic-hotels/trpc/utils/contactConfig"
import { serverClient } from "@/lib/trpc/server" import { serverClient } from "@/lib/trpc/server"
// import { getValueFromContactConfig } from "@/utils/contactConfig"
import styles from "./contactRow.module.css" import styles from "./contactRow.module.css"
import type { ContactRowProps } from "@/types/components/sidebar/joinLoyaltyContact" import type { ContactRowProps } from "@/types/components/sidebar/joinLoyaltyContact"
@@ -46,22 +44,27 @@ export default async function ContactRow({ contact }: ContactRowProps) {
return ( return (
<div className={styles.wrapper}> <div className={styles.wrapper}>
<Typography {contact.display_text ? (
variant="Body/Paragraph/mdBold" <Typography
className={styles.displayText} variant="Body/Paragraph/mdBold"
> className={styles.displayText}
<p>{contact.display_text}</p> >
</Typography> <p>{contact.display_text}</p>
<Link </Typography>
) : null}
<TextLink
typography="Link/sm"
className={styles.link} className={styles.link}
href={openableLink} href={openableLink}
textDecoration="underline"
size="small"
> >
{Icon ? <Icon size={20} color="Icon/Interactive/Default" /> : null} {Icon ? <Icon size={20} color="Icon/Interactive/Default" /> : null}
{val} {val}
</Link> </TextLink>
{footnote && <Footnote color="burgundy">{footnote}</Footnote>} {footnote && (
<Typography variant="Body/Supporting text (caption)/smRegular">
<p>{footnote}</p>
</Typography>
)}
</div> </div>
) )
} }

View File

@@ -13,18 +13,9 @@
gap: var(--Space-x15); gap: var(--Space-x15);
} }
.contact > div {
display: flex;
justify-content: center;
}
@media screen and (min-width: 1367px) { @media screen and (min-width: 1367px) {
.contactContainer { .contactContainer {
align-items: start; align-items: start;
padding-top: var(--Space-x2); padding-top: var(--Space-x2);
} }
.contact > div {
justify-content: start;
}
} }

View File

@@ -16,6 +16,11 @@ export const env = createEnv({
.transform((s) => .transform((s) =>
getSemver("scandic-web", s, process.env.BRANCH || "development") getSemver("scandic-web", s, process.env.BRANCH || "development")
), ),
NEXT_PUBLIC_NEW_POINTCLAIMS: z
.string()
.optional()
.default("false")
.transform((s) => s === "true"),
}, },
emptyStringAsUndefined: true, emptyStringAsUndefined: true,
runtimeEnv: { runtimeEnv: {
@@ -26,5 +31,6 @@ export const env = createEnv({
process.env.NEXT_PUBLIC_SENTRY_CLIENT_SAMPLERATE, process.env.NEXT_PUBLIC_SENTRY_CLIENT_SAMPLERATE,
NEXT_PUBLIC_PUBLIC_URL: process.env.NEXT_PUBLIC_PUBLIC_URL, NEXT_PUBLIC_PUBLIC_URL: process.env.NEXT_PUBLIC_PUBLIC_URL,
NEXT_PUBLIC_RELEASE_TAG: process.env.NEXT_PUBLIC_RELEASE_TAG, NEXT_PUBLIC_RELEASE_TAG: process.env.NEXT_PUBLIC_RELEASE_TAG,
NEXT_PUBLIC_NEW_POINTCLAIMS: process.env.NEXT_PUBLIC_NEW_POINTCLAIMS,
}, },
}) })

View File

@@ -96,11 +96,6 @@ export const env = createEnv({
.refine((s) => s === "1" || s === "0") .refine((s) => s === "1" || s === "0")
.transform((s) => s === "1") .transform((s) => s === "1")
.default("0"), .default("0"),
ENABLE_PROFILE_CONSENT: z
.string()
.refine((s) => s === "true" || s === "false")
.transform((s) => s === "true")
.default("false"),
RELEASE_TAG: z RELEASE_TAG: z
.string() .string()
.optional() .optional()
@@ -160,7 +155,6 @@ export const env = createEnv({
DTMC_ENTRA_ID_SECRET: process.env.DTMC_ENTRA_ID_SECRET, DTMC_ENTRA_ID_SECRET: process.env.DTMC_ENTRA_ID_SECRET,
CHATBOT_LIVE_LANGS: process.env.CHATBOT_LIVE_LANGS, CHATBOT_LIVE_LANGS: process.env.CHATBOT_LIVE_LANGS,
SEO_INERT: process.env.SEO_INERT, SEO_INERT: process.env.SEO_INERT,
ENABLE_PROFILE_CONSENT: process.env.ENABLE_PROFILE_CONSENT,
RELEASE_TAG: process.env.NEXT_PUBLIC_RELEASE_TAG, 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

View File

@@ -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" import styles from "./list.module.css"
export default function Label({ children }: React.PropsWithChildren) { export default function Label({ children }: React.PropsWithChildren) {
return ( return (
<li className={styles.label}> <Typography variant="Title/Overline/sm">
<Footnote color="uiTextPlaceholder" textTransform="uppercase"> <li className={styles.label}>{children}</li>
{children} </Typography>
</Footnote>
</li>
) )
} }

View File

@@ -5,5 +5,6 @@
} }
.label { .label {
padding: 0 var(--Space-x1); padding: 0 var(--Space-x1) var(--Space-x05);
color: var(--Text-Tertiary);
} }

View File

@@ -6,7 +6,6 @@ import { useIntl } from "react-intl"
import { useDebounceValue } from "usehooks-ts" import { useDebounceValue } from "usehooks-ts"
import { Divider } from "@scandic-hotels/design-system/Divider" 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 { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
import { Typography } from "@scandic-hotels/design-system/Typography" import { Typography } from "@scandic-hotels/design-system/Typography"
import { trpc } from "@scandic-hotels/trpc/client" import { trpc } from "@scandic-hotels/trpc/client"
@@ -192,16 +191,14 @@ export default function SearchList({
{typeFilteredSearchHistory && typeFilteredSearchHistory.length > 0 && ( {typeFilteredSearchHistory && typeFilteredSearchHistory.length > 0 && (
<> <>
<Divider className={styles.noResultsDivider} /> <Divider className={styles.noResultsDivider} />
<Footnote <Typography variant="Title/Overline/sm">
className={styles.text} <p className={styles.text}>
color="uiTextPlaceholder" {intl.formatMessage({
textTransform="uppercase" id: "bookingWidget.searchList.latestSearches",
> defaultMessage: "Latest searches",
{intl.formatMessage({ })}
id: "bookingWidget.searchList.latestSearches", </p>
defaultMessage: "Latest searches", </Typography>
})}
</Footnote>
<List <List
getItemProps={getItemProps} getItemProps={getItemProps}
highlightedIndex={highlightedIndex} highlightedIndex={highlightedIndex}
@@ -226,12 +223,14 @@ export default function SearchList({
if (displaySearchHistory) { if (displaySearchHistory) {
return ( return (
<Dialog getMenuProps={getMenuProps}> <Dialog getMenuProps={getMenuProps}>
<Footnote color="uiTextPlaceholder" textTransform="uppercase"> <Typography variant="Title/Overline/sm">
{intl.formatMessage({ <p className={styles.text}>
id: "bookingWidget.searchList.latestSearches", {intl.formatMessage({
defaultMessage: "Latest searches", id: "bookingWidget.searchList.latestSearches",
})} defaultMessage: "Latest searches",
</Footnote> })}
</p>
</Typography>
<List <List
getItemProps={getItemProps} getItemProps={getItemProps}
highlightedIndex={highlightedIndex} highlightedIndex={highlightedIndex}

View File

@@ -33,6 +33,7 @@
.text { .text {
padding: 0 var(--Space-x1); padding: 0 var(--Space-x1);
color: var(--Text-Tertiary);
white-space: normal; white-space: normal;
} }
.textPlaceholderColor { .textPlaceholderColor {

View File

@@ -65,7 +65,7 @@ export default function DatePickerRangeDesktop({
range_start: styles.rangeStart, range_start: styles.rangeStart,
root: `${classNames.root} ${styles.container}`, root: `${classNames.root} ${styles.container}`,
week: styles.week, week: styles.week,
weekday: `${classNames.weekday} ${styles.weekDay}`, weekday: styles.weekDay,
nav: `${classNames.nav} ${styles.nav}`, nav: `${classNames.nav} ${styles.nav}`,
button_next: `${classNames.button_next} ${styles.button_next}`, button_next: `${classNames.button_next} ${styles.button_next}`,
button_previous: `${classNames.button_previous} ${styles.button_previous}`, button_previous: `${classNames.button_previous} ${styles.button_previous}`,

View File

@@ -98,7 +98,7 @@ export default function DatePickerRangeMobile({
range_start: styles.rangeStart, range_start: styles.rangeStart,
root: `${classNames.root} ${styles.root}`, root: `${classNames.root} ${styles.root}`,
week: styles.week, week: styles.week,
weekday: `${classNames.weekday} ${styles.weekDay}`, weekday: styles.weekDay,
}} }}
disabled={[ disabled={[
{ from: lastDayOfPreviousMonth, to: yesterday }, { from: lastDayOfPreviousMonth, to: yesterday },

View File

@@ -20,12 +20,12 @@ div.months {
td.day, td.day,
td.rangeEnd, td.rangeEnd,
td.rangeStart { td.rangeStart {
font-family: var(--typography-Body-Bold-fontFamily); font-family: var(--Body-Paragraph-Font-family), var(--Body-Paragraph-Font-fallback);
font-size: var(--typography-Body-Bold-fontSize); font-size: var(--Body-Paragraph-Size);
font-weight: 500; font-weight: var(--Body-Paragraph-Font-weight-2);
letter-spacing: var(--typography-Body-Bold-letterSpacing); letter-spacing: var(--Body-Paragraph-Letter-spacing);
line-height: var(--typography-Body-Bold-lineHeight); line-height: 1.5;
text-decoration: var(--typography-Body-Bold-textDecoration); text-decoration: none;
} }
td.rangeEnd, td.rangeEnd,
@@ -92,14 +92,15 @@ td.day[data-outside="true"] ~ td.day[data-disabled="true"] button.dayButton,
} }
.weekDay { .weekDay {
color: var(--UI-Text-Placeholder); color: var(--Text-Tertiary);
font-family: var(--typography-Footnote-Labels-fontFamily); font-family: var(--Title-Overline-sm-Font-family), var(--Title-Overline-sm-Font-fallback);
font-size: var(--typography-Footnote-Labels-fontSize); font-size: var(--Title-Overline-sm-Size);
font-weight: var(--typography-Footnote-Labels-fontWeight); font-style: normal;
letter-spacing: var(--typography-Footnote-Labels-letterSpacing); font-weight: var(--Title-Overline-sm-Font-weight);
line-height: var(--typography-Footnote-Labels-lineHeight); line-height: 1.5;
text-decoration: var(--typography-Footnote-Labels-textDecoration); letter-spacing: var(--Title-Overline-sm-Letter-spacing);
text-transform: uppercase; text-transform: var(--Title-Overline-sm-Text-Transform);
text-decoration: none;
} }
.footer { .footer {

View File

@@ -97,12 +97,12 @@ div.months {
td.day, td.day,
td.rangeEnd, td.rangeEnd,
td.rangeStart { td.rangeStart {
font-family: var(--typography-Body-Bold-fontFamily); font-family: var(--Body-Paragraph-Font-family), var(--Body-Paragraph-Font-fallback);
font-size: var(--typography-Body-Bold-fontSize); font-size: var(--Body-Paragraph-Size);
font-weight: 500; font-weight: var(--Body-Paragraph-Font-weight-2);
letter-spacing: var(--typography-Body-Bold-letterSpacing); letter-spacing: var(--Body-Paragraph-Letter-spacing);
line-height: var(--typography-Body-Bold-lineHeight); line-height: 1.5;
text-decoration: var(--typography-Body-Bold-textDecoration); text-decoration: none;
} }
td.rangeEnd, td.rangeEnd,
@@ -165,15 +165,15 @@ td.day[data-outside="true"] ~ td.day[data-disabled="true"] button.dayButton,
} }
.weekDay { .weekDay {
color: var(--Base-Text-Medium-contrast); color: var(--Text-Tertiary);
opacity: 1; font-family: var(--Title-Overline-sm-Font-family), var(--Title-Overline-sm-Font-fallback);
font-family: var(--typography-Caption-Labels-fontFamily); font-size: var(--Title-Overline-sm-Size);
font-size: var(--typography-Caption-Labels-fontSize); font-style: normal;
font-weight: var(--typography-Caption-Labels-fontWeight); font-weight: var(--Title-Overline-sm-Font-weight);
letter-spacing: var(--typography-Caption-Labels-letterSpacing); line-height: 1.5;
line-height: var(--typography-Caption-Labels-lineHeight); letter-spacing: var(--Title-Overline-sm-Letter-spacing);
text-decoration: var(--typography-Caption-Labels-textDecoration); text-transform: var(--Title-Overline-sm-Text-Transform);
text-transform: uppercase; text-decoration: none;
} }
@media screen and (min-width: 1367px) { @media screen and (min-width: 1367px) {

View File

@@ -4,9 +4,8 @@ import { useIntl } from "react-intl"
import { CurrencyEnum } from "@scandic-hotels/common/constants/currency" import { CurrencyEnum } from "@scandic-hotels/common/constants/currency"
import { formatPrice } from "@scandic-hotels/common/utils/numberFormatting" 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 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 { Typography } from "@scandic-hotels/design-system/Typography"
import { useBookingFlowConfig } from "../../../../../bookingFlowConfig/bookingFlowConfigContext" import { useBookingFlowConfig } from "../../../../../bookingFlowConfig/bookingFlowConfigContext"
@@ -83,8 +82,8 @@ export default function JoinScandicFriendsCard({
</Typography> </Typography>
</Checkbox> </Checkbox>
<div className={styles.terms}> <Typography variant="Body/Supporting text (caption)/smRegular">
<Footnote color="uiTextPlaceholder"> <p className={styles.terms}>
{intl.formatMessage( {intl.formatMessage(
{ {
id: "enterDetails.joinScandicFriendsCard.terms", id: "enterDetails.joinScandicFriendsCard.terms",
@@ -93,19 +92,18 @@ export default function JoinScandicFriendsCard({
}, },
{ {
termsAndConditionsLink: (str) => ( termsAndConditionsLink: (str) => (
<Link <TextLink
textDecoration="underline" typography="Link/sm"
size="tiny"
target="_blank" target="_blank"
href={routes.membershipTermsAndConditions[lang]} href={routes.membershipTermsAndConditions[lang]}
> >
{str} {str}
</Link> </TextLink>
), ),
} }
)} )}
</Footnote> </p>
</div> </Typography>
</div> </div>
) )
} }

View File

@@ -28,6 +28,7 @@
.terms { .terms {
grid-area: terms; grid-area: terms;
color: var(--Text-Secondary);
} }
@media screen and (min-width: 768px) { @media screen and (min-width: 768px) {

View File

@@ -4,9 +4,8 @@ import { useIntl } from "react-intl"
import { CurrencyEnum } from "@scandic-hotels/common/constants/currency" import { CurrencyEnum } from "@scandic-hotels/common/constants/currency"
import { formatPrice } from "@scandic-hotels/common/utils/numberFormatting" 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 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 { Typography } from "@scandic-hotels/design-system/Typography"
import { useBookingFlowConfig } from "../../../../../bookingFlowConfig/bookingFlowConfigContext" import { useBookingFlowConfig } from "../../../../../bookingFlowConfig/bookingFlowConfigContext"
@@ -97,8 +96,8 @@ export function PartnerSASJoinScandicFriendsCard({
/> />
</div> </div>
<div className={styles.terms}> <Typography variant="Body/Supporting text (caption)/smRegular">
<Footnote color="uiTextPlaceholder"> <p className={styles.terms}>
{intl.formatMessage( {intl.formatMessage(
{ {
id: "enterDetails.joinScandicFriendsCard.terms", id: "enterDetails.joinScandicFriendsCard.terms",
@@ -107,19 +106,18 @@ export function PartnerSASJoinScandicFriendsCard({
}, },
{ {
termsAndConditionsLink: (str) => ( termsAndConditionsLink: (str) => (
<Link <TextLink
textDecoration="underline" typography="Link/sm"
size="tiny"
target="_blank" target="_blank"
href={routes.membershipTermsAndConditions[lang]} href={routes.membershipTermsAndConditions[lang]}
> >
{str} {str}
</Link> </TextLink>
), ),
} }
)} )}
</Footnote> </p>
</div> </Typography>
</div> </div>
) )
} }

View File

@@ -31,6 +31,7 @@
.terms { .terms {
grid-area: terms; grid-area: terms;
color: var(--Text-Secondary);
} }
@media screen and (min-width: 768px) { @media screen and (min-width: 768px) {

View File

@@ -4,10 +4,9 @@ import { useIntl } from "react-intl"
import { CurrencyEnum } from "@scandic-hotels/common/constants/currency" import { CurrencyEnum } from "@scandic-hotels/common/constants/currency"
import { useLazyPathname } from "@scandic-hotels/common/hooks/useLazyPathname" import { useLazyPathname } from "@scandic-hotels/common/hooks/useLazyPathname"
import { formatPrice } from "@scandic-hotels/common/utils/numberFormatting" 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 Checkbox from "@scandic-hotels/design-system/Form/Checkbox"
import { LoginButton } from "@scandic-hotels/design-system/LoginButton" 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 { Typography } from "@scandic-hotels/design-system/Typography"
import { trackEvent } from "@scandic-hotels/tracking/base" import { trackEvent } from "@scandic-hotels/tracking/base"
import { trackLoginClick } from "@scandic-hotels/tracking/navigation" import { trackLoginClick } from "@scandic-hotels/tracking/navigation"
@@ -101,8 +100,8 @@ export function JoinScandicFriendsCard({ name = "join" }: Props) {
})} })}
</LoginButton> </LoginButton>
<div className={styles.terms}> <Typography variant="Body/Supporting text (caption)/smRegular">
<Footnote color="uiTextPlaceholder"> <p className={styles.terms}>
{intl.formatMessage( {intl.formatMessage(
{ {
id: "enterDetails.joinScandicFriendsCard.terms", id: "enterDetails.joinScandicFriendsCard.terms",
@@ -111,19 +110,18 @@ export function JoinScandicFriendsCard({ name = "join" }: Props) {
}, },
{ {
termsAndConditionsLink: (str) => ( termsAndConditionsLink: (str) => (
<Link <TextLink
textDecoration="underline" typography="Link/sm"
size="tiny"
target="_blank" target="_blank"
href={routes.membershipTermsAndConditions[lang]} href={routes.membershipTermsAndConditions[lang]}
> >
{str} {str}
</Link> </TextLink>
), ),
} }
)} )}
</Footnote> </p>
</div> </Typography>
</div> </div>
) )
} }

View File

@@ -34,6 +34,7 @@
.terms { .terms {
grid-area: terms; grid-area: terms;
color: var(--Text-Secondary);
} }
@media screen and (min-width: 768px) { @media screen and (min-width: 768px) {

View File

@@ -4,9 +4,8 @@ import { useIntl } from "react-intl"
import { CurrencyEnum } from "@scandic-hotels/common/constants/currency" import { CurrencyEnum } from "@scandic-hotels/common/constants/currency"
import { formatPrice } from "@scandic-hotels/common/utils/numberFormatting" 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 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 { Typography } from "@scandic-hotels/design-system/Typography"
import { trpc } from "@scandic-hotels/trpc/client" import { trpc } from "@scandic-hotels/trpc/client"
@@ -94,8 +93,8 @@ export function PartnerSASJoinScandicFriendsCard({
/> />
</div> </div>
<div className={styles.terms}> <Typography variant="Body/Supporting text (caption)/smRegular">
<Footnote color="uiTextPlaceholder"> <p className={styles.terms}>
{intl.formatMessage( {intl.formatMessage(
{ {
id: "enterDetails.joinScandicFriendsCard.terms", id: "enterDetails.joinScandicFriendsCard.terms",
@@ -104,19 +103,18 @@ export function PartnerSASJoinScandicFriendsCard({
}, },
{ {
termsAndConditionsLink: (str) => ( termsAndConditionsLink: (str) => (
<Link <TextLink
textDecoration="underline" typography="Link/sm"
size="tiny"
target="_blank" target="_blank"
href={routes.membershipTermsAndConditions[lang]} href={routes.membershipTermsAndConditions[lang]}
> >
{str} {str}
</Link> </TextLink>
), ),
} }
)} )}
</Footnote> </p>
</div> </Typography>
</div> </div>
) )
} }

View File

@@ -31,6 +31,7 @@
.terms { .terms {
grid-area: terms; grid-area: terms;
color: var(--Text-Secondary);
} }
@media screen and (min-width: 768px) { @media screen and (min-width: 768px) {

View File

@@ -6,7 +6,6 @@ import { useIntl } from "react-intl"
import { selectRate } from "@scandic-hotels/common/constants/routes/hotelReservation" import { selectRate } from "@scandic-hotels/common/constants/routes/hotelReservation"
import { Button } from "@scandic-hotels/design-system/Button" 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 { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
import { Typography } from "@scandic-hotels/design-system/Typography" import { Typography } from "@scandic-hotels/design-system/Typography"
import { getHotelRoom } from "@scandic-hotels/trpc/routers/booking/helpers" 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.wrapper} data-available={room.isAvailable}>
<div className={styles.main}> <div className={styles.main}>
<div className={styles.headerContainer}> <div className={styles.headerContainer}>
<Footnote <Typography variant="Title/Overline/sm">
className={styles.title} <h2 className={styles.title}>
asChild
textTransform="uppercase"
type="label"
color="uiTextHighContrast"
>
<h2>
{intl.formatMessage({ {intl.formatMessage({
id: "common.room", id: "common.room",
defaultMessage: "Room", defaultMessage: "Room",
})} })}
</h2> </h2>
</Footnote> </Typography>
<Typography <Typography
variant="Title/Subtitle/md" variant="Title/Subtitle/md"
className={styles.description} className={styles.description}

View File

@@ -9,7 +9,7 @@
} }
.facilities { .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); padding-bottom: var(--Space-x3);
} }

View File

@@ -177,12 +177,12 @@ export function SelectHotelMapContent({
> >
<MaterialIcon icon="close" size={20} color="CurrentColor" /> <MaterialIcon icon="close" size={20} color="CurrentColor" />
<Typography variant="Body/Supporting text (caption)/smBold"> <Typography variant="Body/Supporting text (caption)/smBold">
<p> <span>
{intl.formatMessage({ {intl.formatMessage({
id: "selectHotel.closeMap", id: "selectHotel.closeMap",
defaultMessage: "Close the map", defaultMessage: "Close the map",
})} })}
</p> </span>
</Typography> </Typography>
</Link> </Link>
</Button> </Button>

View File

@@ -29,7 +29,7 @@
.link { .link {
display: flex; display: flex;
gap: var(--Space-x05); gap: var(--Space-x05);
align-items: baseline; align-items: center;
} }
.bookingCodeFilter { .bookingCodeFilter {

View File

@@ -3,7 +3,6 @@
import { useIntl } from "react-intl" import { useIntl } from "react-intl"
import { formatPrice } from "@scandic-hotels/common/utils/numberFormatting" 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 { Typography } from "@scandic-hotels/design-system/Typography"
import { useBookingFlowConfig } from "../../bookingFlowConfig/bookingFlowConfigContext" import { useBookingFlowConfig } from "../../bookingFlowConfig/bookingFlowConfigContext"
@@ -37,9 +36,11 @@ export default function SignupPromoDesktop({
data-testid="signup-promo-desktop" data-testid="signup-promo-desktop"
> >
{badgeContent && <span className={styles.badge}>{badgeContent}</span>} {badgeContent && <span className={styles.badge}>{badgeContent}</span>}
<Footnote color="burgundy"> <Typography variant="Body/Supporting text (caption)/smRegular">
<Message price={price} isEnterDetailsPage={isEnterDetailsPage} /> <p>
</Footnote> <Message price={price} isEnterDetailsPage={isEnterDetailsPage} />
</p>
</Typography>
</div> </div>
) : null ) : null
} }

View File

@@ -2,7 +2,7 @@
import { useIntl } from "react-intl" 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" import { useBookingFlowConfig } from "../../bookingFlowConfig/bookingFlowConfigContext"
@@ -14,9 +14,11 @@ export default function SignupPromoMobile() {
data-footer-spacing-signup data-footer-spacing-signup
className={styles.memberDiscountBannerMobile} className={styles.memberDiscountBannerMobile}
> >
<Footnote color="burgundy"> <Typography variant="Body/Supporting text (caption)/smRegular">
<Message /> <p>
</Footnote> <Message />
</p>
</Typography>
</div> </div>
) )
} }

View File

@@ -5,6 +5,7 @@
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
color: var(--Text-Default);
} }
.memberDiscountBannerDesktop { .memberDiscountBannerDesktop {
@@ -16,10 +17,11 @@
padding: var(--Space-x15) var(--Space-x2); padding: var(--Space-x15) var(--Space-x2);
gap: var(--Space-x2); gap: var(--Space-x2);
position: relative; position: relative;
color: var(--Text-Default);
} }
.red { .red {
color: var(--Text-Accent-Primary); color: var(--Scandic-Brand-Scandic-Red);
} }
.badge { .badge {

View File

@@ -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);
}

View File

@@ -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} />
}

View File

@@ -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
)

View File

@@ -85,7 +85,10 @@ export const FormInput = forwardRef<HTMLInputElement, FormInputProps>(
ref={mergeRefs(field.ref, ref)} ref={mergeRefs(field.ref, ref)}
name={field.name} name={field.name}
onBlur={field.onBlur} onBlur={field.onBlur}
onChange={field.onChange} onChange={(event) => {
field.onChange(event)
props.onChange?.(event)
}}
value={field.value ?? ""} value={field.value ?? ""}
autoComplete={autoComplete} autoComplete={autoComplete}
id={id ?? field.name} id={id ?? field.name}

View File

@@ -35,8 +35,3 @@
justify-content: start; justify-content: start;
align-items: baseline; align-items: baseline;
} }
.perNight {
font-weight: 400;
font-size: var(--typography-Caption-Regular-fontSize);
}

View File

@@ -1,8 +1,8 @@
import { cx } from "class-variance-authority" import { cx } from "class-variance-authority"
import { useIntl } from "react-intl" import { useIntl } from "react-intl"
import { Divider } from "../../Divider"
import { RateTypeEnum } from "@scandic-hotels/common/constants/rateType" import { RateTypeEnum } from "@scandic-hotels/common/constants/rateType"
import { Divider } from "../../Divider"
import { Typography } from "../../Typography" import { Typography } from "../../Typography"
import styles from "./hotelPriceCard.module.css" import styles from "./hotelPriceCard.module.css"
@@ -117,14 +117,16 @@ export function HotelPriceCard({
> >
<p> <p>
{productTypePrices.localPrice.currency} {productTypePrices.localPrice.currency}
{/* eslint-disable-next-line formatjs/no-literal-string-in-jsx */} <Typography variant="Body/Supporting text (caption)/smRegular">
<span className={styles.perNight}> {/* eslint-disable-next-line formatjs/no-literal-string-in-jsx */}
/ <span>
{intl.formatMessage({ /
id: "common.night", {intl.formatMessage({
defaultMessage: "night", id: "common.night",
})} defaultMessage: "night",
</span> })}
</span>
</Typography>
</p> </p>
</Typography> </Typography>
</div> </div>

View File

@@ -160,6 +160,8 @@ import EditOutlined from "./generated/EditOutlined"
import EditFilled from "./generated/EditFilled" import EditFilled from "./generated/EditFilled"
import EditCalendarOutlined from "./generated/EditCalendarOutlined" import EditCalendarOutlined from "./generated/EditCalendarOutlined"
import EditCalendarFilled from "./generated/EditCalendarFilled" import EditCalendarFilled from "./generated/EditCalendarFilled"
import EditDocumentOutlined from "./generated/EditDocumentOutlined"
import EditDocumentFilled from "./generated/EditDocumentFilled"
import EditSquareOutlined from "./generated/EditSquareOutlined" import EditSquareOutlined from "./generated/EditSquareOutlined"
import EditSquareFilled from "./generated/EditSquareFilled" import EditSquareFilled from "./generated/EditSquareFilled"
import ElectricBikeOutlined from "./generated/ElectricBikeOutlined" import ElectricBikeOutlined from "./generated/ElectricBikeOutlined"
@@ -642,6 +644,9 @@ const _materialIcons = {
edit_calendar: { edit_calendar: {
rounded: { outlined: EditCalendarOutlined, filled: EditCalendarFilled }, rounded: { outlined: EditCalendarOutlined, filled: EditCalendarFilled },
}, },
edit_document: {
rounded: { outlined: EditDocumentOutlined, filled: EditDocumentFilled },
},
edit_square: { edit_square: {
rounded: { outlined: EditSquareOutlined, filled: EditSquareFilled }, rounded: { outlined: EditSquareOutlined, filled: EditSquareFilled },
}, },

View File

@@ -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

View File

@@ -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

View File

@@ -4,9 +4,7 @@ import { nodesToHtml } from "./utils"
import styles from "./jsontohtml.module.css" import styles from "./jsontohtml.module.css"
import { AlertTypeEnum } from "@scandic-hotels/common/constants/alert"
import { ImageVaultAsset } from "@scandic-hotels/common/utils/imageVault" import { ImageVaultAsset } from "@scandic-hotels/common/utils/imageVault"
import { AlertSidepeekContent } from "../../types/sidepeekContent"
import { ContentBlockType } from "./types/rte/enums" import { ContentBlockType } from "./types/rte/enums"
import type { RTENode } from "./types/rte/node" import type { RTENode } from "./types/rte/node"
import type { RenderOptions } from "./types/rte/option" import type { RenderOptions } from "./types/rte/option"
@@ -17,7 +15,7 @@ export type Node<T> = {
export type Embeds = export type Embeds =
| { | {
__typename: Exclude<ContentBlockType, "ImageContainer" | "Alert"> __typename: Exclude<ContentBlockType, "ImageContainer">
system?: { uid: string } | null system?: { uid: string } | null
url?: string | null url?: string | null
permanent_url?: string | null permanent_url?: string | null
@@ -31,25 +29,6 @@ export type Embeds =
image_left?: ImageVaultAsset image_left?: ImageVaultAsset
image_right?: 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>> export type EmbedByUid = Record<string, Node<Embeds>>

View File

@@ -20,7 +20,6 @@ import {
mapImageVaultAssetResponseToImageVaultAsset, mapImageVaultAssetResponseToImageVaultAsset,
mapInsertResponseToImageVaultAsset, mapInsertResponseToImageVaultAsset,
} from "@scandic-hotels/common/utils/imageVault" } from "@scandic-hotels/common/utils/imageVault"
import { Alert } from "../Alert"
import { TextLink } from "../TextLink" import { TextLink } from "../TextLink"
import type { EmbedByUid } from "./JsonToHtml" import type { EmbedByUid } from "./JsonToHtml"
import type { Attributes } from "./types/rte/attrs" import type { Attributes } from "./types/rte/attrs"
@@ -459,8 +458,6 @@ export const renderOptions: RenderOptions = {
) )
} }
return null return null
} else if (entry?.node.__typename === "Alert") {
return <Alert key={node.uid} {...entry.node} />
} else if ( } else if (
entry?.node.__typename === "AccountPage" || entry?.node.__typename === "AccountPage" ||
entry?.node.__typename === "CampaignOverviewPage" || entry?.node.__typename === "CampaignOverviewPage" ||

View File

@@ -50,30 +50,15 @@
gap: var(--Space-x05); 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 { .myPageMobileDropdown {
display: flex; display: flex;
align-items: center; align-items: center;
color: var(--Scandic-Brand-Burgundy); color: var(--Scandic-Brand-Burgundy);
font-family: var(--typography-Body-Regular-fontFamily); font-family:
font-size: var(--typography-Body-Regular-fontSize); var(--Body-Paragraph-Font-family), var(--Body-Paragraph-Font-fallback);
line-height: var(--typography-Body-Regular-lineHeight); font-size: var(--Body-Paragraph-Size);
letter-spacing: var(--typography-Body-Regular-letterSpacing); line-height: 1.5;
letter-spacing: var(--Body-Paragraph-Letter-spacing);
padding: var(--Space-x1); padding: var(--Space-x1);
border-radius: var(--Corner-Radius-md); border-radius: var(--Corner-Radius-md);
gap: var(--Space-x1); gap: var(--Space-x1);
@@ -97,11 +82,12 @@
.shortcut { .shortcut {
display: grid; display: grid;
align-items: center; align-items: center;
font-family: var(--typography-Body-Regular-fontFamily); font-family:
font-size: var(--typography-Body-Regular-fontSize); var(--Body-Paragraph-Font-family), var(--Body-Paragraph-Font-fallback);
font-weight: var(--typography-Body-Regular-fontWeight); font-size: var(--Body-Paragraph-Size);
letter-spacing: var(--typography-Body-Regular-letterSpacing); font-weight: var(--Body-Paragraph-Font-weight);
line-height: var(--typography-Body-Regular-lineHeight); letter-spacing: var(--Body-Paragraph-Letter-spacing);
line-height: 1.5;
gap: var(--Space-x2); gap: var(--Space-x2);
grid-template-columns: 1fr auto; grid-template-columns: 1fr auto;
padding: var(--Space-x2) var(--Space-x3); padding: var(--Space-x2) var(--Space-x3);
@@ -133,22 +119,13 @@
line-height: 140%; 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 { .bold {
font-family: var(--typography-Body-Bold-fontFamily); font-family:
font-size: var(--typography-Body-Bold-fontSize); var(--Body-Paragraph-Font-family), var(--Body-Paragraph-Font-fallback);
font-weight: 500 font-size: var(--Body-Paragraph-Size);
/* Should be fixed when variables starts working: var(--typography-Body-Bold-fontWeight) */; font-weight: var(--Body-Paragraph-Font-weight-2);
letter-spacing: var(--typography-Body-Bold-letterSpacing); letter-spacing: var(--Body-Paragraph-Letter-spacing);
line-height: var(--typography-Body-Bold-lineHeight); line-height: 1.5;
} }
.menu { .menu {

View File

@@ -18,7 +18,6 @@ export const linkVariants = cva(styles.link, {
size: { size: {
small: styles.small, small: styles.small,
large: styles.large, large: styles.large,
tiny: styles.tiny,
none: "", none: "",
}, },
textDecoration: { textDecoration: {
@@ -29,7 +28,6 @@ export const linkVariants = cva(styles.link, {
}, },
variant: { variant: {
icon: styles.icon, icon: styles.icon,
breadcrumb: styles.breadcrumb,
myPageMobileDropdown: styles.myPageMobileDropdown, myPageMobileDropdown: styles.myPageMobileDropdown,
navigation: styles.navigation, navigation: styles.navigation,
menu: styles.menu, menu: styles.menu,

View File

@@ -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 { :root {
--Base-Border-Hover: var(--Scandic-Peach-80); --Base-Border-Hover: var(--Scandic-Peach-80);
--Base-Border-Inverted: var(--UI-Opacity-White-100); --Base-Border-Inverted: var(--UI-Opacity-White-100);

View File

@@ -26,7 +26,6 @@
"./Divider": "./lib/components/Divider/index.tsx", "./Divider": "./lib/components/Divider/index.tsx",
"./FacilityToIcon": "./lib/components/FacilityToIcon/index.tsx", "./FacilityToIcon": "./lib/components/FacilityToIcon/index.tsx",
"./FakeButton": "./lib/components/FakeButton/index.tsx", "./FakeButton": "./lib/components/FakeButton/index.tsx",
"./Footnote": "./lib/components/Footnote/index.tsx",
"./Form/Checkbox": "./lib/components/Form/Checkbox/index.tsx", "./Form/Checkbox": "./lib/components/Form/Checkbox/index.tsx",
"./Form/Country": "./lib/components/Form/Country/index.tsx", "./Form/Country": "./lib/components/Form/Country/index.tsx",
"./Form/Date": "./lib/components/Form/Date/index.tsx", "./Form/Date": "./lib/components/Form/Date/index.tsx",

View File

@@ -1,6 +1,5 @@
import { gql } from "graphql-tag" import { gql } from "graphql-tag"
import { Alert } from "../Alert.graphql"
import { ImageContainer } from "../ImageContainer.graphql" import { ImageContainer } from "../ImageContainer.graphql"
import { AccountPageLink } from "../PageLink/AccountPageLink.graphql" import { AccountPageLink } from "../PageLink/AccountPageLink.graphql"
import { CampaignOverviewPageLink } from "../PageLink/CampaignOverviewPageLink.graphql" import { CampaignOverviewPageLink } from "../PageLink/CampaignOverviewPageLink.graphql"
@@ -25,7 +24,6 @@ export const Content_ContentPage = gql`
node { node {
__typename __typename
...SysAsset ...SysAsset
...Alert
...ImageContainer ...ImageContainer
...AccountPageLink ...AccountPageLink
...CampaignOverviewPageLink ...CampaignOverviewPageLink
@@ -47,7 +45,6 @@ export const Content_ContentPage = gql`
} }
} }
${SysAsset} ${SysAsset}
${Alert}
${ImageContainer} ${ImageContainer}
${AccountPageLink} ${AccountPageLink}
${CampaignOverviewPageLink} ${CampaignOverviewPageLink}

View File

@@ -1,4 +1,5 @@
import { router } from "../.." import { router } from "../.."
import { findBookingForCurrentUserRoute } from "./query/findBookingForCurrentUserRoute"
import { findBookingRoute } from "./query/findBookingRoute" import { findBookingRoute } from "./query/findBookingRoute"
import { getBookingRoute } from "./query/getBookingRoute" import { getBookingRoute } from "./query/getBookingRoute"
import { getBookingStatusRoute } from "./query/getBookingStatusRoute" import { getBookingStatusRoute } from "./query/getBookingStatusRoute"
@@ -7,6 +8,7 @@ import { getLinkedReservationsRoute } from "./query/getLinkedReservationsRoute"
export const bookingQueryRouter = router({ export const bookingQueryRouter = router({
get: getBookingRoute, get: getBookingRoute,
findBooking: findBookingRoute, findBooking: findBookingRoute,
findBookingForCurrentUser: findBookingForCurrentUserRoute,
linkedReservations: getLinkedReservationsRoute, linkedReservations: getLinkedReservationsRoute,
status: getBookingStatusRoute, status: getBookingStatusRoute,
}) })

View File

@@ -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,
}
})

View File

@@ -46,7 +46,7 @@ const getContactConfig = cache(async (lang: Lang) => {
GetContactConfig, GetContactConfig,
variables, variables,
{ {
key: `${lang}:contact`, key: `${lang}:contact_config`,
ttl: "max", ttl: "max",
} }
) )

View File

@@ -1,8 +1,6 @@
import { z } from "zod" import { z } from "zod"
import { BlocksEnums } from "../../../../types/blocksEnum" import { BlocksEnums } from "../../../../types/blocksEnum"
import { ContentEnum } from "../../../../types/content"
import { alertSchema, transformAlertSchema } from "../alert"
import { rawLinkUnionSchema, transformPageLink } from "../pageLinks" import { rawLinkUnionSchema, transformPageLink } from "../pageLinks"
import { imageContainerSchema } from "./imageContainer" import { imageContainerSchema } from "./imageContainer"
import { sysAssetSchema } from "./sysAsset" import { sysAssetSchema } from "./sysAsset"
@@ -24,11 +22,7 @@ export const contentSchema = z.object({
.discriminatedUnion("__typename", [ .discriminatedUnion("__typename", [
imageContainerSchema, imageContainerSchema,
sysAssetSchema, sysAssetSchema,
alertSchema.merge(
z.object({
__typename: z.literal(ContentEnum.blocks.Alert),
})
),
...rawLinkUnionSchema.options, ...rawLinkUnionSchema.options,
]) ])
.transform((data) => { .transform((data) => {
@@ -36,12 +30,6 @@ export const contentSchema = z.object({
if (link) { if (link) {
return link return link
} }
if (data.__typename === ContentEnum.blocks.Alert) {
return {
__typename: data.__typename,
...transformAlertSchema(data),
}
}
return data return data
}), }),
}) })

View File

@@ -12,7 +12,7 @@ import type { FilterType, HotelFilter } from "./output"
export async function getHotelFilters(lang: Lang) { export async function getHotelFilters(lang: Lang) {
const cacheClient = await getCacheClient() const cacheClient = await getCacheClient()
const cacheKey = `${lang}:getHotelFilters` const cacheKey = `${lang}:hotel_filter:outer`
return await cacheClient.cacheOrGet( return await cacheClient.cacheOrGet(
cacheKey, cacheKey,
@@ -27,7 +27,7 @@ export async function getHotelFilters(lang: Lang) {
const response = await request<unknown>( const response = await request<unknown>(
GetHotelFilters, GetHotelFilters,
{ locale: lang }, { locale: lang },
{ key: `${lang}:hotel_filters`, ttl: "1d" } { key: `${lang}:hotel_filter`, ttl: "1d" }
) )
if (!response.data) { if (!response.data) {

View File

@@ -1,3 +1,5 @@
import z from "zod"
import { signupVerify } from "@scandic-hotels/common/constants/routes/signup" import { signupVerify } from "@scandic-hotels/common/constants/routes/signup"
import { createLogger } from "@scandic-hotels/common/logger/createLogger" import { createLogger } from "@scandic-hotels/common/logger/createLogger"
import { createCounter } from "@scandic-hotels/common/telemetry" import { createCounter } from "@scandic-hotels/common/telemetry"
@@ -318,4 +320,33 @@ export const userMutationRouter = router({
return true 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
}),
}) })

View File

@@ -91,6 +91,7 @@ const ICONS = [
"download", "download",
"dresser", "dresser",
"edit_calendar", "edit_calendar",
"edit_document",
"edit_square", "edit_square",
"edit", "edit",
"electric_bike", "electric_bike",