From 7db914084ea3cc32cf83fe7e21f6b068ab3fcf8c Mon Sep 17 00:00:00 2001 From: Timo Uttenweiler Date: Thu, 9 Apr 2026 10:08:00 +0200 Subject: [PATCH] Stitch redesign, Energieversorger-Kampagne, UI improvements MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 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 --- .dockerignore | 3 +- app/api/stadtwerke-cities/route.ts | 84 +++++ app/globals.css | 113 ++++++- app/layout.tsx | 8 +- app/leadspeicher/page.tsx | 186 +++++----- app/suche/page.tsx | 508 ++++++++++++++++++++-------- components/layout/TopBar.tsx | 159 +++------ components/search/AiSearchModal.tsx | 2 +- lib/data/stadtwerke-cities.ts | 161 +++++++++ 9 files changed, 868 insertions(+), 356 deletions(-) create mode 100644 app/api/stadtwerke-cities/route.ts create mode 100644 lib/data/stadtwerke-cities.ts diff --git a/.dockerignore b/.dockerignore index 8527c32..18cef64 100644 --- a/.dockerignore +++ b/.dockerignore @@ -12,7 +12,8 @@ out *.db-wal /data -# Environment files (injected at runtime) +# Environment files (injected at runtime via Coolify) +.env .env.local .env.*.local diff --git a/app/api/stadtwerke-cities/route.ts b/app/api/stadtwerke-cities/route.ts new file mode 100644 index 0000000..d888a6e --- /dev/null +++ b/app/api/stadtwerke-cities/route.ts @@ -0,0 +1,84 @@ +import { NextRequest, NextResponse } from "next/server"; +import { prisma } from "@/lib/db"; +import { ALL_CITIES } from "@/lib/data/stadtwerke-cities"; + +const OPENROUTER_API_KEY = process.env.OPENROUTER_API_KEY || ""; +const OPENROUTER_BASE = "https://openrouter.ai/api/v1"; + +export async function GET(req: NextRequest) { + try { + const { searchParams } = new URL(req.url); + const count = Math.min(parseInt(searchParams.get("count") || "20", 10), 100); + + // Get all regions already used for stadtwerke mode + const used = await prisma.searchHistory.findMany({ + where: { searchMode: "stadtwerke" }, + select: { region: true }, + }); + const usedRegions = new Set(used.map(u => u.region.toLowerCase().trim())); + + // Filter static list — cities not yet searched + const remaining = ALL_CITIES.filter(city => !usedRegions.has(city.toLowerCase().trim())); + + if (remaining.length === 0) { + // All static cities exhausted — ask AI for new suggestions + const newCities = await generateNewCities(usedRegions); + return NextResponse.json({ cities: newCities.slice(0, count), exhausted: true, totalRemaining: newCities.length }); + } + + return NextResponse.json({ + cities: remaining.slice(0, count), + exhausted: false, + totalRemaining: remaining.length, + }); + } catch (err) { + console.error("GET /api/stadtwerke-cities error:", err); + return NextResponse.json({ error: "Failed to fetch cities" }, { status: 500 }); + } +} + +async function generateNewCities(usedRegions: Set): Promise { + if (!OPENROUTER_API_KEY) return []; + + const usedList = Array.from(usedRegions) + .filter(r => r.length > 0) + .slice(0, 150) + .join(", "); + + try { + const res = await fetch(`${OPENROUTER_BASE}/chat/completions`, { + method: "POST", + headers: { + "Authorization": `Bearer ${OPENROUTER_API_KEY}`, + "Content-Type": "application/json", + "HTTP-Referer": "https://mein-solar.de", + }, + body: JSON.stringify({ + model: "openai/gpt-4o-mini", + messages: [ + { + role: "system", + content: + "Du bist ein Experte für kommunale Energieversorger in Deutschland. Antworte ausschließlich mit einem gültigen JSON-Array aus Städtenamen, ohne Erklärungen.", + }, + { + role: "user", + content: `Generiere 40 deutsche Städte und Gemeinden, in denen es lokale Stadtwerke oder kommunale Energieversorger gibt. Keine dieser Städte darf in der folgenden Liste enthalten sein:\n${usedList}\n\nGib nur ein JSON-Array zurück, z.B.: ["Landsberg am Lech", "Bühl", "Leutkirch"]`, + }, + ], + max_tokens: 600, + temperature: 0.7, + }), + }); + + const data = await res.json() as { choices?: Array<{ message?: { content?: string } }> }; + const content = data.choices?.[0]?.message?.content?.trim() || "[]"; + + // Strip markdown code blocks if present + const cleaned = content.replace(/^```(?:json)?\s*/i, "").replace(/\s*```$/, "").trim(); + const cities = JSON.parse(cleaned) as unknown; + return Array.isArray(cities) ? (cities as string[]).filter(c => typeof c === "string") : []; + } catch { + return []; + } +} diff --git a/app/globals.css b/app/globals.css index 6d4cf27..76ba337 100644 --- a/app/globals.css +++ b/app/globals.css @@ -1,21 +1,80 @@ @import "tailwindcss"; +@theme { + /* Stitch Solar Design System Colors */ + --color-background: #0d1419; + --color-surface: #0d1419; + --color-surface-dim: #0d1419; + --color-surface-container-lowest: #080f13; + --color-surface-container-low: #161c21; + --color-surface-container: #1a2025; + --color-surface-container-high: #242b30; + --color-surface-container-highest: #2f363b; + --color-surface-bright: #333a3f; + --color-surface-variant: #2f363b; + --color-surface-tint: #adc7ff; + --color-primary: #adc7ff; + --color-primary-container: #1a73e8; + --color-primary-fixed: #d8e2ff; + --color-primary-fixed-dim: #adc7ff; + --color-on-primary: #002e68; + --color-on-primary-container: #ffffff; + --color-on-primary-fixed: #001a41; + --color-on-primary-fixed-variant: #004493; + --color-secondary: #ffb77b; + --color-secondary-container: #fb8c00; + --color-secondary-fixed: #ffdcc2; + --color-secondary-fixed-dim: #ffb77b; + --color-on-secondary: #4c2700; + --color-on-secondary-container: #5f3200; + --color-on-secondary-fixed: #2e1500; + --color-on-secondary-fixed-variant: #6d3a00; + --color-tertiary: #a0d82c; + --color-tertiary-container: #5c8200; + --color-tertiary-fixed: #baf549; + --color-tertiary-fixed-dim: #a0d82c; + --color-on-tertiary: #243600; + --color-on-tertiary-container: #ffffff; + --color-on-tertiary-fixed: #131f00; + --color-on-tertiary-fixed-variant: #364e00; + --color-on-surface: #dce3ea; + --color-on-surface-variant: #c1c6d6; + --color-on-background: #dce3ea; + --color-outline: #8b909f; + --color-outline-variant: #414754; + --color-error: #ffb4ab; + --color-error-container: #93000a; + --color-on-error: #690005; + --color-on-error-container: #ffdad6; + --color-inverse-surface: #dce3ea; + --color-inverse-on-surface: #2a3136; + --color-inverse-primary: #005bc0; + + /* Font families */ + --font-headline: 'Manrope', sans-serif; + --font-body: 'Inter', sans-serif; + --font-label: 'Inter', sans-serif; +} + +button { cursor: pointer; } +button:disabled { cursor: not-allowed; } + @keyframes shimmer { 0% { transform: translateX(-100%); } 100% { transform: translateX(400%); } } :root { - --background: #0a0a0f; - --card: #111118; - --border: #1e1e2e; - --primary: #3b82f6; - --secondary: #8b5cf6; - --success: #22c55e; - --warning: #f59e0b; - --error: #ef4444; - --foreground: #f0f0f5; - --muted: #6b7280; + --background: #0d1419; + --card: #161c21; + --border: #414754; + --primary: #adc7ff; + --secondary: #fb8c00; + --success: #a0d82c; + --warning: #ffb77b; + --error: #ffb4ab; + --foreground: #dce3ea; + --muted: #c1c6d6; } * { @@ -23,13 +82,35 @@ } body { - background-color: var(--background); - color: var(--foreground); - font-family: var(--font-inter), Inter, system-ui, -apple-system, sans-serif; + background-color: #0d1419; + color: #dce3ea; + font-family: 'Inter', system-ui, -apple-system, sans-serif; +} + +h1, h2, h3, h4, h5, h6 { + font-family: 'Manrope', sans-serif; +} + +.material-symbols-outlined { + font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24; + font-family: 'Material Symbols Outlined'; + font-style: normal; + line-height: 1; + letter-spacing: normal; + text-transform: none; + display: inline-block; + white-space: nowrap; + word-wrap: normal; + direction: ltr; + -webkit-font-feature-settings: 'liga'; + -webkit-font-smoothing: antialiased; } /* Scrollbar */ ::-webkit-scrollbar { width: 6px; height: 6px; } -::-webkit-scrollbar-track { background: #0a0a0f; } -::-webkit-scrollbar-thumb { background: #1e1e2e; border-radius: 3px; } -::-webkit-scrollbar-thumb:hover { background: #3b82f6; } +::-webkit-scrollbar-track { background: #0d1419; } +::-webkit-scrollbar-thumb { background: #414754; border-radius: 3px; } +::-webkit-scrollbar-thumb:hover { background: #adc7ff; } + +.no-scrollbar::-webkit-scrollbar { display: none; } +.no-scrollbar { -ms-overflow-style: none; scrollbar-width: none; } diff --git a/app/layout.tsx b/app/layout.tsx index a614bc2..f689fd3 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -11,8 +11,14 @@ export const metadata: Metadata = { export default function RootLayout({ children }: { children: React.ReactNode }) { return ( + + + + + + -
+
{children} diff --git a/app/leadspeicher/page.tsx b/app/leadspeicher/page.tsx index 5b84448..a48f503 100644 --- a/app/leadspeicher/page.tsx +++ b/app/leadspeicher/page.tsx @@ -153,14 +153,14 @@ function SidePanel({ lead, onClose, onUpdate, onDelete }: {
e.stopPropagation()} > {/* Header */} -
+

{lead.companyName || lead.domain || "Unbekannt"}

{lead.domain && ( @@ -169,7 +169,7 @@ function SidePanel({ lead, onClose, onUpdate, onDelete }: { {lead.domain} )} - {lead.industry &&

{lead.industry}

} + {lead.industry &&

{lead.industry}

}
{/* Delete */} -
+
@@ -585,7 +586,7 @@ export default function LeadVaultPage() { {exportOpen && ( <>
setExportOpen(false)} /> -
+
{([ ["Aktuelle Ansicht", () => exportFile("xlsx")], ["Nur mit E-Mail", () => exportFile("xlsx", true)], @@ -604,33 +605,36 @@ export default function LeadVaultPage() { {stats && (
{[ - { label: "Leads gesamt", value: stats.total, color: "#a78bfa" }, - { label: "Neu / Nicht kontaktiert", value: stats.new, color: "#60a5fa" }, - { label: "Kontaktiert / In Bearbeitung", value: stats.contacted, color: "#2dd4bf" }, - { label: "Mit verifizierter E-Mail", value: stats.withEmail, color: "#34d399" }, - ].map(({ label, value, color }) => ( - -

{label}

+ { label: "Leads gesamt", value: stats.total, color: "#adc7ff", accent: "rgba(173,199,255,0.15)" }, + { label: "Neu / Unbearbeitet", value: stats.new, color: "#adc7ff", accent: "rgba(173,199,255,0.1)" }, + { label: "Kontaktiert", value: stats.contacted, color: "#a0d82c", accent: "rgba(160,216,44,0.1)" }, + { label: "Mit E-Mail", value: stats.withEmail, color: "#ffb77b", accent: "rgba(255,183,123,0.1)" }, + ].map(({ label, value, color, accent }) => ( +
+

{label}

-

{value.toLocaleString()}

- d.count)} color={color} /> +

{value.toLocaleString()}

+
+ d.count)} color={color} /> +
- +
))}
)} {/* Quick SERP */} - +
{serpOpen && ( -
+
setSerpQuery(e.target.value)} onKeyDown={e => e.key === "Enter" && runQuickSerp()} - className="bg-[#0d0d18] border-[#2e2e3e] text-white placeholder:text-gray-600 focus:border-purple-500" + className="text-white placeholder:text-gray-600" + style={{ background: "#080f13", borderColor: "rgba(255,255,255,0.08)" }} />
-
)} - +
{/* Filter Bar */} - +
{/* Search */}
- + setSearch(e.target.value)} placeholder="Domain, Firma, Name, E-Mail suchen..." - className="w-full bg-[#0d0d18] border border-[#2e2e3e] rounded-lg pl-8 pr-3 py-1.5 text-sm text-white outline-none focus:border-purple-500" + className="w-full rounded-lg pl-8 pr-3 py-1.5 text-sm text-white outline-none" + style={{ background: "#080f13", border: "1px solid rgba(255,255,255,0.08)" }} />
{/* Has Email toggle */} -
+
{[["", "Alle"], ["yes", "Mit E-Mail"], ["no", "Ohne E-Mail"]].map(([v, l]) => ( ))} @@ -696,11 +706,10 @@ export default function LeadVaultPage() { {/* Kontaktiert toggle */} @@ -708,11 +717,10 @@ export default function LeadVaultPage() { {/* Favoriten toggle */} @@ -720,55 +728,58 @@ export default function LeadVaultPage() { {/* Clear + count */}
{(search || filterHasEmail || filterContacted || filterFavorite || filterSearchTerms.length) && ( - )} - {total.toLocaleString()} Leads + {total.toLocaleString()} Leads
- +
{/* Bulk Actions */} {selected.size > 0 && ( -
- {selected.size} ausgewählt +
+ {selected.size} ausgewählt
setBulkTag(e.target.value)} placeholder="Tag hinzufügen..." - className="bg-[#1a1a28] border border-[#2e2e3e] text-gray-300 text-xs rounded px-2 py-1 w-32 outline-none" + className="text-gray-300 text-xs rounded px-2 py-1 w-32 outline-none" + style={{ background: "#1a2025", border: "1px solid rgba(255,255,255,0.08)" }} />
- +
)} {/* Table */} - +
- + @@ -805,7 +816,7 @@ export default function LeadVaultPage() { : "Noch keine Leads. Pipeline ausführen oder Quick SERP nutzen."}

{search && ( - + )} @@ -821,9 +832,13 @@ export default function LeadVaultPage() { setPanelLead(lead)} - className={`border-b border-[#1a1a28] cursor-pointer transition-colors hover:bg-[#1a1a28] ${ - isSelected ? "bg-[#1a1a2e]" : i % 2 === 0 ? "bg-[#111118]" : "bg-[#0f0f16]" - }`} + className="cursor-pointer transition-colors" + style={{ + borderBottom: "1px solid rgba(255,255,255,0.03)", + background: isSelected ? "rgba(173,199,255,0.07)" : i % 2 === 0 ? "#161c21" : "#1a2025", + }} + onMouseEnter={e => { if (!isSelected) e.currentTarget.style.background = "#242b30"; }} + onMouseLeave={e => { if (!isSelected) e.currentTarget.style.background = i % 2 === 0 ? "#161c21" : "#1a2025"; }} > @@ -883,11 +898,10 @@ export default function LeadVaultPage() { {lead.sourceTerm ? ( {Array.from({ length: Math.min(7, pages) }, (_, i) => { @@ -1033,20 +1049,24 @@ export default function LeadVaultPage() { } return ( ); })} - Seite {page} von {pages} + Seite {page} von {pages} )} - + {/* Side Panel */} {panelLead && ( diff --git a/app/suche/page.tsx b/app/suche/page.tsx index 1cae768..ddc338b 100644 --- a/app/suche/page.tsx +++ b/app/suche/page.tsx @@ -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((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((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(); + 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(); + 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 ( -
- {/* Hero */} -
-
-
-
- - Lead-Suche -
-
-
-

- Leads finden -

-

- Suchbegriff eingeben — wir finden passende Unternehmen mit Kontaktdaten. +

+ + + {/* ── Strategische Kampagnen ── */} +
+

+ Strategische Energiewirtschafts-Kampagnen +

+
+ {/* CTA Card — Stadtwerke */} +
+
+
+

Hauptkampagne

+

+ Bundesweite
Lead-Gewinnung +

+

+ Startet sofort den Deep-Scrape aller Stadtwerke in allen deutschen Bundesländern.

+ {queueRunning ? ( +
+ + + +
+
{queueLabel || "Wird durchsucht…"} ({queueIndex}/{queueTotal})
+
Leads werden automatisch gespeichert
+
+
+ ) : ( + + )} +
+
+ + {/* CTA Card — Energieversorger */} +
+
+
+

Energieversorger-Kampagne

+

+ Industrie &
Energieversorger +

+

+ Sucht nach Netzbetreiber, Fernwärme und Industriepark — priorisiert BW & Bayern, dann alle weiteren Bundesländer. +

+ {industrieRunning ? ( +
+ + + +
+
{industrieLabel || "Wird durchsucht…"} ({industrieIndex}/{industrieTotal})
+
Leads werden automatisch gespeichert
+
+
+ ) : ( + + )}
-
-
+
- {/* Mode Tabs */} -
- {([ - { 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 => ( - - ))} -
- - {/* Stadtwerke Queue Button */} - {searchMode === "stadtwerke" && !loading && !queueRunning && ( - - )} - - {/* Queue running indicator */} - {queueRunning && ( -
- - - + {/* ── Manuelle Suche ── */} +
+
-
Bundesland {queueIndex} von {queueTotal} wird durchsucht…
-
Nicht schließen — Leads werden automatisch gespeichert
+

+ Manuelle Lead- & Nischensuche +

+

Verfeinern Sie Ihre Zielparameter für eine hochspezifische Extraktion.

+
+ +
+ +
+ {/* Form fields */} +
+
+ +
+ 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"; }} + /> + search +
+
+
+ +
+ 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"; }} + /> + location_on +
+
+
+ + +
+
+ +
+
+ + {/* Quick Presets */} +
+

Quick Presets

+
+ {["Stadtwerke", "Energieversorger", "Industrie-Energie", "Gemeindewerke", "Netzbetreiber"].map(preset => ( + + ))} +
- )} - - - {/* Search Card */} - +
{/* Loading Card */} @@ -314,7 +527,7 @@ export default function SuchePage() { {/* AI Modal */} {aiOpen && ( { 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 (
-
- {/* 5-pointed star SVG */} - - - -
- OnyvaLeads -
- ); -} - export function Topbar() { const pathname = usePathname(); const [newLeadsCount, setNewLeadsCount] = useState(0); @@ -48,90 +20,65 @@ export function Topbar() { return () => clearInterval(t); }, []); - const tabs = [ - { href: "/suche", label: "Suche" }, - { href: "/leadspeicher", label: "Leadspeicher" }, - ]; + const isSearch = pathname === "/suche" || pathname.startsWith("/suche/"); + const isLeadspeicher = pathname === "/leadspeicher" || pathname.startsWith("/leadspeicher/"); return ( -
- {/* Logo */} - - - - - {/* Tab switcher */} -
- {tabs.map((tab) => { - const isActive = pathname === tab.href || pathname.startsWith(tab.href + "/"); - return ( - - {tab.label} - {tab.href === "/leadspeicher" && newLeadsCount > 0 && ( - - {newLeadsCount > 99 ? "99+" : newLeadsCount} - - )} - - ); - })} + {/* Left: Logo + Nav */} +
+ + + mein-solar | Lead Finder + + +
+
); } diff --git a/components/search/AiSearchModal.tsx b/components/search/AiSearchModal.tsx index 39ad6b7..7d4a666 100644 --- a/components/search/AiSearchModal.tsx +++ b/components/search/AiSearchModal.tsx @@ -121,7 +121,7 @@ export function AiSearchModal({ onStart, onClose, searchMode = "custom" }: AiSea onBlur={e => { e.currentTarget.style.borderColor = "#1e1e2e"; }} />

- Tipp: Branche, Region, Firmengröße und gewünschten Entscheidungsträger erwähnen · Strg+Enter zum Generieren + Tipp: Branche, Region, Firmengröße und gewünschten Entscheidungsträger erwähnen

diff --git a/lib/data/stadtwerke-cities.ts b/lib/data/stadtwerke-cities.ts new file mode 100644 index 0000000..75c3649 --- /dev/null +++ b/lib/data/stadtwerke-cities.ts @@ -0,0 +1,161 @@ +// Größte Städte je Bundesland, sortiert nach Einwohnerzahl (absteigend) +// Quelle: Destatis / Wikipedia, Stand 2024 + +export const STADTWERKE_CITIES: Record = { + Bayern: [ + "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", + ], + "Nordrhein-Westfalen": [ + "Köln", "Düsseldorf", "Dortmund", "Essen", "Duisburg", + "Bochum", "Wuppertal", "Bielefeld", "Bonn", "Münster", + "Gelsenkirchen", "Aachen", "Mönchengladbach", "Krefeld", "Oberhausen", + "Hagen", "Hamm", "Solingen", "Leverkusen", "Osnabrück", + "Herne", "Neuss", "Paderborn", "Gütersloh", "Recklinghausen", + "Mülheim", "Siegen", "Bergisch Gladbach", "Witten", "Bottrop", + "Heiligenhaus", "Velbert", "Troisdorf", "Moers", "Iserlohn", + "Lünen", "Detmold", "Remscheid", "Castrop-Rauxel", "Minden", + "Lippstadt", "Herford", "Viersen", "Düren", "Marl", + "Dinslaken", "Dormagen", "Ratingen", "Wesel", "Gladbeck", + ], + "Baden-Württemberg": [ + "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", + ], + Hessen: [ + "Frankfurt", "Wiesbaden", "Kassel", "Darmstadt", "Offenbach", + "Hanau", "Marburg", "Gießen", "Fulda", "Wetzlar", + "Rüsselsheim", "Langen", "Bad Homburg", "Dreieich", "Viernheim", + "Maintal", "Friedberg", "Bensheim", "Rodgau", "Eschborn", + "Limburg", "Hofheim", "Bad Nauheim", "Gelnhausen", "Herborn", + "Mörfelden-Walldorf", "Heppenheim", "Seligenstadt", "Bruchköbel", "Büdingen", + "Korbach", "Mühlheim", "Neu-Isenburg", "Oberursel", "Königstein", + "Seligenstädt", "Lampertheim", "Bad Hersfeld", "Groß-Gerau", "Lauterbach", + "Riedstadt", "Baunatal", "Taunusstein", "Bebra", "Schlüchtern", + "Dillenburg", "Alsfeld", "Bad Vilbel", "Griesheim", "Hünfeld", + ], + Niedersachsen: [ + "Hannover", "Braunschweig", "Osnabrück", "Oldenburg", "Wolfsburg", + "Göttingen", "Salzgitter", "Hildesheim", "Delmenhorst", "Wilhelmshaven", + "Celle", "Lüneburg", "Wolfenbüttel", "Garbsen", "Hameln", + "Lingen", "Langenhagen", "Peine", "Cuxhaven", "Emden", + "Nordhorn", "Goslar", "Stade", "Rheine", "Leer", + "Buxtehude", "Hameln", "Alfeld", "Rotenburg", "Achim", + "Winsen", "Buchholz", "Sarstedt", "Bad Salzdetfurth", "Seelze", + "Wunstorf", "Nienburg", "Uelzen", "Holzminden", "Osterode", + "Clausthal-Zellerfeld", "Bückeburg", "Springe", "Hemmingen", "Isernhagen", + "Ganderkesee", "Papenburg", "Meppen", "Gifhorn", "Schöningen", + ], + Sachsen: [ + "Leipzig", "Dresden", "Chemnitz", "Zwickau", "Erfurt", + "Plauen", "Görlitz", "Hoyerswerda", "Bautzen", "Gera", + "Zittau", "Freiberg", "Riesa", "Pirna", "Döbeln", + "Freital", "Mittweida", "Meißen", "Werdau", "Crimmitschau", + "Annaberg-Buchholz", "Stollberg", "Torgau", "Oelsnitz", "Aue-Bad Schlema", + "Limbach-Oberfrohna", "Borna", "Glauchau", "Delitzsch", "Coswig", + "Radebeul", "Weißwasser", "Grimma", "Meerane", "Frankenberg", + "Wittenberg", "Zschopau", "Reichenbach", "Marienberg", "Auerbach", + "Großenhain", "Lößnitz", "Hohenstein-Ernstthal", "Schneeberg", "Flöha", + "Eilenburg", "Geithain", "Brand-Erbisdorf", "Lugau", "Radeberg", + ], + Berlin: [ + "Berlin-Mitte", "Berlin-Charlottenburg", "Berlin-Spandau", "Berlin-Steglitz", + "Berlin-Tempelhof", "Berlin-Schöneberg", "Berlin-Kreuzberg", "Berlin-Prenzlauer Berg", + "Berlin-Friedrichshain", "Berlin-Lichtenberg", "Berlin-Hohenschönhausen", + "Berlin-Reinickendorf", "Berlin-Wedding", "Berlin-Neukölln", "Berlin-Treptow", + "Berlin-Köpenick", "Berlin-Wilmersdorf", "Berlin-Zehlendorf", "Berlin-Pankow", + "Berlin-Weißensee", "Berlin-Hellersdorf", "Berlin-Marzahn", "Berlin-Adlershof", + "Berlin-Buch", "Berlin-Mahlsdorf", + ], + Hamburg: [ + "Hamburg-Mitte", "Hamburg-Altona", "Hamburg-Eimsbüttel", "Hamburg-Nord", + "Hamburg-Wandsbek", "Hamburg-Bergedorf", "Hamburg-Harburg", + "Norderstedt", "Ahrensburg", "Reinbek", "Glinde", "Bargteheide", + "Bad Oldesloe", "Elmshorn", "Pinneberg", "Wedel", "Geesthacht", + "Lauenburg", "Buchholz", "Buxtehude", "Stade", "Winsen", + "Heide", "Itzehoe", "Bad Segeberg", + ], + Bremen: [ + "Bremen", "Bremerhaven", "Delmenhorst", "Achim", "Syke", + "Lilienthal", "Stuhr", "Weyhe", "Bassum", "Schwanewede", + ], + Thüringen: [ + "Erfurt", "Jena", "Gera", "Weimar", "Gotha", + "Nordhausen", "Suhl", "Ilmenau", "Eisenach", "Altenburg", + "Mühlhausen", "Sonneberg", "Sömmerda", "Saalfeld", "Bad Langensalza", + "Pößneck", "Apolda", "Arnstadt", "Greiz", "Schmalkalden", + "Hildburghausen", "Rudolstadt", "Zeulenroda", "Leinefelde-Worbis", "Bad Salzungen", + "Meiningen", "Sonneberg", "Schleiz", "Neustadt an der Orla", "Eisenberg", + "Lobenstein", "Sondershausen", "Bleicherode", "Heilbad Heiligenstadt", "Dingelstädt", + ], + "Sachsen-Anhalt": [ + "Halle", "Magdeburg", "Dessau-Roßlau", "Wittenberg", "Halle-Neustadt", + "Halberstadt", "Stendal", "Quedlinburg", "Bitterfeld-Wolfen", "Merseburg", + "Bernburg", "Köthen", "Weißenfels", "Zeitz", "Naumburg", + "Sangerhausen", "Aschersleben", "Staßfurt", "Burg", "Gardelegen", + "Wernigerode", "Schönebeck", "Blauen", "Eisleben", "Wolfen", + "Zerbst", "Calbe", "Tangermünde", "Wanzleben", "Klötze", + ], + Brandenburg: [ + "Potsdam", "Cottbus", "Brandenburg an der Havel", "Frankfurt (Oder)", "Oranienburg", + "Eberswalde", "Bernau", "Neuruppin", "Schwedt", "Falkensee", + "Strausberg", "Eisenhüttenstadt", "Ludwigsfelde", "Werder", "Königs Wusterhausen", + "Prenzlau", "Nauen", "Luckenwalde", "Senftenberg", "Spremberg", + "Forst", "Guben", "Neuenhagen", "Templin", "Rathenow", + "Finowfurt", "Bad Belzig", "Jüterbog", "Zossen", "Zehdenick", + ], + "Mecklenburg-Vorpommern": [ + "Rostock", "Schwerin", "Neubrandenburg", "Stralsund", "Greifswald", + "Wismar", "Güstrow", "Neustadt-Glewe", "Waren", "Bergen auf Rügen", + "Ribnitz-Damgarten", "Ueckermünde", "Wolgast", "Anklam", "Parchim", + "Hagenow", "Neustrelitz", "Teterow", "Pasewalk", "Demmin", + "Sassnitz", "Lüdershagen", "Ludwigslust", "Malchin", "Stavenhagen", + ], + Saarland: [ + "Saarbrücken", "Neunkirchen", "Saarlouis", "Sankt Ingbert", "Homburg", + "Völklingen", "Merzig", "Dillingen", "Sulzbach", "Überherrn", + "Saarwellingen", "Bexbach", "Püttlingen", "Friedrichsthal", "Blieskastel", + "Sankt Wendel", "Lebach", "Ottweiler", "Wadern", "Losheim", + ], + "Rheinland-Pfalz": [ + "Mainz", "Ludwigshafen", "Koblenz", "Trier", "Kaiserslautern", + "Worms", "Neustadt", "Bad Kreuznach", "Pirmasens", "Andernach", + "Speyer", "Zweibrücken", "Frankenthal", "Bingen", "Neuwied", + "Idar-Oberstein", "Landau", "Ingelheim", "Mayen", "Remagen", + "Konz", "Simmern", "Montabaur", "Kusel", "Birkenfeld", + "Cochem", "Lahnstein", "Alzey", "Bad Dürkheim", "Germersheim", + "Grünstadt", "Winnweiler", "Daun", "Gerolstein", "Linz", + "Bendorf", "Nastätten", "Simmern", "Kirchheimbolanden", "Rockenhausen", + ], + "Schleswig-Holstein": [ + "Kiel", "Lübeck", "Flensburg", "Neumünster", "Norderstedt", + "Elmshorn", "Pinneberg", "Itzehoe", "Schleswig", "Heide", + "Bad Oldesloe", "Wedel", "Reinbek", "Ahrensburg", "Bargteheide", + "Kaltenkirchen", "Quickborn", "Büdelsdorf", "Rendsburg", "Brunsbüttel", + "Husum", "Niebüll", "Eckernförde", "Neustadt", "Bad Segeberg", + "Preetz", "Eutin", "Mölln", "Ratzeburg", "Geesthacht", + "Glinde", "Wahlstedt", "Bad Schwartau", "Lütjenburg", "Plön", + ], +}; + +// Flache Liste aller Städte +export const ALL_CITIES = Object.values(STADTWERKE_CITIES).flat(); + +// Alle Bundesländer +export const BUNDESLAENDER = Object.keys(STADTWERKE_CITIES);
e.stopPropagation()}>