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:
100
app/suche/page.tsx
Normal file
100
app/suche/page.tsx
Normal file
@@ -0,0 +1,100 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useCallback } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { toast } from "sonner";
|
||||
import { SearchCard } from "@/components/search/SearchCard";
|
||||
import { LoadingCard } from "@/components/search/LoadingCard";
|
||||
|
||||
export default function SuchePage() {
|
||||
const router = useRouter();
|
||||
const [query, setQuery] = useState("");
|
||||
const [region, setRegion] = useState("");
|
||||
const [count, setCount] = useState(50);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [jobId, setJobId] = useState<string | null>(null);
|
||||
|
||||
function handleChange(field: "query" | "region" | "count", value: string | number) {
|
||||
if (field === "query") setQuery(value as string);
|
||||
if (field === "region") setRegion(value as string);
|
||||
if (field === "count") setCount(value as number);
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
if (!query.trim() || loading) return;
|
||||
setLoading(true);
|
||||
setJobId(null);
|
||||
try {
|
||||
const res = await fetch("/api/search", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ query: query.trim(), region: region.trim(), count }),
|
||||
});
|
||||
if (!res.ok) {
|
||||
const err = await res.json() as { error?: string };
|
||||
throw new Error(err.error || "Fehler beim Starten der Suche");
|
||||
}
|
||||
const data = await res.json() as { jobId: string };
|
||||
setJobId(data.jobId);
|
||||
} catch (err) {
|
||||
const msg = err instanceof Error ? err.message : "Unbekannter Fehler";
|
||||
toast.error(msg);
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
const handleDone = useCallback((total: number) => {
|
||||
setLoading(false);
|
||||
toast.success(`✓ ${total} Leads gefunden — Leadspeicher wird geöffnet`, {
|
||||
duration: 3000,
|
||||
});
|
||||
setTimeout(() => {
|
||||
router.push("/leadspeicher");
|
||||
}, 1500);
|
||||
}, [router]);
|
||||
|
||||
const handleError = useCallback(() => {
|
||||
setLoading(false);
|
||||
setJobId(null);
|
||||
toast.error("Suche fehlgeschlagen. Bitte prüfe deine API-Einstellungen.");
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
padding: "40px 40px",
|
||||
maxWidth: 680,
|
||||
margin: "0 auto",
|
||||
}}
|
||||
>
|
||||
{/* Hero */}
|
||||
<div style={{ textAlign: "center", marginBottom: 32 }}>
|
||||
<h2 style={{ fontSize: 22, fontWeight: 500, color: "#ffffff", margin: 0, marginBottom: 8 }}>
|
||||
Leads finden
|
||||
</h2>
|
||||
<p style={{ fontSize: 13, color: "#9ca3af", margin: 0 }}>
|
||||
Suchbegriff eingeben — wir finden passende Unternehmen mit Kontaktdaten.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Search Card */}
|
||||
<SearchCard
|
||||
query={query}
|
||||
region={region}
|
||||
count={count}
|
||||
loading={loading}
|
||||
onChange={handleChange}
|
||||
onSubmit={handleSubmit}
|
||||
/>
|
||||
|
||||
{/* Loading Card */}
|
||||
{loading && jobId && (
|
||||
<LoadingCard
|
||||
jobId={jobId}
|
||||
onDone={handleDone}
|
||||
onError={handleError}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user