feat: add geolocation features to locked content

This commit is contained in:
log101 2024-07-11 10:47:04 +03:00
parent ad19793879
commit 6aae6b37bd
7 changed files with 6936 additions and 70 deletions

BIN
bun.lockb

Binary file not shown.

6730
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -20,6 +20,7 @@
"@radix-ui/react-label": "^2.0.2",
"@radix-ui/react-separator": "^1.0.3",
"@radix-ui/react-slot": "^1.0.2",
"@types/leaflet": "^1.9.12",
"@types/react": "^18.2.47",
"@types/react-dom": "^18.2.18",
"astro": "^4.1.2",
@ -28,6 +29,7 @@
"dayjs": "^1.11.10",
"htmx.org": "^1.9.12",
"kysely": "^0.26.0",
"leaflet": "^1.9.4",
"lucide-react": "^0.309.0",
"nanoid": "^5.0.4",
"react": "^18.2.0",
@ -35,10 +37,12 @@
"tailwind-merge": "^2.2.0",
"tailwindcss": "^3.4.1",
"tailwindcss-animate": "^1.0.7",
"toastify-js": "^1.12.0",
"typescript": "^5.3.3"
},
"devDependencies": {
"@types/bun": "^1.0.5",
"@types/google.maps": "^3.54.10"
"@types/google.maps": "^3.54.10",
"@types/toastify-js": "^1.12.3"
}
}

View File

@ -22,7 +22,7 @@ const LocationButton = ({
imageUrl?: string;
location?: string;
}) => {
const [atTarget, setAtTarget] = useState(true);
const [atTarget, setAtTarget] = useState(false);
const [contentVisible, setContentVisible] = useState(false);
const [hasPermission, setHasPermission] = useState(false);
const [watchId, setWatchId] = useState<number>();
@ -30,8 +30,6 @@ const LocationButton = ({
const targetCoordinates = JSON.parse(location);
console.log("coor", targetCoordinates);
const targetPos = {
lat: targetCoordinates[0],
lng: targetCoordinates[1],
@ -57,9 +55,10 @@ const LocationButton = ({
if (betweenMeters > 1000) {
setDistanceRemain(`${(betweenMeters / 1000).toFixed()} KM`);
} else if (betweenMeters > 200) {
} else if (betweenMeters > 100) {
setDistanceRemain(`${betweenMeters.toFixed(0)} M`);
} else {
navigator.geolocation.clearWatch(id);
setAtTarget(true);
}
},

View File

@ -17,6 +17,30 @@ const { contentId = "", imageUrl = "#", location = "" } = Astro.props;
<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
@ -29,7 +53,9 @@ const { contentId = "", imageUrl = "#", location = "" } = Astro.props;
class="rounded-lg border bg-card text-card-foreground shadow-sm p-2"
>
<div class="pb-0 px-4 text-center">
İçeriği görmek için konuma gitmelisin!
<p id="locked-content-description">
İçeriği görmek için konuma gitmelisin!
</p>
</div>
</div>
</div>
@ -49,13 +75,16 @@ const { contentId = "", imageUrl = "#", location = "" } = Astro.props;
class="rounded-lg border bg-card text-card-foreground shadow-sm p-2"
>
<div class="pb-0 px-4 text-center">
İçeriği görmek için butona bas!
<p id="locked-content-description">
İçeriği görmek için butona bas!
</p>
</div>
</div>
</div>
</template>
<unlock-content-button></unlock-content-button>
<unlock-content-button locked id="unlock-content-button-element"
></unlock-content-button>
</div>
</div>
<style>
@ -77,7 +106,10 @@ const { contentId = "", imageUrl = "#", location = "" } = Astro.props;
</style>
</template>
<locked-content contentId={contentId} imageURL={imageUrl} id="locked-content"
></locked-content>
<locked-content
contentId={contentId}
imageURL={imageUrl}
targetPos={location}
id="locked-content"></locked-content>
<script src="../components/locked-content.ts"></script>

View File

@ -1,4 +1,16 @@
import L, { type LatLngTuple } from "leaflet";
import Toastify from "toastify-js";
class LockedContent extends HTMLElement {
unlockButtonElement: HTMLElement | null;
watchId: number;
targetPos: LatLngTuple;
geolocationOptions = {
enableHighAccuracy: true,
timeout: 5000,
maximumAge: 0,
};
constructor() {
super();
@ -10,6 +22,8 @@ class LockedContent extends HTMLElement {
// 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" });
@ -19,85 +33,180 @@ class LockedContent extends HTMLElement {
const content = shadowRoot.getElementById("content") as HTMLImageElement;
content.src = imageURL;
// Add onclick listener to unlock content button
const unlockContentButton = shadowRoot.getElementById(
"unlock-content-button"
// start geolocation services
const id = navigator.geolocation.watchPosition(
this.successCallback.bind(this),
this.errorCallback,
this.geolocationOptions
);
if (unlockContentButton) {
unlockContentButton.addEventListener("click", (el) => {});
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
successCallback(position: GeolocationPosition) {
const pos = {
lat: position.coords.latitude,
lng: position.coords.longitude,
};
const targetLatLng = L.latLng(this.targetPos);
const currentLatLng = L.latLng(pos);
const betweenMeters = currentLatLng.distanceTo(targetLatLng);
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 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;
}
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;
// Image element to blur and show when user is on target
imageElement: HTMLElement | null;
// Content id is required to count how many times a content
// is unlocked
contentId: string | null;
static observedAttributes = ["locked"];
private incrementCounter = async (id: string) =>
incrementCounter = async (id: string) =>
fetch(`http://localhost:3000/api/location/increment/${id}`, {
method: "PATCH",
});
constructor() {
super();
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() {
const host = document.getElementById("locked-content");
if (host) {
const contentId = host.getAttribute("contentId");
let lockedTemplate = host.shadowRoot?.getElementById(
"locked-button-template"
) as HTMLTemplateElement;
let lockedTemplateContent = lockedTemplate.content;
let unlockedTemplate = host.shadowRoot?.getElementById(
"unlocked-button-template"
) as HTMLTemplateElement;
let unlockedTemplateContent = unlockedTemplate.content;
if (this.hasAttribute("locked")) {
this.appendChild(lockedTemplateContent.cloneNode(true));
} else {
this.appendChild(unlockedTemplateContent.cloneNode(true));
if (this.hasAttribute("locked")) {
if (this.lockedTemplateContent) {
this.appendChild(this.lockedTemplateContent.cloneNode(true));
}
} else {
if (this.unlockedTemplateContent) {
this.appendChild(this.unlockedTemplateContent.cloneNode(true));
}
this.addEventListener("click", (el) => {
if (contentId) {
this.incrementCounter(contentId);
const imageContent = host.shadowRoot?.getElementById("content");
imageContent?.classList.remove("blur-2xl");
this.remove();
}
});
}
}
attributeChangedCallback(name: string, _: string, newValue: string) {
if (name != "locked") return;
const host = document.getElementById("locked-content");
if (host) {
let lockedTemplate = host.shadowRoot?.getElementById(
"locked-button-template"
) as HTMLTemplateElement;
let lockedTemplateContent = lockedTemplate.content;
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));
let unlockedTemplate = host.shadowRoot?.getElementById(
"unlocked-button-template"
) as HTMLTemplateElement;
let unlockedTemplateContent = unlockedTemplate.content;
const unlockButton = this.host?.shadowRoot?.getElementById(
"unlock-content-button"
);
if (newValue == "true") {
const child = this.firstElementChild;
child?.replaceWith(lockedTemplateContent.cloneNode(true));
this.replaceWith;
} else {
const child = this.firstElementChild;
child?.replaceWith(unlockedTemplateContent.cloneNode(true));
}
unlockButton?.addEventListener("click", (el) => {
if (this.contentId) {
this.incrementCounter(this.contentId);
}
this.imageElement?.classList.remove("blur-2xl");
this.remove();
});
}
}
}

View File

@ -28,8 +28,6 @@ const res = await fetch(`http://localhost:3000/api/location/${id}`);
const data: Content | null = res.status === 200 ? await res.json() : null;
console.log(data);
dayjs.extend(relativeTime);
dayjs.extend(utc);
@ -73,12 +71,6 @@ const dateFromNow = dayjs.utc(data?.created_at).from(dayjs.utc());
imageUrl={`http://localhost:3000/images/${data?.blob_url}`}
location={data?.loc}
/>
<LockedContent
contentId={data?.url}
imageUrl={`http://localhost:3000/images/${data?.blob_url}`}
location={data?.loc}
client:load
/>
<div
id="map"