Stitch redesign, Energieversorger-Kampagne, UI improvements
- Apply Stitch design system to leadspeicher, suche, TopBar, globals.css - Add Energieversorger queue campaign (Netzbetreiber, Fernwärme, Industriepark) with BW + Bayern priority, tracks usage per term+location combo - Remove TopBar right-side actions (Leads finden, bell, settings) - Remove mode tabs from manual search, rename KI button - Fix Google Fonts @import order (move to <link> in layout.tsx) - Add cursor-pointer globally via globals.css - Responsive fixes for campaign buttons and KI button - Fix .dockerignore to exclude .env from image build - Add stadtwerke-cities API + city data (50 cities per Bundesland) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -3,7 +3,6 @@
|
||||
import { useState, useCallback } from "react";
|
||||
import Link from "next/link";
|
||||
import { toast } from "sonner";
|
||||
import { SearchCard } from "@/components/search/SearchCard";
|
||||
import { LoadingCard, type LeadResult } from "@/components/search/LoadingCard";
|
||||
import { AiSearchModal } from "@/components/search/AiSearchModal";
|
||||
|
||||
@@ -20,10 +19,16 @@ export default function SuchePage() {
|
||||
const [onlyNew, setOnlyNew] = useState(false);
|
||||
const [saveOnlyNew, setSaveOnlyNew] = useState(false);
|
||||
const [aiOpen, setAiOpen] = useState(false);
|
||||
const [searchMode, setSearchMode] = useState<"stadtwerke" | "industrie" | "custom">("stadtwerke");
|
||||
const [queueRunning, setQueueRunning] = useState(false);
|
||||
const [queueIndex, setQueueIndex] = useState(0);
|
||||
const [queueTotal, setQueueTotal] = useState(0);
|
||||
const [queueLabel, setQueueLabel] = useState("");
|
||||
const [industrieRunning, setIndustrieRunning] = useState(false);
|
||||
const [industrieIndex, setIndustrieIndex] = useState(0);
|
||||
const [industrieTotal, setIndustrieTotal] = useState(0);
|
||||
const [industrieLabel, setIndustrieLabel] = useState("");
|
||||
|
||||
const INDUSTRIE_TERMS = ["Netzbetreiber", "Fernwärme", "Industriepark"];
|
||||
|
||||
const BUNDESLAENDER = [
|
||||
"Bayern", "NRW", "Baden-Württemberg", "Hessen", "Niedersachsen",
|
||||
@@ -85,51 +90,174 @@ export default function SuchePage() {
|
||||
}
|
||||
}, [jobId]);
|
||||
|
||||
async function runSearchAndWait(query: string, region: string, historyMode = "stadtwerke") {
|
||||
try {
|
||||
const res = await fetch("/api/search", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ query, region, count: 50 }),
|
||||
});
|
||||
const data = await res.json() as { jobId?: string };
|
||||
if (!data.jobId) return;
|
||||
// Save to history
|
||||
fetch("/api/search-history", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ query, region, searchMode: historyMode }),
|
||||
}).catch(() => {});
|
||||
// Poll until done
|
||||
await new Promise<void>((resolve) => {
|
||||
const interval = setInterval(async () => {
|
||||
try {
|
||||
const statusRes = await fetch(`/api/jobs/${data.jobId}/status`);
|
||||
const status = await statusRes.json() as { status: string };
|
||||
if (status.status === "complete" || status.status === "failed") {
|
||||
clearInterval(interval);
|
||||
resolve();
|
||||
}
|
||||
} catch { clearInterval(interval); resolve(); }
|
||||
}, 3000);
|
||||
});
|
||||
} catch { /* continue with next */ }
|
||||
}
|
||||
|
||||
async function startStadtwerkeQueue() {
|
||||
if (loading || queueRunning) return;
|
||||
setQueueRunning(true);
|
||||
setQueueTotal(BUNDESLAENDER.length);
|
||||
setQueueIndex(0);
|
||||
setLeads([]);
|
||||
setSearchDone(false);
|
||||
|
||||
for (let i = 0; i < BUNDESLAENDER.length; i++) {
|
||||
setQueueIndex(i + 1);
|
||||
const bl = BUNDESLAENDER[i];
|
||||
toast.info(`Suche ${i + 1}/${BUNDESLAENDER.length}: Stadtwerke ${bl}`, { duration: 2000 });
|
||||
try {
|
||||
const res = await fetch("/api/search", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ query: "Stadtwerke", region: bl, count: 50 }),
|
||||
});
|
||||
const data = await res.json() as { jobId?: string };
|
||||
if (data.jobId) {
|
||||
// Save to history
|
||||
fetch("/api/search-history", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ query: "Stadtwerke", region: bl, searchMode: "stadtwerke" }),
|
||||
}).catch(() => {});
|
||||
// Poll until done
|
||||
await new Promise<void>((resolve) => {
|
||||
const interval = setInterval(async () => {
|
||||
try {
|
||||
const statusRes = await fetch(`/api/jobs/${data.jobId}/status`);
|
||||
const status = await statusRes.json() as { status: string };
|
||||
if (status.status === "complete" || status.status === "failed") {
|
||||
clearInterval(interval);
|
||||
resolve();
|
||||
}
|
||||
} catch { clearInterval(interval); resolve(); }
|
||||
}, 3000);
|
||||
});
|
||||
// Fetch already-used regions to skip Bundesländer already done
|
||||
let usedRegions = new Set<string>();
|
||||
try {
|
||||
const hRes = await fetch("/api/search-history?mode=stadtwerke");
|
||||
if (hRes.ok) {
|
||||
const hist = await hRes.json() as Array<{ region: string }>;
|
||||
usedRegions = new Set(hist.map(h => h.region));
|
||||
}
|
||||
} catch { /* ignore */ }
|
||||
|
||||
// Phase 1: Unused Bundesländer
|
||||
const unusedBL = BUNDESLAENDER.filter(bl => !usedRegions.has(bl));
|
||||
|
||||
// Phase 2: Next batch of unused cities
|
||||
let cities: string[] = [];
|
||||
try {
|
||||
const cRes = await fetch("/api/stadtwerke-cities?count=50");
|
||||
if (cRes.ok) {
|
||||
const cData = await cRes.json() as { cities: string[]; exhausted: boolean };
|
||||
cities = cData.cities;
|
||||
if (cData.exhausted) {
|
||||
toast.info("Alle vordefinierten Städte durchsucht — KI generiert neue Vorschläge", { duration: 4000 });
|
||||
}
|
||||
} catch { /* continue with next */ }
|
||||
}
|
||||
} catch { /* ignore */ }
|
||||
|
||||
const allTargets = [
|
||||
...unusedBL.map(bl => ({ label: `Bundesland: ${bl}`, query: "Stadtwerke", region: bl })),
|
||||
...cities.map(city => ({ label: `Stadt: ${city}`, query: "Stadtwerke", region: city })),
|
||||
];
|
||||
|
||||
if (allTargets.length === 0) {
|
||||
setQueueRunning(false);
|
||||
toast.info("Alle bekannten Regionen wurden bereits durchsucht", { duration: 4000 });
|
||||
return;
|
||||
}
|
||||
|
||||
setQueueTotal(allTargets.length);
|
||||
|
||||
for (let i = 0; i < allTargets.length; i++) {
|
||||
const target = allTargets[i];
|
||||
setQueueIndex(i + 1);
|
||||
setQueueLabel(target.label);
|
||||
await runSearchAndWait(target.query, target.region);
|
||||
}
|
||||
|
||||
setQueueRunning(false);
|
||||
toast.success(`✓ Alle ${BUNDESLAENDER.length} Bundesländer durchsucht — Leads im Leadspeicher`, { duration: 5000 });
|
||||
const blCount = unusedBL.length;
|
||||
const cityCount = cities.length;
|
||||
const parts = [];
|
||||
if (blCount > 0) parts.push(`${blCount} Bundesländer`);
|
||||
if (cityCount > 0) parts.push(`${cityCount} Städte`);
|
||||
toast.success(`✓ ${parts.join(" + ")} durchsucht — Leads im Leadspeicher`, { duration: 5000 });
|
||||
}
|
||||
|
||||
async function startIndustrieQueue() {
|
||||
if (loading || queueRunning || industrieRunning) return;
|
||||
setIndustrieRunning(true);
|
||||
setIndustrieIndex(0);
|
||||
|
||||
// Load already-searched [term::location] combos
|
||||
let usedKeys = new Set<string>();
|
||||
try {
|
||||
const hRes = await fetch("/api/search-history?mode=industrie");
|
||||
if (hRes.ok) {
|
||||
const hist = await hRes.json() as Array<{ region: string }>;
|
||||
usedKeys = new Set(hist.map(h => h.region));
|
||||
}
|
||||
} catch { /* ignore */ }
|
||||
|
||||
// Cities data (inline priority order)
|
||||
const BW_CITIES = [
|
||||
"Stuttgart","Karlsruhe","Mannheim","Freiburg","Heidelberg","Ulm","Heilbronn","Pforzheim","Reutlingen","Ludwigsburg",
|
||||
"Esslingen","Tübingen","Villingen-Schwenningen","Konstanz","Aalen","Friedrichshafen","Sindelfingen","Ravensburg","Offenburg","Göppingen",
|
||||
"Böblingen","Schwäbisch Gmünd","Lahr","Waiblingen","Baden-Baden","Bruchsal","Weinheim","Leonberg","Bietigheim-Bissingen","Heidenheim",
|
||||
"Schwäbisch Hall","Nagold","Singen","Nürtingen","Fellbach","Tuttlingen","Überlingen","Backnang","Ditzingen","Kirchheim",
|
||||
"Schorndorf","Filderstadt","Leinfelden-Echterdingen","Ettlingen","Weil am Rhein","Rottenburg","Rheinfelden","Leutkirch","Mosbach","Crailsheim",
|
||||
];
|
||||
const BAYERN_CITIES = [
|
||||
"München","Nürnberg","Augsburg","Regensburg","Ingolstadt","Würzburg","Fürth","Erlangen","Bayreuth","Landshut",
|
||||
"Rosenheim","Kempten","Bamberg","Aschaffenburg","Neu-Ulm","Schweinfurt","Ansbach","Straubing","Passau","Coburg",
|
||||
"Dachau","Freising","Germering","Memmingen","Kaufbeuren","Hof","Amberg","Weiden","Pfaffenhofen","Starnberg",
|
||||
"Traunreut","Gauting","Garching","Erding","Fürstenfeldbruck","Unterschleißheim","Waldkraiburg","Marktoberdorf","Neumarkt","Altötting",
|
||||
"Weißenburg","Schwabach","Deggendorf","Traunstein","Burghausen","Bad Reichenhall","Neuburg an der Donau","Kelheim","Dillingen","Günzburg",
|
||||
];
|
||||
const OTHER_BL = ["NRW","Hessen","Niedersachsen","Sachsen","Berlin","Hamburg","Bremen","Thüringen","Sachsen-Anhalt","Brandenburg","Mecklenburg-Vorpommern","Saarland","Rheinland-Pfalz","Schleswig-Holstein"];
|
||||
|
||||
// Build priority targets: BW + Bayern first, then rest
|
||||
const priorityLocations = [
|
||||
"Baden-Württemberg",
|
||||
...BW_CITIES,
|
||||
"Bayern",
|
||||
...BAYERN_CITIES,
|
||||
];
|
||||
const restLocations = OTHER_BL;
|
||||
|
||||
// All [term, location] combos in priority order
|
||||
const allTargets: Array<{ label: string; term: string; location: string }> = [];
|
||||
for (const loc of [...priorityLocations, ...restLocations]) {
|
||||
for (const term of INDUSTRIE_TERMS) {
|
||||
const key = `${term}::${loc}`;
|
||||
if (!usedKeys.has(key)) {
|
||||
allTargets.push({ label: `${term} · ${loc}`, term, location: loc });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (allTargets.length === 0) {
|
||||
setIndustrieRunning(false);
|
||||
toast.info("Alle Energieversorger-Suchen wurden bereits durchgeführt", { duration: 4000 });
|
||||
return;
|
||||
}
|
||||
|
||||
setIndustrieTotal(allTargets.length);
|
||||
|
||||
for (let i = 0; i < allTargets.length; i++) {
|
||||
const t = allTargets[i];
|
||||
setIndustrieIndex(i + 1);
|
||||
setIndustrieLabel(t.label);
|
||||
await runSearchAndWait(t.term, t.location, "industrie");
|
||||
// Override the region saved in history with the key so we can track term+location combos
|
||||
fetch("/api/search-history", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ query: t.term, region: `${t.term}::${t.location}`, searchMode: "industrie" }),
|
||||
}).catch(() => {});
|
||||
}
|
||||
|
||||
setIndustrieRunning(false);
|
||||
toast.success(`✓ ${allTargets.length} Energieversorger-Suchen abgeschlossen — Leads im Leadspeicher`, { duration: 5000 });
|
||||
}
|
||||
|
||||
async function handleDelete(ids: string[]) {
|
||||
@@ -182,121 +310,206 @@ export default function SuchePage() {
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
padding: "72px 120px",
|
||||
maxWidth: 900,
|
||||
margin: "0 auto",
|
||||
}}
|
||||
>
|
||||
{/* Hero */}
|
||||
<div className="relative rounded-2xl border border-[#1e1e2e] p-6 overflow-hidden mb-6"
|
||||
style={{ background: "linear-gradient(135deg, rgba(59,130,246,0.08) 0%, rgba(139,92,246,0.08) 100%)" }}>
|
||||
<div className="absolute inset-0" style={{ background: "linear-gradient(135deg, rgba(59,130,246,0.04) 0%, transparent 60%)" }} />
|
||||
<div className="relative">
|
||||
<div className="flex items-center gap-2 text-xs mb-2" style={{ color: "#3b82f6" }}>
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.35-4.35"/></svg>
|
||||
<span>Lead-Suche</span>
|
||||
</div>
|
||||
<div className="flex items-start justify-between">
|
||||
<div>
|
||||
<h1 style={{ fontSize: 22, fontWeight: 500, color: "#ffffff", margin: 0, marginBottom: 6 }}>
|
||||
Leads finden
|
||||
</h1>
|
||||
<p style={{ fontSize: 13, color: "#9ca3af", margin: 0 }}>
|
||||
Suchbegriff eingeben — wir finden passende Unternehmen mit Kontaktdaten.
|
||||
<div className="pb-12 px-8 max-w-7xl mx-auto pt-8 space-y-10">
|
||||
<style>{`
|
||||
@keyframes spin { to { transform: rotate(360deg) } }
|
||||
.del-btn:hover { opacity: 1 !important; }
|
||||
.lead-row:hover { background: rgba(255,255,255,0.02); }
|
||||
.lead-row:hover .row-del { opacity: 1 !important; }
|
||||
`}</style>
|
||||
|
||||
{/* ── Strategische Kampagnen ── */}
|
||||
<section>
|
||||
<h2 className="text-2xl font-extrabold tracking-tight mb-6" style={{ fontFamily: "Manrope, sans-serif", color: "#dce3ea" }}>
|
||||
Strategische Energiewirtschafts-Kampagnen
|
||||
</h2>
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
{/* CTA Card — Stadtwerke */}
|
||||
<div className="rounded-xl p-8 relative overflow-hidden flex flex-col justify-center" style={{ background: "#161c21", border: "1px solid rgba(255,255,255,0.05)" }}>
|
||||
<div className="absolute -top-24 -right-24 w-64 h-64 rounded-full pointer-events-none" style={{ background: "rgba(26,115,232,0.2)", filter: "blur(100px)" }} />
|
||||
<div className="relative z-10">
|
||||
<p className="text-xs font-bold uppercase tracking-widest mb-4" style={{ color: "#c1c6d6", fontFamily: "Inter, sans-serif", letterSpacing: "0.1em" }}>Hauptkampagne</p>
|
||||
<h3 className="text-3xl font-extrabold mb-6 leading-tight" style={{ fontFamily: "Manrope, sans-serif" }}>
|
||||
Bundesweite<br />Lead-Gewinnung
|
||||
</h3>
|
||||
<p className="text-sm leading-relaxed mb-8 max-w-md" style={{ color: "#c1c6d6" }}>
|
||||
Startet sofort den Deep-Scrape aller Stadtwerke in allen deutschen Bundesländern.
|
||||
</p>
|
||||
{queueRunning ? (
|
||||
<div className="w-full py-4 rounded-xl flex items-center justify-center gap-3" style={{ background: "#242b30" }}>
|
||||
<svg style={{ animation: "spin 1s linear infinite", flexShrink: 0 }} width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#adc7ff" strokeWidth="2">
|
||||
<path d="M21 12a9 9 0 1 1-6.219-8.56" />
|
||||
</svg>
|
||||
<div>
|
||||
<div className="font-bold text-sm" style={{ color: "#adc7ff", fontFamily: "Manrope, sans-serif" }}>{queueLabel || "Wird durchsucht…"} ({queueIndex}/{queueTotal})</div>
|
||||
<div className="text-xs mt-0.5" style={{ color: "#c1c6d6" }}>Leads werden automatisch gespeichert</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<button
|
||||
onClick={startStadtwerkeQueue}
|
||||
disabled={loading}
|
||||
className="w-full py-5 rounded-xl font-black text-lg text-white transition-all active:scale-[0.98] disabled:opacity-50"
|
||||
style={{
|
||||
fontFamily: "Manrope, sans-serif",
|
||||
background: "linear-gradient(135deg, #adc7ff, #1a73e8)",
|
||||
boxShadow: "0 10px 30px rgba(26,115,232,0.4)",
|
||||
}}
|
||||
onMouseEnter={e => { e.currentTarget.style.boxShadow = "0 15px 40px rgba(26,115,232,0.6)"; }}
|
||||
onMouseLeave={e => { e.currentTarget.style.boxShadow = "0 10px 30px rgba(26,115,232,0.4)"; }}
|
||||
>
|
||||
START 16 BUNDESLÄNDER STADTWERKE-SUCHE (1-Klick)
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* CTA Card — Energieversorger */}
|
||||
<div className="rounded-xl p-8 relative overflow-hidden flex flex-col justify-center" style={{ background: "#161c21", border: "1px solid rgba(255,255,255,0.05)" }}>
|
||||
<div className="absolute -top-24 -right-24 w-64 h-64 rounded-full pointer-events-none" style={{ background: "rgba(160,216,44,0.12)", filter: "blur(100px)" }} />
|
||||
<div className="relative z-10">
|
||||
<p className="text-xs font-bold uppercase tracking-widest mb-4" style={{ color: "#c1c6d6", fontFamily: "Inter, sans-serif", letterSpacing: "0.1em" }}>Energieversorger-Kampagne</p>
|
||||
<h3 className="text-3xl font-extrabold mb-6 leading-tight" style={{ fontFamily: "Manrope, sans-serif" }}>
|
||||
Industrie &<br />Energieversorger
|
||||
</h3>
|
||||
<p className="text-sm leading-relaxed mb-8 max-w-md" style={{ color: "#c1c6d6" }}>
|
||||
Sucht nach <strong style={{ color: "#a0d82c" }}>Netzbetreiber</strong>, <strong style={{ color: "#a0d82c" }}>Fernwärme</strong> und <strong style={{ color: "#a0d82c" }}>Industriepark</strong> — priorisiert BW & Bayern, dann alle weiteren Bundesländer.
|
||||
</p>
|
||||
{industrieRunning ? (
|
||||
<div className="w-full py-4 rounded-xl flex items-center justify-center gap-3" style={{ background: "#242b30" }}>
|
||||
<svg style={{ animation: "spin 1s linear infinite", flexShrink: 0 }} width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#a0d82c" strokeWidth="2">
|
||||
<path d="M21 12a9 9 0 1 1-6.219-8.56" />
|
||||
</svg>
|
||||
<div>
|
||||
<div className="font-bold text-sm" style={{ color: "#a0d82c", fontFamily: "Manrope, sans-serif" }}>{industrieLabel || "Wird durchsucht…"} ({industrieIndex}/{industrieTotal})</div>
|
||||
<div className="text-xs mt-0.5" style={{ color: "#c1c6d6" }}>Leads werden automatisch gespeichert</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<button
|
||||
onClick={startIndustrieQueue}
|
||||
disabled={loading || queueRunning}
|
||||
className="w-full py-5 rounded-xl font-black text-lg transition-all active:scale-[0.98] disabled:opacity-50"
|
||||
style={{
|
||||
fontFamily: "Manrope, sans-serif",
|
||||
background: "linear-gradient(135deg, #a0d82c, #5c8200)",
|
||||
color: "#131f00",
|
||||
boxShadow: "0 10px 30px rgba(160,216,44,0.3)",
|
||||
}}
|
||||
onMouseEnter={e => { e.currentTarget.style.boxShadow = "0 15px 40px rgba(160,216,44,0.5)"; }}
|
||||
onMouseLeave={e => { e.currentTarget.style.boxShadow = "0 10px 30px rgba(160,216,44,0.3)"; }}
|
||||
>
|
||||
START ENERGIEVERSORGER-SUCHE
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setAiOpen(true)}
|
||||
disabled={loading}
|
||||
style={{
|
||||
display: "flex", alignItems: "center", gap: 7,
|
||||
padding: "8px 14px", borderRadius: 8,
|
||||
border: "1px solid rgba(139,92,246,0.35)",
|
||||
background: "rgba(139,92,246,0.1)", color: "#a78bfa",
|
||||
fontSize: 13, fontWeight: 500,
|
||||
cursor: loading ? "not-allowed" : "pointer",
|
||||
opacity: loading ? 0.5 : 1, whiteSpace: "nowrap",
|
||||
}}
|
||||
onMouseEnter={e => { if (!loading) { e.currentTarget.style.background = "rgba(139,92,246,0.18)"; e.currentTarget.style.borderColor = "rgba(139,92,246,0.6)"; }}}
|
||||
onMouseLeave={e => { e.currentTarget.style.background = "rgba(139,92,246,0.1)"; e.currentTarget.style.borderColor = "rgba(139,92,246,0.35)"; }}
|
||||
>
|
||||
✨ KI-Suche
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Mode Tabs */}
|
||||
<div style={{ display: "flex", gap: 8, marginBottom: 16 }}>
|
||||
{([
|
||||
{ id: "stadtwerke" as const, icon: "⚡", label: "Stadtwerke", desc: "Kommunale Energieversorger" },
|
||||
{ id: "industrie" as const, icon: "🏭", label: "Industriebetriebe", desc: "Energieintensive Betriebe" },
|
||||
{ id: "custom" as const, icon: "🔍", label: "Freie Suche", desc: "Beliebige Zielgruppe" },
|
||||
]).map(tab => (
|
||||
<button
|
||||
key={tab.id}
|
||||
onClick={() => setSearchMode(tab.id)}
|
||||
style={{
|
||||
flex: 1, padding: "12px 16px", borderRadius: 10, cursor: "pointer", textAlign: "left",
|
||||
border: searchMode === tab.id ? "1px solid rgba(59,130,246,0.5)" : "1px solid #1e1e2e",
|
||||
background: searchMode === tab.id ? "rgba(59,130,246,0.08)" : "#111118",
|
||||
transition: "all 0.15s",
|
||||
}}
|
||||
>
|
||||
<div style={{ fontSize: 16, marginBottom: 4 }}>{tab.icon}</div>
|
||||
<div style={{ fontSize: 13, fontWeight: 500, color: searchMode === tab.id ? "#93c5fd" : "#fff", marginBottom: 2 }}>{tab.label}</div>
|
||||
<div style={{ fontSize: 11, color: "#6b7280" }}>{tab.desc}</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Stadtwerke Queue Button */}
|
||||
{searchMode === "stadtwerke" && !loading && !queueRunning && (
|
||||
<button
|
||||
onClick={startStadtwerkeQueue}
|
||||
style={{
|
||||
width: "100%", padding: "14px 20px", borderRadius: 10, marginBottom: 12,
|
||||
border: "1px solid rgba(59,130,246,0.4)", background: "rgba(59,130,246,0.06)",
|
||||
color: "#60a5fa", cursor: "pointer", textAlign: "left",
|
||||
display: "flex", flexDirection: "column", gap: 4,
|
||||
}}
|
||||
onMouseEnter={e => { e.currentTarget.style.background = "rgba(59,130,246,0.12)"; }}
|
||||
onMouseLeave={e => { e.currentTarget.style.background = "rgba(59,130,246,0.06)"; }}
|
||||
>
|
||||
<div style={{ fontSize: 13, fontWeight: 500 }}>⚡ Alle deutschen Stadtwerke durchsuchen (16 Bundesländer)</div>
|
||||
<div style={{ fontSize: 11, color: "#6b7280" }}>Startet 16 aufeinanderfolgende Suchen · ca. 800 Ergebnisse</div>
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* Queue running indicator */}
|
||||
{queueRunning && (
|
||||
<div style={{
|
||||
padding: "14px 20px", borderRadius: 10, marginBottom: 12,
|
||||
border: "1px solid rgba(59,130,246,0.3)", background: "rgba(59,130,246,0.06)",
|
||||
display: "flex", alignItems: "center", gap: 12,
|
||||
}}>
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#3b82f6" strokeWidth="2" style={{ animation: "spin 1s linear infinite", flexShrink: 0 }}>
|
||||
<path d="M21 12a9 9 0 1 1-6.219-8.56"/>
|
||||
</svg>
|
||||
{/* ── Manuelle Suche ── */}
|
||||
<section>
|
||||
<div className="flex flex-col sm:flex-row sm:justify-between sm:items-end gap-3 mb-6">
|
||||
<div>
|
||||
<div style={{ fontSize: 13, color: "#93c5fd", fontWeight: 500 }}>Bundesland {queueIndex} von {queueTotal} wird durchsucht…</div>
|
||||
<div style={{ fontSize: 11, color: "#6b7280", marginTop: 2 }}>Nicht schließen — Leads werden automatisch gespeichert</div>
|
||||
<h2 className="text-2xl font-extrabold tracking-tight" style={{ fontFamily: "Manrope, sans-serif", color: "#dce3ea" }}>
|
||||
Manuelle Lead- & Nischensuche
|
||||
</h2>
|
||||
<p className="text-sm mt-1" style={{ color: "#c1c6d6" }}>Verfeinern Sie Ihre Zielparameter für eine hochspezifische Extraktion.</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setAiOpen(true)}
|
||||
disabled={loading}
|
||||
className="flex items-center gap-2 px-3 py-2 md:px-4 rounded-xl transition-all disabled:opacity-50 flex-shrink-0"
|
||||
style={{ background: "#161c21", border: "1px solid rgba(173,199,255,0.2)", boxShadow: "0 0 20px rgba(26,115,232,0.1)" }}
|
||||
>
|
||||
<span className="material-symbols-outlined text-xl" style={{ color: "#adc7ff", fontVariationSettings: "'FILL' 1" }}>smart_toy</span>
|
||||
<span className="hidden sm:inline font-bold text-sm uppercase tracking-wider" style={{ fontFamily: "Manrope, sans-serif", color: "#adc7ff" }}>KI-gestützte Suche</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="rounded-2xl p-8 shadow-2xl" style={{ background: "#161c21", border: "1px solid rgba(255,255,255,0.05)" }}>
|
||||
{/* Form fields */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-12 gap-4 md:gap-6 items-end">
|
||||
<div className="md:col-span-5 space-y-2">
|
||||
<label className="block text-[10px] font-bold uppercase tracking-widest ml-1" style={{ color: "#c1c6d6" }}>Suchbegriff</label>
|
||||
<div className="relative">
|
||||
<input
|
||||
className="w-full rounded-xl px-4 py-4 text-sm transition-all outline-none"
|
||||
style={{ background: "#080f13", border: "1px solid rgba(65,71,84,0.4)", color: "#dce3ea" }}
|
||||
value={query}
|
||||
onChange={e => handleChange("query", e.target.value)}
|
||||
onKeyDown={e => { if (e.key === "Enter" && query.trim() && !loading) handleSubmit(); }}
|
||||
placeholder="z.B. Stadtwerke"
|
||||
onFocus={e => { e.currentTarget.style.borderColor = "#adc7ff"; e.currentTarget.style.boxShadow = "0 0 0 2px rgba(173,199,255,0.2)"; }}
|
||||
onBlur={e => { e.currentTarget.style.borderColor = "rgba(65,71,84,0.4)"; e.currentTarget.style.boxShadow = "none"; }}
|
||||
/>
|
||||
<span className="absolute right-4 top-1/2 -translate-y-1/2 material-symbols-outlined" style={{ color: "#8b909f", fontSize: 20 }}>search</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="md:col-span-4 space-y-2">
|
||||
<label className="block text-[10px] font-bold uppercase tracking-widest ml-1" style={{ color: "#c1c6d6" }}>Region</label>
|
||||
<div className="relative">
|
||||
<input
|
||||
className="w-full rounded-xl px-4 py-4 text-sm transition-all outline-none"
|
||||
style={{ background: "#080f13", border: "1px solid rgba(65,71,84,0.4)", color: "#dce3ea" }}
|
||||
value={region}
|
||||
onChange={e => handleChange("region", e.target.value)}
|
||||
onKeyDown={e => { if (e.key === "Enter" && query.trim() && !loading) handleSubmit(); }}
|
||||
placeholder="z.B. Bayern"
|
||||
onFocus={e => { e.currentTarget.style.borderColor = "#adc7ff"; e.currentTarget.style.boxShadow = "0 0 0 2px rgba(173,199,255,0.2)"; }}
|
||||
onBlur={e => { e.currentTarget.style.borderColor = "rgba(65,71,84,0.4)"; e.currentTarget.style.boxShadow = "none"; }}
|
||||
/>
|
||||
<span className="absolute right-4 top-1/2 -translate-y-1/2 material-symbols-outlined" style={{ color: "#8b909f", fontSize: 20 }}>location_on</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="md:col-span-2 space-y-2">
|
||||
<label className="block text-[10px] font-bold uppercase tracking-widest ml-1" style={{ color: "#c1c6d6" }}>Anzahl</label>
|
||||
<select
|
||||
className="w-full rounded-xl px-4 py-4 text-sm outline-none appearance-none"
|
||||
style={{ background: "#080f13", border: "1px solid rgba(65,71,84,0.4)", color: "#dce3ea" }}
|
||||
value={count}
|
||||
onChange={e => handleChange("count", Number(e.target.value))}
|
||||
>
|
||||
<option value={25}>25</option>
|
||||
<option value={50}>50</option>
|
||||
<option value={100}>100</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className="sm:col-span-2 md:col-span-1">
|
||||
<button
|
||||
onClick={handleSubmit}
|
||||
disabled={!query.trim() || loading || queueRunning}
|
||||
className="w-full h-[58px] rounded-xl flex items-center justify-center gap-2 transition-all active:scale-95 disabled:opacity-50"
|
||||
style={{ background: "#333a3f" }}
|
||||
onMouseEnter={e => { if (!e.currentTarget.disabled) { e.currentTarget.style.background = "#1a73e8"; } }}
|
||||
onMouseLeave={e => { e.currentTarget.style.background = "#333a3f"; }}
|
||||
>
|
||||
<span className="material-symbols-outlined" style={{ color: "#dce3ea" }}>bolt</span>
|
||||
<span className="md:hidden text-sm font-bold" style={{ color: "#dce3ea", fontFamily: "Manrope, sans-serif" }}>Suchen</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Quick Presets */}
|
||||
<div className="mt-8 pt-8" style={{ borderTop: "1px solid rgba(255,255,255,0.05)" }}>
|
||||
<p className="text-[10px] font-bold uppercase tracking-widest mb-4" style={{ color: "#c1c6d6" }}>Quick Presets</p>
|
||||
<div className="flex flex-wrap gap-3">
|
||||
{["Stadtwerke", "Energieversorger", "Industrie-Energie", "Gemeindewerke", "Netzbetreiber"].map(preset => (
|
||||
<button
|
||||
key={preset}
|
||||
onClick={() => handleChange("query", preset)}
|
||||
className="px-5 py-2 rounded-full text-sm font-medium transition-all"
|
||||
style={{ background: "rgba(47,54,59,0.5)", border: "1px solid rgba(255,255,255,0.05)", color: "#dce3ea" }}
|
||||
onMouseEnter={e => { e.currentTarget.style.background = "rgba(26,115,232,0.2)"; e.currentTarget.style.borderColor = "rgba(173,199,255,0.3)"; }}
|
||||
onMouseLeave={e => { e.currentTarget.style.background = "rgba(47,54,59,0.5)"; e.currentTarget.style.borderColor = "rgba(255,255,255,0.05)"; }}
|
||||
>
|
||||
{preset}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<style>{`@keyframes spin { to { transform: rotate(360deg) } }`}</style>
|
||||
|
||||
{/* Search Card */}
|
||||
<SearchCard
|
||||
query={query}
|
||||
region={region}
|
||||
count={count}
|
||||
loading={loading || queueRunning}
|
||||
onChange={handleChange}
|
||||
onSubmit={handleSubmit}
|
||||
/>
|
||||
</section>
|
||||
|
||||
|
||||
{/* Loading Card */}
|
||||
@@ -314,7 +527,7 @@ export default function SuchePage() {
|
||||
{/* AI Modal */}
|
||||
{aiOpen && (
|
||||
<AiSearchModal
|
||||
searchMode={searchMode}
|
||||
searchMode="custom"
|
||||
onStart={(queries) => {
|
||||
setAiOpen(false);
|
||||
if (!queries.length) return;
|
||||
@@ -331,7 +544,7 @@ export default function SuchePage() {
|
||||
fetch("/api/search-history", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ query: first.query, region: first.region, searchMode }),
|
||||
body: JSON.stringify({ query: first.query, region: first.region, searchMode: "custom" }),
|
||||
}).catch(() => {});
|
||||
fetch("/api/search", {
|
||||
method: "POST",
|
||||
@@ -363,9 +576,8 @@ export default function SuchePage() {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
marginTop: 16,
|
||||
background: "#111118",
|
||||
border: "1px solid #1e1e2e",
|
||||
background: "#161c21",
|
||||
border: "1px solid rgba(65,71,84,0.3)",
|
||||
borderRadius: 12,
|
||||
overflow: "hidden",
|
||||
}}
|
||||
|
||||
Reference in New Issue
Block a user