192 lines
6.2 KiB
TypeScript
192 lines
6.2 KiB
TypeScript
import { useEffect, useState } from "react";
|
|
import { Link, useNavigate } from "react-router-dom";
|
|
import type { ApiSettings } from "../../types";
|
|
import Card from "../../components/Card";
|
|
import Button from "../../components/Button";
|
|
import StatusBadge from "../../components/StatusBadge";
|
|
import PageTitle from "../../components/PageTitle";
|
|
import LoadingFrame from "../../components/LoadingFrame";
|
|
import DismissibleAlert from "../../components/DismissibleAlert";
|
|
import DataGrid, { type DataGridColumn } from "../../components/table/DataGrid";
|
|
import { createNewCampaign, listCampaigns } from "../../api/campaigns";
|
|
import type { CampaignListItem } from "../../types";
|
|
|
|
export default function CampaignListPage({ settings }: { settings: ApiSettings }) {
|
|
const navigate = useNavigate();
|
|
const [campaigns, setCampaigns] = useState<CampaignListItem[]>([]);
|
|
const [error, setError] = useState<string>("");
|
|
const [loading, setLoading] = useState(false);
|
|
const [creating, setCreating] = useState(false);
|
|
const [lastLoadedAt, setLastLoadedAt] = useState<string>("");
|
|
|
|
async function load() {
|
|
setLoading(true);
|
|
setError("");
|
|
try {
|
|
const data = await listCampaigns(settings);
|
|
setCampaigns(data);
|
|
setLastLoadedAt(formatLoadedAt(new Date()));
|
|
} catch (err) {
|
|
setError(err instanceof Error ? err.message : String(err));
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}
|
|
|
|
async function create() {
|
|
setCreating(true);
|
|
setError("");
|
|
try {
|
|
const created = await createNewCampaign(settings);
|
|
navigate(`/campaigns/${created.campaign.id}/wizard/create`);
|
|
} catch (err) {
|
|
setError(err instanceof Error ? err.message : String(err));
|
|
} finally {
|
|
setCreating(false);
|
|
}
|
|
}
|
|
|
|
useEffect(() => {
|
|
load();
|
|
}, [settings.apiBaseUrl, settings.apiKey, settings.accessToken]);
|
|
|
|
const columns: DataGridColumn<CampaignListItem>[] = [
|
|
{
|
|
id: "campaign",
|
|
header: "Campaign",
|
|
width: "minmax(260px, 1.8fr)",
|
|
sortable: true,
|
|
filterable: true,
|
|
sticky: "start",
|
|
render: (campaign) => (
|
|
<div>
|
|
<Link className="table-primary-link" to={`/campaigns/${campaign.id}`}>
|
|
{campaign.name || campaign.external_id || campaign.id}
|
|
</Link>
|
|
<div className="table-subline">{campaign.description || campaign.external_id || campaign.id}</div>
|
|
</div>
|
|
),
|
|
value: (campaign) => `${campaign.name ?? ""} ${campaign.external_id ?? ""} ${campaign.id}`
|
|
},
|
|
{
|
|
id: "status",
|
|
header: "Status",
|
|
width: 150,
|
|
sortable: true,
|
|
filterable: true,
|
|
columnType: "from-list",
|
|
list: {
|
|
options: [
|
|
{ value: "draft", label: "Draft" },
|
|
{ value: "active", label: "Active" },
|
|
{ value: "completed", label: "Completed" },
|
|
{ value: "archived", label: "Archived" }
|
|
],
|
|
display: "pill"
|
|
},
|
|
render: (campaign) => <StatusBadge status={campaign.status || "draft"} />,
|
|
value: (campaign) => campaign.status || "draft"
|
|
},
|
|
{
|
|
id: "current_version",
|
|
header: "Current version",
|
|
width: 170,
|
|
sortable: true,
|
|
filterable: true,
|
|
className: "version-cell mono-small",
|
|
render: (campaign) => <span title={campaign.current_version_id || undefined}>{campaign.current_version_id ? shortId(campaign.current_version_id) : "—"}</span>,
|
|
value: (campaign) => campaign.current_version_id ?? ""
|
|
},
|
|
{
|
|
id: "updated",
|
|
header: "Updated",
|
|
width: 190,
|
|
sortable: true,
|
|
filterable: true,
|
|
filterType: "date",
|
|
className: "updated-cell",
|
|
render: (campaign) => formatDateTime(campaign.updated_at ?? campaign.updatedAt ?? campaign.created_at),
|
|
value: (campaign) => campaign.updated_at ?? campaign.updatedAt ?? campaign.created_at ?? ""
|
|
},
|
|
{
|
|
id: "actions",
|
|
header: "Actions",
|
|
width: 110,
|
|
sticky: "end",
|
|
render: (campaign) => <Link to={`/campaigns/${campaign.id}`}><Button variant="primary">Open</Button></Link>
|
|
}
|
|
];
|
|
|
|
return (
|
|
<div className="content-pad campaigns-page">
|
|
{error && <DismissibleAlert tone="danger" resetKey={error} floating>{error}</DismissibleAlert>}
|
|
|
|
<div className="page-heading split workspace-heading">
|
|
<div>
|
|
<PageTitle loading={loading}>All campaigns</PageTitle>
|
|
<p className="mono-small">{lastLoadedAt ? `Last loaded: ${lastLoadedAt}` : "Not loaded yet"}</p>
|
|
</div>
|
|
<div className="button-row compact-actions">
|
|
<Button onClick={load} disabled={loading}>Reload</Button>
|
|
<Button variant="primary" onClick={create} disabled={creating}>
|
|
{creating ? "Creating…" : "New campaign"}
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
|
|
<Card>
|
|
<LoadingFrame loading={loading} label="Loading campaigns…">
|
|
{campaigns.length === 0 ? (
|
|
<div className="empty-state">
|
|
<h2>No campaigns yet</h2>
|
|
<p>Start with a guided campaign draft. The WebUI will create a portable campaign JSON in the background.</p>
|
|
<Button variant="primary" onClick={create} disabled={creating}>
|
|
Create first campaign
|
|
</Button>
|
|
</div>
|
|
) : (
|
|
<DataGrid
|
|
id="campaigns"
|
|
rows={campaigns}
|
|
columns={columns}
|
|
getRowKey={(campaign) => campaign.id}
|
|
initialSort={{ columnId: "updated", direction: "desc" }}
|
|
className="campaign-table-wrap"
|
|
emptyText="No campaigns found."
|
|
/>
|
|
)}
|
|
</LoadingFrame>
|
|
</Card>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function shortId(value: string): string {
|
|
if (value.length <= 20) return value;
|
|
return `${value.slice(0, 12)}…${value.slice(-6)}`;
|
|
}
|
|
|
|
function formatDateTime(value?: string): string {
|
|
if (!value) return "—";
|
|
const date = new Date(value);
|
|
if (Number.isNaN(date.getTime())) return value;
|
|
return date.toLocaleString(undefined, {
|
|
year: "numeric",
|
|
month: "short",
|
|
day: "2-digit",
|
|
hour: "2-digit",
|
|
minute: "2-digit"
|
|
});
|
|
}
|
|
|
|
function formatLoadedAt(value: Date): string {
|
|
return value.toLocaleString(undefined, {
|
|
year: "numeric",
|
|
month: "short",
|
|
day: "2-digit",
|
|
hour: "2-digit",
|
|
minute: "2-digit",
|
|
second: "2-digit"
|
|
});
|
|
}
|