6.12.11 Public submit-request FORM + duplicate-detection "upvote this instead" UI (wire the 6.12.5 backend into the public surface)
Estimate: 60m · Depends on: 6.12.1, 6.12.4, 6.12.5, 6.12.6
Why this exists (planning gap, found running 6.12.10): 6.12.5 shipped the public submit-to-triage + duplicate-detection BACKEND only (the POST /api/public/projects/[projectId]/requests create + the GET …/requests/duplicates pre-check + their service/tests) — but NO public-facing FORM was built. The 6.12.4 PublicSubmitRequestButton is still a sign-in-to-act STUB (its own comment: "The submit FORM is 6.12.5 and isn’t built here"). So a public viewer cannot actually submit a request, and the e2e (6.12.10) has no real UI to drive. This card builds the missing UI half of 6.12.5, per the 6.12.1 Panel 3 design (the submit form + the duplicate-detection "upvote this instead" state).
- The real submit form (replaces the stub for signed-in viewers): turn
PublicSubmitRequestButtoninto the real composer — a kind toggle (bug | feature; "feature" maps to the backendtaskkind the route accepts — see the 6.12.5 route grammar), a title, and a descriptionMd body — that POSTs to/api/public/projects/[projectId]/requests(the GLOBAL project id, not thePRODidentifier — ADR §2.2; the public page already has the id server-side, thread it in). On 201 show the design’s "thanks, we got it" confirmation. A LOGGED-OUT viewer keeps the existing sign-in-to-act prompt (reading is open; posting needs an account) — do NOT remove that branch. - SIGNED-IN ONLY — the unauthenticated public portal form is DROPPED (Yue, 2026-06-14;
design/triage/design-notes.md). This form is the signed-in external-intake channel: NO name/email capture, NO honeypot, NO per-form anonymous-token route. A work item is created only by a signed-in account (the submission is attributed to it, per the Panel 4 "Submitted as {name} ({org})" hint), so a logged-out viewer NEVER sees the form — only the sign-in-to-act prompt. Do NOT build an anonymous public form. - Duplicate detection BEFORE create (Canny’s behaviour): as the title is entered (debounced), call
GET /api/public/projects/[projectId]/requests/duplicates?title=…and surface the returned matching existing public request(s) with an "Upvote this instead" affordance. Choosing it UPVOTES the existing request (reuse the 6.12.6POST /api/public-requests/[id]/upvotethe roadmap’sPublicRoadmapVotealready calls) and creates NO new item; "Submit as new" runs the create path. Render the empty / loading / rate-limited (typed-error, not a raw 500) states per the design. - No new backend. This is a pure UI + wiring card over the shipped 6.12.5 routes; no new service/route/migration. Stay design-compliant: ONLY shipped
components/ui/*+--el-*/[data-display-style]tokens (no Tier-0--color-*, no rawrounded-*/p-*/h-*).
Acceptance criteria
- A signed-in viewer (cross-org included) opens the submit form on a public project, picks a kind, enters a title + description, and submits → a triage request is created (201) and the confirmation shows; a logged-out viewer still sees the sign-in-to-act prompt (no form).
- As the title is typed, matching existing public requests surface with an "Upvote this instead" control; choosing it upvotes the existing request (vote increments) and creates NO duplicate; "Submit as new" creates the item.
- Empty / loading / rate-limited states render gracefully (no raw 500); the dedupe match drives the UI (no AI).
- ONLY
--el-*+[data-display-style]tokens + shippedcomponents/ui/*; matches the 6.12.1 Panel 3 design; no new service/route (pure wiring over 6.12.5/6.12.6).
Context refs
- 6.12.1 Panel 3 (the submit + duplicate-detection design) — the surface to match;
design/public-projects/. design/triage/design-notes.md(Yue, 2026-06-14) — the sign-in-to-act intake model: the unauthenticated public portal form is DROPPED; the 6.12 public-project "Submit a request" is the SIGNED-IN external-intake channel into the 6.11 triage queue.- 6.12.5 routes (shipped):
app/api/public/projects/[projectId]/requests/route.ts(POST create,{ kind: bug|task, title, descriptionMd? }→ 201) +…/requests/duplicates/route.ts(GET ?title= →{ candidates: [{ id, kind, identifier, title, status, voteCount }] }). - 6.12.6 upvote route (shipped):
app/api/public-requests/[id]/upvote/route.ts(POST →{ voted, voteCount }) — the "upvote this instead" target. app/(public)/_components/PublicSubmitRequestButton.tsx(the stub to replace) +PublicRoadmapVote.tsx(the shipped upvote-control pattern to reuse).motir-core/CLAUDE.md§ 4-layer + § colour/shape tokens.