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:
parent
6317d89be0
commit
ef574be75f
@ -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">
|
||||
|
||||
@ -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>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user