full code V1

This commit is contained in:
omar 2025-08-31 18:08:11 +03:00
parent 75d6aa155f
commit 9b241c8d1a
24 changed files with 6332 additions and 0 deletions

26
.gitignore vendored Normal file
View File

@ -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

4
.vscode/extensions.json vendored Normal file
View File

@ -0,0 +1,4 @@
{
"recommendations": ["astro-build.astro-vscode"],
"unwantedRecommendations": []
}

11
.vscode/launch.json vendored Normal file
View File

@ -0,0 +1,11 @@
{
"version": "0.2.0",
"configurations": [
{
"command": "./node_modules/.bin/astro dev",
"name": "Development server",
"request": "launch",
"type": "node-terminal"
}
]
}

15
astro.config.mjs Normal file
View File

@ -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)),
},
},
},
});

5680
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

17
package.json Normal file
View File

@ -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"
}
}

BIN
public/authorized.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

9
public/favicon.svg Normal file
View File

@ -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

BIN
public/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

BIN
public/logo_old.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

BIN
public/scanQr.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 625 KiB

BIN
public/truckEnter.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

BIN
public/unauthorized.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

View File

@ -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>

View File

@ -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>

View File

@ -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>

44
src/pages/index.astro Normal file
View File

@ -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>

71
src/pages/scan.astro Normal file
View File

@ -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>

View File

@ -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>

79
src/pages/waiting.astro Normal file
View File

@ -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>

44
src/scripts/store.ts Normal file
View File

@ -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);
}

23
src/scripts/ws.ts Normal file
View File

@ -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;
}

35
src/styles/global.css Normal file
View File

@ -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; }

11
tsconfig.json Normal file
View File

@ -0,0 +1,11 @@
{
"extends": "astro/tsconfigs/strict",
"include": [".astro/types.d.ts", "**/*"],
"exclude": ["dist"],
"compilerOptions": {
"baseUrl": ".",
"paths": {
"/*": ["src/*"]
}
}
}