Compare commits
22 Commits
e11466d3db
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 011dfdffc6 | |||
| eca7510eb8 | |||
| 05fc2496d7 | |||
| 0f913ee940 | |||
| 730044f0b7 | |||
| cb2f5732eb | |||
| e44a7672d5 | |||
| 84c6d72faf | |||
| 0ee01bc313 | |||
| 14b0a44117 | |||
| 49b6f79545 | |||
| 8fbaf158df | |||
| ff0625ea76 | |||
| 9d1428fde0 | |||
| b0deea5311 | |||
| 527a2586b7 | |||
| d5a5fd2d64 | |||
| 2c5461b0e0 | |||
| cb326f7190 | |||
| 741afb6e81 | |||
| ff81967a59 | |||
| e60c801f4e |
17
.env.example
Normal file
17
.env.example
Normal 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
3
.gitignore
vendored
@@ -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
|
||||
|
||||
94
README.md
94
README.md
@@ -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
59
app/api/events/route.js
Normal 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
106
app/api/purchase/route.js
Normal 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 })
|
||||
}
|
||||
}
|
||||
362
app/api/socketio/route.js
Normal file
362
app/api/socketio/route.js
Normal file
@@ -0,0 +1,362 @@
|
||||
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 - minden híváskor új connection
|
||||
async function getDbConnection() {
|
||||
if (!process.env.MYSQL_HOST) return null
|
||||
try {
|
||||
const connection = await mysql.createConnection({
|
||||
host: process.env.MYSQL_HOST,
|
||||
user: process.env.MYSQL_USER,
|
||||
password: process.env.MYSQL_PASSWORD,
|
||||
database: process.env.MYSQL_DATABASE,
|
||||
})
|
||||
return connection
|
||||
} catch (error) {
|
||||
console.error('MySQL connection error:', error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
// Get database time for consistency
|
||||
const [timeRows] = await connection.execute('SELECT NOW() as db_time')
|
||||
const dbTime = new Date(timeRows[0].db_time)
|
||||
const expiresAt = new Date(dbTime.getTime() + TOKEN_TTL_SECONDS * 1000)
|
||||
|
||||
const token = jwt.sign({ sid: next, eventId }, process.env.JWT_SECRET || "dev-secret", {
|
||||
expiresIn: TOKEN_TTL_SECONDS,
|
||||
})
|
||||
|
||||
console.log(`Creating queued token for ${next.substring(0, 8)}: DB time ${dbTime.toISOString()}, expires at ${expiresAt.toISOString()}, TTL: ${TOKEN_TTL_SECONDS}s`)
|
||||
|
||||
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 {
|
||||
// Debug: check current time vs stored times
|
||||
const [allSessions] = await connection.execute(
|
||||
'SELECT socket_id, expires_at, NOW() as server_time FROM active_sessions WHERE event_id = ?',
|
||||
[eventId]
|
||||
)
|
||||
|
||||
console.log(`Event ${eventId} - Current sessions:`, allSessions.map(s => ({
|
||||
socket: s.socket_id.substring(0, 8),
|
||||
expires: s.expires_at,
|
||||
server_time: s.server_time,
|
||||
expired: new Date(s.expires_at) < new Date(s.server_time)
|
||||
})))
|
||||
|
||||
const [expiredSessions] = await connection.execute(
|
||||
'SELECT socket_id FROM active_sessions WHERE event_id = ? AND expires_at < NOW()',
|
||||
[eventId]
|
||||
)
|
||||
|
||||
if (expiredSessions.length > 0) {
|
||||
console.log(`Found ${expiredSessions.length} expired sessions for event ${eventId}`)
|
||||
}
|
||||
|
||||
for (const session of expiredSessions) {
|
||||
const sid = session.socket_id
|
||||
if (ev.active.has(sid)) {
|
||||
console.log(`Sending token_expired to ${sid.substring(0, 8)}`)
|
||||
ev.active.delete(sid)
|
||||
io.to(sid).emit("token_expired")
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up expired sessions from database
|
||||
if (expiredSessions.length > 0) {
|
||||
console.log(`Cleaning up ${expiredSessions.length} expired sessions for event ${eventId}`);
|
||||
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
|
||||
|
||||
// Close database connection
|
||||
if (connection) {
|
||||
try {
|
||||
await connection.end()
|
||||
} catch (error) {
|
||||
console.error('Error closing DB connection:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
|
||||
// Clean up all expired sessions on server start
|
||||
const startupConnection = await getDbConnection()
|
||||
if (startupConnection) {
|
||||
try {
|
||||
const [result] = await startupConnection.execute('DELETE FROM active_sessions WHERE expires_at < NOW()')
|
||||
console.log(`Server startup: Cleaned ${result.affectedRows} expired sessions`)
|
||||
await startupConnection.end()
|
||||
} catch (error) {
|
||||
console.error('Cleanup error on startup:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 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.substring(0, 8)} (under threshold)`)
|
||||
|
||||
// Get server time from database to ensure consistency
|
||||
const [timeRows] = await connection.execute('SELECT NOW() as db_time')
|
||||
const dbTime = new Date(timeRows[0].db_time)
|
||||
const expiresAt = new Date(dbTime.getTime() + TOKEN_TTL_SECONDS * 1000)
|
||||
|
||||
console.log(`DB time: ${dbTime.toISOString()}, Token expires: ${expiresAt.toISOString()}, TTL: ${TOKEN_TTL_SECONDS}s`)
|
||||
|
||||
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,
|
||||
})
|
||||
|
||||
// Close database connection
|
||||
if (connection) {
|
||||
try {
|
||||
await connection.end()
|
||||
} catch (error) {
|
||||
console.error('Error closing DB connection:', error)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
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 })
|
||||
}
|
||||
}
|
||||
400
app/event/[id]/page.tsx
Normal file
400
app/event/[id]/page.tsx
Normal file
@@ -0,0 +1,400 @@
|
||||
"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')}`;
|
||||
};
|
||||
|
||||
// Function to clear all token and queue state
|
||||
const clearAllTokenState = () => {
|
||||
console.log("Clearing all token state");
|
||||
setHasAccess(false);
|
||||
setTokenExpiry(null);
|
||||
setPosition(null);
|
||||
setEstimatedWait(null);
|
||||
try {
|
||||
localStorage.removeItem("event_token");
|
||||
} catch (e) {}
|
||||
if (socketRef.current) {
|
||||
socketRef.current.disconnect();
|
||||
}
|
||||
};
|
||||
|
||||
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 => {
|
||||
console.log('Event API response:', data);
|
||||
if (data.error) {
|
||||
console.log('Event API error, redirecting:', 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 socketHost = process.env.NEXT_PUBLIC_SOCKET_HOST || window.location.hostname;
|
||||
const socket = io(`http://${socketHost}:${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) => {
|
||||
console.log("Granted event received:", data);
|
||||
const expiryTime = data.expiresAt ? Date.parse(data.expiresAt) : Date.now() + 15 * 60 * 1000;
|
||||
console.log("Setting token expiry:", {
|
||||
raw: data.expiresAt,
|
||||
parsed: new Date(expiryTime),
|
||||
now: new Date(),
|
||||
diffMs: expiryTime - Date.now(),
|
||||
diffMin: Math.round((expiryTime - Date.now()) / 60000)
|
||||
});
|
||||
|
||||
setHasAccess(true);
|
||||
setTokenExpiry(expiryTime);
|
||||
try {
|
||||
localStorage.setItem("event_token", data.token);
|
||||
} catch (e) {}
|
||||
});
|
||||
|
||||
socket.on("revoked", () => {
|
||||
setHasAccess(false);
|
||||
setTokenExpiry(null);
|
||||
localStorage.removeItem("event_token");
|
||||
});
|
||||
|
||||
socket.on("token_expired", () => {
|
||||
console.log("Token expired received from server");
|
||||
clearAllTokenState();
|
||||
setTimeout(() => {
|
||||
window.location.href = "/";
|
||||
}, 100);
|
||||
});
|
||||
})
|
||||
.catch(error => {
|
||||
console.error("Socket initialization error:", error);
|
||||
});
|
||||
|
||||
return () => {
|
||||
mounted = false;
|
||||
if (socketRef.current) socketRef.current.disconnect();
|
||||
};
|
||||
}, [eventId, loading]);
|
||||
|
||||
// Check for existing token on page load and clear everything
|
||||
useEffect(() => {
|
||||
console.log('Page loaded, clearing any existing token state');
|
||||
clearAllTokenState();
|
||||
}, []);
|
||||
|
||||
// Token expiry timer - ellenőrzés minden másodpercben
|
||||
useEffect(() => {
|
||||
if (!tokenExpiry) return;
|
||||
console.log('Setting token expiry timer for:', new Date(tokenExpiry));
|
||||
const id = setInterval(() => {
|
||||
const msLeft = tokenExpiry - Date.now();
|
||||
console.log('Token check - ms left:', msLeft);
|
||||
if (msLeft <= 0) {
|
||||
console.log('Token expired locally, redirecting to homepage');
|
||||
clearAllTokenState();
|
||||
setTimeout(() => {
|
||||
window.location.href = "/";
|
||||
}, 100);
|
||||
}
|
||||
}, 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>
|
||||
);
|
||||
}
|
||||
189
app/page.tsx
189
app/page.tsx
@@ -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} fő</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
81
database/schema.sql
Normal 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);
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { NextConfig } from "next";
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
/* config options here */
|
||||
allowedDevOrigins: ["10.20.0.188"]
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
|
||||
15
package.json
15
package.json
@@ -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
394
pnpm-lock.yaml
generated
@@ -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: {}
|
||||
|
||||
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
Reference in New Issue
Block a user