feat(SW-189): added static map to hotel page

This commit is contained in:
Erik Tiekstra
2024-09-06 15:02:25 +02:00
parent 4cc325f54c
commit 50d74648b5
24 changed files with 383 additions and 61 deletions

View File

@@ -0,0 +1,37 @@
"use client"
import Button from "@/components/TempDesignSystem/Button"
import Caption from "@/components/TempDesignSystem/Text/Caption"
import Title from "@/components/TempDesignSystem/Text/Title"
import styles from "./mapCard.module.css"
import { MapCardProps } from "@/types/components/hotelPage/mapCard"
export default function MapCard({ hotelName }: MapCardProps) {
return (
<div className={styles.mapCard}>
<Caption color="burgundy" textTransform="uppercase" textAlign="center">
Nearby
</Caption>
<Title
color="burgundy"
level="h3"
as="h4"
textAlign="center"
textTransform="capitalize"
>
{hotelName}
</Title>
<Button
theme="base"
intent="secondary"
size="small"
className={styles.ctaButton}
>
Explore nearby
</Button>
</div>
)
}

View File

@@ -0,0 +1,14 @@
.mapCard {
position: absolute;
bottom: 15%;
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 {
margin-top: var(--Spacing-x2);
}

View File

@@ -0,0 +1,51 @@
"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

@@ -0,0 +1,43 @@
.mobileToggle {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: var(--Spacing-x-half);
align-items: center;
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;
}
.button {
display: flex;
align-items: center;
justify-content: center;
gap: var(--Spacing-x-half);
padding: var(--Spacing-x1);
background-color: var(--Base-Surface-Primary-light-Normal);
border-width: 0;
cursor: pointer;
border-radius: 2.5rem;
color: var(--Base-Text-Accent);
font-family: var(--typography-Body-Bold-fontFamily);
font-size: var(--typography-Body-Bold-fontSize);
font-weight: 500;
}
.button:hover {
background-color: var(--Base-Surface-Primary-light-Hover);
}
.button.active {
background-color: var(--Primary-Strong-Surface-Normal);
color: var(--Base-Text-Inverted);
}
.button.active:hover {
background-color: var(--Primary-Strong-Surface-Hover);
}
@media screen and (min-width: 1367px) {
.mobileToggle {
display: none;
}
}

View File

@@ -0,0 +1,37 @@
/* eslint-disable @next/next/no-img-element */
import { env } from "@/env/server"
import { getIntl } from "@/i18n"
import { calculateLatWithOffset, getUrlWithSignature } from "./util"
import { StaticMapProps } from "@/types/components/hotelPage/staticMap"
export default async function StaticMap({
coordinates,
hotelName,
zoomLevel = 16,
}: StaticMapProps) {
const intl = await getIntl()
const key = env.GOOGLE_STATIC_MAP_KEY
const secret = env.GOOGLE_STATIC_MAP_SIGNATURE_SECRET
const baseUrl = "https://maps.googleapis.com/maps/api/staticmap"
const { lng, lat } = coordinates
const mapHeight = 785
const size = `380x${mapHeight}`
const mapLatitude = calculateLatWithOffset(lat, mapHeight * 0.2, 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}`
// Google Maps Static API only supports images smaller than 640x640px. Read: https://developers.google.com/maps/documentation/maps-static/start#Largerimagesizes
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}`
)
const src = getUrlWithSignature(url, secret)
return <img src={src} alt={alt} />
}

View File

@@ -0,0 +1,88 @@
import crypto from "node:crypto"
// Helper function to calculate the latitude offset
export function calculateLatWithOffset(
latitude: number,
offsetPx: number,
zoomLevel: number
): number {
const earthCircumference = 40075017 // Earth's circumference in meters
const tileSize = 256 // Height of a tile in pixels (standard in Google Maps)
// Calculate ground resolution (meters per pixel) at the given latitude and zoom level
const groundResolution =
(earthCircumference * Math.cos((latitude * Math.PI) / 180)) /
(tileSize * Math.pow(2, zoomLevel))
// Calculate the number of meters for the given offset in pixels
const metersOffset = groundResolution * offsetPx
// Convert the meters offset into a latitude offset (1 degree latitude is ~111,320 meters)
const latOffset = metersOffset / 111320
// Return the new latitude by subtracting the offset
return latitude - latOffset
}
/**
* Util functions taken from https://developers.google.com/maps/documentation/maps-static/digital-signature#sample-code-for-url-signing
* Used to sign the URL for the Google Static Maps API.
*/
/**
* Convert from 'web safe' base64 to true base64.
*
* @param {string} safeEncodedString The code you want to translate
* from a web safe form.
* @return {string}
*/
function removeWebSafe(safeEncodedString: string) {
return safeEncodedString.replace(/-/g, "+").replace(/_/g, "/")
}
/**
* Convert from true base64 to 'web safe' base64
*
* @param {string} encodedString The code you want to translate to a
* web safe form.
* @return {string}
*/
function makeWebSafe(encodedString: string) {
return encodedString.replace(/\+/g, "-").replace(/\//g, "_")
}
/**
* Takes a base64 code and decodes it.
*
* @param {string} code The encoded data.
* @return {string}
*/
function decodeBase64Hash(code: string) {
return Buffer.from(code, "base64")
}
/**
* Takes a key and signs the data with it.
*
* @param {string} key Your unique secret key.
* @param {string} data The url to sign.
* @return {string}
*/
function encodeBase64Hash(key: Buffer, data: string) {
return crypto.createHmac("sha1", key).update(data).digest("base64")
}
/**
* Sign a URL using a secret key.
*
* @param {URL} url The url you want to sign.
* @param {string} secret Your unique secret key.
* @return {string}
*/
export function getUrlWithSignature(url: URL, secret = "") {
const path = url.pathname + url.search
const safeSecret = decodeBase64Hash(removeWebSafe(secret))
const hashedSignature = makeWebSafe(encodeBase64Hash(safeSecret, path))
return `${url.toString()}&signature=${hashedSignature}`
}

View File

@@ -0,0 +1,17 @@
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

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

View File

@@ -1,45 +0,0 @@
import { env } from "@/env/server"
import { getIntl } from "@/i18n"
import { StaticMapProps } from "@/types/components/hotelPage/staticMap"
import crypto from "node:crypto"
function removeWebSafe(safeEncodedString: string) {
return safeEncodedString.replace(/-/g, "+").replace(/_/g, "/")
}
function makeWebSafe(encodedString: string) {
return encodedString.replace(/\+/g, "-").replace(/\//g, "_")
}
function decodeBase64Hash(code: string) {
return Buffer.from(code, "base64")
}
function encodeBase64Hash(key: Buffer, data: string) {
return crypto.createHmac("sha1", key).update(data).digest("base64")
}
// Google Maps Static API only supports images smaller than 640x640px. Read: https://developers.google.com/maps/documentation/maps-static/start#Largerimagesizes
export default async function StaticMap({
coordinates,
hotelName,
zoomLevel = 14,
}: StaticMapProps) {
const intl = await getIntl()
const key = env.GOOGLE_STATIC_MAP_KEY
const secret = env.GOOGLE_STATIC_MAP_SIGNATURE_SECRET
const safeSecret = decodeBase64Hash(removeWebSafe(secret ?? ""))
const alt = intl.formatMessage({ id: "Map of HOTEL_NAME" }, { hotelName })
const url = new URL(
`https://maps.googleapis.com/maps/api/staticmap?center=${coordinates.latitude},${coordinates.longitude}&zoom=${zoomLevel}&size=380x640&key=${key}`
)
const hashedSignature = makeWebSafe(
encodeBase64Hash(safeSecret, url.pathname + url.search)
)
const src = url.toString() + "&signature=" + hashedSignature
return <img src={src} alt={alt} />
}

View File

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

View File

@@ -1,20 +1,17 @@
import { serverClient } from "@/lib/trpc/server"
<<<<<<< HEAD
import { MOCK_FACILITIES } from "./Facilities/mockData"
import { setActivityCard } from "./Facilities/utils"
=======
>>>>>>> 491f385b (fix: wrong prop name)
import AmenitiesList from "./AmenitiesList"
import Facilities from "./Facilities"
import { MOCK_FACILITIES } from "./Facilities/mockData"
import { setActivityCard } from "./Facilities/utils"
import IntroSection from "./IntroSection"
import Map from "./Map"
import PreviewImages from "./PreviewImages"
import { Rooms } from "./Rooms"
import SidePeeks from "./SidePeeks"
import TabNavigation from "./TabNavigation"
import styles from "./hotelPage.module.css"
import StaticMap from "./StaticMap"
export default async function HotelPage() {
const hotelData = await serverClient().hotel.get({
@@ -38,10 +35,10 @@ export default async function HotelPage() {
const facilities = [...MOCK_FACILITIES]
activitiesCard && facilities.push(setActivityCard(activitiesCard))
const coordinates = {
latitude: hotelLocation.latitude,
longitude: hotelLocation.longitude,
lat: hotelLocation.latitude,
lng: hotelLocation.longitude,
}
return (
@@ -65,7 +62,7 @@ export default async function HotelPage() {
<Facilities facilities={facilities} />
</main>
<aside className={styles.mapContainer}>
<StaticMap coordinates={coordinates} hotelName={hotelName} />
<Map coordinates={coordinates} hotelName={hotelName} />
</aside>
<SidePeeks />
</div>