Client page: Competitor analysis tab with its reports and a Find competitors button

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
metatroncubeswdev 2026-10-28 00:26:36 -04:00
parent 6317d89be0
commit ef574be75f
2 changed files with 41 additions and 5 deletions

View File

@ -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() {
</Empty>
</Panel>
))}
{tab === "docs" && <div className="space-y-5"><MonthlyReport slug={slug} onBuilt={load} /><Documents slug={slug} docs={data.docs} reload={load} openPath={params.get("doc")} /></div>}
{tab === "docs" && <div className="space-y-5"><MonthlyReport slug={slug} onBuilt={load} /><Documents slug={slug} docs={data.docs.filter((d) => d.kind !== "competitors")} reload={load} openPath={params.get("doc")} /></div>}
{tab === "competitors" && <CompetitorReports slug={slug} docs={data.docs.filter((d) => d.kind === "competitors")} reload={load} openPath={params.get("doc")} onStarted={(job) => nav(`/runs/${job.id}`)} />}
{tab === "runs" && <ClientRuns jobs={data.jobs} />}
{tab === "shots" && <Screenshots slug={slug} clientName={client.name} />}
{tab === "brand" && <BrandKit slug={slug} />}
@ -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 (
<div className="space-y-5">
<Panel
title="Competitor analysis"
action={<div className="flex flex-wrap gap-2">
<Link to={`/competitors?client=${encodeURIComponent(slug)}`}><Button icon={ArrowUpRight}>Tracked accounts</Button></Link>
<Button variant="primary" icon={Sparkles} loading={busy} onClick={scan}>Find competitors</Button>
</div>}
>
<p className="text-[13px] text-muted">
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.
</p>
<ErrorNote error={error} />
</Panel>
<Documents slug={slug} docs={docs} reload={reload} openPath={openPath}
empty={<Empty icon={FileBarChart} title="No competitor reports yet">Click Find competitors to create the first one.</Empty>} />
</div>
);
}
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 <Panel><Empty icon={FileText} title="No documents yet">Run an account audit to create the first ones.</Empty></Panel>;
if (!docs.length) return <Panel>{empty || <Empty icon={FileText} title="No documents yet">Run an account audit to create the first ones.</Empty>}</Panel>;
return (
<div className="space-y-5">

View File

@ -199,7 +199,7 @@ export default function Competitors() {
<select id="comp-client" aria-label="Client" className="input h-9 w-auto" value={client} onChange={(e) => setParams({ client: e.target.value })}>
{clients.map((c) => <option key={c.slug} value={c.slug}>{c.name}</option>)}
</select>
{data?.report && <Button icon={FileBarChart} title={`Competitor report, updated ${fmtDate(data.report.updatedAt)}`} onClick={() => navigate(`/clients/${client}?tab=docs&doc=${encodeURIComponent(data.report.path)}`)}>Open report</Button>}
{data?.report && <Button icon={FileBarChart} title={`Competitor report, updated ${fmtDate(data.report.updatedAt)}`} onClick={() => navigate(`/clients/${client}?tab=competitors&doc=${encodeURIComponent(data.report.path)}`)}>Open report</Button>}
{data?.competitors.length > 0 && <Button icon={RefreshCw} loading={busy === "refresh"} onClick={refresh}>Refresh</Button>}
<Button icon={Sparkles} loading={busy === "scan"} onClick={scan} title="AI run: searches the market, checks each account on Instagram and writes a competitor report">Find competitors</Button>
<Button variant="primary" icon={Plus} onClick={() => setAdding(true)} disabled={data?.competitors.length >= 10}>Add competitor</Button>