Merged in feat/SW-2038-static-map-button (pull request #2715)

feat(SW-2038): refactor and create wrapper for static map and button

* feat(SW-2038): refactor and create wrapper for static map and button

* feature: use button from design-system over creating a new one

* remove unused fragment

* fix(SW-2038): add removed css

* fix(SW-2038): update fake button component

* fix(SW-2038): move FakeButton to design system


Approved-by: Erik Tiekstra
Approved-by: Joakim Jäderberg
This commit is contained in:
Matilda Landström
2025-09-01 08:16:27 +00:00
parent 19063da08a
commit 93a90bef9d
16 changed files with 164 additions and 174 deletions

View File

@@ -1,46 +0,0 @@
"use client"
import { useParams } from "next/navigation"
import { useEffect, useState } from "react"
import { useIntl } from "react-intl"
import ButtonLink from "@scandic-hotels/design-system/ButtonLink"
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
import styles from "./mapButton.module.css"
interface MapButtonProps {
className?: string
}
export default function MapButton({ className = "" }: MapButtonProps) {
const intl = useIntl()
const params = useParams()
const [mapUrl, setMapUrl] = useState<string | null>(null)
useEffect(() => {
const url = new URL(window.location.href)
url.searchParams.set("view", "map")
setMapUrl(url.toString())
}, [params])
if (!mapUrl) {
return null
}
return (
<ButtonLink
href={mapUrl}
variant="Primary"
color="Inverted"
size="Small"
typography="Body/Supporting text (caption)/smBold"
className={`${className} ${styles.button}`}
>
<MaterialIcon icon="map" color="CurrentColor" />
{intl.formatMessage({
defaultMessage: "See on map",
})}
</ButtonLink>
)
}

View File

@@ -1,3 +0,0 @@
.button {
box-shadow: 0px 0px 8px 1px rgba(0, 0, 0, 0.1);
}

View File

@@ -0,0 +1,30 @@
"use client"
import Link from "next/link"
import { useParams } from "next/navigation"
import { useEffect, useState } from "react"
import { MapWithButtonWrapper } from "@/components/Maps/MapWithButtonWrapper"
import styles from "./mapWrapper.module.css"
export default function MapWrapper({ children }: React.PropsWithChildren) {
const params = useParams()
const [mapUrl, setMapUrl] = useState<string | null>(null)
useEffect(() => {
const url = new URL(window.location.href)
url.searchParams.set("view", "map")
setMapUrl(url.toString())
}, [params])
if (!mapUrl) {
return null
}
return (
<Link className={styles.link} href={mapUrl}>
<MapWithButtonWrapper>{children}</MapWithButtonWrapper>
</Link>
)
}

View File

@@ -0,0 +1,9 @@
.link {
display: none;
}
@media (min-width: 1367px) {
.link {
display: flex;
}
}

View File

@@ -1,9 +1,7 @@
import StaticMap from "@/components/Maps/StaticMap"
import { getIntl } from "@/i18n"
import MapButton from "./MapButton"
import styles from "./staticMap.module.css"
import MapWrapper from "./MapWrapper"
import type { MapLocation } from "@/types/components/mapLocation"
@@ -43,7 +41,7 @@ export default async function DestinationStaticMap({
: undefined
return (
<div className={styles.mapWrapper}>
<MapWrapper>
<StaticMap
country={country}
city={city}
@@ -53,7 +51,6 @@ export default async function DestinationStaticMap({
zoomLevel={getZoomLevel(location?.default_zoom, !!country)}
altText={altText}
/>
<MapButton className={styles.button} />
</div>
</MapWrapper>
)
}

View File

@@ -1,39 +0,0 @@
.mapWrapper {
position: relative;
width: 100%;
height: 200px;
display: flex;
justify-content: center;
align-items: center;
border-radius: var(--Corner-radius-md);
overflow: hidden;
}
.mapWrapper::before {
content: "";
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(31, 28, 27, 0);
transition: background-color 0.3s ease;
pointer-events: none;
z-index: 1;
}
.mapWrapper:hover::before {
background-color: rgba(31, 28, 27, 0.1);
}
.button {
position: absolute;
right: var(--Space-x2);
bottom: var(--Space-x2);
}
@media screen and (max-width: 1366px) {
.mapWrapper {
display: none;
}
}

View File

@@ -2,7 +2,7 @@
import { useIntl } from "react-intl"
import Preamble from "@scandic-hotels/design-system/Preamble"
import { Typography } from "@scandic-hotels/design-system/Typography"
import { useHotelFilterStore } from "@/stores/hotel-filters"
@@ -11,13 +11,15 @@ export default function HotelCount() {
const resultCount = useHotelFilterStore((state) => state.resultCount)
return (
<Preamble>
{intl.formatMessage(
{
defaultMessage: "{amount, plural, one {# hotel} other {# hotels}}",
},
{ amount: resultCount }
)}
</Preamble>
<Typography variant="Title/Subtitle/md">
<span>
{intl.formatMessage(
{
defaultMessage: "{amount, plural, one {# hotel} other {# hotels}}",
},
{ amount: resultCount }
)}
</span>
</Typography>
)
}

View File

@@ -1,7 +1,5 @@
import BookingCodeFilter from "@scandic-hotels/booking-flow/BookingCodeFilter"
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
import Link from "@scandic-hotels/design-system/Link"
import Subtitle from "@scandic-hotels/design-system/Subtitle"
import { Typography } from "@scandic-hotels/design-system/Typography"
import HotelCardListing from "@/components/HotelReservation/HotelCardListing"
@@ -10,8 +8,8 @@ import HotelCount from "@/components/HotelReservation/SelectHotel/HotelCount"
import HotelSorter from "@/components/HotelReservation/SelectHotel/HotelSorter"
import MobileMapButtonContainer from "@/components/HotelReservation/SelectHotel/MobileMapButtonContainer"
import NoAvailabilityAlert from "@/components/HotelReservation/SelectHotel/NoAvailabilityAlert"
import { MapWithButtonWrapper } from "@/components/Maps/MapWithButtonWrapper"
import StaticMap from "@/components/Maps/StaticMap"
import { getIntl } from "@/i18n"
import { getFiltersFromHotels, type HotelResponse } from "./helpers"
@@ -38,8 +36,6 @@ export default async function SelectHotel({
mapHref,
title,
}: SelectHotelProps) {
const intl = await getIntl()
const isAllUnavailable = hotels.every(
(hotel) => hotel.availability.status !== "Available"
)
@@ -63,7 +59,9 @@ export default async function SelectHotel({
<div className={styles.headerContent}>
<div className={styles.title}>
<div className={styles.cityInformation}>
<Subtitle>{title}</Subtitle>
<Typography variant="Title/Subtitle/lg">
<p>{title}</p>
</Typography>
<HotelCount />
</div>
<div className={styles.sorter}>
@@ -78,7 +76,7 @@ export default async function SelectHotel({
<div className={styles.sideBar}>
{hotels.length ? (
<Link className={styles.link} href={mapHref} keepSearchParams>
<div className={styles.mapContainer}>
<MapWithButtonWrapper>
<StaticMap
city={city.name}
country={isCityWithCountry(city) ? city.country : undefined}
@@ -88,15 +86,7 @@ export default async function SelectHotel({
mapType="roadmap"
altText={`Map of ${city.name} city center`}
/>
<Typography variant="Body/Supporting text (caption)/smBold">
<span className={styles.mapButton}>
<MaterialIcon icon="map" color="CurrentColor" size={20} />
{intl.formatMessage({
defaultMessage: "See on map",
})}
</span>
</Typography>
</div>
</MapWithButtonWrapper>
</Link>
) : (
<div className={styles.mapContainer}>

View File

@@ -8,7 +8,7 @@
}
.header {
padding: var(--Spacing-x3) 0 var(--Spacing-x2);
padding: var(--Space-x3) 0 var(--Space-x2);
}
.headerContent {
@@ -16,13 +16,13 @@
margin: 0 auto;
display: flex;
flex-direction: column;
gap: var(--Spacing-x2);
gap: var(--Space-x2);
}
.cityInformation {
display: flex;
flex-wrap: wrap;
gap: var(--Spacing-x1);
gap: var(--Space-x1);
align-items: baseline;
}
@@ -43,21 +43,11 @@
display: none;
}
.buttonContainer {
display: flex;
gap: var(--Spacing-x2);
margin-bottom: var(--Spacing-x3);
}
.button {
flex: 1;
}
.hotelList {
flex: 1;
display: flex;
flex-direction: column;
gap: var(--Spacing-x3);
gap: var(--Space-x3);
}
.filter {
@@ -65,25 +55,14 @@
}
.skeletonContainer .title {
margin-bottom: var(--Spacing-x3);
}
.mapButton {
display: flex;
padding: 10px var(--Space-x2);
justify-content: center;
align-items: center;
border-radius: var(--Corner-radius-rounded);
background: var(--Component-Button-Inverted-Fill-Default);
box-shadow: 0px 0px 8px 1px rgba(0, 0, 0, 0.1);
gap: var(--Space-x05);
margin-bottom: var(--Space-x3);
}
@media (min-width: 768px) {
.main {
padding: var(--Spacing-x5) 0;
padding: var(--Space-x5) 0;
flex-direction: row;
gap: var(--Spacing-x5);
gap: var(--Space-x5);
flex-wrap: wrap;
}
@@ -93,7 +72,7 @@
.header {
background-color: var(--Base-Surface-Subtle-Normal);
padding: var(--Spacing-x4) 0 var(--Spacing-x3);
padding: var(--Space-x4) 0 var(--Space-x3);
}
.sorter {
@@ -126,35 +105,11 @@
margin-bottom: var(--Space-x6);
}
.mapContainer {
display: flex;
flex-direction: column;
background: var(--Base-Surface-Primary-light-Normal);
border-radius: var(--Corner-radius-md);
border: 1px solid var(--Base-Border-Subtle);
position: relative;
}
.mapButton {
position: absolute;
bottom: var(--Space-x2);
right: var(--Space-x2);
}
.mapButton:hover {
background-color: var(--Base-Button-Inverted-Fill-Hover);
color: var(--Base-Button-Inverted-On-Fill-Hover);
}
.buttonContainer {
display: none;
}
.skeletonContainer .title {
margin-bottom: 0;
}
.skeletonContainer .sideBar {
gap: var(--Spacing-x3);
gap: var(--Space-x3);
}
}

View File

@@ -0,0 +1,28 @@
"use client"
import { useIntl } from "react-intl"
import { FakeButton } from "@scandic-hotels/design-system/FakeButton"
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
import styles from "./mapWithButtonWrapper.module.css"
export function MapWithButtonWrapper({ children }: React.PropsWithChildren) {
const intl = useIntl()
return (
<div className={styles.container}>
{children}
<FakeButton
variant="Primary"
color="Inverted"
size="Small"
typography="Body/Supporting text (caption)/smBold"
className={styles.button}
>
<MaterialIcon icon="map" color="CurrentColor" size={20} />
{intl.formatMessage({
defaultMessage: "See on map",
})}
</FakeButton>
</div>
)
}

View File

@@ -0,0 +1,16 @@
.container {
display: flex;
position: relative;
border-radius: var(--Corner-radius-md);
overflow: hidden;
flex-direction: column;
align-items: center;
}
.button {
position: absolute;
bottom: var(--Space-x2);
right: var(--Space-x2);
box-shadow: 0px 0px 8px 1px rgba(0, 0, 0, 0.1);
}

View File

@@ -0,0 +1,6 @@
.fakeButton {
display: flex;
align-items: center;
border-radius: var(--Corner-radius-rounded);
gap: var(--Space-x05);
}

View File

@@ -0,0 +1,39 @@
'use client'
import { variants } from './variants'
import type { VariantProps } from 'class-variance-authority'
import type { ComponentProps, PropsWithChildren } from 'react'
import type { Button } from 'react-aria-components'
interface FakeButtonProps
extends PropsWithChildren,
Omit<ComponentProps<typeof Button>, 'children' | 'onPress'>,
VariantProps<typeof variants> {}
export function FakeButton({
variant,
color,
size,
typography,
children,
className,
...props
}: FakeButtonProps) {
const classNames = variants({
color,
size,
variant,
typography,
className,
})
return (
<span
className={classNames}
{...(props as React.HTMLProps<HTMLSpanElement>)}
>
{children}
</span>
)
}

View File

@@ -0,0 +1,6 @@
import { cva } from 'class-variance-authority'
import styles from './fakeButton.module.css'
import { withButton } from '../Button'
export const variants = cva(styles.fakeButton, withButton({}))

View File

@@ -70,6 +70,5 @@ export default async function StaticMap({
}
const src = getUrlWithSignature(url, googleMapSecret)
return <img src={src} alt={altText} />
}

View File

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