Files
funcode2.0/app/event/[id]/page.tsx

401 lines
15 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"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>
);
}