144 lines
6.9 KiB
TypeScript
144 lines
6.9 KiB
TypeScript
import React, { useEffect, useState } from 'react';
|
|
import { Link } from 'react-router-dom';
|
|
import { Plus, Users, QrCode, Trash2 } from 'lucide-react';
|
|
import { getTickets, deleteTicket } from '../utils/storage';
|
|
import type { Ticket } from '../types';
|
|
|
|
export const Home: React.FC = () => {
|
|
const [tickets, setTickets] = useState<Ticket[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
|
|
useEffect(() => {
|
|
const fetchTickets = async () => {
|
|
try {
|
|
setLoading(true);
|
|
const fetchedTickets = await getTickets('default-event'); // For now, single event
|
|
setTickets(fetchedTickets);
|
|
} catch (err) {
|
|
setError('Failed to load tickets.');
|
|
console.error(err);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
fetchTickets();
|
|
}, []);
|
|
|
|
const handleDelete = async (id: string) => {
|
|
if (window.confirm('Are you sure you want to delete this ticket?')) {
|
|
try {
|
|
await deleteTicket(id);
|
|
setTickets(tickets.filter(t => t.id !== id));
|
|
} catch (err) {
|
|
alert('Failed to delete ticket.');
|
|
console.error(err);
|
|
}
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className="space-y-6 md:space-y-8">
|
|
<header className="flex flex-col sm:flex-row justify-between items-start sm:items-end gap-4">
|
|
<div>
|
|
<h1 className="text-3xl md:text-4xl font-bold mb-2">Dashboard</h1>
|
|
<p className="text-slate-400 text-sm md:text-base">Manage your events and attendees</p>
|
|
</div>
|
|
<Link to="/generate" className="btn-primary whitespace-nowrap">
|
|
<Plus size={20} />
|
|
<span className="hidden sm:inline">New Ticket</span>
|
|
<span className="sm:hidden">New</span>
|
|
</Link>
|
|
</header>
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
|
<div className="glass-panel p-6">
|
|
<div className="flex items-center gap-4 mb-4">
|
|
<div className="p-3 bg-indigo-500/20 rounded-lg text-indigo-400">
|
|
<Users size={24} />
|
|
</div>
|
|
<div>
|
|
<p className="text-sm text-slate-400">Total Attendees</p>
|
|
<p className="text-2xl font-bold">{tickets.length}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="glass-panel p-6">
|
|
<div className="flex items-center gap-4 mb-4">
|
|
<div className="p-3 bg-purple-500/20 rounded-lg text-purple-400">
|
|
<QrCode size={24} />
|
|
</div>
|
|
<div>
|
|
<p className="text-sm text-slate-400">Scanned Tickets</p>
|
|
<p className="text-2xl font-bold">{tickets.filter(t => t.status === 'used').length}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="glass-panel p-4 md:p-6">
|
|
<h2 className="text-lg md:text-xl font-bold mb-4">Recent Tickets</h2>
|
|
{loading ? (
|
|
<div className="text-center py-8 text-slate-500">
|
|
Loading tickets...
|
|
</div>
|
|
) : error ? (
|
|
<div className="text-center py-8 text-red-500">
|
|
{error}
|
|
</div>
|
|
) : tickets.length === 0 ? (
|
|
<div className="text-center py-8 text-slate-500">
|
|
No tickets generated yet.
|
|
</div>
|
|
) : (
|
|
<div className="overflow-x-auto -mx-4 md:mx-0">
|
|
<table className="w-full text-left border-collapse min-w-[600px]">
|
|
<thead>
|
|
<tr className="text-slate-400 border-b border-slate-700 text-sm">
|
|
<th className="p-3 md:p-4">Name</th>
|
|
<th className="p-3 md:p-4 hidden sm:table-cell">Email</th>
|
|
<th className="p-3 md:p-4">Type</th>
|
|
<th className="p-3 md:p-4">Status</th>
|
|
<th className="p-3 md:p-4 text-right">Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
|
|
{tickets.slice().reverse().map(ticket => (
|
|
<tr key={ticket.id} className="border-b border-slate-800 hover:bg-slate-800/50 text-sm">
|
|
<td className="p-3 md:p-4 font-medium">{ticket.attendeeName}</td>
|
|
<td className="p-3 md:p-4 text-slate-400 hidden sm:table-cell">{ticket.attendeeEmail}</td>
|
|
<td className="p-3 md:p-4">
|
|
<span className={`px-2 py-1 rounded-full text-xs whitespace-nowrap ${ticket.ticketType === 'Partyborner' ? 'bg-amber-500/20 text-amber-400' : 'bg-blue-500/20 text-blue-400'
|
|
}`}>
|
|
{ticket.ticketType}
|
|
</span>
|
|
</td>
|
|
<td className="p-3 md:p-4">
|
|
<span className={`px-2 py-1 rounded-full text-xs whitespace-nowrap ${ticket.status === 'valid' ? 'bg-green-500/20 text-green-400' : 'bg-slate-500/20 text-slate-400'
|
|
}`}>
|
|
{ticket.status}
|
|
</span>
|
|
</td>
|
|
<td className="p-3 md:p-4 text-right">
|
|
<button
|
|
onClick={() => handleDelete(ticket.id)}
|
|
className="text-slate-500 hover:text-red-400 transition-colors p-2"
|
|
title="Delete Ticket"
|
|
>
|
|
<Trash2 size={18} />
|
|
</button>
|
|
</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|