Files
lead-scraper/components/shared/ResultsTable.tsx
Timo Uttenweiler 115cdacd08 UI improvements: Leadspeicher, Maps enrichment, exports
- Rename LeadVault → Leadspeicher throughout (sidebar, topbar, page)
- SidePanel: full lead detail view with contact, source, tags (read-only), Google Maps link for address
- Tags: kontaktiert stored as tag (toggleable), favorit tag toggle
- Remove Status column, StatusBadge dropdown, Priority feature
- Remove Aktualisieren button from Leadspeicher
- Bulk actions: remove status dropdown
- Export: LeadVault Excel-only, clean columns, freeze row + autofilter
- Export dropdown: click-based (fix overflow-hidden clipping)
- ExportButtons: remove CSV, Excel only everywhere
- Maps page: post-search Anymailfinder enrichment button
- ProgressCard: "Suche läuft..." instead of "Warte auf Anymailfinder-Server..."
- Quick SERP renamed to "Schnell neue Suche"
- Results page: Excel export, always-enabled download button
- Anymailfinder: fix bulk field names, array-of-arrays format
- Apify: fix countryCode lowercase
- API: sourceTerm search, contacted/favorite tag filters

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-21 18:12:31 +01:00

136 lines
5.0 KiB
TypeScript

"use client";
import { useState } from "react";
import { Skeleton } from "@/components/ui/skeleton";
import { cn } from "@/lib/utils";
import { CheckCircle2, XCircle, ChevronUp, ChevronDown } from "lucide-react";
export interface ResultRow {
id: string;
companyName?: string;
domain?: string;
contactName?: string;
contactTitle?: string;
email?: string;
linkedinUrl?: string;
status?: string;
selected?: boolean;
}
interface ResultsTableProps {
rows: ResultRow[];
loading?: boolean;
selectable?: boolean;
onSelectionChange?: (ids: string[]) => void;
extraColumns?: Array<{ key: string; label: string }>;
}
function EmailStatusIcon({ email }: { email?: string }) {
if (!email) return <XCircle className="w-4 h-4 text-red-400" />;
return <CheckCircle2 className="w-4 h-4 text-green-400" />;
}
export function ResultsTable({ rows, loading, selectable, onSelectionChange, extraColumns }: ResultsTableProps) {
const [selected, setSelected] = useState<Set<string>>(new Set());
const [sort, setSort] = useState<{ key: string; dir: "asc" | "desc" } | null>(null);
const toggleSelect = (id: string) => {
const next = new Set(selected);
next.has(id) ? next.delete(id) : next.add(id);
setSelected(next);
onSelectionChange?.(Array.from(next));
};
const toggleAll = () => {
if (selected.size === rows.length) {
setSelected(new Set());
onSelectionChange?.([]);
} else {
const all = new Set(rows.map(r => r.id));
setSelected(all);
onSelectionChange?.(Array.from(all));
}
};
if (loading) {
return (
<div className="space-y-2">
{Array.from({ length: 5 }).map((_, i) => (
<Skeleton key={i} className="h-10 w-full bg-[#1e1e2e]" />
))}
</div>
);
}
if (rows.length === 0) return null;
return (
<div className="overflow-x-auto rounded-xl border border-[#1e1e2e]">
<table className="w-full text-sm">
<thead>
<tr className="border-b border-[#1e1e2e] bg-[#0d0d18]">
{selectable && (
<th className="w-10 px-3 py-3">
<input
type="checkbox"
checked={selected.size === rows.length && rows.length > 0}
onChange={toggleAll}
className="rounded border-[#2e2e3e] bg-[#1e1e2e]"
/>
</th>
)}
<th className="px-4 py-3 text-left text-xs font-medium text-gray-400 uppercase tracking-wider">Unternehmen</th>
<th className="px-4 py-3 text-left text-xs font-medium text-gray-400 uppercase tracking-wider">Domain</th>
<th className="px-4 py-3 text-left text-xs font-medium text-gray-400 uppercase tracking-wider">Kontakt</th>
<th className="px-4 py-3 text-left text-xs font-medium text-gray-400 uppercase tracking-wider">Position</th>
<th className="px-4 py-3 text-left text-xs font-medium text-gray-400 uppercase tracking-wider">E-Mail</th>
{extraColumns?.map(col => (
<th key={col.key} className="px-4 py-3 text-left text-xs font-medium text-gray-400 uppercase tracking-wider">{col.label}</th>
))}
</tr>
</thead>
<tbody>
{rows.map((row, idx) => (
<tr
key={row.id}
className={cn(
"border-b border-[#1e1e2e]/50 transition-colors",
idx % 2 === 0 ? "bg-[#111118]" : "bg-[#0f0f1a]",
selectable && "cursor-pointer hover:bg-[#1a1a28]",
selectable && selected.has(row.id) && "bg-blue-500/5"
)}
onClick={() => selectable && toggleSelect(row.id)}
>
{selectable && (
<td className="px-3 py-2.5" onClick={e => e.stopPropagation()}>
<input
type="checkbox"
checked={selected.has(row.id)}
onChange={() => toggleSelect(row.id)}
className="rounded border-[#2e2e3e] bg-[#1e1e2e]"
/>
</td>
)}
<td className="px-4 py-2.5 text-white font-medium">{row.companyName || "—"}</td>
<td className="px-4 py-2.5 text-gray-400 font-mono text-xs">{row.domain || "—"}</td>
<td className="px-4 py-2.5 text-gray-300">{row.contactName || "—"}</td>
<td className="px-4 py-2.5 text-gray-400 text-xs">{row.contactTitle || "—"}</td>
<td className="px-4 py-2.5">
<div className="flex items-center gap-1.5">
<EmailStatusIcon email={row.email} />
<span className="text-gray-300 font-mono text-xs">{row.email || "—"}</span>
</div>
</td>
{extraColumns?.map(col => (
<td key={col.key} className="px-4 py-2.5 text-gray-400 text-xs">
{((row as unknown) as Record<string, string>)[col.key] || "—"}
</td>
))}
</tr>
))}
</tbody>
</table>
</div>
);
}