Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 | 1x 1x 1x 1x 1x 4x 4x 4x 4x 4x 4x 4x 4x 4x 4x 4x 4x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 5x 1x 1x 1x 1x 5x 1x 1x 1x 1x 5x 2x 2x 2x 2x 2x 5x 5x | import { Link } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { useTicketsViewModel } from '@/viewmodels/tickets/useTicketsViewModel';
import { getTicketStatusPresentation } from '@/utils/ticketStatus';
import type { TicketListItem } from '@/models/ticket';
import '@/styles/tickets.css';
function formatDateTime(iso: string): string {
const d = new Date(iso);
if (Number.isNaN(d.getTime())) return iso;
return d.toLocaleString(undefined, {
weekday: 'short',
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
hour12: false,
});
}
function TicketRow({ ticket }: { ticket: TicketListItem }) {
const { t } = useTranslation();
const presentation = getTicketStatusPresentation(ticket.status);
return (
<Link
to={`/tickets/${ticket.ticket_id}`}
className="tk-card"
data-testid={`ticket-${ticket.ticket_id}`}
>
<div className="tk-card-main">
<div className="tk-card-header">
<span className={`tk-status-badge tk-status-${presentation.tone}`}>
{presentation.label}
</span>
<span className="tk-card-event-time">
{formatDateTime(ticket.event.start_time)}
</span>
</div>
<h3 className="tk-card-title">{ticket.event.title}</h3>
{ticket.event.address && (
<p className="tk-card-address">{ticket.event.address}</p>
)}
<div className="tk-card-footer">
<span className="tk-card-meta">
{t('tickets.expires', { date: formatDateTime(ticket.expires_at) })}
</span>
<span className="tk-card-link">{t('tickets.view_ticket')}</span>
</div>
</div>
</Link>
);
}
export default function TicketsPage() {
const { t } = useTranslation();
const vm = useTicketsViewModel();
return (
<div className="tk-page">
<header className="tk-page-header">
<h1 className="tk-page-title">{t('tickets.page_title')}</h1>
<p className="tk-page-subtitle">{t('tickets.page_subtitle')}</p>
</header>
{vm.error && (
<div className="tk-error" role="alert">
<span>{vm.error}</span>
<button
type="button"
className="tk-error-dismiss"
onClick={vm.dismissError}
aria-label={t('tickets.dismiss_error')}
>
×
</button>
</div>
)}
{vm.isLoading && vm.tickets.length === 0 && (
<div className="tk-loading">
<span className="spinner" />
<p>{t('tickets.loading')}</p>
</div>
)}
{!vm.isLoading && vm.tickets.length === 0 && !vm.error && (
<div className="tk-empty">
<h2>{t('tickets.empty_title')}</h2>
<p>{t('tickets.empty_body')}</p>
</div>
)}
{vm.tickets.length > 0 && (
<div className="tk-list">
{vm.tickets.map((t) => (
<TicketRow key={t.ticket_id} ticket={t} />
))}
</div>
)}
</div>
);
}
|