All files / src/views/tickets TicketsPage.tsx

100% Statements 84/84
91.66% Branches 11/12
100% Functions 3/3
100% Lines 84/84

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 1071x 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')}
          >
            &times;
          </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>
  );
}