Merged in fix/BOOK-210-update-hotel-card-local-charges (pull request #2835)

fix(BOOK-210): add local charges for Finland and update design for hotel card

* fix(BOOK-210): add local charges for Finland and update design for hotel card

* feat(BOOK-210): change variant to conditional classname

* fix(BOOK-210): update link with icon

* fix(BOOK-210): update buttonlink tripadvisor

* fix(BOOK-210): switch wrapper logic

* fix(BOOK-210): update variants tripadvisor


Approved-by: Erik Tiekstra
This commit is contained in:
Bianca Widstam
2025-09-23 08:54:07 +00:00
parent 16e6c1596c
commit 914b095e16
11 changed files with 227 additions and 116 deletions

View File

@@ -1,60 +0,0 @@
import TripadvisorIcon from "@scandic-hotels/design-system/Icons/TripadvisorIcon"
import Link from "@scandic-hotels/design-system/Link"
import { getIntl } from "@/i18n"
import styles from "./tripAdvisorLink.module.css"
import type { HotelTripAdvisor } from "@scandic-hotels/trpc/types/hotel"
import { SidepeekSlugs } from "@/types/components/hotelPage/hotelPage"
interface TripAdvisorLinkProps {
tripAdvisor: NonNullable<HotelTripAdvisor>
}
export default async function TripAdvisorLink({
tripAdvisor,
}: TripAdvisorLinkProps) {
const intl = await getIntl()
const { rating, numberOfReviews, reviews } = tripAdvisor
const hasTripAdvisorData = !!(rating && numberOfReviews)
if (!hasTripAdvisorData) {
return null
}
const formattedTripAdvisorText = intl.formatMessage(
{
defaultMessage: "{rating} ({count} reviews on Tripadvisor)",
},
{ rating, count: numberOfReviews }
)
const hasTripAdvisorIframeSrc = !!reviews.widgetScriptEmbedUrlIframe
const tripAdvisorHref = hasTripAdvisorIframeSrc
? `?s=${SidepeekSlugs.tripAdvisor}`
: null
if (!tripAdvisorHref) {
return (
<span className={styles.tripAdvisorText}>
<TripadvisorIcon color="CurrentColor" />
{formattedTripAdvisorText}
</span>
)
}
return (
<Link
variant="icon"
textDecoration="underline"
color="Text/Interactive/Secondary"
size="small"
href={tripAdvisorHref}
>
<TripadvisorIcon color="CurrentColor" size={20} />
{formattedTripAdvisorText}
</Link>
)
}

View File

@@ -1,6 +0,0 @@
.tripAdvisorText {
display: flex;
gap: var(--Space-x05);
align-items: center;
color: var(--Text-Secondary);
}

View File

@@ -0,0 +1,70 @@
import ButtonLink from "@scandic-hotels/design-system/ButtonLink"
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
import { TripAdvisorChip } from "@scandic-hotels/design-system/TripAdvisorChip"
import { getIntl } from "@/i18n"
import styles from "./tripAdvisorSection.module.css"
import type { HotelTripAdvisor } from "@scandic-hotels/trpc/types/hotel"
import { SidepeekSlugs } from "@/types/components/hotelPage/hotelPage"
interface TripAdvisorSectionProps {
tripAdvisor: NonNullable<HotelTripAdvisor>
}
export default async function TripAdvisorSection({
tripAdvisor,
}: TripAdvisorSectionProps) {
const intl = await getIntl()
const { rating, numberOfReviews, reviews } = tripAdvisor
const hasTripAdvisorData = !!(rating && numberOfReviews)
if (!hasTripAdvisorData) {
return null
}
const formattedTripAdvisorText = intl.formatMessage(
{
defaultMessage: "{count} reviews",
},
{ count: numberOfReviews }
)
const hasTripAdvisorIframeSrc = !!reviews.widgetScriptEmbedUrlIframe
const tripAdvisorHref = hasTripAdvisorIframeSrc
? `?s=${SidepeekSlugs.tripAdvisor}`
: null
if (!tripAdvisorHref) {
return (
<TripAdvisorChip
rating={tripAdvisor.rating}
color="subtle"
wrapper={false}
/>
)
}
return (
<div className={styles.tripAdvisorSection}>
<TripAdvisorChip
rating={tripAdvisor.rating}
color="subtle"
wrapper={false}
/>
<ButtonLink
href={tripAdvisorHref}
variant="Text"
color="Primary"
size="Small"
typography="Body/Supporting text (caption)/smBold"
wrapping={false}
>
{formattedTripAdvisorText}
<MaterialIcon icon="chevron_right" color="CurrentColor" size={20} />
</ButtonLink>
</div>
)
}

View File

@@ -0,0 +1,4 @@
.tripAdvisorSection {
display: flex;
gap: var(--Space-x1);
}

View File

@@ -1,12 +1,17 @@
import { cx } from "class-variance-authority"
import { getSingleDecimal } from "@scandic-hotels/common/utils/numberFormatting"
import ButtonLink from "@scandic-hotels/design-system/ButtonLink"
import { Divider } from "@scandic-hotels/design-system/Divider"
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
import Link from "@scandic-hotels/design-system/Link"
import { Typography } from "@scandic-hotels/design-system/Typography"
import { Country } from "@scandic-hotels/trpc/types/country"
import LocalCallCharges from "@/components/LocalCallCharges"
import { getIntl } from "@/i18n"
import TripAdvisorLink from "./TripAdvisorLink"
import TripAdvisorSection from "./TripAdvisorSection"
import styles from "./introSection.module.css"
@@ -37,7 +42,7 @@ export default async function IntroSection({
phoneNumber,
}: IntroSectionProps) {
const intl = await getIntl()
const { streetAddress, city } = address
const { streetAddress, city, country } = address
const { distanceToCentre } = location
const formattedDistanceText = intl.formatMessage(
{
@@ -48,6 +53,8 @@ export default async function IntroSection({
const formattedLocationText = `${streetAddress}, ${city} (${formattedDistanceText})`
const showLocalCharges = country === Country.Finland
return (
<section className={styles.introSection}>
<div className={styles.mainContent}>
@@ -63,7 +70,11 @@ export default async function IntroSection({
<h1 className={styles.title}>{hotelName}</h1>
</Typography>
</div>
<address className={styles.adressPhoneNumber}>
<address
className={cx(styles.addressPhoneNumber, {
[styles.stacked]: showLocalCharges,
})}
>
<span className={styles.location}>
<span className={styles.address}>
<MaterialIcon icon="location_on" color="CurrentColor" size={20} />
@@ -71,16 +82,35 @@ export default async function IntroSection({
<p>{formattedLocationText}</p>
</Typography>
</span>
<Divider variant="vertical" color="Border/Divider/Default" />
{!showLocalCharges && (
<Divider
className={styles.divider}
variant="vertical"
color="Border/Divider/Default"
/>
)}
</span>
<Typography variant="Body/Underline/sm">
<a href={`tel:${phoneNumber}`} className={styles.phoneNumber}>
<div className={styles.phoneWrapper}>
<Link
href={`tel:${phoneNumber}`}
size="small"
color="Text/Interactive/Secondary"
textDecoration="underline"
variant="icon"
>
<MaterialIcon icon="phone" color="CurrentColor" size={20} />
{phoneNumber}
</a>
</Typography>
</Link>
<LocalCallCharges
className={styles.localCharges}
country={country}
/>
</div>
</address>
{tripAdvisor ? <TripAdvisorLink tripAdvisor={tripAdvisor} /> : null}
{tripAdvisor ? <TripAdvisorSection tripAdvisor={tripAdvisor} /> : null}
</div>
<div className={styles.subtitleContent}>
<Typography variant="Body/Lead text">

View File

@@ -5,6 +5,14 @@
max-width: var(--hotel-page-intro-section-width);
}
.localCharges::before {
content: "(";
}
.localCharges::after {
content: ")";
}
.mainContent {
display: grid;
gap: var(--Space-x15);
@@ -31,20 +39,13 @@
gap: var(--Space-x2);
}
.adressPhoneNumber {
.addressPhoneNumber {
font-style: normal;
display: grid;
gap: var(--Space-x15);
align-items: center;
}
.phoneNumber {
color: var(--Text-Interactive-Default);
display: flex;
gap: var(--Space-x05);
align-items: center;
}
.address {
color: var(--Text-Secondary);
gap: var(--Space-x05);
@@ -58,14 +59,40 @@
height: 100%;
}
.divider {
display: none;
}
.phoneWrapper {
display: flex;
flex-direction: column;
gap: var(--Space-x15);
}
@media screen and (min-width: 767px) {
.adressPhoneNumber {
.addressPhoneNumber {
display: flex;
flex-direction: row;
gap: var(--Space-x2);
}
.addressPhoneNumber.stacked {
display: grid;
gap: var(--Space-x15);
align-items: center;
}
.address {
align-items: center;
}
.phoneWrapper {
flex-direction: row;
gap: var(--Space-x1);
align-items: center;
}
.divider {
display: block;
}
}

View File

@@ -5,15 +5,17 @@ import { getIntl } from "@/i18n"
interface LocalCallChargesProps {
country: string
className?: string
}
export default async function LocalCallCharges({
country,
className,
}: LocalCallChargesProps) {
const intl = await getIntl()
return country === Country.Finland ? (
<Typography variant="Body/Supporting text (caption)/smRegular">
<p>
<p className={className}>
{intl.formatMessage({
defaultMessage: "Price 0,16 €/min + local call charges",
})}

View File

@@ -39,7 +39,7 @@ export function HotelCardDialogImage({
{rating?.tripAdvisor && (
<TripAdvisorChip
rating={rating.tripAdvisor}
variant={position === 'top' ? 'small' : 'default'}
size={position === 'top' ? 'small' : 'default'}
/>
)}
</div>

View File

@@ -7,36 +7,57 @@ const meta: Meta<typeof TripAdvisorChip> = {
component: TripAdvisorChip,
argTypes: {
rating: {
control: {
type: 'number',
min: 0,
max: 5,
step: 0.1,
},
control: { type: 'number', min: 0, max: 5, step: 0.1 },
},
variant: {
control: {
type: 'select',
},
size: {
control: { type: 'select' },
options: ['default', 'small'],
},
color: {
control: { type: 'select' },
options: ['default', 'subtle'],
},
wrapper: {
control: { type: 'boolean' },
},
},
}
export default meta
type Story = StoryObj<typeof TripAdvisorChip>
export const PrimaryDefault: Story = {
export const Default: Story = {
args: {
rating: 4.5,
variant: 'default',
size: 'default',
color: 'default',
wrapper: false,
},
}
export const WithWrapper: Story = {
args: {
rating: 4.5,
size: 'default',
color: 'default',
wrapper: true,
},
}
export const Small: Story = {
args: {
rating: 4.5,
variant: 'small',
size: 'small',
color: 'default',
wrapper: true,
},
}
export const Subtle: Story = {
args: {
rating: 4.5,
size: 'default',
color: 'subtle',
wrapper: false,
},
}

View File

@@ -5,42 +5,57 @@ import { Typography } from '../Typography'
const container = cva(styles.container, {
variants: {
variant: {
size: {
default: null,
small: styles.containerSmall,
},
},
defaultVariants: {
variant: 'default',
size: 'default',
},
})
const chip = cva(styles.tripAdvisor, {
variants: {
variant: {
size: {
default: null,
small: styles.tripAdvisorSmall,
},
color: {
default: null,
subtle: styles.tripAdvisorSubtle,
},
},
defaultVariants: {
variant: 'default',
size: 'default',
color: 'default',
},
})
type TripAdvisorProps = {
rating: number
} & VariantProps<typeof container>
wrapper?: boolean
} & VariantProps<typeof chip>
export function TripAdvisorChip({ rating, variant }: TripAdvisorProps) {
return (
// Wrapping the chip in a transparent container with some padding to increase the touch target
<div className={container({ variant })}>
<div className={chip({ variant })}>
<TripadvisorIcon size={16} color="Icon/Interactive/Default" />
<Typography variant="Tag/sm">
<p>{rating}</p>
</Typography>
</div>
export function TripAdvisorChip({
rating,
wrapper = true,
size,
color,
}: TripAdvisorProps) {
const content = (
<div className={chip({ size, color })}>
<TripadvisorIcon size={16} color="CurrentColor" />
<Typography variant="Tag/sm">
<p>{rating}</p>
</Typography>
</div>
)
return wrapper ? (
// Wrapping the chip in a transparent container with some padding to increase the touch target
<div className={container({ size })}>{content}</div>
) : (
content
)
}

View File

@@ -6,14 +6,18 @@
}
.containerSmall {
position: absolute;
left: 0;
top: 0;
padding: var(--Space-x05);
}
.tripAdvisor {
display: flex;
display: inline-flex;
align-items: center;
gap: var(--Space-x05);
background-color: var(--Base-Surface-Primary-light-Normal);
color: var(--Text-Interactive-Default);
padding: var(--Space-x05) var(--Space-x1);
border-radius: var(--Corner-radius-sm);
}
@@ -22,3 +26,7 @@
padding: 0 var(--Space-x05) 0 3px;
border-radius: 2px;
}
.tripAdvisorSubtle {
background-color: var(--Surface-Secondary-Subtle, #e3d9d1);
}