From 2c5461b0e0c29cc0d090d287b78ed50bf99d8080 Mon Sep 17 00:00:00 2001 From: devbeni Date: Fri, 19 Sep 2025 19:02:15 +0200 Subject: [PATCH] Refactor socket handling and integrate database for event management - Removed old socket handling code and replaced it with a new implementation in `app/api/socketio/route.js`. - Added MySQL database integration for managing active sessions and queue entries. - Implemented event retrieval and ticket purchasing APIs in `app/api/events/route.js` and `app/api/purchase/route.js`. - Created database schema for events, tickets, active sessions, and orders in `database/schema.sql`. - Updated front-end to handle event data fetching and ticket purchasing with improved UI components. - Removed unused SVG files from the public directory. --- README.md | 44 +++++-- app/api/events/route.js | 59 +++++++++ app/api/purchase/route.js | 106 +++++++++++++++ app/api/socket/route.js | 157 ----------------------- app/api/socketio/route.js | 94 ++++++++++++-- app/page.tsx | 264 ++++++++++++++++++++++++++++++-------- database/schema.sql | 81 ++++++++++++ public/file.svg | 1 - public/globe.svg | 1 - public/next.svg | 1 - public/vercel.svg | 1 - public/window.svg | 1 - 12 files changed, 574 insertions(+), 236 deletions(-) create mode 100644 app/api/events/route.js create mode 100644 app/api/purchase/route.js delete mode 100644 app/api/socket/route.js create mode 100644 database/schema.sql delete mode 100644 public/file.svg delete mode 100644 public/globe.svg delete mode 100644 public/next.svg delete mode 100644 public/vercel.svg delete mode 100644 public/window.svg diff --git a/README.md b/README.md index a455f1d..54d59b6 100644 --- a/README.md +++ b/README.md @@ -47,9 +47,12 @@ This repository contains a minimal demo of a queue system for high-concurrency t Files added in this demo: -- `app/api/socketio/route.js` — Next.js App Router API route that initializes the Socket.IO server, in-memory queue logic and JWT issuance (demo only). -- `.env.example` — example environment variables for the queue system. -- `app/page.tsx` — client UI (Next.js) that connects via Socket.IO and displays queue position, estimated wait, and purchase button. +- `app/api/socketio/route.js` — Next.js App Router API route that initializes the Socket.IO server, queue logic and JWT issuance with MySQL persistence. +- `app/api/events/route.js` — API for fetching event and ticket information from MySQL. +- `app/api/purchase/route.js` — API for processing ticket purchases with JWT verification. +- `database/schema.sql` — MySQL database schema with tables for events, tickets, queue, orders, and active sessions. +- `.env.example` — example environment variables for the queue system and MySQL connection. +- `app/page.tsx` — modern, responsive client UI that connects via Socket.IO and provides a full ticket purchasing experience. Important notes and limitations: @@ -58,20 +61,35 @@ Important notes and limitations: Running locally -1. Copy `.env.example` to `.env` and adjust values (especially `JWT_SECRET`). -2. Install dependencies (already included in package.json): +1. **Setup MySQL Database:** + ```sql + -- Execute the schema in database/schema.sql + mysql -u root -p < database/schema.sql + ``` -```powershell -pnpm install -``` +2. **Environment Configuration:** + Copy `.env.example` to `.env` and configure your settings: + ```bash + cp .env.example .env + ``` + Update the MySQL connection details and JWT secret. -3. Start the Next.js dev server: +3. **Install Dependencies:** + ```powershell + pnpm install + ``` -```powershell -pnpm dev -``` +4. **Start the Application:** + ```powershell + pnpm dev + ``` -4. Open `http://localhost:3000` - the page will automatically initialize the Socket.IO server via API call to `/api/socketio`, then connect to it on port 4000. +5. **Access the Application:** + Open `http://localhost:3000` - the page will automatically: + - Initialize the Socket.IO server via `/api/socketio` + - Connect to the queue system on port 4000 + - Load event and ticket data from MySQL + - Provide a modern, responsive ticket purchasing interface Next steps / improvements diff --git a/app/api/events/route.js b/app/api/events/route.js new file mode 100644 index 0000000..d0ea8df --- /dev/null +++ b/app/api/events/route.js @@ -0,0 +1,59 @@ +import mysql from 'mysql2/promise' + +async function getDbConnection() { + if (!process.env.MYSQL_HOST) return null + return await mysql.createConnection({ + host: process.env.MYSQL_HOST, + user: process.env.MYSQL_USER, + password: process.env.MYSQL_PASSWORD, + database: process.env.MYSQL_DATABASE, + }) +} + +export async function GET(request) { + try { + const { searchParams } = new URL(request.url) + const eventId = searchParams.get('id') + + const connection = await getDbConnection() + if (!connection) { + return Response.json({ error: 'Database not configured' }, { status: 500 }) + } + + if (eventId) { + // Get specific event with tickets + const [eventRows] = await connection.execute( + 'SELECT * FROM events WHERE id = ?', + [eventId] + ) + + const [ticketRows] = await connection.execute( + 'SELECT * FROM tickets WHERE event_id = ? ORDER BY price ASC', + [eventId] + ) + + if (eventRows.length === 0) { + return Response.json({ error: 'Event not found' }, { status: 404 }) + } + + await connection.end() + + return Response.json({ + event: eventRows[0], + tickets: ticketRows + }) + } else { + // Get all events + const [eventRows] = await connection.execute( + 'SELECT e.*, COUNT(t.id) as ticket_types FROM events e LEFT JOIN tickets t ON e.id = t.event_id GROUP BY e.id ORDER BY e.created_at DESC' + ) + + await connection.end() + + return Response.json({ events: eventRows }) + } + } catch (error) { + console.error('Events API error:', error) + return Response.json({ error: 'Internal server error' }, { status: 500 }) + } +} diff --git a/app/api/purchase/route.js b/app/api/purchase/route.js new file mode 100644 index 0000000..926da2b --- /dev/null +++ b/app/api/purchase/route.js @@ -0,0 +1,106 @@ +import mysql from 'mysql2/promise' +import jwt from 'jsonwebtoken' + +async function getDbConnection() { + if (!process.env.MYSQL_HOST) return null + return await mysql.createConnection({ + host: process.env.MYSQL_HOST, + user: process.env.MYSQL_USER, + password: process.env.MYSQL_PASSWORD, + database: process.env.MYSQL_DATABASE, + }) +} + +export async function POST(request) { + try { + const { eventId, ticketType, quantity, token } = await request.json() + + if (!eventId || !ticketType || !quantity || !token) { + return Response.json({ error: 'Missing required fields' }, { status: 400 }) + } + + // Verify JWT token + let decoded + try { + decoded = jwt.verify(token, process.env.JWT_SECRET || 'dev-secret') + } catch (error) { + return Response.json({ error: 'Invalid or expired token' }, { status: 401 }) + } + + const connection = await getDbConnection() + if (!connection) { + return Response.json({ error: 'Database not configured' }, { status: 500 }) + } + + // Check if token is still active in database + const [activeRows] = await connection.execute( + 'SELECT * FROM active_sessions WHERE event_id = ? AND socket_id = ? AND expires_at > NOW()', + [eventId, decoded.sid] + ) + + if (activeRows.length === 0) { + await connection.end() + return Response.json({ error: 'Session expired or not authorized' }, { status: 401 }) + } + + // Get ticket information + const [ticketRows] = await connection.execute( + 'SELECT * FROM tickets WHERE event_id = ? AND type = ?', + [eventId, ticketType] + ) + + if (ticketRows.length === 0) { + await connection.end() + return Response.json({ error: 'Ticket type not found' }, { status: 404 }) + } + + const ticket = ticketRows[0] + const availableQuantity = ticket.total_quantity - ticket.sold_quantity + + if (quantity > availableQuantity) { + await connection.end() + return Response.json({ + error: 'Not enough tickets available', + available: availableQuantity + }, { status: 400 }) + } + + const totalPrice = ticket.price * quantity + + // Start transaction + await connection.beginTransaction() + + try { + // Create order + const [orderResult] = await connection.execute( + 'INSERT INTO orders (event_id, socket_id, ticket_type, quantity, total_price, status) VALUES (?, ?, ?, ?, ?, ?)', + [eventId, decoded.sid, ticketType, quantity, totalPrice, 'completed'] + ) + + // Update sold quantity + await connection.execute( + 'UPDATE tickets SET sold_quantity = sold_quantity + ? WHERE event_id = ? AND type = ?', + [quantity, eventId, ticketType] + ) + + await connection.commit() + await connection.end() + + return Response.json({ + success: true, + orderId: orderResult.insertId, + totalPrice, + message: `Successfully purchased ${quantity} ${ticketType} ticket(s)` + }) + + } catch (error) { + await connection.rollback() + await connection.end() + throw error + } + + } catch (error) { + console.error('Purchase API error:', error) + return Response.json({ error: 'Purchase failed' }, { status: 500 }) + } +} diff --git a/app/api/socket/route.js b/app/api/socket/route.js deleted file mode 100644 index 27a47da..0000000 --- a/app/api/socket/route.js +++ /dev/null @@ -1,157 +0,0 @@ -import { Server } from "socket.io"; -import jwt from "jsonwebtoken"; -import mysql from "mysql2/promise"; -import { NextRequest, NextResponse } from "next/server"; - -// Simple in-memory structures keyed by eventId -const events = {}; - -const QUEUE_THRESHOLD = parseInt(process.env.QUEUE_THRESHOLD || "100", 10); -const CONCURRENT_ACTIVE = parseInt(process.env.CONCURRENT_ACTIVE || "50", 10); -const TOKEN_TTL_SECONDS = parseInt(process.env.TOKEN_TTL_SECONDS || `${15 * 60}`, 10); - -function ensureEvent(eventId) { - if (!events[eventId]) { - events[eventId] = { - sockets: new Set(), // connected socket ids - queue: [], // array of socket ids in order - active: new Set(), // sockets that currently hold a token (allowed to buy) - queueOn: false, - }; - } - return events[eventId]; -} - -async function createDbPool() { - if (!process.env.MYSQL_HOST) return null; - return mysql.createPool({ - host: process.env.MYSQL_HOST, - user: process.env.MYSQL_USER, - password: process.env.MYSQL_PASSWORD, - database: process.env.MYSQL_DATABASE, - waitForConnections: true, - connectionLimit: 5, - }); -} - -function buildUpdate(ev) { - const positionMap = {}; - ev.queue.forEach((sid, idx) => (positionMap[sid] = idx + 1)); - return { - activeCount: ev.sockets.size, - queueOn: ev.queueOn, - estimatedWait: ev.queue.length * 5, // naive: 5s per person - positions: positionMap, - }; -} - -function broadcastUpdate(eventId, io) { - const ev = events[eventId]; - if (!ev) return; - // notify all connected sockets in room - for (const sid of ev.sockets) { - const pos = ev.queue.indexOf(sid); - io.to(sid).emit("queue_update", { - activeCount: ev.sockets.size, - position: pos === -1 ? null : pos + 1, - estimatedWait: pos === -1 ? null : ev.queue.length * 5, - }); - } -} - -function evaluateQueue(eventId, io) { - const ev = events[eventId]; - if (!ev) return; - - // ensure active set size <= CONCURRENT_ACTIVE - while (ev.active.size < CONCURRENT_ACTIVE && ev.queue.length > 0) { - const next = ev.queue.shift(); - if (!next) break; - // sign token - const token = jwt.sign({ sid: next, eventId }, process.env.JWT_SECRET || "dev-secret", { - expiresIn: TOKEN_TTL_SECONDS, - }); - ev.active.add(next); - io.to(next).emit("granted", { token, expiresAt: new Date(Date.now() + TOKEN_TTL_SECONDS * 1000).toISOString() }); - } - - // If too many active (rare), revoke oldest - if (ev.active.size > CONCURRENT_ACTIVE) { - const toRevoke = Array.from(ev.active).slice(CONCURRENT_ACTIVE); - toRevoke.forEach((sid) => { - ev.active.delete(sid); - io.to(sid).emit("revoked"); - }); - } - - // If queue no longer needed, clear it - if (ev.sockets.size < QUEUE_THRESHOLD) ev.queueOn = false; - - // broadcast - broadcastUpdate(eventId, io); -} - -export async function GET(req) { - const res = NextResponse.next(); - - if (global.io) { - console.log("Socket is already running"); - } else { - console.log("Socket is initializing"); - const httpServer = req.socket?.server; - - if (httpServer) { - const io = new Server(httpServer, { - path: "/api/socket", - cors: { origin: true }, - }); - - global.io = io; - - const db = await createDbPool(); - - io.on("connection", (socket) => { - console.log("connect", socket.id); - - socket.on("join_event", async ({ eventId }) => { - if (!eventId) return; - const ev = ensureEvent(eventId); - ev.sockets.add(socket.id); - socket.join(eventId); - - // compute counts - const activeCount = ev.sockets.size; - // turn on queue if threshold reached - if (activeCount >= QUEUE_THRESHOLD) ev.queueOn = true; - - // if queueOn and socket not active, add to queue - if (ev.queueOn && !ev.active.has(socket.id)) { - if (!ev.queue.includes(socket.id)) ev.queue.push(socket.id); - } - - // evaluate granting - evaluateQueue(eventId, io); - - // send initial update - io.to(socket.id).emit("queue_update", buildUpdate(ev)); - }); - - socket.on("disconnect", () => { - // remove from every event - for (const [eventId, ev] of Object.entries(events)) { - if (ev.sockets.has(socket.id)) ev.sockets.delete(socket.id); - const qi = ev.queue.indexOf(socket.id); - if (qi !== -1) ev.queue.splice(qi, 1); - if (ev.active.has(socket.id)) ev.active.delete(socket.id); - // re-evaluate to grant next in line - evaluateQueue(eventId, io); - // notify remaining sockets - broadcastUpdate(eventId, io); - } - }); - }); - } - } - - return new Response("Socket.IO server initialized", { status: 200 }); -} diff --git a/app/api/socketio/route.js b/app/api/socketio/route.js index f22bf4f..f355250 100644 --- a/app/api/socketio/route.js +++ b/app/api/socketio/route.js @@ -1,13 +1,29 @@ import { Server } from 'socket.io' import jwt from 'jsonwebtoken' +import mysql from 'mysql2/promise' -// Simple in-memory structures keyed by eventId +// Simple in-memory structures keyed by eventId (for fast access) const events = {} const QUEUE_THRESHOLD = parseInt(process.env.QUEUE_THRESHOLD || "100", 10) const CONCURRENT_ACTIVE = parseInt(process.env.CONCURRENT_ACTIVE || "50", 10) const TOKEN_TTL_SECONDS = parseInt(process.env.TOKEN_TTL_SECONDS || `${15 * 60}`, 10) +// MySQL kapcsolat +let db = null + +async function getDbConnection() { + if (!db && process.env.MYSQL_HOST) { + db = await mysql.createConnection({ + host: process.env.MYSQL_HOST, + user: process.env.MYSQL_USER, + password: process.env.MYSQL_PASSWORD, + database: process.env.MYSQL_DATABASE, + }) + } + return db +} + function ensureEvent(eventId) { if (!events[eventId]) { events[eventId] = { @@ -34,29 +50,59 @@ function broadcastUpdate(eventId, io) { } } -function evaluateQueue(eventId, io) { +async function evaluateQueue(eventId, io) { const ev = events[eventId] if (!ev) return + const connection = await getDbConnection() + // ensure active set size <= CONCURRENT_ACTIVE while (ev.active.size < CONCURRENT_ACTIVE && ev.queue.length > 0) { const next = ev.queue.shift() if (!next) break + // sign token + const expiresAt = new Date(Date.now() + TOKEN_TTL_SECONDS * 1000) const token = jwt.sign({ sid: next, eventId }, process.env.JWT_SECRET || "dev-secret", { expiresIn: TOKEN_TTL_SECONDS, }) + ev.active.add(next) - io.to(next).emit("granted", { token, expiresAt: new Date(Date.now() + TOKEN_TTL_SECONDS * 1000).toISOString() }) + + // Save to database + if (connection) { + try { + await connection.execute( + 'INSERT INTO active_sessions (event_id, socket_id, jwt_token, expires_at) VALUES (?, ?, ?, ?) ON DUPLICATE KEY UPDATE jwt_token = VALUES(jwt_token), expires_at = VALUES(expires_at)', + [eventId, next, token, expiresAt] + ) + } catch (error) { + console.error('DB error saving active session:', error) + } + } + + io.to(next).emit("granted", { token, expiresAt: expiresAt.toISOString() }) } // If too many active (rare), revoke oldest if (ev.active.size > CONCURRENT_ACTIVE) { const toRevoke = Array.from(ev.active).slice(CONCURRENT_ACTIVE) - toRevoke.forEach((sid) => { + for (const sid of toRevoke) { ev.active.delete(sid) io.to(sid).emit("revoked") - }) + + // Remove from database + if (connection) { + try { + await connection.execute( + 'DELETE FROM active_sessions WHERE event_id = ? AND socket_id = ?', + [eventId, sid] + ) + } catch (error) { + console.error('DB error removing active session:', error) + } + } + } } // If queue no longer needed, clear it @@ -98,18 +144,50 @@ export async function GET(req) { ev.sockets.add(socket.id) socket.join(eventId) + const connection = await getDbConnection() + + // Get event threshold from database + let eventThreshold = QUEUE_THRESHOLD + if (connection) { + try { + const [rows] = await connection.execute( + 'SELECT max_concurrent_users FROM events WHERE id = ?', + [eventId] + ) + if (rows.length > 0) { + eventThreshold = rows[0].max_concurrent_users + } + } catch (error) { + console.error('DB error getting event:', error) + } + } + // compute counts const activeCount = ev.sockets.size // turn on queue if threshold reached - if (activeCount >= QUEUE_THRESHOLD) ev.queueOn = true + if (activeCount >= eventThreshold) ev.queueOn = true // if queueOn and socket not active, add to queue if (ev.queueOn && !ev.active.has(socket.id)) { - if (!ev.queue.includes(socket.id)) ev.queue.push(socket.id) + if (!ev.queue.includes(socket.id)) { + ev.queue.push(socket.id) + + // Save queue position to database + if (connection) { + try { + await connection.execute( + 'INSERT INTO queue_entries (event_id, socket_id, position) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE position = VALUES(position)', + [eventId, socket.id, ev.queue.length] + ) + } catch (error) { + console.error('DB error saving queue entry:', error) + } + } + } } // evaluate granting - evaluateQueue(eventId, io) + await evaluateQueue(eventId, io) // send initial update const pos = ev.queue.indexOf(socket.id) diff --git a/app/page.tsx b/app/page.tsx index a94c702..31aa5d9 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,6 +1,4 @@ "use client"; - -import Image from "next/image"; import { useEffect, useState, useRef } from "react"; export default function Home() { @@ -10,8 +8,20 @@ export default function Home() { 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); + // Load event data + useEffect(() => { + fetch('/api/events?id=pamkutya') + .then(res => res.json()) + .then(data => setEventData(data)) + .catch(console.error); + }, []); + useEffect(() => { let mounted = true; @@ -87,68 +97,216 @@ export default function Home() { 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)."); + 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); + return ( -
-
-

Koncert jegyek — Queue rendszer demo

- -
-
-
- Event: pamkutya -
-
- Socket:{" "} - - {connected ? "connected" : "disconnected"} - -
+
+ {/* Background Effects */} +
+
+
+ +
+ {/* Header */} +
+
+
+ {connected ? 'Kapcsolódva' : 'Nincs kapcsolat'}
+

+ 🎵 Pam Kutya +

+

Legendás Underground Koncert

+
-
-
-
Aktív felhasználók
-
{activeUsers}
-
-
-
Helyed a sorban
-
{position ?? "—"}
-
-
-
Becsült várakozás
-
{estimatedWait ? `${Math.ceil(estimatedWait)} s` : "—"}
-
-
+ {/* 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ő
+
+
- {tokenExpiry && ( -
- Token lejár: {new Date(tokenExpiry).toLocaleTimeString()} + {/* 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'} +
+
+ ); + })} +
)}
-
+
-

A rendszer WebSocket + JWT alapú. Ha a rendezvény eléri a küszöböt, a rendszer sorba helyez.

- -
- Globe icon - Demo — Queue system -
+ {/* Footer */} +
+ WebSocket + JWT alapú várakozási rendszer • Demo célokra +
+
); } diff --git a/database/schema.sql b/database/schema.sql new file mode 100644 index 0000000..f405562 --- /dev/null +++ b/database/schema.sql @@ -0,0 +1,81 @@ +-- Adatbázis létrehozása a queue rendszerhez +CREATE DATABASE IF NOT EXISTS queue_demo CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +USE queue_demo; + +-- Events tábla - koncert események +CREATE TABLE IF NOT EXISTS events ( + id VARCHAR(100) PRIMARY KEY, + name VARCHAR(255) NOT NULL, + description TEXT, + max_concurrent_users INT DEFAULT 100, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP +); + +-- Queue tábla - várakozási sor +CREATE TABLE IF NOT EXISTS queue_entries ( + id INT AUTO_INCREMENT PRIMARY KEY, + event_id VARCHAR(100) NOT NULL, + socket_id VARCHAR(100) NOT NULL, + position INT NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + INDEX idx_event_position (event_id, position), + INDEX idx_socket_event (socket_id, event_id), + FOREIGN KEY (event_id) REFERENCES events(id) ON DELETE CASCADE +); + +-- Active sessions tábla - aktív felhasználók akik vásárolhatnak +CREATE TABLE IF NOT EXISTS active_sessions ( + id INT AUTO_INCREMENT PRIMARY KEY, + event_id VARCHAR(100) NOT NULL, + socket_id VARCHAR(100) NOT NULL, + jwt_token TEXT NOT NULL, + expires_at TIMESTAMP NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + INDEX idx_event_socket (event_id, socket_id), + INDEX idx_expires (expires_at), + FOREIGN KEY (event_id) REFERENCES events(id) ON DELETE CASCADE +); + +-- Tickets tábla - jegyek +CREATE TABLE IF NOT EXISTS tickets ( + id INT AUTO_INCREMENT PRIMARY KEY, + event_id VARCHAR(100) NOT NULL, + type VARCHAR(100) NOT NULL, -- 'VIP', 'Normal', 'Student' + price DECIMAL(10,2) NOT NULL, + total_quantity INT NOT NULL, + sold_quantity INT DEFAULT 0, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + INDEX idx_event (event_id), + FOREIGN KEY (event_id) REFERENCES events(id) ON DELETE CASCADE +); + +-- Orders tábla - rendelések +CREATE TABLE IF NOT EXISTS orders ( + id INT AUTO_INCREMENT PRIMARY KEY, + event_id VARCHAR(100) NOT NULL, + socket_id VARCHAR(100) NOT NULL, + ticket_type VARCHAR(100) NOT NULL, + quantity INT NOT NULL, + total_price DECIMAL(10,2) NOT NULL, + status ENUM('pending', 'completed', 'cancelled') DEFAULT 'pending', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + INDEX idx_event (event_id), + INDEX idx_socket (socket_id), + FOREIGN KEY (event_id) REFERENCES events(id) ON DELETE CASCADE +); + +-- Példa adatok beszúrása +INSERT IGNORE INTO events (id, name, description, max_concurrent_users) VALUES +('pamkutya', 'Pam Kutya Koncert', 'Legendás underground koncert a városban', 100), +('rock-fest', 'Rock Fesztivál 2025', 'Három napos rock fesztivál', 150); + +INSERT IGNORE INTO tickets (event_id, type, price, total_quantity) VALUES +('pamkutya', 'Normal', 5000.00, 500), +('pamkutya', 'VIP', 8000.00, 50), +('pamkutya', 'Student', 3000.00, 100), +('rock-fest', 'Normal', 12000.00, 1000), +('rock-fest', 'VIP', 25000.00, 200); diff --git a/public/file.svg b/public/file.svg deleted file mode 100644 index 004145c..0000000 --- a/public/file.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/public/globe.svg b/public/globe.svg deleted file mode 100644 index 567f17b..0000000 --- a/public/globe.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/public/next.svg b/public/next.svg deleted file mode 100644 index 5174b28..0000000 --- a/public/next.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/public/vercel.svg b/public/vercel.svg deleted file mode 100644 index 7705396..0000000 --- a/public/vercel.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/public/window.svg b/public/window.svg deleted file mode 100644 index b2b2a44..0000000 --- a/public/window.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file