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} onChange={setTab}
tabs={[ tabs={[
{ value: "performance", label: "Performance" }, { 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: "runs", label: "AI runs", count: data.jobs.length },
{ value: "shots", label: "Screenshots" }, { value: "shots", label: "Screenshots" },
{ value: "brand", label: "Brand kit" }, { value: "brand", label: "Brand kit" },
@ -120,7 +121,8 @@ export default function Client() {
</Empty> </Empty>
</Panel> </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 === "runs" && <ClientRuns jobs={data.jobs} />}
{tab === "shots" && <Screenshots slug={slug} clientName={client.name} />} {tab === "shots" && <Screenshots slug={slug} clientName={client.name} />}
{tab === "brand" && <BrandKit slug={slug} />} {tab === "brand" && <BrandKit slug={slug} />}
@ -390,7 +392,41 @@ function FormatBars({ d }) {
// ---------------------------------------------------------------- documents // ---------------------------------------------------------------- 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" }; 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 [preview, setPreview] = useState(null);
const [busy, setBusy] = useState(null); const [busy, setBusy] = useState(null);
const [error, setError] = 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); 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 ( return (
<div className="space-y-5"> <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 })}> <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>)} {clients.map((c) => <option key={c.slug} value={c.slug}>{c.name}</option>)}
</select> </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>} {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 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> <Button variant="primary" icon={Plus} onClick={() => setAdding(true)} disabled={data?.competitors.length >= 10}>Add competitor</Button>