feat(SW-189): added dynamic map

This commit is contained in:
Erik Tiekstra
2024-09-10 13:05:24 +02:00
parent 50d74648b5
commit 789133af11
28 changed files with 700 additions and 147 deletions

View File

@@ -4,4 +4,5 @@
font-family: var(--typography-Body-Regular-fontFamily);
gap: var(--Spacing-x3);
grid-template-rows: auto 1fr;
position: relative;
}

View File

@@ -106,11 +106,13 @@
--max-width-navigation: 89.5rem;
--main-menu-mobile-height: 75px;
--main-menu-desktop-height: 118px;
--hotel-page-map-desktop-width: 23.75rem;
/* Z-INDEX */
--header-z-index: 10;
--menu-overlay-z-index: 10;
--hotel-page-map-desktop-width: 23.75rem;
--dialog-z-index: 9;
}
* {

View File

@@ -0,0 +1,133 @@
"use client"
import { AdvancedMarker, Map, useMap } from "@vis.gl/react-google-maps"
import { useState } from "react"
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 ScandicMarker from "../Markers/Scandic"
import styles from "./dynamicMap.module.css"
export default function DynamicMapContent({
hotelName,
coordinates,
}: {
hotelName: string
coordinates: { lat: number; lng: number }
}) {
const { isDynamicMapOpen, closeDynamicMap } = useHotelPageStore()
const [isFullScreenSidebar, setIsFullScreenSidebar] = useState(false)
const map = useMap()
const mapOptions = {
defaultZoom: 15,
defaultCenter: coordinates,
disableDefaultUI: true,
clickableIcons: false,
mapId: `${hotelName}-map`,
// 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>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="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="Zoom in"
>
<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}
>
{isFullScreenSidebar ? "View as map" : "View as list"}
</button>
</div>
<div className={styles.sidebarContent}>
<Title as="h4" level="h2" textTransform="regular">
Things nearby {hotelName}
</Title>
<Divider color="subtle" />
</div>
</aside>
<div className={styles.mapContainer}>
<Map {...mapOptions}>
<AdvancedMarker position={coordinates}>
<ScandicMarker />
</AdvancedMarker>
</Map>
</div>
</>
)
}

View File

@@ -0,0 +1,156 @@
.dynamicMap {
--sidebar-height: 88px;
position: fixed;
top: var(--main-menu-mobile-height);
right: 0;
bottom: 0;
left: 0;
z-index: var(--dialog-z-index);
display: flex;
}
.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: 500;
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-items: 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: 40px;
padding: 0;
pointer-events: initial;
box-shadow: 0 0 8px 1px rgba(0, 0, 0, 0.1);
}
/* @media screen and (max-width: 767px) {
.sidebar:not(.fullscreen) .sidebarContent {
display: none;
}
} */
@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;
}
}

View File

@@ -0,0 +1,42 @@
"use client"
import { APIProvider } from "@vis.gl/react-google-maps"
import { Dialog, Modal } from "react-aria-components"
import useHotelPageStore from "@/stores/hotel-page"
import { useHandleKeyUp } from "@/hooks/useHandleKeyUp"
import DynamicMapContent from "./Content"
import styles from "./dynamicMap.module.css"
export default function DynamicMap({
apiKey,
hotelName,
coordinates,
}: {
apiKey: string
hotelName: string
coordinates: { lat: number; lng: number }
}) {
const { isDynamicMapOpen, closeDynamicMap } = useHotelPageStore()
useHandleKeyUp((event: KeyboardEvent) => {
if (event.key === "Escape" && isDynamicMapOpen) {
closeDynamicMap()
}
})
return (
<APIProvider apiKey={apiKey}>
<Modal isOpen={isDynamicMapOpen}>
<Dialog
className={styles.dynamicMap}
aria-label={`Things nearby ${hotelName}`}
>
<DynamicMapContent hotelName={hotelName} coordinates={coordinates} />
</Dialog>
</Modal>
</APIProvider>
)
}

View File

@@ -1,5 +1,7 @@
"use client"
import useHotelPageStore from "@/stores/hotel-page"
import Button from "@/components/TempDesignSystem/Button"
import Caption from "@/components/TempDesignSystem/Text/Caption"
import Title from "@/components/TempDesignSystem/Text/Title"
@@ -9,9 +11,15 @@ import styles from "./mapCard.module.css"
import { MapCardProps } from "@/types/components/hotelPage/mapCard"
export default function MapCard({ hotelName }: MapCardProps) {
const { openDynamicMap } = useHotelPageStore()
return (
<div className={styles.mapCard}>
<Caption color="burgundy" textTransform="uppercase" textAlign="center">
<Caption
color="textMediumContrast"
textTransform="uppercase"
textAlign="center"
>
Nearby
</Caption>
<Title
@@ -29,6 +37,7 @@ export default function MapCard({ hotelName }: MapCardProps) {
intent="secondary"
size="small"
className={styles.ctaButton}
onClick={openDynamicMap}
>
Explore nearby
</Button>

View File

@@ -1,12 +1,13 @@
.mapCard {
display: grid;
position: absolute;
bottom: 15%;
left: var(--Spacing-x2);
right: var(--Spacing-x2);
background-color: var(--Base-Surface-Primary-light-Normal);
margin: 0 var(--Spacing-x4);
padding: var(--Spacing-x2);
box-shadow: 0 0 2.5rem 0 rgba(0, 0, 0, 0.12);
border-radius: var(--Corner-radius-Medium);
display: grid;
}
.ctaButton {

View File

@@ -0,0 +1,153 @@
export default function ScandicMarker({
className,
...props
}: React.SVGAttributes<HTMLOrSVGElement>) {
return (
<svg
className={className}
height="100"
viewBox="0 0 85 108"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<g filter="url(#filter0_d_1623_21496)">
<path
fillRule="evenodd"
clipRule="evenodd"
d="M52.842 72.1048C64.7996 67.7006 73.3378 56.108 73.3378 42.5C73.3378 25.103 59.383 11 42.1689 11C24.9548 11 11 25.103 11 42.5C11 56.6689 20.2565 68.6528 32.9914 72.6123L41.236 86.0475C42.0252 87.3336 43.9 87.3158 44.6647 86.015L52.842 72.1048Z"
fill="white"
/>
<circle cx="42" cy="43" r="27" fill="#CD0921" />
</g>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M34.1848 46.7474C33.592 46.8953 32.9991 46.9446 32.4062 46.9446C30.2818 46.9446 28.8984 45.9587 28.8984 43.6418C28.8984 41.4234 30.3312 40.4868 32.3568 40.4868C32.9497 40.4868 33.5426 40.5361 34.1354 40.6347V41.7192C33.6908 41.6699 33.3449 41.6206 32.9497 41.6206C31.3193 41.6206 30.5288 42.1136 30.5288 43.7404C30.5288 45.2685 31.2699 45.8108 32.9497 45.8108C33.3944 45.8108 33.7402 45.7615 34.1354 45.7122V46.7474H34.1848Z"
fill="white"
/>
<path
d="M56.0547 46.7961H57.6357V40.5848H56.0547V46.7961ZM56.0547 39.3524H57.6357V37.9229H56.0547V39.3524Z"
fill="white"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M54.7828 46.6483C54.4863 46.6975 53.3994 46.8947 52.3125 46.8947C50.2869 46.8947 48.8047 45.9581 48.8047 43.6905C48.8047 41.5215 50.1386 40.437 52.1643 40.437C52.4607 40.437 52.9548 40.4862 53.2018 40.4862V37.9229H54.7828C54.7828 38.2186 54.7828 46.5004 54.7828 46.6483ZM53.2512 45.7116V41.6694C53.0536 41.6694 52.7077 41.6201 52.3619 41.6201C50.9291 41.6201 50.3857 42.3102 50.3857 43.7398C50.3857 45.0215 50.8797 45.8102 52.3125 45.8102C52.6089 45.8102 52.9054 45.7609 53.2512 45.7116Z"
fill="white"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M47.7727 46.7967H46.2411V42.7051C46.2411 42.015 45.9446 41.6206 44.9565 41.6206C44.2648 41.6206 43.5238 41.7685 43.5238 41.7685V46.7967H41.9922V40.7826C42.091 40.7826 44.0672 40.4868 45.2036 40.4868C46.5869 40.4868 47.8221 40.7333 47.8221 42.6065V46.7967H47.7727Z"
fill="white"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M40.6998 46.7954C39.6129 46.894 38.773 46.9432 37.8343 46.9432C36.2533 46.9432 34.9688 46.6968 34.9688 44.9221C34.9688 43.1968 36.5497 43.0982 37.9825 43.0489C38.3283 43.0489 38.7236 43.0489 39.1188 43.0489C39.1188 41.9643 39.0694 41.57 37.4884 41.57C36.6979 41.57 35.8581 41.7179 35.364 41.8165V40.6827C35.9569 40.5348 36.8956 40.4855 37.5873 40.4362C39.3659 40.4362 40.6998 40.6334 40.6998 42.5066V46.7954ZM39.1188 45.8587V44.0841C38.8224 44.0841 38.2789 44.0841 38.0813 44.0841C37.1426 44.0841 36.5497 44.1334 36.5497 45.0207C36.5497 45.908 37.2908 45.9573 38.0319 45.9573C38.4766 45.908 38.7236 45.908 39.1188 45.8587Z"
fill="white"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M63.927 46.7474C63.3342 46.8953 62.7413 46.9446 62.1484 46.9446C60.024 46.9446 58.6406 45.9587 58.6406 43.6418C58.6406 41.4234 60.0734 40.4868 62.099 40.4868C62.6919 40.4868 63.2848 40.5361 63.8776 40.6347V41.7192C63.433 41.6699 63.0871 41.6206 62.6919 41.6206C61.0615 41.6206 60.271 42.1136 60.271 43.7404C60.271 45.2685 61.0121 45.8108 62.6919 45.8108C63.1365 45.8108 63.4824 45.7615 63.8776 45.7122V46.7474H63.927Z"
fill="white"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M28.1054 44.3303C28.1054 46.1542 26.6726 46.943 24.5482 46.943C23.1648 46.943 22.1767 46.6965 22.0285 46.6472V45.3655C22.5226 45.4641 23.5601 45.7106 24.4494 45.7106C25.3387 45.7106 26.475 45.5134 26.475 44.5275C26.475 43.8866 25.981 43.6401 25.0917 43.3936C24.7458 43.2951 24.4 43.1965 24.0541 43.0979C22.8684 42.7528 21.9297 42.2598 21.9297 40.8796C21.9297 38.9077 23.5107 38.4147 25.3387 38.4147C26.4256 38.4147 27.3149 38.6119 27.4631 38.6612V39.8443C27.2655 39.795 26.4256 39.5979 25.4869 39.5979C24.5976 39.5979 23.5601 39.6964 23.5601 40.6824C23.5601 41.4218 24.1529 41.619 24.9928 41.8162C25.3881 41.9641 25.7833 42.0134 26.1786 42.1612C27.2161 42.457 28.1054 42.95 28.1054 44.3303Z"
fill="white"
/>
<g filter="url(#filter1_d_1623_21496)">
<rect x="37" y="93" width="12" height="12" rx="6" fill="white" />
<circle cx="43.1172" cy="99" r="4" fill="#CD0921" />
</g>
<defs>
<filter
id="filter0_d_1623_21496"
x="0"
y="0"
width="84.3379"
height="98.0014"
filterUnits="userSpaceOnUse"
colorInterpolationFilters="sRGB"
>
<feFlood floodOpacity="0" result="BackgroundImageFix" />
<feColorMatrix
in="SourceAlpha"
type="matrix"
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
result="hardAlpha"
/>
<feMorphology
radius="3"
operator="dilate"
in="SourceAlpha"
result="effect1_dropShadow_1623_21496"
/>
<feOffset />
<feGaussianBlur stdDeviation="4" />
<feComposite in2="hardAlpha" operator="out" />
<feColorMatrix
type="matrix"
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.25 0"
/>
<feBlend
mode="normal"
in2="BackgroundImageFix"
result="effect1_dropShadow_1623_21496"
/>
<feBlend
mode="normal"
in="SourceGraphic"
in2="effect1_dropShadow_1623_21496"
result="shape"
/>
</filter>
<filter
id="filter1_d_1623_21496"
x="34"
y="90"
width="18"
height="18"
filterUnits="userSpaceOnUse"
colorInterpolationFilters="sRGB"
>
<feFlood floodOpacity="0" result="BackgroundImageFix" />
<feColorMatrix
in="SourceAlpha"
type="matrix"
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
result="hardAlpha"
/>
<feMorphology
radius="1"
operator="dilate"
in="SourceAlpha"
result="effect1_dropShadow_1623_21496"
/>
<feOffset />
<feGaussianBlur stdDeviation="1" />
<feComposite in2="hardAlpha" operator="out" />
<feColorMatrix
type="matrix"
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.1 0"
/>
<feBlend
mode="normal"
in2="BackgroundImageFix"
result="effect1_dropShadow_1623_21496"
/>
<feBlend
mode="normal"
in="SourceGraphic"
in2="effect1_dropShadow_1623_21496"
result="shape"
/>
</filter>
</defs>
</svg>
)
}

View File

@@ -0,0 +1,43 @@
"use client"
import useHotelPageStore from "@/stores/hotel-page"
import { HouseIcon, LocationIcon } from "@/components/Icons"
import styles from "./mobileToggle.module.css"
export default function MobileMapToggle() {
const { isDynamicMapOpen, openDynamicMap, closeDynamicMap } =
useHotelPageStore()
return (
<div className={styles.mobileToggle}>
<button
type="button"
className={`${styles.button} ${!isDynamicMapOpen ? styles.active : ""}`}
onClick={closeDynamicMap}
>
<HouseIcon
className={styles.icon}
color={!isDynamicMapOpen ? "white" : "red"}
height={24}
width={24}
/>
<span>Hotel</span>
</button>
<button
type="button"
className={`${styles.button} ${isDynamicMapOpen ? styles.active : ""}`}
onClick={openDynamicMap}
>
<LocationIcon
className={styles.icon}
color={isDynamicMapOpen ? "white" : "red"}
height={24}
width={24}
/>
<span>Nearby</span>
</button>
</div>
)
}

View File

@@ -1,4 +1,9 @@
.mobileToggle {
position: fixed;
bottom: var(--Spacing-x5);
left: 50%;
transform: translateX(-50%);
z-index: 1;
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: var(--Spacing-x-half);

View File

@@ -1,51 +0,0 @@
"use client"
import { HouseIcon, LocationIcon } from "@/components/Icons"
import styles from "./mobileToggle.module.css"
import { IconName } from "@/types/components/icon"
export default function MobileToggle() {
const options: { text: string; icon: IconName }[] = [
{
text: "Hotel",
icon: IconName.House,
},
{
text: "Nearby",
icon: IconName.Location,
},
]
function onToggle() {
console.log("Toggle mobile map")
}
return (
<div className={styles.mobileToggle}>
<button
type="button"
className={`${styles.button} ${styles.active}`}
onClick={onToggle}
>
<HouseIcon
className={styles.icon}
color={"white"}
height={24}
width={24}
/>
<span>Hotel</span>
</button>
<button type="button" className={styles.button} onClick={onToggle}>
<LocationIcon
className={styles.icon}
color={"red"}
height={24}
width={24}
/>
<span>Nearby</span>
</button>
</div>
)
}

View File

@@ -4,14 +4,17 @@ import { env } from "@/env/server"
import { getIntl } from "@/i18n"
import ScandicMarker from "../Markers/Scandic"
import { calculateLatWithOffset, getUrlWithSignature } from "./util"
import styles from "./staticMap.module.css"
import { StaticMapProps } from "@/types/components/hotelPage/staticMap"
export default async function StaticMap({
coordinates,
hotelName,
zoomLevel = 16,
zoomLevel = 14,
}: StaticMapProps) {
const intl = await getIntl()
const key = env.GOOGLE_STATIC_MAP_KEY
@@ -19,8 +22,11 @@ export default async function StaticMap({
const baseUrl = "https://maps.googleapis.com/maps/api/staticmap"
const { lng, lat } = coordinates
const mapHeight = 785
const markerHeight = 100
const mapLatitudeInPx = mapHeight * 0.2
const size = `380x${mapHeight}`
const mapLatitude = calculateLatWithOffset(lat, mapHeight * 0.2, zoomLevel)
const mapLatitude = calculateLatWithOffset(lat, mapLatitudeInPx, zoomLevel)
// Custom Icon should be available from a public URL accessible by Google Static Maps API. At the moment, we don't have a public URL for the custom icon.
// const marker = `icon:https://IMAGE_URL|${lat},${lng}`
const marker = `${lat},${lng}`
@@ -28,10 +34,22 @@ export default async function StaticMap({
const alt = intl.formatMessage({ id: "Map of HOTEL_NAME" }, { hotelName })
const url = new URL(
`${baseUrl}?zoom=${zoomLevel}&center=${mapLatitude},${lng}&size=${size}&markers=${marker}&key=${key}`
`${baseUrl}?zoom=${zoomLevel}&center=${mapLatitude},${lng}&size=${size}&key=${key}`
)
// const url = new URL(
// `${baseUrl}?zoom=${zoomLevel}&center=${mapLatitude},${lng}&size=${size}&markers=${marker}&key=${key}`
// )
const src = getUrlWithSignature(url, secret)
return <img src={src} alt={alt} />
return (
<div className={styles.staticMap}>
<img src={src} alt={alt} />
<ScandicMarker
className={styles.mapMarker}
height={markerHeight}
style={{ top: `${mapLatitudeInPx - markerHeight}px` }}
/>
</div>
)
}

View File

@@ -0,0 +1,9 @@
.staticMap {
position: relative;
}
.mapMarker {
position: absolute;
left: 50%;
transform: translateX(-50%);
}

View File

@@ -1,17 +0,0 @@
import MapCard from "./MapCard"
import MobileToggle from "./MobileToggle"
import StaticMap from "./StaticMap"
import styles from "./map.module.css"
export default function Map({ coordinates, hotelName }: any) {
return (
<>
<div className={styles.desktopContent}>
<StaticMap coordinates={coordinates} hotelName={hotelName} />
<MapCard hotelName={hotelName} />
</div>
<MobileToggle />
</>
)
}

View File

@@ -1,12 +0,0 @@
.desktopContent {
display: none;
}
@media screen and (min-width: 1367px) {
.desktopContent {
align-self: start;
position: relative;
display: grid;
justify-items: center;
}
}

View File

@@ -1,7 +1,7 @@
.stickyWrapper {
position: sticky;
top: 0;
z-index: 10;
z-index: 1;
background-color: var(--Base-Surface-Subtle-Normal);
border-bottom: 1px solid var(--Base-Border-Subtle);
overflow-x: auto;

View File

@@ -22,12 +22,7 @@
}
.mapContainer {
position: fixed;
bottom: var(--Spacing-x5);
display: flex;
justify-content: center;
width: 100vw;
z-index: 1;
display: none;
}
.introContainer {
@@ -48,13 +43,13 @@
padding: var(--Spacing-x6) 0;
}
.mapContainer {
display: flex;
grid-area: mapContainer;
align-self: start;
position: sticky;
top: 0;
align-self: start;
justify-content: initial;
justify-content: center;
width: 100%;
z-index: 0;
}
.pageContainer > nav {

View File

@@ -1,3 +1,4 @@
import { env } from "@/env/server"
import { serverClient } from "@/lib/trpc/server"
import AmenitiesList from "./AmenitiesList"
@@ -5,7 +6,10 @@ import Facilities from "./Facilities"
import { MOCK_FACILITIES } from "./Facilities/mockData"
import { setActivityCard } from "./Facilities/utils"
import IntroSection from "./IntroSection"
import Map from "./Map"
import DynamicMap from "./Map/DynamicMap"
import MapCard from "./Map/MapCard"
import MobileMapToggle from "./Map/MobileMapToggle"
import StaticMap from "./Map/StaticMap"
import PreviewImages from "./PreviewImages"
import { Rooms } from "./Rooms"
import SidePeeks from "./SidePeeks"
@@ -14,6 +18,7 @@ import TabNavigation from "./TabNavigation"
import styles from "./hotelPage.module.css"
export default async function HotelPage() {
const googleMapsApiKey = env.GOOGLE_STATIC_MAP_KEY
const hotelData = await serverClient().hotel.get({
include: ["RoomCategories"],
})
@@ -61,9 +66,21 @@ export default async function HotelPage() {
<Rooms rooms={roomCategories} />
<Facilities facilities={facilities} />
</main>
<aside className={styles.mapContainer}>
<Map coordinates={coordinates} hotelName={hotelName} />
</aside>
{googleMapsApiKey ? (
<>
<aside className={styles.mapContainer}>
{/* <Map coordinates={coordinates} hotelName={hotelName} /> */}
<StaticMap coordinates={coordinates} hotelName={hotelName} />
<MapCard hotelName={hotelName} />
</aside>
<MobileMapToggle />
<DynamicMap
apiKey={googleMapsApiKey}
hotelName={hotelName}
coordinates={coordinates}
/>
</>
) : null}
<SidePeeks />
</div>
)

View File

@@ -0,0 +1,23 @@
import { iconVariants } from "./variants"
import type { IconProps } from "@/types/components/icon"
export default function MinusIcon({ className, color, ...props }: IconProps) {
const classNames = iconVariants({ className, color })
return (
<svg
className={classNames}
xmlns="http://www.w3.org/2000/svg"
width="14"
height="2"
viewBox="0 0 14 2"
fill="none"
{...props}
>
<path
d="M1.125 1.86249C0.877083 1.86249 0.669267 1.77842 0.50155 1.61029C0.33385 1.44217 0.25 1.23384 0.25 0.985287C0.25 0.736754 0.33385 0.529154 0.50155 0.362488C0.669267 0.195821 0.877083 0.112488 1.125 0.112488H12.875C13.1229 0.112488 13.3307 0.196555 13.4985 0.364688C13.6662 0.532805 13.75 0.741138 13.75 0.989688C13.75 1.23822 13.6662 1.44582 13.4985 1.61249C13.3307 1.77915 13.1229 1.86249 12.875 1.86249H1.125Z"
fill="#4D001B"
/>
</svg>
)
}

23
components/Icons/Plus.tsx Normal file
View File

@@ -0,0 +1,23 @@
import { iconVariants } from "./variants"
import type { IconProps } from "@/types/components/icon"
export default function PlusIcon({ className, color, ...props }: IconProps) {
const classNames = iconVariants({ className, color })
return (
<svg
className={classNames}
xmlns="http://www.w3.org/2000/svg"
width="14"
height="14"
viewBox="0 0 14 14"
fill="none"
{...props}
>
<path
d="M6.125 7.86249H1.125C0.877083 7.86249 0.669267 7.77842 0.50155 7.61029C0.33385 7.44217 0.25 7.23384 0.25 6.98529C0.25 6.73675 0.33385 6.52915 0.50155 6.36249C0.669267 6.19582 0.877083 6.11249 1.125 6.11249H6.125V1.11249C6.125 0.864571 6.20907 0.656755 6.3772 0.489038C6.54532 0.321338 6.75365 0.237488 7.0022 0.237488C7.25073 0.237488 7.45833 0.321338 7.625 0.489038C7.79167 0.656755 7.875 0.864571 7.875 1.11249V6.11249H12.875C13.1229 6.11249 13.3307 6.19656 13.4985 6.36469C13.6662 6.5328 13.75 6.74114 13.75 6.98969C13.75 7.23822 13.6662 7.44582 13.4985 7.61249C13.3307 7.77915 13.1229 7.86249 12.875 7.86249H7.875V12.8625C7.875 13.1104 7.79093 13.3182 7.6228 13.4859C7.45468 13.6536 7.24635 13.7375 6.9978 13.7375C6.74927 13.7375 6.54167 13.6536 6.375 13.4859C6.20833 13.3182 6.125 13.1104 6.125 12.8625V7.86249Z"
fill="#26201E"
/>
</svg>
)
}

View File

@@ -33,12 +33,14 @@ import {
InfoCircleIcon,
LocationIcon,
LockIcon,
MinusIcon,
ParkingIcon,
People2Icon,
PersonIcon,
PetsIcon,
PhoneIcon,
PlusCircleIcon,
PlusIcon,
RestaurantIcon,
SaunaIcon,
SearchIcon,
@@ -114,6 +116,8 @@ export function getIconByIconName(icon?: IconName): FC<IconProps> | null {
return LocationIcon
case IconName.Lock:
return LockIcon
case IconName.Minus:
return MinusIcon
case IconName.Parking:
return ParkingIcon
case IconName.Person:
@@ -124,6 +128,8 @@ export function getIconByIconName(icon?: IconName): FC<IconProps> | null {
return PetsIcon
case IconName.Phone:
return PhoneIcon
case IconName.Plus:
return PlusIcon
case IconName.PlusCircle:
return PlusCircleIcon
case IconName.Restaurant:

View File

@@ -29,11 +29,13 @@ export { default as ImageIcon } from "./Image"
export { default as InfoCircleIcon } from "./InfoCircle"
export { default as LocationIcon } from "./Location"
export { default as LockIcon } from "./Lock"
export { default as MinusIcon } from "./Minus"
export { default as ParkingIcon } from "./Parking"
export { default as People2Icon } from "./People2"
export { default as PersonIcon } from "./Person"
export { default as PetsIcon } from "./Pets"
export { default as PhoneIcon } from "./Phone"
export { default as PlusIcon } from "./Plus"
export { default as PlusCircleIcon } from "./PlusCircle"
export { default as PriceTagIcon } from "./PriceTag"
export { default as RestaurantIcon } from "./Restaurant"

View File

@@ -48,7 +48,7 @@
}
.textMediumContrast {
color: var(--UI-Text-Medium-contrast);
color: var(--Base-Text-Medium-contrast);
}
.red {

22
package-lock.json generated
View File

@@ -27,6 +27,7 @@
"@trpc/react-query": "^11.0.0-rc.467",
"@trpc/server": "^11.0.0-rc.467",
"@vercel/otel": "^1.9.1",
"@vis.gl/react-google-maps": "^1.1.3",
"class-variance-authority": "^0.7.0",
"clean-deep": "^3.4.0",
"dayjs": "^1.11.10",
@@ -6268,6 +6269,11 @@
"integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==",
"license": "MIT"
},
"node_modules/@types/google.maps": {
"version": "3.58.0",
"resolved": "https://registry.npmjs.org/@types/google.maps/-/google.maps-3.58.0.tgz",
"integrity": "sha512-rZPrbNHoGxeY70uuQYFLGQqcz5mLd3pZy0u286GSugvN7PLFsHNRF2wN2QXtUgNiC33IC0LX+MD3LGAC3wN7Eg=="
},
"node_modules/@types/graceful-fs": {
"version": "4.1.9",
"resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz",
@@ -6649,6 +6655,19 @@
"@opentelemetry/sdk-trace-base": "^1.19.0"
}
},
"node_modules/@vis.gl/react-google-maps": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@vis.gl/react-google-maps/-/react-google-maps-1.1.3.tgz",
"integrity": "sha512-jsjj5OpL5AyQDSfzBHU+r5UGAuZxv/7mw9gM2dP4EhqRXkXBpwDKKlml47/4l0m1WL4fSEXLzv85Mrh1VhPiFA==",
"dependencies": {
"@types/google.maps": "^3.54.10",
"fast-deep-equal": "^3.1.3"
},
"peerDependencies": {
"react": ">=16.8.0",
"react-dom": ">=16.8.0"
}
},
"node_modules/abab": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz",
@@ -9968,8 +9987,7 @@
"node_modules/fast-deep-equal": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
"dev": true
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="
},
"node_modules/fast-glob": {
"version": "3.3.2",

View File

@@ -43,6 +43,7 @@
"@trpc/react-query": "^11.0.0-rc.467",
"@trpc/server": "^11.0.0-rc.467",
"@vercel/otel": "^1.9.1",
"@vis.gl/react-google-maps": "^1.1.3",
"class-variance-authority": "^0.7.0",
"clean-deep": "^3.4.0",
"dayjs": "^1.11.10",

View File

@@ -1,41 +0,0 @@
<svg width="85" height="108" viewBox="0 0 85 108" fill="none" xmlns="http://www.w3.org/2000/svg">
<g filter="url(#filter0_d_1623_21496)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M52.842 72.1048C64.7996 67.7006 73.3378 56.108 73.3378 42.5C73.3378 25.103 59.383 11 42.1689 11C24.9548 11 11 25.103 11 42.5C11 56.6689 20.2565 68.6528 32.9914 72.6123L41.236 86.0475C42.0252 87.3336 43.9 87.3158 44.6647 86.015L52.842 72.1048Z" fill="white"/>
<circle cx="42" cy="43" r="27" fill="#CD0921"/>
</g>
<path fill-rule="evenodd" clip-rule="evenodd" d="M34.1848 46.7474C33.592 46.8953 32.9991 46.9446 32.4062 46.9446C30.2818 46.9446 28.8984 45.9587 28.8984 43.6418C28.8984 41.4234 30.3312 40.4868 32.3568 40.4868C32.9497 40.4868 33.5426 40.5361 34.1354 40.6347V41.7192C33.6908 41.6699 33.3449 41.6206 32.9497 41.6206C31.3193 41.6206 30.5288 42.1136 30.5288 43.7404C30.5288 45.2685 31.2699 45.8108 32.9497 45.8108C33.3944 45.8108 33.7402 45.7615 34.1354 45.7122V46.7474H34.1848Z" fill="white"/>
<path d="M56.0547 46.7961H57.6357V40.5848H56.0547V46.7961ZM56.0547 39.3524H57.6357V37.9229H56.0547V39.3524Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M54.7828 46.6483C54.4863 46.6975 53.3994 46.8947 52.3125 46.8947C50.2869 46.8947 48.8047 45.9581 48.8047 43.6905C48.8047 41.5215 50.1386 40.437 52.1643 40.437C52.4607 40.437 52.9548 40.4862 53.2018 40.4862V37.9229H54.7828C54.7828 38.2186 54.7828 46.5004 54.7828 46.6483ZM53.2512 45.7116V41.6694C53.0536 41.6694 52.7077 41.6201 52.3619 41.6201C50.9291 41.6201 50.3857 42.3102 50.3857 43.7398C50.3857 45.0215 50.8797 45.8102 52.3125 45.8102C52.6089 45.8102 52.9054 45.7609 53.2512 45.7116Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M47.7727 46.7967H46.2411V42.7051C46.2411 42.015 45.9446 41.6206 44.9565 41.6206C44.2648 41.6206 43.5238 41.7685 43.5238 41.7685V46.7967H41.9922V40.7826C42.091 40.7826 44.0672 40.4868 45.2036 40.4868C46.5869 40.4868 47.8221 40.7333 47.8221 42.6065V46.7967H47.7727Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M40.6998 46.7954C39.6129 46.894 38.773 46.9432 37.8343 46.9432C36.2533 46.9432 34.9688 46.6968 34.9688 44.9221C34.9688 43.1968 36.5497 43.0982 37.9825 43.0489C38.3283 43.0489 38.7236 43.0489 39.1188 43.0489C39.1188 41.9643 39.0694 41.57 37.4884 41.57C36.6979 41.57 35.8581 41.7179 35.364 41.8165V40.6827C35.9569 40.5348 36.8956 40.4855 37.5873 40.4362C39.3659 40.4362 40.6998 40.6334 40.6998 42.5066V46.7954ZM39.1188 45.8587V44.0841C38.8224 44.0841 38.2789 44.0841 38.0813 44.0841C37.1426 44.0841 36.5497 44.1334 36.5497 45.0207C36.5497 45.908 37.2908 45.9573 38.0319 45.9573C38.4766 45.908 38.7236 45.908 39.1188 45.8587Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M63.927 46.7474C63.3342 46.8953 62.7413 46.9446 62.1484 46.9446C60.024 46.9446 58.6406 45.9587 58.6406 43.6418C58.6406 41.4234 60.0734 40.4868 62.099 40.4868C62.6919 40.4868 63.2848 40.5361 63.8776 40.6347V41.7192C63.433 41.6699 63.0871 41.6206 62.6919 41.6206C61.0615 41.6206 60.271 42.1136 60.271 43.7404C60.271 45.2685 61.0121 45.8108 62.6919 45.8108C63.1365 45.8108 63.4824 45.7615 63.8776 45.7122V46.7474H63.927Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M28.1054 44.3303C28.1054 46.1542 26.6726 46.943 24.5482 46.943C23.1648 46.943 22.1767 46.6965 22.0285 46.6472V45.3655C22.5226 45.4641 23.5601 45.7106 24.4494 45.7106C25.3387 45.7106 26.475 45.5134 26.475 44.5275C26.475 43.8866 25.981 43.6401 25.0917 43.3936C24.7458 43.2951 24.4 43.1965 24.0541 43.0979C22.8684 42.7528 21.9297 42.2598 21.9297 40.8796C21.9297 38.9077 23.5107 38.4147 25.3387 38.4147C26.4256 38.4147 27.3149 38.6119 27.4631 38.6612V39.8443C27.2655 39.795 26.4256 39.5979 25.4869 39.5979C24.5976 39.5979 23.5601 39.6964 23.5601 40.6824C23.5601 41.4218 24.1529 41.619 24.9928 41.8162C25.3881 41.9641 25.7833 42.0134 26.1786 42.1612C27.2161 42.457 28.1054 42.95 28.1054 44.3303Z" fill="white"/>
<g filter="url(#filter1_d_1623_21496)">
<rect x="37" y="93" width="12" height="12" rx="6" fill="white"/>
<circle cx="43.1172" cy="99" r="4" fill="#CD0921"/>
</g>
<defs>
<filter id="filter0_d_1623_21496" x="0" y="0" width="84.3379" height="98.0014" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feMorphology radius="3" operator="dilate" in="SourceAlpha" result="effect1_dropShadow_1623_21496"/>
<feOffset/>
<feGaussianBlur stdDeviation="4"/>
<feComposite in2="hardAlpha" operator="out"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.25 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_1623_21496"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_1623_21496" result="shape"/>
</filter>
<filter id="filter1_d_1623_21496" x="34" y="90" width="18" height="18" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feMorphology radius="1" operator="dilate" in="SourceAlpha" result="effect1_dropShadow_1623_21496"/>
<feOffset/>
<feGaussianBlur stdDeviation="1"/>
<feComposite in2="hardAlpha" operator="out"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.1 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_1623_21496"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_1623_21496" result="shape"/>
</filter>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 5.5 KiB

15
stores/hotel-page.ts Normal file
View File

@@ -0,0 +1,15 @@
import { create } from "zustand"
interface HotelPageState {
isDynamicMapOpen: boolean
openDynamicMap: () => void
closeDynamicMap: () => void
}
const useHotelPageStore = create<HotelPageState>((set) => ({
isDynamicMapOpen: false,
openDynamicMap: () => set({ isDynamicMapOpen: true }),
closeDynamicMap: () => set({ isDynamicMapOpen: false }),
}))
export default useHotelPageStore

View File

@@ -38,11 +38,13 @@ export enum IconName {
Instagram = "Instagram",
Location = "Location",
Lock = "Lock",
Minus = "Minus",
Parking = "Parking",
Person = "Person",
People2 = "People2",
Pets = "Pets",
Phone = "Phone",
Plus = "Plus",
PlusCircle = "PlusCircle",
Restaurant = "Restaurant",
Sauna = "Sauna",