fix(29-02): lint fixes for GearImage integration
Fix unused parameter warning and formatting issues across all updated components. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
105
.planning/phases/12-comparison-view/12-CONTEXT.md
Normal file
105
.planning/phases/12-comparison-view/12-CONTEXT.md
Normal file
@@ -0,0 +1,105 @@
|
||||
# Phase 12: Comparison View - Context
|
||||
|
||||
**Gathered:** 2026-03-17
|
||||
**Status:** Ready for planning
|
||||
|
||||
<domain>
|
||||
## Phase Boundary
|
||||
|
||||
Users can view all candidates for a thread side-by-side in a tabular comparison layout with relative weight and price deltas. The table scrolls horizontally on narrow viewports with a sticky label column. Resolved threads display the comparison in read-only mode with the winning candidate visually marked. Impact preview (setup deltas) is a separate phase (13).
|
||||
|
||||
</domain>
|
||||
|
||||
<decisions>
|
||||
## Implementation Decisions
|
||||
|
||||
### Compare mode entry point
|
||||
- Add a third icon to the existing list/grid toggle bar, making it list | grid | compare (three-way toggle)
|
||||
- Use `candidateViewMode: 'list' | 'grid' | 'compare'` in uiStore — extends the existing Zustand state
|
||||
- Compare icon only appears when 2+ candidates exist in the thread (hidden otherwise)
|
||||
- "Add Candidate" button visibility in compare mode is Claude's discretion
|
||||
|
||||
### Table orientation and layout
|
||||
- Candidates as columns, attribute labels as rows (classic product-comparison pattern — Amazon/Wirecutter style)
|
||||
- Sticky left column for attribute labels; table scrolls horizontally on narrow viewports
|
||||
- Attribute row order: Image → Name → Rank → Weight (with delta) → Price (with delta) → Status → Product Link → Notes → Pros → Cons
|
||||
- Image row: sizing is Claude's discretion (balance compactness with product visibility)
|
||||
- Multi-line text (notes, pros, cons): rendering approach is Claude's discretion (keep table scannable)
|
||||
|
||||
### Delta highlighting style
|
||||
- Lightest candidate's weight cell gets a subtle colored background tint (e.g., bg-green-50); cheapest similarly
|
||||
- Non-best cells show delta text in neutral gray — no colored badges for deltas, only the "best" cell gets color
|
||||
- Missing weight/price data: Claude's discretion on indicator style (must satisfy COMP-04 — no misleading zeroes)
|
||||
- Delta format (absolute + delta, or delta only): Claude's discretion based on readability
|
||||
|
||||
### Resolved thread presentation
|
||||
- Winner column highlight and trophy/banner approach: Claude's discretion (existing resolution banner + column tint are both available patterns)
|
||||
- Interactive elements in resolved comparison (links clickable vs everything static): Claude's discretion, following the existing Phase 11 pattern where resolved threads disable mutation actions but keep read-only indicators
|
||||
- Existing resolution banner above the comparison table: Claude's discretion on whether to keep it, remove it, or adapt it
|
||||
|
||||
### Claude's Discretion
|
||||
- "Add Candidate" button visibility when in compare view
|
||||
- Image thumbnail sizing in comparison cells (square crop vs wider aspect)
|
||||
- Multi-line text rendering strategy (clamped with expand vs full text)
|
||||
- Missing data indicator style (dash with label, empty cell, etc.)
|
||||
- Delta format: absolute value + delta underneath, or delta only for non-best cells
|
||||
- Winner column marking approach (column tint, trophy icon, or both)
|
||||
- Resolved thread interactivity (links clickable vs all read-only)
|
||||
- Resolution banner behavior in compare view
|
||||
- View mode persistence (already in Zustand — whether compare resets on navigation or persists)
|
||||
- Compare toggle icon choice (e.g., Lucide `columns-3`, `table-2`, or similar)
|
||||
- Table cell padding, border styling, and overall table chrome
|
||||
- Column minimum/maximum widths
|
||||
- Keyboard accessibility for horizontal scrolling
|
||||
|
||||
</decisions>
|
||||
|
||||
<code_context>
|
||||
## Existing Code Insights
|
||||
|
||||
### Reusable Assets
|
||||
- `candidateViewMode` in `uiStore` (`stores/uiStore.ts`): Already stores `'list' | 'grid'` — extend to include `'compare'`
|
||||
- `CandidateCard` / `CandidateListItem`: Data shape reference for what fields are available per candidate
|
||||
- `formatWeight()` / `formatPrice()` in `lib/formatters.ts`: Unit-aware formatting for table cells and deltas
|
||||
- `useWeightUnit()` / `useCurrency()` hooks: Current unit/currency for display
|
||||
- `RankBadge` (`CandidateListItem.tsx`): Exported component for gold/silver/bronze medals — reuse in compare table name row
|
||||
- `StatusBadge` (`StatusBadge.tsx`): Click-to-cycle status — render as static text in compare view (no interaction needed)
|
||||
- `LucideIcon` helper: For compare toggle icon and any icons in the table
|
||||
- `useThread(threadId)` hook: Returns `thread.candidates[]` with all fields needed (name, weightGrams, priceCents, status, pros, cons, notes, productUrl, imageFilename, categoryName, categoryIcon)
|
||||
|
||||
### Established Patterns
|
||||
- Three-way toggle: Extend existing `bg-gray-100 rounded-lg p-0.5` toggle bar pattern from thread toolbar
|
||||
- Pill badges: blue=weight, green=price, gray=category, purple=pros/cons — table can reference these colors for consistency
|
||||
- framer-motion already installed — AnimatePresence for view transitions if desired
|
||||
- React Query for server data, Zustand for UI-only state
|
||||
- Resolution banner: amber-50 bg with amber-200 border in resolved thread header — reusable pattern for winner column
|
||||
|
||||
### Integration Points
|
||||
- `src/client/routes/threads/$threadId.tsx`: Add compare view branch to the existing list/grid conditional rendering
|
||||
- `src/client/stores/uiStore.ts`: Extend `candidateViewMode` union type to include `'compare'`
|
||||
- New component: `ComparisonTable.tsx` (or similar) — receives candidates array, renders the tabular comparison
|
||||
- No backend changes needed — all data already available from `useThread` hook
|
||||
- No schema changes — this is a pure frontend/UI phase
|
||||
|
||||
</code_context>
|
||||
|
||||
<specifics>
|
||||
## Specific Ideas
|
||||
|
||||
- Classic product-comparison table like Amazon or Wirecutter — candidates as columns, attributes as rows
|
||||
- Subtle green tint on the "best" cell rather than heavy badges or bold formatting — keeps the minimalist feel
|
||||
- Gray delta text for non-best values — visual hierarchy: best stands out, others recede
|
||||
|
||||
</specifics>
|
||||
|
||||
<deferred>
|
||||
## Deferred Ideas
|
||||
|
||||
None — discussion stayed within phase scope
|
||||
|
||||
</deferred>
|
||||
|
||||
---
|
||||
|
||||
*Phase: 12-comparison-view*
|
||||
*Context gathered: 2026-03-17*
|
||||
50
.planning/phases/29-image-presentation/29-01-SUMMARY.md
Normal file
50
.planning/phases/29-image-presentation/29-01-SUMMARY.md
Normal file
@@ -0,0 +1,50 @@
|
||||
---
|
||||
phase: 29
|
||||
plan: 01
|
||||
subsystem: backend
|
||||
tags: [schema, image-processing, sharp]
|
||||
key-files:
|
||||
created: []
|
||||
modified:
|
||||
- src/db/schema.ts
|
||||
- src/shared/schemas.ts
|
||||
- src/server/services/image.service.ts
|
||||
- src/server/routes/images.ts
|
||||
- package.json
|
||||
metrics:
|
||||
tasks: 7
|
||||
commits: 5
|
||||
files-changed: 6
|
||||
---
|
||||
|
||||
# Plan 29-01 Summary: Schema + Dominant Color Extraction
|
||||
|
||||
## What was built
|
||||
- Installed Sharp image processing library for server-side color extraction
|
||||
- Added `dominant_color`, `crop_zoom`, `crop_x`, `crop_y` columns to items, global_items, and thread_candidates tables
|
||||
- Created `extractDominantColor()` function that resizes image to 1x1 pixel for weighted average color
|
||||
- Integrated color extraction into both image upload endpoints (direct and from-url)
|
||||
- Updated Zod schemas for items, candidates, and global items to accept new fields
|
||||
- Generated Drizzle migration (db:push deferred — requires running database)
|
||||
|
||||
## Commits
|
||||
|
||||
| Task | Commit | Description |
|
||||
|------|--------|-------------|
|
||||
| 1 | cee1500 | Install Sharp for image processing |
|
||||
| 2 | 36363a8 | Add dominantColor and crop fields to schema |
|
||||
| 3 | b637b10 | Generate migration for image presentation fields |
|
||||
| 4 | e305fa7 | Add dominant color extraction via Sharp |
|
||||
| 5 | 2696b78 | Extract dominant color in image upload endpoints |
|
||||
| 6 | 3480473 | Add image presentation fields to Zod schemas |
|
||||
| 7 | — | No changes needed (storage service already spreads fields) |
|
||||
|
||||
## Deviations
|
||||
- Task 3 (db:push): Database not accessible in dev environment — migration generated but push deferred to deployment. This is non-blocking for frontend work.
|
||||
|
||||
## Self-Check: PASSED
|
||||
- Sharp installed: YES
|
||||
- dominant_color in 3 tables: YES (grep confirms 3 occurrences)
|
||||
- Zod schemas updated: YES (3 schemas)
|
||||
- Upload returns dominantColor: YES
|
||||
- Lint passes: YES
|
||||
3
bun.lock
3
bun.lock
@@ -38,7 +38,6 @@
|
||||
"@types/bun": "latest",
|
||||
"@types/react": "^19.2.14",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@types/sharp": "^0.32.0",
|
||||
"@vitejs/plugin-react": "^6.0.1",
|
||||
"concurrently": "^9.1.2",
|
||||
"drizzle-kit": "^0.31.9",
|
||||
@@ -560,8 +559,6 @@
|
||||
|
||||
"@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="],
|
||||
|
||||
"@types/sharp": ["@types/sharp@0.32.0", "", { "dependencies": { "sharp": "*" } }, "sha512-OOi3kL+FZDnPhVzsfD37J88FNeZh6gQsGcLc95NbeURRGvmSjeXiDcyWzF2o3yh/gQAUn2uhh/e+CPCa5nwAxw=="],
|
||||
|
||||
"@types/use-sync-external-store": ["@types/use-sync-external-store@0.0.6", "", {}, "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg=="],
|
||||
|
||||
"@vitejs/plugin-react": ["@vitejs/plugin-react@6.0.1", "", { "dependencies": { "@rolldown/pluginutils": "1.0.0-rc.7" }, "peerDependencies": { "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", "babel-plugin-react-compiler": "^1.0.0", "vite": "^8.0.0" }, "optionalPeers": ["@rolldown/plugin-babel", "babel-plugin-react-compiler"] }, "sha512-l9X/E3cDb+xY3SWzlG1MOGt2usfEHGMNIaegaUGFsLkb3RCn/k8/TOXBcab+OndDI4TBtktT8/9BwwW8Vi9KUQ=="],
|
||||
|
||||
@@ -26,7 +26,6 @@
|
||||
"@types/bun": "latest",
|
||||
"@types/react": "^19.2.14",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@types/sharp": "^0.32.0",
|
||||
"@vitejs/plugin-react": "^6.0.1",
|
||||
"concurrently": "^9.1.2",
|
||||
"drizzle-kit": "^0.31.9",
|
||||
|
||||
@@ -95,10 +95,7 @@ export function CandidateListItem({
|
||||
}}
|
||||
>
|
||||
{candidate.imageUrl ? (
|
||||
<GearImage
|
||||
src={candidate.imageUrl}
|
||||
alt={candidate.name}
|
||||
/>
|
||||
<GearImage src={candidate.imageUrl} alt={candidate.name} />
|
||||
) : (
|
||||
<LucideIcon
|
||||
name={candidate.categoryIcon}
|
||||
|
||||
@@ -631,15 +631,13 @@ function GridCard({ item, onAdd, onCardClick, weight, price }: CardProps) {
|
||||
className="aspect-[4/3] overflow-hidden"
|
||||
style={{
|
||||
backgroundColor: item.imageUrl
|
||||
? ((item as Record<string, unknown>).dominantColor as string) || "#f3f4f6"
|
||||
? ((item as Record<string, unknown>).dominantColor as string) ||
|
||||
"#f3f4f6"
|
||||
: undefined,
|
||||
}}
|
||||
>
|
||||
{item.imageUrl ? (
|
||||
<GearImage
|
||||
src={item.imageUrl}
|
||||
alt={`${item.brand} ${item.model}`}
|
||||
/>
|
||||
<GearImage src={item.imageUrl} alt={`${item.brand} ${item.model}`} />
|
||||
) : (
|
||||
<div className="w-full h-full bg-gray-50 flex items-center justify-center">
|
||||
<svg
|
||||
@@ -707,15 +705,13 @@ function ListRow({ item, onAdd, onCardClick, weight, price }: CardProps) {
|
||||
className="w-12 h-12 rounded-lg shrink-0 overflow-hidden"
|
||||
style={{
|
||||
backgroundColor: item.imageUrl
|
||||
? ((item as Record<string, unknown>).dominantColor as string) || "#f3f4f6"
|
||||
? ((item as Record<string, unknown>).dominantColor as string) ||
|
||||
"#f3f4f6"
|
||||
: undefined,
|
||||
}}
|
||||
>
|
||||
{item.imageUrl ? (
|
||||
<GearImage
|
||||
src={item.imageUrl}
|
||||
alt={`${item.brand} ${item.model}`}
|
||||
/>
|
||||
<GearImage src={item.imageUrl} alt={`${item.brand} ${item.model}`} />
|
||||
) : (
|
||||
<div className="w-full h-full bg-gray-50 flex items-center justify-center">
|
||||
<svg
|
||||
|
||||
@@ -119,15 +119,13 @@ export function ComparisonTable({
|
||||
className="w-12 h-12 rounded-lg overflow-hidden flex items-center justify-center"
|
||||
style={{
|
||||
backgroundColor: c.imageUrl
|
||||
? ((c as Record<string, unknown>).dominantColor as string) || "#f3f4f6"
|
||||
? ((c as Record<string, unknown>).dominantColor as string) ||
|
||||
"#f3f4f6"
|
||||
: undefined,
|
||||
}}
|
||||
>
|
||||
{c.imageUrl ? (
|
||||
<GearImage
|
||||
src={c.imageUrl}
|
||||
alt={c.name}
|
||||
/>
|
||||
<GearImage src={c.imageUrl} alt={c.name} />
|
||||
) : (
|
||||
<LucideIcon
|
||||
name={c.categoryIcon}
|
||||
|
||||
@@ -12,7 +12,7 @@ interface GearImageProps {
|
||||
export function GearImage({
|
||||
src,
|
||||
alt,
|
||||
dominantColor,
|
||||
dominantColor: _dominantColor,
|
||||
cropZoom,
|
||||
cropX,
|
||||
cropY,
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { Link } from "@tanstack/react-router";
|
||||
import { useEffect, useState } from "react";
|
||||
import { GearImage } from "./GearImage";
|
||||
import {
|
||||
useGlobalItem,
|
||||
useGlobalItems,
|
||||
useLinkItem,
|
||||
useUnlinkItem,
|
||||
} from "../hooks/useGlobalItems";
|
||||
import { GearImage } from "./GearImage";
|
||||
|
||||
interface LinkToGlobalItemProps {
|
||||
itemId: number;
|
||||
@@ -179,11 +179,7 @@ export function LinkToGlobalItem({
|
||||
>
|
||||
{item.imageUrl ? (
|
||||
<div className="w-8 h-8 rounded overflow-hidden shrink-0">
|
||||
<GearImage
|
||||
src={item.imageUrl}
|
||||
alt=""
|
||||
cover
|
||||
/>
|
||||
<GearImage src={item.imageUrl} alt="" cover />
|
||||
</div>
|
||||
) : (
|
||||
<div className="w-8 h-8 rounded bg-gray-100 shrink-0" />
|
||||
|
||||
@@ -547,10 +547,7 @@ function GlobalItemListRow({ item, weight, price }: ListRowProps) {
|
||||
}}
|
||||
>
|
||||
{item.imageUrl ? (
|
||||
<GearImage
|
||||
src={item.imageUrl}
|
||||
alt={`${item.brand} ${item.model}`}
|
||||
/>
|
||||
<GearImage src={item.imageUrl} alt={`${item.brand} ${item.model}`} />
|
||||
) : (
|
||||
<div className="w-full h-full bg-gray-50 flex items-center justify-center">
|
||||
<svg
|
||||
|
||||
Reference in New Issue
Block a user