Initial commit: CI Agent competitive-intelligence monitoring app
FastAPI + Celery + Next.js + Postgres/Redis app with company monitoring, source collection, LLM-based change analysis, enrichment, and account security (Turnstile, escalating lockout, email verification).
This commit is contained in:
@@ -0,0 +1,162 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { useState } from "react";
|
||||
import { Building2, Pause, Play, Plus, RefreshCw, Trash2 } from "lucide-react";
|
||||
import { CompanyStatusBadge } from "@/components/ui/badge";
|
||||
import { useCompanies, useDeleteCompany, useSetCompanyPaused } from "@/hooks/use-companies";
|
||||
import { useRunAnyCompanyNow } from "@/hooks/use-monitoring-runs";
|
||||
import { FREQUENCY_LABELS } from "@/lib/types";
|
||||
import { formatDateTime } from "@/lib/format";
|
||||
|
||||
export default function CompaniesPage() {
|
||||
const { data: companies, isLoading } = useCompanies();
|
||||
const setPaused = useSetCompanyPaused();
|
||||
const deleteCompany = useDeleteCompany();
|
||||
const runNow = useRunAnyCompanyNow();
|
||||
const [pendingDeleteId, setPendingDeleteId] = useState<string | null>(null);
|
||||
|
||||
const companyList = companies ?? [];
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-2xl font-semibold text-slate-900">Companies</h1>
|
||||
<p className="mt-1 text-sm text-slate-600">
|
||||
Everything you're currently monitoring.
|
||||
</p>
|
||||
</div>
|
||||
<Link
|
||||
href="/companies/new"
|
||||
className="focus-ring inline-flex items-center gap-2 rounded-md bg-brand-600 px-4 py-2 text-sm font-semibold text-white hover:bg-brand-700"
|
||||
>
|
||||
<Plus className="h-4 w-4" aria-hidden /> Add company
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{isLoading ? (
|
||||
<p className="mt-8 text-sm text-slate-500">Loading…</p>
|
||||
) : companyList.length === 0 ? (
|
||||
<div className="mt-8 rounded-lg border border-dashed border-slate-300 bg-white p-10 text-center">
|
||||
<Building2 className="mx-auto h-8 w-8 text-slate-400" aria-hidden />
|
||||
<p className="mt-3 text-sm text-slate-600">No companies yet.</p>
|
||||
<Link
|
||||
href="/companies/new"
|
||||
className="focus-ring mt-4 inline-block rounded-md bg-brand-600 px-4 py-2 text-sm font-semibold text-white hover:bg-brand-700"
|
||||
>
|
||||
Add your first company
|
||||
</Link>
|
||||
</div>
|
||||
) : (
|
||||
<div className="mt-6 overflow-x-auto rounded-lg border border-slate-200 bg-white">
|
||||
<table className="w-full min-w-[720px] text-left text-sm">
|
||||
<thead className="border-b border-slate-200 bg-slate-50 text-xs uppercase tracking-wide text-slate-500">
|
||||
<tr>
|
||||
<th className="px-4 py-3">Company</th>
|
||||
<th className="px-4 py-3">Status</th>
|
||||
<th className="px-4 py-3">Frequency</th>
|
||||
<th className="px-4 py-3">Last checked</th>
|
||||
<th className="px-4 py-3">Next check</th>
|
||||
<th className="px-4 py-3">Alerts</th>
|
||||
<th className="px-4 py-3 text-right">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-slate-100">
|
||||
{companyList.map((company) => (
|
||||
<tr key={company.id}>
|
||||
<td className="px-4 py-3">
|
||||
<Link
|
||||
href={`/companies/${company.id}`}
|
||||
className="font-medium text-slate-900 hover:text-brand-600"
|
||||
>
|
||||
{company.name}
|
||||
</Link>
|
||||
{company.official_website && (
|
||||
<div className="text-xs text-slate-500">{company.official_website}</div>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
<CompanyStatusBadge status={company.status} />
|
||||
</td>
|
||||
<td className="px-4 py-3 text-slate-600">
|
||||
{company.monitor_configuration
|
||||
? FREQUENCY_LABELS[company.monitor_configuration.frequency_type]
|
||||
: "—"}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-slate-600">
|
||||
{formatDateTime(company.monitor_configuration?.last_run)}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-slate-600">
|
||||
{formatDateTime(company.monitor_configuration?.next_run)}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-slate-600">{company.unresolved_alert_count}</td>
|
||||
<td className="px-4 py-3">
|
||||
<div className="flex items-center justify-end gap-1">
|
||||
<button
|
||||
title={
|
||||
company.status === "active" ? "Pause monitoring" : "Resume monitoring"
|
||||
}
|
||||
onClick={() =>
|
||||
setPaused.mutate({
|
||||
companyId: company.id,
|
||||
paused: company.status === "active",
|
||||
})
|
||||
}
|
||||
className="focus-ring rounded-md p-2 text-slate-500 hover:bg-slate-100"
|
||||
>
|
||||
{company.status === "active" ? (
|
||||
<Pause className="h-4 w-4" aria-hidden />
|
||||
) : (
|
||||
<Play className="h-4 w-4" aria-hidden />
|
||||
)}
|
||||
<span className="sr-only">
|
||||
{company.status === "active" ? "Pause" : "Resume"} {company.name}
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
title="Run now"
|
||||
onClick={() => runNow.mutate(company.id)}
|
||||
disabled={runNow.isPending}
|
||||
className="focus-ring rounded-md p-2 text-slate-500 hover:bg-slate-100 disabled:opacity-60"
|
||||
>
|
||||
<RefreshCw className="h-4 w-4" aria-hidden />
|
||||
<span className="sr-only">Run now for {company.name}</span>
|
||||
</button>
|
||||
{pendingDeleteId === company.id ? (
|
||||
<div className="flex items-center gap-1 text-xs">
|
||||
<span className="text-slate-600">Delete?</span>
|
||||
<button
|
||||
onClick={() => deleteCompany.mutate(company.id)}
|
||||
className="focus-ring rounded-md bg-red-600 px-2 py-1 font-semibold text-white hover:bg-red-700"
|
||||
>
|
||||
Yes
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setPendingDeleteId(null)}
|
||||
className="focus-ring rounded-md px-2 py-1 text-slate-600 hover:bg-slate-100"
|
||||
>
|
||||
No
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<button
|
||||
title="Delete company"
|
||||
onClick={() => setPendingDeleteId(company.id)}
|
||||
className="focus-ring rounded-md p-2 text-slate-500 hover:bg-red-50 hover:text-red-600"
|
||||
>
|
||||
<Trash2 className="h-4 w-4" aria-hidden />
|
||||
<span className="sr-only">Delete {company.name}</span>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user