Files
leocrm/frontend/src/components/mail/MailList.tsx
T

135 lines
5.3 KiB
TypeScript
Raw Normal View History

/**
* Mail list component with pagination.
* Shows list of mails in selected folder with seen/flagged indicators.
*/
import React from 'react';
import clsx from 'clsx';
import { useTranslation } from 'react-i18next';
import type { Mail } from '@/api/mail';
import { EmptyState } from '@/components/ui/EmptyState';
import { Pagination } from '@/components/ui/Pagination';
export interface MailListProps {
mails: Mail[];
selectedMailId: string | null;
onSelectMail: (mail: Mail) => void;
loading: boolean;
currentPage: number;
total: number;
pageSize: number;
onPageChange: (page: number) => void;
}
function formatDate(dateStr: string): string {
const date = new Date(dateStr);
const now = new Date();
if (date.toDateString() === now.toDateString()) {
return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
}
return date.toLocaleDateString([], { month: 'short', day: 'numeric' });
}
export function MailList({
mails,
selectedMailId,
onSelectMail,
loading,
currentPage,
total,
pageSize,
onPageChange,
}: MailListProps) {
const { t } = useTranslation();
const totalPages = Math.ceil(total / pageSize);
if (loading) {
return (
<div className="flex items-center justify-center py-12" data-testid="mail-list-loading">
<svg className="animate-spin h-5 w-5 text-secondary-400" fill="none" viewBox="0 0 24 24" aria-hidden="true">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
</svg>
<span className="ml-2 text-sm text-secondary-500">{t('common.loading')}</span>
</div>
);
}
if (mails.length === 0) {
return (
<EmptyState
title={t('mail.noMails')}
description={t('mail.noMailsDesc')}
data-testid="mail-list-empty"
/>
);
}
return (
<div data-testid="mail-list" role="listbox" aria-label={t('mail.selectMail')}>
<ul className="divide-y divide-secondary-100" role="list">
{mails.map((mail) => (
<li key={mail.id} role="listitem">
<button
onClick={() => onSelectMail(mail)}
className={clsx(
2026-07-15 17:53:56 +02:00
'w-full text-left px-3 py-2 md:px-4 md:py-3 hover:bg-secondary-50 min-h-touch motion-safe:transition-colors',
'focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-500',
mail.id === selectedMailId && 'bg-primary-50',
!mail.is_seen && 'font-semibold',
)}
aria-selected={mail.id === selectedMailId}
data-testid={`mail-item-${mail.id}`}
>
<div className="flex items-center gap-2">
{!mail.is_seen && (
<span className="w-2 h-2 rounded-full bg-primary-500 flex-shrink-0" aria-label={t('mail.unread')} />
)}
{mail.is_flagged && (
<svg className="w-4 h-4 text-warning-500 flex-shrink-0" fill="currentColor" viewBox="0 0 24 24" aria-label={t('mail.flagged')}>
<path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z" />
</svg>
)}
{mail.has_attachments && (
<svg className="w-4 h-4 text-secondary-400 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15.172 13l-3.586 3.586a2 2 0 01-2.828 0L5 12.828a2 2 0 010-2.828l5.657-5.657a2 2 0 012.828 0L17 6.343M14.828 8.172a2 2 0 00-2.828 0l-3.586 3.586a2 2 0 000 2.828l1.414 1.414a2 2 0 002.828 0" />
</svg>
)}
<div className="flex-1 min-w-0">
<div className="flex items-center justify-between gap-2">
<span className={clsx('text-sm truncate', !mail.is_seen ? 'text-secondary-900 font-semibold' : 'text-secondary-700')}>
{mail.from_name || mail.from_address}
</span>
<span className="text-xs text-secondary-400 flex-shrink-0">{formatDate(mail.date)}</span>
</div>
2026-07-15 17:53:56 +02:00
<p className={clsx('text-xs md:text-sm truncate mt-0.5', !mail.is_seen ? 'text-secondary-800' : 'text-secondary-600')}>
{mail.subject || t('mail.noSubject')}
</p>
{mail.labels && mail.labels.length > 0 && (
<div className="flex gap-1 mt-1">
{mail.labels.map((label) => (
<span key={label.id} className="inline-flex items-center px-2 py-0.5 rounded-full text-xs text-white" style={{ backgroundColor: label.color }}>
{label.name}
</span>
))}
</div>
)}
</div>
</div>
</button>
</li>
))}
</ul>
{totalPages > 1 && (
<Pagination
currentPage={currentPage}
totalPages={totalPages}
total={total}
pageSize={pageSize}
onPageChange={onPageChange}
/>
)}
</div>
);
}