feat: add standalone templates

This commit is contained in:
log101 2024-07-17 18:23:55 +03:00
parent e406ed40a6
commit 0539bedc84
4 changed files with 169 additions and 33 deletions

BIN
bun.lockb

Binary file not shown.

View File

@ -12,38 +12,38 @@
"dependencies": { "dependencies": {
"@astrojs/check": "^0.4.1", "@astrojs/check": "^0.4.1",
"@astrojs/node": "^8.3.2", "@astrojs/node": "^8.3.2",
"@astrojs/react": "^3.0.9", "@astrojs/react": "^3.6.0",
"@astrojs/tailwind": "^5.1.0", "@astrojs/tailwind": "^5.1.0",
"@astrojs/vercel": "^7.3.1", "@astrojs/vercel": "^7.7.2",
"@radix-ui/react-checkbox": "^1.0.4", "@radix-ui/react-checkbox": "^1.1.1",
"@radix-ui/react-icons": "^1.3.0", "@radix-ui/react-icons": "^1.3.0",
"@radix-ui/react-label": "^2.0.2", "@radix-ui/react-label": "^2.1.0",
"@radix-ui/react-separator": "^1.0.3", "@radix-ui/react-separator": "^1.1.0",
"@radix-ui/react-slot": "^1.0.2", "@radix-ui/react-slot": "^1.1.0",
"@types/leaflet": "^1.9.12", "@types/leaflet": "^1.9.12",
"@types/react": "^18.2.47", "@types/react": "^18.3.3",
"@types/react-dom": "^18.2.18", "@types/react-dom": "^18.3.0",
"astro": "^4.1.2", "astro": "^4.11.6",
"class-variance-authority": "^0.7.0", "class-variance-authority": "^0.7.0",
"clsx": "^2.1.0", "clsx": "^2.1.1",
"dayjs": "^1.11.10", "dayjs": "^1.11.11",
"htmx.org": "^1.9.12", "htmx.org": "^1.9.12",
"kysely": "^0.26.0", "kysely": "^0.26.3",
"leaflet": "^1.9.4", "leaflet": "^1.9.4",
"lit": "^3.1.4", "lit": "^3.1.4",
"lucide-react": "^0.309.0", "lucide-react": "^0.309.0",
"nanoid": "^5.0.4", "nanoid": "^5.0.7",
"react": "^18.2.0", "react": "^18.3.1",
"react-dom": "^18.2.0", "react-dom": "^18.3.1",
"tailwind-merge": "^2.2.0", "tailwind-merge": "^2.4.0",
"tailwindcss": "^3.4.1", "tailwindcss": "^3.4.6",
"tailwindcss-animate": "^1.0.7", "tailwindcss-animate": "^1.0.7",
"toastify-js": "^1.12.0", "toastify-js": "^1.12.0",
"typescript": "^5.3.3" "typescript": "^5.5.3"
}, },
"devDependencies": { "devDependencies": {
"@types/bun": "^1.0.5", "@types/bun": "^1.1.6",
"@types/google.maps": "^3.54.10", "@types/google.maps": "^3.55.11",
"@types/toastify-js": "^1.12.3" "@types/toastify-js": "^1.12.3"
} }
} }

View File

@ -0,0 +1,80 @@
import { html, type TemplateResult } from "lit";
// This template is shown when user hasn't give geolocation permission yet
// When user click the button user is asked for geolocation permission
function permissionButtonTemplate(onClickHandler: () => void) {
return html`
<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="${onClickHandler}"
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
function lockedButtonTemplate(icon: TemplateResult<1>, proximityText: string) {
return html`<div class="flex flex-col justify-center gap-4 overlay">
<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"
>
${icon}
<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: ${proximityText}
</p>
</div>
</div>
</div>`;
}
// 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
// and image is revealed
function unlockedButtonTemplate(
icon: TemplateResult<1>,
onClickHandler: () => void
) {
return html` <div class="flex flex-col justify-center gap-4 overlay">
<button
@click="${onClickHandler}"
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"
>
${icon}
<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>`;
}
export {
lockedButtonTemplate,
unlockedButtonTemplate,
permissionButtonTemplate,
};

View File

@ -22,14 +22,14 @@ export class LockedContent extends LitElement {
unsafeCSS(lockedContentStyles), unsafeCSS(lockedContentStyles),
]; ];
// Static properties, no accessor attribute disables detecting changes // Components properties/attributes, no accessor attribute disables detecting
// as these are readonly attriubtes there is no need to attach setters // changes as these are readonly attriubtes there is no need to attach setters
@property({ noAccessor: true }) readonly imageId?: string; @property({ noAccessor: true }) readonly imageId?: string;
@property({ noAccessor: true }) readonly imageURL?: string; @property({ noAccessor: true }) readonly imageURL?: string;
@property({ type: Object, noAccessor: true }) @property({ type: Object, noAccessor: true })
readonly targetPosition?: LatLngTuple; readonly targetPosition?: LatLngTuple;
// Reactive state // Reactive states, template is rendered according to this states
@state() @state()
protected _hasGeolocationPermission = false; protected _hasGeolocationPermission = false;
@state() @state()
@ -41,8 +41,37 @@ export class LockedContent extends LitElement {
@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
if (!this._hasGeolocationPermission) this._hasGeolocationPermission = true; if (!this._hasGeolocationPermission) this._hasGeolocationPermission = true;
const pos = { const pos = {
@ -50,21 +79,29 @@ export class LockedContent extends LitElement {
lng: position.coords.longitude, 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); const targetLatLng = L.latLng(this.targetPosition);
// Get current position in latitudes and longitudes
const currentLatLng = L.latLng(pos); const currentLatLng = L.latLng(pos);
// Calculate the distance between target and current position in meters
const betweenMeters = currentLatLng.distanceTo(targetLatLng); const betweenMeters = currentLatLng.distanceTo(targetLatLng);
// Update the proximity text according to the distance remaining
if (betweenMeters > 1000) { if (betweenMeters > 1000) {
this._targetProximityText = `${(betweenMeters / 1000).toFixed()} KM`; this._targetProximityText = `${(betweenMeters / 1000).toFixed()} KM`;
} else if (betweenMeters > 100) { } else if (betweenMeters > 100) {
this._targetProximityText = `${betweenMeters.toFixed(0)} M`; this._targetProximityText = `${betweenMeters.toFixed(0)} M`;
} else { } else {
// If target is close less then 100 meters user has arrived to target location
if (this._watchId) { if (this._watchId) {
// Stop watching location
navigator.geolocation.clearWatch(this._watchId); navigator.geolocation.clearWatch(this._watchId);
// Update state to reveal the image
this._arrived = true; this._arrived = true;
} }
} }
@ -73,6 +110,7 @@ export class LockedContent extends LitElement {
// This callback will be fired on geolocation error // This callback will be fired on geolocation error
errorCallback(err: GeolocationPositionError) { errorCallback(err: GeolocationPositionError) {
let errorMessage; let errorMessage;
// Show toast accoring to the error state
switch (err.code) { switch (err.code) {
case GeolocationPositionError.PERMISSION_DENIED: case GeolocationPositionError.PERMISSION_DENIED:
errorMessage = errorMessage =
@ -95,18 +133,20 @@ export class LockedContent extends LitElement {
Toastify({ Toastify({
text: errorMessage, text: errorMessage,
duration: 3000, duration: 3000,
gravity: "top", // `top` or `bottom` gravity: "top",
position: "center", // `left`, `center` or `right` position: "center",
stopOnFocus: true, // Prevents dismissing of toast on hover stopOnFocus: true,
style: { style: {
background: "black", background: "black",
borderRadius: "6px", borderRadius: "6px",
margin: "16px", margin: "16px",
}, },
onClick: function () {}, // Callback after click onClick: function () {},
}).showToast(); }).showToast();
} }
// This template is shown when user hasn't give geolocation permission yet
// When user click the button user is asked for geolocation permission
permissionButtonTemplate() { permissionButtonTemplate() {
return html` return html`
<div class="flex flex-col justify-center gap-4 overlay"> <div class="flex flex-col justify-center gap-4 overlay">
@ -135,13 +175,15 @@ export class LockedContent extends LitElement {
`; `;
} }
// This template is shown when user has given permission but has not arrived yet
lockedButtonTemplate() { lockedButtonTemplate() {
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 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"
> >
İçerik Kilitli ${this.lockSVG}
<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">
<div class="pb-0 px-4 text-center"> <div class="pb-0 px-4 text-center">
@ -154,6 +196,9 @@ export class LockedContent extends LitElement {
</div>`; </div>`;
} }
// 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
// and image is revealed
unlockedButtonTemplate() { unlockedButtonTemplate() {
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
@ -162,9 +207,10 @@ export class LockedContent extends LitElement {
this._unlocked = true; this._unlocked = true;
}}" }}"
id="unlock-content-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 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"
> >
İçeriğin Kilidi ıldı ${this.unlockSVG}
<p>İçeriğin Kilidi ıldı</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">
@ -175,8 +221,9 @@ export class LockedContent extends LitElement {
</div>`; </div>`;
} }
// Start watching user location, if user has not given permission yet
// this will ask the user for permission and update the watch id
private _startWatchingLocation() { private _startWatchingLocation() {
// start geolocation services
const id = navigator.geolocation.watchPosition( const id = navigator.geolocation.watchPosition(
this.successCallback.bind(this), this.successCallback.bind(this),
this.errorCallback.bind(this), this.errorCallback.bind(this),
@ -186,6 +233,8 @@ export class LockedContent extends LitElement {
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) { private async _incrementUnlockCounter(id: string | undefined) {
if (id) { if (id) {
fetch(`http://localhost:3000/api/location/increment/${id}`, { fetch(`http://localhost:3000/api/location/increment/${id}`, {
@ -197,6 +246,8 @@ export class LockedContent extends LitElement {
connectedCallback(): void { connectedCallback(): void {
super.connectedCallback(); super.connectedCallback();
// Check geolocation permission, if user has given permission before
// start watching user location
navigator.permissions navigator.permissions
.query({ name: "geolocation" }) .query({ name: "geolocation" })
.then((permissionStatus) => { .then((permissionStatus) => {
@ -217,6 +268,11 @@ export class LockedContent extends LitElement {
render() { render() {
let buttonTemplate; let buttonTemplate;
// Determine which template to show, there are 3 states:
// 1 - No geolocation permission given
// 2 - Permission given but has no arrived to target position yet
// 3 - Arrived to target position
// 4 - User did not give geolocation permission
if (this._arrived) { if (this._arrived) {
buttonTemplate = this.unlockedButtonTemplate.bind(this); buttonTemplate = this.unlockedButtonTemplate.bind(this);
} else if (this._hasGeolocationPermission) { } else if (this._hasGeolocationPermission) {
@ -233,7 +289,7 @@ export class LockedContent extends LitElement {
<img <img
id="content" id="content"
src="${this.imageURL}" src="${this.imageURL}"
class="${this._unlocked ? "" : "blur-2xl"} h-[450px]" class="${this._unlocked ? nothing : "blur-2xl"} h-[450px]"
/> />
${this._unlocked ? nothing : buttonTemplate()} ${this._unlocked ? nothing : buttonTemplate()}