UI overhault
This commit is contained in:
@@ -1,12 +1,19 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useState, useEffect, useMemo } from 'react';
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
import Link from 'next/link';
|
||||
import { AppHeader } from '@/components/layout/AppHeader';
|
||||
import { Sidebar } from '@/components/layout/Sidebar';
|
||||
import { Breadcrumb } from '@/components/layout/Breadcrumb';
|
||||
import { TableSkeleton } from '@/components/ui/Skeleton';
|
||||
|
||||
interface Email {
|
||||
key: string;
|
||||
subject: string;
|
||||
from: string;
|
||||
to: string[];
|
||||
preview?: string;
|
||||
date: string;
|
||||
processed: string;
|
||||
processedAt: string | null;
|
||||
@@ -19,9 +26,14 @@ export default function Emails() {
|
||||
const searchParams = useSearchParams();
|
||||
const bucket = searchParams.get('bucket');
|
||||
const mailbox = searchParams.get('mailbox');
|
||||
|
||||
const [emails, setEmails] = useState<Email[]>([]);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [statusFilter, setStatusFilter] = useState<string>('all');
|
||||
const [dateFilter, setDateFilter] = useState<string>('all');
|
||||
const [sortBy, setSortBy] = useState<'newest' | 'oldest' | 'subject'>('newest');
|
||||
|
||||
useEffect(() => {
|
||||
if (!bucket || !mailbox) {
|
||||
@@ -36,121 +48,394 @@ export default function Emails() {
|
||||
return;
|
||||
}
|
||||
|
||||
fetch(`/api/emails?bucket=${bucket}&mailbox=${encodeURIComponent(mailbox)}`, {
|
||||
headers: { Authorization: `Basic ${auth}` }
|
||||
fetch(`/api/emails?bucket=${bucket}&mailbox=${encodeURIComponent(mailbox)}`, {
|
||||
headers: { Authorization: `Basic ${auth}` }
|
||||
})
|
||||
.then(res => {
|
||||
if (!res.ok) throw new Error('Failed to fetch emails');
|
||||
return res.json();
|
||||
})
|
||||
.then(data => {
|
||||
const sorted = data.sort((a: Email, b: Email) => new Date(b.date).getTime() - new Date(a.date).getTime());
|
||||
setEmails(sorted);
|
||||
setEmails(data);
|
||||
})
|
||||
.catch(err => setError(err.message))
|
||||
.finally(() => setLoading(false));
|
||||
}, [bucket, mailbox]);
|
||||
|
||||
if (loading) return <div className="min-h-screen flex items-center justify-center bg-gray-50">Loading...</div>;
|
||||
if (error) return <div className="min-h-screen flex items-center justify-center bg-gray-50 text-red-500">{error}</div>;
|
||||
// Filter and search emails
|
||||
const filteredEmails = useMemo(() => {
|
||||
let result = [...emails];
|
||||
|
||||
const formatDate = (dateStr: string | null) => {
|
||||
if (!dateStr) return '';
|
||||
// Apply status filter
|
||||
if (statusFilter !== 'all') {
|
||||
result = result.filter(e => e.status === statusFilter);
|
||||
}
|
||||
|
||||
// Apply date filter
|
||||
if (dateFilter !== 'all') {
|
||||
const now = new Date();
|
||||
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
||||
|
||||
result = result.filter(e => {
|
||||
const emailDate = new Date(e.date);
|
||||
const emailDay = new Date(emailDate.getFullYear(), emailDate.getMonth(), emailDate.getDate());
|
||||
const daysDiff = Math.floor((today.getTime() - emailDay.getTime()) / (1000 * 60 * 60 * 24));
|
||||
|
||||
switch (dateFilter) {
|
||||
case 'today':
|
||||
return daysDiff === 0;
|
||||
case 'yesterday':
|
||||
return daysDiff === 1;
|
||||
case 'last2days':
|
||||
return daysDiff <= 1;
|
||||
case 'last3days':
|
||||
return daysDiff <= 2;
|
||||
case '2daysago':
|
||||
return daysDiff === 2;
|
||||
case '3daysago':
|
||||
return daysDiff === 3;
|
||||
case '4daysago':
|
||||
return daysDiff === 4;
|
||||
case '5daysago':
|
||||
return daysDiff === 5;
|
||||
case 'lastweek':
|
||||
return daysDiff <= 7;
|
||||
case 'lastmonth':
|
||||
return daysDiff <= 30;
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Apply search
|
||||
if (searchQuery.trim()) {
|
||||
const query = searchQuery.toLowerCase();
|
||||
result = result.filter(e =>
|
||||
e.subject.toLowerCase().includes(query) ||
|
||||
(e.from && e.from.toLowerCase().includes(query)) ||
|
||||
(e.to && Array.isArray(e.to) && e.to.some(addr => addr.toLowerCase().includes(query)))
|
||||
);
|
||||
}
|
||||
|
||||
// Apply sorting
|
||||
if (sortBy === 'newest') {
|
||||
result.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime());
|
||||
} else if (sortBy === 'oldest') {
|
||||
result.sort((a, b) => new Date(a.date).getTime() - new Date(b.date).getTime());
|
||||
} else if (sortBy === 'subject') {
|
||||
result.sort((a, b) => a.subject.localeCompare(b.subject));
|
||||
}
|
||||
|
||||
return result;
|
||||
}, [emails, statusFilter, dateFilter, searchQuery, sortBy]);
|
||||
|
||||
// Calculate stats
|
||||
const stats = useMemo(() => {
|
||||
const total = emails.length;
|
||||
const delivered = emails.filter(e => e.status === 'delivered').length;
|
||||
const failed = emails.filter(e => e.status === 'failed').length;
|
||||
const pending = emails.filter(e => !e.status || e.status === 'pending').length;
|
||||
return { total, delivered, failed, pending };
|
||||
}, [emails]);
|
||||
|
||||
const formatDate = (dateStr: string) => {
|
||||
const date = new Date(dateStr);
|
||||
return date.toLocaleString('en-US', { month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', hour12: false });
|
||||
const now = new Date();
|
||||
const diff = now.getTime() - date.getTime();
|
||||
const days = Math.floor(diff / (1000 * 60 * 60 * 24));
|
||||
|
||||
if (days === 0) {
|
||||
return date.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' });
|
||||
} else if (days < 7) {
|
||||
return date.toLocaleDateString('de-DE', { weekday: 'short' });
|
||||
} else {
|
||||
return date.toLocaleDateString('de-DE', { month: 'short', day: 'numeric' });
|
||||
}
|
||||
};
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<>
|
||||
<AppHeader />
|
||||
<Sidebar />
|
||||
<div className="ml-64 mt-16 min-h-screen bg-gray-50 p-8">
|
||||
<main id="main-content">
|
||||
<div className="max-w-4xl mx-auto bg-white rounded-lg shadow-md p-8 fade-in" role="alert">
|
||||
<p className="text-red-600 font-medium text-center">{error}</p>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 p-6">
|
||||
<nav className="w-full mb-8 bg-white p-4 rounded shadow-sm border border-gray-100">
|
||||
<ol className="flex flex-wrap space-x-2 text-sm text-gray-500">
|
||||
<li><Link href="/" className="hover:text-blue-600">Home</Link></li>
|
||||
<li className="mx-1">/</li>
|
||||
<li><Link href={`/mailboxes?bucket=${bucket}`} className="hover:text-blue-600">Mailboxes</Link></li>
|
||||
<li className="mx-1">/</li>
|
||||
<li className="font-semibold text-gray-700">Emails</li>
|
||||
</ol>
|
||||
</nav>
|
||||
|
||||
<h1 className="text-2xl font-bold mb-8 text-gray-800 break-all">
|
||||
{mailbox} <span className="text-gray-400 font-normal text-lg">in {bucket}</span>
|
||||
</h1>
|
||||
|
||||
{/* Container mit Padding für den "Rahmen um die Tabelle" */}
|
||||
<div className="w-full bg-white rounded shadow-sm border border-gray-200 p-2 overflow-hidden">
|
||||
|
||||
{/* table-fixed: Zwingt die Tabelle, die definierten Breiten einzuhalten (kein Überlauf).
|
||||
border-separate + border-spacing-x-2: Erzeugt die "Gaps" zwischen den Spalten.
|
||||
*/}
|
||||
<table className="w-full table-fixed border-separate border-spacing-x-2 border-spacing-y-1 text-left">
|
||||
<thead className="text-xs font-semibold text-gray-600 uppercase tracking-wider">
|
||||
<tr>
|
||||
{/* SUBJECT: Keine Breite definiert (w-auto) -> nimmt den RESTLICHEN Platz */}
|
||||
<th className="pb-3 border-b border-gray-200 w-[40%]">Subject</th>
|
||||
<>
|
||||
<AppHeader />
|
||||
<Sidebar />
|
||||
<div className="ml-64 mt-16 min-h-screen bg-gray-50">
|
||||
<Breadcrumb items={[
|
||||
{ label: 'Domains', href: '/domains' },
|
||||
{ label: 'Mailboxes', href: `/mailboxes?bucket=${bucket}` },
|
||||
{ label: 'Emails' }
|
||||
]} />
|
||||
<main id="main-content">
|
||||
{/* Page Header with Stats */}
|
||||
<div className="bg-white border-b border-gray-200 px-6 py-4">
|
||||
<h1 className="text-2xl font-bold text-gray-800 mb-4 truncate">
|
||||
{mailbox}
|
||||
</h1>
|
||||
|
||||
{/* FESTE BREITEN für den Rest, damit sie nicht zerdrückt werden */}
|
||||
<th className="pb-3 border-b border-gray-200 w-[5%]">Date</th>
|
||||
<th className="pb-3 border-b border-gray-200 w-[22%]">Key</th> {/* Breit genug für lange Keys */}
|
||||
{/* Stats */}
|
||||
<div className="grid grid-cols-4 gap-4">
|
||||
<div className="text-center p-3 bg-gray-50 rounded-lg">
|
||||
<div className="text-2xl font-bold text-gray-900">{stats.total}</div>
|
||||
<div className="text-sm text-gray-600">Total</div>
|
||||
</div>
|
||||
<div className="text-center p-3 bg-green-50 rounded-lg">
|
||||
<div className="text-2xl font-bold text-green-600">{stats.delivered}</div>
|
||||
<div className="text-sm text-gray-600">Delivered</div>
|
||||
</div>
|
||||
<div className="text-center p-3 bg-red-50 rounded-lg">
|
||||
<div className="text-2xl font-bold text-red-600">{stats.failed}</div>
|
||||
<div className="text-sm text-gray-600">Failed</div>
|
||||
</div>
|
||||
<div className="text-center p-3 bg-yellow-50 rounded-lg">
|
||||
<div className="text-2xl font-bold text-yellow-600">{stats.pending}</div>
|
||||
<div className="text-sm text-gray-600">Pending</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<th className="pb-3 border-b border-gray-200 w-[12%]">Proc. By</th>
|
||||
<th className="pb-3 border-b border-gray-200 w-[11%]">Queued</th>
|
||||
<th className="pb-3 border-b border-gray-200 w-[5%]">Status</th>
|
||||
<th className="pb-3 border-b border-gray-200 text-right w-[5%]">Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="text-sm">
|
||||
{emails.length === 0 ? (
|
||||
<tr>
|
||||
<td colSpan={8} className="py-8 text-center text-gray-500">No emails found</td>
|
||||
</tr>
|
||||
) : emails.map((e: Email) => (
|
||||
<tr key={e.key} className="hover:bg-blue-50 transition-colors group">
|
||||
|
||||
{/* 1. Subject: truncate sorgt für "..." am Ende, da die Tabellenbreite fix ist */}
|
||||
<td className="py-2 text-gray-900 truncate " title={e.subject}>
|
||||
<div className="truncate">{e.subject}</div>
|
||||
</td>
|
||||
{/* Filter & Toolbar */}
|
||||
<div className="bg-white border-b border-gray-200 px-6 py-3 flex items-center justify-between">
|
||||
{/* Search */}
|
||||
<div className="flex-1 max-w-md">
|
||||
<div className="relative">
|
||||
<input
|
||||
type="search"
|
||||
placeholder="E-Mails durchsuchen..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="w-full pl-10 pr-4 py-2 bg-gray-100 rounded-lg focus:bg-white focus:ring-2 focus:ring-blue-500 focus:outline-none transition-all text-sm"
|
||||
/>
|
||||
<svg className="absolute left-3 top-2.5 w-5 h-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<td className="py-2 text-gray-500 text-xs whitespace-nowrap">
|
||||
{formatDate(e.date)}
|
||||
</td>
|
||||
|
||||
{/* Key: Monospace für exakte Breite, text-xs damit er in die 28rem passt */}
|
||||
<td className="py-2 font-mono text-xs text-gray-600 select-all truncate " title={e.key}>
|
||||
{e.key}
|
||||
</td>
|
||||
|
||||
|
||||
<td className="py-2 text-gray-500 text-xs truncate" title={e.processedBy || ''}>
|
||||
{e.processedBy ? e.processedBy.split('@')[0] : '-'}
|
||||
</td>
|
||||
|
||||
<td className="py-2 text-gray-500 text-xs truncate " title={e.queuedTo || ''}>
|
||||
{e.queuedTo || '-'}
|
||||
</td>
|
||||
|
||||
<td className="py-2 whitespace-nowrap ">
|
||||
{e.status ? (
|
||||
<span className={`text-xs font-medium ${
|
||||
e.status === 'delivered' ? 'text-green-600' :
|
||||
e.status === 'failed' ? 'text-red-600' :
|
||||
'text-gray-500'
|
||||
}`}>
|
||||
{e.status}
|
||||
</span>
|
||||
) : <span className="text-gray-300">-</span>}
|
||||
</td>
|
||||
|
||||
<td className="py-2 text-right font-medium w-fit">
|
||||
<Link href={`/email?bucket=${bucket}&key=${e.key}&mailbox=${encodeURIComponent(mailbox || '')}`} className="text-blue-600 hover:text-blue-800 underline decoration-blue-200 hover:decoration-blue-800">
|
||||
View
|
||||
</Link>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
{/* Date Filter */}
|
||||
<div className="flex gap-2 ml-4">
|
||||
<select
|
||||
value={dateFilter}
|
||||
onChange={(e) => setDateFilter(e.target.value)}
|
||||
className="text-sm border border-gray-300 rounded-full px-4 py-1.5 bg-white"
|
||||
>
|
||||
<option value="all">Alle Zeiträume</option>
|
||||
<option value="today">Heute</option>
|
||||
<option value="yesterday">Gestern</option>
|
||||
<option value="last2days">Letzte 2 Tage</option>
|
||||
<option value="last3days">Letzte 3 Tage</option>
|
||||
<option value="2daysago">Vor 2 Tagen</option>
|
||||
<option value="3daysago">Vor 3 Tagen</option>
|
||||
<option value="4daysago">Vor 4 Tagen</option>
|
||||
<option value="5daysago">Vor 5 Tagen</option>
|
||||
<option value="lastweek">Letzte Woche</option>
|
||||
<option value="lastmonth">Letzter Monat</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* Status Filter Chips */}
|
||||
<div className="flex gap-2 ml-4">
|
||||
<button
|
||||
onClick={() => setStatusFilter('all')}
|
||||
className={`px-3 py-1.5 rounded-full text-sm font-medium transition-colors ${
|
||||
statusFilter === 'all'
|
||||
? 'bg-blue-100 text-blue-800'
|
||||
: 'text-gray-600 hover:bg-gray-100'
|
||||
}`}
|
||||
>
|
||||
Alle
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setStatusFilter('delivered')}
|
||||
className={`px-3 py-1.5 rounded-full text-sm font-medium transition-colors flex items-center gap-1 ${
|
||||
statusFilter === 'delivered'
|
||||
? 'bg-green-100 text-green-800'
|
||||
: 'text-gray-600 hover:bg-gray-100'
|
||||
}`}
|
||||
>
|
||||
<span className="w-2 h-2 bg-green-500 rounded-full"></span>
|
||||
Delivered
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setStatusFilter('failed')}
|
||||
className={`px-3 py-1.5 rounded-full text-sm font-medium transition-colors flex items-center gap-1 ${
|
||||
statusFilter === 'failed'
|
||||
? 'bg-red-100 text-red-800'
|
||||
: 'text-gray-600 hover:bg-gray-100'
|
||||
}`}
|
||||
>
|
||||
<span className="w-2 h-2 bg-red-500 rounded-full"></span>
|
||||
Failed
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setStatusFilter('pending')}
|
||||
className={`px-3 py-1.5 rounded-full text-sm font-medium transition-colors flex items-center gap-1 ${
|
||||
statusFilter === 'pending'
|
||||
? 'bg-yellow-100 text-yellow-800'
|
||||
: 'text-gray-600 hover:bg-gray-100'
|
||||
}`}
|
||||
>
|
||||
<span className="w-2 h-2 bg-yellow-500 rounded-full"></span>
|
||||
Pending
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Sort & View Options */}
|
||||
<div className="flex items-center gap-3 ml-4">
|
||||
<select
|
||||
value={sortBy}
|
||||
onChange={(e) => setSortBy(e.target.value as any)}
|
||||
className="text-sm border border-gray-300 rounded px-3 py-1.5 bg-white"
|
||||
>
|
||||
<option value="newest">Neueste zuerst</option>
|
||||
<option value="oldest">Älteste zuerst</option>
|
||||
<option value="subject">Nach Betreff</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Email List (Gmail-Style) */}
|
||||
<div className="bg-white">
|
||||
{loading ? (
|
||||
<div className="p-6">
|
||||
<TableSkeleton rows={10} />
|
||||
</div>
|
||||
) : filteredEmails.length === 0 ? (
|
||||
<div className="p-12 text-center text-gray-500">
|
||||
<div className="flex flex-col items-center gap-3">
|
||||
<svg className="w-16 h-16 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
|
||||
</svg>
|
||||
<p className="text-lg">Keine E-Mails gefunden</p>
|
||||
{searchQuery && (
|
||||
<button
|
||||
onClick={() => setSearchQuery('')}
|
||||
className="text-blue-600 hover:text-blue-800 text-sm"
|
||||
>
|
||||
Suche zurücksetzen
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{filteredEmails.map((email) => (
|
||||
<div
|
||||
key={email.key}
|
||||
className="group flex items-start gap-3 px-6 py-3 border-b border-gray-100 hover:shadow-md hover:bg-gray-50 transition-all"
|
||||
>
|
||||
|
||||
{/* From - breiter und mit Tooltip */}
|
||||
<div className="w-48 flex-shrink-0 pt-1" title={email.from || 'Unknown'}>
|
||||
<span className="text-sm text-gray-900 group-hover:text-blue-600 transition-colors block break-words">
|
||||
{email.from || 'Unknown'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Subject & Preview - klickbar */}
|
||||
<Link
|
||||
href={`/email?bucket=${bucket}&key=${email.key}&mailbox=${encodeURIComponent(mailbox || '')}`}
|
||||
className="flex-1 min-w-0 cursor-pointer"
|
||||
>
|
||||
<div className="font-medium text-gray-900 truncate mb-1 hover:text-blue-600">
|
||||
{email.subject || '(No Subject)'}
|
||||
</div>
|
||||
{email.preview && (
|
||||
<div className="text-sm text-gray-600 truncate">
|
||||
{email.preview}
|
||||
</div>
|
||||
)}
|
||||
</Link>
|
||||
|
||||
{/* S3 Key Column - kopierbar */}
|
||||
<div className="w-64 flex-shrink-0 pt-1">
|
||||
<div className="flex items-center gap-2 bg-gray-50 px-3 py-1.5 rounded border border-gray-200">
|
||||
<code className="text-xs font-semibold text-gray-700 flex-1 truncate" title={email.key}>
|
||||
{email.key}
|
||||
</code>
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
navigator.clipboard.writeText(email.key);
|
||||
// Optional: Feedback anzeigen
|
||||
const btn = e.currentTarget;
|
||||
const originalHTML = btn.innerHTML;
|
||||
btn.innerHTML = '<svg class="w-4 h-4 text-green-600" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd" /></svg>';
|
||||
setTimeout(() => { btn.innerHTML = originalHTML; }, 1000);
|
||||
}}
|
||||
className="p-1 hover:bg-gray-200 rounded transition-colors flex-shrink-0"
|
||||
title="S3 Key kopieren"
|
||||
>
|
||||
<svg className="w-4 h-4 text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Status Badge */}
|
||||
<div className="w-24 flex-shrink-0 pt-1">
|
||||
{email.status && (
|
||||
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${
|
||||
email.status === 'delivered' ? 'bg-green-100 text-green-800' :
|
||||
email.status === 'failed' ? 'bg-red-100 text-red-800' :
|
||||
'bg-gray-100 text-gray-700'
|
||||
}`}>
|
||||
{email.status === 'delivered' && (
|
||||
<svg className="w-3 h-3 mr-1" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
|
||||
</svg>
|
||||
)}
|
||||
{email.status === 'failed' && (
|
||||
<svg className="w-3 h-3 mr-1" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clipRule="evenodd" />
|
||||
</svg>
|
||||
)}
|
||||
{email.status}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Date & Time */}
|
||||
<div className="w-28 text-right text-xs text-gray-600 flex-shrink-0 pt-1">
|
||||
{new Date(email.date).toLocaleDateString('de-DE', {
|
||||
day: '2-digit',
|
||||
month: '2-digit',
|
||||
year: 'numeric'
|
||||
})}
|
||||
<br />
|
||||
{new Date(email.date).toLocaleTimeString('de-DE', {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{!loading && (
|
||||
<div role="status" aria-live="polite" className="sr-only">
|
||||
{filteredEmails.length} emails loaded
|
||||
</div>
|
||||
)}
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user