feat(SW-189): added translations and some minor changes
This commit is contained in:
@@ -1,6 +1,12 @@
|
||||
"use client"
|
||||
import { AdvancedMarker, Map, useMap } from "@vis.gl/react-google-maps"
|
||||
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"
|
||||
|
||||
@@ -9,28 +15,31 @@ 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,
|
||||
}: {
|
||||
hotelName: string
|
||||
coordinates: { lat: number; lng: number }
|
||||
}) {
|
||||
}: DynamicMapContentProps) {
|
||||
const intl = useIntl()
|
||||
const lang = useLang()
|
||||
const { isDynamicMapOpen, closeDynamicMap } = useHotelPageStore()
|
||||
const [isFullScreenSidebar, setIsFullScreenSidebar] = useState(false)
|
||||
const map = useMap()
|
||||
|
||||
const mapOptions = {
|
||||
const mapOptions: MapProps = {
|
||||
defaultZoom: 15,
|
||||
defaultCenter: coordinates,
|
||||
disableDefaultUI: true,
|
||||
clickableIcons: false,
|
||||
mapId: `${hotelName}-map`,
|
||||
mapId: `${hotelName}-${lang}-map`,
|
||||
// As reference for future styles when adding POIs
|
||||
// styles: [
|
||||
// {
|
||||
// featureType: "poi",
|
||||
@@ -75,7 +84,7 @@ export default function DynamicMapContent({
|
||||
onClick={closeDynamicMap}
|
||||
>
|
||||
<CloseIcon color="burgundy" width={24} height={24} />
|
||||
<span>Close the map</span>
|
||||
<span>{intl.formatMessage({ id: "Close the map" })}</span>
|
||||
</Button>
|
||||
<div className={styles.zoomButtons}>
|
||||
<Button
|
||||
@@ -85,7 +94,7 @@ export default function DynamicMapContent({
|
||||
size="small"
|
||||
className={styles.zoomButton}
|
||||
onClick={zoomOut}
|
||||
aria-label="Zoom in"
|
||||
aria-label={intl.formatMessage({ id: "Zoom in" })}
|
||||
>
|
||||
<MinusIcon color="burgundy" width={20} height={20} />
|
||||
</Button>
|
||||
@@ -96,7 +105,7 @@ export default function DynamicMapContent({
|
||||
size="small"
|
||||
className={styles.zoomButton}
|
||||
onClick={zoomIn}
|
||||
aria-label="Zoom in"
|
||||
aria-label={intl.formatMessage({ id: "Zoom out" })}
|
||||
>
|
||||
<PlusIcon color="burgundy" width={20} height={20} />
|
||||
</Button>
|
||||
@@ -111,12 +120,17 @@ export default function DynamicMapContent({
|
||||
className={styles.toggleButton}
|
||||
onClick={toggleFullScreenSidebar}
|
||||
>
|
||||
{isFullScreenSidebar ? "View as map" : "View as list"}
|
||||
{intl.formatMessage({
|
||||
id: isFullScreenSidebar ? "View as map" : "View as list",
|
||||
})}
|
||||
</button>
|
||||
</div>
|
||||
<div className={styles.sidebarContent}>
|
||||
<Title as="h4" level="h2" textTransform="regular">
|
||||
Things nearby {hotelName}
|
||||
{intl.formatMessage(
|
||||
{ id: "Things nearby HOTEL_NAME" },
|
||||
{ hotelName }
|
||||
)}
|
||||
</Title>
|
||||
<Divider color="subtle" />
|
||||
</div>
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
left: 0;
|
||||
z-index: var(--dialog-z-index);
|
||||
display: flex;
|
||||
background-color: var(--Base-Surface-Primary-light-Normal);
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
@@ -44,7 +45,7 @@
|
||||
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;
|
||||
font-weight: var(--typography-Body-Bold-fontWeight);
|
||||
color: var(--UI-Text-Medium-contrast);
|
||||
width: 100%;
|
||||
}
|
||||
@@ -62,7 +63,7 @@
|
||||
.sidebarContent {
|
||||
display: grid;
|
||||
gap: var(--Spacing-x3);
|
||||
align-items: start;
|
||||
align-content: start;
|
||||
padding: var(--Spacing-x3) var(--Spacing-x2);
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
@@ -112,18 +113,13 @@
|
||||
}
|
||||
|
||||
.zoomButton {
|
||||
width: 40px;
|
||||
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 (max-width: 767px) {
|
||||
.sidebar:not(.fullscreen) .sidebarContent {
|
||||
display: none;
|
||||
}
|
||||
} */
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
.dynamicMap {
|
||||
top: var(--main-menu-desktop-height);
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
"use client"
|
||||
import { APIProvider } from "@vis.gl/react-google-maps"
|
||||
import { useEffect, useRef, useState } from "react"
|
||||
import { Dialog, Modal } from "react-aria-components"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import useHotelPageStore from "@/stores/hotel-page"
|
||||
|
||||
@@ -10,16 +12,17 @@ import DynamicMapContent from "./Content"
|
||||
|
||||
import styles from "./dynamicMap.module.css"
|
||||
|
||||
import type { DynamicMapProps } from "@/types/components/hotelPage/map/dynamicMap"
|
||||
|
||||
export default function DynamicMap({
|
||||
apiKey,
|
||||
hotelName,
|
||||
coordinates,
|
||||
}: {
|
||||
apiKey: string
|
||||
hotelName: string
|
||||
coordinates: { lat: number; lng: number }
|
||||
}) {
|
||||
}: DynamicMapProps) {
|
||||
const intl = useIntl()
|
||||
const { isDynamicMapOpen, closeDynamicMap } = useHotelPageStore()
|
||||
const [scrollHeightWhenOpened, setScrollHeightWhenOpened] = useState(0)
|
||||
const hasMounted = useRef(false)
|
||||
|
||||
useHandleKeyUp((event: KeyboardEvent) => {
|
||||
if (event.key === "Escape" && isDynamicMapOpen) {
|
||||
@@ -27,12 +30,33 @@ export default function DynamicMap({
|
||||
}
|
||||
})
|
||||
|
||||
// Making sure the map is always opened at the top of the page, just below the header.
|
||||
// When closing, the page should scroll back to the position it was before opening the map.
|
||||
useEffect(() => {
|
||||
// Skip the first render
|
||||
if (!hasMounted.current) {
|
||||
hasMounted.current = true
|
||||
return
|
||||
}
|
||||
|
||||
if (isDynamicMapOpen && scrollHeightWhenOpened === 0) {
|
||||
setScrollHeightWhenOpened(window.scrollY)
|
||||
window.scrollTo({ top: 0, behavior: "instant" })
|
||||
} else if (!isDynamicMapOpen && scrollHeightWhenOpened !== 0) {
|
||||
window.scrollTo({ top: scrollHeightWhenOpened, behavior: "instant" })
|
||||
setScrollHeightWhenOpened(0)
|
||||
}
|
||||
}, [isDynamicMapOpen, scrollHeightWhenOpened])
|
||||
|
||||
return (
|
||||
<APIProvider apiKey={apiKey}>
|
||||
<Modal isOpen={isDynamicMapOpen}>
|
||||
<Dialog
|
||||
className={styles.dynamicMap}
|
||||
aria-label={`Things nearby ${hotelName}`}
|
||||
aria-label={intl.formatMessage(
|
||||
{ id: "Things nearby HOTEL_NAME" },
|
||||
{ hotelName }
|
||||
)}
|
||||
>
|
||||
<DynamicMapContent hotelName={hotelName} coordinates={coordinates} />
|
||||
</Dialog>
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
"use client"
|
||||
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import useHotelPageStore from "@/stores/hotel-page"
|
||||
|
||||
import Button from "@/components/TempDesignSystem/Button"
|
||||
@@ -8,9 +10,10 @@ import Title from "@/components/TempDesignSystem/Text/Title"
|
||||
|
||||
import styles from "./mapCard.module.css"
|
||||
|
||||
import { MapCardProps } from "@/types/components/hotelPage/mapCard"
|
||||
import type { MapCardProps } from "@/types/components/hotelPage/map/mapCard"
|
||||
|
||||
export default function MapCard({ hotelName }: MapCardProps) {
|
||||
const intl = useIntl()
|
||||
const { openDynamicMap } = useHotelPageStore()
|
||||
|
||||
return (
|
||||
@@ -20,7 +23,7 @@ export default function MapCard({ hotelName }: MapCardProps) {
|
||||
textTransform="uppercase"
|
||||
textAlign="center"
|
||||
>
|
||||
Nearby
|
||||
{intl.formatMessage({ id: "Nearby" })}
|
||||
</Caption>
|
||||
<Title
|
||||
color="burgundy"
|
||||
@@ -39,7 +42,7 @@ export default function MapCard({ hotelName }: MapCardProps) {
|
||||
className={styles.ctaButton}
|
||||
onClick={openDynamicMap}
|
||||
>
|
||||
Explore nearby
|
||||
{intl.formatMessage({ id: "Explore nearby" })}
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
"use client"
|
||||
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import useHotelPageStore from "@/stores/hotel-page"
|
||||
|
||||
import { HouseIcon, LocationIcon } from "@/components/Icons"
|
||||
import { HouseIcon, MapIcon } from "@/components/Icons"
|
||||
|
||||
import styles from "./mobileToggle.module.css"
|
||||
|
||||
export default function MobileMapToggle() {
|
||||
const intl = useIntl()
|
||||
const { isDynamicMapOpen, openDynamicMap, closeDynamicMap } =
|
||||
useHotelPageStore()
|
||||
|
||||
@@ -23,20 +26,20 @@ export default function MobileMapToggle() {
|
||||
height={24}
|
||||
width={24}
|
||||
/>
|
||||
<span>Hotel</span>
|
||||
<span>{intl.formatMessage({ id: "Hotel" })}</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={`${styles.button} ${isDynamicMapOpen ? styles.active : ""}`}
|
||||
onClick={openDynamicMap}
|
||||
>
|
||||
<LocationIcon
|
||||
<MapIcon
|
||||
className={styles.icon}
|
||||
color={isDynamicMapOpen ? "white" : "red"}
|
||||
height={24}
|
||||
width={24}
|
||||
/>
|
||||
<span>Nearby</span>
|
||||
<span>{intl.formatMessage({ id: "Map" })}</span>
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -19,15 +19,15 @@
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: var(--Spacing-x-half);
|
||||
padding: var(--Spacing-x1);
|
||||
padding: var(--Spacing-x1) var(--Spacing-x2);
|
||||
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;
|
||||
font-family: var(--typography-Caption-Bold-Desktop-fontFamily);
|
||||
font-size: var(--typography-Caption-Bold-Desktop-fontSize);
|
||||
font-weight: var(--typography-Caption-Bold-Desktop-fontWeight);
|
||||
}
|
||||
.button:hover {
|
||||
background-color: var(--Base-Surface-Primary-light-Hover);
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
# Google Map Static API
|
||||
|
||||
### About
|
||||
|
||||
The Google Maps Static API lets you embed a Google Maps image on your web page. The Google Maps Static API service creates your map based on URL parameters sent through a standard HTTP request and returns the map as an image you can display on your web page. Due to regulations from Google we are not allowed to store and serve copies of images generated using the Google Maps Static API. All web pages that require static images must link the src attribute of an HTML img tag or as a background-image using CSS.
|
||||
|
||||
### API Key Restrictions
|
||||
|
||||
You can restrict your API key on websites, IP addresses, Android apps and iOS apps. For now (12/8-24) the API key is restricted by IP address, hence the request will only work at Scandic HQ network. This will be changed to a referrer website in the future.
|
||||
[Read more about API key restrictions](https://developers.google.com/maps/api-security-best-practices#restricting-api-keys)
|
||||
|
||||
### Digital Signature
|
||||
|
||||
Requests exceeding 25,000 requests per day require an API key and a digital signature. However, it is strongly recommended by Google to use both an API key and digital signature, regardless of the usage.
|
||||
|
||||
The digital signature is set on the Google Maps Platform. For dynamically generated requests, the signature is handled through server side signing appending the signature as base64 on the request url.
|
||||
[Read more about digital signature](https://developers.google.com/maps/documentation/maps-static/digital-signature)
|
||||
|
||||
### Generating new API keys
|
||||
|
||||
Regenerating an API key creates a new key that has all the old key's restrictions. This process also starts a 24-hour timer after which the old API key is deleted. During this time window, both the old and new key are accepted, giving you a chance to migrate your apps to use the new key. However, after this time period elapses, any apps still using the old API key stop working.
|
||||
|
||||
**Caution:** Only regenerate an API key if you absolutely must to avoid unauthorized use. This process can shut down legitimate traffic and prevent your apps from functioning properly.
|
||||
[Read more about regenerating keys](https://developers.google.com/maps/api-security-best-practices#regenerate-apikey)
|
||||
|
||||
### Version history
|
||||
|
||||
| Description | Version | Date | Author |
|
||||
| ------------------- | ------- | ------- | ---------------- |
|
||||
| Create ReadMe file. | 1.0.0 | 12/8-24 | Fredrik Thorsson |
|
||||
@@ -1,15 +1,14 @@
|
||||
/* eslint-disable @next/next/no-img-element */
|
||||
|
||||
import { env } from "@/env/server"
|
||||
|
||||
import StaticMapComp from "@/components/Maps/StaticMap"
|
||||
import { getIntl } from "@/i18n"
|
||||
import { calculateLatWithOffset } from "@/utils/map"
|
||||
|
||||
import ScandicMarker from "../Markers/Scandic"
|
||||
import { calculateLatWithOffset, getUrlWithSignature } from "./util"
|
||||
|
||||
import styles from "./staticMap.module.css"
|
||||
|
||||
import { StaticMapProps } from "@/types/components/hotelPage/staticMap"
|
||||
import type { StaticMapProps } from "@/types/components/hotelPage/map/staticMap"
|
||||
|
||||
export default async function StaticMap({
|
||||
coordinates,
|
||||
@@ -17,34 +16,23 @@ export default async function StaticMap({
|
||||
zoomLevel = 14,
|
||||
}: 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 markerHeight = 100
|
||||
const mapLatitudeInPx = mapHeight * 0.2
|
||||
const size = `380x${mapHeight}`
|
||||
|
||||
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}`
|
||||
// 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}¢er=${mapLatitude},${lng}&size=${size}&key=${key}`
|
||||
)
|
||||
// const url = new URL(
|
||||
// `${baseUrl}?zoom=${zoomLevel}¢er=${mapLatitude},${lng}&size=${size}&markers=${marker}&key=${key}`
|
||||
// )
|
||||
|
||||
const src = getUrlWithSignature(url, secret)
|
||||
const mapCoordinates = {
|
||||
lat: calculateLatWithOffset(coordinates.lat, mapLatitudeInPx, zoomLevel),
|
||||
lng: coordinates.lng,
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.staticMap}>
|
||||
<img src={src} alt={alt} />
|
||||
<StaticMapComp
|
||||
coordinates={mapCoordinates}
|
||||
width={380}
|
||||
height={mapHeight}
|
||||
zoomLevel={zoomLevel}
|
||||
altText={intl.formatMessage({ id: "Map of HOTEL_NAME" }, { hotelName })}
|
||||
/>
|
||||
<ScandicMarker
|
||||
className={styles.mapMarker}
|
||||
height={markerHeight}
|
||||
|
||||
@@ -1,88 +0,0 @@
|
||||
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}`
|
||||
}
|
||||
@@ -40,7 +40,7 @@
|
||||
}
|
||||
.mainSection {
|
||||
grid-area: mainSection;
|
||||
padding: var(--Spacing-x6) 0;
|
||||
padding: var(--Spacing-x6) var(--Spacing-x4) 0;
|
||||
}
|
||||
.mapContainer {
|
||||
display: flex;
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import { env } from "@/env/server"
|
||||
import { serverClient } from "@/lib/trpc/server"
|
||||
|
||||
import AmenitiesList from "./AmenitiesList"
|
||||
import Facilities from "./Facilities"
|
||||
import { MOCK_FACILITIES } from "./Facilities/mockData"
|
||||
import { setActivityCard } from "./Facilities/utils"
|
||||
import IntroSection from "./IntroSection"
|
||||
import DynamicMap from "./Map/DynamicMap"
|
||||
import MapCard from "./Map/MapCard"
|
||||
import MobileMapToggle from "./Map/MobileMapToggle"
|
||||
import StaticMap from "./Map/StaticMap"
|
||||
import AmenitiesList from "./AmenitiesList"
|
||||
import Facilities from "./Facilities"
|
||||
import IntroSection from "./IntroSection"
|
||||
import PreviewImages from "./PreviewImages"
|
||||
import { Rooms } from "./Rooms"
|
||||
import SidePeeks from "./SidePeeks"
|
||||
@@ -69,7 +69,6 @@ export default async function HotelPage() {
|
||||
{googleMapsApiKey ? (
|
||||
<>
|
||||
<aside className={styles.mapContainer}>
|
||||
{/* <Map coordinates={coordinates} hotelName={hotelName} /> */}
|
||||
<StaticMap coordinates={coordinates} hotelName={hotelName} />
|
||||
<MapCard hotelName={hotelName} />
|
||||
</aside>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"use client"
|
||||
|
||||
import { useEffect } from "react"
|
||||
import { Dialog, Modal } from "react-aria-components"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
@@ -38,6 +39,13 @@ export default function MobileMenu({
|
||||
}
|
||||
})
|
||||
|
||||
// Making sure the menu is always opened at the top of the page, just below the header.
|
||||
useEffect(() => {
|
||||
if (isHamburgerMenuOpen) {
|
||||
window.scrollTo({ top: 0, behavior: "instant" })
|
||||
}
|
||||
}, [isHamburgerMenuOpen])
|
||||
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"use client"
|
||||
|
||||
import { useEffect } from "react"
|
||||
import { Dialog, Modal } from "react-aria-components"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
@@ -31,6 +32,13 @@ export default function MyPagesMobileMenu({
|
||||
}
|
||||
})
|
||||
|
||||
// Making sure the menu is always opened at the top of the page, just below the header.
|
||||
useEffect(() => {
|
||||
if (isMyPagesMobileMenuOpen) {
|
||||
window.scrollTo({ top: 0, behavior: "instant" })
|
||||
}
|
||||
}, [isMyPagesMobileMenuOpen])
|
||||
|
||||
return (
|
||||
<div className={styles.myPagesMobileMenu}>
|
||||
<MainMenuButton
|
||||
|
||||
36
components/Icons/Map.tsx
Normal file
36
components/Icons/Map.tsx
Normal file
@@ -0,0 +1,36 @@
|
||||
import { iconVariants } from "./variants"
|
||||
|
||||
import type { IconProps } from "@/types/components/icon"
|
||||
|
||||
export default function MapIcon({ className, color, ...props }: IconProps) {
|
||||
const classNames = iconVariants({ className, color })
|
||||
return (
|
||||
<svg
|
||||
className={classNames}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="25"
|
||||
height="25"
|
||||
viewBox="0 0 25 25"
|
||||
fill="none"
|
||||
{...props}
|
||||
>
|
||||
<mask
|
||||
id="mask0_4031_935"
|
||||
style={{ maskType: "alpha" }}
|
||||
maskUnits="userSpaceOnUse"
|
||||
x="0"
|
||||
y="0"
|
||||
width="25"
|
||||
height="25"
|
||||
>
|
||||
<rect x="0.5" y="0.194336" width="24" height="24" fill="#D9D9D9" />
|
||||
</mask>
|
||||
<g mask="url(#mask0_4031_935)">
|
||||
<path
|
||||
d="M15.5 21.1943L9.5 19.0943L4.85 20.8943C4.51667 21.0277 4.20833 20.9902 3.925 20.7818C3.64167 20.5735 3.5 20.2943 3.5 19.9443V5.94434C3.5 5.72767 3.5625 5.536 3.6875 5.36934C3.8125 5.20267 3.98333 5.07767 4.2 4.99434L9.5 3.19434L15.5 5.29434L20.15 3.49434C20.4833 3.361 20.7917 3.3985 21.075 3.60684C21.3583 3.81517 21.5 4.09434 21.5 4.44434V18.4443C21.5 18.661 21.4375 18.8527 21.3125 19.0193C21.1875 19.186 21.0167 19.311 20.8 19.3943L15.5 21.1943ZM14.5 18.7443V7.04434L10.5 5.64434V17.3443L14.5 18.7443ZM16.5 18.7443L19.5 17.7443V5.89434L16.5 7.04434V18.7443ZM5.5 18.4943L8.5 17.3443V5.64434L5.5 6.64434V18.4943Z"
|
||||
fill="white"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
@@ -33,6 +33,7 @@ import {
|
||||
InfoCircleIcon,
|
||||
LocationIcon,
|
||||
LockIcon,
|
||||
MapIcon,
|
||||
MinusIcon,
|
||||
ParkingIcon,
|
||||
People2Icon,
|
||||
@@ -116,6 +117,8 @@ export function getIconByIconName(icon?: IconName): FC<IconProps> | null {
|
||||
return LocationIcon
|
||||
case IconName.Lock:
|
||||
return LockIcon
|
||||
case IconName.Map:
|
||||
return MapIcon
|
||||
case IconName.Minus:
|
||||
return MinusIcon
|
||||
case IconName.Parking:
|
||||
|
||||
@@ -29,6 +29,7 @@ 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 MapIcon } from "./Map"
|
||||
export { default as MinusIcon } from "./Minus"
|
||||
export { default as ParkingIcon } from "./Parking"
|
||||
export { default as People2Icon } from "./People2"
|
||||
|
||||
@@ -1,46 +1,33 @@
|
||||
/* eslint-disable @next/next/no-img-element */
|
||||
import crypto from "node:crypto"
|
||||
|
||||
import { env } from "@/env/server"
|
||||
|
||||
import { StaticMapProps } from "@/types/components/maps/staticMap/staticMap"
|
||||
import { getUrlWithSignature } from "@/utils/map"
|
||||
|
||||
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")
|
||||
}
|
||||
import { StaticMapProps } from "@/types/components/maps/staticMap"
|
||||
|
||||
export default function StaticMap({
|
||||
city,
|
||||
coordinates,
|
||||
width,
|
||||
height,
|
||||
zoomLevel,
|
||||
mapType,
|
||||
zoomLevel = 14,
|
||||
mapType = "roadmap",
|
||||
altText,
|
||||
}: StaticMapProps) {
|
||||
const key = env.GOOGLE_STATIC_MAP_KEY
|
||||
const secret = env.GOOGLE_STATIC_MAP_SIGNATURE_SECRET
|
||||
const safeSecret = decodeBase64Hash(removeWebSafe(secret ?? ""))
|
||||
const baseUrl = "https://maps.googleapis.com/maps/api/staticmap"
|
||||
const center = coordinates ? `${coordinates.lat},${coordinates.lng}` : city
|
||||
|
||||
if (!center) {
|
||||
return null
|
||||
}
|
||||
|
||||
// Google Maps Static API only supports images smaller than 640x640px. Read: https://developers.google.com/maps/documentation/maps-static/start#Largerimagesizes
|
||||
const url = new URL(
|
||||
`https://maps.googleapis.com/maps/api/staticmap?center=${city}&zoom=${zoomLevel}&size=${width}x${height}&maptype=${mapType}&key=${key}`
|
||||
`${baseUrl}?center=${center}&zoom=${zoomLevel}&size=${width}x${height}&maptype=${mapType}&key=${key}`
|
||||
)
|
||||
const src = getUrlWithSignature(url, secret)
|
||||
|
||||
const hashedSignature = makeWebSafe(
|
||||
encodeBase64Hash(safeSecret, url.pathname + url.search)
|
||||
)
|
||||
|
||||
const src = url.toString() + "&signature=" + hashedSignature
|
||||
|
||||
return <img src={src} alt={`Map of ${city} city center`} />
|
||||
return <img src={src} alt={altText} />
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user