feat: OnyvaLeads customer UI — Suche + Leadspeicher
- New Topbar: logo, 2-tab pill switcher, live "Neu" badge - /suche page: SearchCard, LoadingCard, ExamplePills - /leadspeicher page: full leads table with filters, pagination - StatusBadge, StatusPopover, LeadSidePanel, BulkActionBar - POST /api/search: unified search entry point → serp-enrich - Remove Sidebar + old TopBar from layout - Title: OnyvaLeads, redirect / → /suche Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
136
components/leadspeicher/LeadsToolbar.tsx
Normal file
136
components/leadspeicher/LeadsToolbar.tsx
Normal file
@@ -0,0 +1,136 @@
|
||||
"use client";
|
||||
|
||||
import { useRef } from "react";
|
||||
|
||||
interface LeadsToolbarProps {
|
||||
search: string;
|
||||
status: string;
|
||||
onSearchChange: (v: string) => void;
|
||||
onStatusChange: (v: string) => void;
|
||||
exportParams: Record<string, string>;
|
||||
}
|
||||
|
||||
export function LeadsToolbar({
|
||||
search,
|
||||
status,
|
||||
onSearchChange,
|
||||
onStatusChange,
|
||||
exportParams,
|
||||
}: LeadsToolbarProps) {
|
||||
const debounceRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
|
||||
function handleSearchInput(e: React.ChangeEvent<HTMLInputElement>) {
|
||||
const val = e.target.value;
|
||||
if (debounceRef.current) clearTimeout(debounceRef.current);
|
||||
debounceRef.current = setTimeout(() => onSearchChange(val), 300);
|
||||
// immediate update of input
|
||||
e.currentTarget.value = val;
|
||||
}
|
||||
|
||||
function handleExport() {
|
||||
const params = new URLSearchParams({ format: "xlsx", ...exportParams });
|
||||
window.location.href = `/api/leads/export?${params.toString()}`;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
position: "sticky",
|
||||
top: 52,
|
||||
zIndex: 40,
|
||||
background: "#111118",
|
||||
borderBottom: "1px solid #1e1e2e",
|
||||
padding: "14px 20px",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 12,
|
||||
}}
|
||||
>
|
||||
{/* Search input */}
|
||||
<div style={{ flex: 1, position: "relative" }}>
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
left: 10,
|
||||
top: "50%",
|
||||
transform: "translateY(-50%)",
|
||||
pointerEvents: "none",
|
||||
color: "#6b7280",
|
||||
}}
|
||||
>
|
||||
<svg width="14" height="14" viewBox="0 0 14 14" fill="none">
|
||||
<circle cx="6" cy="6" r="4.5" stroke="currentColor" strokeWidth="1.5" />
|
||||
<line x1="9.5" y1="9.5" x2="12.5" y2="12.5" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
|
||||
</svg>
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
defaultValue={search}
|
||||
onChange={handleSearchInput}
|
||||
placeholder="Unternehmen, E-Mail, Branche…"
|
||||
style={{
|
||||
width: "100%",
|
||||
background: "#0d0d18",
|
||||
border: "1px solid #1e1e2e",
|
||||
borderRadius: 8,
|
||||
padding: "8px 12px 8px 32px",
|
||||
fontSize: 13,
|
||||
color: "#ffffff",
|
||||
outline: "none",
|
||||
boxSizing: "border-box",
|
||||
}}
|
||||
onFocus={(e) => { e.currentTarget.style.borderColor = "#3b82f6"; }}
|
||||
onBlur={(e) => { e.currentTarget.style.borderColor = "#1e1e2e"; }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Status filter */}
|
||||
<select
|
||||
value={status}
|
||||
onChange={(e) => onStatusChange(e.target.value)}
|
||||
style={{
|
||||
width: 150,
|
||||
background: "#0d0d18",
|
||||
border: "1px solid #1e1e2e",
|
||||
borderRadius: 8,
|
||||
padding: "8px 12px",
|
||||
fontSize: 13,
|
||||
color: "#ffffff",
|
||||
cursor: "pointer",
|
||||
outline: "none",
|
||||
flexShrink: 0,
|
||||
}}
|
||||
>
|
||||
<option value="">Alle Status</option>
|
||||
<option value="new">Neu</option>
|
||||
<option value="contacted">Kontaktiert</option>
|
||||
<option value="in_progress">In Bearbeitung</option>
|
||||
<option value="not_relevant">Nicht relevant</option>
|
||||
<option value="converted">Konvertiert</option>
|
||||
</select>
|
||||
|
||||
{/* Export button */}
|
||||
<button
|
||||
onClick={handleExport}
|
||||
style={{
|
||||
background: "#0d0d18",
|
||||
border: "1px solid #2e2e3e",
|
||||
borderRadius: 8,
|
||||
padding: "8px 14px",
|
||||
fontSize: 13,
|
||||
color: "#9ca3af",
|
||||
cursor: "pointer",
|
||||
flexShrink: 0,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 6,
|
||||
transition: "color 0.15s",
|
||||
}}
|
||||
onMouseEnter={(e) => { (e.currentTarget as HTMLButtonElement).style.color = "#ffffff"; }}
|
||||
onMouseLeave={(e) => { (e.currentTarget as HTMLButtonElement).style.color = "#9ca3af"; }}
|
||||
>
|
||||
↓ Excel Export
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user