full code V1
|
|
@ -0,0 +1,26 @@
|
|||
# build output
|
||||
dist/
|
||||
# generated types
|
||||
.astro/
|
||||
|
||||
# dependencies
|
||||
node_modules/
|
||||
|
||||
# logs
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
|
||||
|
||||
# environment variables
|
||||
.env
|
||||
.env.production
|
||||
|
||||
# macOS-specific files
|
||||
.DS_Store
|
||||
|
||||
# jetbrains setting folder
|
||||
.idea/
|
||||
|
||||
.env
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"recommendations": ["astro-build.astro-vscode"],
|
||||
"unwantedRecommendations": []
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"command": "./node_modules/.bin/astro dev",
|
||||
"name": "Development server",
|
||||
"request": "launch",
|
||||
"type": "node-terminal"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
// @ts-check
|
||||
import { defineConfig } from 'astro/config';
|
||||
import tailwindcss from '@tailwindcss/vite';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
export default defineConfig({
|
||||
vite: {
|
||||
plugins: [tailwindcss()],
|
||||
resolve: {
|
||||
alias: {
|
||||
"@": fileURLToPath(new URL("./src", import.meta.url)),
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
{
|
||||
"name": "nfident-kiosk",
|
||||
"type": "module",
|
||||
"version": "0.0.1",
|
||||
"scripts": {
|
||||
"dev": "astro dev",
|
||||
"build": "astro build",
|
||||
"preview": "astro preview",
|
||||
"astro": "astro"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tailwindcss/vite": "^4.1.12",
|
||||
"astro": "^5.13.5",
|
||||
"socket.io-client": "^4.8.1",
|
||||
"tailwindcss": "^4.1.12"
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 23 KiB |
|
|
@ -0,0 +1,9 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 128 128">
|
||||
<path d="M50.4 78.5a75.1 75.1 0 0 0-28.5 6.9l24.2-65.7c.7-2 1.9-3.2 3.4-3.2h29c1.5 0 2.7 1.2 3.4 3.2l24.2 65.7s-11.6-7-28.5-7L67 45.5c-.4-1.7-1.6-2.8-2.9-2.8-1.3 0-2.5 1.1-2.9 2.7L50.4 78.5Zm-1.1 28.2Zm-4.2-20.2c-2 6.6-.6 15.8 4.2 20.2a17.5 17.5 0 0 1 .2-.7 5.5 5.5 0 0 1 5.7-4.5c2.8.1 4.3 1.5 4.7 4.7.2 1.1.2 2.3.2 3.5v.4c0 2.7.7 5.2 2.2 7.4a13 13 0 0 0 5.7 4.9v-.3l-.2-.3c-1.8-5.6-.5-9.5 4.4-12.8l1.5-1a73 73 0 0 0 3.2-2.2 16 16 0 0 0 6.8-11.4c.3-2 .1-4-.6-6l-.8.6-1.6 1a37 37 0 0 1-22.4 2.7c-5-.7-9.7-2-13.2-6.2Z" />
|
||||
<style>
|
||||
path { fill: #000; }
|
||||
@media (prefers-color-scheme: dark) {
|
||||
path { fill: #FFF; }
|
||||
}
|
||||
</style>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 749 B |
|
After Width: | Height: | Size: 8.5 KiB |
|
After Width: | Height: | Size: 61 KiB |
|
After Width: | Height: | Size: 625 KiB |
|
After Width: | Height: | Size: 84 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
|
@ -0,0 +1,129 @@
|
|||
---
|
||||
import {} from "astro";
|
||||
---
|
||||
|
||||
<div id="testbar" class="hidden fixed bottom-4 left-1/2 -translate-x-1/2 z-50">
|
||||
<div
|
||||
class="rounded-2xl shadow-lg border bg-white px-4 py-3 flex flex-wrap gap-2 items-center"
|
||||
>
|
||||
<span class="text-xs font-semibold text-slate-500 mr-2">TEST</span>
|
||||
|
||||
<!-- تنقّل صفحات -->
|
||||
<a
|
||||
href="/"
|
||||
class="px-3 py-1.5 rounded bg-slate-100 hover:bg-slate-200 text-sm"
|
||||
>Logo</a
|
||||
>
|
||||
<a
|
||||
href="/scan"
|
||||
class="px-3 py-1.5 rounded bg-slate-100 hover:bg-slate-200 text-sm"
|
||||
>Scan</a
|
||||
>
|
||||
<a
|
||||
href="/waiting"
|
||||
class="px-3 py-1.5 rounded bg-slate-100 hover:bg-slate-200 text-sm"
|
||||
>Waiting</a
|
||||
>
|
||||
<a
|
||||
href="/authorized"
|
||||
class="px-3 py-1.5 rounded bg-slate-100 hover:bg-slate-200 text-sm"
|
||||
>Authorized</a
|
||||
>
|
||||
<a
|
||||
href="/unauthorized"
|
||||
class="px-3 py-1.5 rounded bg-slate-100 hover:bg-slate-200 text-sm"
|
||||
>Unauthorized</a
|
||||
>
|
||||
|
||||
<span class="w-px h-5 bg-slate-200 mx-1"></span>
|
||||
|
||||
<!-- إشارات سوكِت -->
|
||||
<button
|
||||
data-emit="kiosk:ready"
|
||||
class="px-3 py-1.5 rounded bg-blue-50 hover:bg-blue-100 text-blue-700 text-sm"
|
||||
>ready</button
|
||||
>
|
||||
<button
|
||||
data-emit="deal:new"
|
||||
data-payload='{"plateNumber":"3212","plateCode":"أ ٦ ٤"}'
|
||||
class="px-3 py-1.5 rounded bg-blue-50 hover:bg-blue-100 text-blue-700 text-sm"
|
||||
>deal:new</button
|
||||
>
|
||||
<button
|
||||
data-emit="qr:scanned"
|
||||
class="px-3 py-1.5 rounded bg-blue-50 hover:bg-blue-100 text-blue-700 text-sm"
|
||||
>qr:scanned</button
|
||||
>
|
||||
<button
|
||||
data-emit="decision"
|
||||
data-payload='{"authorized":true,"id":1234}'
|
||||
class="px-3 py-1.5 rounded bg-green-50 hover:bg-green-100 text-green-700 text-sm"
|
||||
>decision ✅</button
|
||||
>
|
||||
<button
|
||||
data-emit="decision"
|
||||
data-payload='{"authorized":false,"id":4321}'
|
||||
class="px-3 py-1.5 rounded bg-red-50 hover:bg-red-100 text-red-700 text-sm"
|
||||
>decision ❌</button
|
||||
>
|
||||
<button
|
||||
data-emit="cycle:reset"
|
||||
class="px-3 py-1.5 rounded bg-amber-50 hover:bg-amber-100 text-amber-700 text-sm"
|
||||
>reset</button
|
||||
>
|
||||
|
||||
<span class="w-px h-5 bg-slate-200 mx-1"></span>
|
||||
<button
|
||||
id="toggleTest"
|
||||
class="px-2 py-1 rounded bg-slate-100 hover:bg-slate-200 text-xs"
|
||||
>Hide</button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script type="module">
|
||||
const KEY = "nfident:test";
|
||||
const bar = document.getElementById("testbar");
|
||||
|
||||
function isTest() {
|
||||
return sessionStorage.getItem(KEY) === "1";
|
||||
}
|
||||
function setTest(v) {
|
||||
if (v) {
|
||||
sessionStorage.setItem(KEY, "1");
|
||||
} else {
|
||||
sessionStorage.removeItem(KEY);
|
||||
}
|
||||
renderBar();
|
||||
}
|
||||
function renderBar() {
|
||||
bar.classList.toggle("hidden", !isTest());
|
||||
}
|
||||
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
// 1) param check
|
||||
const url = new URL(location.href);
|
||||
if (url.searchParams.get("test") === "1") setTest(true);
|
||||
if (url.searchParams.get("test") === "0") setTest(false);
|
||||
renderBar();
|
||||
|
||||
// 2) hotkey: Ctrl + Alt + T
|
||||
let lastToggle = 0;
|
||||
|
||||
window.addEventListener("keydown", (e) => {
|
||||
if (e.ctrlKey && e.altKey && (e.key === "t" || e.key === "T")) {
|
||||
e.preventDefault();
|
||||
setTest(true); // إظهار فقط
|
||||
}
|
||||
if (e.ctrlKey && e.altKey && (e.key === "y" || e.key === "Y")) {
|
||||
e.preventDefault();
|
||||
setTest(false); // إخفاء فقط
|
||||
}
|
||||
});
|
||||
|
||||
// 3) button inside bar
|
||||
document.getElementById("toggleTest")?.addEventListener("click", () => {
|
||||
setTest(false);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
---
|
||||
import TestBar from "/components/TestBar.astro";
|
||||
import "../styles/global.css";
|
||||
const { title = "NFident", withHeader = true } = Astro.props;
|
||||
---
|
||||
|
||||
<html lang="en" dir="ltr">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#FFFFFF" />
|
||||
<title>{title}</title>
|
||||
<meta
|
||||
name="description"
|
||||
content="NFident Kiosk - Secure and seamless entry management system."
|
||||
/>
|
||||
<link rel="canonical" href="https://nfident.com/kiosk" />
|
||||
<meta property="og:title" content={title} />
|
||||
<meta
|
||||
property="og:description"
|
||||
content="NFident Kiosk - Secure and seamless entry management system."
|
||||
/>
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:url" content="https://nfident.com/kiosk" />
|
||||
<meta property="og:image" content="https://nfident.com/logo.png" />
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
<meta name="twitter:title" content={title} />
|
||||
<meta
|
||||
name="twitter:description"
|
||||
content="NFident Kiosk - Secure and seamless entry management system."
|
||||
/>
|
||||
<meta name="twitter:image" content="https://nfident.com/logo.png" />
|
||||
<link rel="icon" type="image/png" href="/logo.png" />
|
||||
</head>
|
||||
<body class="min-h-screen bg-gray-500 flex items-center justify-center">
|
||||
<main
|
||||
class="w-[1024px] h-[768px] bg-[#FFFFFF] rounded-md shadow-xl overflow-hidden flex flex-col"
|
||||
>
|
||||
{
|
||||
withHeader && (
|
||||
<header class="px-6 py-4 bg-[#FBFBFB] flex items-center border-b border-b-[#E8E8E8]">
|
||||
<img src="/logo.png" alt="NFident" class="h-8 w-30" />
|
||||
</header>
|
||||
)
|
||||
}
|
||||
<TestBar />
|
||||
<section class="flex-1 px-24 py-16">
|
||||
<slot />
|
||||
</section>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
---
|
||||
import KioskLayout from "/layouts/KioskLayout.astro";
|
||||
import { getDecisionId } from "/scripts/store";
|
||||
const id = getDecisionId();
|
||||
---
|
||||
|
||||
<KioskLayout title="Authorized">
|
||||
<div
|
||||
class="w-full bg-[#FBFBFB] border-b border-b-[#E8E8E8] rounded-2xl p-6 flex flex-col items-center gap-6"
|
||||
>
|
||||
<div
|
||||
class="w-full rounded-2xl max-w-full text-center flex flex-col items-center gap-6"
|
||||
>
|
||||
<img src="/authorized.png" alt="Authorized" class="w-85 h-85" />
|
||||
<h2 class="text-4xl font-extrabold text-[#006C3D] mb-2">"Authorized"</h2>
|
||||
<div
|
||||
data-layer="Line 18"
|
||||
class="Line18 self-stretch h-0 outline-1 outline-offset-[-0.50px] outline-gray-200"
|
||||
>
|
||||
</div>
|
||||
|
||||
<p class="text-3xl font-medium text-[#0A3B5C]">Welcome To Terminal</p>
|
||||
{id && <p class="mt-2 text-slate-500">ID: {id}</p>}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script type="module">
|
||||
import { createSocket } from "/scripts/ws";
|
||||
import { clearFlow } from "/scripts/store";
|
||||
|
||||
const socket = createSocket();
|
||||
socket.on("cycle:reset", () => {
|
||||
clearFlow();
|
||||
location.href = "/";
|
||||
});
|
||||
|
||||
// fallback: ارجع تلقائيًا بعد 8 ثوانٍ لو ما وصل reset
|
||||
setTimeout(() => socket.emit("kiosk:ready"), 8000);
|
||||
</script>
|
||||
</KioskLayout>
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
---
|
||||
import KioskLayout from "/layouts/KioskLayout.astro";
|
||||
import TestBar from "/components/TestBar.astro";
|
||||
---
|
||||
|
||||
<KioskLayout withHeader={false} title="Welcome">
|
||||
<div class="h-full w-full flex flex-col items-center justify-center gap-6">
|
||||
<img src="/logo.png" alt="NFident" class="w-[326.654px] max-w-full" />
|
||||
<p class="text-[#0E5382] font-semibold text-center text-3xl">
|
||||
"Welcome – Your Entry Information Will Show Up Here"
|
||||
</p>
|
||||
<TestBar />
|
||||
|
||||
<p id="debug" class="text-sm text-slate-400"></p>
|
||||
</div>
|
||||
|
||||
<script type="module">
|
||||
import { createSocket } from "/scripts/ws";
|
||||
import { setPlate, clearFlow } from "/scripts/store";
|
||||
|
||||
clearFlow(); // New cycle
|
||||
const socket = createSocket();
|
||||
|
||||
socket.on("connect", () => {
|
||||
document.getElementById("debug").textContent = "Connected";
|
||||
socket.emit("kiosk:ready"); // Notify the server that the kiosk is ready
|
||||
});
|
||||
|
||||
// The server sends a new deal (with optional plate data)
|
||||
// payload: { plateNumber?: string, plateCode?: string }
|
||||
socket.on("decision-response", (p = {}) => {
|
||||
console.log(p);
|
||||
setPlate(p.plateNumber, p.plateCode);
|
||||
window.location.href = "/scan";
|
||||
});
|
||||
|
||||
// If at any time the server says to reset the cycle
|
||||
socket.on("cycle:reset", () => {
|
||||
// We are already on the logo
|
||||
clearFlow();
|
||||
socket.emit("kiosk:ready");
|
||||
});
|
||||
</script>
|
||||
</KioskLayout>
|
||||
|
|
@ -0,0 +1,71 @@
|
|||
---
|
||||
import KioskLayout from "/layouts/KioskLayout.astro";
|
||||
import { getPlate } from "/scripts/store";
|
||||
const plate = getPlate();
|
||||
---
|
||||
|
||||
<KioskLayout title="Scan QR">
|
||||
<div class="h-full flex flex-col items-center justify-start gap-14">
|
||||
<div
|
||||
class="w-full bg-[#FBFBFB] border-b border-b-[#E8E8E8] rounded-2xl p-6 flex flex-col items-center gap-6"
|
||||
>
|
||||
<img
|
||||
src="/truckEnter.png"
|
||||
alt=""
|
||||
class="w-[434.677px] h-45 object-contain"
|
||||
/>
|
||||
<div
|
||||
class="Line18 self-stretch h-0 outline-1 outline-offset-[-0.50px] outline-gray-200 my-6"
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="w-full flex justify-between">
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="justify-start text-[#0A3B5C] text-3xl font-medium">
|
||||
Plate Number
|
||||
</div>
|
||||
<div id="plateNumber" class="text-3xl font-bold text-[#0A3B5C]">
|
||||
{plate.number}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col gap-2 items-end">
|
||||
<div class="justify-start text-[#0A3B5C] text-3xl font-medium">
|
||||
Plate Code
|
||||
</div>
|
||||
<div id="plateCode" class="text-3xl font-bold text-[#0A3B5C]">
|
||||
{plate.code}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-center">
|
||||
<div class="flex items-center justify-center gap-3 text-[#C47908]">
|
||||
<img src="/scanQr.gif" alt="Scan QR Code" class="w-16 h-16" />
|
||||
<div
|
||||
class="text-center justify-center text-[#C47908] text-4xl font-semibold"
|
||||
>
|
||||
"Please Scan the QR Code to Continue"
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script type="module">
|
||||
import { createSocket } from "/scripts/ws";
|
||||
import { getPlate } from "/scripts/store";
|
||||
const plate = getPlate();
|
||||
|
||||
document.getElementById("plateNumber").textContent = plate.number;
|
||||
document.getElementById("plateCode").textContent = plate.code;
|
||||
|
||||
const socket = createSocket();
|
||||
socket.on("decision-response", () => {
|
||||
window.location.href = "/waiting";
|
||||
});
|
||||
|
||||
socket.on("cycle:reset", () => {
|
||||
window.location.href = "/";
|
||||
});
|
||||
</script>
|
||||
</KioskLayout>
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
---
|
||||
import KioskLayout from "/layouts/KioskLayout.astro";
|
||||
import { getDecisionId } from "/scripts/store";
|
||||
const id = getDecisionId();
|
||||
---
|
||||
|
||||
<KioskLayout title="Not Authorized">
|
||||
<div
|
||||
class="w-full bg-[#FBFBFB] border-b border-b-[#E8E8E8] rounded-2xl p-6 flex flex-col items-center gap-6"
|
||||
>
|
||||
<div
|
||||
class="w-full rounded-2xl max-w-full text-center flex flex-col items-center gap-6"
|
||||
>
|
||||
<img src="/unauthorized.png" alt="Unauthorized" class="w-85 h-85" />
|
||||
<h2 class="text-4xl font-extrabold text-[#AF1D1D] mb-2">
|
||||
"Not Authorized"
|
||||
</h2>
|
||||
<div
|
||||
data-layer="Line 18"
|
||||
class="Line18 self-stretch h-0 outline-1 outline-offset-[-0.50px] outline-gray-200"
|
||||
>
|
||||
</div>
|
||||
|
||||
<p class="text-3xl font-medium text-[#0A3B5C]">
|
||||
Please Contact The Administrator
|
||||
</p>
|
||||
{id && <p class="mt-2 text-slate-500">ID: {id}</p>}
|
||||
</div>
|
||||
</div>
|
||||
<script type="module">
|
||||
import { createSocket } from "/scripts/ws";
|
||||
import { clearFlow } from "/scripts/store";
|
||||
|
||||
const socket = createSocket();
|
||||
socket.on("cycle:reset", () => {
|
||||
clearFlow();
|
||||
location.href = "/";
|
||||
});
|
||||
|
||||
setTimeout(() => socket.emit("kiosk:ready"), 8000);
|
||||
</script>
|
||||
</KioskLayout>
|
||||
|
|
@ -0,0 +1,79 @@
|
|||
---
|
||||
import KioskLayout from "/layouts/KioskLayout.astro";
|
||||
---
|
||||
|
||||
<KioskLayout title="Processing">
|
||||
<div class="h-full flex flex-col items-center justify-center gap-6">
|
||||
<div
|
||||
class="w-full bg-[#FBFBFB] border-b border-b-[#E8E8E8] rounded-2xl p-6 flex flex-col items-center gap-6"
|
||||
>
|
||||
<img
|
||||
src="/truckEnter.png"
|
||||
alt=""
|
||||
class="w-[434.677px] h-45 object-contain"
|
||||
/>
|
||||
<div
|
||||
class="Line18 self-stretch h-0 outline-1 outline-offset-[-0.50px] outline-gray-200 my-6"
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="w-full flex justify-between">
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="justify-start text-[#0A3B5C] text-3xl font-medium">
|
||||
Plate Number
|
||||
</div>
|
||||
<div id="plateNumber" class="text-3xl font-bold text-[#0A3B5C]">
|
||||
-
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col gap-2 items-end">
|
||||
<div class="justify-start text-[#0A3B5C] text-3xl font-medium">
|
||||
Plate Code
|
||||
</div>
|
||||
<div id="plateCode" class="text-3xl font-bold text-[#0A3B5C]">-</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-[#006C3D] text-4xl font-semibold">
|
||||
"QR Code Scanned Successfully - Please Wait"
|
||||
</p>
|
||||
<div class="flex text-[#C47908] text-3xl font-semibold items-center gap-2">
|
||||
<span> Processing Validation </span>
|
||||
<div class="nf-bounce pt-2">
|
||||
<span></span>
|
||||
<span></span>
|
||||
<span></span>
|
||||
<span></span>
|
||||
<span></span>
|
||||
</div>
|
||||
</div>
|
||||
<p id="debug" class="text-sm text-slate-400"></p>
|
||||
</div>
|
||||
|
||||
<script type="module">
|
||||
import { createSocket } from "/scripts/ws";
|
||||
import { setDecisionId } from "/scripts/store";
|
||||
import { getPlate } from "/scripts/store";
|
||||
const plate = getPlate();
|
||||
|
||||
document.getElementById("plateNumber").textContent = plate.number;
|
||||
document.getElementById("plateCode").textContent = plate.code;
|
||||
|
||||
const socket = createSocket();
|
||||
|
||||
socket.on("decision", (res = {}) => {
|
||||
setDecisionId(res.id);
|
||||
if (res.authorized) {
|
||||
location.href = "/authorized";
|
||||
} else {
|
||||
location.href = "/unauthorized";
|
||||
}
|
||||
});
|
||||
|
||||
socket.on("cycle:reset", () => (location.href = "/"));
|
||||
|
||||
// أمان: في حال طال الانتظار، أبلغ السيرفر أو ارجع للوغو
|
||||
const TIMEOUT = 60000;
|
||||
setTimeout(() => socket.emit("kiosk:timeout"), TIMEOUT);
|
||||
</script>
|
||||
</KioskLayout>
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
const S = {
|
||||
plateNumber: "nfident:plateNumber",
|
||||
plateCode: "nfident:plateCode",
|
||||
decisionId: "nfident:decisionId",
|
||||
};
|
||||
|
||||
function safeSession() {
|
||||
if (typeof window === "undefined") return null;
|
||||
return window.sessionStorage;
|
||||
}
|
||||
|
||||
export function setPlate(pn?: string, pc?: string) {
|
||||
const ss = safeSession();
|
||||
if (!ss) return;
|
||||
if (pn) ss.setItem(S.plateNumber, pn);
|
||||
if (pc) ss.setItem(S.plateCode, pc);
|
||||
}
|
||||
|
||||
export function getPlate() {
|
||||
const ss = safeSession();
|
||||
return {
|
||||
number: ss?.getItem(S.plateNumber) || "—",
|
||||
code: ss?.getItem(S.plateCode) || "—",
|
||||
};
|
||||
}
|
||||
|
||||
export function setDecisionId(id?: number | string) {
|
||||
const ss = safeSession();
|
||||
if (!ss) return;
|
||||
if (id != null) ss.setItem(S.decisionId, String(id));
|
||||
}
|
||||
|
||||
export function getDecisionId() {
|
||||
const ss = safeSession();
|
||||
return ss?.getItem(S.decisionId) || "";
|
||||
}
|
||||
|
||||
export function clearFlow() {
|
||||
const ss = safeSession();
|
||||
if (!ss) return;
|
||||
ss.removeItem(S.plateNumber);
|
||||
ss.removeItem(S.plateCode);
|
||||
ss.removeItem(S.decisionId);
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
import { io, Socket } from "socket.io-client";
|
||||
const WS_URL = import.meta.env.PUBLIC_WS_URL || "ws://127.0.0.1:9091/decision-ui";
|
||||
// const KIOSK_KEY = "nfident:kioskId";
|
||||
|
||||
function getKioskId(): string {
|
||||
const key = "nfident:kioskId";
|
||||
let id = localStorage.getItem(key);
|
||||
if (!id) {
|
||||
id = crypto.randomUUID();
|
||||
localStorage.setItem(key, id);
|
||||
}
|
||||
return id;
|
||||
}
|
||||
|
||||
export function createSocket(): Socket {
|
||||
const kioskId = getKioskId();
|
||||
const socket = io(WS_URL, {
|
||||
transports: ["websocket"],
|
||||
reconnection: true,
|
||||
auth: { kioskId },
|
||||
});
|
||||
return socket;
|
||||
}
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
@import "tailwindcss";
|
||||
|
||||
@theme {
|
||||
--font-sans: "Alexandria", sans-serif;
|
||||
}
|
||||
|
||||
@keyframes bounceY {
|
||||
0%, 80%, 100% {
|
||||
transform: translateY(0);
|
||||
}
|
||||
40% {
|
||||
transform: translateY(-8px);
|
||||
}
|
||||
}
|
||||
|
||||
.nf-bounce {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.nf-bounce span {
|
||||
width: 5px;
|
||||
height: 5px;
|
||||
background: #d58b1a;
|
||||
border-radius: 9999px;
|
||||
display: inline-block;
|
||||
animation: bounceY 1.2s infinite ease-in-out;
|
||||
}
|
||||
|
||||
.nf-bounce span:nth-child(1) { animation-delay: 0s; }
|
||||
.nf-bounce span:nth-child(2) { animation-delay: 0.15s;}
|
||||
.nf-bounce span:nth-child(3) { animation-delay: 0.3s; }
|
||||
.nf-bounce span:nth-child(4) { animation-delay: 0.45s;}
|
||||
.nf-bounce span:nth-child(5) { animation-delay: 0.6s; }
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"extends": "astro/tsconfigs/strict",
|
||||
"include": [".astro/types.d.ts", "**/*"],
|
||||
"exclude": ["dist"],
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"/*": ["src/*"]
|
||||
}
|
||||
}
|
||||
}
|
||||