feat(admin): 'List on request page' toggle on project page
Phase 9a Task 5. Single switch, owner-only via the Server Action wired in Task 4. Saves immediately on click; reverts on error so a transient PB failure can't leave the UI lying. Default off, per the Phase 9 spec §4.5.
This commit is contained in:
parent
00c70eba8e
commit
0c310801b5
2 changed files with 92 additions and 0 deletions
|
|
@ -6,6 +6,7 @@ import { IngestPanel } from "@/components/admin/ingest-panel";
|
|||
import { SummaryPanel } from "@/components/admin/summary-panel";
|
||||
import { IntakeLinksPanel } from "@/components/admin/intake-links-panel";
|
||||
import { BackgroundMdEditor } from "@/components/admin/background-md-editor";
|
||||
import { ShowInIntakeToggle } from "@/components/admin/show-in-intake-toggle";
|
||||
import { getAdminPb } from "@/lib/pb/admin";
|
||||
import { summaryStatus } from "@/lib/ai/summarise";
|
||||
import type { IntakeLinkRecord, ProjectRecord } from "@/lib/pb/types";
|
||||
|
|
@ -65,6 +66,10 @@ export default async function ProjectAdminPage({
|
|||
/>
|
||||
<SummaryPanel projectId={project.id} status={summary} />
|
||||
<IntakeLinksPanel projectId={project.id} links={links} />
|
||||
<ShowInIntakeToggle
|
||||
projectId={project.id}
|
||||
initial={Boolean(project.show_in_intake)}
|
||||
/>
|
||||
<BackgroundMdEditor
|
||||
projectId={project.id}
|
||||
initial={project.background_md}
|
||||
|
|
|
|||
87
src/components/admin/show-in-intake-toggle.tsx
Normal file
87
src/components/admin/show-in-intake-toggle.tsx
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
"use client";
|
||||
|
||||
import { useState, useTransition } from "react";
|
||||
|
||||
import { setProjectShowInIntakeAction } from "@/app/actions/project-show-in-intake";
|
||||
|
||||
/**
|
||||
* The "List on request page" toggle on the project admin page.
|
||||
*
|
||||
* Default off. When on, the project appears in the public intake picker
|
||||
* under any client link that targets this project's client. The flag is
|
||||
* the leak-prevention boundary — see the Phase 9 spec §3.
|
||||
*
|
||||
* Two clicks to flip; no confirm dialog because the flip is reversible
|
||||
* from the same surface.
|
||||
*/
|
||||
export function ShowInIntakeToggle({
|
||||
projectId,
|
||||
initial,
|
||||
}: {
|
||||
projectId: string;
|
||||
initial: boolean;
|
||||
}) {
|
||||
const [value, setValue] = useState(initial);
|
||||
const [pending, startTransition] = useTransition();
|
||||
const [saved, setSaved] = useState<boolean>(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
function flip() {
|
||||
const next = !value;
|
||||
setValue(next);
|
||||
setSaved(false);
|
||||
setError(null);
|
||||
startTransition(async () => {
|
||||
try {
|
||||
await setProjectShowInIntakeAction({ projectId, show: next });
|
||||
setSaved(true);
|
||||
} catch (e) {
|
||||
setError(e instanceof Error ? e.message : "Could not save");
|
||||
setValue(!next); // revert
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="flex items-center justify-between gap-3 rounded-[var(--radius-tile)] border border-border-subtle p-4">
|
||||
<div>
|
||||
<h2 className="text-sm font-semibold text-text-primary">
|
||||
List on request page
|
||||
</h2>
|
||||
<p className="mt-0.5 text-xs text-text-muted">
|
||||
Show this project in the public request page's project picker.
|
||||
Off by default.
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
role="switch"
|
||||
aria-checked={value}
|
||||
disabled={pending}
|
||||
onClick={flip}
|
||||
className={
|
||||
"inline-flex h-6 w-11 items-center rounded-full transition-colors disabled:opacity-50 " +
|
||||
(value ? "bg-brand" : "bg-border-subtle")
|
||||
}
|
||||
>
|
||||
<span
|
||||
className={
|
||||
"inline-block h-5 w-5 transform rounded-full bg-white shadow transition-transform " +
|
||||
(value ? "translate-x-5" : "translate-x-0.5")
|
||||
}
|
||||
/>
|
||||
</button>
|
||||
{pending && <span className="text-xs text-text-muted">Saving…</span>}
|
||||
{error && (
|
||||
<span role="alert" className="text-xs text-accent">
|
||||
{error}
|
||||
</span>
|
||||
)}
|
||||
{!pending && saved && (
|
||||
<span className="text-xs text-text-muted">Saved.</span>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
Loading…
Reference in a new issue