feat: add geolocation features to locked content
This commit is contained in:
parent
ad19793879
commit
6aae6b37bd
6730
package-lock.json
generated
Normal file
6730
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
},
|
||||
|
|
|
@ -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">
|
||||
<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">
|
||||
<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>
|
||||
|
|
|
@ -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));
|
||||
if (this.lockedTemplateContent) {
|
||||
this.appendChild(this.lockedTemplateContent.cloneNode(true));
|
||||
}
|
||||
} 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) {
|
||||
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") {
|
||||
const child = this.firstElementChild;
|
||||
child?.replaceWith(lockedTemplateContent.cloneNode(true));
|
||||
if (this.lockedTemplateContent)
|
||||
child?.replaceWith(this.lockedTemplateContent.cloneNode(true));
|
||||
this.replaceWith;
|
||||
} else {
|
||||
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();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
|
|
Loading…
Reference in New Issue
Block a user