feat: add standalone templates
This commit is contained in:
parent
e406ed40a6
commit
0539bedc84
40
package.json
40
package.json
|
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
80
src/components/LockedContent/templates.ts
Normal file
80
src/components/LockedContent/templates.ts
Normal 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 Açı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,
|
||||||
|
};
|
|
@ -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 Açıldı
|
${this.unlockSVG}
|
||||||
|
<p>İçeriğin Kilidi Açı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()}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user