milestone 9: react spa admin ui, frontend ci and embedded dist

This commit is contained in:
2026-08-02 13:04:09 +02:00
parent 5253c47303
commit 617cc966a2
82 changed files with 11833 additions and 17 deletions
@@ -0,0 +1,79 @@
import { useState } from "react";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { groupSourcesPutMutation, groupSourcesQuery } from "@/lib/queries";
import type { Blocklist } from "@/lib/types";
import { sameSet, toggleSource } from "./sourceSet";
import InlineError from "@/lib/InlineError";
interface Props {
groupId: number;
blocklists: Blocklist[];
}
export default function GroupSourcesEditor({ groupId, blocklists }: Props) {
const queryClient = useQueryClient();
const sources = useQuery(groupSourcesQuery(groupId));
const mutation = useMutation(groupSourcesPutMutation(queryClient));
const [selected, setSelected] = useState<number[] | null>(null);
if (sources.isPending) {
return (
<p role="status" className="mt-3 text-sm text-zinc-500">
Loading sources
</p>
);
}
if (sources.isError) return <InlineError error={sources.error} />;
if (blocklists.length === 0) {
return (
<p className="mt-3 text-sm text-zinc-500">
No blocklist sources exist yet add them on the Blocklists page.
</p>
);
}
const current = selected ?? sources.data;
const dirty = !sameSet(current, sources.data);
return (
<div className="mt-3">
<ul className="space-y-1">
{blocklists.map((blocklist) => (
<li key={blocklist.id}>
<label className="inline-flex items-center gap-2 text-sm">
<input
type="checkbox"
checked={current.includes(blocklist.id)}
onChange={() => setSelected(toggleSource(current, blocklist.id))}
/>
{blocklist.name}
</label>
</li>
))}
</ul>
<InlineError error={mutation.error} />
<div className="mt-3 flex gap-2">
<button
type="button"
disabled={!dirty || mutation.isPending}
onClick={() =>
mutation.mutate({ id: groupId, sourceIds: current }, { onSuccess: () => setSelected(null) })
}
className="rounded bg-blue-600 px-3 py-1.5 text-sm font-medium text-white disabled:opacity-50 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600"
>
Save sources
</button>
{dirty && (
<button
type="button"
onClick={() => setSelected(null)}
className="rounded border border-zinc-300 px-3 py-1.5 text-sm focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600 dark:border-zinc-700"
>
Discard
</button>
)}
</div>
</div>
);
}