5.7 KiB
5.7 KiB
Phase 11: Candidate Ranking - Context
Gathered: 2026-03-16 Status: Ready for planning
## Phase BoundaryUsers can drag candidates into a priority order within a thread. The rank persists across sessions and is visually communicated with medal badges on the top 3. Drag handles and reordering are disabled on resolved threads. Comparison view, impact preview, and pros/cons editing are separate phases.
## Implementation DecisionsCard layout and view toggle
- Add a grid/list view toggle in the thread header (list view is default)
- List view: vertical stack of horizontal cards (image thumbnail on left, name + badges on right) — enables drag-to-reorder
- Grid view: current 3-column responsive card layout preserved
- Both views render candidates in rank order (sort_order ascending)
- Rank badges visible in both views
Drag handle design
- Always-visible GripVertical icon (Lucide) on the left side of each list-view card
- Grip icon color: muted gray (text-gray-300), darkens to text-gray-500 on hover
- Cursor changes to 'grab' on hover, 'grabbing' during drag
- Drag feedback: elevated card with shadow + scale-up effect; other cards animate to show drop target gap (standard framer-motion Reorder behavior)
- On resolved threads: grip icon disappears entirely (not disabled/grayed)
- Drag only available in list view (grid view has no drag handles)
Rank badge style
- Medal icons (Lucide 'medal' or 'trophy') in gold (#D4AF37), silver (#C0C0C0), bronze (#CD7F32) for top 3 candidates
- Positioned inline before the candidate name text
- Candidates ranked 4th and below show no rank indicator — position implied by list order
- On resolved threads: rank badges remain visible (static, read-only) — overrides roadmap success criteria #4 which said "rank badges absent on resolved thread"; user prefers retrospective visibility
Sort order and persistence
- Schema migration adds
sort_order REAL NOT NULL DEFAULT 0tothread_candidates - Migration initializes existing candidates with spaced values (1000, 2000, 3000...) ordered by
created_at— ensures immediate correct ordering - Fractional indexing: only the moved item gets a single UPDATE (midpoint between neighbors)
- New candidates added to a thread get the highest sort_order (appended to bottom of rank)
- Auto-save on drop — no "Save order" button; reorder persists immediately via
PATCH /api/threads/:id/candidates/reorder tempItemslocal state pattern: render fromtempItems ?? queryData.candidates; clear on mutationonSettled— prevents React Query flicker
Claude's Discretion
- Exact horizontal card dimensions and spacing in list view
- Grid/list toggle icon style and placement
- Drag animation timing and spring config
- Image thumbnail size in list view cards
- How action buttons (Winner, Delete, Link) adapt to horizontal card layout
- Keyboard accessibility for reordering (arrow keys to move)
<code_context>
Existing Code Insights
Reusable Assets
CandidateCard(src/client/components/CandidateCard.tsx): Current card component — needs horizontal variant for list view, or a newCandidateListItemcomponentStatusBadge(src/client/components/StatusBadge.tsx): Click-to-cycle pattern reusable in list viewuseCandidates.tshooks:useCreateCandidate,useUpdateCandidate,useDeleteCandidate— need newuseReorderCandidatesmutationuseThreadhook: Returns thread withcandidates[]array — already has all data needed, just needs sort_order orderingformatWeight/formatPriceformatters: Reuse in list view card badgesuseWeightUnit/useCurrencyhooks: Already used by CandidateCardLucideIconhelper: For GripVertical drag handle and medal rank badgesuiStore(Zustand): AddcandidateViewMode: 'list' | 'grid'for view toggle persistence
Established Patterns
- framer-motion@12.37.0 already installed —
Reorder.Group/Reorder.Itemfor drag ordering - React Query for server data, Zustand for UI-only state
- Pill badges: blue for weight, green for price, gray for category, purple for pros/cons
- Services accept db as first param (DI pattern for testability)
- API validation via
@hono/zod-validatorwith Zod schemas - Hover-reveal action buttons on CandidateCard (Winner, Delete, External link)
Integration Points
src/db/schema.ts: AddsortOrder: real("sort_order").notNull().default(0)tothreadCandidatessrc/server/services/thread.service.ts: NewreorderCandidates()function (transactional); updategetCandidatesto ORDER BY sort_ordersrc/server/routes/threads.ts: NewPATCH /:id/candidates/reorderendpoint; reject if thread is resolvedsrc/shared/schemas.ts: NewreorderCandidatesSchema(z.object with orderedIds array)src/client/routes/threads/$threadId.tsx: Wrap candidates inReorder.Group, add view toggle, use tempItems patternsrc/client/hooks/useCandidates.ts: NewuseReorderCandidatesmutation hooktests/helpers/db.ts: Update CREATE TABLE for thread_candidates to include sort_order column- Drizzle migration:
sort_order REAL NOT NULL DEFAULT 0+ data migration to space existing rows
</code_context>
## Specific Ideas- List view cards should feel like Trello or Linear cards — horizontal layout, grip handle on the left, compact but informative
- Drag feedback should use standard framer-motion spring animations (elevated + gap), not custom physics
- Medal badges should use actual metallic-feeling colors (gold #D4AF37, silver #C0C0C0, bronze #CD7F32), not generic highlight colors
None — discussion stayed within phase scope
Phase: 11-candidate-ranking Context gathered: 2026-03-16