feat: refactor locked content component

This commit is contained in:
log101 2024-07-18 14:02:45 +03:00
parent 0539bedc84
commit e7e449a079
5 changed files with 168 additions and 182 deletions

View File

@ -0,0 +1,63 @@
import Toastify from "toastify-js";
import L from "leaflet";
// This callback will be fired on geolocation error
function errorCallback(err: GeolocationPositionError) {
let errorMessage;
// Show toast accoring to the error state
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;
}
Toastify({
text: errorMessage,
duration: 3000,
gravity: "top",
position: "center",
stopOnFocus: true,
style: {
background: "black",
borderRadius: "6px",
margin: "16px",
},
onClick: function () {},
}).showToast();
}
function calculateDistance(
currentPosition: GeolocationPosition,
targetPosition: L.LatLngTuple
) {
const pos = {
lat: currentPosition.coords.latitude,
lng: currentPosition.coords.longitude,
};
// Get target position in latitudes and longitudes
const targetLatLng = L.latLng(targetPosition);
// Get current position in latitudes and longitudes
const currentLatLng = L.latLng(pos);
// Calculate the distance between target and current position in meters
const betweenMeters = currentLatLng.distanceTo(targetLatLng);
return betweenMeters;
}
export { errorCallback, calculateDistance };

View File

@ -0,0 +1,11 @@
// This counter is shown at the bottom of the page and incremented
// each time "show content" button is clicked
function incrementUnlockCounter(id: string | undefined) {
if (id) {
fetch(`http://localhost:3000/api/location/increment/${id}`, {
method: "PATCH",
});
}
}
export { incrementUnlockCounter };

View File

@ -0,0 +1,31 @@
import { html } from "lit";
// Locked lock icon
const lockSVG = html`<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
fill="#ffffff"
viewBox="0 0 256 256"
>
<path
d="M208,80H176V56a48,48,0,0,0-96,0V80H48A16,16,0,0,0,32,96V208a16,16,0,0,0,16,16H208a16,16,0,0,0,16-16V96A16,16,0,0,0,208,80ZM96,56a32,32,0,0,1,64,0V80H96ZM208,208H48V96H208V208Zm-68-56a12,12,0,1,1-12-12A12,12,0,0,1,140,152Z"
></path>
</svg>`;
// Unlocked lock icon
const unlockSVG = html`
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
fill="#ffffff"
viewBox="0 0 256 256"
>
<path
d="M208,80H96V56a32,32,0,0,1,32-32c15.37,0,29.2,11,32.16,25.59a8,8,0,0,0,15.68-3.18C171.32,24.15,151.2,8,128,8A48.05,48.05,0,0,0,80,56V80H48A16,16,0,0,0,32,96V208a16,16,0,0,0,16,16H208a16,16,0,0,0,16-16V96A16,16,0,0,0,208,80Zm0,128H48V96H208V208Zm-68-56a12,12,0,1,1-12-12A12,12,0,0,1,140,152Z"
></path>
</svg>
`;
export { lockSVG, unlockSVG };

View File

@ -1,4 +1,5 @@
import { html, type TemplateResult } from "lit"; import { html, type TemplateResult } from "lit";
import { lockSVG, unlockSVG } from "./svg";
// 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
@ -29,13 +30,13 @@ function permissionButtonTemplate(onClickHandler: () => void) {
} }
// This template is shown when user has given permission but has not arrived yet // This template is shown when user has given permission but has not arrived yet
function lockedButtonTemplate(icon: TemplateResult<1>, proximityText: string) { function lockedButtonTemplate(proximityText: string | undefined) {
return html`<div class="flex flex-col justify-center gap-4 overlay"> return html`<div class="flex flex-col justify-center gap-4 overlay">
<button <button
id="unlock-content-button" id="unlock-content-button"
class="inline-flex gap-2 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" class="inline-flex gap-2 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"
> >
${icon} ${lockSVG}
<p>İçerik Kilitli</p> <p>İçerik Kilitli</p>
</button> </button>
<div class="rounded-lg border bg-card text-card-foreground shadow-sm p-2"> <div class="rounded-lg border bg-card text-card-foreground shadow-sm p-2">
@ -51,17 +52,14 @@ function lockedButtonTemplate(icon: TemplateResult<1>, proximityText: string) {
// This template is shown when user has arrived to the target location // 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 // When user click the button counter at the bottom of the page is incremented
// and image is revealed // and image is revealed
function unlockedButtonTemplate( function unlockedButtonTemplate(onClickHandler: () => void) {
icon: TemplateResult<1>,
onClickHandler: () => void
) {
return html` <div class="flex flex-col justify-center gap-4 overlay"> return html` <div class="flex flex-col justify-center gap-4 overlay">
<button <button
@click="${onClickHandler}" @click="${onClickHandler}"
id="unlock-content-button" id="unlock-content-button"
class="inline-flex gap-2 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" class="inline-flex gap-2 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"
> >
${icon} ${unlockSVG}
<p>İçeriğin Kilidi ıldı</p> <p>İçeriğin Kilidi ıldı</p>
</button> </button>

View File

@ -1,12 +1,27 @@
// Lit imports
import { LitElement, html, nothing, unsafeCSS, type CSSResultGroup } from "lit"; import { LitElement, html, nothing, unsafeCSS, type CSSResultGroup } from "lit";
import { customElement, property, state } from "lit/decorators.js"; import { customElement, property, state } from "lit/decorators.js";
import L, { type LatLngTuple } from "leaflet"; // Leaflet
import Toastify from "toastify-js"; import { type LatLngTuple } from "leaflet";
// Styles
import globalStyles from "@/styles/globals.css?inline"; import globalStyles from "@/styles/globals.css?inline";
import lockedContentStyles from "../styles/locked-content.css?inline"; import lockedContentStyles from "../styles/locked-content.css?inline";
// Templates
import {
lockedButtonTemplate,
permissionButtonTemplate,
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") @customElement("locked-content-lit")
export class LockedContent extends LitElement { export class LockedContent extends LitElement {
// Constants // Constants
@ -37,212 +52,82 @@ export class LockedContent extends LitElement {
@state() @state()
protected _arrived = false; protected _arrived = false;
@state() @state()
protected _targetProximityText?: string; protected _distanceText?: string;
@state() @state()
protected _watchId?: number; protected _watchId?: number;
// Locked lock icon
lockSVG = html`<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
fill="#ffffff"
viewBox="0 0 256 256"
>
<path
d="M208,80H176V56a48,48,0,0,0-96,0V80H48A16,16,0,0,0,32,96V208a16,16,0,0,0,16,16H208a16,16,0,0,0,16-16V96A16,16,0,0,0,208,80ZM96,56a32,32,0,0,1,64,0V80H96ZM208,208H48V96H208V208Zm-68-56a12,12,0,1,1-12-12A12,12,0,0,1,140,152Z"
></path>
</svg>`;
// Unlocked lock icon
unlockSVG = html`
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
fill="#ffffff"
viewBox="0 0 256 256"
>
<path
d="M208,80H96V56a32,32,0,0,1,32-32c15.37,0,29.2,11,32.16,25.59a8,8,0,0,0,15.68-3.18C171.32,24.15,151.2,8,128,8A48.05,48.05,0,0,0,80,56V80H48A16,16,0,0,0,32,96V208a16,16,0,0,0,16,16H208a16,16,0,0,0,16-16V96A16,16,0,0,0,208,80Zm0,128H48V96H208V208Zm-68-56a12,12,0,1,1-12-12A12,12,0,0,1,140,152Z"
></path>
</svg>
`;
// 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) {
// Set hasGeolocationPermission state true to change the template // Set hasGeolocationPermission state true to change the template
if (!this._hasGeolocationPermission) this._hasGeolocationPermission = true; if (!this._hasGeolocationPermission) this._hasGeolocationPermission = true;
const pos = { // Target position must be set
lat: position.coords.latitude,
lng: position.coords.longitude,
};
// targetPosition attribute must be set for geolocation feature to work
if (!this.targetPosition) return; if (!this.targetPosition) return;
// Get target position in latitudes and longitudes
const targetLatLng = L.latLng(this.targetPosition);
// Get current position in latitudes and longitudes
const currentLatLng = L.latLng(pos);
// Calculate the distance between target and current position in meters // Calculate the distance between target and current position in meters
const betweenMeters = currentLatLng.distanceTo(targetLatLng); 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 // Update the proximity text according to the distance remaining
if (betweenMeters > 1000) { if (distance > 1000) {
this._targetProximityText = `${(betweenMeters / 1000).toFixed()} KM`; this._distanceText = `${(distance / 1000).toFixed()} KM`;
} else if (betweenMeters > 100) { } else if (distance > 100) {
this._targetProximityText = `${betweenMeters.toFixed(0)} M`; this._distanceText = `${distance.toFixed(0)} M`;
} else {
// If target is close less then 100 meters user has arrived to target location
if (this._watchId) {
// Stop watching location
navigator.geolocation.clearWatch(this._watchId);
// Update state to reveal the image
this._arrived = true;
}
} }
} }
// 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) {
// Show toast accoring to the error state if (this._watchId) {
switch (err.code) { // Stop watching location
case GeolocationPositionError.PERMISSION_DENIED: navigator.geolocation.clearWatch(this._watchId);
errorMessage = }
"Konum izni alınamadı, lütfen tarayıcınızın ve cihazınızın gizlilik ayarlarını kontrol edin."; // Update state to reveal the image
break; this._arrived = true;
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;
} }
Toastify({
text: errorMessage,
duration: 3000,
gravity: "top",
position: "center",
stopOnFocus: true,
style: {
background: "black",
borderRadius: "6px",
margin: "16px",
},
onClick: function () {},
}).showToast();
} }
// 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
permissionButtonTemplate() { private _permissionButtonTemplate = () =>
return html` permissionButtonTemplate(this._startWatchingLocation);
<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"
>
İç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
@click="${this._startWatchingLocation}"
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>
`;
}
// This template is shown when user has given permission but has not arrived yet // This template is shown when user has given permission but has not arrived yet
lockedButtonTemplate() { private _lockedButtonTemplate = () =>
return html`<div class="flex flex-col justify-center gap-4 overlay"> lockedButtonTemplate(this._distanceText);
<button
id="unlock-content-button"
class="inline-flex gap-2 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"
>
${this.lockSVG}
<p>İçerik Kilitli</p>
</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! Kalan mesafe:
${this._targetProximityText}
</p>
</div>
</div>
</div>`;
}
// This template is shown when user has arrived to the target location // 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 // When user click the button counter at the bottom of the page is incremented
// and image is revealed // and image is revealed
unlockedButtonTemplate() { private _unlockedButtonTemplate = () =>
return html` <div class="flex flex-col justify-center gap-4 overlay"> unlockedButtonTemplate(() => {
<button incrementUnlockCounter(this.imageId);
@click="${() => { this._unlocked = true;
this._incrementUnlockCounter(this.imageId); });
this._unlocked = true;
}}"
id="unlock-content-button"
class="inline-flex gap-2 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"
>
${this.unlockSVG}
<p>İçeriğin Kilidi ıldı</p>
</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>`;
}
// Start watching user location, if user has not given permission yet // Start watching user location, if user has not given permission yet
// this will ask the user for permission and update the watch id // this will ask the user for permission and update the watch id
private _startWatchingLocation() { private _startWatchingLocation() {
// User is already being watched no need to
// watch position
if (this._watchId) return;
const id = navigator.geolocation.watchPosition( const id = navigator.geolocation.watchPosition(
this.successCallback.bind(this), this.successCallback.bind(this),
this.errorCallback.bind(this), errorCallback,
this.geolocationOptions this.geolocationOptions
); );
this._watchId = id; this._watchId = id;
} }
// This counter is shown at the bottom of the page and incremented
// each time "show content" button is clicked
private async _incrementUnlockCounter(id: string | undefined) {
if (id) {
fetch(`http://localhost:3000/api/location/increment/${id}`, {
method: "PATCH",
});
}
}
connectedCallback(): void { connectedCallback(): void {
super.connectedCallback(); super.connectedCallback();
@ -253,13 +138,11 @@ export class LockedContent extends LitElement {
.then((permissionStatus) => { .then((permissionStatus) => {
switch (permissionStatus.state) { switch (permissionStatus.state) {
case "granted": case "granted":
this._hasGeolocationPermission = true;
this._startWatchingLocation(); this._startWatchingLocation();
break; break;
case "denied": case "denied":
case "prompt": case "prompt":
default: default:
this._hasGeolocationPermission = false;
break; break;
} }
}); });
@ -274,11 +157,11 @@ export class LockedContent extends LitElement {
// 3 - Arrived to target position // 3 - Arrived to target position
// 4 - User did not give geolocation permission // 4 - User did not give geolocation permission
if (this._arrived) { if (this._arrived) {
buttonTemplate = this.unlockedButtonTemplate.bind(this); buttonTemplate = this._unlockedButtonTemplate;
} else if (this._hasGeolocationPermission) { } else if (this._hasGeolocationPermission) {
buttonTemplate = this.lockedButtonTemplate.bind(this); buttonTemplate = this._lockedButtonTemplate;
} else { } else {
buttonTemplate = this.permissionButtonTemplate.bind(this); buttonTemplate = this._permissionButtonTemplate;
} }
return html` return html`
@ -289,7 +172,7 @@ export class LockedContent extends LitElement {
<img <img
id="content" id="content"
src="${this.imageURL}" src="${this.imageURL}"
class="${this._unlocked ? nothing : "blur-2xl"} h-[450px]" class="h-[450px] ${this._unlocked ? "" : "blur-2xl"}"
/> />
${this._unlocked ? nothing : buttonTemplate()} ${this._unlocked ? nothing : buttonTemplate()}