249 lines
8.5 KiB
React
249 lines
8.5 KiB
React
|
|
import { useState, useEffect } from 'react';
|
|||
|
|
import { useParams, Link } from 'react-router-dom';
|
|||
|
|
import { QRCodeSVG } from 'qrcode.react';
|
|||
|
|
import { Copy, Check, Users, Calendar, MapPin, QrCode } from 'lucide-react';
|
|||
|
|
import Layout from '../components/layout/Layout';
|
|||
|
|
import { eventsAPI } from '../services/api';
|
|||
|
|
|
|||
|
|
export default function EventDetailsPage() {
|
|||
|
|
const { slug } = useParams();
|
|||
|
|
const [eventDetails, setEventDetails] = useState(null);
|
|||
|
|
const [loading, setLoading] = useState(true);
|
|||
|
|
const [error, setError] = useState('');
|
|||
|
|
const [copied, setCopied] = useState(false);
|
|||
|
|
|
|||
|
|
useEffect(() => {
|
|||
|
|
fetchEventDetails();
|
|||
|
|
}, [slug]);
|
|||
|
|
|
|||
|
|
const fetchEventDetails = async () => {
|
|||
|
|
try {
|
|||
|
|
setLoading(true);
|
|||
|
|
const response = await eventsAPI.getDetails(slug);
|
|||
|
|
setEventDetails(response.data);
|
|||
|
|
} catch (err) {
|
|||
|
|
console.error('Error loading event details:', err);
|
|||
|
|
setError(err.message || 'Failed to load event details');
|
|||
|
|
} finally {
|
|||
|
|
setLoading(false);
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const copyToClipboard = async () => {
|
|||
|
|
try {
|
|||
|
|
await navigator.clipboard.writeText(eventDetails.checkin.url);
|
|||
|
|
setCopied(true);
|
|||
|
|
setTimeout(() => setCopied(false), 2000);
|
|||
|
|
} catch (err) {
|
|||
|
|
console.error('Failed to copy:', err);
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const formatDate = (dateString) => {
|
|||
|
|
return new Date(dateString).toLocaleDateString('en-US', {
|
|||
|
|
year: 'numeric',
|
|||
|
|
month: 'long',
|
|||
|
|
day: 'numeric',
|
|||
|
|
});
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
if (loading) {
|
|||
|
|
return (
|
|||
|
|
<Layout>
|
|||
|
|
<div className="flex items-center justify-center min-h-screen">
|
|||
|
|
<div className="text-center">
|
|||
|
|
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-primary-600 mx-auto"></div>
|
|||
|
|
<p className="mt-4 text-gray-600">Loading event details...</p>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</Layout>
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (error) {
|
|||
|
|
return (
|
|||
|
|
<Layout>
|
|||
|
|
<div className="max-w-4xl mx-auto p-6">
|
|||
|
|
<div className="bg-red-50 border border-red-200 rounded-lg p-4">
|
|||
|
|
<p className="text-red-800">{error}</p>
|
|||
|
|
<Link to="/events" className="text-red-600 hover:underline mt-2 inline-block">
|
|||
|
|
Back to Events
|
|||
|
|
</Link>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</Layout>
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (!eventDetails) {
|
|||
|
|
return null;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const { event, checkin, participants, stats } = eventDetails;
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<Layout>
|
|||
|
|
<div className="max-w-6xl mx-auto p-6">
|
|||
|
|
{/* Header */}
|
|||
|
|
<div className="mb-6">
|
|||
|
|
<Link to="/events" className="text-primary-600 hover:underline mb-2 inline-block">
|
|||
|
|
← Back to Events
|
|||
|
|
</Link>
|
|||
|
|
<h1 className="text-3xl font-bold text-gray-900">{event.name}</h1>
|
|||
|
|
<div className="flex items-center gap-4 mt-2 text-gray-600">
|
|||
|
|
<div className="flex items-center gap-1">
|
|||
|
|
<MapPin size={16} />
|
|||
|
|
<span>{event.location}</span>
|
|||
|
|
</div>
|
|||
|
|
<div className="flex items-center gap-1">
|
|||
|
|
<Calendar size={16} />
|
|||
|
|
<span>{formatDate(event.startDate)} - {formatDate(event.endDate)}</span>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div className="grid md:grid-cols-2 gap-6">
|
|||
|
|
{/* QR Code Section */}
|
|||
|
|
<div className="bg-white rounded-lg shadow-md p-6">
|
|||
|
|
<h2 className="text-xl font-semibold mb-4 flex items-center gap-2">
|
|||
|
|
<QrCode className="text-primary-600" />
|
|||
|
|
Event Check-in QR Code
|
|||
|
|
</h2>
|
|||
|
|
|
|||
|
|
{/* QR Code Display */}
|
|||
|
|
<div className="bg-white p-6 rounded-lg border-2 border-gray-200 mb-4 flex justify-center">
|
|||
|
|
<QRCodeSVG
|
|||
|
|
value={checkin.url}
|
|||
|
|
size={256}
|
|||
|
|
level="H"
|
|||
|
|
includeMargin={true}
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{/* Check-in URL */}
|
|||
|
|
<div className="mb-4">
|
|||
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|||
|
|
Check-in Link
|
|||
|
|
</label>
|
|||
|
|
<div className="flex gap-2">
|
|||
|
|
<input
|
|||
|
|
type="text"
|
|||
|
|
value={checkin.url}
|
|||
|
|
readOnly
|
|||
|
|
className="flex-1 px-3 py-2 border border-gray-300 rounded-md bg-gray-50 text-sm"
|
|||
|
|
/>
|
|||
|
|
<button
|
|||
|
|
onClick={copyToClipboard}
|
|||
|
|
className="px-4 py-2 bg-primary-600 text-white rounded-md hover:bg-primary-700 transition-colors flex items-center gap-2"
|
|||
|
|
>
|
|||
|
|
{copied ? <Check size={16} /> : <Copy size={16} />}
|
|||
|
|
{copied ? 'Copied!' : 'Copy'}
|
|||
|
|
</button>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{/* Valid Dates */}
|
|||
|
|
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4 text-sm">
|
|||
|
|
<p className="font-medium text-blue-900 mb-1">Valid Period</p>
|
|||
|
|
<p className="text-blue-700">
|
|||
|
|
{formatDate(checkin.validFrom)} - {formatDate(checkin.validUntil)}
|
|||
|
|
</p>
|
|||
|
|
{process.env.NODE_ENV === 'development' && (
|
|||
|
|
<p className="text-blue-600 mt-2 text-xs">
|
|||
|
|
⚠️ Development mode: Date validation disabled
|
|||
|
|
</p>
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{/* Participants Section */}
|
|||
|
|
<div className="bg-white rounded-lg shadow-md p-6">
|
|||
|
|
<h2 className="text-xl font-semibold mb-4 flex items-center gap-2">
|
|||
|
|
<Users className="text-primary-600" />
|
|||
|
|
Participants ({stats.totalParticipants})
|
|||
|
|
</h2>
|
|||
|
|
|
|||
|
|
{participants.length === 0 ? (
|
|||
|
|
<div className="text-center py-8 text-gray-500">
|
|||
|
|
<Users size={48} className="mx-auto mb-2 text-gray-300" />
|
|||
|
|
<p>No participants yet</p>
|
|||
|
|
<p className="text-sm">Share the QR code to get started!</p>
|
|||
|
|
</div>
|
|||
|
|
) : (
|
|||
|
|
<div className="space-y-3 max-h-[500px] overflow-y-auto">
|
|||
|
|
{participants.map((participant) => (
|
|||
|
|
<div
|
|||
|
|
key={participant.userId}
|
|||
|
|
className="flex items-center gap-3 p-3 bg-gray-50 rounded-lg hover:bg-gray-100 transition-colors"
|
|||
|
|
>
|
|||
|
|
{/* Avatar */}
|
|||
|
|
<div className="w-10 h-10 rounded-full bg-primary-600 flex items-center justify-center text-white font-semibold">
|
|||
|
|
{participant.avatar ? (
|
|||
|
|
<img
|
|||
|
|
src={participant.avatar}
|
|||
|
|
alt={participant.username}
|
|||
|
|
className="w-10 h-10 rounded-full object-cover"
|
|||
|
|
/>
|
|||
|
|
) : (
|
|||
|
|
<span>{participant.username.charAt(0).toUpperCase()}</span>
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{/* User Info */}
|
|||
|
|
<div className="flex-1">
|
|||
|
|
<p className="font-medium text-gray-900">
|
|||
|
|
{participant.firstName && participant.lastName
|
|||
|
|
? `${participant.firstName} ${participant.lastName}`
|
|||
|
|
: participant.username}
|
|||
|
|
</p>
|
|||
|
|
<p className="text-sm text-gray-600">@{participant.username}</p>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{/* Joined Date */}
|
|||
|
|
<div className="text-xs text-gray-500">
|
|||
|
|
{new Date(participant.joinedAt).toLocaleDateString()}
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
))}
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{/* Action Buttons */}
|
|||
|
|
<div className="mt-6 flex gap-4">
|
|||
|
|
<Link
|
|||
|
|
to={`/events/${slug}/chat`}
|
|||
|
|
className="flex-1 bg-primary-600 text-white px-6 py-3 rounded-lg hover:bg-primary-700 transition-colors text-center font-medium"
|
|||
|
|
>
|
|||
|
|
Go to Event Chat
|
|||
|
|
</Link>
|
|||
|
|
<button
|
|||
|
|
onClick={() => window.print()}
|
|||
|
|
className="px-6 py-3 border border-gray-300 rounded-lg hover:bg-gray-50 transition-colors font-medium"
|
|||
|
|
>
|
|||
|
|
Print QR Code
|
|||
|
|
</button>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{/* Print Styles */}
|
|||
|
|
<style>{`
|
|||
|
|
@media print {
|
|||
|
|
body * {
|
|||
|
|
visibility: hidden;
|
|||
|
|
}
|
|||
|
|
.print-area, .print-area * {
|
|||
|
|
visibility: visible;
|
|||
|
|
}
|
|||
|
|
.print-area {
|
|||
|
|
position: absolute;
|
|||
|
|
left: 0;
|
|||
|
|
top: 0;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
`}</style>
|
|||
|
|
</div>
|
|||
|
|
</Layout>
|
|||
|
|
);
|
|||
|
|
}
|