Merge branch 'develop' into feat/sw-386-header-fixes
This commit is contained in:
@@ -73,6 +73,7 @@ export default async function IntroSection({
|
||||
color="burgundy"
|
||||
variant="icon"
|
||||
href={`?s=${about[lang]}`}
|
||||
scroll={false}
|
||||
>
|
||||
{intl.formatMessage({ id: "Read more about the hotel" })}
|
||||
<ArrowRight color="burgundy" />
|
||||
|
||||
@@ -1,147 +0,0 @@
|
||||
"use client"
|
||||
import {
|
||||
AdvancedMarker,
|
||||
Map,
|
||||
type MapProps,
|
||||
useMap,
|
||||
} from "@vis.gl/react-google-maps"
|
||||
import { useState } from "react"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import useHotelPageStore from "@/stores/hotel-page"
|
||||
|
||||
import { CloseIcon, MinusIcon, PlusIcon } from "@/components/Icons"
|
||||
import Button from "@/components/TempDesignSystem/Button"
|
||||
import Divider from "@/components/TempDesignSystem/Divider"
|
||||
import Title from "@/components/TempDesignSystem/Text/Title"
|
||||
import { useHandleKeyUp } from "@/hooks/useHandleKeyUp"
|
||||
import useLang from "@/hooks/useLang"
|
||||
|
||||
import ScandicMarker from "../Markers/Scandic"
|
||||
|
||||
import styles from "./dynamicMap.module.css"
|
||||
|
||||
import type { DynamicMapContentProps } from "@/types/components/hotelPage/map/dynamicMapContent"
|
||||
|
||||
export default function DynamicMapContent({
|
||||
hotelName,
|
||||
coordinates,
|
||||
}: DynamicMapContentProps) {
|
||||
const intl = useIntl()
|
||||
const lang = useLang()
|
||||
const { isDynamicMapOpen, closeDynamicMap } = useHotelPageStore()
|
||||
const [isFullScreenSidebar, setIsFullScreenSidebar] = useState(false)
|
||||
const map = useMap()
|
||||
|
||||
const mapOptions: MapProps = {
|
||||
defaultZoom: 15,
|
||||
defaultCenter: coordinates,
|
||||
disableDefaultUI: true,
|
||||
clickableIcons: false,
|
||||
mapId: `${hotelName}-${lang}-map`,
|
||||
// As reference for future styles when adding POIs
|
||||
// styles: [
|
||||
// {
|
||||
// featureType: "poi",
|
||||
// elementType: "all",
|
||||
// stylers: [{ visibility: "off" }],
|
||||
// },
|
||||
// ],
|
||||
}
|
||||
|
||||
useHandleKeyUp((event: KeyboardEvent) => {
|
||||
if (event.key === "Escape" && isDynamicMapOpen) {
|
||||
closeDynamicMap()
|
||||
}
|
||||
})
|
||||
|
||||
function zoomIn() {
|
||||
const currentZoom = map && map.getZoom()
|
||||
if (currentZoom) {
|
||||
map.setZoom(currentZoom + 1)
|
||||
}
|
||||
}
|
||||
function zoomOut() {
|
||||
const currentZoom = map && map.getZoom()
|
||||
if (currentZoom) {
|
||||
map.setZoom(currentZoom - 1)
|
||||
}
|
||||
}
|
||||
|
||||
function toggleFullScreenSidebar() {
|
||||
setIsFullScreenSidebar((prev) => !prev)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={styles.ctaButtons}>
|
||||
<Button
|
||||
theme="base"
|
||||
intent="inverted"
|
||||
variant="icon"
|
||||
size="small"
|
||||
className={styles.closeButton}
|
||||
onClick={closeDynamicMap}
|
||||
>
|
||||
<CloseIcon color="burgundy" width={24} height={24} />
|
||||
<span>{intl.formatMessage({ id: "Close the map" })}</span>
|
||||
</Button>
|
||||
<div className={styles.zoomButtons}>
|
||||
<Button
|
||||
theme="base"
|
||||
intent="inverted"
|
||||
variant="icon"
|
||||
size="small"
|
||||
className={styles.zoomButton}
|
||||
onClick={zoomOut}
|
||||
aria-label={intl.formatMessage({ id: "Zoom in" })}
|
||||
>
|
||||
<MinusIcon color="burgundy" width={20} height={20} />
|
||||
</Button>
|
||||
<Button
|
||||
theme="base"
|
||||
intent="inverted"
|
||||
variant="icon"
|
||||
size="small"
|
||||
className={styles.zoomButton}
|
||||
onClick={zoomIn}
|
||||
aria-label={intl.formatMessage({ id: "Zoom out" })}
|
||||
>
|
||||
<PlusIcon color="burgundy" width={20} height={20} />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<aside
|
||||
className={`${styles.sidebar} ${isFullScreenSidebar ? styles.fullscreen : ""}`}
|
||||
>
|
||||
<div className={styles.sidebarToggle}>
|
||||
<span className={styles.rectangle} />
|
||||
<button
|
||||
className={styles.toggleButton}
|
||||
onClick={toggleFullScreenSidebar}
|
||||
>
|
||||
{intl.formatMessage({
|
||||
id: isFullScreenSidebar ? "View as map" : "View as list",
|
||||
})}
|
||||
</button>
|
||||
</div>
|
||||
<div className={styles.sidebarContent}>
|
||||
<Title as="h4" level="h2" textTransform="regular">
|
||||
{intl.formatMessage(
|
||||
{ id: "Things nearby HOTEL_NAME" },
|
||||
{ hotelName }
|
||||
)}
|
||||
</Title>
|
||||
<Divider color="subtle" />
|
||||
</div>
|
||||
</aside>
|
||||
<div className={styles.mapContainer}>
|
||||
<Map {...mapOptions}>
|
||||
<AdvancedMarker position={coordinates}>
|
||||
<ScandicMarker />
|
||||
</AdvancedMarker>
|
||||
</Map>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
138
components/ContentType/HotelPage/Map/DynamicMap/Map/index.tsx
Normal file
138
components/ContentType/HotelPage/Map/DynamicMap/Map/index.tsx
Normal file
@@ -0,0 +1,138 @@
|
||||
"use client"
|
||||
import {
|
||||
AdvancedMarker,
|
||||
AdvancedMarkerAnchorPoint,
|
||||
Map,
|
||||
type MapProps,
|
||||
useMap,
|
||||
} from "@vis.gl/react-google-maps"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import useHotelPageStore from "@/stores/hotel-page"
|
||||
|
||||
import { MinusIcon, PlusIcon } from "@/components/Icons"
|
||||
import CloseLargeIcon from "@/components/Icons/CloseLarge"
|
||||
import PoiMarker from "@/components/Maps/Markers/Poi"
|
||||
import ScandicMarker from "@/components/Maps/Markers/Scandic"
|
||||
import Button from "@/components/TempDesignSystem/Button"
|
||||
import Body from "@/components/TempDesignSystem/Text/Body"
|
||||
import Caption from "@/components/TempDesignSystem/Text/Caption"
|
||||
|
||||
import styles from "./map.module.css"
|
||||
|
||||
import type { MapContentProps } from "@/types/components/hotelPage/map/mapContent"
|
||||
|
||||
export default function MapContent({
|
||||
coordinates,
|
||||
pointsOfInterest,
|
||||
activePoi,
|
||||
mapId,
|
||||
onActivePoiChange,
|
||||
}: MapContentProps) {
|
||||
const intl = useIntl()
|
||||
const { closeDynamicMap } = useHotelPageStore()
|
||||
const map = useMap()
|
||||
|
||||
const mapOptions: MapProps = {
|
||||
defaultZoom: 14,
|
||||
defaultCenter: coordinates,
|
||||
disableDefaultUI: true,
|
||||
clickableIcons: false,
|
||||
mapId,
|
||||
}
|
||||
|
||||
function zoomIn() {
|
||||
const currentZoom = map && map.getZoom()
|
||||
if (currentZoom) {
|
||||
map.setZoom(currentZoom + 1)
|
||||
}
|
||||
}
|
||||
function zoomOut() {
|
||||
const currentZoom = map && map.getZoom()
|
||||
if (currentZoom) {
|
||||
map.setZoom(currentZoom - 1)
|
||||
}
|
||||
}
|
||||
|
||||
function toggleActivePoi(poiName: string) {
|
||||
onActivePoiChange(activePoi === poiName ? null : poiName)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.mapContainer}>
|
||||
<Map {...mapOptions}>
|
||||
<AdvancedMarker position={coordinates} zIndex={1}>
|
||||
<ScandicMarker />
|
||||
</AdvancedMarker>
|
||||
|
||||
{pointsOfInterest.map((poi) => (
|
||||
<AdvancedMarker
|
||||
key={poi.name}
|
||||
className={styles.advancedMarker}
|
||||
position={poi.coordinates}
|
||||
anchorPoint={AdvancedMarkerAnchorPoint.CENTER}
|
||||
zIndex={activePoi === poi.name ? 2 : 0}
|
||||
onMouseEnter={() => onActivePoiChange(poi.name)}
|
||||
onMouseLeave={() => onActivePoiChange(null)}
|
||||
onClick={() => toggleActivePoi(poi.name)}
|
||||
>
|
||||
<span
|
||||
className={`${styles.poi} ${activePoi === poi.name ? styles.active : ""}`}
|
||||
>
|
||||
<PoiMarker
|
||||
category={poi.category}
|
||||
className={styles.poiMarker}
|
||||
size={activePoi === poi.name ? 20 : 16}
|
||||
/>
|
||||
<Body className={styles.poiLabel} asChild>
|
||||
<span>
|
||||
{poi.name}
|
||||
<Caption asChild>
|
||||
<span>{poi.distance} km</span>
|
||||
</Caption>
|
||||
</span>
|
||||
</Body>
|
||||
</span>
|
||||
</AdvancedMarker>
|
||||
))}
|
||||
</Map>
|
||||
<div className={styles.ctaButtons}>
|
||||
<Button
|
||||
theme="base"
|
||||
intent="inverted"
|
||||
variant="icon"
|
||||
size="small"
|
||||
className={styles.closeButton}
|
||||
onClick={closeDynamicMap}
|
||||
>
|
||||
<CloseLargeIcon color="burgundy" />
|
||||
<span>{intl.formatMessage({ id: "Close the map" })}</span>
|
||||
</Button>
|
||||
<div className={styles.zoomButtons}>
|
||||
<Button
|
||||
theme="base"
|
||||
intent="inverted"
|
||||
variant="icon"
|
||||
size="small"
|
||||
className={styles.zoomButton}
|
||||
onClick={zoomOut}
|
||||
aria-label={intl.formatMessage({ id: "Zoom in" })}
|
||||
>
|
||||
<MinusIcon color="burgundy" width={20} height={20} />
|
||||
</Button>
|
||||
<Button
|
||||
theme="base"
|
||||
intent="inverted"
|
||||
variant="icon"
|
||||
size="small"
|
||||
className={styles.zoomButton}
|
||||
onClick={zoomIn}
|
||||
aria-label={intl.formatMessage({ id: "Zoom out" })}
|
||||
>
|
||||
<PlusIcon color="burgundy" width={20} height={20} />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
.mapContainer {
|
||||
--button-box-shadow: 0 0 8px 1px rgba(0, 0, 0, 0.1);
|
||||
width: 100%;
|
||||
position: relative;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.ctaButtons {
|
||||
position: absolute;
|
||||
top: var(--Spacing-x2);
|
||||
right: var(--Spacing-x2);
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--Spacing-x7);
|
||||
align-items: flex-end;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.zoomButtons {
|
||||
display: grid;
|
||||
gap: var(--Spacing-x1);
|
||||
}
|
||||
|
||||
.closeButton {
|
||||
pointer-events: initial;
|
||||
box-shadow: var(--button-box-shadow);
|
||||
gap: var(--Spacing-x-half);
|
||||
}
|
||||
|
||||
.zoomButton {
|
||||
width: var(--Spacing-x5);
|
||||
height: var(--Spacing-x5);
|
||||
padding: 0;
|
||||
pointer-events: initial;
|
||||
box-shadow: var(--button-box-shadow);
|
||||
}
|
||||
|
||||
.advancedMarker {
|
||||
height: var(--Spacing-x4);
|
||||
width: var(
|
||||
--Spacing-x4
|
||||
) !important; /* Overwriting the default width of the @vis.gl/react-google-maps AdvancedMarker */
|
||||
}
|
||||
|
||||
.advancedMarker.active {
|
||||
height: var(--Spacing-x5);
|
||||
width: var(
|
||||
--Spacing-x5
|
||||
) !important; /* Overwriting the default width of the @vis.gl/react-google-maps AdvancedMarker */
|
||||
}
|
||||
|
||||
.poi {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: var(--Spacing-x-half);
|
||||
border-radius: var(--Corner-radius-Rounded);
|
||||
background-color: var(--Base-Surface-Primary-light-Normal);
|
||||
box-shadow: 0 0 4px 2px rgba(0, 0, 0, 0.1);
|
||||
gap: var(--Spacing-x1);
|
||||
}
|
||||
|
||||
.poi.active {
|
||||
padding-right: var(--Spacing-x-one-and-half);
|
||||
}
|
||||
|
||||
.poiLabel {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.poi.active .poiLabel {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--Spacing-x2);
|
||||
text-wrap: nowrap;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
.ctaButtons {
|
||||
top: var(--Spacing-x4);
|
||||
right: var(--Spacing-x4);
|
||||
bottom: var(--Spacing-x4);
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.zoomButtons {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import PoiMarker from "@/components/Maps/Markers/Poi"
|
||||
import Button from "@/components/TempDesignSystem/Button"
|
||||
import Body from "@/components/TempDesignSystem/Text/Body"
|
||||
import Title from "@/components/TempDesignSystem/Text/Title"
|
||||
|
||||
import styles from "./sidebar.module.css"
|
||||
|
||||
import type { SidebarProps } from "@/types/components/hotelPage/map/sidebar"
|
||||
|
||||
export default function Sidebar({
|
||||
activePoi,
|
||||
hotelName,
|
||||
pointsOfInterest,
|
||||
onActivePoiChange,
|
||||
}: SidebarProps) {
|
||||
const intl = useIntl()
|
||||
const [isFullScreenSidebar, setIsFullScreenSidebar] = useState(false)
|
||||
const poiCategories = new Set(
|
||||
pointsOfInterest.map(({ category }) => category)
|
||||
)
|
||||
const poisInCategories = Array.from(poiCategories).map((category) => ({
|
||||
category,
|
||||
pois: pointsOfInterest.filter((poi) => poi.category === category),
|
||||
}))
|
||||
|
||||
function toggleFullScreenSidebar() {
|
||||
setIsFullScreenSidebar((prev) => !prev)
|
||||
}
|
||||
|
||||
return (
|
||||
<aside
|
||||
className={`${styles.sidebar} ${
|
||||
isFullScreenSidebar ? styles.fullscreen : ""
|
||||
}`}
|
||||
>
|
||||
<Button
|
||||
theme="base"
|
||||
intent="text"
|
||||
className={styles.sidebarToggle}
|
||||
onClick={toggleFullScreenSidebar}
|
||||
>
|
||||
<Body textTransform="bold" color="textMediumContrast" asChild>
|
||||
<span>
|
||||
{intl.formatMessage({
|
||||
id: isFullScreenSidebar ? "View as map" : "View as list",
|
||||
})}
|
||||
</span>
|
||||
</Body>
|
||||
</Button>
|
||||
<div className={styles.sidebarContent}>
|
||||
<Title as="h4" level="h2" textTransform="regular">
|
||||
{intl.formatMessage(
|
||||
{ id: "Things nearby HOTEL_NAME" },
|
||||
{ hotelName }
|
||||
)}
|
||||
</Title>
|
||||
|
||||
{poisInCategories.map(({ category, pois }) =>
|
||||
pois.length ? (
|
||||
<div key={category} className={styles.poiGroup}>
|
||||
<Body
|
||||
color="black"
|
||||
textTransform="bold"
|
||||
className={styles.poiHeading}
|
||||
asChild
|
||||
>
|
||||
<h3>
|
||||
<PoiMarker category={category} />
|
||||
{intl.formatMessage({ id: category })}
|
||||
</h3>
|
||||
</Body>
|
||||
<ul className={styles.poiList}>
|
||||
{pois.map((poi) => (
|
||||
<li key={poi.name} className={styles.poiItem}>
|
||||
<button
|
||||
className={`${styles.poiButton} ${activePoi === poi.name ? styles.active : ""}`}
|
||||
onMouseEnter={() => onActivePoiChange(poi.name)}
|
||||
onMouseLeave={() => onActivePoiChange(null)}
|
||||
onClick={() =>
|
||||
onActivePoiChange(
|
||||
activePoi === poi.name ? null : poi.name
|
||||
)
|
||||
}
|
||||
>
|
||||
<span>{poi.name}</span>
|
||||
<span>{poi.distance} km</span>
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
) : null
|
||||
)}
|
||||
</div>
|
||||
</aside>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
.sidebar {
|
||||
--sidebar-max-width: 26.25rem;
|
||||
--sidebar-mobile-toggle-height: 91px;
|
||||
--sidebar-mobile-fullscreen-height: calc(
|
||||
100vh - var(--main-menu-mobile-height) - var(--sidebar-mobile-toggle-height)
|
||||
);
|
||||
|
||||
position: absolute;
|
||||
top: var(--sidebar-mobile-fullscreen-height);
|
||||
height: 100%;
|
||||
right: 0;
|
||||
left: 0;
|
||||
background-color: var(--Base-Surface-Primary-light-Normal);
|
||||
z-index: 1;
|
||||
transition: top 0.3s;
|
||||
}
|
||||
|
||||
.sidebar:not(.fullscreen) {
|
||||
border-radius: var(--Corner-radius-Large) var(--Corner-radius-Large) 0 0;
|
||||
}
|
||||
|
||||
.sidebar.fullscreen {
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.sidebarToggle {
|
||||
position: relative;
|
||||
margin: var(--Spacing-x4) 0 var(--Spacing-x2);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.sidebarToggle::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
display: block;
|
||||
top: -0.5rem;
|
||||
width: 100px;
|
||||
height: 3px;
|
||||
background-color: var(--UI-Text-High-contrast);
|
||||
}
|
||||
|
||||
.sidebarContent {
|
||||
display: grid;
|
||||
gap: var(--Spacing-x5);
|
||||
align-content: start;
|
||||
padding: var(--Spacing-x3) var(--Spacing-x2);
|
||||
height: var(--sidebar-mobile-fullscreen-height);
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.poiGroup {
|
||||
display: grid;
|
||||
gap: var(--Spacing-x2);
|
||||
}
|
||||
|
||||
.poiHeading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--Spacing-x1);
|
||||
}
|
||||
|
||||
.poiList {
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.poiItem {
|
||||
padding: var(--Spacing-x1) 0;
|
||||
border-bottom: 1px solid var(--Base-Border-Subtle);
|
||||
}
|
||||
|
||||
.poiButton {
|
||||
background-color: var(--Base-Surface-Primary-light-Normal);
|
||||
border-width: 0;
|
||||
font-family: var(--typography-Body-Regular-fontFamily);
|
||||
font-size: var(--typography-Body-Regular-fontSize);
|
||||
font-weight: var(--typography-Body-Regular-fontWeight);
|
||||
color: var(--UI-Text-High-contrast);
|
||||
width: 100%;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr max-content;
|
||||
gap: var(--Spacing-x2);
|
||||
align-items: center;
|
||||
padding: var(--Spacing-x-half) var(--Spacing-x1);
|
||||
border-radius: var(--Corner-radius-Medium);
|
||||
cursor: pointer;
|
||||
text-align: left;
|
||||
transition: background-color 0.3s;
|
||||
}
|
||||
.poiButton.active {
|
||||
background-color: var(--Base-Surface-Primary-light-Hover);
|
||||
}
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
.sidebar {
|
||||
position: static;
|
||||
width: 40vw;
|
||||
min-width: 10rem;
|
||||
max-width: var(--sidebar-max-width);
|
||||
background-color: var(--Base-Surface-Primary-light-Normal);
|
||||
}
|
||||
|
||||
.sidebarToggle {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.sidebarContent {
|
||||
padding: var(--Spacing-x4) var(--Spacing-x5);
|
||||
height: 100%;
|
||||
position: relative;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
.dynamicMap {
|
||||
--sidebar-height: 88px;
|
||||
position: fixed;
|
||||
top: var(--main-menu-mobile-height);
|
||||
right: 0;
|
||||
@@ -10,143 +9,8 @@
|
||||
background-color: var(--Base-Surface-Primary-light-Normal);
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
position: absolute;
|
||||
top: calc(100vh - var(--main-menu-mobile-height) - var(--sidebar-height));
|
||||
height: 100%;
|
||||
right: 0;
|
||||
left: 0;
|
||||
background-color: var(--Base-Surface-Primary-light-Normal);
|
||||
z-index: 1;
|
||||
transition: top 0.3s;
|
||||
}
|
||||
|
||||
.sidebar:not(.fullscreen) {
|
||||
border-radius: var(--Corner-radius-Large) var(--Corner-radius-Large) 0 0;
|
||||
}
|
||||
|
||||
.sidebar.fullscreen {
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.sidebarToggle {
|
||||
display: grid;
|
||||
gap: var(--Spacing-x1);
|
||||
padding: var(--Spacing-x2);
|
||||
height: var(--sidebar-height);
|
||||
}
|
||||
|
||||
.toggleButton {
|
||||
position: relative;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
background-color: transparent;
|
||||
border-width: 0;
|
||||
padding: var(--Spacing-x-one-and-half) var(--Spacing-x2);
|
||||
font-family: var(--typography-Body-Bold-fontFamily);
|
||||
font-size: var(--typography-Body-Bold-fontSize);
|
||||
font-weight: var(--typography-Body-Bold-fontWeight);
|
||||
color: var(--UI-Text-Medium-contrast);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.toggleButton::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
display: block;
|
||||
top: -0.5rem;
|
||||
width: 100px;
|
||||
height: 3px;
|
||||
background-color: var(--UI-Text-High-contrast);
|
||||
}
|
||||
|
||||
.sidebarContent {
|
||||
display: grid;
|
||||
gap: var(--Spacing-x3);
|
||||
align-content: start;
|
||||
padding: var(--Spacing-x3) var(--Spacing-x2);
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.mapContainer {
|
||||
flex: 1;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.mapContainer::after {
|
||||
content: "";
|
||||
display: block;
|
||||
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;
|
||||
}
|
||||
|
||||
.ctaButtons {
|
||||
position: absolute;
|
||||
top: var(--Spacing-x2);
|
||||
right: var(--Spacing-x2);
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--Spacing-x7);
|
||||
align-items: flex-end;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.zoomButtons {
|
||||
display: grid;
|
||||
gap: var(--Spacing-x1);
|
||||
}
|
||||
|
||||
.closeButton {
|
||||
pointer-events: initial;
|
||||
box-shadow: 0 0 8px 1px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.zoomButton {
|
||||
width: var(--Spacing-x5);
|
||||
height: var(--Spacing-x5);
|
||||
padding: 0;
|
||||
pointer-events: initial;
|
||||
box-shadow: 0 0 8px 1px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
.dynamicMap {
|
||||
top: var(--main-menu-desktop-height);
|
||||
}
|
||||
.sidebar {
|
||||
position: static;
|
||||
width: 40vw;
|
||||
min-width: 10rem;
|
||||
max-width: 26.25rem;
|
||||
background-color: var(--Base-Surface-Primary-light-Normal);
|
||||
}
|
||||
.sidebarToggle {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.sidebarContent {
|
||||
padding: var(--Spacing-x4) var(--Spacing-x5);
|
||||
}
|
||||
|
||||
.ctaButtons {
|
||||
top: var(--Spacing-x4);
|
||||
right: var(--Spacing-x4);
|
||||
bottom: var(--Spacing-x4);
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.zoomButtons {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,8 @@ import useHotelPageStore from "@/stores/hotel-page"
|
||||
|
||||
import { useHandleKeyUp } from "@/hooks/useHandleKeyUp"
|
||||
|
||||
import DynamicMapContent from "./Content"
|
||||
import MapContent from "./Map"
|
||||
import Sidebar from "./Sidebar"
|
||||
|
||||
import styles from "./dynamicMap.module.css"
|
||||
|
||||
@@ -18,11 +19,14 @@ export default function DynamicMap({
|
||||
apiKey,
|
||||
hotelName,
|
||||
coordinates,
|
||||
pointsOfInterest,
|
||||
mapId,
|
||||
}: DynamicMapProps) {
|
||||
const intl = useIntl()
|
||||
const { isDynamicMapOpen, closeDynamicMap } = useHotelPageStore()
|
||||
const [scrollHeightWhenOpened, setScrollHeightWhenOpened] = useState(0)
|
||||
const hasMounted = useRef(false)
|
||||
const [activePoi, setActivePoi] = useState<string | null>(null)
|
||||
|
||||
useHandleKeyUp((event: KeyboardEvent) => {
|
||||
if (event.key === "Escape" && isDynamicMapOpen) {
|
||||
@@ -58,7 +62,19 @@ export default function DynamicMap({
|
||||
{ hotelName }
|
||||
)}
|
||||
>
|
||||
<DynamicMapContent hotelName={hotelName} coordinates={coordinates} />
|
||||
<Sidebar
|
||||
activePoi={activePoi}
|
||||
hotelName={hotelName}
|
||||
pointsOfInterest={pointsOfInterest}
|
||||
onActivePoiChange={setActivePoi}
|
||||
/>
|
||||
<MapContent
|
||||
coordinates={coordinates}
|
||||
pointsOfInterest={pointsOfInterest}
|
||||
activePoi={activePoi}
|
||||
onActivePoiChange={setActivePoi}
|
||||
mapId={mapId}
|
||||
/>
|
||||
</Dialog>
|
||||
</Modal>
|
||||
</APIProvider>
|
||||
|
||||
@@ -4,7 +4,9 @@ import { useIntl } from "react-intl"
|
||||
|
||||
import useHotelPageStore from "@/stores/hotel-page"
|
||||
|
||||
import PoiMarker from "@/components/Maps/Markers/Poi"
|
||||
import Button from "@/components/TempDesignSystem/Button"
|
||||
import Body from "@/components/TempDesignSystem/Text/Body"
|
||||
import Caption from "@/components/TempDesignSystem/Text/Caption"
|
||||
import Title from "@/components/TempDesignSystem/Text/Title"
|
||||
|
||||
@@ -12,7 +14,7 @@ import styles from "./mapCard.module.css"
|
||||
|
||||
import type { MapCardProps } from "@/types/components/hotelPage/map/mapCard"
|
||||
|
||||
export default function MapCard({ hotelName }: MapCardProps) {
|
||||
export default function MapCard({ hotelName, pois }: MapCardProps) {
|
||||
const intl = useIntl()
|
||||
const { openDynamicMap } = useHotelPageStore()
|
||||
|
||||
@@ -34,6 +36,15 @@ export default function MapCard({ hotelName }: MapCardProps) {
|
||||
>
|
||||
{hotelName}
|
||||
</Title>
|
||||
<ul className={styles.poiList}>
|
||||
{pois.map((poi) => (
|
||||
<li key={poi.name} className={styles.poiItem}>
|
||||
<PoiMarker category={poi.category} skipBackground size={20} />
|
||||
<Body color="black">{poi.name}</Body>
|
||||
<Caption>{poi.distance} km</Caption>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
<Button
|
||||
theme="base"
|
||||
|
||||
@@ -13,3 +13,17 @@
|
||||
.ctaButton {
|
||||
margin-top: var(--Spacing-x2);
|
||||
}
|
||||
|
||||
.poiList {
|
||||
list-style: none;
|
||||
margin-top: var(--Spacing-x1);
|
||||
}
|
||||
|
||||
.poiItem {
|
||||
display: grid;
|
||||
grid-template-columns: max-content 1fr max-content;
|
||||
gap: var(--Spacing-x-half);
|
||||
align-items: center;
|
||||
padding: var(--Spacing-x2) 0;
|
||||
border-bottom: 1px solid var(--Base-Border-Subtle);
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
border-radius: 4rem;
|
||||
background-color: var(--Base-Surface-Primary-light-Normal);
|
||||
box-shadow: 0 0 30px 2px rgba(0, 0, 0, 0.15);
|
||||
padding: 0.375rem;
|
||||
padding: var(--Spacing-x-half);
|
||||
}
|
||||
|
||||
.button {
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
/* eslint-disable @next/next/no-img-element */
|
||||
|
||||
import { env } from "@/env/server"
|
||||
|
||||
import ScandicMarker from "@/components/Maps/Markers/Scandic"
|
||||
import StaticMapComp from "@/components/Maps/StaticMap"
|
||||
import { getIntl } from "@/i18n"
|
||||
import { calculateLatWithOffset } from "@/utils/map"
|
||||
|
||||
import ScandicMarker from "../Markers/Scandic"
|
||||
|
||||
import styles from "./staticMap.module.css"
|
||||
|
||||
import type { StaticMapProps } from "@/types/components/hotelPage/map/staticMap"
|
||||
@@ -16,6 +17,7 @@ export default async function StaticMap({
|
||||
zoomLevel = 14,
|
||||
}: StaticMapProps) {
|
||||
const intl = await getIntl()
|
||||
const mapId = env.GOOGLE_STATIC_MAP_ID
|
||||
const mapHeight = 785
|
||||
const markerHeight = 100
|
||||
const mapLatitudeInPx = mapHeight * 0.2
|
||||
@@ -32,6 +34,7 @@ export default async function StaticMap({
|
||||
height={mapHeight}
|
||||
zoomLevel={zoomLevel}
|
||||
altText={intl.formatMessage({ id: "Map of HOTEL_NAME" }, { hotelName })}
|
||||
mapId={mapId}
|
||||
/>
|
||||
<ScandicMarker
|
||||
className={styles.mapMarker}
|
||||
|
||||
@@ -1,99 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import { usePathname, useRouter, useSearchParams } from "next/navigation"
|
||||
import { useEffect, useState } from "react"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import {
|
||||
about,
|
||||
activities,
|
||||
amenities,
|
||||
meetingsAndConferences,
|
||||
restaurantAndBar,
|
||||
wellnessAndExercise,
|
||||
} from "@/constants/routes/hotelPageParams"
|
||||
|
||||
import SidePeek from "@/components/TempDesignSystem/SidePeek"
|
||||
import SidePeekItem from "@/components/TempDesignSystem/SidePeek/Item"
|
||||
import { SidePeekContentKey } from "@/components/TempDesignSystem/SidePeek/types"
|
||||
import useLang from "@/hooks/useLang"
|
||||
|
||||
function SidePeekContainer() {
|
||||
const router = useRouter()
|
||||
const pathname = usePathname()
|
||||
const searchParams = useSearchParams()
|
||||
const [activeSidePeek, setActiveSidePeek] =
|
||||
useState<SidePeekContentKey | null>(() => {
|
||||
const sidePeekParam = searchParams.get("s") as SidePeekContentKey | null
|
||||
return sidePeekParam || null
|
||||
})
|
||||
|
||||
const lang = useLang()
|
||||
const intl = useIntl()
|
||||
|
||||
useEffect(() => {
|
||||
const sidePeekParam = searchParams.get("s") as SidePeekContentKey | null
|
||||
if (sidePeekParam !== activeSidePeek) {
|
||||
setActiveSidePeek(sidePeekParam)
|
||||
}
|
||||
}, [searchParams, activeSidePeek])
|
||||
|
||||
function handleClose(isOpen: boolean) {
|
||||
if (!isOpen) {
|
||||
setActiveSidePeek(null)
|
||||
|
||||
const nextSearchParams = new URLSearchParams(searchParams.toString())
|
||||
nextSearchParams.delete("s")
|
||||
|
||||
router.push(`${pathname}?${nextSearchParams}`, { scroll: false })
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<SidePeek handleClose={handleClose} activeSidePeek={activeSidePeek}>
|
||||
<SidePeekItem
|
||||
contentKey={amenities[lang]}
|
||||
title={intl.formatMessage({ id: "Amenities" })}
|
||||
>
|
||||
{/* TODO: Render amenities as per the design. */}
|
||||
Read more about the amenities here
|
||||
</SidePeekItem>
|
||||
<SidePeekItem
|
||||
contentKey={about[lang]}
|
||||
title={intl.formatMessage({ id: "Read more about the hotel" })}
|
||||
>
|
||||
Some additional information about the hotel
|
||||
</SidePeekItem>
|
||||
<SidePeekItem
|
||||
contentKey={restaurantAndBar[lang]}
|
||||
title={intl.formatMessage({ id: "Restaurant & Bar" })}
|
||||
>
|
||||
{/* TODO */}
|
||||
Restaurant & Bar
|
||||
</SidePeekItem>
|
||||
<SidePeekItem
|
||||
contentKey={wellnessAndExercise[lang]}
|
||||
title={intl.formatMessage({ id: "Wellness & Exercise" })}
|
||||
>
|
||||
{/* TODO */}
|
||||
Wellness & Exercise
|
||||
</SidePeekItem>
|
||||
<SidePeekItem
|
||||
contentKey={activities[lang]}
|
||||
title={intl.formatMessage({ id: "Activities" })}
|
||||
>
|
||||
{/* TODO */}
|
||||
Activities
|
||||
</SidePeekItem>
|
||||
<SidePeekItem
|
||||
contentKey={meetingsAndConferences[lang]}
|
||||
title={intl.formatMessage({ id: "Meetings & Conferences" })}
|
||||
>
|
||||
{/* TODO */}
|
||||
Meetings & Conferences
|
||||
</SidePeekItem>
|
||||
</SidePeek>
|
||||
)
|
||||
}
|
||||
|
||||
export default SidePeekContainer
|
||||
@@ -1,6 +1,12 @@
|
||||
import hotelPageParams from "@/constants/routes/hotelPageParams"
|
||||
import { env } from "@/env/server"
|
||||
import { serverClient } from "@/lib/trpc/server"
|
||||
|
||||
import SidePeekProvider from "@/components/SidePeekProvider"
|
||||
import SidePeek from "@/components/TempDesignSystem/SidePeek"
|
||||
import { getIntl } from "@/i18n"
|
||||
import { getLang } from "@/i18n/serverContext"
|
||||
|
||||
import { MOCK_FACILITIES } from "./Facilities/mockData"
|
||||
import { setActivityCard } from "./Facilities/utils"
|
||||
import DynamicMap from "./Map/DynamicMap"
|
||||
@@ -12,13 +18,15 @@ import Facilities from "./Facilities"
|
||||
import IntroSection from "./IntroSection"
|
||||
import PreviewImages from "./PreviewImages"
|
||||
import { Rooms } from "./Rooms"
|
||||
import SidePeeks from "./SidePeeks"
|
||||
import TabNavigation from "./TabNavigation"
|
||||
|
||||
import styles from "./hotelPage.module.css"
|
||||
|
||||
export default async function HotelPage() {
|
||||
const intl = await getIntl()
|
||||
const lang = getLang()
|
||||
const googleMapsApiKey = env.GOOGLE_STATIC_MAP_KEY
|
||||
const googleMapId = env.GOOGLE_DYNAMIC_MAP_ID
|
||||
const hotelData = await serverClient().hotel.get({
|
||||
include: ["RoomCategories"],
|
||||
})
|
||||
@@ -36,10 +44,12 @@ export default async function HotelPage() {
|
||||
hotelImages,
|
||||
roomCategories,
|
||||
activitiesCard,
|
||||
pointsOfInterest,
|
||||
} = hotelData
|
||||
|
||||
const facilities = [...MOCK_FACILITIES]
|
||||
activitiesCard && facilities.push(setActivityCard(activitiesCard))
|
||||
const topThreePois = pointsOfInterest.slice(0, 3)
|
||||
|
||||
const coordinates = {
|
||||
lat: hotelLocation.latitude,
|
||||
@@ -61,6 +71,51 @@ export default async function HotelPage() {
|
||||
address={hotelAddress}
|
||||
tripAdvisor={hotelRatings?.tripAdvisor}
|
||||
/>
|
||||
<SidePeekProvider>
|
||||
{/* eslint-disable import/no-named-as-default-member */}
|
||||
<SidePeek
|
||||
contentKey={hotelPageParams.amenities[lang]}
|
||||
title={intl.formatMessage({ id: "Amenities" })}
|
||||
>
|
||||
{/* TODO: Render amenities as per the design. */}
|
||||
Read more about the amenities here
|
||||
</SidePeek>
|
||||
<SidePeek
|
||||
contentKey={hotelPageParams.about[lang]}
|
||||
title={intl.formatMessage({ id: "Read more about the hotel" })}
|
||||
>
|
||||
Some additional information about the hotel
|
||||
</SidePeek>
|
||||
<SidePeek
|
||||
contentKey={hotelPageParams.restaurantAndBar[lang]}
|
||||
title={intl.formatMessage({ id: "Restaurant & Bar" })}
|
||||
>
|
||||
{/* TODO */}
|
||||
Restaurant & Bar
|
||||
</SidePeek>
|
||||
<SidePeek
|
||||
contentKey={hotelPageParams.wellnessAndExercise[lang]}
|
||||
title={intl.formatMessage({ id: "Wellness & Exercise" })}
|
||||
>
|
||||
{/* TODO */}
|
||||
Wellness & Exercise
|
||||
</SidePeek>
|
||||
<SidePeek
|
||||
contentKey={hotelPageParams.activities[lang]}
|
||||
title={intl.formatMessage({ id: "Activities" })}
|
||||
>
|
||||
{/* TODO */}
|
||||
Activities
|
||||
</SidePeek>
|
||||
<SidePeek
|
||||
contentKey={hotelPageParams.meetingsAndConferences[lang]}
|
||||
title={intl.formatMessage({ id: "Meetings & Conferences" })}
|
||||
>
|
||||
{/* TODO */}
|
||||
Meetings & Conferences
|
||||
</SidePeek>
|
||||
{/* eslint-enable import/no-named-as-default-member */}
|
||||
</SidePeekProvider>
|
||||
<AmenitiesList detailedFacilities={hotelDetailedFacilities} />
|
||||
</div>
|
||||
<Rooms rooms={roomCategories} />
|
||||
@@ -70,17 +125,18 @@ export default async function HotelPage() {
|
||||
<>
|
||||
<aside className={styles.mapContainer}>
|
||||
<StaticMap coordinates={coordinates} hotelName={hotelName} />
|
||||
<MapCard hotelName={hotelName} />
|
||||
<MapCard hotelName={hotelName} pois={topThreePois} />
|
||||
</aside>
|
||||
<MobileMapToggle />
|
||||
<DynamicMap
|
||||
apiKey={googleMapsApiKey}
|
||||
hotelName={hotelName}
|
||||
coordinates={coordinates}
|
||||
pointsOfInterest={pointsOfInterest}
|
||||
mapId={googleMapId}
|
||||
/>
|
||||
</>
|
||||
) : null}
|
||||
<SidePeeks />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
40
components/Icons/Cultural.tsx
Normal file
40
components/Icons/Cultural.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
import { iconVariants } from "./variants"
|
||||
|
||||
import type { IconProps } from "@/types/components/icon"
|
||||
|
||||
export default function CulturalIcon({
|
||||
className,
|
||||
color,
|
||||
...props
|
||||
}: IconProps) {
|
||||
const classNames = iconVariants({ className, color })
|
||||
return (
|
||||
<svg
|
||||
className={classNames}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
{...props}
|
||||
>
|
||||
<mask
|
||||
id="mask0_71_1002"
|
||||
style={{ maskType: "alpha" }}
|
||||
maskUnits="userSpaceOnUse"
|
||||
x="0"
|
||||
y="0"
|
||||
width="24"
|
||||
height="24"
|
||||
>
|
||||
<rect width="24" height="24" fill="#D9D9D9" />
|
||||
</mask>
|
||||
<g mask="url(#mask0_71_1002)">
|
||||
<path
|
||||
d="M18.8424 7.51255C19.1058 7.51255 19.3292 7.4226 19.5125 7.2427C19.6959 7.0628 19.7875 6.83988 19.7875 6.57395C19.7875 6.30802 19.6965 6.08547 19.5144 5.9063C19.3323 5.72713 19.1067 5.63755 18.8375 5.63755C18.5755 5.63755 18.3558 5.72922 18.1785 5.91255C18.0012 6.09588 17.9125 6.31672 17.9125 6.57505C17.9125 6.83338 18.0017 7.05422 18.1799 7.23755C18.3581 7.42088 18.5789 7.51255 18.8424 7.51255ZM14.9 7.51255C15.1584 7.51255 15.3792 7.42088 15.5625 7.23755C15.7459 7.05422 15.8375 6.83338 15.8375 6.57505C15.8375 6.31672 15.7459 6.09588 15.5625 5.91255C15.3792 5.72922 15.1584 5.63755 14.9 5.63755C14.6417 5.63755 14.4209 5.72922 14.2375 5.91255C14.0542 6.09588 13.9625 6.31672 13.9625 6.57505C13.9625 6.83338 14.0542 7.05422 14.2375 7.23755C14.4209 7.42088 14.6417 7.51255 14.9 7.51255ZM16.875 9.06255C16.4212 9.06255 15.9869 9.1438 15.5721 9.3063C15.1574 9.4688 14.8417 9.74172 14.625 10.125C14.5167 10.3 14.5292 10.4709 14.6625 10.6375C14.7959 10.8042 14.975 10.8875 15.2 10.8875H18.55C18.775 10.8875 18.9542 10.8042 19.0875 10.6375C19.2209 10.4709 19.2334 10.3 19.125 10.125C18.9084 9.74172 18.5927 9.4688 18.1779 9.3063C17.7632 9.1438 17.3289 9.06255 16.875 9.06255ZM7.11035 21.75C5.49515 21.75 4.12297 21.1849 2.9938 20.0547C1.86463 18.9245 1.30005 17.5521 1.30005 15.9375V11.0125C1.30005 10.4969 1.48364 10.0555 1.85082 9.68832C2.21801 9.32114 2.65942 9.13755 3.17505 9.13755H11.05C11.5657 9.13755 12.0071 9.32114 12.3743 9.68832C12.7415 10.0555 12.925 10.4969 12.925 11.0125V15.9375C12.925 17.5521 12.3597 18.9245 11.2291 20.0547C10.0985 21.1849 8.72555 21.75 7.11035 21.75ZM7.11255 19.875C8.20422 19.875 9.13338 19.4917 9.90005 18.725C10.6667 17.9584 11.05 17.0292 11.05 15.9375V11.0125H3.17505V15.9375C3.17505 17.0292 3.55838 17.9584 4.32505 18.725C5.09172 19.4917 6.02088 19.875 7.11255 19.875ZM16.875 14.85C16.4417 14.85 16.0084 14.8021 15.575 14.7063C15.1417 14.6105 14.7334 14.4667 14.35 14.275V12.0625C14.7167 12.3542 15.1153 12.5792 15.5457 12.7375C15.9762 12.8959 16.4193 12.975 16.875 12.975C17.9667 12.975 18.8959 12.5917 19.6625 11.825C20.4292 11.0584 20.8125 10.1292 20.8125 9.03755V4.11255H12.925V7.71255H11.05V4.11255C11.05 3.59692 11.2336 3.15551 11.6008 2.78832C11.968 2.42114 12.4094 2.23755 12.925 2.23755H20.8125C21.3282 2.23755 21.7696 2.42114 22.1368 2.78832C22.504 3.15551 22.6875 3.59692 22.6875 4.11255V9.03755C22.6875 10.6521 22.1224 12.0245 20.9922 13.1547C19.862 14.2849 18.4896 14.85 16.875 14.85ZM5.16255 14.4C5.42463 14.4 5.64432 14.3109 5.82162 14.1327C5.99891 13.9545 6.08755 13.7337 6.08755 13.4702C6.08755 13.2068 5.99844 12.9834 5.82022 12.8C5.64201 12.6167 5.42117 12.525 5.15772 12.525C4.89427 12.525 4.67088 12.6161 4.48755 12.7982C4.30422 12.9803 4.21255 13.2059 4.21255 13.475C4.21255 13.7371 4.30359 13.9568 4.48567 14.1341C4.66776 14.3114 4.89338 14.4 5.16255 14.4ZM9.08755 14.4C9.34588 14.4 9.56672 14.3109 9.75005 14.1327C9.93338 13.9545 10.025 13.7337 10.025 13.4702C10.025 13.2068 9.9351 12.9834 9.7552 12.8C9.5753 12.6167 9.35238 12.525 9.08645 12.525C8.82052 12.525 8.59797 12.6161 8.4188 12.7982C8.23963 12.9803 8.15005 13.2059 8.15005 13.475C8.15005 13.7371 8.24172 13.9568 8.42505 14.1341C8.60838 14.3114 8.82922 14.4 9.08755 14.4ZM7.12317 17.7875C7.57442 17.7875 8.00213 17.7042 8.4063 17.5375C8.81047 17.3709 9.12922 17.1042 9.36255 16.7375C9.47088 16.5542 9.46418 16.375 9.34245 16.2C9.2207 16.025 9.04823 15.9375 8.82505 15.9375H5.42505C5.20187 15.9375 5.0294 16.025 4.90765 16.2C4.78592 16.375 4.77922 16.5542 4.88755 16.7375C5.12088 17.1042 5.43901 17.3709 5.84192 17.5375C6.24482 17.7042 6.67191 17.7875 7.12317 17.7875Z"
|
||||
fill="#26201E"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
36
components/Icons/Museum.tsx
Normal file
36
components/Icons/Museum.tsx
Normal file
@@ -0,0 +1,36 @@
|
||||
import { iconVariants } from "./variants"
|
||||
|
||||
import type { IconProps } from "@/types/components/icon"
|
||||
|
||||
export default function MuseumIcon({ className, color, ...props }: IconProps) {
|
||||
const classNames = iconVariants({ className, color })
|
||||
return (
|
||||
<svg
|
||||
className={classNames}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
{...props}
|
||||
>
|
||||
<mask
|
||||
id="mask0_3958_408"
|
||||
style={{ maskType: "alpha" }}
|
||||
maskUnits="userSpaceOnUse"
|
||||
x="0"
|
||||
y="0"
|
||||
width="24"
|
||||
height="24"
|
||||
>
|
||||
<rect width="24" height="24" fill="#D9D9D9" />
|
||||
</mask>
|
||||
<g mask="url(#mask0_3958_408)">
|
||||
<path
|
||||
d="M2.5 21V19.1H4.4V10.55H2.5V8.65L12 2L21.5 8.65V10.55H19.6V19.1H21.5V21H2.5ZM8.2 17.2H10.1V13.4L12 16.25L13.9 13.4V17.2H15.8V10.55H13.9L12 13.4L10.1 10.55H8.2V17.2ZM17.7 19.1V8.3175L12 4.3275L6.3 8.3175V19.1H17.7Z"
|
||||
fill="#26201E"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
40
components/Icons/Shopping.tsx
Normal file
40
components/Icons/Shopping.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
import { iconVariants } from "./variants"
|
||||
|
||||
import type { IconProps } from "@/types/components/icon"
|
||||
|
||||
export default function ShoppingIcon({
|
||||
className,
|
||||
color,
|
||||
...props
|
||||
}: IconProps) {
|
||||
const classNames = iconVariants({ className, color })
|
||||
return (
|
||||
<svg
|
||||
className={classNames}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
{...props}
|
||||
>
|
||||
<mask
|
||||
id="mask0_69_3482"
|
||||
style={{ maskType: "alpha" }}
|
||||
maskUnits="userSpaceOnUse"
|
||||
x="0"
|
||||
y="0"
|
||||
width="24"
|
||||
height="24"
|
||||
>
|
||||
<rect width="24" height="24" fill="#D9D9D9" />
|
||||
</mask>
|
||||
<g mask="url(#mask0_69_3482)">
|
||||
<path
|
||||
d="M6.125 21.75C5.60937 21.75 5.16796 21.5664 4.80078 21.1992C4.43359 20.832 4.25 20.3906 4.25 19.875V8.05C4.25 7.53437 4.43359 7.09296 4.80078 6.72578C5.16796 6.35859 5.60937 6.175 6.125 6.175H8.0625C8.0625 5.09167 8.4481 4.16667 9.2193 3.4C9.9905 2.63333 10.9176 2.25 12.0006 2.25C13.0835 2.25 14.0104 2.63433 14.7812 3.40298C15.5521 4.17161 15.9375 5.09562 15.9375 6.175H17.875C18.3906 6.175 18.832 6.35859 19.1992 6.72578C19.5664 7.09296 19.75 7.53437 19.75 8.05V19.875C19.75 20.3906 19.5664 20.832 19.1992 21.1992C18.832 21.5664 18.3906 21.75 17.875 21.75H6.125ZM6.125 19.875H17.875V8.05H15.9375V10.1125C15.9375 10.3708 15.8458 10.5917 15.6625 10.775C15.4792 10.9583 15.2583 11.05 15 11.05C14.7417 11.05 14.5208 10.9583 14.3375 10.775C14.1542 10.5917 14.0625 10.3708 14.0625 10.1125V8.05H9.9375V10.1125C9.9375 10.3708 9.84583 10.5917 9.6625 10.775C9.47917 10.9583 9.25833 11.05 9 11.05C8.74167 11.05 8.52083 10.9583 8.3375 10.775C8.15417 10.5917 8.0625 10.3708 8.0625 10.1125V8.05H6.125V19.875ZM9.9375 6.175H14.0625C14.0625 5.60833 13.8606 5.125 13.4568 4.725C13.053 4.325 12.5676 4.125 12.0006 4.125C11.4335 4.125 10.9479 4.32573 10.5438 4.7272C10.1396 5.12865 9.9375 5.61125 9.9375 6.175Z"
|
||||
fill="#26201E"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
40
components/Icons/StarFilled.tsx
Normal file
40
components/Icons/StarFilled.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
import { iconVariants } from "./variants"
|
||||
|
||||
import type { IconProps } from "@/types/components/icon"
|
||||
|
||||
export default function StarFilledIcon({
|
||||
className,
|
||||
color,
|
||||
...props
|
||||
}: IconProps) {
|
||||
const classNames = iconVariants({ className, color })
|
||||
return (
|
||||
<svg
|
||||
className={classNames}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
{...props}
|
||||
>
|
||||
<mask
|
||||
id="mask0_69_3249"
|
||||
style={{ maskType: "alpha" }}
|
||||
maskUnits="userSpaceOnUse"
|
||||
x="0"
|
||||
y="0"
|
||||
width="24"
|
||||
height="24"
|
||||
>
|
||||
<rect width="24" height="24" fill="#D9D9D9" />
|
||||
</mask>
|
||||
<g mask="url(#mask0_69_3249)">
|
||||
<path
|
||||
d="M11.9999 17.5625L7.81244 20.0875C7.63744 20.1958 7.45619 20.2417 7.26869 20.225C7.08119 20.2083 6.91661 20.1458 6.77494 20.0375C6.63328 19.9292 6.52494 19.7917 6.44994 19.625C6.37494 19.4583 6.36244 19.275 6.41244 19.075L7.52494 14.3125L3.82494 11.1125C3.66661 10.9708 3.56661 10.8104 3.52494 10.6312C3.48328 10.4521 3.49161 10.2792 3.54994 10.1125C3.60828 9.94583 3.70619 9.80417 3.84369 9.6875C3.98119 9.57083 4.15411 9.5 4.36244 9.475L9.23744 9.05L11.1374 4.5625C11.2208 4.37083 11.3416 4.22917 11.4999 4.1375C11.6583 4.04583 11.8249 4 11.9999 4C12.1749 4 12.3416 4.04583 12.4999 4.1375C12.6583 4.22917 12.7791 4.37083 12.8624 4.5625L14.7624 9.05L19.6374 9.475C19.8458 9.5 20.0187 9.57083 20.1562 9.6875C20.2937 9.80417 20.3916 9.94583 20.4499 10.1125C20.5083 10.2792 20.5166 10.4521 20.4749 10.6312C20.4333 10.8104 20.3333 10.9708 20.1749 11.1125L16.4749 14.3125L17.5874 19.075C17.6374 19.275 17.6249 19.4583 17.5499 19.625C17.4749 19.7917 17.3666 19.9292 17.2249 20.0375C17.0833 20.1458 16.9187 20.2083 16.7312 20.225C16.5437 20.2417 16.3624 20.1958 16.1874 20.0875L11.9999 17.5625Z"
|
||||
fill="#26201E"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
36
components/Icons/Train.tsx
Normal file
36
components/Icons/Train.tsx
Normal file
@@ -0,0 +1,36 @@
|
||||
import { iconVariants } from "./variants"
|
||||
|
||||
import type { IconProps } from "@/types/components/icon"
|
||||
|
||||
export default function TrainIcon({ className, color, ...props }: IconProps) {
|
||||
const classNames = iconVariants({ className, color })
|
||||
return (
|
||||
<svg
|
||||
className={classNames}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
{...props}
|
||||
>
|
||||
<mask
|
||||
id="mask0_71_1020"
|
||||
style={{ maskType: "alpha" }}
|
||||
maskUnits="userSpaceOnUse"
|
||||
x="0"
|
||||
y="0"
|
||||
width="24"
|
||||
height="24"
|
||||
>
|
||||
<rect width="24" height="24" fill="#D9D9D9" />
|
||||
</mask>
|
||||
<g mask="url(#mask0_71_1020)">
|
||||
<path
|
||||
d="M4.22498 15.425V6.1C4.22498 5.25 4.43956 4.57292 4.86873 4.06875C5.29789 3.56458 5.87081 3.17917 6.58748 2.9125C7.30414 2.64583 8.13123 2.46875 9.06873 2.38125C10.0062 2.29375 10.9833 2.25 12 2.25C13.0666 2.25 14.0729 2.29375 15.0187 2.38125C15.9646 2.46875 16.7896 2.64583 17.4937 2.9125C18.1979 3.17917 18.7541 3.56458 19.1625 4.06875C19.5708 4.57292 19.775 5.25 19.775 6.1V15.425C19.775 16.3833 19.4521 17.1854 18.8062 17.8313C18.1604 18.4771 17.3583 18.8 16.4 18.8L16.9375 19.3375C17.1958 19.5958 17.2541 19.8917 17.1125 20.225C16.9708 20.5583 16.7166 20.725 16.35 20.725C16.2416 20.725 16.1396 20.7063 16.0437 20.6688C15.9479 20.6313 15.8583 20.5708 15.775 20.4875L14.0875 18.8H9.91248L8.22498 20.4875C8.14164 20.5708 8.05206 20.6313 7.95623 20.6688C7.86039 20.7063 7.75831 20.725 7.64998 20.725C7.29164 20.725 7.03956 20.5583 6.89373 20.225C6.74789 19.8917 6.80414 19.5958 7.06248 19.3375L7.59998 18.8C6.64164 18.8 5.83956 18.4771 5.19373 17.8313C4.54789 17.1854 4.22498 16.3833 4.22498 15.425ZM12 4.125C10.2583 4.125 8.96456 4.22708 8.11873 4.43125C7.27289 4.63542 6.71664 4.875 6.44998 5.15H17.6C17.3583 4.85833 16.8125 4.61458 15.9625 4.41875C15.1125 4.22292 13.7916 4.125 12 4.125ZM6.09998 10.125H11.075V7.025H6.09998V10.125ZM12.95 10.125H17.9V7.025H12.95V10.125ZM8.54998 15.875C8.96664 15.875 9.30831 15.7417 9.57498 15.475C9.84164 15.2083 9.97498 14.8667 9.97498 14.45C9.97498 14.0333 9.84164 13.6917 9.57498 13.425C9.30831 13.1583 8.96664 13.025 8.54998 13.025C8.13331 13.025 7.79164 13.1583 7.52498 13.425C7.25831 13.6917 7.12498 14.0333 7.12498 14.45C7.12498 14.8667 7.25831 15.2083 7.52498 15.475C7.79164 15.7417 8.13331 15.875 8.54998 15.875ZM15.45 15.875C15.8666 15.875 16.2083 15.7417 16.475 15.475C16.7416 15.2083 16.875 14.8667 16.875 14.45C16.875 14.0333 16.7416 13.6917 16.475 13.425C16.2083 13.1583 15.8666 13.025 15.45 13.025C15.0333 13.025 14.6916 13.1583 14.425 13.425C14.1583 13.6917 14.025 14.0333 14.025 14.45C14.025 14.8667 14.1583 15.2083 14.425 15.475C14.6916 15.7417 15.0333 15.875 15.45 15.875ZM7.59998 16.925H16.4C16.8333 16.925 17.1916 16.7833 17.475 16.5C17.7583 16.2167 17.9 15.8583 17.9 15.425V12H6.09998V15.425C6.09998 15.8583 6.24164 16.2167 6.52498 16.5C6.80831 16.7833 7.16664 16.925 7.59998 16.925Z"
|
||||
fill="#26201E"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
CoffeeIcon,
|
||||
ConciergeIcon,
|
||||
CrossCircle,
|
||||
CulturalIcon,
|
||||
DoorOpenIcon,
|
||||
ElectricBikeIcon,
|
||||
EmailIcon,
|
||||
@@ -35,6 +36,7 @@ import {
|
||||
LockIcon,
|
||||
MapIcon,
|
||||
MinusIcon,
|
||||
MuseumIcon,
|
||||
ParkingIcon,
|
||||
People2Icon,
|
||||
PersonIcon,
|
||||
@@ -46,6 +48,9 @@ import {
|
||||
SaunaIcon,
|
||||
SearchIcon,
|
||||
ServiceIcon,
|
||||
ShoppingIcon,
|
||||
StarFilledIcon,
|
||||
TrainIcon,
|
||||
TshirtWashIcon,
|
||||
WarningTriangle,
|
||||
WifiIcon,
|
||||
@@ -91,6 +96,8 @@ export function getIconByIconName(icon?: IconName): FC<IconProps> | null {
|
||||
return CoffeeIcon
|
||||
case IconName.Concierge:
|
||||
return ConciergeIcon
|
||||
case IconName.Cultural:
|
||||
return CulturalIcon
|
||||
case IconName.DoorOpen:
|
||||
return DoorOpenIcon
|
||||
case IconName.ElectricBike:
|
||||
@@ -121,6 +128,8 @@ export function getIconByIconName(icon?: IconName): FC<IconProps> | null {
|
||||
return MapIcon
|
||||
case IconName.Minus:
|
||||
return MinusIcon
|
||||
case IconName.Museum:
|
||||
return MuseumIcon
|
||||
case IconName.Parking:
|
||||
return ParkingIcon
|
||||
case IconName.Person:
|
||||
@@ -143,6 +152,12 @@ export function getIconByIconName(icon?: IconName): FC<IconProps> | null {
|
||||
return SearchIcon
|
||||
case IconName.Service:
|
||||
return ServiceIcon
|
||||
case IconName.Shopping:
|
||||
return ShoppingIcon
|
||||
case IconName.StarFilled:
|
||||
return StarFilledIcon
|
||||
case IconName.Train:
|
||||
return TrainIcon
|
||||
case IconName.Tripadvisor:
|
||||
return TripAdvisorIcon
|
||||
case IconName.TshirtWash:
|
||||
|
||||
@@ -17,6 +17,7 @@ export { default as CoffeeIcon } from "./Coffee"
|
||||
export { default as ConciergeIcon } from "./Concierge"
|
||||
export { default as CreditCard } from "./CreditCard"
|
||||
export { default as CrossCircle } from "./CrossCircle"
|
||||
export { default as CulturalIcon } from "./Cultural"
|
||||
export { default as Delete } from "./Delete"
|
||||
export { default as DoorOpenIcon } from "./DoorOpen"
|
||||
export { default as ElectricBikeIcon } from "./ElectricBike"
|
||||
@@ -31,6 +32,7 @@ export { default as LocationIcon } from "./Location"
|
||||
export { default as LockIcon } from "./Lock"
|
||||
export { default as MapIcon } from "./Map"
|
||||
export { default as MinusIcon } from "./Minus"
|
||||
export { default as MuseumIcon } from "./Museum"
|
||||
export { default as ParkingIcon } from "./Parking"
|
||||
export { default as People2Icon } from "./People2"
|
||||
export { default as PersonIcon } from "./Person"
|
||||
@@ -44,6 +46,9 @@ export { default as SaunaIcon } from "./Sauna"
|
||||
export { default as ScandicLogoIcon } from "./ScandicLogo"
|
||||
export { default as SearchIcon } from "./Search"
|
||||
export { default as ServiceIcon } from "./Service"
|
||||
export { default as ShoppingIcon } from "./Shopping"
|
||||
export { default as StarFilledIcon } from "./StarFilled"
|
||||
export { default as TrainIcon } from "./Train"
|
||||
export { default as TshirtWashIcon } from "./TshirtWash"
|
||||
export { default as WarningTriangle } from "./WarningTriangle"
|
||||
export { default as WifiIcon } from "./Wifi"
|
||||
|
||||
27
components/Maps/Markers/Poi/index.tsx
Normal file
27
components/Maps/Markers/Poi/index.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
import { getIconByIconName } from "@/components/Icons/get-icon-by-icon-name"
|
||||
|
||||
import { getCategoryIconName } from "../utils"
|
||||
import { poiVariants } from "./variants"
|
||||
|
||||
import type { PoiMarkerProps } from "@/types/components/maps/poiMarker"
|
||||
|
||||
export default function PoiMarker({
|
||||
category,
|
||||
skipBackground,
|
||||
size = 16,
|
||||
className = "",
|
||||
}: PoiMarkerProps) {
|
||||
const iconName = getCategoryIconName(category)
|
||||
const Icon = iconName ? getIconByIconName(iconName) : null
|
||||
const classNames = poiVariants({ category, skipBackground, className })
|
||||
|
||||
return Icon ? (
|
||||
<span className={classNames}>
|
||||
<Icon
|
||||
color={skipBackground ? "grey80" : "white"}
|
||||
width={size}
|
||||
height={size}
|
||||
/>
|
||||
</span>
|
||||
) : null
|
||||
}
|
||||
52
components/Maps/Markers/Poi/poi.module.css
Normal file
52
components/Maps/Markers/Poi/poi.module.css
Normal file
@@ -0,0 +1,52 @@
|
||||
/* 2024-09-18: At the moment, the background-colors for the poi marker is unknown.
|
||||
This will be handled later. */
|
||||
|
||||
.icon {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: var(--Spacing-x-half);
|
||||
border-radius: var(--Corner-radius-Rounded);
|
||||
background-color: var(--Scandic-Beige-90);
|
||||
}
|
||||
.airport,
|
||||
.amusementPark,
|
||||
.busTerminal,
|
||||
.fair,
|
||||
.hospital,
|
||||
.hotel,
|
||||
.marketingCity {
|
||||
}
|
||||
|
||||
.museum {
|
||||
background: var(--Base-Interactive-Surface-Secondary-normal);
|
||||
}
|
||||
.nearbyCompanies,
|
||||
.parkingGarage {
|
||||
}
|
||||
|
||||
.restaurant {
|
||||
background: var(--Scandic-Peach-50);
|
||||
}
|
||||
|
||||
.shopping {
|
||||
background: var(--Base-Interactive-Surface-Primary-normal);
|
||||
}
|
||||
.sports,
|
||||
.theatre {
|
||||
}
|
||||
|
||||
.tourist {
|
||||
background: var(--Scandic-Yellow-60);
|
||||
}
|
||||
|
||||
.transportations {
|
||||
background: var(--Base-Interactive-Surface-Tertiary-normal);
|
||||
}
|
||||
.zoo {
|
||||
}
|
||||
|
||||
.icon.transparent {
|
||||
background: transparent;
|
||||
padding: 0;
|
||||
}
|
||||
34
components/Maps/Markers/Poi/variants.ts
Normal file
34
components/Maps/Markers/Poi/variants.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { cva } from "class-variance-authority"
|
||||
|
||||
import styles from "./poi.module.css"
|
||||
|
||||
export const poiVariants = cva(styles.icon, {
|
||||
variants: {
|
||||
category: {
|
||||
Airport: styles.airport,
|
||||
"Amusement park": styles.amusementPark,
|
||||
"Bus terminal": styles.busTerminal,
|
||||
Fair: styles.fair,
|
||||
Hospital: styles.hospital,
|
||||
Hotel: styles.hotel,
|
||||
"Marketing city": styles.marketingCity,
|
||||
Museum: styles.museum,
|
||||
"Nearby companies": styles.nearbyCompanies,
|
||||
"Parking / Garage": styles.parkingGarage,
|
||||
Restaurant: styles.restaurant,
|
||||
Shopping: styles.shopping,
|
||||
Sports: styles.sports,
|
||||
Theatre: styles.theatre,
|
||||
Tourist: styles.tourist,
|
||||
Transportations: styles.transportations,
|
||||
Zoo: styles.zoo,
|
||||
},
|
||||
skipBackground: {
|
||||
true: styles.transparent,
|
||||
false: "",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
skipBackground: false,
|
||||
},
|
||||
})
|
||||
21
components/Maps/Markers/utils.ts
Normal file
21
components/Maps/Markers/utils.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { IconName } from "@/types/components/icon"
|
||||
import type { PointOfInterestCategory } from "@/types/hotel"
|
||||
|
||||
/* 2024-09-18: At the moment, the icons for the different categories is unknown.
|
||||
This will be handled later. */
|
||||
export function getCategoryIconName(category?: PointOfInterestCategory | null) {
|
||||
switch (category) {
|
||||
case "Transportations":
|
||||
return IconName.Train
|
||||
case "Shopping":
|
||||
return IconName.Shopping
|
||||
case "Museum":
|
||||
return IconName.Museum
|
||||
case "Tourist":
|
||||
return IconName.Cultural
|
||||
case "Restaurant":
|
||||
return IconName.Restaurant
|
||||
default:
|
||||
return IconName.StarFilled
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,7 @@ export default function StaticMap({
|
||||
zoomLevel = 14,
|
||||
mapType = "roadmap",
|
||||
altText,
|
||||
mapId,
|
||||
}: StaticMapProps) {
|
||||
const key = env.GOOGLE_STATIC_MAP_KEY
|
||||
const secret = env.GOOGLE_STATIC_MAP_SIGNATURE_SECRET
|
||||
@@ -27,6 +28,11 @@ export default function StaticMap({
|
||||
const url = new URL(
|
||||
`${baseUrl}?center=${center}&zoom=${zoomLevel}&size=${width}x${height}&maptype=${mapType}&key=${key}`
|
||||
)
|
||||
|
||||
if (mapId) {
|
||||
url.searchParams.append("map_id", mapId)
|
||||
}
|
||||
|
||||
const src = getUrlWithSignature(url, secret)
|
||||
|
||||
return <img src={src} alt={altText} />
|
||||
|
||||
@@ -6,11 +6,7 @@ import { getMembershipLevelObject } from "@/utils/membershipLevel"
|
||||
import { getMembership } from "@/utils/user"
|
||||
|
||||
import PointsContainer from "./Container"
|
||||
import {
|
||||
NextLevelNightsColumn,
|
||||
NextLevelPointsColumn,
|
||||
YourPointsColumn,
|
||||
} from "./PointsColumn"
|
||||
import { NextLevelPointsColumn, YourPointsColumn } from "./PointsColumn"
|
||||
|
||||
import { UserProps } from "@/types/components/myPages/user"
|
||||
|
||||
@@ -32,7 +28,8 @@ export default async function Points({ user }: UserProps) {
|
||||
subtitle={`${formatMessage({ id: "next level:" })} ${nextLevel.name}`}
|
||||
/>
|
||||
)}
|
||||
{membership?.nightsToTopTier && (
|
||||
{/* TODO: Show NextLevelNightsColumn when nightsToTopTier data is correct from Antavo */}
|
||||
{/* {membership?.nightsToTopTier && (
|
||||
<NextLevelNightsColumn
|
||||
nights={membership.nightsToTopTier}
|
||||
subtitle={
|
||||
@@ -40,7 +37,7 @@ export default async function Points({ user }: UserProps) {
|
||||
`by ${membership.tierExpirationDate}`
|
||||
}
|
||||
/>
|
||||
)}
|
||||
)} */}
|
||||
</PointsContainer>
|
||||
)
|
||||
}
|
||||
|
||||
45
components/SidePeekProvider/index.tsx
Normal file
45
components/SidePeekProvider/index.tsx
Normal file
@@ -0,0 +1,45 @@
|
||||
"use client"
|
||||
import { usePathname, useRouter, useSearchParams } from "next/navigation"
|
||||
import { createContext, useEffect, useState } from "react"
|
||||
|
||||
interface ISidePeekContext {
|
||||
handleClose: (isOpen: boolean) => void
|
||||
activeSidePeek: string | null
|
||||
}
|
||||
|
||||
export const SidePeekContext = createContext<ISidePeekContext | null>(null)
|
||||
|
||||
function SidePeekProvider({ children }: React.PropsWithChildren) {
|
||||
const router = useRouter()
|
||||
const pathname = usePathname()
|
||||
const searchParams = useSearchParams()
|
||||
const [activeSidePeek, setActiveSidePeek] = useState<string | null>(() => {
|
||||
const sidePeekParam = searchParams.get("s")
|
||||
return sidePeekParam || null
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
const sidePeekParam = searchParams.get("s")
|
||||
if (sidePeekParam !== activeSidePeek) {
|
||||
setActiveSidePeek(sidePeekParam)
|
||||
}
|
||||
}, [searchParams, activeSidePeek])
|
||||
|
||||
function handleClose(isOpen: boolean) {
|
||||
if (!isOpen) {
|
||||
const nextSearchParams = new URLSearchParams(searchParams.toString())
|
||||
nextSearchParams.delete("s")
|
||||
|
||||
router.push(`${pathname}?${nextSearchParams}`, { scroll: false })
|
||||
setActiveSidePeek(null)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<SidePeekContext.Provider value={{ handleClose, activeSidePeek }}>
|
||||
{children}
|
||||
</SidePeekContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export default SidePeekProvider
|
||||
@@ -75,7 +75,7 @@ export default function Link({
|
||||
trackPageViewStart()
|
||||
startTransition(() => {
|
||||
startRouterTransition()
|
||||
router.push(href)
|
||||
router.push(href, { scroll })
|
||||
})
|
||||
}}
|
||||
href={href}
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import { PropsWithChildren } from "react"
|
||||
|
||||
import { CloseIcon } from "@/components/Icons"
|
||||
import { SidePeekContentProps } from "@/components/TempDesignSystem/SidePeek/types"
|
||||
import Title from "@/components/TempDesignSystem/Text/Title"
|
||||
|
||||
import Button from "../../Button"
|
||||
|
||||
import styles from "./sidePeekItem.module.css"
|
||||
|
||||
function SidePeekItem({
|
||||
title,
|
||||
children,
|
||||
isActive = false,
|
||||
onClose,
|
||||
}: PropsWithChildren<SidePeekContentProps>) {
|
||||
return isActive ? (
|
||||
<aside className={styles.sidePeekItem}>
|
||||
<header className={styles.header}>
|
||||
<Title color="burgundy" textTransform="uppercase" level="h2" as="h3">
|
||||
{title}
|
||||
</Title>
|
||||
<Button
|
||||
intent="text"
|
||||
onClick={() => {
|
||||
onClose && onClose()
|
||||
}}
|
||||
>
|
||||
<CloseIcon color="burgundy" height={32} width={32} />
|
||||
</Button>
|
||||
</header>
|
||||
{children}
|
||||
</aside>
|
||||
) : null
|
||||
}
|
||||
|
||||
export default SidePeekItem
|
||||
@@ -1,27 +0,0 @@
|
||||
.sidePeekItem {
|
||||
display: grid;
|
||||
grid-template-rows: min-content auto;
|
||||
gap: var(--Spacing-x4);
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.content>* {
|
||||
padding: var(--Spacing-x3) var(--Spacing-x2);
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
border-bottom: 1px solid var(--Base-Border-Subtle);
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.header:has(> h2) {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 1367px) {
|
||||
.content>* {
|
||||
padding: var(--Spacing-x4);
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,20 @@
|
||||
"use client"
|
||||
|
||||
import { useIsSSR } from "@react-aria/ssr"
|
||||
import React, { Children, cloneElement } from "react"
|
||||
import { useContext } from "react"
|
||||
import {
|
||||
Dialog,
|
||||
DialogTrigger,
|
||||
Modal,
|
||||
ModalOverlay,
|
||||
} from "react-aria-components"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import { SidePeekContentKey } from "@/components/TempDesignSystem/SidePeek/types"
|
||||
import { CloseIcon } from "@/components/Icons"
|
||||
import { SidePeekContext } from "@/components/SidePeekProvider"
|
||||
|
||||
import Button from "../Button"
|
||||
import Title from "../Text/Title"
|
||||
|
||||
import styles from "./sidePeek.module.css"
|
||||
|
||||
@@ -17,33 +22,61 @@ import type { SidePeekProps } from "./sidePeek"
|
||||
|
||||
function SidePeek({
|
||||
children,
|
||||
title,
|
||||
contentKey,
|
||||
handleClose,
|
||||
activeSidePeek,
|
||||
isOpen,
|
||||
}: React.PropsWithChildren<SidePeekProps>) {
|
||||
const sidePeekChildren = Children.map(children, (child) => {
|
||||
if (!React.isValidElement(child)) {
|
||||
return child
|
||||
}
|
||||
return cloneElement(child as React.ReactElement, {
|
||||
isActive:
|
||||
(child.props.contentKey as SidePeekContentKey) === activeSidePeek,
|
||||
onClose: handleClose,
|
||||
})
|
||||
})
|
||||
|
||||
const isSSR = useIsSSR()
|
||||
return isSSR ? (
|
||||
<div>{children}</div>
|
||||
) : (
|
||||
const intl = useIntl()
|
||||
const context = useContext(SidePeekContext)
|
||||
function onClose() {
|
||||
const closeHandler = handleClose || context?.handleClose
|
||||
closeHandler && closeHandler(false)
|
||||
}
|
||||
|
||||
if (isSSR) {
|
||||
return (
|
||||
<div>
|
||||
<h2>{title}</h2>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<DialogTrigger>
|
||||
<ModalOverlay
|
||||
className={styles.overlay}
|
||||
isOpen={!!activeSidePeek}
|
||||
onOpenChange={handleClose}
|
||||
isOpen={isOpen || contentKey === context?.activeSidePeek}
|
||||
onOpenChange={onClose}
|
||||
isDismissable
|
||||
>
|
||||
<Modal className={styles.sidePeek}>
|
||||
<Dialog className={styles.dialog}>{sidePeekChildren}</Dialog>
|
||||
<Modal className={styles.modal}>
|
||||
<Dialog className={styles.dialog}>
|
||||
<aside className={styles.sidePeek}>
|
||||
<header className={styles.header}>
|
||||
{title ? (
|
||||
<Title
|
||||
color="burgundy"
|
||||
textTransform="uppercase"
|
||||
level="h2"
|
||||
as="h3"
|
||||
>
|
||||
{title}
|
||||
</Title>
|
||||
) : null}
|
||||
<Button
|
||||
aria-label={intl.formatMessage({ id: "Close" })}
|
||||
className={styles.closeButton}
|
||||
intent="text"
|
||||
onPress={onClose}
|
||||
>
|
||||
<CloseIcon color="burgundy" height={32} width={32} />
|
||||
</Button>
|
||||
</header>
|
||||
<div className={styles.sidePeekContent}>{children}</div>
|
||||
</aside>
|
||||
</Dialog>
|
||||
</Modal>
|
||||
</ModalOverlay>
|
||||
</DialogTrigger>
|
||||
|
||||
@@ -1,38 +1,9 @@
|
||||
.sidePeek {
|
||||
position: fixed;
|
||||
top: var(--current-mobile-site-header-height);
|
||||
right: auto;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
height: calc(100vh - var(--current-mobile-site-header-height));
|
||||
background-color: var(--Base-Background-Primary-Normal);
|
||||
z-index: 100;
|
||||
box-shadow: 0 0 10px rgba(0, 0, 0, 0.85);
|
||||
}
|
||||
|
||||
.sidePeek[data-entering] {
|
||||
animation: slide-up 300ms;
|
||||
}
|
||||
|
||||
.sidePeek[data-exiting] {
|
||||
animation: slide-up 300ms reverse;
|
||||
}
|
||||
|
||||
.dialog {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.overlay {
|
||||
position: absolute;
|
||||
top: var(--current-mobile-site-header-height);
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 99;
|
||||
.modal {
|
||||
--sidepeek-desktop-width: 600px;
|
||||
}
|
||||
@keyframes slide-in {
|
||||
from {
|
||||
right: -600px;
|
||||
right: calc(-1 * var(--sidepeek-desktop-width));
|
||||
}
|
||||
|
||||
to {
|
||||
@@ -46,24 +17,84 @@
|
||||
}
|
||||
|
||||
to {
|
||||
top: var(--current-mobile-site-header-height);
|
||||
top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 99;
|
||||
}
|
||||
|
||||
.modal {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: auto;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
background-color: var(--Base-Background-Primary-Normal);
|
||||
z-index: 100;
|
||||
box-shadow: 0 0 10px rgba(0, 0, 0, 0.85);
|
||||
}
|
||||
|
||||
.modal[data-entering] {
|
||||
animation: slide-up 300ms;
|
||||
}
|
||||
|
||||
.modal[data-exiting] {
|
||||
animation: slide-up 300ms reverse;
|
||||
}
|
||||
|
||||
.dialog {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.sidePeek {
|
||||
display: grid;
|
||||
grid-template-rows: min-content auto;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
border-bottom: 1px solid var(--Base-Border-Subtle);
|
||||
align-items: center;
|
||||
padding: var(--Spacing-x4);
|
||||
}
|
||||
|
||||
.header:has(> h2) {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.closeButton {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.sidePeekContent {
|
||||
padding: var(--Spacing-x4);
|
||||
}
|
||||
@media screen and (min-width: 1367px) {
|
||||
.sidePeek {
|
||||
.modal {
|
||||
top: 0;
|
||||
right: 0px;
|
||||
width: 600px;
|
||||
width: var(--sidepeek-desktop-width);
|
||||
height: 100vh;
|
||||
}
|
||||
.sidePeek[data-entering] {
|
||||
|
||||
.modal[data-entering] {
|
||||
animation: slide-in 250ms;
|
||||
}
|
||||
|
||||
.sidePeek[data-exiting] {
|
||||
.modal[data-exiting] {
|
||||
animation: slide-in 250ms reverse;
|
||||
}
|
||||
|
||||
.overlay {
|
||||
top: 0;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
export interface SidePeekProps {
|
||||
handleClose: (isOpen: boolean) => void
|
||||
activeSidePeek: string | null
|
||||
contentKey: string
|
||||
title: string
|
||||
isOpen?: boolean
|
||||
handleClose?: (isOpen: boolean) => void
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
export type SidePeekContentKey = string
|
||||
|
||||
export type SidePeekProps = {
|
||||
activeContent: string | null
|
||||
onClose: (isOpen: boolean) => void
|
||||
@@ -7,7 +5,7 @@ export type SidePeekProps = {
|
||||
|
||||
export type SidePeekContentProps = {
|
||||
title?: string
|
||||
contentKey: SidePeekContentKey
|
||||
contentKey: string
|
||||
isActive?: boolean
|
||||
onClose?: () => void
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user