Merged in feature/SW-3595-sas-info-boxes (pull request #3177)
Feature/SW-3595 Add info boxes to SAS start page & Eurobonus alert to select-hotel page on SAS
* wip
* feat(SW-3595): Add info boxes to SAS start page
* Add InfoBox to design-system
* Add background gradient to SAS start page
* update variable naming and conditionalize the eurobonus message on select-hotel
* SAS startpage update default message
* make select-hotel a bit more generic with slot={} instead of alert={}
Approved-by: Anton Gunnarsson
This commit is contained in:
@@ -1,8 +1,11 @@
|
|||||||
import { SelectHotelPage as SelectHotelPagePrimitive } from "@scandic-hotels/booking-flow/pages/SelectHotelPage"
|
import { SelectHotelPage as SelectHotelPagePrimitive } from "@scandic-hotels/booking-flow/pages/SelectHotelPage"
|
||||||
|
import { AlertTypeEnum } from "@scandic-hotels/common/constants/alert"
|
||||||
import { toCapitalCase } from "@scandic-hotels/common/utils/toCapitalCase"
|
import { toCapitalCase } from "@scandic-hotels/common/utils/toCapitalCase"
|
||||||
|
import { Alert } from "@scandic-hotels/design-system/Alert"
|
||||||
|
|
||||||
import { bookingFlowConfig } from "@/constants/bookingFlowConfig"
|
import { bookingFlowConfig } from "@/constants/bookingFlowConfig"
|
||||||
|
|
||||||
|
import { getIntl } from "@/i18n"
|
||||||
import { getLang } from "@/i18n/serverContext"
|
import { getLang } from "@/i18n/serverContext"
|
||||||
|
|
||||||
import type { Metadata } from "next"
|
import type { Metadata } from "next"
|
||||||
@@ -26,12 +29,29 @@ export async function generateMetadata({
|
|||||||
export default async function SelectHotelPage(props: PageArgs<LangParams>) {
|
export default async function SelectHotelPage(props: PageArgs<LangParams>) {
|
||||||
const searchParams = await props.searchParams
|
const searchParams = await props.searchParams
|
||||||
const lang = await getLang()
|
const lang = await getLang()
|
||||||
|
const intl = await getIntl()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SelectHotelPagePrimitive
|
<SelectHotelPagePrimitive
|
||||||
lang={lang}
|
lang={lang}
|
||||||
searchParams={searchParams}
|
searchParams={searchParams}
|
||||||
config={bookingFlowConfig}
|
config={bookingFlowConfig}
|
||||||
|
topSlot={
|
||||||
|
bookingFlowConfig.redemptionEnabled ? (
|
||||||
|
<Alert
|
||||||
|
heading={intl.formatMessage({
|
||||||
|
id: "selectHotel.earnEuroBonusPointsAlert.heading",
|
||||||
|
defaultMessage: "Earn & use EuroBonus points",
|
||||||
|
})}
|
||||||
|
text={intl.formatMessage({
|
||||||
|
id: "selectHotel.earnEuroBonusPointsAlert.text",
|
||||||
|
defaultMessage:
|
||||||
|
"Collect new points with every booking, or use your current points to pay for hotel stays.",
|
||||||
|
})}
|
||||||
|
type={AlertTypeEnum.Info}
|
||||||
|
/>
|
||||||
|
) : undefined
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,42 @@
|
|||||||
.mainContent {
|
.mainContent {
|
||||||
/* Calculate height for large screens and displays */
|
/* Calculate height for large screens and displays */
|
||||||
height: calc(100dvh - 402px);
|
height: calc(100dvh - 100px);
|
||||||
|
|
||||||
min-height: 480px;
|
min-height: 480px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
background: linear-gradient(
|
||||||
|
color-mix(in srgb, var(--Scandic-Grey-100) 14%, transparent),
|
||||||
|
color-mix(in srgb, var(--Scandic-Red-100) 40%, transparent)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
.contentContainer {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--Space-x3);
|
||||||
|
height: 100%;
|
||||||
|
justify-content: space-evenly;
|
||||||
|
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.bookingWidgetWrapper {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--Space-x3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.heading {
|
||||||
|
text-transform: uppercase;
|
||||||
|
text-align: center;
|
||||||
|
text-wrap: balance;
|
||||||
|
color: white;
|
||||||
|
text-shadow: black 0px 0px 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.backdrop {
|
.backdrop {
|
||||||
@@ -15,4 +46,17 @@
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
|
z-index: -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.infoBoxes {
|
||||||
|
width: var(--max-width-content);
|
||||||
|
margin: 0 auto;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(1, 1fr);
|
||||||
|
gap: var(--Space-x2);
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
gap: var(--Space-x3);
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,14 @@
|
|||||||
import { FloatingBookingWidget } from "@scandic-hotels/booking-flow/BookingWidget/FloatingBookingWidget"
|
import { FloatingBookingWidget } from "@scandic-hotels/booking-flow/BookingWidget/FloatingBookingWidget"
|
||||||
import { parseBookingWidgetSearchParams } from "@scandic-hotels/booking-flow/utils/url"
|
import { parseBookingWidgetSearchParams } from "@scandic-hotels/booking-flow/utils/url"
|
||||||
|
import { IconName } from "@scandic-hotels/design-system/Icons/iconName"
|
||||||
import Image from "@scandic-hotels/design-system/Image"
|
import Image from "@scandic-hotels/design-system/Image"
|
||||||
|
import { InfoBox } from "@scandic-hotels/design-system/InfoBox"
|
||||||
|
import { Typography } from "@scandic-hotels/design-system/Typography"
|
||||||
import { TrackingSDK } from "@scandic-hotels/tracking/TrackingSDK"
|
import { TrackingSDK } from "@scandic-hotels/tracking/TrackingSDK"
|
||||||
|
|
||||||
import { bookingFlowConfig } from "@/constants/bookingFlowConfig"
|
import { bookingFlowConfig } from "@/constants/bookingFlowConfig"
|
||||||
|
|
||||||
|
import { getIntl } from "@/i18n"
|
||||||
import { getLang } from "@/i18n/serverContext"
|
import { getLang } from "@/i18n/serverContext"
|
||||||
import LandingPageHeroImage from "@/public/_static/img/landing-page-hero.png"
|
import LandingPageHeroImage from "@/public/_static/img/landing-page-hero.png"
|
||||||
|
|
||||||
@@ -18,6 +22,7 @@ export default async function Home(props: PageArgs<LangParams>) {
|
|||||||
const booking = parseBookingWidgetSearchParams(searchParams)
|
const booking = parseBookingWidgetSearchParams(searchParams)
|
||||||
|
|
||||||
const lang = await getLang()
|
const lang = await getLang()
|
||||||
|
const intl = await getIntl()
|
||||||
|
|
||||||
const trackingData = {
|
const trackingData = {
|
||||||
pageName: "startpage",
|
pageName: "startpage",
|
||||||
@@ -31,12 +36,52 @@ export default async function Home(props: PageArgs<LangParams>) {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<section className={styles.mainContent}>
|
<section className={styles.mainContent}>
|
||||||
<div className={styles.bookingWidgetWrapper}>
|
<div className={styles.contentContainer}>
|
||||||
<FloatingBookingWidget
|
<section className={styles.bookingWidgetWrapper}>
|
||||||
booking={booking}
|
<Typography variant={"Title/lg"}>
|
||||||
lang={lang}
|
<h1 className={styles.heading}>
|
||||||
config={bookingFlowConfig}
|
{intl.formatMessage({
|
||||||
/>
|
id: "partnerSas.startPage.heading",
|
||||||
|
defaultMessage: "Book a hotel at the best price",
|
||||||
|
})}
|
||||||
|
</h1>
|
||||||
|
</Typography>
|
||||||
|
<FloatingBookingWidget
|
||||||
|
booking={booking}
|
||||||
|
lang={lang}
|
||||||
|
config={bookingFlowConfig}
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
{bookingFlowConfig.redemptionEnabled && (
|
||||||
|
<section className={styles.infoBoxes}>
|
||||||
|
<InfoBox
|
||||||
|
heading={intl.formatMessage({
|
||||||
|
id: "partnerSas.startPage.infoBox1.heading",
|
||||||
|
defaultMessage: "Earn points",
|
||||||
|
})}
|
||||||
|
text={intl.formatMessage({
|
||||||
|
id: "partnerSas.startPage.infoBox1.text",
|
||||||
|
defaultMessage:
|
||||||
|
"Collect new SAS EuroBonus points with your bookings.",
|
||||||
|
})}
|
||||||
|
theme={"SAS-Blue"}
|
||||||
|
icon={IconName.Diamond}
|
||||||
|
/>
|
||||||
|
<InfoBox
|
||||||
|
heading={intl.formatMessage({
|
||||||
|
id: "partnerSas.startPage.infoBox2.heading",
|
||||||
|
defaultMessage: "Pay with points",
|
||||||
|
})}
|
||||||
|
text={intl.formatMessage({
|
||||||
|
id: "partnerSas.startPage.infoBox2.text",
|
||||||
|
defaultMessage:
|
||||||
|
"Then use your points to pay for your next Scandic stay!",
|
||||||
|
})}
|
||||||
|
theme={"SAS-Blue"}
|
||||||
|
icon={IconName.CreditCard}
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<Image
|
<Image
|
||||||
alt="Hero Image"
|
alt="Hero Image"
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
.floatingBookingWidget {
|
.floatingBookingWidget {
|
||||||
width: var(--max-width-content);
|
width: var(--max-width-content);
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
min-height: 88px;
|
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
.floatingBackground {
|
.floatingBackground {
|
||||||
@@ -25,9 +24,3 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (min-width: 768px) and (max-width: 1366px) {
|
|
||||||
.floatingBookingWidget {
|
|
||||||
min-height: 150px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -16,10 +16,10 @@ export function FloatingBookingWidgetClient(props: Props) {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
observerRef.current = new IntersectionObserver(
|
observerRef.current = new IntersectionObserver(
|
||||||
([entry]) => {
|
([entry]) => {
|
||||||
const hasScrolledPastTop = entry.boundingClientRect.top < 0
|
const hasScrolledPastTop = entry.boundingClientRect.bottom < 0
|
||||||
setStickyTop(hasScrolledPastTop)
|
setStickyTop(hasScrolledPastTop)
|
||||||
},
|
},
|
||||||
{ threshold: 0, rootMargin: "0px 0px -100% 0px" }
|
{ threshold: 0, rootMargin: "0px 0px 0% 0px" }
|
||||||
)
|
)
|
||||||
|
|
||||||
if (containerRef.current) {
|
if (containerRef.current) {
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ interface SelectHotelProps {
|
|||||||
isBookingCodeRateAvailable?: boolean
|
isBookingCodeRateAvailable?: boolean
|
||||||
title: ReactNode
|
title: ReactNode
|
||||||
lang: Lang
|
lang: Lang
|
||||||
|
topSlot?: ReactNode
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SelectHotel({
|
export function SelectHotel({
|
||||||
@@ -42,6 +43,7 @@ export function SelectHotel({
|
|||||||
isBookingCodeRateAvailable = false,
|
isBookingCodeRateAvailable = false,
|
||||||
title,
|
title,
|
||||||
lang,
|
lang,
|
||||||
|
topSlot,
|
||||||
}: SelectHotelProps) {
|
}: SelectHotelProps) {
|
||||||
const isAllUnavailable = hotels.every(
|
const isAllUnavailable = hotels.every(
|
||||||
(hotel) => hotel.availability.status !== "Available"
|
(hotel) => hotel.availability.status !== "Available"
|
||||||
@@ -83,54 +85,60 @@ export function SelectHotel({
|
|||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<main className={styles.main}>
|
<main className={styles.main}>
|
||||||
{showBookingCodeFilter ? <BookingCodeFilter /> : null}
|
{topSlot && <div className={styles.topSlotContainer}>{topSlot}</div>}
|
||||||
<div className={styles.sideBar}>
|
<div className={styles.availabilityContainer}>
|
||||||
{hotels.length ? (
|
{showBookingCodeFilter ? <BookingCodeFilter /> : null}
|
||||||
<Link
|
<div className={styles.sideBar}>
|
||||||
className={styles.link}
|
{hotels.length ? (
|
||||||
href={
|
<Link
|
||||||
isAlternative
|
className={styles.link}
|
||||||
? alternativeHotelsMap(lang)
|
href={
|
||||||
: selectHotelMap(lang)
|
isAlternative
|
||||||
}
|
? alternativeHotelsMap(lang)
|
||||||
keepSearchParams
|
: selectHotelMap(lang)
|
||||||
>
|
}
|
||||||
<MapWithButtonWrapper>
|
keepSearchParams
|
||||||
|
>
|
||||||
|
<MapWithButtonWrapper>
|
||||||
|
<StaticMap
|
||||||
|
city={city.name}
|
||||||
|
country={isCityWithCountry(city) ? city.country : undefined}
|
||||||
|
width={340}
|
||||||
|
height={200}
|
||||||
|
zoomLevel={11}
|
||||||
|
mapType="roadmap"
|
||||||
|
altText={`Map of ${city.name} city center`}
|
||||||
|
/>
|
||||||
|
</MapWithButtonWrapper>
|
||||||
|
</Link>
|
||||||
|
) : (
|
||||||
|
<div className={styles.mapContainer}>
|
||||||
<StaticMap
|
<StaticMap
|
||||||
city={city.name}
|
city={city.name}
|
||||||
country={isCityWithCountry(city) ? city.country : undefined}
|
|
||||||
width={340}
|
width={340}
|
||||||
height={200}
|
height={200}
|
||||||
zoomLevel={11}
|
zoomLevel={11}
|
||||||
mapType="roadmap"
|
mapType="roadmap"
|
||||||
altText={`Map of ${city.name} city center`}
|
altText={`Map of ${city.name} city center`}
|
||||||
/>
|
/>
|
||||||
</MapWithButtonWrapper>
|
</div>
|
||||||
</Link>
|
)}
|
||||||
) : (
|
<HotelFilter filters={filterList} className={styles.filter} />
|
||||||
<div className={styles.mapContainer}>
|
</div>
|
||||||
<StaticMap
|
<div className={styles.hotelList}>
|
||||||
city={city.name}
|
<NoAvailabilityAlert
|
||||||
width={340}
|
hotelsLength={hotels.length}
|
||||||
height={200}
|
isAlternative={isAlternative}
|
||||||
zoomLevel={11}
|
isAllUnavailable={isAllUnavailable}
|
||||||
mapType="roadmap"
|
operaId={hotels?.[0]?.hotel.operaId}
|
||||||
altText={`Map of ${city.name} city center`}
|
bookingCode={bookingCode}
|
||||||
/>
|
isBookingCodeRateNotAvailable={!isBookingCodeRateAvailable}
|
||||||
</div>
|
/>
|
||||||
)}
|
<HotelCardListing
|
||||||
<HotelFilter filters={filterList} className={styles.filter} />
|
hotelData={hotels}
|
||||||
</div>
|
isAlternative={isAlternative}
|
||||||
<div className={styles.hotelList}>
|
/>
|
||||||
<NoAvailabilityAlert
|
</div>
|
||||||
hotelsLength={hotels.length}
|
|
||||||
isAlternative={isAlternative}
|
|
||||||
isAllUnavailable={isAllUnavailable}
|
|
||||||
operaId={hotels?.[0]?.hotel.operaId}
|
|
||||||
bookingCode={bookingCode}
|
|
||||||
isBookingCodeRateNotAvailable={!isBookingCodeRateAvailable}
|
|
||||||
/>
|
|
||||||
<HotelCardListing hotelData={hotels} isAlternative={isAlternative} />
|
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -1,14 +1,38 @@
|
|||||||
.main {
|
.main {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--Space-x5);
|
||||||
|
justify-items: center;
|
||||||
|
padding-top: var(--Space-x4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.topSlotContainer {
|
||||||
|
width: var(--max-width-page);
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.availabilityContainer {
|
||||||
display: flex;
|
display: flex;
|
||||||
background-color: var(--Scandic-Brand-Warm-White);
|
background-color: var(--Scandic-Brand-Warm-White);
|
||||||
min-height: min(100dvh, 750px);
|
min-height: min(100dvh, 750px);
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
max-width: var(--max-width-page);
|
width: var(--max-width-page);
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
|
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
flex-direction: row;
|
||||||
|
gap: var(--Space-x5);
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.header {
|
.header {
|
||||||
padding: var(--Space-x3) 0 var(--Space-x2);
|
padding: var(--Space-x3) 0 var(--Space-x2);
|
||||||
|
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
background-color: var(--Base-Surface-Subtle-Normal);
|
||||||
|
padding: var(--Space-x4) 0 var(--Space-x3);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.headerContent {
|
.headerContent {
|
||||||
@@ -17,6 +41,9 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: var(--Space-x2);
|
gap: var(--Space-x2);
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.cityInformation {
|
.cityInformation {
|
||||||
@@ -28,19 +55,37 @@
|
|||||||
|
|
||||||
.sorter {
|
.sorter {
|
||||||
display: none;
|
display: none;
|
||||||
|
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
display: block;
|
||||||
|
width: 339px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.sideBar {
|
.sideBar {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
max-width: 340px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.sideBarItem {
|
.sideBarItem {
|
||||||
display: none;
|
display: none;
|
||||||
|
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.link {
|
.link {
|
||||||
display: none;
|
display: none;
|
||||||
|
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
display: flex;
|
||||||
|
margin-bottom: var(--Space-x6);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.hotelList {
|
.hotelList {
|
||||||
@@ -56,59 +101,26 @@
|
|||||||
|
|
||||||
.skeletonContainer .title {
|
.skeletonContainer .title {
|
||||||
margin-bottom: var(--Space-x3);
|
margin-bottom: var(--Space-x3);
|
||||||
|
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
.title {
|
||||||
@media (min-width: 768px) {
|
@media (min-width: 768px) {
|
||||||
.main {
|
|
||||||
padding: var(--Space-x5) 0;
|
|
||||||
flex-direction: row;
|
|
||||||
gap: var(--Space-x5);
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.headerContent {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header {
|
|
||||||
background-color: var(--Base-Surface-Subtle-Normal);
|
|
||||||
padding: var(--Space-x4) 0 var(--Space-x3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.sorter {
|
|
||||||
display: block;
|
|
||||||
width: 339px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.title {
|
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
display: flex;
|
display: flex;
|
||||||
max-width: var(--max-width-navigation);
|
max-width: var(--max-width-navigation);
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.sideBar {
|
@media (min-width: 768px) {
|
||||||
max-width: 340px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sideBarItem {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.filter {
|
.filter {
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
.link {
|
|
||||||
display: flex;
|
|
||||||
margin-bottom: var(--Space-x6);
|
|
||||||
}
|
|
||||||
|
|
||||||
.skeletonContainer .title {
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.skeletonContainer .sideBar {
|
.skeletonContainer .sideBar {
|
||||||
gap: var(--Space-x3);
|
gap: var(--Space-x3);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import { getSelectHotelTracking } from "../misc/selectHotelTracking"
|
|||||||
import { parseSelectHotelSearchParams } from "../utils/url"
|
import { parseSelectHotelSearchParams } from "../utils/url"
|
||||||
|
|
||||||
import type { Lang } from "@scandic-hotels/common/constants/language"
|
import type { Lang } from "@scandic-hotels/common/constants/language"
|
||||||
|
import type { ReactNode } from "react"
|
||||||
|
|
||||||
import type { NextSearchParams } from "../types"
|
import type { NextSearchParams } from "../types"
|
||||||
|
|
||||||
@@ -23,10 +24,12 @@ export async function SelectHotelPage({
|
|||||||
lang,
|
lang,
|
||||||
searchParams,
|
searchParams,
|
||||||
config,
|
config,
|
||||||
|
topSlot,
|
||||||
}: {
|
}: {
|
||||||
lang: Lang
|
lang: Lang
|
||||||
searchParams: NextSearchParams
|
searchParams: NextSearchParams
|
||||||
config: BookingFlowConfig
|
config: BookingFlowConfig
|
||||||
|
topSlot?: ReactNode
|
||||||
}) {
|
}) {
|
||||||
const booking = parseSelectHotelSearchParams(searchParams)
|
const booking = parseSelectHotelSearchParams(searchParams)
|
||||||
|
|
||||||
@@ -111,6 +114,7 @@ export async function SelectHotelPage({
|
|||||||
hotels={hotels}
|
hotels={hotels}
|
||||||
title={city.name}
|
title={city.name}
|
||||||
lang={lang}
|
lang={lang}
|
||||||
|
topSlot={topSlot}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<TrackingSDK hotelInfo={hotelsTrackingData} pageData={pageTrackingData} />
|
<TrackingSDK hotelInfo={hotelsTrackingData} pageData={pageTrackingData} />
|
||||||
|
|||||||
@@ -0,0 +1,74 @@
|
|||||||
|
import type { Meta, StoryObj } from '@storybook/nextjs-vite'
|
||||||
|
import { Alert } from './index'
|
||||||
|
import { AlertTypeEnum } from '@scandic-hotels/common/constants/alert'
|
||||||
|
import { expect, fn } from 'storybook/test'
|
||||||
|
|
||||||
|
const meta: Meta<typeof Alert> = {
|
||||||
|
title: 'Components/Alert',
|
||||||
|
component: Alert,
|
||||||
|
parameters: {
|
||||||
|
layout: 'centered',
|
||||||
|
},
|
||||||
|
tags: ['autodocs'],
|
||||||
|
argTypes: {
|
||||||
|
variant: {
|
||||||
|
control: { type: 'select' },
|
||||||
|
options: ['banner', 'inline'],
|
||||||
|
},
|
||||||
|
type: {
|
||||||
|
control: { type: 'select' },
|
||||||
|
options: Object.values(AlertTypeEnum),
|
||||||
|
},
|
||||||
|
close: {
|
||||||
|
table: {
|
||||||
|
disable: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export default meta
|
||||||
|
type Story = StoryObj<typeof meta>
|
||||||
|
|
||||||
|
export const Default: Story = {
|
||||||
|
args: {
|
||||||
|
variant: 'inline',
|
||||||
|
type: AlertTypeEnum.Info,
|
||||||
|
heading: 'Heading',
|
||||||
|
text: 'Caramels danish jelly-o pudding tart croissant. Pie cotton candy jujubes carrot cake gummies. Apple pie cake chocolate bar halvah tootsie roll bonbon cheesecake. Brownie dessert macaroon bear claw pastry.',
|
||||||
|
close: undefined,
|
||||||
|
ariaRole: 'alert',
|
||||||
|
},
|
||||||
|
play: async ({ canvas }) => {
|
||||||
|
canvas.findByRole('alert')
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Closable: Story = {
|
||||||
|
args: {
|
||||||
|
variant: 'inline',
|
||||||
|
type: AlertTypeEnum.Info,
|
||||||
|
heading: 'Heading',
|
||||||
|
text: 'Caramels danish jelly-o pudding tart croissant. Pie cotton candy jujubes carrot cake gummies. Apple pie cake chocolate bar halvah tootsie roll bonbon cheesecake. Brownie dessert macaroon bear claw pastry.',
|
||||||
|
close: fn(),
|
||||||
|
},
|
||||||
|
play: async ({ canvas, userEvent, args }) => {
|
||||||
|
await userEvent.click(await canvas.findByRole('button'))
|
||||||
|
expect(args.close).toHaveBeenCalledTimes(1)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const WithPhonenumber: Story = {
|
||||||
|
args: {
|
||||||
|
variant: 'inline',
|
||||||
|
type: AlertTypeEnum.Info,
|
||||||
|
heading: 'Heading',
|
||||||
|
text: 'Caramels danish jelly-o pudding tart croissant. Pie cotton candy jujubes carrot cake gummies. Apple pie cake chocolate bar halvah tootsie roll bonbon cheesecake. Brownie dessert macaroon bear claw pastry.',
|
||||||
|
close: fn(),
|
||||||
|
phoneContact: {
|
||||||
|
displayText: 'Call us:',
|
||||||
|
phoneNumber: '+4685551234',
|
||||||
|
footnote: 'Available 24/7',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -192,6 +192,8 @@ export function IconByIconName({
|
|||||||
return <MaterialIcon icon="concierge" {...props} />
|
return <MaterialIcon icon="concierge" {...props} />
|
||||||
case IconName.Cultural:
|
case IconName.Cultural:
|
||||||
return <MaterialIcon icon="theater_comedy" {...props} />
|
return <MaterialIcon icon="theater_comedy" {...props} />
|
||||||
|
case IconName.CreditCard:
|
||||||
|
return <MaterialIcon icon="credit_card" {...props} />
|
||||||
case IconName.Diamond:
|
case IconName.Diamond:
|
||||||
return <MaterialIcon icon="diamond" {...props} />
|
return <MaterialIcon icon="diamond" {...props} />
|
||||||
case IconName.Directions:
|
case IconName.Directions:
|
||||||
|
|||||||
@@ -7,9 +7,9 @@ export enum IconName {
|
|||||||
AirConditioningInRoom = 'AirConditioningInRoom',
|
AirConditioningInRoom = 'AirConditioningInRoom',
|
||||||
Airplane = 'Airplane',
|
Airplane = 'Airplane',
|
||||||
ArmChair = 'ArmChair',
|
ArmChair = 'ArmChair',
|
||||||
|
ArrowFrom = 'ArrowFrom',
|
||||||
ArrowLeft = 'ArrowLeft',
|
ArrowLeft = 'ArrowLeft',
|
||||||
ArrowLeftSmall = 'ArrowLeftSmall',
|
ArrowLeftSmall = 'ArrowLeftSmall',
|
||||||
ArrowFrom = 'ArrowFrom',
|
|
||||||
ArrowRight = 'ArrowRight',
|
ArrowRight = 'ArrowRight',
|
||||||
ArrowRightSmall = 'ArrowRightSmall',
|
ArrowRightSmall = 'ArrowRightSmall',
|
||||||
ArrowTo = 'ArrowTo',
|
ArrowTo = 'ArrowTo',
|
||||||
@@ -22,6 +22,7 @@ export enum IconName {
|
|||||||
Bike = 'Bike',
|
Bike = 'Bike',
|
||||||
Bouquet = 'Bouquet',
|
Bouquet = 'Bouquet',
|
||||||
Bowling = 'Bowling',
|
Bowling = 'Bowling',
|
||||||
|
Breakfast = 'Breakfast',
|
||||||
Business = 'Business',
|
Business = 'Business',
|
||||||
BusinessCentre = 'BusinessCentre',
|
BusinessCentre = 'BusinessCentre',
|
||||||
Calendar = 'Calendar',
|
Calendar = 'Calendar',
|
||||||
@@ -51,6 +52,7 @@ export enum IconName {
|
|||||||
ConferenceRoom = 'ConferenceRoom',
|
ConferenceRoom = 'ConferenceRoom',
|
||||||
ConvenienceStore24h = 'ConvenienceStore24h',
|
ConvenienceStore24h = 'ConvenienceStore24h',
|
||||||
ConventionCentre = 'ConventionCentre',
|
ConventionCentre = 'ConventionCentre',
|
||||||
|
CreditCard = 'CreditCard',
|
||||||
CroissantCoffeeEgg = 'CroissantCoffeeEgg',
|
CroissantCoffeeEgg = 'CroissantCoffeeEgg',
|
||||||
CrossCircle = 'CrossCircle',
|
CrossCircle = 'CrossCircle',
|
||||||
CrossCircleOutline = 'CrossCircleOutline',
|
CrossCircleOutline = 'CrossCircleOutline',
|
||||||
@@ -92,8 +94,8 @@ export enum IconName {
|
|||||||
HandKey = 'HandKey',
|
HandKey = 'HandKey',
|
||||||
Hanger = 'Hanger',
|
Hanger = 'Hanger',
|
||||||
HangerAlt = 'HangerAlt',
|
HangerAlt = 'HangerAlt',
|
||||||
Heat = 'Heat',
|
|
||||||
Heart = 'Heart',
|
Heart = 'Heart',
|
||||||
|
Heat = 'Heat',
|
||||||
Hiking = 'Hiking',
|
Hiking = 'Hiking',
|
||||||
HotelNight = 'HotelNight',
|
HotelNight = 'HotelNight',
|
||||||
House = 'House',
|
House = 'House',
|
||||||
@@ -108,15 +110,15 @@ export enum IconName {
|
|||||||
Kettle = 'Kettle',
|
Kettle = 'Kettle',
|
||||||
Kids = 'Kids',
|
Kids = 'Kids',
|
||||||
KidsMocktail = 'KidsMocktail',
|
KidsMocktail = 'KidsMocktail',
|
||||||
Landscape = 'Landscape',
|
Kitchen = 'Kitchen',
|
||||||
Lamp = 'Lamp',
|
Lamp = 'Lamp',
|
||||||
|
Landscape = 'Landscape',
|
||||||
LaptopSafe = 'LaptopSafe',
|
LaptopSafe = 'LaptopSafe',
|
||||||
LaundryMachine = 'LaundryMachine',
|
LaundryMachine = 'LaundryMachine',
|
||||||
Link = 'Link',
|
Link = 'Link',
|
||||||
LocalBar = 'LocalBar',
|
LocalBar = 'LocalBar',
|
||||||
Location = 'Location',
|
Location = 'Location',
|
||||||
Lock = 'Lock',
|
Lock = 'Lock',
|
||||||
Breakfast = 'Breakfast',
|
|
||||||
Luggage = 'Luggage',
|
Luggage = 'Luggage',
|
||||||
LuggageLockers = 'LuggageLockers',
|
LuggageLockers = 'LuggageLockers',
|
||||||
MagicWand = 'MagicWand',
|
MagicWand = 'MagicWand',
|
||||||
@@ -163,11 +165,11 @@ export enum IconName {
|
|||||||
StarFilled = 'StarFilled',
|
StarFilled = 'StarFilled',
|
||||||
Street = 'Street',
|
Street = 'Street',
|
||||||
Swim = 'Swim',
|
Swim = 'Swim',
|
||||||
Theatre = 'Theatre',
|
|
||||||
Swipe = 'Swipe',
|
Swipe = 'Swipe',
|
||||||
|
Theatre = 'Theatre',
|
||||||
Thermostat = 'Thermostat',
|
Thermostat = 'Thermostat',
|
||||||
Toilet = 'Toilet',
|
|
||||||
Ticket = 'Ticket',
|
Ticket = 'Ticket',
|
||||||
|
Toilet = 'Toilet',
|
||||||
Train = 'Train',
|
Train = 'Train',
|
||||||
Tripadvisor = 'Tripadvisor',
|
Tripadvisor = 'Tripadvisor',
|
||||||
Trophy = 'Trophy',
|
Trophy = 'Trophy',
|
||||||
@@ -179,5 +181,4 @@ export enum IconName {
|
|||||||
WarningTriangle = 'WarningTriangle',
|
WarningTriangle = 'WarningTriangle',
|
||||||
Wheelchair = 'Wheelchair',
|
Wheelchair = 'Wheelchair',
|
||||||
Wifi = 'Wifi',
|
Wifi = 'Wifi',
|
||||||
Kitchen = 'Kitchen',
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
.infoBox {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: auto 1fr;
|
||||||
|
gap: 1rem;
|
||||||
|
|
||||||
|
padding: var(--Space-x2) var(--Space-x3);
|
||||||
|
border-radius: var(--Corner-radius-Medium);
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
background: var(--Background-Secondary);
|
||||||
|
|
||||||
|
&.sasBlue {
|
||||||
|
background: var(--SAS-Blue-90);
|
||||||
|
}
|
||||||
|
|
||||||
|
.iconContainer {
|
||||||
|
display: flex;
|
||||||
|
width: 24px;
|
||||||
|
padding: var(--Space-x05);
|
||||||
|
border-radius: 100%;
|
||||||
|
aspect-ratio: 1 / 1;
|
||||||
|
box-sizing: content-box;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: white;
|
||||||
|
|
||||||
|
background: var(--Surface-Brand-Accent-OnAccent-Accent);
|
||||||
|
|
||||||
|
&.sasBlue {
|
||||||
|
color: white;
|
||||||
|
background: var(--SAS-Blue-Default);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--Space-x05);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
import type { Meta, StoryObj } from '@storybook/nextjs-vite'
|
||||||
|
import { expect } from 'storybook/test'
|
||||||
|
import { InfoBox, Props } from './InfoBox'
|
||||||
|
import { IconName } from '../Icons/iconName'
|
||||||
|
|
||||||
|
const meta: Meta<typeof InfoBox> = {
|
||||||
|
title: 'Components/InfoBox',
|
||||||
|
component: InfoBox,
|
||||||
|
parameters: {
|
||||||
|
layout: 'padded',
|
||||||
|
},
|
||||||
|
tags: ['autodocs'],
|
||||||
|
}
|
||||||
|
|
||||||
|
export default meta
|
||||||
|
type Story = StoryObj<typeof meta>
|
||||||
|
|
||||||
|
export const Default: Story = {
|
||||||
|
args: {
|
||||||
|
theme: 'Default',
|
||||||
|
icon: IconName.Accessibility,
|
||||||
|
heading: 'Heading',
|
||||||
|
text: 'This is an informational message',
|
||||||
|
},
|
||||||
|
argTypes: {
|
||||||
|
icon: {
|
||||||
|
control: { type: 'select' },
|
||||||
|
options: Object.values(IconName),
|
||||||
|
},
|
||||||
|
theme: {
|
||||||
|
control: { type: 'select' },
|
||||||
|
options: ['Default', 'SAS-Blue'] satisfies Props['theme'][],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
play: async ({ canvas, args }) => {
|
||||||
|
const article = await canvas.findByRole('article')
|
||||||
|
await expect(article).toBeVisible()
|
||||||
|
|
||||||
|
const heading = await canvas.findByRole('heading', { name: args.heading })
|
||||||
|
await expect(heading).toBeVisible()
|
||||||
|
|
||||||
|
const paragraph = await canvas.findByText(args.text)
|
||||||
|
await expect(paragraph).toBeVisible()
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const WithoutIcon: Story = {
|
||||||
|
args: {
|
||||||
|
heading: 'Heading',
|
||||||
|
text: 'This is an informational message',
|
||||||
|
},
|
||||||
|
}
|
||||||
57
packages/design-system/lib/components/InfoBox/InfoBox.tsx
Normal file
57
packages/design-system/lib/components/InfoBox/InfoBox.tsx
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
import { cva } from 'class-variance-authority'
|
||||||
|
import { IconByIconName } from '../Icons/IconByIconName'
|
||||||
|
import { IconName } from '../Icons/iconName'
|
||||||
|
import { Typography } from '../Typography'
|
||||||
|
import styles from './InfoBox.module.css'
|
||||||
|
import type { VariantProps } from 'class-variance-authority'
|
||||||
|
|
||||||
|
const infoBoxVariants = cva(styles.infoBox, {
|
||||||
|
variants: {
|
||||||
|
theme: {
|
||||||
|
'SAS-Blue': styles.sasBlue,
|
||||||
|
Default: styles.default,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
theme: 'Default',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const iconVariants = cva(styles.iconContainer, {
|
||||||
|
variants: {
|
||||||
|
theme: {
|
||||||
|
'SAS-Blue': styles.sasBlue,
|
||||||
|
Default: styles.default,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
theme: 'Default',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
export type Props = {
|
||||||
|
heading: string
|
||||||
|
text: string
|
||||||
|
theme?: VariantProps<typeof infoBoxVariants>['theme']
|
||||||
|
icon?: IconName
|
||||||
|
}
|
||||||
|
|
||||||
|
export function InfoBox({ heading, text, theme, icon }: Props) {
|
||||||
|
return (
|
||||||
|
<article className={infoBoxVariants({ theme })}>
|
||||||
|
{icon && (
|
||||||
|
<div className={iconVariants({ theme })}>
|
||||||
|
<IconByIconName iconName={icon} color={'CurrentColor'} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className={styles.content}>
|
||||||
|
<Typography variant="Body/Paragraph/mdBold">
|
||||||
|
<h2>{heading}</h2>
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="Body/Supporting text (caption)/smRegular">
|
||||||
|
<p>{text}</p>
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -137,6 +137,7 @@
|
|||||||
"./ImageContainer": "./lib/components/ImageContainer/index.tsx",
|
"./ImageContainer": "./lib/components/ImageContainer/index.tsx",
|
||||||
"./ImageFallback": "./lib/components/ImageFallback/index.tsx",
|
"./ImageFallback": "./lib/components/ImageFallback/index.tsx",
|
||||||
"./ImageGallery": "./lib/components/ImageGallery/index.tsx",
|
"./ImageGallery": "./lib/components/ImageGallery/index.tsx",
|
||||||
|
"./InfoBox": "./lib/components/InfoBox/InfoBox.tsx",
|
||||||
"./InfoCard": "./lib/components/InfoCard/index.tsx",
|
"./InfoCard": "./lib/components/InfoCard/index.tsx",
|
||||||
"./Input": "./lib/components/Input/index.tsx",
|
"./Input": "./lib/components/Input/index.tsx",
|
||||||
"./JsonToHtml": "./lib/components/JsonToHtml/JsonToHtml.tsx",
|
"./JsonToHtml": "./lib/components/JsonToHtml/JsonToHtml.tsx",
|
||||||
|
|||||||
Reference in New Issue
Block a user