Compare commits

...

10 Commits

Author SHA1 Message Date
ff0625ea76 feat: enhance estimated wait time display and handle token expiration in event page 2025-09-19 19:31:18 +02:00
9d1428fde0 fix: update estimated wait time calculation in queue updates 2025-09-19 19:22:15 +02:00
b0deea5311 feat: add event page with Socket.IO integration and ticket purchasing functionality 2025-09-19 19:17:25 +02:00
527a2586b7 feat: enhance queue management with logging and immediate access granting 2025-09-19 19:07:28 +02:00
d5a5fd2d64 docs: update README to enhance project overview and clarify Socket.IO implementation details 2025-09-19 19:03:57 +02:00
2c5461b0e0 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.
2025-09-19 19:02:15 +02:00
cb326f7190 feat: implement Socket.IO server with queue management and update environment variables 2025-09-19 18:58:22 +02:00
741afb6e81 feat: remove Socket.IO server implementation from socket.js 2025-09-19 18:51:51 +02:00
ff81967a59 feat: refactor Socket.IO server implementation and update README for queue system 2025-09-19 18:51:28 +02:00
e60c801f4e 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.
2025-09-19 18:49:12 +02:00
16 changed files with 1490 additions and 139 deletions

17
.env.example Normal file
View File

@@ -0,0 +1,17 @@
# Socket server
SOCKET_PORT=4000
NEXT_PUBLIC_SOCKET_PORT=4000
# Queue settings
QUEUE_THRESHOLD=100
CONCURRENT_ACTIVE=50
TOKEN_TTL_SECONDS=900
# JWT
JWT_SECRET=your_jwt_secret_here
# MySQL (optional)
MYSQL_HOST=localhost
MYSQL_USER=root
MYSQL_PASSWORD=yourpassword
MYSQL_DATABASE=queue_demo

3
.gitignore vendored
View File

@@ -31,7 +31,8 @@ yarn-error.log*
.pnpm-debug.log*
# env files (can opt-in for committing if needed)
.env*
.env
.env.local
# vercel
.vercel

View File

@@ -1,36 +1,70 @@
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
### Why this project exists
## Getting Started
[Funcode.hu](https://funcode.hu/en) is a terrible website that handles queuing systems poorly. Their queue implementation is unreliable, crashes frequently, and provides a frustrating user experience during high-traffic events. This project demonstrates how to properly implement a robust, real-time queue system that can handle concurrent users efficiently.
First, run the development server:
### How it works
```bash
npm run dev
# or
yarn dev
# or
pnpm dev
# or
bun dev
**Core Architecture:**
- When an event reaches a concurrency threshold (configurable, default 100 users), the server enables a queue for that event
- Users connect via WebSocket (Socket.IO) for real-time communication
- When queueing is active, users are placed in a FIFO (First In, First Out) queue
- The server grants access (issues a signed JWT token) to up to CONCURRENT_ACTIVE users (default 50) at a time
- JWT tokens are valid for a short period (default 15 minutes). When expired, the server revokes access and grants tokens to the next users in queue
- Users must authenticate with their JWT token to make purchase API calls
**Socket.IO Implementation Details:**
1. **Connection Flow:**
```
Client connects → Socket.IO handshake → emit 'join_event' → Server adds to queue/grants access
```
2. **Event Types:**
- `join_event`: Client joins an event queue
- `queue_update`: Server sends position, wait time, active user count
- `granted`: Server grants access with JWT token
- `revoked`: Server revokes access (token expired)
- `disconnect`: Client leaves, removed from all queues
3. **Queue Management:**
- In-memory queue per event ID for fast access
- MySQL persistence for queue positions and active sessions
- Automatic queue activation when threshold reached
- Real-time updates to all connected clients
4. **Token Management:**
- JWT tokens signed with server secret
- Tokens include socket ID and event ID
- Database tracking of active sessions with expiration times
- Automatic cleanup of expired tokens
**Technical Flow Diagram:**
```
┌─────────────┐ WebSocket ┌──────────────┐ MySQL ┌─────────────┐
│ Browser │ ◄──────────────► │ Socket.IO │ ◄──────────► │ Database │
│ │ │ Server │ │ │
│ • Queue UI │ │ • Queue Mgmt │ │ • Events │
│ • Token │ │ • JWT Issue │ │ • Tickets │
│ • Purchase │ │ • Real-time │ │ • Orders │
└─────────────┘ └──────────────┘ └─────────────┘
│ │
│ HTTP API │
└────────────────────────────────┘
Purchase Requests
```
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
**Why it's better than Funcode.hu:**
- ✅ Real-time updates vs. page refreshes
- ✅ Persistent queue positions in database vs. memory-only
- ✅ JWT-based authentication vs. unreliable session management
- ✅ Graceful handling of disconnections vs. losing queue position
- ✅ Configurable thresholds vs. hard-coded limits
- ✅ Modern, responsive UI vs. outdated interface
- ✅ Transparent queue position and wait times vs. vague "please wait"
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
## Learn More
To learn more about Next.js, take a look at the following resources:
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
## Deploy on Vercel
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
**Production Considerations:**
- Use Redis for distributed queue state across multiple server instances
- Implement proper user authentication and rate limiting
- Add comprehensive logging and monitoring
- Set up load balancing for Socket.IO with sticky sessions
- Add automated cleanup of stale connections and expired tokens

59
app/api/events/route.js Normal file
View File

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

106
app/api/purchase/route.js Normal file
View File

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

299
app/api/socketio/route.js Normal file
View File

@@ -0,0 +1,299 @@
import { Server } from 'socket.io'
import jwt from 'jsonwebtoken'
import mysql from 'mysql2/promise'
// 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] = {
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]
}
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.active.size + pos) * TOKEN_TTL_SECONDS,
})
}
}
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)
// 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() })
}
// Check for expired tokens in database and notify clients
if (connection) {
try {
const [expiredSessions] = await connection.execute(
'SELECT socket_id FROM active_sessions WHERE event_id = ? AND expires_at < NOW()',
[eventId]
)
for (const session of expiredSessions) {
const sid = session.socket_id
if (ev.active.has(sid)) {
ev.active.delete(sid)
io.to(sid).emit("token_expired")
}
}
// Clean up expired sessions from database
if (expiredSessions.length > 0) {
await connection.execute(
'DELETE FROM active_sessions WHERE event_id = ? AND expires_at < NOW()',
[eventId]
)
}
} catch (error) {
console.error('DB error checking expired tokens:', error)
}
}
// If too many active (rare), revoke oldest
if (ev.active.size > CONCURRENT_ACTIVE) {
const toRevoke = Array.from(ev.active).slice(CONCURRENT_ACTIVE)
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
if (ev.sockets.size < QUEUE_THRESHOLD) ev.queueOn = false
// broadcast
broadcastUpdate(eventId, io)
}
export async function GET(req) {
if (global.io) {
console.log("Socket.IO már fut")
return Response.json({ message: "Socket.IO már inicializálva" })
}
console.log("Socket.IO inicializálása...")
try {
// Get the HTTP server instance from Next.js
const server = req.nextUrl.protocol === 'https:'
? require('https').createServer()
: require('http').createServer()
const io = new Server(server, {
cors: {
origin: "*",
methods: ["GET", "POST"]
}
})
global.io = io
// Periodikus token ellenőrzés minden 5 másodpercben
setInterval(async () => {
for (const eventId of Object.keys(events)) {
await evaluateQueue(eventId, io)
}
}, 5000)
io.on("connection", (socket) => {
console.log("Csatlakozott:", socket.id)
socket.on("join_event", async ({ eventId }) => {
if (!eventId) return
const ev = ensureEvent(eventId)
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
console.log(`Event ${eventId}: ${activeCount} users, threshold: ${eventThreshold}`)
// turn on queue if threshold reached
if (activeCount >= eventThreshold) {
ev.queueOn = true
console.log(`Queue activated for event ${eventId}`)
}
// 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)
console.log(`Added ${socket.id} to queue at position ${ev.queue.length}`)
// 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)
}
}
}
}
// If queue is NOT active and user doesn't have access, grant it immediately
if (!ev.queueOn && !ev.active.has(socket.id)) {
console.log(`Granting immediate access to ${socket.id} (under threshold)`)
const expiresAt = new Date(Date.now() + TOKEN_TTL_SECONDS * 1000)
const token = jwt.sign({ sid: socket.id, eventId }, process.env.JWT_SECRET || "dev-secret", {
expiresIn: TOKEN_TTL_SECONDS,
})
ev.active.add(socket.id)
// 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, socket.id, token, expiresAt]
)
} catch (error) {
console.error('DB error saving active session:', error)
}
}
io.to(socket.id).emit("granted", { token, expiresAt: expiresAt.toISOString() })
}
// evaluate granting (for queue users)
await evaluateQueue(eventId, io)
// send initial update
const pos = ev.queue.indexOf(socket.id)
io.to(socket.id).emit("queue_update", {
activeCount: ev.sockets.size,
position: pos === -1 ? null : pos + 1,
estimatedWait: pos === -1 ? null : (ev.active.size + pos) * TOKEN_TTL_SECONDS,
})
})
socket.on("disconnect", () => {
console.log("Lecsatlakozott:", socket.id)
// 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)
}
})
})
// Start the Socket.IO server on a different port
const SOCKET_PORT = process.env.SOCKET_PORT || 4000
server.listen(SOCKET_PORT, () => {
console.log(`Socket.IO szerver fut a ${SOCKET_PORT} porton`)
})
return Response.json({
message: "Socket.IO szerver inicializálva",
port: SOCKET_PORT
})
} catch (error) {
console.error("Socket.IO inicializálási hiba:", error)
return Response.json({ error: "Nem sikerült inicializálni a Socket.IO szervert" }, { status: 500 })
}
}

367
app/event/[id]/page.tsx Normal file
View File

@@ -0,0 +1,367 @@
"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;
// Helper function to format seconds to HH:MM format
const formatTime = (seconds: number): string => {
const hours = Math.floor(seconds / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}`;
};
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 [eventData, setEventData] = useState<any>(null);
const [selectedTicket, setSelectedTicket] = useState<string>("Normal");
const [quantity, setQuantity] = useState(1);
const [purchasing, setPurchasing] = useState(false);
const [loading, setLoading] = useState(true);
const socketRef = useRef<any>(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");
});
socket.on("token_expired", () => {
setHasAccess(false);
setTokenExpiry(null);
localStorage.removeItem("event_token");
// Redirect to homepage
window.location.href = "/";
});
})
.catch(error => {
console.error("Socket initialization error:", error);
});
return () => {
mounted = false;
if (socketRef.current) socketRef.current.disconnect();
};
}, [eventId, loading]);
// Token expiry timer - ellenőrzés minden másodpercben
useEffect(() => {
if (!tokenExpiry) return;
const id = setInterval(() => {
const msLeft = tokenExpiry - Date.now();
if (msLeft <= 0) {
setHasAccess(false);
setTokenExpiry(null);
localStorage.removeItem("event_token");
// Disconnect socket and redirect to homepage
if (socketRef.current) {
socketRef.current.disconnect();
}
window.location.href = "/";
}
}, 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 (
<div className="min-h-screen bg-gradient-to-br from-purple-900 via-blue-900 to-indigo-900 flex items-center justify-center">
<div className="text-white text-2xl">Loading event...</div>
</div>
);
}
const timeLeft = tokenExpiry ? Math.max(0, tokenExpiry - Date.now()) : 0;
const minutesLeft = Math.floor(timeLeft / 60000);
const secondsLeft = Math.floor((timeLeft % 60000) / 1000);
return (
<div className="min-h-screen bg-gradient-to-br from-purple-900 via-blue-900 to-indigo-900 relative overflow-hidden">
{/* Background Effects */}
<div className="absolute inset-0 opacity-10">
<div className="absolute bottom-0 left-0 right-0 h-1/2 bg-gradient-to-t from-white/5 to-transparent"></div>
</div>
<div className="relative z-10 flex flex-col min-h-screen">
{/* Header */}
<header className="p-6 text-center">
<div className="mb-4">
<Link href="/" className="inline-flex items-center text-white/60 hover:text-white transition-colors">
Vissza az eseményekhez
</Link>
</div>
<div className="inline-flex items-center gap-2 text-white/60 text-sm mb-2">
<div className={`w-2 h-2 rounded-full ${connected ? 'bg-green-400 animate-pulse' : 'bg-red-400'}`}></div>
{connected ? 'Kapcsolódva' : 'Nincs kapcsolat'}
</div>
<h1 className="text-4xl md:text-6xl font-bold text-white mb-2">
🎵 {eventData?.event?.name || eventId}
</h1>
<p className="text-xl text-white/80">{eventData?.event?.description}</p>
</header>
{/* Main Content */}
<main className="flex-1 flex items-center justify-center p-6">
<div className="w-full max-w-2xl">
{/* Queue Status Card */}
<div className="bg-white/10 backdrop-blur-lg rounded-3xl p-8 shadow-2xl border border-white/20">
<div className="text-center mb-8">
{position ? (
<>
<div className="text-6xl font-bold text-white mb-4">#{position}</div>
<p className="text-xl text-white/80">Helyed a várakozási sorban</p>
{estimatedWait && (
<p className="text-white/60 mt-2">
Becsült várakozási idő: <span className="font-mono">{formatTime(Math.ceil(estimatedWait))}</span>
</p>
)}
</>
) : hasAccess ? (
<>
<div className="text-6xl mb-4">🎫</div>
<p className="text-2xl font-bold text-green-400 mb-2">Hozzáféred engedélyezve!</p>
<p className="text-white/80">Most vásárolhatsz jegyeket</p>
</>
) : (
<>
<div className="text-6xl mb-4"></div>
<p className="text-xl text-white/80">Csatlakozás a sorhoz...</p>
</>
)}
</div>
{/* Stats Grid */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-8">
<div className="bg-white/5 rounded-2xl p-4 text-center">
<div className="text-2xl font-bold text-white">{activeUsers}</div>
<div className="text-sm text-white/60">Aktív felhasználó</div>
</div>
<div className="bg-white/5 rounded-2xl p-4 text-center">
<div className="text-2xl font-bold text-white">{position || '—'}</div>
<div className="text-sm text-white/60">Pozíció</div>
</div>
<div className="bg-white/5 rounded-2xl p-4 text-center">
<div className="text-2xl font-bold text-white">
{estimatedWait ? formatTime(Math.ceil(estimatedWait)) : '—'}
</div>
<div className="text-sm text-white/60">Várható idő</div>
</div>
</div>
{/* Purchase Form */}
{hasAccess && (
<div className="space-y-4 mb-6">
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-white/80 text-sm mb-2">Jegytípus</label>
<select
value={selectedTicket}
onChange={(e) => setSelectedTicket(e.target.value)}
className="w-full bg-white/10 border border-white/20 rounded-xl px-4 py-3 text-white"
>
{eventData?.tickets?.map((ticket: any) => (
<option key={ticket.type} value={ticket.type} className="bg-gray-800">
{ticket.type} - {ticket.price.toLocaleString()} Ft
</option>
))}
</select>
</div>
<div>
<label className="block text-white/80 text-sm mb-2">Darabszám</label>
<input
type="number"
min="1"
max="10"
value={quantity}
onChange={(e) => setQuantity(parseInt(e.target.value))}
className="w-full bg-white/10 border border-white/20 rounded-xl px-4 py-3 text-white"
/>
</div>
</div>
</div>
)}
{/* Action Button */}
<div className="space-y-4">
<button
onClick={tryBuy}
disabled={!hasAccess || purchasing}
className={`w-full py-4 px-6 rounded-2xl font-bold text-lg transition-all duration-300 ${
hasAccess && !purchasing
? 'bg-gradient-to-r from-green-500 to-emerald-600 hover:from-green-600 hover:to-emerald-700 text-white shadow-lg hover:shadow-xl transform hover:scale-105'
: 'bg-white/10 text-white/50 cursor-not-allowed'
}`}
>
{purchasing ? '⏳ Vásárlás...' : hasAccess ? '🎫 Jegyvásárlás' : '⏳ Várakozás...'}
</button>
{/* Token Timer */}
{hasAccess && tokenExpiry && (
<div className="bg-orange-500/20 border border-orange-400/30 rounded-xl p-4 text-center">
<div className="text-orange-300 font-semibold">
Hozzáférés lejár: {minutesLeft}:{secondsLeft.toString().padStart(2, '0')}
</div>
<div className="text-orange-200/80 text-sm mt-1">
Használd ki a lehetőséget!
</div>
</div>
)}
</div>
</div>
{/* Ticket Prices */}
{eventData?.tickets && (
<div className="mt-8 bg-white/5 backdrop-blur-lg rounded-2xl p-6 border border-white/10">
<h3 className="text-white font-bold text-lg mb-4 text-center">🎟 Jegytípusok</h3>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
{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 (
<div key={ticket.type} className={`bg-white/5 rounded-xl p-4 text-center ${index === 1 ? borders[index] + ' border' : ''}`}>
<div className="text-white font-bold">{ticket.type}</div>
<div className={`text-2xl font-bold ${colors[index % 3]}`}>
{ticket.price.toLocaleString()} Ft
</div>
<div className="text-white/60 text-sm">
{available > 0 ? `${available} db elérhető` : 'Elfogyott'}
</div>
</div>
);
})}
</div>
</div>
)}
</div>
</main>
{/* Footer */}
<footer className="p-4 text-center text-white/40 text-sm">
WebSocket + JWT alapú várakozási rendszer {eventData?.event?.name}
</footer>
</div>
</div>
);
}

View File

@@ -1,103 +1,96 @@
import Image from "next/image";
"use client";
import { useEffect, useState } from "react";
import Link from "next/link";
export default function Home() {
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>
const [events, setEvents] = useState<any[]>([]);
const [loading, setLoading] = useState(true);
<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>
</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>
</footer>
// Load all events
useEffect(() => {
fetch('/api/events')
.then(res => res.json())
.then(data => {
setEvents(data.events || []);
setLoading(false);
})
.catch(error => {
console.error('Error loading events:', error);
setLoading(false);
});
}, []);
if (loading) {
return (
<div className="min-h-screen bg-gradient-to-br from-purple-900 via-blue-900 to-indigo-900 flex items-center justify-center">
<div className="text-white text-2xl">Loading events...</div>
</div>
);
}
return (
<div className="min-h-screen bg-gradient-to-br from-purple-900 via-blue-900 to-indigo-900 relative overflow-hidden">
{/* Background Effects */}
<div className="absolute inset-0 opacity-10">
<div className="absolute bottom-0 left-0 right-0 h-1/2 bg-gradient-to-t from-white/5 to-transparent"></div>
</div>
<div className="relative z-10 flex flex-col min-h-screen">
{/* Header */}
<header className="p-6 text-center">
<h1 className="text-4xl md:text-6xl font-bold text-white mb-2">
🎵 Koncert Jegyek
</h1>
<p className="text-xl text-white/80">Válassz egy eseményt</p>
</header>
{/* Main Content */}
<main className="flex-1 flex items-center justify-center p-6">
<div className="w-full max-w-6xl">
{events.length === 0 ? (
<div className="text-center text-white/60">
<div className="text-6xl mb-4">🎪</div>
<p className="text-xl">Jelenleg nincsenek elérhető események</p>
</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
{events.map((event) => (
<Link key={event.id} href={`/event/${event.id}`}>
<div className="bg-white/10 backdrop-blur-lg rounded-3xl p-8 shadow-2xl border border-white/20 hover:bg-white/15 transition-all duration-300 hover:scale-105 cursor-pointer">
<div className="text-center">
<div className="text-4xl mb-4">🎵</div>
<h2 className="text-2xl font-bold text-white mb-4">{event.name}</h2>
<p className="text-white/80 mb-6">{event.description}</p>
<div className="space-y-3">
<div className="flex justify-between items-center bg-white/5 rounded-xl p-3">
<span className="text-white/60">Max egyidejű:</span>
<span className="text-white font-semibold">{event.max_concurrent_users} </span>
</div>
<div className="flex justify-between items-center bg-white/5 rounded-xl p-3">
<span className="text-white/60">Jegytípusok:</span>
<span className="text-white font-semibold">{event.ticket_types || 0} db</span>
</div>
</div>
<div className="mt-6 bg-gradient-to-r from-green-500 to-emerald-600 hover:from-green-600 hover:to-emerald-700 text-white py-3 px-6 rounded-2xl font-bold transition-all duration-300">
🎫 Jegyvásárlás
</div>
</div>
</div>
</Link>
))}
</div>
)}
</div>
</main>
{/* Footer */}
<footer className="p-4 text-center text-white/40 text-sm">
WebSocket + JWT alapú várakozási rendszer Válassz egy eseményt a jegyvásárláshoz
</footer>
</div>
</div>
);
}

81
database/schema.sql Normal file
View File

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

View File

@@ -9,19 +9,24 @@
"lint": "eslint"
},
"dependencies": {
"dotenv": "^17.2.2",
"jsonwebtoken": "^9.0.2",
"mysql2": "^3.15.0",
"next": "15.5.3",
"react": "19.1.0",
"react-dom": "19.1.0",
"next": "15.5.3"
"socket.io": "^4.8.1",
"socket.io-client": "^4.8.1"
},
"devDependencies": {
"typescript": "^5",
"@eslint/eslintrc": "^3",
"@tailwindcss/postcss": "^4",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
"@tailwindcss/postcss": "^4",
"tailwindcss": "^4",
"eslint": "^9",
"eslint-config-next": "15.5.3",
"@eslint/eslintrc": "^3"
"tailwindcss": "^4",
"typescript": "^5"
}
}

394
pnpm-lock.yaml generated
View File

@@ -8,6 +8,15 @@ importers:
.:
dependencies:
dotenv:
specifier: ^17.2.2
version: 17.2.2
jsonwebtoken:
specifier: ^9.0.2
version: 9.0.2
mysql2:
specifier: ^3.15.0
version: 3.15.0
next:
specifier: 15.5.3
version: 15.5.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
@@ -17,6 +26,12 @@ importers:
react-dom:
specifier: 19.1.0
version: 19.1.0(react@19.1.0)
socket.io:
specifier: ^4.8.1
version: 4.8.1
socket.io-client:
specifier: ^4.8.1
version: 4.8.1
devDependencies:
'@eslint/eslintrc':
specifier: ^3
@@ -340,6 +355,9 @@ packages:
'@rushstack/eslint-patch@1.12.0':
resolution: {integrity: sha512-5EwMtOqvJMMa3HbmxLlF74e+3/HhwBTMcvt3nqVJgGCozO6hzIPOBlwm8mGVNR9SN2IJpxSnlxczyDjcn7qIyw==}
'@socket.io/component-emitter@3.1.2':
resolution: {integrity: sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==}
'@swc/helpers@0.5.15':
resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==}
@@ -434,6 +452,9 @@ packages:
'@tybys/wasm-util@0.10.1':
resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==}
'@types/cors@2.8.19':
resolution: {integrity: sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==}
'@types/estree@1.0.8':
resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==}
@@ -608,6 +629,10 @@ packages:
cpu: [x64]
os: [win32]
accepts@1.3.8:
resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==}
engines: {node: '>= 0.6'}
acorn-jsx@5.3.2:
resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==}
peerDependencies:
@@ -675,6 +700,10 @@ packages:
resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==}
engines: {node: '>= 0.4'}
aws-ssl-profiles@1.1.2:
resolution: {integrity: sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==}
engines: {node: '>= 6.0.0'}
axe-core@4.10.3:
resolution: {integrity: sha512-Xm7bpRXnDSX2YE2YFfBk2FnF0ep6tmG7xPh8iHee8MIcrgq762Nkce856dYtJYLkuIoYZvGfTs/PbZhideTcEg==}
engines: {node: '>=4'}
@@ -686,6 +715,10 @@ packages:
balanced-match@1.0.2:
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
base64id@2.0.0:
resolution: {integrity: sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==}
engines: {node: ^4.5.0 || >= 5.9}
brace-expansion@1.1.12:
resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==}
@@ -696,6 +729,9 @@ packages:
resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==}
engines: {node: '>=8'}
buffer-equal-constant-time@1.0.1:
resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==}
call-bind-apply-helpers@1.0.2:
resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==}
engines: {node: '>= 0.4'}
@@ -736,6 +772,14 @@ packages:
concat-map@0.0.1:
resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
cookie@0.7.2:
resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==}
engines: {node: '>= 0.6'}
cors@2.8.5:
resolution: {integrity: sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==}
engines: {node: '>= 0.10'}
cross-spawn@7.0.6:
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
engines: {node: '>= 8'}
@@ -766,6 +810,15 @@ packages:
supports-color:
optional: true
debug@4.3.7:
resolution: {integrity: sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==}
engines: {node: '>=6.0'}
peerDependencies:
supports-color: '*'
peerDependenciesMeta:
supports-color:
optional: true
debug@4.4.3:
resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==}
engines: {node: '>=6.0'}
@@ -786,6 +839,10 @@ packages:
resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==}
engines: {node: '>= 0.4'}
denque@2.1.0:
resolution: {integrity: sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==}
engines: {node: '>=0.10'}
detect-libc@2.1.0:
resolution: {integrity: sha512-vEtk+OcP7VBRtQZ1EJ3bdgzSfBjgnEalLTp5zjJrS+2Z1w2KZly4SBdac/WDU3hhsNAZ9E8SC96ME4Ey8MZ7cg==}
engines: {node: '>=8'}
@@ -794,13 +851,31 @@ packages:
resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==}
engines: {node: '>=0.10.0'}
dotenv@17.2.2:
resolution: {integrity: sha512-Sf2LSQP+bOlhKWWyhFsn0UsfdK/kCWRv1iuA2gXAwt3dyNabr6QSj00I2V10pidqz69soatm9ZwZvpQMTIOd5Q==}
engines: {node: '>=12'}
dunder-proto@1.0.1:
resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
engines: {node: '>= 0.4'}
ecdsa-sig-formatter@1.0.11:
resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==}
emoji-regex@9.2.2:
resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==}
engine.io-client@6.6.3:
resolution: {integrity: sha512-T0iLjnyNWahNyv/lcjS2y4oE358tVS/SYQNxYXGAJ9/GLgH4VCvOQ/mhTjqU88mLZCQgiG8RIegFHYCdVC+j5w==}
engine.io-parser@5.2.3:
resolution: {integrity: sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==}
engines: {node: '>=10.0.0'}
engine.io@6.6.4:
resolution: {integrity: sha512-ZCkIjSYNDyGn0R6ewHDtXgns/Zre/NT6Agvq1/WobF7JXgFff4SeDroKiCO3fNJreU9YG429Sc81o4w5ok/W5g==}
engines: {node: '>=10.2.0'}
enhanced-resolve@5.18.3:
resolution: {integrity: sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==}
engines: {node: '>=10.13.0'}
@@ -1019,6 +1094,9 @@ packages:
functions-have-names@1.2.3:
resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==}
generate-function@2.3.1:
resolution: {integrity: sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==}
get-intrinsic@1.3.0:
resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==}
engines: {node: '>= 0.4'}
@@ -1087,6 +1165,10 @@ packages:
resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
engines: {node: '>= 0.4'}
iconv-lite@0.7.0:
resolution: {integrity: sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==}
engines: {node: '>=0.10.0'}
ignore@5.3.2:
resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==}
engines: {node: '>= 4'}
@@ -1174,6 +1256,9 @@ packages:
resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==}
engines: {node: '>=0.12.0'}
is-property@1.0.2:
resolution: {integrity: sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==}
is-regex@1.2.1:
resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==}
engines: {node: '>= 0.4'}
@@ -1244,10 +1329,20 @@ packages:
resolution: {integrity: sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==}
hasBin: true
jsonwebtoken@9.0.2:
resolution: {integrity: sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==}
engines: {node: '>=12', npm: '>=6'}
jsx-ast-utils@3.3.5:
resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==}
engines: {node: '>=4.0'}
jwa@1.4.2:
resolution: {integrity: sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw==}
jws@3.2.2:
resolution: {integrity: sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==}
keyv@4.5.4:
resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==}
@@ -1330,13 +1425,45 @@ packages:
resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==}
engines: {node: '>=10'}
lodash.includes@4.3.0:
resolution: {integrity: sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==}
lodash.isboolean@3.0.3:
resolution: {integrity: sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==}
lodash.isinteger@4.0.4:
resolution: {integrity: sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==}
lodash.isnumber@3.0.3:
resolution: {integrity: sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==}
lodash.isplainobject@4.0.6:
resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==}
lodash.isstring@4.0.1:
resolution: {integrity: sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==}
lodash.merge@4.6.2:
resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==}
lodash.once@4.1.1:
resolution: {integrity: sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==}
long@5.3.2:
resolution: {integrity: sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==}
loose-envify@1.4.0:
resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==}
hasBin: true
lru-cache@7.18.3:
resolution: {integrity: sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==}
engines: {node: '>=12'}
lru.min@1.1.2:
resolution: {integrity: sha512-Nv9KddBcQSlQopmBHXSsZVY5xsdlZkdH/Iey0BlcBYggMd4two7cZnKOK9vmy3nY0O5RGH99z1PCeTpPqszUYg==}
engines: {bun: '>=1.0.0', deno: '>=1.30.0', node: '>=8.0.0'}
magic-string@0.30.19:
resolution: {integrity: sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw==}
@@ -1352,6 +1479,14 @@ packages:
resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==}
engines: {node: '>=8.6'}
mime-db@1.52.0:
resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==}
engines: {node: '>= 0.6'}
mime-types@2.1.35:
resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==}
engines: {node: '>= 0.6'}
minimatch@3.1.2:
resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==}
@@ -1378,6 +1513,14 @@ packages:
ms@2.1.3:
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
mysql2@3.15.0:
resolution: {integrity: sha512-tT6pomf5Z/I7Jzxu8sScgrYBMK9bUFWd7Kbo6Fs1L0M13OOIJ/ZobGKS3Z7tQ8Re4lj+LnLXIQVZZxa3fhYKzA==}
engines: {node: '>= 8.0'}
named-placeholders@1.1.3:
resolution: {integrity: sha512-eLoBxg6wE/rZkJPhU/xRX1WTpkFEwDJEN96oxFrTsqBdbT5ec295Q+CoHrL9IT0DipqKhmGcaZmwOt8OON5x1w==}
engines: {node: '>=12.0.0'}
nanoid@3.3.11:
resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==}
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
@@ -1391,6 +1534,10 @@ packages:
natural-compare@1.4.0:
resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
negotiator@0.6.3:
resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==}
engines: {node: '>= 0.6'}
next@15.5.3:
resolution: {integrity: sha512-r/liNAx16SQj4D+XH/oI1dlpv9tdKJ6cONYPwwcCC46f2NjpaRWY+EKCzULfgQYV6YKXjHBchff2IZBSlZmJNw==}
engines: {node: ^18.18.0 || ^19.8.0 || >= 20.0.0}
@@ -1559,6 +1706,9 @@ packages:
resolution: {integrity: sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==}
engines: {node: '>=0.4'}
safe-buffer@5.2.1:
resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==}
safe-push-apply@1.0.0:
resolution: {integrity: sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==}
engines: {node: '>= 0.4'}
@@ -1567,6 +1717,9 @@ packages:
resolution: {integrity: sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==}
engines: {node: '>= 0.4'}
safer-buffer@2.1.2:
resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
scheduler@0.26.0:
resolution: {integrity: sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==}
@@ -1579,6 +1732,9 @@ packages:
engines: {node: '>=10'}
hasBin: true
seq-queue@0.0.5:
resolution: {integrity: sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q==}
set-function-length@1.2.2:
resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==}
engines: {node: '>= 0.4'}
@@ -1619,10 +1775,29 @@ packages:
resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==}
engines: {node: '>= 0.4'}
socket.io-adapter@2.5.5:
resolution: {integrity: sha512-eLDQas5dzPgOWCk9GuuJC2lBqItuhKI4uxGgo9aIV7MYbk2h9Q6uULEh8WBzThoI7l+qU9Ast9fVUmkqPP9wYg==}
socket.io-client@4.8.1:
resolution: {integrity: sha512-hJVXfu3E28NmzGk8o1sHhN3om52tRvwYeidbj7xKy2eIIse5IoKX3USlS6Tqt3BHAtflLIkCQBkzVrEEfWUyYQ==}
engines: {node: '>=10.0.0'}
socket.io-parser@4.2.4:
resolution: {integrity: sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==}
engines: {node: '>=10.0.0'}
socket.io@4.8.1:
resolution: {integrity: sha512-oZ7iUCxph8WYRHHcjBEc9unw3adt5CmSNlppj/5Q4k2RIrhl8Z5yY2Xr4j9zj0+wzVZ0bxmYoGSzKJnRl6A4yg==}
engines: {node: '>=10.2.0'}
source-map-js@1.2.1:
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
engines: {node: '>=0.10.0'}
sqlstring@2.3.3:
resolution: {integrity: sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==}
engines: {node: '>= 0.6'}
stable-hash@0.0.5:
resolution: {integrity: sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA==}
@@ -1751,6 +1926,10 @@ packages:
uri-js@4.4.1:
resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
vary@1.1.2:
resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==}
engines: {node: '>= 0.8'}
which-boxed-primitive@1.1.1:
resolution: {integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==}
engines: {node: '>= 0.4'}
@@ -1776,6 +1955,22 @@ packages:
resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==}
engines: {node: '>=0.10.0'}
ws@8.17.1:
resolution: {integrity: sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==}
engines: {node: '>=10.0.0'}
peerDependencies:
bufferutil: ^4.0.1
utf-8-validate: '>=5.0.2'
peerDependenciesMeta:
bufferutil:
optional: true
utf-8-validate:
optional: true
xmlhttprequest-ssl@2.1.2:
resolution: {integrity: sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==}
engines: {node: '>=0.4.0'}
yallist@5.0.0:
resolution: {integrity: sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==}
engines: {node: '>=18'}
@@ -2026,6 +2221,8 @@ snapshots:
'@rushstack/eslint-patch@1.12.0': {}
'@socket.io/component-emitter@3.1.2': {}
'@swc/helpers@0.5.15':
dependencies:
tslib: 2.8.1
@@ -2107,6 +2304,10 @@ snapshots:
tslib: 2.8.1
optional: true
'@types/cors@2.8.19':
dependencies:
'@types/node': 20.19.17
'@types/estree@1.0.8': {}
'@types/json-schema@7.0.15': {}
@@ -2277,6 +2478,11 @@ snapshots:
'@unrs/resolver-binding-win32-x64-msvc@1.11.1':
optional: true
accepts@1.3.8:
dependencies:
mime-types: 2.1.35
negotiator: 0.6.3
acorn-jsx@5.3.2(acorn@8.15.0):
dependencies:
acorn: 8.15.0
@@ -2373,12 +2579,16 @@ snapshots:
dependencies:
possible-typed-array-names: 1.1.0
aws-ssl-profiles@1.1.2: {}
axe-core@4.10.3: {}
axobject-query@4.1.0: {}
balanced-match@1.0.2: {}
base64id@2.0.0: {}
brace-expansion@1.1.12:
dependencies:
balanced-match: 1.0.2
@@ -2392,6 +2602,8 @@ snapshots:
dependencies:
fill-range: 7.1.1
buffer-equal-constant-time@1.0.1: {}
call-bind-apply-helpers@1.0.2:
dependencies:
es-errors: 1.3.0
@@ -2430,6 +2642,13 @@ snapshots:
concat-map@0.0.1: {}
cookie@0.7.2: {}
cors@2.8.5:
dependencies:
object-assign: 4.1.1
vary: 1.1.2
cross-spawn@7.0.6:
dependencies:
path-key: 3.1.1
@@ -2462,6 +2681,10 @@ snapshots:
dependencies:
ms: 2.1.3
debug@4.3.7:
dependencies:
ms: 2.1.3
debug@4.4.3:
dependencies:
ms: 2.1.3
@@ -2480,20 +2703,58 @@ snapshots:
has-property-descriptors: 1.0.2
object-keys: 1.1.1
denque@2.1.0: {}
detect-libc@2.1.0: {}
doctrine@2.1.0:
dependencies:
esutils: 2.0.3
dotenv@17.2.2: {}
dunder-proto@1.0.1:
dependencies:
call-bind-apply-helpers: 1.0.2
es-errors: 1.3.0
gopd: 1.2.0
ecdsa-sig-formatter@1.0.11:
dependencies:
safe-buffer: 5.2.1
emoji-regex@9.2.2: {}
engine.io-client@6.6.3:
dependencies:
'@socket.io/component-emitter': 3.1.2
debug: 4.3.7
engine.io-parser: 5.2.3
ws: 8.17.1
xmlhttprequest-ssl: 2.1.2
transitivePeerDependencies:
- bufferutil
- supports-color
- utf-8-validate
engine.io-parser@5.2.3: {}
engine.io@6.6.4:
dependencies:
'@types/cors': 2.8.19
'@types/node': 20.19.17
accepts: 1.3.8
base64id: 2.0.0
cookie: 0.7.2
cors: 2.8.5
debug: 4.3.7
engine.io-parser: 5.2.3
ws: 8.17.1
transitivePeerDependencies:
- bufferutil
- supports-color
- utf-8-validate
enhanced-resolve@5.18.3:
dependencies:
graceful-fs: 4.2.11
@@ -2866,6 +3127,10 @@ snapshots:
functions-have-names@1.2.3: {}
generate-function@2.3.1:
dependencies:
is-property: 1.0.2
get-intrinsic@1.3.0:
dependencies:
call-bind-apply-helpers: 1.0.2
@@ -2937,6 +3202,10 @@ snapshots:
dependencies:
function-bind: 1.1.2
iconv-lite@0.7.0:
dependencies:
safer-buffer: 2.1.2
ignore@5.3.2: {}
ignore@7.0.5: {}
@@ -3026,6 +3295,8 @@ snapshots:
is-number@7.0.0: {}
is-property@1.0.2: {}
is-regex@1.2.1:
dependencies:
call-bound: 1.0.4
@@ -3096,6 +3367,19 @@ snapshots:
dependencies:
minimist: 1.2.8
jsonwebtoken@9.0.2:
dependencies:
jws: 3.2.2
lodash.includes: 4.3.0
lodash.isboolean: 3.0.3
lodash.isinteger: 4.0.4
lodash.isnumber: 3.0.3
lodash.isplainobject: 4.0.6
lodash.isstring: 4.0.1
lodash.once: 4.1.1
ms: 2.1.3
semver: 7.7.2
jsx-ast-utils@3.3.5:
dependencies:
array-includes: 3.1.9
@@ -3103,6 +3387,17 @@ snapshots:
object.assign: 4.1.7
object.values: 1.2.1
jwa@1.4.2:
dependencies:
buffer-equal-constant-time: 1.0.1
ecdsa-sig-formatter: 1.0.11
safe-buffer: 5.2.1
jws@3.2.2:
dependencies:
jwa: 1.4.2
safe-buffer: 5.2.1
keyv@4.5.4:
dependencies:
json-buffer: 3.0.1
@@ -3167,12 +3462,32 @@ snapshots:
dependencies:
p-locate: 5.0.0
lodash.includes@4.3.0: {}
lodash.isboolean@3.0.3: {}
lodash.isinteger@4.0.4: {}
lodash.isnumber@3.0.3: {}
lodash.isplainobject@4.0.6: {}
lodash.isstring@4.0.1: {}
lodash.merge@4.6.2: {}
lodash.once@4.1.1: {}
long@5.3.2: {}
loose-envify@1.4.0:
dependencies:
js-tokens: 4.0.0
lru-cache@7.18.3: {}
lru.min@1.1.2: {}
magic-string@0.30.19:
dependencies:
'@jridgewell/sourcemap-codec': 1.5.5
@@ -3186,6 +3501,12 @@ snapshots:
braces: 3.0.3
picomatch: 2.3.1
mime-db@1.52.0: {}
mime-types@2.1.35:
dependencies:
mime-db: 1.52.0
minimatch@3.1.2:
dependencies:
brace-expansion: 1.1.12
@@ -3206,12 +3527,30 @@ snapshots:
ms@2.1.3: {}
mysql2@3.15.0:
dependencies:
aws-ssl-profiles: 1.1.2
denque: 2.1.0
generate-function: 2.3.1
iconv-lite: 0.7.0
long: 5.3.2
lru.min: 1.1.2
named-placeholders: 1.1.3
seq-queue: 0.0.5
sqlstring: 2.3.3
named-placeholders@1.1.3:
dependencies:
lru-cache: 7.18.3
nanoid@3.3.11: {}
napi-postinstall@0.3.3: {}
natural-compare@1.4.0: {}
negotiator@0.6.3: {}
next@15.5.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0):
dependencies:
'@next/env': 15.5.3
@@ -3401,6 +3740,8 @@ snapshots:
has-symbols: 1.1.0
isarray: 2.0.5
safe-buffer@5.2.1: {}
safe-push-apply@1.0.0:
dependencies:
es-errors: 1.3.0
@@ -3412,12 +3753,16 @@ snapshots:
es-errors: 1.3.0
is-regex: 1.2.1
safer-buffer@2.1.2: {}
scheduler@0.26.0: {}
semver@6.3.1: {}
semver@7.7.2: {}
seq-queue@0.0.5: {}
set-function-length@1.2.2:
dependencies:
define-data-property: 1.1.4
@@ -3504,8 +3849,51 @@ snapshots:
side-channel-map: 1.0.1
side-channel-weakmap: 1.0.2
socket.io-adapter@2.5.5:
dependencies:
debug: 4.3.7
ws: 8.17.1
transitivePeerDependencies:
- bufferutil
- supports-color
- utf-8-validate
socket.io-client@4.8.1:
dependencies:
'@socket.io/component-emitter': 3.1.2
debug: 4.3.7
engine.io-client: 6.6.3
socket.io-parser: 4.2.4
transitivePeerDependencies:
- bufferutil
- supports-color
- utf-8-validate
socket.io-parser@4.2.4:
dependencies:
'@socket.io/component-emitter': 3.1.2
debug: 4.3.7
transitivePeerDependencies:
- supports-color
socket.io@4.8.1:
dependencies:
accepts: 1.3.8
base64id: 2.0.0
cors: 2.8.5
debug: 4.3.7
engine.io: 6.6.4
socket.io-adapter: 2.5.5
socket.io-parser: 4.2.4
transitivePeerDependencies:
- bufferutil
- supports-color
- utf-8-validate
source-map-js@1.2.1: {}
sqlstring@2.3.3: {}
stable-hash@0.0.5: {}
stop-iteration-iterator@1.1.0:
@@ -3689,6 +4077,8 @@ snapshots:
dependencies:
punycode: 2.3.1
vary@1.1.2: {}
which-boxed-primitive@1.1.1:
dependencies:
is-bigint: 1.1.0
@@ -3736,6 +4126,10 @@ snapshots:
word-wrap@1.2.5: {}
ws@8.17.1: {}
xmlhttprequest-ssl@2.1.2: {}
yallist@5.0.0: {}
yocto-queue@0.1.0: {}

View File

@@ -1 +0,0 @@
<svg fill="none" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M14.5 13.5V5.41a1 1 0 0 0-.3-.7L9.8.29A1 1 0 0 0 9.08 0H1.5v13.5A2.5 2.5 0 0 0 4 16h8a2.5 2.5 0 0 0 2.5-2.5m-1.5 0v-7H8v-5H3v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1M9.5 5V2.12L12.38 5zM5.13 5h-.62v1.25h2.12V5zm-.62 3h7.12v1.25H4.5zm.62 3h-.62v1.25h7.12V11z" clip-rule="evenodd" fill="#666" fill-rule="evenodd"/></svg>

Before

Width:  |  Height:  |  Size: 391 B

View File

@@ -1 +0,0 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><g clip-path="url(#a)"><path fill-rule="evenodd" clip-rule="evenodd" d="M10.27 14.1a6.5 6.5 0 0 0 3.67-3.45q-1.24.21-2.7.34-.31 1.83-.97 3.1M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16m.48-1.52a7 7 0 0 1-.96 0H7.5a4 4 0 0 1-.84-1.32q-.38-.89-.63-2.08a40 40 0 0 0 3.92 0q-.25 1.2-.63 2.08a4 4 0 0 1-.84 1.31zm2.94-4.76q1.66-.15 2.95-.43a7 7 0 0 0 0-2.58q-1.3-.27-2.95-.43a18 18 0 0 1 0 3.44m-1.27-3.54a17 17 0 0 1 0 3.64 39 39 0 0 1-4.3 0 17 17 0 0 1 0-3.64 39 39 0 0 1 4.3 0m1.1-1.17q1.45.13 2.69.34a6.5 6.5 0 0 0-3.67-3.44q.65 1.26.98 3.1M8.48 1.5l.01.02q.41.37.84 1.31.38.89.63 2.08a40 40 0 0 0-3.92 0q.25-1.2.63-2.08a4 4 0 0 1 .85-1.32 7 7 0 0 1 .96 0m-2.75.4a6.5 6.5 0 0 0-3.67 3.44 29 29 0 0 1 2.7-.34q.31-1.83.97-3.1M4.58 6.28q-1.66.16-2.95.43a7 7 0 0 0 0 2.58q1.3.27 2.95.43a18 18 0 0 1 0-3.44m.17 4.71q-1.45-.12-2.69-.34a6.5 6.5 0 0 0 3.67 3.44q-.65-1.27-.98-3.1" fill="#666"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"/></clipPath></defs></svg>

Before

Width:  |  Height:  |  Size: 1.0 KiB

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>

Before

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -1 +0,0 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1155 1000"><path d="m577.3 0 577.4 1000H0z" fill="#fff"/></svg>

Before

Width:  |  Height:  |  Size: 128 B

View File

@@ -1 +0,0 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill-rule="evenodd" clip-rule="evenodd" d="M1.5 2.5h13v10a1 1 0 0 1-1 1h-11a1 1 0 0 1-1-1zM0 1h16v11.5a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 0 12.5zm3.75 4.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5M7 4.75a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0m1.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5" fill="#666"/></svg>

Before

Width:  |  Height:  |  Size: 385 B