- 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>
92 lines
4.0 KiB
TypeScript
92 lines
4.0 KiB
TypeScript
"use client";
|
||
|
||
import { cn } from "@/lib/utils";
|
||
|
||
interface ProgressCardProps {
|
||
title: string;
|
||
current: number;
|
||
total: number;
|
||
subtitle?: string;
|
||
status?: "running" | "complete" | "failed" | "idle";
|
||
}
|
||
|
||
export function ProgressCard({ title, current, total, subtitle, status = "running" }: ProgressCardProps) {
|
||
const pct = total > 0 ? Math.round((current / total) * 100) : 0;
|
||
// Indeterminate = läuft aber noch kein Fortschritt (z.B. warten auf Anymailfinder Bulk)
|
||
const indeterminate = status === "running" && current === 0 && total > 0;
|
||
|
||
return (
|
||
<div className="bg-[#111118] border border-[#1e1e2e] rounded-xl p-5 space-y-4">
|
||
<div className="flex items-center justify-between">
|
||
<div>
|
||
<h3 className="text-sm font-medium text-white">{title}</h3>
|
||
{subtitle && <p className="text-xs text-gray-500 mt-0.5">{subtitle}</p>}
|
||
</div>
|
||
<StatusBadge status={status} />
|
||
</div>
|
||
|
||
<div className="space-y-2">
|
||
<div className="flex justify-between text-xs text-gray-400">
|
||
{indeterminate ? (
|
||
<>
|
||
<span className="flex items-center gap-1.5">
|
||
<span className="inline-block w-1.5 h-1.5 rounded-full bg-blue-400 animate-bounce" style={{ animationDelay: "0ms" }} />
|
||
<span className="inline-block w-1.5 h-1.5 rounded-full bg-blue-400 animate-bounce" style={{ animationDelay: "150ms" }} />
|
||
<span className="inline-block w-1.5 h-1.5 rounded-full bg-blue-400 animate-bounce" style={{ animationDelay: "300ms" }} />
|
||
<span className="text-blue-300 ml-1">Suche läuft...</span>
|
||
</span>
|
||
<span className="text-gray-600">{total} Domains</span>
|
||
</>
|
||
) : (
|
||
<>
|
||
<span>{current.toLocaleString()} / {total.toLocaleString()}</span>
|
||
<span>{pct}%</span>
|
||
</>
|
||
)}
|
||
</div>
|
||
<div className="h-2 bg-[#1e1e2e] rounded-full overflow-hidden relative">
|
||
{indeterminate ? (
|
||
<div className="absolute inset-0 overflow-hidden rounded-full">
|
||
<div className="h-full w-1/3 bg-gradient-to-r from-transparent via-blue-500 to-transparent animate-[shimmer_1.5s_ease-in-out_infinite] rounded-full" />
|
||
</div>
|
||
) : (
|
||
<div
|
||
className={cn(
|
||
"h-full rounded-full transition-all duration-500",
|
||
status === "failed"
|
||
? "bg-red-500"
|
||
: status === "complete"
|
||
? "bg-green-500"
|
||
: "bg-gradient-to-r from-blue-500 to-purple-600"
|
||
)}
|
||
style={{ width: `${Math.max(pct, status === "running" ? 2 : 0)}%` }}
|
||
/>
|
||
)}
|
||
</div>
|
||
{indeterminate && (
|
||
<p className="text-[11px] text-gray-600">
|
||
Batch-Verarbeitung läuft auf Anymailfinder-Servern. Dauert ca. 1–3 Min. für {total} Domains.
|
||
</p>
|
||
)}
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
export function StatusBadge({ status }: { status: string }) {
|
||
const config: Record<string, { label: string; color: string; dot: string }> = {
|
||
running: { label: "Läuft", color: "bg-blue-500/10 text-blue-400 border-blue-500/20", dot: "bg-blue-400 animate-pulse" },
|
||
complete: { label: "Abgeschlossen", color: "bg-green-500/10 text-green-400 border-green-500/20", dot: "bg-green-400" },
|
||
failed: { label: "Fehlgeschlagen", color: "bg-red-500/10 text-red-400 border-red-500/20", dot: "bg-red-400" },
|
||
pending: { label: "Ausstehend", color: "bg-yellow-500/10 text-yellow-400 border-yellow-500/20", dot: "bg-yellow-400" },
|
||
idle: { label: "Bereit", color: "bg-gray-500/10 text-gray-400 border-gray-500/20", dot: "bg-gray-400" },
|
||
};
|
||
const c = config[status] || config.idle;
|
||
return (
|
||
<span className={`inline-flex items-center gap-1.5 px-2.5 py-0.5 rounded-full text-xs font-medium border ${c.color}`}>
|
||
<span className={`w-1.5 h-1.5 rounded-full ${c.dot}`} />
|
||
{c.label}
|
||
</span>
|
||
);
|
||
}
|