Feat/BOOK-63 hotel subpages branding
* feat(BOOK-63): Replaced css variables and components to apply hotel branding on subpages * feat(BOOK-63): Replaced css variables and components to apply hotel branding on hotel page map view Approved-by: Christel Westerberg Approved-by: Matilda Landström
This commit is contained in:
@@ -64,7 +64,8 @@
|
||||
"./utils/promiseWithTimeout": "./utils/promiseWithTimeout.ts",
|
||||
"./utils/rangeArray": "./utils/rangeArray.ts",
|
||||
"./utils/safeTry": "./utils/safeTry.ts",
|
||||
"./utils/theme": "./utils/theme.ts",
|
||||
"./utils/theme": "./utils/theme/index.ts",
|
||||
"./utils/theme/serverContext": "./utils/theme/serverContext.ts",
|
||||
"./utils/toCapitalCase": "./utils/toCapitalCase.ts",
|
||||
"./utils/url": "./utils/url.ts",
|
||||
"./utils/zod/*": "./utils/zod/*.ts"
|
||||
|
||||
31
packages/common/utils/theme/serverContext.ts
Normal file
31
packages/common/utils/theme/serverContext.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import "server-only"
|
||||
|
||||
import { cache } from "react"
|
||||
|
||||
import { DEFAULT_THEME, type Theme } from "."
|
||||
|
||||
const getRef = cache(() => ({ current: DEFAULT_THEME as Theme }))
|
||||
|
||||
/**
|
||||
* Set the global theme
|
||||
*
|
||||
* It works kind of like React's context,
|
||||
* but on the server side, per request.
|
||||
*
|
||||
* @param newTheme
|
||||
*/
|
||||
export function setTheme(newTheme: Theme) {
|
||||
getRef().current = newTheme
|
||||
|
||||
return newTheme
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the global theme
|
||||
*
|
||||
* Note: This must be called after setTheme() has been called in the page/layout.
|
||||
* If called before setTheme(), it will return DEFAULT_THEME.
|
||||
*/
|
||||
export function getTheme(): Theme {
|
||||
return getRef().current ?? DEFAULT_THEME
|
||||
}
|
||||
@@ -27,12 +27,12 @@
|
||||
}
|
||||
|
||||
.Outlined:active {
|
||||
border-color: var(--Border-Interactive-Selected);
|
||||
border-color: var(--Border-Interactive-Active);
|
||||
}
|
||||
|
||||
.FilterRounded {
|
||||
background-color: transparent;
|
||||
border: 1px solid var(--Border-Interactive-Selected);
|
||||
border: 1px solid var(--Border-Interactive-Active);
|
||||
border-radius: var(--Corner-radius-rounded);
|
||||
padding: var(--Space-x025) var(--Space-x2);
|
||||
color: var(--Text-Default);
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
}
|
||||
|
||||
.label:has(:checked) {
|
||||
border: 2px solid var(--Border-Interactive-Selected);
|
||||
border: 2px solid var(--Border-Interactive-Active);
|
||||
}
|
||||
|
||||
.label:not(:has(:checked)) .selectedIcon {
|
||||
|
||||
@@ -4,7 +4,7 @@ import NextImage, { ImageProps as NextImageProps } from 'next/image'
|
||||
|
||||
import ImageFallback from '../ImageFallback'
|
||||
|
||||
import type { CSSProperties } from 'react'
|
||||
import { useState, type CSSProperties, type SyntheticEvent } from 'react'
|
||||
import { imageLoader } from './imageLoader'
|
||||
|
||||
type FocalPoint = {
|
||||
@@ -22,8 +22,11 @@ export default function Image({
|
||||
focalPoint,
|
||||
dimensions,
|
||||
style,
|
||||
src,
|
||||
onError,
|
||||
...props
|
||||
}: ImageProps) {
|
||||
const [imageError, setImageError] = useState(false)
|
||||
const styles: CSSProperties = focalPoint
|
||||
? {
|
||||
objectFit: 'cover',
|
||||
@@ -32,14 +35,24 @@ export default function Image({
|
||||
}
|
||||
: { ...style }
|
||||
|
||||
if (!props.src) {
|
||||
function handleError(error: SyntheticEvent<HTMLImageElement, Event>) {
|
||||
if (onError) {
|
||||
onError(error)
|
||||
} else {
|
||||
setImageError(true)
|
||||
}
|
||||
}
|
||||
|
||||
if (!src || imageError) {
|
||||
return <ImageFallback />
|
||||
}
|
||||
|
||||
return (
|
||||
<NextImage
|
||||
{...props}
|
||||
src={src}
|
||||
style={styles}
|
||||
onError={handleError}
|
||||
loader={imageLoader({ dimensions, focalPoint })}
|
||||
/>
|
||||
)
|
||||
|
||||
@@ -87,19 +87,26 @@
|
||||
.li:has(.heart)::before,
|
||||
.check > .li::before,
|
||||
.li:has(.check)::before {
|
||||
content: '';
|
||||
position: relative;
|
||||
height: 8px;
|
||||
top: 3px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
display: inline-flex;
|
||||
flex-shrink: 0;
|
||||
background-color: var(--Icon-Accent);
|
||||
mask-size: contain;
|
||||
mask-repeat: no-repeat;
|
||||
}
|
||||
|
||||
.check > .li::before,
|
||||
.li:has(.check)::before {
|
||||
content: url('/_static/icons/check-ring.svg');
|
||||
mask-image: url('/_static/icons/check_circle.svg');
|
||||
}
|
||||
|
||||
.heart > .li::before,
|
||||
.li:has(.heart)::before {
|
||||
content: url('/_static/icons/heart.svg');
|
||||
mask-image: url('/_static/icons/heart.svg');
|
||||
}
|
||||
|
||||
.li > * {
|
||||
|
||||
@@ -4,77 +4,63 @@
|
||||
height: 100%;
|
||||
position: relative;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.mapContainer :global(.gm-style .gm-style-iw-d) {
|
||||
padding: 0 !important;
|
||||
overflow: hidden !important;
|
||||
max-height: none !important;
|
||||
max-width: none !important;
|
||||
}
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
background: linear-gradient(
|
||||
43deg,
|
||||
rgba(172, 172, 172, 0) 57.66%,
|
||||
rgba(0, 0, 0, 0.25) 92.45%
|
||||
);
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.mapContainer :global(.gm-style .gm-style-iw-c) {
|
||||
padding: 0 !important;
|
||||
overflow: hidden !important;
|
||||
max-height: none !important;
|
||||
max-width: none !important;
|
||||
}
|
||||
|
||||
.mapContainer::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
background: linear-gradient(
|
||||
43deg,
|
||||
rgba(172, 172, 172, 0) 57.66%,
|
||||
rgba(0, 0, 0, 0.25) 92.45%
|
||||
);
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
:global(.gm-style .gm-style-iw-d),
|
||||
:global(.gm-style .gm-style-iw-c) {
|
||||
padding: 0 !important;
|
||||
overflow: hidden !important;
|
||||
max-height: none !important;
|
||||
max-width: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
.ctaButtons {
|
||||
position: absolute;
|
||||
top: var(--Spacing-x2);
|
||||
right: var(--Spacing-x2);
|
||||
z-index: 1;
|
||||
top: var(--Space-x2);
|
||||
right: var(--Space-x2);
|
||||
display: flex;
|
||||
gap: var(--Space-x7);
|
||||
flex-direction: column;
|
||||
gap: var(--Spacing-x7);
|
||||
align-items: flex-end;
|
||||
pointer-events: none;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.zoomButtons {
|
||||
display: grid;
|
||||
gap: var(--Spacing-x2);
|
||||
}
|
||||
|
||||
.closeButton {
|
||||
pointer-events: initial;
|
||||
box-shadow: var(--button-box-shadow);
|
||||
gap: var(--Spacing-x-half);
|
||||
display: flex;
|
||||
gap: var(--Space-x2);
|
||||
}
|
||||
|
||||
.zoomButton {
|
||||
width: var(--Space-x5);
|
||||
height: var(--Space-x5);
|
||||
padding: 0;
|
||||
pointer-events: initial;
|
||||
box-shadow: var(--button-box-shadow);
|
||||
}
|
||||
|
||||
@media screen and (max-width: 767px) {
|
||||
.zoomButtons {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
.ctaButtons {
|
||||
top: var(--Spacing-x4);
|
||||
right: var(--Spacing-x5);
|
||||
bottom: var(--Spacing-x7);
|
||||
top: var(--Space-x4);
|
||||
right: var(--Space-x5);
|
||||
bottom: var(--Space-x7);
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.zoomButtons {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,19 +1,14 @@
|
||||
import { IconByIconName } from '../../../Icons/IconByIconName'
|
||||
|
||||
import { getIconByPoiGroupAndCategory } from '../utils'
|
||||
import {
|
||||
getIconByPoiGroupAndCategory,
|
||||
type PointOfInterestGroup,
|
||||
} from '../utils'
|
||||
import { poiVariants } from './variants'
|
||||
|
||||
import { VariantProps } from 'class-variance-authority'
|
||||
import type { VariantProps } from 'class-variance-authority'
|
||||
|
||||
export type PointOfInterestGroup =
|
||||
| 'Public transport'
|
||||
| 'Attractions'
|
||||
| 'Business'
|
||||
| 'Location'
|
||||
| 'Parking'
|
||||
| 'Shopping & Dining'
|
||||
|
||||
export interface PoiMarkerProps extends VariantProps<typeof poiVariants> {
|
||||
interface PoiMarkerProps extends VariantProps<typeof poiVariants> {
|
||||
group: PointOfInterestGroup
|
||||
categoryName?: string
|
||||
className?: string
|
||||
@@ -33,7 +28,7 @@ export function PoiMarker({
|
||||
<span className={classNames}>
|
||||
<IconByIconName
|
||||
iconName={iconName}
|
||||
color={skipBackground ? 'Icon/Feedback/Neutral' : 'Icon/Inverted'}
|
||||
color={skipBackground ? 'CurrentColor' : 'Icon/Inverted'}
|
||||
size={size === 'small' ? 16 : size === 'large' ? 24 : 20}
|
||||
/>
|
||||
</span>
|
||||
|
||||
@@ -1,9 +1,37 @@
|
||||
.icon {
|
||||
.poiMarker {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border-radius: var(--Corner-radius-rounded);
|
||||
background-color: var(--Surface-UI-Fill-Default);
|
||||
background-color: var(--Surface-Feedback-Neutral);
|
||||
|
||||
> span {
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
&.skipBackground {
|
||||
background-color: transparent;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.shoppingDining {
|
||||
background-color: var(--Surface-Accent-1);
|
||||
}
|
||||
.publicTransport {
|
||||
background-color: var(--Surface-Accent-2);
|
||||
}
|
||||
.attractions {
|
||||
background-color: var(--Surface-Accent-3);
|
||||
}
|
||||
.business {
|
||||
background-color: var(--Surface-Accent-4);
|
||||
}
|
||||
.parking {
|
||||
background-color: var(--Surface-Accent-5);
|
||||
}
|
||||
.location {
|
||||
background-color: var(--Surface-Feedback-Neutral);
|
||||
}
|
||||
|
||||
.small {
|
||||
@@ -14,27 +42,3 @@
|
||||
width: var(--Space-x4);
|
||||
height: var(--Space-x4);
|
||||
}
|
||||
|
||||
.attractions {
|
||||
background-color: var(--Surface-Accent-3);
|
||||
}
|
||||
.business {
|
||||
background-color: var(--Surface-Accent-4);
|
||||
}
|
||||
.location {
|
||||
background-color: var(--Surface-Feedback-Neutral);
|
||||
}
|
||||
.parking {
|
||||
background-color: var(--Surface-Accent-5);
|
||||
}
|
||||
.publicTransport {
|
||||
background-color: var(--Surface-Accent-2);
|
||||
}
|
||||
.shoppingDining {
|
||||
background-color: var(--Surface-Accent-1);
|
||||
}
|
||||
|
||||
.icon.transparent {
|
||||
background-color: transparent;
|
||||
padding: 0;
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
import { cva } from 'class-variance-authority'
|
||||
|
||||
import styles from './poi.module.css'
|
||||
import { PointOfInterestGroup } from '.'
|
||||
import type { PointOfInterestGroup } from '../utils'
|
||||
import styles from './poiMarker.module.css'
|
||||
|
||||
export const poiVariants = cva(styles.icon, {
|
||||
export const poiVariants = cva(styles.poiMarker, {
|
||||
variants: {
|
||||
group: {
|
||||
['Attractions']: styles.attractions,
|
||||
@@ -14,7 +14,7 @@ export const poiVariants = cva(styles.icon, {
|
||||
['Shopping & Dining']: styles.shoppingDining,
|
||||
} satisfies Record<PointOfInterestGroup, string>,
|
||||
skipBackground: {
|
||||
true: styles.transparent,
|
||||
true: styles.skipBackground,
|
||||
false: '',
|
||||
},
|
||||
size: {
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
import { IconName } from '../../Icons/iconName'
|
||||
import { PointOfInterestGroup } from './PoiMarker'
|
||||
|
||||
export type PointOfInterestGroup =
|
||||
| 'Public transport'
|
||||
| 'Attractions'
|
||||
| 'Business'
|
||||
| 'Location'
|
||||
| 'Parking'
|
||||
| 'Shopping & Dining'
|
||||
|
||||
export function getIconByPoiGroupAndCategory(
|
||||
group: PointOfInterestGroup,
|
||||
|
||||
@@ -37,9 +37,9 @@ export default function ParkingList({
|
||||
|
||||
return (
|
||||
<Typography variant="Body/Paragraph/mdRegular">
|
||||
<ul className={styles.listStyling}>
|
||||
<ul className={styles.list}>
|
||||
{numberOfChargingSpaces ? (
|
||||
<li>
|
||||
<li className={styles.listItem}>
|
||||
{intl.formatMessage(
|
||||
{
|
||||
id: 'parkingInformation.numberOfChargingPoints',
|
||||
@@ -50,13 +50,13 @@ export default function ParkingList({
|
||||
)}
|
||||
</li>
|
||||
) : null}
|
||||
<li>
|
||||
<li className={styles.listItem}>
|
||||
{canMakeReservation
|
||||
? canMakeReservationYesMsg
|
||||
: canMakeReservationNoMsg}
|
||||
</li>
|
||||
{numberOfParkingSpots ? (
|
||||
<li>
|
||||
<li className={styles.listItem}>
|
||||
{intl.formatMessage(
|
||||
{
|
||||
id: 'parkingInformation.numberOfParkingSpots',
|
||||
@@ -67,7 +67,7 @@ export default function ParkingList({
|
||||
</li>
|
||||
) : null}
|
||||
{distanceToHotel ? (
|
||||
<li>
|
||||
<li className={styles.listItem}>
|
||||
{intl.formatMessage(
|
||||
{
|
||||
id: 'parkingInformation.distanceToHotel',
|
||||
@@ -78,7 +78,7 @@ export default function ParkingList({
|
||||
</li>
|
||||
) : null}
|
||||
{address ? (
|
||||
<li>
|
||||
<li className={styles.listItem}>
|
||||
{intl.formatMessage(
|
||||
{
|
||||
id: 'parkingInformation.address',
|
||||
|
||||
@@ -1,11 +1,24 @@
|
||||
.listStyling {
|
||||
.list {
|
||||
display: grid;
|
||||
gap: var(--Space-x1);
|
||||
list-style-type: none;
|
||||
}
|
||||
|
||||
.listStyling > li::before {
|
||||
content: url('/_static/icons/heart.svg');
|
||||
position: relative;
|
||||
height: 8px;
|
||||
top: 3px;
|
||||
margin-right: var(--Spacing-x1);
|
||||
.listItem {
|
||||
display: flex;
|
||||
gap: var(--Space-x1);
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: relative;
|
||||
top: 3px;
|
||||
display: inline-flex;
|
||||
flex-shrink: 0;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
background-color: var(--Icon-Accent);
|
||||
mask-image: url('/_static/icons/heart.svg');
|
||||
mask-size: contain;
|
||||
mask-repeat: no-repeat;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
|
||||
import { useIntl } from 'react-intl'
|
||||
|
||||
import ButtonLink from '../ButtonLink'
|
||||
import { Divider } from '../Divider'
|
||||
import { MaterialIcon } from '../Icons/MaterialIcon'
|
||||
import { Typography } from '../Typography'
|
||||
import ButtonLink from '../ButtonLink'
|
||||
import ParkingList from './ParkingList'
|
||||
import ParkingPrices from './ParkingPrices'
|
||||
|
||||
@@ -88,6 +88,7 @@ export default function ParkingInformation({
|
||||
{parking.externalParkingUrl && showExternalParkingButton && (
|
||||
<ButtonLink
|
||||
typography="Body/Paragraph/mdBold"
|
||||
size="Medium"
|
||||
href={parking.externalParkingUrl}
|
||||
target="_blank"
|
||||
>
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
.parkingInformation {
|
||||
display: grid;
|
||||
gap: var(--Spacing-x3);
|
||||
gap: var(--Space-x3);
|
||||
}
|
||||
|
||||
.list,
|
||||
.prices {
|
||||
display: grid;
|
||||
gap: var(--Spacing-x-one-and-half);
|
||||
gap: var(--Space-x15);
|
||||
}
|
||||
|
||||
.priceWrapper {
|
||||
background-color: var(--Base-Surface-Subtle-Normal);
|
||||
background-color: var(--Surface-Secondary-Default);
|
||||
border-radius: var(--Corner-radius-md);
|
||||
padding: var(--Spacing-x2) var(--Spacing-x3);
|
||||
padding: var(--Space-x2) var(--Space-x3);
|
||||
display: grid;
|
||||
gap: var(--Spacing-x1);
|
||||
gap: var(--Space-x1);
|
||||
}
|
||||
|
||||
.heading {
|
||||
|
||||
@@ -19,7 +19,6 @@
|
||||
|
||||
.theme-primary:not(.disabled) {
|
||||
color: var(--Text-Interactive-Secondary);
|
||||
text-decoration: underline;
|
||||
|
||||
&:hover {
|
||||
color: var(--Text-Interactive-Secondary-Hover);
|
||||
@@ -28,7 +27,6 @@
|
||||
|
||||
.theme-inverted:not(.disabled) {
|
||||
color: var(--Text-Inverted);
|
||||
text-decoration: underline;
|
||||
|
||||
&:hover {
|
||||
opacity: 0.7;
|
||||
|
||||
Reference in New Issue
Block a user