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-label": "^2.0.2",
"@radix-ui/react-separator": "^1.0.3", "@radix-ui/react-separator": "^1.0.3",
"@radix-ui/react-slot": "^1.0.2", "@radix-ui/react-slot": "^1.0.2",
"@types/leaflet": "^1.9.12",
"@types/react": "^18.2.47", "@types/react": "^18.2.47",
"@types/react-dom": "^18.2.18", "@types/react-dom": "^18.2.18",
"astro": "^4.1.2", "astro": "^4.1.2",
@ -28,6 +29,7 @@
"dayjs": "^1.11.10", "dayjs": "^1.11.10",
"htmx.org": "^1.9.12", "htmx.org": "^1.9.12",
"kysely": "^0.26.0", "kysely": "^0.26.0",
"leaflet": "^1.9.4",
"lucide-react": "^0.309.0", "lucide-react": "^0.309.0",
"nanoid": "^5.0.4", "nanoid": "^5.0.4",
"react": "^18.2.0", "react": "^18.2.0",
@ -35,10 +37,12 @@
"tailwind-merge": "^2.2.0", "tailwind-merge": "^2.2.0",
"tailwindcss": "^3.4.1", "tailwindcss": "^3.4.1",
"tailwindcss-animate": "^1.0.7", "tailwindcss-animate": "^1.0.7",
"toastify-js": "^1.12.0",
"typescript": "^5.3.3" "typescript": "^5.3.3"
}, },
"devDependencies": { "devDependencies": {
"@types/bun": "^1.0.5", "@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; imageUrl?: string;
location?: string; location?: string;
}) => { }) => {
const [atTarget, setAtTarget] = useState(true); const [atTarget, setAtTarget] = useState(false);
const [contentVisible, setContentVisible] = useState(false); const [contentVisible, setContentVisible] = useState(false);
const [hasPermission, setHasPermission] = useState(false); const [hasPermission, setHasPermission] = useState(false);
const [watchId, setWatchId] = useState<number>(); const [watchId, setWatchId] = useState<number>();
@ -30,8 +30,6 @@ const LocationButton = ({
const targetCoordinates = JSON.parse(location); const targetCoordinates = JSON.parse(location);
console.log("coor", targetCoordinates);
const targetPos = { const targetPos = {
lat: targetCoordinates[0], lat: targetCoordinates[0],
lng: targetCoordinates[1], lng: targetCoordinates[1],
@ -57,9 +55,10 @@ const LocationButton = ({
if (betweenMeters > 1000) { if (betweenMeters > 1000) {
setDistanceRemain(`${(betweenMeters / 1000).toFixed()} KM`); setDistanceRemain(`${(betweenMeters / 1000).toFixed()} KM`);
} else if (betweenMeters > 200) { } else if (betweenMeters > 100) {
setDistanceRemain(`${betweenMeters.toFixed(0)} M`); setDistanceRemain(`${betweenMeters.toFixed(0)} M`);
} else { } else {
navigator.geolocation.clearWatch(id);
setAtTarget(true); 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"> <div class="flex flex-col justify-center items-center image-wrapper">
<img id="content" src="#" class="blur-2xl h-[450px]" /> <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"> <template id="locked-button-template">
<div class="flex flex-col justify-center gap-4 overlay"> <div class="flex flex-col justify-center gap-4 overlay">
<button <button
@ -29,7 +53,9 @@ const { contentId = "", imageUrl = "#", location = "" } = Astro.props;
class="rounded-lg border bg-card text-card-foreground shadow-sm p-2" class="rounded-lg border bg-card text-card-foreground shadow-sm p-2"
> >
<div class="pb-0 px-4 text-center"> <div class="pb-0 px-4 text-center">
<p id="locked-content-description">
İçeriği görmek için konuma gitmelisin! İçeriği görmek için konuma gitmelisin!
</p>
</div> </div>
</div> </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" class="rounded-lg border bg-card text-card-foreground shadow-sm p-2"
> >
<div class="pb-0 px-4 text-center"> <div class="pb-0 px-4 text-center">
<p id="locked-content-description">
İçeriği görmek için butona bas! İçeriği görmek için butona bas!
</p>
</div> </div>
</div> </div>
</div> </div>
</template> </template>
<unlock-content-button></unlock-content-button> <unlock-content-button locked id="unlock-content-button-element"
></unlock-content-button>
</div> </div>
</div> </div>
<style> <style>
@ -77,7 +106,10 @@ const { contentId = "", imageUrl = "#", location = "" } = Astro.props;
</style> </style>
</template> </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> <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 { class LockedContent extends HTMLElement {
unlockButtonElement: HTMLElement | null;
watchId: number;
targetPos: LatLngTuple;
geolocationOptions = {
enableHighAccuracy: true,
timeout: 5000,
maximumAge: 0,
};
constructor() { constructor() {
super(); super();
@ -10,6 +22,8 @@ class LockedContent extends HTMLElement {
// Get attributes // Get attributes
const imageURL = this.getAttribute("imageURL") ?? "#"; const imageURL = this.getAttribute("imageURL") ?? "#";
const targetPosAttribute = this.getAttribute("targetPos") ?? "[]";
this.targetPos = JSON.parse(targetPosAttribute);
// Attach cloned template to DOM // Attach cloned template to DOM
const shadowRoot = this.attachShadow({ mode: "open" }); const shadowRoot = this.attachShadow({ mode: "open" });
@ -19,85 +33,180 @@ class LockedContent extends HTMLElement {
const content = shadowRoot.getElementById("content") as HTMLImageElement; const content = shadowRoot.getElementById("content") as HTMLImageElement;
content.src = imageURL; content.src = imageURL;
// Add onclick listener to unlock content button // start geolocation services
const unlockContentButton = shadowRoot.getElementById( const id = navigator.geolocation.watchPosition(
"unlock-content-button" this.successCallback.bind(this),
this.errorCallback,
this.geolocationOptions
); );
if (unlockContentButton) { this.watchId = id;
unlockContentButton.addEventListener("click", (el) => {});
} }
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 { 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"]; static observedAttributes = ["locked"];
private incrementCounter = async (id: string) => incrementCounter = async (id: string) =>
fetch(`http://localhost:3000/api/location/increment/${id}`, { fetch(`http://localhost:3000/api/location/increment/${id}`, {
method: "PATCH", method: "PATCH",
}); });
constructor() { constructor() {
super(); 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() { 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")) { if (this.hasAttribute("locked")) {
this.appendChild(lockedTemplateContent.cloneNode(true)); if (this.lockedTemplateContent) {
this.appendChild(this.lockedTemplateContent.cloneNode(true));
}
} else { } else {
this.appendChild(unlockedTemplateContent.cloneNode(true)); 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) { attributeChangedCallback(name: string, _: string, newValue: string) {
if (name != "locked") return; 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;
let unlockedTemplate = host.shadowRoot?.getElementById(
"unlocked-button-template"
) as HTMLTemplateElement;
let unlockedTemplateContent = unlockedTemplate.content;
if (newValue == "true") { if (newValue == "true") {
const child = this.firstElementChild; const child = this.firstElementChild;
child?.replaceWith(lockedTemplateContent.cloneNode(true)); if (this.lockedTemplateContent)
child?.replaceWith(this.lockedTemplateContent.cloneNode(true));
this.replaceWith; this.replaceWith;
} else { } else {
const child = this.firstElementChild; const child = this.firstElementChild;
child?.replaceWith(unlockedTemplateContent.cloneNode(true)); 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);
} }
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; const data: Content | null = res.status === 200 ? await res.json() : null;
console.log(data);
dayjs.extend(relativeTime); dayjs.extend(relativeTime);
dayjs.extend(utc); 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}`} imageUrl={`http://localhost:3000/images/${data?.blob_url}`}
location={data?.loc} location={data?.loc}
/> />
<LockedContent
contentId={data?.url}
imageUrl={`http://localhost:3000/images/${data?.blob_url}`}
location={data?.loc}
client:load
/>
<div <div
id="map" id="map"