feat(board): add cards as owner, request features as client

This commit is contained in:
Dave 2026-09-16 11:06:06 +02:00
parent 07656a5bc7
commit 043d8ddc8c
2 changed files with 142 additions and 0 deletions

View file

@ -0,0 +1,132 @@
"use client";
import { useState, useTransition } from "react";
import { useRouter } from "next/navigation";
import { Plus } from "lucide-react";
import { createCard } from "@/app/actions/cards";
import type { Role } from "@/lib/pb/types";
export function AddCard({
role,
projectId,
columnId,
clientSlug,
projectSlug,
}: {
role: Role;
projectId: string;
columnId: string;
clientSlug: string;
projectSlug: string;
}) {
const router = useRouter();
const [open, setOpen] = useState(false);
const [title, setTitle] = useState("");
const [pending, startTransition] = useTransition();
const [error, setError] = useState<string | null>(null);
// Takes the minimal shape rather than React.FormEvent, because it is called
// both from onSubmit and from the Enter handler in onKeyDown, and those
// carry different event types.
async function submit(event: { preventDefault: () => void }) {
event.preventDefault();
const value = title.trim();
if (!value) return;
setError(null);
if (role === "owner") {
setTitle("");
setOpen(false);
startTransition(() =>
createCard({
projectId,
columnId,
title: value,
clientSlug,
projectSlug,
}).then(() => undefined)
);
return;
}
// A client manager's card is a request: the route decides the column,
// the priority and the accepted flag, whatever is sent here.
const response = await fetch("/api/client/cards", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ project: projectId, title: value }),
});
if (!response.ok) {
setError("Could not add that");
return;
}
setTitle("");
setOpen(false);
router.refresh();
}
if (!open) {
return (
<button
type="button"
onClick={() => setOpen(true)}
className="flex items-center gap-1.5 rounded-[var(--radius-tile)] px-2 py-2 text-xs font-medium text-text-muted transition-colors hover:bg-surface-raised hover:text-text-secondary"
>
<Plus size={13} strokeWidth={2} />
{role === "owner" ? "Add a card" : "Request a feature"}
</button>
);
}
return (
<form
onSubmit={submit}
className="rounded-[var(--radius-tile)] border border-border-subtle bg-panel p-2"
>
<textarea
autoFocus
value={title}
onChange={(event) => setTitle(event.target.value)}
onKeyDown={(event) => {
if (event.key === "Escape") setOpen(false);
if (event.key === "Enter" && !event.shiftKey) {
event.preventDefault();
submit(event);
}
}}
rows={2}
placeholder={
role === "owner" ? "What needs doing?" : "What would you like added?"
}
className="w-full resize-none bg-transparent text-[13px] leading-[1.45] text-text-primary outline-none placeholder:text-text-muted"
/>
{error && (
<p role="alert" className="px-0.5 pb-1 text-[11px] text-accent">
{error}
</p>
)}
<div className="flex items-center gap-2">
<button
type="submit"
disabled={pending || !title.trim()}
className="rounded-md bg-brand px-2.5 py-1 text-xs font-medium text-brand-fg disabled:opacity-50"
>
Add
</button>
<button
type="button"
onClick={() => setOpen(false)}
className="text-xs text-text-muted hover:text-text-secondary"
>
Cancel
</button>
</div>
</form>
);
}

View file

@ -18,6 +18,7 @@ import {
} from "@dnd-kit/sortable";
import { CSS } from "@dnd-kit/utilities";
import { AddCard } from "@/components/board/add-card";
import { BoardColumn } from "@/components/board/board-column";
import { CardTile } from "@/components/board/card-tile";
import { ClientCardControls } from "@/components/board/client-card-controls";
@ -233,6 +234,15 @@ export function Board({
/>
))}
</SortableContext>
{(role === "owner" || column.is_inbox) && (
<AddCard
role={role}
projectId={projectId}
columnId={column.id}
clientSlug={clientSlug}
projectSlug={projectSlug}
/>
)}
</Droppable>
);
})}