From b0deea53118c2720501b9474ea31c67fbac08b67 Mon Sep 17 00:00:00 2001 From: devbeni Date: Fri, 19 Sep 2025 19:17:25 +0200 Subject: [PATCH] feat: add event page with Socket.IO integration and ticket purchasing functionality --- app/event/[id]/page.tsx | 347 ++++++++++++++++++++++++++++++++++++++++ app/page.tsx | 322 ++++++------------------------------- 2 files changed, 400 insertions(+), 269 deletions(-) create mode 100644 app/event/[id]/page.tsx diff --git a/app/event/[id]/page.tsx b/app/event/[id]/page.tsx new file mode 100644 index 0000000..64a673a --- /dev/null +++ b/app/event/[id]/page.tsx @@ -0,0 +1,347 @@ +"use client"; +import { useEffect, useState, useRef } from "react"; +import { useParams, useRouter } from "next/navigation"; +import Link from "next/link"; + +export default function EventPage() { + const params = useParams(); + const router = useRouter(); + const eventId = params.id as string; + + const [connected, setConnected] = useState(false); + const [position, setPosition] = useState(null); + const [estimatedWait, setEstimatedWait] = useState(null); + const [activeUsers, setActiveUsers] = useState(0); + const [hasAccess, setHasAccess] = useState(false); + const [tokenExpiry, setTokenExpiry] = useState(null); + const [eventData, setEventData] = useState(null); + const [selectedTicket, setSelectedTicket] = useState("Normal"); + const [quantity, setQuantity] = useState(1); + const [purchasing, setPurchasing] = useState(false); + const [loading, setLoading] = useState(true); + const socketRef = useRef(null); + + // Load event data + useEffect(() => { + if (!eventId) return; + + fetch(`/api/events?id=${eventId}`) + .then(res => res.json()) + .then(data => { + if (data.error) { + router.push('/'); + return; + } + setEventData(data); + setLoading(false); + if (data.tickets && data.tickets.length > 0) { + setSelectedTicket(data.tickets[0].type); + } + }) + .catch(error => { + console.error('Error loading event:', error); + router.push('/'); + }); + }, [eventId, router]); + + // Socket.IO connection + useEffect(() => { + if (!eventId || loading) return; + + let mounted = true; + + // Initialize Socket.IO server + fetch('/api/socketio') + .then(() => { + return import("socket.io-client"); + }) + .then(({ io }) => { + if (!mounted) return; + const socketPort = process.env.NEXT_PUBLIC_SOCKET_PORT || "4000"; + const socket = io(`http://localhost:${socketPort}`, { + autoConnect: true + }); + socketRef.current = socket; + + socket.on("connect", () => { + setConnected(true); + socket.emit("join_event", { eventId }); + }); + + 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) => { + setHasAccess(true); + setTokenExpiry(data.expiresAt ? Date.parse(data.expiresAt) : Date.now() + 15 * 60 * 1000); + try { + localStorage.setItem("event_token", data.token); + } catch (e) {} + }); + + socket.on("revoked", () => { + setHasAccess(false); + setTokenExpiry(null); + localStorage.removeItem("event_token"); + }); + }) + .catch(error => { + console.error("Socket initialization error:", error); + }); + + return () => { + mounted = false; + if (socketRef.current) socketRef.current.disconnect(); + }; + }, [eventId, loading]); + + // Token expiry timer + 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 = async () => { + if (!hasAccess || purchasing) return; + + setPurchasing(true); + + try { + const token = localStorage.getItem("event_token"); + if (!token) { + alert("Nincs érvényes token!"); + return; + } + + const response = await fetch('/api/purchase', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + eventId, + ticketType: selectedTicket, + quantity, + token + }) + }); + + const result = await response.json(); + + if (response.ok) { + alert(`🎉 Sikeres vásárlás!\\n\\nJegyek: ${quantity}x ${selectedTicket}\\nÖsszeg: ${result.totalPrice.toLocaleString()} Ft\\nRendelés ID: ${result.orderId}`); + // Refresh event data + const eventResponse = await fetch(`/api/events?id=${eventId}`); + const eventData = await eventResponse.json(); + setEventData(eventData); + } else { + alert(`Hiba: ${result.error}`); + } + } catch (error) { + console.error('Purchase error:', error); + alert('Hiba történt a vásárlás során!'); + } finally { + setPurchasing(false); + } + }; + + if (loading) { + return ( +
+
Loading event...
+
+ ); + } + + const timeLeft = tokenExpiry ? Math.max(0, tokenExpiry - Date.now()) : 0; + const minutesLeft = Math.floor(timeLeft / 60000); + const secondsLeft = Math.floor((timeLeft % 60000) / 1000); + + return ( +
+ {/* Background Effects */} +
+
+
+ +
+ {/* Header */} +
+
+ + ← Vissza az eseményekhez + +
+
+
+ {connected ? 'Kapcsolódva' : 'Nincs kapcsolat'} +
+

+ 🎵 {eventData?.event?.name || eventId} +

+

{eventData?.event?.description}

+
+ + {/* Main Content */} +
+
+ {/* Queue Status Card */} +
+
+ {position ? ( + <> +
#{position}
+

Helyed a várakozási sorban

+ {estimatedWait && ( +

+ Becsült várakozási idő: {Math.ceil(estimatedWait)}s +

+ )} + + ) : hasAccess ? ( + <> +
🎫
+

Hozzáféred engedélyezve!

+

Most vásárolhatsz jegyeket

+ + ) : ( + <> +
+

Csatlakozás a sorhoz...

+ + )} +
+ + {/* Stats Grid */} +
+
+
{activeUsers}
+
Aktív felhasználó
+
+
+
{position || '—'}
+
Pozíció
+
+
+
+ {estimatedWait ? `${Math.ceil(estimatedWait)}s` : '—'} +
+
Várható idő
+
+
+ + {/* Purchase Form */} + {hasAccess && ( +
+
+
+ + +
+
+ + setQuantity(parseInt(e.target.value))} + className="w-full bg-white/10 border border-white/20 rounded-xl px-4 py-3 text-white" + /> +
+
+
+ )} + + {/* Action Button */} +
+ + + {/* Token Timer */} + {hasAccess && tokenExpiry && ( +
+
+ ⏰ Hozzáférés lejár: {minutesLeft}:{secondsLeft.toString().padStart(2, '0')} +
+
+ Használd ki a lehetőséget! +
+
+ )} +
+
+ + {/* Ticket Prices */} + {eventData?.tickets && ( +
+

🎟️ Jegytípusok

+
+ {eventData.tickets.map((ticket: any, index: number) => { + const available = ticket.total_quantity - ticket.sold_quantity; + const colors = ['text-green-400', 'text-yellow-400', 'text-purple-400']; + const borders = ['border-green-400/30', 'border-yellow-400/30', 'border-purple-400/30']; + + return ( +
+
{ticket.type}
+
+ {ticket.price.toLocaleString()} Ft +
+
+ {available > 0 ? `${available} db elérhető` : 'Elfogyott'} +
+
+ ); + })} +
+
+ )} +
+
+ + {/* Footer */} +
+ WebSocket + JWT alapú várakozási rendszer • {eventData?.event?.name} +
+
+
+ ); +} diff --git a/app/page.tsx b/app/page.tsx index 31aa5d9..e27882f 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,149 +1,32 @@ "use client"; -import { useEffect, useState, useRef } from "react"; +import { useEffect, useState } from "react"; +import Link from "next/link"; export default function Home() { - const [connected, setConnected] = useState(false); - const [position, setPosition] = useState(null); - const [estimatedWait, setEstimatedWait] = useState(null); - const [activeUsers, setActiveUsers] = useState(0); - const [hasAccess, setHasAccess] = useState(false); - const [tokenExpiry, setTokenExpiry] = useState(null); - const [eventData, setEventData] = useState(null); - const [selectedTicket, setSelectedTicket] = useState("Normal"); - const [quantity, setQuantity] = useState(1); - const [purchasing, setPurchasing] = useState(false); - const socketRef = useRef(null); + const [events, setEvents] = useState([]); + const [loading, setLoading] = useState(true); - // Load event data + // Load all events useEffect(() => { - fetch('/api/events?id=pamkutya') + fetch('/api/events') .then(res => res.json()) - .then(data => setEventData(data)) - .catch(console.error); - }, []); - - useEffect(() => { - let mounted = true; - - // Először inicializáljuk a Socket.IO szervert - fetch('/api/socketio') - .then(() => { - // dynamic import to avoid bundling issues on server - return import("socket.io-client"); - }) - .then(({ io }) => { - if (!mounted) return; - const socketPort = process.env.NEXT_PUBLIC_SOCKET_PORT || "4000"; - const socket = io(`http://localhost:${socketPort}`, { - autoConnect: true - }); - 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"); - }); + .then(data => { + setEvents(data.events || []); + setLoading(false); }) .catch(error => { - console.error("Socket inicializálási hiba:", error); + console.error('Error loading events:', error); + setLoading(false); }); - - 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 = async () => { - if (!hasAccess || purchasing) return; - - setPurchasing(true); - - try { - const token = localStorage.getItem("event_token"); - if (!token) { - alert("Nincs érvényes token!"); - return; - } - - const response = await fetch('/api/purchase', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - eventId: 'pamkutya', - ticketType: selectedTicket, - quantity, - token - }) - }); - - const result = await response.json(); - - if (response.ok) { - alert(`🎉 Sikeres vásárlás!\n\nJegyek: ${quantity}x ${selectedTicket}\nÖsszeg: ${result.totalPrice.toLocaleString()} Ft\nRendelés ID: ${result.orderId}`); - // Refresh event data to show updated availability - const eventResponse = await fetch('/api/events?id=pamkutya'); - const eventData = await eventResponse.json(); - setEventData(eventData); - } else { - alert(`Hiba: ${result.error}`); - } - } catch (error) { - console.error('Purchase error:', error); - alert('Hiba történt a vásárlás során!'); - } finally { - setPurchasing(false); - } - }; - - const timeLeft = tokenExpiry ? Math.max(0, tokenExpiry - Date.now()) : 0; - const minutesLeft = Math.floor(timeLeft / 60000); - const secondsLeft = Math.floor((timeLeft % 60000) / 1000); + if (loading) { + return ( +
+
Loading events...
+
+ ); + } return (
@@ -155,148 +38,49 @@ export default function Home() {
{/* Header */}
-
-
- {connected ? 'Kapcsolódva' : 'Nincs kapcsolat'} -

- 🎵 Pam Kutya + 🎵 Koncert Jegyek

-

Legendás Underground Koncert

+

Válassz egy eseményt

{/* Main Content */}
-
- {/* Queue Status Card */} -
-
- {position ? ( - <> -
#{position}
-

Helyed a várakozási sorban

- {estimatedWait && ( -

- Becsült várakozási idő: {Math.ceil(estimatedWait)}s -

- )} - - ) : hasAccess ? ( - <> -
🎫
-

Hozzáféred engedélyezve!

-

Most vásárolhatsz jegyeket

- - ) : ( - <> -
-

Csatlakozás a sorhoz...

- - )} +
+ {events.length === 0 ? ( +
+
🎪
+

Jelenleg nincsenek elérhető események

- - {/* Stats Grid */} -
-
-
{activeUsers}
-
Aktív felhasználó
-
-
-
{position || '—'}
-
Pozíció
-
-
-
- {estimatedWait ? `${Math.ceil(estimatedWait)}s` : '—'} -
-
Várható idő
-
-
- - {/* Purchase Form */} - {hasAccess && ( -
-
-
- - -
-
- - setQuantity(parseInt(e.target.value))} - className="w-full bg-white/10 border border-white/20 rounded-xl px-4 py-3 text-white" - /> -
-
-
- )} - - {/* Action Button */} -
- - - {/* Token Timer */} - {hasAccess && tokenExpiry && ( -
-
- ⏰ Hozzáférés lejár: {minutesLeft}:{secondsLeft.toString().padStart(2, '0')} -
-
- Használd ki a lehetőséget! -
-
- )} -
-
- - {/* Ticket Prices */} - {eventData?.tickets && ( -
-

🎟️ Jegytípusok

-
- {eventData.tickets.map((ticket: any, index: number) => { - const available = ticket.total_quantity - ticket.sold_quantity; - const colors = ['text-green-400', 'text-yellow-400', 'text-purple-400']; - const borders = ['border-green-400/30', 'border-yellow-400/30', 'border-purple-400/30']; - - return ( -
-
{ticket.type}
-
- {ticket.price.toLocaleString()} Ft + ) : ( +
+ {events.map((event) => ( + +
+
+
🎵
+

{event.name}

+

{event.description}

+ +
+
+ Max egyidejű: + {event.max_concurrent_users} fő +
+ +
+ Jegytípusok: + {event.ticket_types || 0} db +
-
- {available > 0 ? `${available} db elérhető` : 'Elfogyott'} + +
+ 🎫 Jegyvásárlás
- ); - })} -
+
+ + ))}
)}
@@ -304,9 +88,9 @@ export default function Home() { {/* Footer */}
); -} +} \ No newline at end of file