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-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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"
|
||||||
|
|
Loading…
Reference in New Issue
Block a user