From d0df5fbbc61bcc596209f103c1f800cfd52048d6 Mon Sep 17 00:00:00 2001 From: log101 Date: Fri, 19 Jul 2024 16:38:58 +0300 Subject: [PATCH] feat: move leaflet map to a web component --- src/components/LockedContent.tsx | 160 -------- .../LockedContent/{svg.ts => icons.ts} | 0 .../{middleware.ts => serverUtils.ts} | 0 src/components/LockedContent/templates.ts | 2 +- src/components/WebComponentWrapper.astro | 122 ------- src/components/leaflet-map.ts | 52 +++ src/components/locked-content-lit.ts | 192 ---------- src/components/locked-content.ts | 344 ++++++++---------- src/pages/[id].astro | 14 +- src/scripts/initMap.ts | 33 +- 10 files changed, 243 insertions(+), 676 deletions(-) delete mode 100644 src/components/LockedContent.tsx rename src/components/LockedContent/{svg.ts => icons.ts} (100%) rename src/components/LockedContent/{middleware.ts => serverUtils.ts} (100%) delete mode 100644 src/components/WebComponentWrapper.astro create mode 100644 src/components/leaflet-map.ts delete mode 100644 src/components/locked-content-lit.ts diff --git a/src/components/LockedContent.tsx b/src/components/LockedContent.tsx deleted file mode 100644 index 3eff0e1..0000000 --- a/src/components/LockedContent.tsx +++ /dev/null @@ -1,160 +0,0 @@ -import { Button } from "@/components/ui/button"; -import { Card, CardContent } from "@/components/ui/card"; - -import { LockClosedIcon, LockOpen1Icon } from "@radix-ui/react-icons"; -import { useEffect, useState } from "react"; - -import "../styles/locked-content.css"; -import type { Generated } from "kysely"; -import { onLocationError } from "@/lib/error"; - -const incrementCounter = async (id: string | Generated) => - await fetch(`http://localhost:3000/api/location/increment/${id}`, { - method: "PATCH", - }); - -const LocationButton = ({ - contentId = "", - imageUrl = "#", - location = "", -}: { - contentId?: string | Generated; - imageUrl?: string; - location?: string; -}) => { - const [atTarget, setAtTarget] = useState(false); - const [contentVisible, setContentVisible] = useState(false); - const [hasPermission, setHasPermission] = useState(false); - const [watchId, setWatchId] = useState(); - const [distanceRemain, setDistanceRemain] = useState(""); - - const targetCoordinates = JSON.parse(location); - - const targetPos = { - lat: targetCoordinates[0], - lng: targetCoordinates[1], - }; - - const startWatchingLocation = () => { - setHasPermission(true); - if (!watchId) { - const id = navigator.geolocation.watchPosition( - (position: GeolocationPosition) => { - const pos = { - lat: position.coords.latitude, - lng: position.coords.longitude, - }; - - // @ts-expect-error 3rd party script - const targetLatLng = L.latLng(targetPos); - - // @ts-expect-error 3rd party script - const currentLatLng = L.latLng(pos); - - const betweenMeters = currentLatLng.distanceTo(targetLatLng); - - if (betweenMeters > 1000) { - setDistanceRemain(`${(betweenMeters / 1000).toFixed()} KM`); - } else if (betweenMeters > 100) { - setDistanceRemain(`${betweenMeters.toFixed(0)} M`); - } else { - navigator.geolocation.clearWatch(id); - setAtTarget(true); - } - }, - (err) => onLocationError(err), - { enableHighAccuracy: true, timeout: 27000, maximumAge: 10000 } - ); - - setWatchId(id); - } - }; - - const handleUnlock = async () => { - setContentVisible(true); - await incrementCounter(contentId); - }; - - useEffect(() => { - navigator.permissions - .query({ name: "geolocation" }) - .then((permissionStatus) => { - if (permissionStatus.state === "granted") { - setHasPermission(true); - startWatchingLocation(); - } - }); - }, []); - - if (contentVisible) { - return ( -
- -
- ); - } else { - return ( - <> -
- {atTarget ? ( -
- - -
- - - - - İçeriği görmek için butona bas! - - -
-
- ) : ( -
- -
- - - {hasPermission ? ( - -

İçeriği görmek için konuma gitmelisin!

-

- {distanceRemain && `Kalan mesafe: ${distanceRemain}`} -

-
- ) : ( -
- - Ne kadar yaklaştığını görmek için aşağıdaki butona bas. - - - -
- )} -
-
-
- )} -
- - ); - } -}; - -export default LocationButton; diff --git a/src/components/LockedContent/svg.ts b/src/components/LockedContent/icons.ts similarity index 100% rename from src/components/LockedContent/svg.ts rename to src/components/LockedContent/icons.ts diff --git a/src/components/LockedContent/middleware.ts b/src/components/LockedContent/serverUtils.ts similarity index 100% rename from src/components/LockedContent/middleware.ts rename to src/components/LockedContent/serverUtils.ts diff --git a/src/components/LockedContent/templates.ts b/src/components/LockedContent/templates.ts index ea89396..b9a2ec6 100644 --- a/src/components/LockedContent/templates.ts +++ b/src/components/LockedContent/templates.ts @@ -1,5 +1,5 @@ import { html } from "lit"; -import { lockSVG, unlockSVG } from "./svg"; +import { lockSVG, unlockSVG } from "./icons"; // This template is shown when user hasn't give geolocation permission yet // When user click the button user is asked for geolocation permission diff --git a/src/components/WebComponentWrapper.astro b/src/components/WebComponentWrapper.astro deleted file mode 100644 index 016ada2..0000000 --- a/src/components/WebComponentWrapper.astro +++ /dev/null @@ -1,122 +0,0 @@ ---- -import { LockClosedIcon, LockOpen1Icon } from "@radix-ui/react-icons"; - -interface Props { - contentId?: string; - imageUrl: string; - location?: string; -} - -const { contentId = "", imageUrl = "#", location = "" } = Astro.props; ---- - - - - - - - - diff --git a/src/components/leaflet-map.ts b/src/components/leaflet-map.ts new file mode 100644 index 0000000..92911c0 --- /dev/null +++ b/src/components/leaflet-map.ts @@ -0,0 +1,52 @@ +// Lit +import { html, LitElement } from "lit"; +import { customElement, property, query, state } from "lit/decorators.js"; + +// Leaflet +import L from "leaflet"; +import type { LatLngTuple } from "leaflet"; + +@customElement("leaflet-map") +export class LeafletMap extends LitElement { + @property({ type: Object }) targetLocation?: LatLngTuple; + + @query("#leaflet-map-container") + _mapElement!: HTMLDivElement; + + @state() + protected _map?: L.Map; + @state() + protected _geolocationPermissionStatus: PermissionState = "prompt"; + @state() + protected _currentLocationMarker?: L.Marker; + @state() + protected _watchingLocation = false; + + connectedCallback(): void { + super.connectedCallback(); + + // Check geolocation permission, if user has given permission before + // start watching user location + navigator.permissions + .query({ name: "geolocation" }) + .then((permissionStatus) => { + switch (permissionStatus.state) { + case "granted": + this._geolocationPermissionStatus = "granted"; + break; + case "denied": + this._geolocationPermissionStatus = "denied"; + case "prompt": + default: + break; + } + }); + } + + render() { + return html`
`; + } +} diff --git a/src/components/locked-content-lit.ts b/src/components/locked-content-lit.ts deleted file mode 100644 index bc377e1..0000000 --- a/src/components/locked-content-lit.ts +++ /dev/null @@ -1,192 +0,0 @@ -// Lit imports -import { LitElement, html, nothing, unsafeCSS, type CSSResultGroup } from "lit"; -import { customElement, property, state } from "lit/decorators.js"; - -// Leaflet -import { type LatLngTuple } from "leaflet"; - -// Styles -import globalStyles from "@/styles/globals.css?inline"; -import lockedContentStyles from "../styles/locked-content.css?inline"; - -// Templates -import { - lockedButtonTemplate, - permissionButtonTemplate, - permissionDeniedButtonTemplate, - unlockedButtonTemplate, -} from "./LockedContent/templates"; - -// Geolocation utils -import { calculateDistance, errorCallback } from "./LockedContent/geolocation"; -import { incrementUnlockCounter } from "./LockedContent/middleware"; - -// LockedContent is a custom element watching user location and blurring -// given image until user has arrived a certain position -@customElement("locked-content-lit") -export class LockedContent extends LitElement { - // Constants - geolocationOptions = { - enableHighAccuracy: true, - timeout: 15000, - maximumAge: 0, - }; - - // Tailwind and custom styles - static styles: CSSResultGroup | undefined = [ - unsafeCSS(globalStyles), - unsafeCSS(lockedContentStyles), - ]; - - // Components properties/attributes, no accessor attribute disables detecting - // changes as these are readonly attriubtes there is no need to attach setters - @property({ noAccessor: true }) readonly imageId?: string; - @property({ noAccessor: true }) readonly imageURL?: string; - @property({ type: Object, noAccessor: true }) - readonly targetPosition?: LatLngTuple; - - // Reactive states, template is rendered according to this states - @state() - protected _geolocationPermissionStatus: PermissionState = "prompt"; - @state() - protected _unlocked = false; - @state() - protected _arrived = false; - @state() - protected _distanceText?: string; - @state() - protected _watchId?: number; - - // This callback will be fired when geolocation info is available - successCallback(position: GeolocationPosition) { - // Set hasGeolocationPermission state true to change the template - this._geolocationPermissionStatus = "granted"; - - // Target position must be set - if (!this.targetPosition) return; - - // Calculate the distance between target and current position in meters - const distance = calculateDistance(position, this.targetPosition); - - // Update the text based on the distance - this._updateDistanceText(distance); - - this._checkArrived(distance); - } - - private _updateDistanceText(distance: number) { - // Update the proximity text according to the distance remaining - if (distance > 1000) { - this._distanceText = `${(distance / 1000).toFixed()} KM`; - } else if (distance > 100) { - this._distanceText = `${distance.toFixed(0)} M`; - } - } - - private _checkArrived(distance: number) { - // If target is close less then 100 meters user has arrived to target location - if (distance < 100) { - if (this._watchId) { - // Stop watching location - navigator.geolocation.clearWatch(this._watchId); - } - // Update state to reveal the image - this._arrived = true; - } - } - - // This template is shown when user hasn't give geolocation permission yet - // When user click the button user is asked for geolocation permission - private _permissionButtonTemplate = () => - permissionButtonTemplate(this._startWatchingLocation); - - // This template is shown when user has given permission but has not arrived yet - private _lockedButtonTemplate = () => - lockedButtonTemplate(this._distanceText); - - // This template is shown when user has arrived to the target location - // When user click the button counter at the bottom of the page is incremented - // and image is revealed - private _unlockedButtonTemplate = () => - unlockedButtonTemplate(() => { - incrementUnlockCounter(this.imageId); - this._unlocked = true; - }); - - // Start watching user location, if user has not given permission yet - // this will ask the user for permission and update the watch id - private _startWatchingLocation() { - // User is already being watched no need to - // watch position - if (this._watchId) return; - - const id = navigator.geolocation.watchPosition( - this.successCallback.bind(this), - (err) => { - if (err.code == GeolocationPositionError.PERMISSION_DENIED) { - this._geolocationPermissionStatus = "denied"; - } - errorCallback(err); - }, - this.geolocationOptions - ); - - this._watchId = id; - } - - connectedCallback(): void { - super.connectedCallback(); - - // Check geolocation permission, if user has given permission before - // start watching user location - navigator.permissions - .query({ name: "geolocation" }) - .then((permissionStatus) => { - switch (permissionStatus.state) { - case "granted": - this._startWatchingLocation(); - break; - case "denied": - this._geolocationPermissionStatus = "denied"; - case "prompt": - default: - break; - } - }); - } - - render() { - let buttonTemplate; - - // Determine which template to show, there are 3 states: - // 1 - No geolocation permission given - // 2 - Permission given but has no arrived to target position yet - // 3 - Arrived to target position - // 4 - User did not give geolocation permission - if (this._arrived) { - buttonTemplate = this._unlockedButtonTemplate; - } else if (this._geolocationPermissionStatus == "granted") { - buttonTemplate = this._lockedButtonTemplate; - } else if (this._geolocationPermissionStatus == "prompt") { - buttonTemplate = this._permissionButtonTemplate; - } else { - buttonTemplate = permissionDeniedButtonTemplate; - } - - return html` -
-
- - - ${this._unlocked ? nothing : buttonTemplate()} -
-
- `; - } -} diff --git a/src/components/locked-content.ts b/src/components/locked-content.ts index 7e06b2d..55459bb 100644 --- a/src/components/locked-content.ts +++ b/src/components/locked-content.ts @@ -1,214 +1,192 @@ -import L, { type LatLngTuple } from "leaflet"; -import Toastify from "toastify-js"; +// Lit imports +import { LitElement, html, nothing, unsafeCSS, type CSSResultGroup } from "lit"; +import { customElement, property, state } from "lit/decorators.js"; -class LockedContent extends HTMLElement { - watchId: number; - targetPos: LatLngTuple; +// Leaflet +import { type LatLngTuple } from "leaflet"; + +// Styles +import globalStyles from "@/styles/globals.css?inline"; +import lockedContentStyles from "../styles/locked-content.css?inline"; + +// Templates +import { + lockedButtonTemplate, + permissionButtonTemplate, + permissionDeniedButtonTemplate, + unlockedButtonTemplate, +} from "./LockedContent/templates"; + +// Geolocation utils +import { calculateDistance, errorCallback } from "./LockedContent/geolocation"; +import { incrementUnlockCounter } from "./LockedContent/serverUtils"; + +// LockedContent is a custom element watching user location and blurring +// given image until user has arrived a certain position +@customElement("locked-content") +export class LockedContent extends LitElement { + // Constants geolocationOptions = { enableHighAccuracy: true, - timeout: 5000, + timeout: 15000, maximumAge: 0, }; - constructor() { - super(); + // Tailwind and custom styles + static styles: CSSResultGroup | undefined = [ + unsafeCSS(globalStyles), + unsafeCSS(lockedContentStyles), + ]; - // Clone the template - let template = document.getElementById( - "locked-content-template" - ) as HTMLTemplateElement; - let templateContent = template.content; + // Components properties/attributes, no accessor attribute disables detecting + // changes as these are readonly attriubtes there is no need to attach setters + @property({ noAccessor: true }) readonly imageId?: string; + @property({ noAccessor: true }) readonly imageURL?: string; + @property({ type: Object, noAccessor: true }) + readonly targetPosition?: LatLngTuple; - // Get attributes - const imageURL = this.getAttribute("imageURL") ?? "#"; - const targetPosAttribute = this.getAttribute("targetPos") ?? "[]"; - this.targetPos = JSON.parse(targetPosAttribute); - - // Attach cloned template to DOM - const shadowRoot = this.attachShadow({ mode: "open" }); - shadowRoot.appendChild(templateContent.cloneNode(true)); - - // Set image source URL - const content = shadowRoot.getElementById("content") as HTMLImageElement; - content.src = imageURL; - - // start geolocation services - const id = navigator.geolocation.watchPosition( - this.successCallback.bind(this), - this.errorCallback, - this.geolocationOptions - ); - - this.watchId = id; - } - - changeDescription(description: string) { - const descriptionElement = this?.shadowRoot?.getElementById( - "locked-content-description" - ); - - if (descriptionElement) { - descriptionElement.innerText = description; - } - } - - unlockContent() { - const unlockButton = this?.shadowRoot?.getElementById( - "unlock-content-button-element" - ); - - if (unlockButton) { - unlockButton.removeAttribute("locked"); - } - } + // Reactive states, template is rendered according to this states + @state() + protected _geolocationPermissionStatus: PermissionState = "prompt"; + @state() + protected _unlocked = false; + @state() + protected _arrived = false; + @state() + protected _distanceText?: string; + @state() + protected _watchId?: number; // This callback will be fired when geolocation info is available successCallback(position: GeolocationPosition) { - const pos = { - lat: position.coords.latitude, - lng: position.coords.longitude, - }; + // Set hasGeolocationPermission state true to change the template + this._geolocationPermissionStatus = "granted"; - const targetLatLng = L.latLng(this.targetPos); + // Target position must be set + if (!this.targetPosition) return; - const currentLatLng = L.latLng(pos); + // Calculate the distance between target and current position in meters + const distance = calculateDistance(position, this.targetPosition); - const betweenMeters = currentLatLng.distanceTo(targetLatLng); + // Update the text based on the distance + this._updateDistanceText(distance); - if (betweenMeters > 1000) { - this.changeDescription(`${(betweenMeters / 1000).toFixed()} KM`); - } else if (betweenMeters > 100) { - this.changeDescription(`${betweenMeters.toFixed(0)} M`); - } else { - navigator.geolocation.clearWatch(this.watchId); - this.unlockContent(); + this._checkArrived(distance); + } + + private _updateDistanceText(distance: number) { + // Update the proximity text according to the distance remaining + if (distance > 1000) { + this._distanceText = `${(distance / 1000).toFixed()} KM`; + } else if (distance > 100) { + this._distanceText = `${distance.toFixed(0)} M`; } } - // This callback will be fired on geolocation error - errorCallback(err: GeolocationPositionError) { - let errorMessage; - switch (err.code) { - case GeolocationPositionError.PERMISSION_DENIED: - errorMessage = - "Konum izni alınamadı, lütfen tarayıcınızın ve cihazınızın gizlilik ayarlarını kontrol edin."; - break; - case GeolocationPositionError.POSITION_UNAVAILABLE: - errorMessage = - "Konumunuz tespit edilemedi, lütfen biraz sonra tekrar deneyiniz."; - break; - case GeolocationPositionError.TIMEOUT: - errorMessage = - "Konum isteği zaman aşımına uğradı, lütfen sayfayı yenileyip tekrar deneyiniz."; - break; - default: - errorMessage = - "Konum izni alınamadı, lütfen tarayıcınızın ve cihazınızın gizlilik ayarlarını kontrol edin."; - break; + private _checkArrived(distance: number) { + // If target is close less then 100 meters user has arrived to target location + if (distance < 100) { + if (this._watchId) { + // Stop watching location + navigator.geolocation.clearWatch(this._watchId); + } + // Update state to reveal the image + this._arrived = true; } - - Toastify({ - text: errorMessage, - duration: 3000, - gravity: "top", // `top` or `bottom` - position: "center", // `left`, `center` or `right` - stopOnFocus: true, // Prevents dismissing of toast on hover - style: { - background: "black", - borderRadius: "6px", - margin: "16px", - }, - onClick: function () {}, // Callback after click - }).showToast(); } -} -class UnlockContentButton extends HTMLElement { - host: HTMLElement | null; - // Templates are required to create nodes - permissionTemplateContent: DocumentFragment | null; - lockedTemplateContent: DocumentFragment | null; - unlockedTemplateContent: DocumentFragment | null; + // This template is shown when user hasn't give geolocation permission yet + // When user click the button user is asked for geolocation permission + private _permissionButtonTemplate = () => + permissionButtonTemplate(this._startWatchingLocation); - // Image element to blur and show when user is on target - imageElement: HTMLElement | null; + // This template is shown when user has given permission but has not arrived yet + private _lockedButtonTemplate = () => + lockedButtonTemplate(this._distanceText); - // Content id is required to count how many times a content - // is unlocked - contentId: string | null; - - static observedAttributes = ["locked"]; - - incrementCounter = async (id: string) => - fetch(`http://localhost:3000/api/location/increment/${id}`, { - method: "PATCH", + // This template is shown when user has arrived to the target location + // When user click the button counter at the bottom of the page is incremented + // and image is revealed + private _unlockedButtonTemplate = () => + unlockedButtonTemplate(() => { + incrementUnlockCounter(this.imageId); + this._unlocked = true; }); - constructor() { - super(); + // Start watching user location, if user has not given permission yet + // this will ask the user for permission and update the watch id + private _startWatchingLocation() { + // User is already being watched no need to + // watch position + if (this._watchId) return; - const host = document.getElementById("locked-content"); - this.host = host; - - this.contentId = host?.getAttribute("contentId") ?? null; - - let permissionTemplate = host?.shadowRoot?.getElementById( - "locked-button-template" - ) as HTMLTemplateElement | null; - this.permissionTemplateContent = permissionTemplate?.content ?? null; - - let lockedTemplate = host?.shadowRoot?.getElementById( - "locked-button-template" - ) as HTMLTemplateElement | null; - this.lockedTemplateContent = lockedTemplate?.content ?? null; - - let unlockedTemplate = host?.shadowRoot?.getElementById( - "unlocked-button-template" - ) as HTMLTemplateElement | null; - this.unlockedTemplateContent = unlockedTemplate?.content ?? null; - - this.imageElement = host?.shadowRoot?.getElementById("content") ?? null; - } - - connectedCallback() { - if (this.hasAttribute("locked")) { - if (this.lockedTemplateContent) { - this.appendChild(this.lockedTemplateContent.cloneNode(true)); - } - } else { - if (this.unlockedTemplateContent) { - this.appendChild(this.unlockedTemplateContent.cloneNode(true)); - } - } - } - - attributeChangedCallback(name: string, _: string, newValue: string) { - if (name != "locked") return; - - if (newValue == "true") { - const child = this.firstElementChild; - if (this.lockedTemplateContent) - child?.replaceWith(this.lockedTemplateContent.cloneNode(true)); - this.replaceWith; - } else { - const child = this.firstElementChild; - if (this.unlockedTemplateContent) - child?.replaceWith(this.unlockedTemplateContent.cloneNode(true)); - - const unlockButton = this.host?.shadowRoot?.getElementById( - "unlock-content-button" - ); - - unlockButton?.addEventListener("click", (el) => { - if (this.contentId) { - this.incrementCounter(this.contentId); + const id = navigator.geolocation.watchPosition( + this.successCallback.bind(this), + (err) => { + if (err.code == GeolocationPositionError.PERMISSION_DENIED) { + this._geolocationPermissionStatus = "denied"; + } + errorCallback(err); + }, + this.geolocationOptions + ); + + this._watchId = id; + } + + connectedCallback(): void { + super.connectedCallback(); + + // Check geolocation permission, if user has given permission before + // start watching user location + navigator.permissions + .query({ name: "geolocation" }) + .then((permissionStatus) => { + switch (permissionStatus.state) { + case "granted": + this._startWatchingLocation(); + break; + case "denied": + this._geolocationPermissionStatus = "denied"; + case "prompt": + default: + break; } - this.imageElement?.classList.remove("blur-2xl"); - this.remove(); }); + } + + render() { + let buttonTemplate; + + // Determine which template to show, there are 3 states: + // 1 - No geolocation permission given + // 2 - Permission given but has no arrived to target position yet + // 3 - Arrived to target position + // 4 - User did not give geolocation permission + if (this._arrived) { + buttonTemplate = this._unlockedButtonTemplate; + } else if (this._geolocationPermissionStatus == "granted") { + buttonTemplate = this._lockedButtonTemplate; + } else if (this._geolocationPermissionStatus == "prompt") { + buttonTemplate = this._permissionButtonTemplate; + } else { + buttonTemplate = permissionDeniedButtonTemplate; } + + return html` +
+
+ + + ${this._unlocked ? nothing : buttonTemplate()} +
+
+ `; } } - -customElements.define("locked-content", LockedContent); -customElements.define("unlock-content-button", UnlockContentButton); diff --git a/src/pages/[id].astro b/src/pages/[id].astro index d8e03e6..d10b62b 100644 --- a/src/pages/[id].astro +++ b/src/pages/[id].astro @@ -11,7 +11,6 @@ import { CardHeader, CardTitle, } from "@/components/ui/card"; -import WebComponentWrapper from "@/components/WebComponentWrapper.astro"; import { CalendarIcon } from "@radix-ui/react-icons"; import { Separator } from "@/components/ui/separator"; import type { ContentTable } from "@/lib/db"; @@ -65,11 +64,12 @@ const dateFromNow = dayjs.utc(data?.created_at).from(dayjs.utc()); - + + +
+ + diff --git a/src/scripts/initMap.ts b/src/scripts/initMap.ts index c5419cd..6c790bc 100644 --- a/src/scripts/initMap.ts +++ b/src/scripts/initMap.ts @@ -1,5 +1,7 @@ import L from "leaflet"; +type TargetLocation = [lat: number, lng: number] | null; + const mapEl = document.getElementById("map"); var targetLocationIcon = L.icon({ @@ -14,19 +16,13 @@ var currentLocationIcon = L.icon({ const targetLocation = mapEl?.dataset.targetLocation; -const data = JSON.parse(targetLocation ?? "{}"); -const TARGET_LOCATION = data; +const data = targetLocation ? JSON.parse(targetLocation) : null; -var map = L.map("map").setView(TARGET_LOCATION, 13); +const TARGET_LOCATION = data as TargetLocation; -L.marker(TARGET_LOCATION, { icon: targetLocationIcon }).addTo(map); +var map = L.map("map"); -L.circle(TARGET_LOCATION, { - color: "blue", - fillColor: "#30f", - fillOpacity: 0.2, - radius: 50, -}).addTo(map); +if (TARGET_LOCATION) map.setView(TARGET_LOCATION, 13); L.tileLayer("https://tile.openstreetmap.org/{z}/{x}/{y}.png", { maxZoom: 19, @@ -161,7 +157,7 @@ L.Control.GoToTargetLocation = L.Control.extend({ locationButton.classList.add("custom-map-control-button"); locationButton.addEventListener("click", () => { - map.setView(TARGET_LOCATION, 18); + if (TARGET_LOCATION) map.setView(TARGET_LOCATION, 18); }); return locationButton; @@ -201,10 +197,22 @@ const targetLocationControl = L.control.targetLocation({ position: "bottomleft", }); +function addTargetLocationMarker(target: TargetLocation) { + if (target) { + L.marker(target, { icon: targetLocationIcon }).addTo(map); + + L.circle(target, { + color: "blue", + fillColor: "#30f", + fillOpacity: 0.2, + radius: 50, + }).addTo(map); + } +} + function startWatchingLocation() { goToCurrentLocationControl.addTo(map); askPermissionControl.remove(); - map.locate({ watch: true }); } function initLocationControls() { @@ -227,4 +235,5 @@ function initLocationControls() { }); } +addTargetLocationMarker(TARGET_LOCATION); initLocationControls();