feat: move leaflet map to a web component

This commit is contained in:
log101 2024-07-19 16:38:58 +03:00
parent 430c50b3ae
commit d0df5fbbc6
10 changed files with 243 additions and 676 deletions

View File

@ -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<string>) =>
await fetch(`http://localhost:3000/api/location/increment/${id}`, {
method: "PATCH",
});
const LocationButton = ({
contentId = "",
imageUrl = "#",
location = "",
}: {
contentId?: string | Generated<string>;
imageUrl?: string;
location?: string;
}) => {
const [atTarget, setAtTarget] = useState(false);
const [contentVisible, setContentVisible] = useState(false);
const [hasPermission, setHasPermission] = useState(false);
const [watchId, setWatchId] = useState<number>();
const [distanceRemain, setDistanceRemain] = useState<string>("");
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 (
<div className="w-full h-[475px] p-4 flex justify-center">
<img src={imageUrl} />
</div>
);
} else {
return (
<>
<div className="w-full h-[475px] overflow-hidden border border-zinc-200 shadow-sm p-4 rounded">
{atTarget ? (
<div className="flex flex-col justify-center items-center image-wrapper">
<img src={imageUrl} className="blur-2xl h-[450px]" />
<div className="flex flex-col justify-center gap-4 overlay">
<Button
size="lg"
className="text-lg p-6 animate-pulse bg-indigo-600 hover:bg-indigo-700 hover:animate-none border-2 border-indigo-800"
onClick={handleUnlock}
>
<LockOpen1Icon className="mr-2 h-4 w-4" />
İçeriğin Kilidi ıldı
</Button>
<Card className="p-2">
<CardContent className="pb-0 text-center">
İçeriği görmek için butona bas!
</CardContent>
</Card>
</div>
</div>
) : (
<div className="flex flex-col justify-center items-center image-wrapper">
<img src={imageUrl} className="blur-2xl h-[450px]" />
<div className="flex flex-col justify-center gap-4 overlay">
<Button size="lg" className="text-md">
<LockClosedIcon className="mr-2 h-4 w-4" /> İçerik Kilitli
</Button>
<Card className="p-2">
{hasPermission ? (
<CardContent className="pb-0 text-center">
<p>İçeriği görmek için konuma gitmelisin!</p>
<p>
{distanceRemain && `Kalan mesafe: ${distanceRemain}`}
</p>
</CardContent>
) : (
<div className="flex flex-col gap-2">
<CardContent className="pb-0 text-center">
Ne kadar yaklaştığını görmek için aşağıdaki butona bas.
</CardContent>
<Button
size="sm"
className="bg-green-700 hover:bg-green-600 text-md"
onClick={() => startWatchingLocation()}
>
Konum İzni Ver
</Button>
</div>
)}
</Card>
</div>
</div>
)}
</div>
</>
);
}
};
export default LocationButton;

View File

@ -1,5 +1,5 @@
import { html } from "lit"; 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 // This template is shown when user hasn't give geolocation permission yet
// When user click the button user is asked for geolocation permission // When user click the button user is asked for geolocation permission

View File

@ -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;
---
<template id="locked-content-template">
<div
class="w-full h-[475px] overflow-hidden border border-zinc-200 shadow-sm p-4 rounded"
>
<div class="flex flex-col justify-center items-center image-wrapper">
<img id="content" src="#" class="blur-2xl h-[450px]" />
<template id="permission-button-template">
<div class="flex flex-col justify-center gap-4 overlay">
<button
id="unlock-content-button"
class="inline-flex items-center justify-center whitespace-nowrap font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 bg-primary text-primary-foreground hover:bg-primary/90 h-11 rounded-md text-lg p-6 text-md"
>
<LockClosedIcon className="mr-2 h-4 w-4" /> İçerik Kilitli
</button>
<div
class="rounded-lg border bg-card text-card-foreground shadow-sm p-2"
>
<div class="pb-0 text-center flex flex-col gap-4">
<p id="locked-content-description">
Ne kadar yaklaştığını görmek için aşağıdaki butona bas.
</p>
<button
class="inline-flex items-center justify-center whitespace-nowrap font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 text-primary-foreground h-9 rounded-md px-3 bg-green-700 hover:bg-green-600 text-md"
>Konum İzni Ver</button
>
</div>
</div>
</div>
</template>
<template id="locked-button-template">
<div class="flex flex-col justify-center gap-4 overlay">
<button
id="unlock-content-button"
class="inline-flex items-center justify-center whitespace-nowrap font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 bg-primary text-primary-foreground hover:bg-primary/90 h-11 rounded-md text-lg p-6 text-md"
>
<LockClosedIcon className="mr-2 h-4 w-4" /> İçerik Kilitli
</button>
<div
class="rounded-lg border bg-card text-card-foreground shadow-sm p-2"
>
<div class="pb-0 px-4 text-center">
<p id="locked-content-description">
İçeriği görmek için konuma gitmelisin!
</p>
</div>
</div>
</div>
</template>
<template id="unlocked-button-template">
<div class="flex flex-col justify-center gap-4 overlay">
<button
id="unlock-content-button"
class="inline-flex items-center justify-center whitespace-nowrap font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 text-primary-foreground h-11 rounded-md text-lg p-6 animate-pulse bg-indigo-600 hover:bg-indigo-700 hover:animate-none border-2 border-indigo-800"
>
<LockOpen1Icon className="mr-2 h-4 w-4" />
İçeriğin Kilidi Açıldı
</button>
<div
class="rounded-lg border bg-card text-card-foreground shadow-sm p-2"
>
<div class="pb-0 px-4 text-center">
<p id="locked-content-description">
İçeriği görmek için butona bas!
</p>
</div>
</div>
</div>
</template>
<unlock-content-button locked id="unlock-content-button-element"
></unlock-content-button>
</div>
</div>
<style>
@import url("css/tailwind.css");
.image-wrapper {
position: relative;
}
.overlay {
position: absolute;
/* center overlay text */
display: flex;
align-items: center;
justify-content: center;
inset: 0;
}
</style>
</template>
<!--
<locked-content
contentId={contentId}
imageURL={imageUrl}
targetPos={location}
id="locked-content"></locked-content>
-->
<locked-content-lit
imageId={contentId}
imageURL={imageUrl}
targetPosition={location}></locked-content-lit>
<script src="../components/locked-content.ts"></script>
<script src="../components/locked-content-lit.ts"></script>

View File

@ -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`<div
id="leaflet-map-container"
class="w-full h-[450px] rounded"
></div>`;
}
}

View File

@ -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`
<div
class="w-full h-[475px] overflow-hidden border border-zinc-200 shadow-sm p-4 rounded"
>
<div class="flex flex-col justify-center items-center image-wrapper">
<img
id="content"
src="${this.imageURL}"
class="h-[450px] ${this._unlocked ? "" : "blur-2xl"}"
/>
${this._unlocked ? nothing : buttonTemplate()}
</div>
</div>
`;
}
}

View File

@ -1,214 +1,192 @@
import L, { type LatLngTuple } from "leaflet"; // Lit imports
import Toastify from "toastify-js"; import { LitElement, html, nothing, unsafeCSS, type CSSResultGroup } from "lit";
import { customElement, property, state } from "lit/decorators.js";
class LockedContent extends HTMLElement { // Leaflet
watchId: number; import { type LatLngTuple } from "leaflet";
targetPos: LatLngTuple;
// 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 = { geolocationOptions = {
enableHighAccuracy: true, enableHighAccuracy: true,
timeout: 5000, timeout: 15000,
maximumAge: 0, maximumAge: 0,
}; };
constructor() { // Tailwind and custom styles
super(); static styles: CSSResultGroup | undefined = [
unsafeCSS(globalStyles),
unsafeCSS(lockedContentStyles),
];
// Clone the template // Components properties/attributes, no accessor attribute disables detecting
let template = document.getElementById( // changes as these are readonly attriubtes there is no need to attach setters
"locked-content-template" @property({ noAccessor: true }) readonly imageId?: string;
) as HTMLTemplateElement; @property({ noAccessor: true }) readonly imageURL?: string;
let templateContent = template.content; @property({ type: Object, noAccessor: true })
readonly targetPosition?: LatLngTuple;
// Get attributes // Reactive states, template is rendered according to this states
const imageURL = this.getAttribute("imageURL") ?? "#"; @state()
const targetPosAttribute = this.getAttribute("targetPos") ?? "[]"; protected _geolocationPermissionStatus: PermissionState = "prompt";
this.targetPos = JSON.parse(targetPosAttribute); @state()
protected _unlocked = false;
// Attach cloned template to DOM @state()
const shadowRoot = this.attachShadow({ mode: "open" }); protected _arrived = false;
shadowRoot.appendChild(templateContent.cloneNode(true)); @state()
protected _distanceText?: string;
// Set image source URL @state()
const content = shadowRoot.getElementById("content") as HTMLImageElement; protected _watchId?: number;
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");
}
}
// This callback will be fired when geolocation info is available // This callback will be fired when geolocation info is available
successCallback(position: GeolocationPosition) { successCallback(position: GeolocationPosition) {
const pos = { // Set hasGeolocationPermission state true to change the template
lat: position.coords.latitude, this._geolocationPermissionStatus = "granted";
lng: position.coords.longitude,
};
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._checkArrived(distance);
this.changeDescription(`${(betweenMeters / 1000).toFixed()} KM`); }
} else if (betweenMeters > 100) {
this.changeDescription(`${betweenMeters.toFixed(0)} M`); private _updateDistanceText(distance: number) {
} else { // Update the proximity text according to the distance remaining
navigator.geolocation.clearWatch(this.watchId); if (distance > 1000) {
this.unlockContent(); this._distanceText = `${(distance / 1000).toFixed()} KM`;
} else if (distance > 100) {
this._distanceText = `${distance.toFixed(0)} M`;
} }
} }
// This callback will be fired on geolocation error private _checkArrived(distance: number) {
errorCallback(err: GeolocationPositionError) { // If target is close less then 100 meters user has arrived to target location
let errorMessage; if (distance < 100) {
switch (err.code) { if (this._watchId) {
case GeolocationPositionError.PERMISSION_DENIED: // Stop watching location
errorMessage = navigator.geolocation.clearWatch(this._watchId);
"Konum izni alınamadı, lütfen tarayıcınızın ve cihazınızın gizlilik ayarlarını kontrol edin."; }
break; // Update state to reveal the image
case GeolocationPositionError.POSITION_UNAVAILABLE: this._arrived = true;
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;
} }
Toastify({ // This template is shown when user hasn't give geolocation permission yet
text: errorMessage, // When user click the button user is asked for geolocation permission
duration: 3000, private _permissionButtonTemplate = () =>
gravity: "top", // `top` or `bottom` permissionButtonTemplate(this._startWatchingLocation);
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 { // This template is shown when user has given permission but has not arrived yet
host: HTMLElement | null; private _lockedButtonTemplate = () =>
// Templates are required to create nodes lockedButtonTemplate(this._distanceText);
permissionTemplateContent: DocumentFragment | null;
lockedTemplateContent: DocumentFragment | null;
unlockedTemplateContent: DocumentFragment | null;
// Image element to blur and show when user is on target // This template is shown when user has arrived to the target location
imageElement: HTMLElement | null; // When user click the button counter at the bottom of the page is incremented
// and image is revealed
// Content id is required to count how many times a content private _unlockedButtonTemplate = () =>
// is unlocked unlockedButtonTemplate(() => {
contentId: string | null; incrementUnlockCounter(this.imageId);
this._unlocked = true;
static observedAttributes = ["locked"];
incrementCounter = async (id: string) =>
fetch(`http://localhost:3000/api/location/increment/${id}`, {
method: "PATCH",
}); });
constructor() { // Start watching user location, if user has not given permission yet
super(); // 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"); const id = navigator.geolocation.watchPosition(
this.host = host; this.successCallback.bind(this),
(err) => {
this.contentId = host?.getAttribute("contentId") ?? null; if (err.code == GeolocationPositionError.PERMISSION_DENIED) {
this._geolocationPermissionStatus = "denied";
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;
} }
errorCallback(err);
connectedCallback() { },
if (this.hasAttribute("locked")) { this.geolocationOptions
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) => { this._watchId = id;
if (this.contentId) { }
this.incrementCounter(this.contentId);
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`
<div
class="w-full h-[475px] overflow-hidden border border-zinc-200 shadow-sm p-4 rounded"
>
<div class="flex flex-col justify-center items-center image-wrapper">
<img
id="content"
src="${this.imageURL}"
class="h-[450px] ${this._unlocked ? "" : "blur-2xl"}"
/>
${this._unlocked ? nothing : buttonTemplate()}
</div>
</div>
`;
} }
} }
customElements.define("locked-content", LockedContent);
customElements.define("unlock-content-button", UnlockContentButton);

View File

@ -11,7 +11,6 @@ import {
CardHeader, CardHeader,
CardTitle, CardTitle,
} from "@/components/ui/card"; } from "@/components/ui/card";
import WebComponentWrapper from "@/components/WebComponentWrapper.astro";
import { CalendarIcon } from "@radix-ui/react-icons"; import { CalendarIcon } from "@radix-ui/react-icons";
import { Separator } from "@/components/ui/separator"; import { Separator } from "@/components/ui/separator";
import type { ContentTable } from "@/lib/db"; import type { ContentTable } from "@/lib/db";
@ -65,11 +64,12 @@ const dateFromNow = dayjs.utc(data?.created_at).from(dayjs.utc());
</CardFooter> </CardFooter>
</Card> </Card>
<WebComponentWrapper <locked-content
contentId={data?.url} imageId={data?.url}
imageUrl={`http://localhost:3000/images/${data?.blob_url}`} imageURL={`http://localhost:3000/images/${data?.blob_url}`}
location={data?.loc} targetPosition={data?.loc}></locked-content>
/>
<leaflet-map targetLocation={data?.loc}></leaflet-map>
<div <div
id="map" id="map"
@ -86,4 +86,6 @@ const dateFromNow = dayjs.utc(data?.created_at).from(dayjs.utc());
</div> </div>
</main> </main>
<script src="../scripts/initMap.ts"></script> <script src="../scripts/initMap.ts"></script>
<script src="../components/locked-content.ts"></script>
<script src="../components/leaflet-map.ts"></script>
</Layout> </Layout>

View File

@ -1,5 +1,7 @@
import L from "leaflet"; import L from "leaflet";
type TargetLocation = [lat: number, lng: number] | null;
const mapEl = document.getElementById("map"); const mapEl = document.getElementById("map");
var targetLocationIcon = L.icon({ var targetLocationIcon = L.icon({
@ -14,19 +16,13 @@ var currentLocationIcon = L.icon({
const targetLocation = mapEl?.dataset.targetLocation; const targetLocation = mapEl?.dataset.targetLocation;
const data = JSON.parse(targetLocation ?? "{}"); const data = targetLocation ? JSON.parse(targetLocation) : null;
const TARGET_LOCATION = data;
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, { if (TARGET_LOCATION) map.setView(TARGET_LOCATION, 13);
color: "blue",
fillColor: "#30f",
fillOpacity: 0.2,
radius: 50,
}).addTo(map);
L.tileLayer("https://tile.openstreetmap.org/{z}/{x}/{y}.png", { L.tileLayer("https://tile.openstreetmap.org/{z}/{x}/{y}.png", {
maxZoom: 19, maxZoom: 19,
@ -161,7 +157,7 @@ L.Control.GoToTargetLocation = L.Control.extend({
locationButton.classList.add("custom-map-control-button"); locationButton.classList.add("custom-map-control-button");
locationButton.addEventListener("click", () => { locationButton.addEventListener("click", () => {
map.setView(TARGET_LOCATION, 18); if (TARGET_LOCATION) map.setView(TARGET_LOCATION, 18);
}); });
return locationButton; return locationButton;
@ -201,10 +197,22 @@ const targetLocationControl = L.control.targetLocation({
position: "bottomleft", 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() { function startWatchingLocation() {
goToCurrentLocationControl.addTo(map); goToCurrentLocationControl.addTo(map);
askPermissionControl.remove(); askPermissionControl.remove();
map.locate({ watch: true });
} }
function initLocationControls() { function initLocationControls() {
@ -227,4 +235,5 @@ function initLocationControls() {
}); });
} }
addTargetLocationMarker(TARGET_LOCATION);
initLocationControls(); initLocationControls();