MotirBuilding in public
MOTIR · moooon
onMotir
You’re viewing a public project. Anyone can view it — no account needed. Sign in to submit, upvote, or comment on requests.View-only — you can’t edit work items
MOTIR-419

6.12.11 Public submit-request FORM + duplicate-detection "upvote this instead" UI (wire the 6.12.5 backend into the public surface)

Done
Description

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 PublicSubmitRequestButton into the real composer — a kind toggle (bug | feature; "feature" maps to the backend task kind 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 the PROD identifier — 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.6 POST /api/public-requests/[id]/upvote the roadmap’s PublicRoadmapVote already 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 raw rounded-*/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 + shipped components/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.