feat: add initial Socket.IO queue server implementation
- 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.
This commit is contained in:
223
app/page.tsx
223
app/page.tsx
@@ -1,102 +1,143 @@
|
||||
"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-[32px] row-start-2 items-center sm:items-start">
|
||||
<Image
|
||||
className="dark:invert"
|
||||
src="/next.svg"
|
||||
alt="Next.js logo"
|
||||
width={180}
|
||||
height={38}
|
||||
priority
|
||||
/>
|
||||
<ol className="font-mono list-inside list-decimal text-sm/6 text-center sm:text-left">
|
||||
<li className="mb-2 tracking-[-.01em]">
|
||||
Get started by editing{" "}
|
||||
<code className="bg-black/[.05] dark:bg-white/[.06] font-mono font-semibold px-1 py-0.5 rounded">
|
||||
app/page.tsx
|
||||
</code>
|
||||
.
|
||||
</li>
|
||||
<li className="tracking-[-.01em]">
|
||||
Save and see your changes instantly.
|
||||
</li>
|
||||
</ol>
|
||||
<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>
|
||||
|
||||
<div className="flex gap-4 items-center flex-col sm:flex-row">
|
||||
<a
|
||||
className="rounded-full border border-solid border-transparent transition-colors flex items-center justify-center bg-foreground text-background gap-2 hover:bg-[#383838] dark:hover:bg-[#ccc] font-medium text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 sm:w-auto"
|
||||
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Image
|
||||
className="dark:invert"
|
||||
src="/vercel.svg"
|
||||
alt="Vercel logomark"
|
||||
width={20}
|
||||
height={20}
|
||||
/>
|
||||
Deploy now
|
||||
</a>
|
||||
<a
|
||||
className="rounded-full border border-solid border-black/[.08] dark:border-white/[.145] transition-colors flex items-center justify-center hover:bg-[#f2f2f2] dark:hover:bg-[#1a1a1a] hover:border-transparent font-medium text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 w-full sm:w-auto md:w-[158px]"
|
||||
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Read our docs
|
||||
</a>
|
||||
</div>
|
||||
<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">
|
||||
<a
|
||||
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
|
||||
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Image
|
||||
aria-hidden
|
||||
src="/file.svg"
|
||||
alt="File icon"
|
||||
width={16}
|
||||
height={16}
|
||||
/>
|
||||
Learn
|
||||
</a>
|
||||
<a
|
||||
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
|
||||
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Image
|
||||
aria-hidden
|
||||
src="/window.svg"
|
||||
alt="Window icon"
|
||||
width={16}
|
||||
height={16}
|
||||
/>
|
||||
Examples
|
||||
</a>
|
||||
<a
|
||||
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
|
||||
href="https://nextjs.org?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Image
|
||||
aria-hidden
|
||||
src="/globe.svg"
|
||||
alt="Globe icon"
|
||||
width={16}
|
||||
height={16}
|
||||
/>
|
||||
Go to nextjs.org →
|
||||
</a>
|
||||
<Image aria-hidden src="/globe.svg" alt="Globe icon" width={16} height={16} />
|
||||
<span className="text-sm">Demo — Queue system</span>
|
||||
</footer>
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user