- Campaigns: new table, CRUD API, React context + provider
- Sessions: scoped to campaigns, paths under campaigns/{id}/
- File browser: scoped per campaign, removed copy/paste/autoPlay
- Sidebar: campaign selector dropdown at top
- Transcribe: GPU model cached/released via job counter
- Jobs: status text updates dynamically in real-time
- Auto-redirect: blocked when summarize job is active
121 lines
4.0 KiB
TypeScript
121 lines
4.0 KiB
TypeScript
import { useEffect, useState } from 'react';
|
|
import { useNavigate } from 'react-router-dom';
|
|
import { campaignApi, type Campaign } from '../api';
|
|
import { useCampaign } from '../contexts/CampaignContext';
|
|
|
|
export default function Campaigns() {
|
|
const [campaigns, setCampaigns] = useState<Campaign[]>([]);
|
|
const [name, setName] = useState('');
|
|
const [editingId, setEditingId] = useState<string | null>(null);
|
|
const [editName, setEditName] = useState('');
|
|
const nav = useNavigate();
|
|
const { setCampaign, refreshCampaigns } = useCampaign();
|
|
|
|
const load = async () => {
|
|
const list = await campaignApi.list();
|
|
setCampaigns(list);
|
|
};
|
|
|
|
useEffect(() => { load(); }, []);
|
|
|
|
const create = async () => {
|
|
if (!name.trim()) return;
|
|
const c = await campaignApi.create(name.trim());
|
|
setName('');
|
|
await load();
|
|
await refreshCampaigns();
|
|
setCampaign(c);
|
|
nav('/');
|
|
};
|
|
|
|
const startEdit = (c: Campaign) => {
|
|
setEditingId(c.id);
|
|
setEditName(c.name);
|
|
};
|
|
|
|
const saveEdit = async (id: string) => {
|
|
if (!editName.trim()) return;
|
|
await campaignApi.update(id, { name: editName.trim() });
|
|
setEditingId(null);
|
|
await load();
|
|
await refreshCampaigns();
|
|
};
|
|
|
|
const deleteCampaign = async (id: string) => {
|
|
if (!confirm('Delete this campaign? Sessions will be unlinked (notes/history preserved).')) return;
|
|
await campaignApi.delete(id);
|
|
await load();
|
|
await refreshCampaigns();
|
|
};
|
|
|
|
const select = (c: Campaign) => {
|
|
setCampaign(c);
|
|
nav('/');
|
|
};
|
|
|
|
return (
|
|
<div className="max-w-3xl mx-auto py-16 px-4">
|
|
<div className="eyebrow mb-2">Nat20 Notes</div>
|
|
<h1 className="font-display text-4xl mb-8">Campaigns</h1>
|
|
|
|
<div className="card p-6 mb-8">
|
|
<h2 className="font-display text-xl mb-4">New campaign</h2>
|
|
<div className="flex gap-3">
|
|
<input
|
|
className="input flex-1"
|
|
placeholder="Campaign name"
|
|
value={name}
|
|
onChange={(e) => setName(e.target.value)}
|
|
onKeyDown={(e) => e.key === 'Enter' && create()}
|
|
/>
|
|
<button className="btn-primary" disabled={!name.trim()} onClick={create}>
|
|
Create
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="space-y-3">
|
|
{campaigns.length === 0 && (
|
|
<p className="text-ink/40">No campaigns yet — create one above.</p>
|
|
)}
|
|
{campaigns.map((c) => (
|
|
<div key={c.id} className="card p-4 flex items-center justify-between hover:border-brass/40 border border-transparent">
|
|
<div className="flex-1 min-w-0 cursor-pointer" onClick={() => select(c)}>
|
|
{editingId === c.id ? (
|
|
<input
|
|
className="input w-full"
|
|
value={editName}
|
|
onChange={(e) => setEditName(e.target.value)}
|
|
onKeyDown={(e) => {
|
|
if (e.key === 'Enter') saveEdit(c.id);
|
|
if (e.key === 'Escape') setEditingId(null);
|
|
}}
|
|
onBlur={() => saveEdit(c.id)}
|
|
autoFocus
|
|
onClick={(e) => e.stopPropagation()}
|
|
/>
|
|
) : (
|
|
<>
|
|
<div className="font-medium">{c.name}</div>
|
|
<div className="text-xs text-ink/40 mt-0.5">
|
|
{c.session_count} session{c.session_count !== 1 ? 's' : ''}
|
|
{c.description && ` \u00B7 ${c.description}`}
|
|
</div>
|
|
</>
|
|
)}
|
|
</div>
|
|
<div className="flex gap-2 ml-4 shrink-0">
|
|
<button className="btn-secondary text-xs px-2 py-1" onClick={(e) => { e.stopPropagation(); startEdit(c); }}>
|
|
Rename
|
|
</button>
|
|
<button className="btn-secondary text-xs px-2 py-1 text-rose-400" onClick={(e) => { e.stopPropagation(); deleteCampaign(c.id); }}>
|
|
Delete
|
|
</button>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|