import { useCallback, useEffect, useState } from "react"; import { useNavigate } from "react-router-dom"; import { Plus, RefreshCw, AlertTriangle, CircleAlert, Info, Link2Off } from "lucide-react"; import { api } from "../api.js"; import { useAuth } from "../App.jsx"; import { Badge, Button, cx, Empty, ErrorNote, fmt, fmtDate, Panel, Spark, Spinner, Stat, StatCell, StatRow, timeAgo } from "../ui.jsx"; import AddClientDialog from "./AddClientDialog.jsx"; import GettingStarted from "../GettingStarted.jsx"; const LEVEL = { bad: { icon: CircleAlert, cls: "text-bad" }, warn: { icon: AlertTriangle, cls: "text-warn" }, info: { icon: Info, cls: "text-brand" }, }; export default function Overview() { const { isAdmin } = useAuth(); const nav = useNavigate(); const [data, setData] = useState(null); const [error, setError] = useState(null); const [refreshing, setRefreshing] = useState(false); const [adding, setAdding] = useState(false); const load = useCallback(() => api("/api/overview").then(setData).catch(setError), []); useEffect(() => { load(); }, [load]); // While any client is refreshing, poll so numbers fill in as they arrive. const anyRefreshing = refreshing || data?.clients.some((c) => c.refreshing); useEffect(() => { if (!anyRefreshing) return; const t = setInterval(load, 6000); return () => clearInterval(t); }, [anyRefreshing, load]); const refreshAll = async () => { setRefreshing(true); await api("/api/refresh-all", { method: "POST" }).catch(setError); setTimeout(load, 1500); setTimeout(() => setRefreshing(false), 20000); }; if (!data && !error) return ; const t = data?.totals; const oldest = data?.clients.filter((c) => c.fetchedAt).map((c) => c.fetchedAt).sort()[0]; return (

Clients

Instagram and Facebook, last 28 days{oldest ? ` · numbers as of ${timeAgo(oldest)}` : ""}

{isAdmin && ( )}
{t && ( )}
{data?.clients.length ? ( {data.clients.map((c) => ( nav(`/clients/${c.slug}`)}> ))}
Client Followers Reached Posts Last post Reach per post Last audit
{c.name}
{c.linked ? (c.igUsername ? `@${c.igUsername}` : "Facebook only") : ( not linked )}
{fmt(c.igFollowers)} {fmt(c.igReach28)} {fmt(c.postsLast28)} {c.refreshing ? ( updating… ) : c.daysSinceLastPost == null ? ( — ) : ( = 14 ? "text-bad font-medium" : c.daysSinceLastPost >= 7 ? "text-warn font-medium" : "text-ink-2")}> {c.daysSinceLastPost === 0 ? "today" : `${c.daysSinceLastPost}d ago`} )} {c.lastAuditAt ? fmtDate(c.lastAuditAt) : none}
) : ( setAdding(true)}>Add business}> Connect a Facebook Page to the agency's Meta account, then add it here. )} {data?.clients.some((c) => !c.fetchedAt && c.linked) && (
Some clients haven't been fetched yet. Use Refresh numbers; each account takes about a minute.
)}
{data?.attention.length ? (
    {data.attention.map((a, i) => { const L = LEVEL[a.level] || LEVEL.info; return (
  • ); })}
) : ( Nothing needs attention right now. )}
setAdding(false)} onAdded={(c) => nav(`/clients/${c.slug}`)} />
); }