- Created a basic Socket.IO server that manages user connections and queues for events. - Implemented queue logic to handle concurrent user limits and JWT token issuance. - Added MySQL configuration for potential persistence of queue positions. - Introduced environment variables for configuration through a .env.example file.
145 lines
5.3 KiB
TypeScript
145 lines
5.3 KiB
TypeScript
"use client";
|
|
|
|
import Image from "next/image";
|
|
import { useEffect, useState, useRef } from "react";
|
|
|
|
export default function Home() {
|
|
const [connected, setConnected] = useState(false);
|
|
const [position, setPosition] = useState<number | null>(null);
|
|
const [estimatedWait, setEstimatedWait] = useState<number | null>(null);
|
|
const [activeUsers, setActiveUsers] = useState(0);
|
|
const [hasAccess, setHasAccess] = useState(false);
|
|
const [tokenExpiry, setTokenExpiry] = useState<number | null>(null);
|
|
const socketRef = useRef<any>(null);
|
|
|
|
useEffect(() => {
|
|
let mounted = true;
|
|
// dynamic import to avoid bundling issues on server
|
|
import("socket.io-client").then(({ io }) => {
|
|
if (!mounted) return;
|
|
// default to the same origin and connect to the Next.js API route at /api/socket
|
|
const serverUrl = process.env.NEXT_PUBLIC_SOCKET_URL || window.location.origin;
|
|
const socket = io(serverUrl, { autoConnect: true, path: "/api/socket" });
|
|
socketRef.current = socket;
|
|
|
|
socket.on("connect", () => {
|
|
setConnected(true);
|
|
// join a named event (example eventId: pamkutya)
|
|
socket.emit("join_event", { eventId: "pamkutya" });
|
|
});
|
|
|
|
socket.on("disconnect", () => {
|
|
setConnected(false);
|
|
setPosition(null);
|
|
setHasAccess(false);
|
|
setTokenExpiry(null);
|
|
});
|
|
|
|
socket.on("queue_update", (data: any) => {
|
|
setPosition(data.position ?? null);
|
|
setEstimatedWait(data.estimatedWait ?? null);
|
|
setActiveUsers(data.activeCount ?? 0);
|
|
});
|
|
|
|
socket.on("granted", (data: any) => {
|
|
// { token, expiresAt }
|
|
setHasAccess(true);
|
|
setTokenExpiry(data.expiresAt ? Date.parse(data.expiresAt) : Date.now() + 15 * 60 * 1000);
|
|
// store token locally for API calls
|
|
try {
|
|
localStorage.setItem("event_token", data.token);
|
|
} catch (e) {}
|
|
});
|
|
|
|
socket.on("revoked", () => {
|
|
setHasAccess(false);
|
|
setTokenExpiry(null);
|
|
localStorage.removeItem("event_token");
|
|
});
|
|
});
|
|
|
|
return () => {
|
|
mounted = false;
|
|
if (socketRef.current) socketRef.current.disconnect();
|
|
};
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
if (!tokenExpiry) return;
|
|
const id = setInterval(() => {
|
|
const msLeft = tokenExpiry - Date.now();
|
|
if (msLeft <= 0) {
|
|
setHasAccess(false);
|
|
setTokenExpiry(null);
|
|
localStorage.removeItem("event_token");
|
|
}
|
|
}, 1000);
|
|
return () => clearInterval(id);
|
|
}, [tokenExpiry]);
|
|
|
|
const tryBuy = () => {
|
|
if (!hasAccess) return;
|
|
// Here you'd call your purchase API with the stored token
|
|
alert("You can proceed to purchase (this is a demo stub).");
|
|
};
|
|
|
|
return (
|
|
<div className="font-sans grid grid-rows-[20px_1fr_20px] items-center justify-items-center min-h-screen p-8 pb-20 gap-16 sm:p-20">
|
|
<main className="flex flex-col gap-[24px] row-start-2 items-center sm:items-start w-full max-w-3xl">
|
|
<h1 className="text-2xl font-bold">Koncert jegyek — Queue rendszer demo</h1>
|
|
|
|
<section className="p-4 border rounded w-full">
|
|
<div className="flex justify-between items-center mb-3">
|
|
<div>
|
|
<strong>Event:</strong> pamkutya
|
|
</div>
|
|
<div>
|
|
<span className="text-sm">Socket:</span>{" "}
|
|
<span className={`font-mono ${connected ? "text-green-600" : "text-red-600"}`}>
|
|
{connected ? "connected" : "disconnected"}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 sm:grid-cols-3 gap-3">
|
|
<div className="p-2 bg-gray-50 rounded">
|
|
<div className="text-xs text-gray-500">Aktív felhasználók</div>
|
|
<div className="text-lg font-semibold">{activeUsers}</div>
|
|
</div>
|
|
<div className="p-2 bg-gray-50 rounded">
|
|
<div className="text-xs text-gray-500">Helyed a sorban</div>
|
|
<div className="text-lg font-semibold">{position ?? "—"}</div>
|
|
</div>
|
|
<div className="p-2 bg-gray-50 rounded">
|
|
<div className="text-xs text-gray-500">Becsült várakozás</div>
|
|
<div className="text-lg font-semibold">{estimatedWait ? `${Math.ceil(estimatedWait)} s` : "—"}</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="mt-4 flex gap-3">
|
|
<button
|
|
onClick={tryBuy}
|
|
disabled={!hasAccess}
|
|
className={`px-4 py-2 rounded font-medium ${hasAccess ? "bg-blue-600 text-white" : "bg-gray-200 text-gray-600 cursor-not-allowed"}`}
|
|
>
|
|
{hasAccess ? "Vásárlás" : "Nincs hozzáférés — váróban vagy"}
|
|
</button>
|
|
|
|
{tokenExpiry && (
|
|
<div className="text-sm text-gray-500 self-center">
|
|
Token lejár: {new Date(tokenExpiry).toLocaleTimeString()}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</section>
|
|
|
|
<p className="text-sm text-gray-600">A rendszer WebSocket + JWT alapú. Ha a rendezvény eléri a küszöböt, a rendszer sorba helyez.</p>
|
|
</main>
|
|
<footer className="row-start-3 flex gap-[24px] flex-wrap items-center justify-center">
|
|
<Image aria-hidden src="/globe.svg" alt="Globe icon" width={16} height={16} />
|
|
<span className="text-sm">Demo — Queue system</span>
|
|
</footer>
|
|
</div>
|
|
);
|
|
}
|