From ef574be75fdd14e83729820dda4fc2175e25565b Mon Sep 17 00:00:00 2001 From: metatroncubeswdev Date: Wed, 28 Oct 2026 00:26:36 -0400 Subject: [PATCH] Client page: Competitor analysis tab with its reports and a Find competitors button Co-Authored-By: Claude Opus 5.5 --- dashboard/web/src/pages/Client.jsx | 44 ++++++++++++++++++++++--- dashboard/web/src/pages/Competitors.jsx | 2 +- 2 files changed, 41 insertions(+), 5 deletions(-) diff --git a/dashboard/web/src/pages/Client.jsx b/dashboard/web/src/pages/Client.jsx index e0ae0ef..5a77355 100644 --- a/dashboard/web/src/pages/Client.jsx +++ b/dashboard/web/src/pages/Client.jsx @@ -101,7 +101,8 @@ export default function Client() { onChange={setTab} tabs={[ { value: "performance", label: "Performance" }, - { value: "docs", label: "Documents", count: data.docs.filter((d) => d.kind !== "setup").length }, + { value: "docs", label: "Documents", count: data.docs.filter((d) => d.kind !== "setup" && d.kind !== "competitors").length }, + { value: "competitors", label: "Competitor analysis", count: data.docs.filter((d) => d.kind === "competitors").length }, { value: "runs", label: "AI runs", count: data.jobs.length }, { value: "shots", label: "Screenshots" }, { value: "brand", label: "Brand kit" }, @@ -120,7 +121,8 @@ export default function Client() { ))} - {tab === "docs" &&
} + {tab === "docs" &&
d.kind !== "competitors")} reload={load} openPath={params.get("doc")} />
} + {tab === "competitors" && d.kind === "competitors")} reload={load} openPath={params.get("doc")} onStarted={(job) => nav(`/runs/${job.id}`)} />} {tab === "runs" && } {tab === "shots" && } {tab === "brand" && } @@ -390,7 +392,41 @@ function FormatBars({ d }) { // ---------------------------------------------------------------- documents const KIND_LABEL = { monthly: "Monthly reports", audit: "Audits", competitors: "Competitor analysis", trends: "Trend scans", report: "Weekly reports", calendar: "Content calendars", "profile-check": "Profile checks", setup: "Client setup files" }; -function Documents({ slug, docs, reload, openPath }) { +function CompetitorReports({ slug, docs, reload, openPath, onStarted }) { + const [busy, setBusy] = useState(false); + const [error, setError] = useState(null); + const scan = async () => { + setBusy(true); + setError(null); + try { + onStarted((await api("/api/jobs", { method: "POST", body: { type: "competitor-scan", slug } })).job); + } catch (e) { + setError(e); + setBusy(false); + } + }; + return ( +
+ + + +
} + > +

+ Each run finds this client's local rivals and niche benchmarks, checks them on Instagram and Google, and rewrites this month's report. + Add or dismiss the accounts it suggests under Tracked accounts. +

+ + + Click Find competitors to create the first one.} /> + + ); +} + +function Documents({ slug, docs, reload, openPath, empty }) { const [preview, setPreview] = useState(null); const [busy, setBusy] = useState(null); const [error, setError] = useState(null); @@ -424,7 +460,7 @@ function Documents({ slug, docs, reload, openPath }) { }; const groups = Object.keys(KIND_LABEL).map((k) => [k, docs.filter((d) => d.kind === k)]).filter(([, list]) => list.length); - if (!docs.length) return Run an account audit to create the first ones.; + if (!docs.length) return {empty || Run an account audit to create the first ones.}; return (
diff --git a/dashboard/web/src/pages/Competitors.jsx b/dashboard/web/src/pages/Competitors.jsx index e8afe49..8ab17e7 100644 --- a/dashboard/web/src/pages/Competitors.jsx +++ b/dashboard/web/src/pages/Competitors.jsx @@ -199,7 +199,7 @@ export default function Competitors() { - {data?.report && } + {data?.report && } {data?.competitors.length > 0 && }