docs: complete project research
This commit is contained in:
@@ -1,244 +1,262 @@
|
||||
# Feature Research: v1.2 Collection Power-Ups
|
||||
# Feature Research
|
||||
|
||||
**Domain:** Gear management -- search/filter, weight classification, weight visualization, candidate status tracking, weight unit selection
|
||||
**Domain:** Gear management — candidate comparison, setup impact preview, and candidate ranking
|
||||
**Researched:** 2026-03-16
|
||||
**Confidence:** HIGH
|
||||
**Scope:** New features only. v1.0/v1.1 features (item CRUD, categories, threads, setups, dashboard, onboarding, images, icons) are already shipped.
|
||||
**Confidence:** HIGH (existing codebase fully understood; UX patterns verified via multiple sources)
|
||||
|
||||
## Table Stakes
|
||||
---
|
||||
|
||||
Features that gear management users expect. Missing these makes the app feel incomplete for collections beyond ~20 items.
|
||||
## Context
|
||||
|
||||
| Feature | Why Expected | Complexity | Dependencies on Existing |
|
||||
|---------|--------------|------------|--------------------------|
|
||||
| Search items by name | Every competitor with an inventory concept has search. Hikt highlights "searchable digital closet." PackLight Supporter Edition has inventory search. Once a collection exceeds 30 items, scrolling to find something is painful. LighterPack notably lacks this and users complain. | LOW | Items query (`useItems`), collection view. Client-side only -- no API changes needed for <500 items. |
|
||||
| Filter items by category | Already partially exists in Planning view (category dropdown for threads). Collection view groups by category visually but has no filter. Users need to quickly narrow to "show me just my shelter items." | LOW | Categories query (`useCategories`), collection view. Client-side filtering of already-fetched items. |
|
||||
| Weight unit selection (g, oz, lb, kg) | Universal across all competitors. LighterPack supports toggling between g/oz/lb/kg. Packrat offers per-item input in any unit with display conversion. Backpacking Light forum users specifically praise apps that let you "enter item weights in grams and switch the entire display to lbs & oz." Gear specs come in mixed units -- a sleeping bag in lbs/oz, a fuel canister in grams. | LOW | `formatWeight()` in `lib/formatters.ts`, `settings` table (already exists with key/value store), TotalsBar, ItemCard, CandidateCard, SetupCard -- every weight display. |
|
||||
| Weight classification (base/worn/consumable) | LighterPack pioneered this three-way split and it is now universal. Hikt, PackLight, Packstack, HikeLite, 99Boulders spreadsheet -- all support it. "Base weight" is the core metric of the ultralight community. Without classification, weight totals are a single number with no actionable insight. | MEDIUM | `setup_items` join table (needs new column), setup detail view, setup service, totals computation. Schema migration required. |
|
||||
This is a subsequent milestone research file for **v1.3 Research & Decision Tools**.
|
||||
The features below are **additive** to v1.2. All three features operate within the existing
|
||||
`threads/$threadId` page and its data model.
|
||||
|
||||
## Differentiators
|
||||
**Existing data model relevant to this milestone:**
|
||||
- `threadCandidates`: id, threadId, name, weightGrams, priceCents, categoryId, notes, productUrl, imageFilename, status — no rank, pros, or cons columns yet
|
||||
- `setups` + `setupItems`: stores weight/cost per setup item with classification (base/worn/consumable)
|
||||
- `getSetupWithItems` already returns `classification` per item — available for impact preview
|
||||
|
||||
Features that set GearBox apart or add meaningful value beyond what competitors offer.
|
||||
---
|
||||
|
||||
| Feature | Value Proposition | Complexity | Dependencies on Existing |
|
||||
|---------|-------------------|------------|--------------------------|
|
||||
| Weight distribution visualization (donut/pie chart) | LighterPack's pie chart is iconic and widely cited as its best feature. "The pie chart at the top is a great way to visualize how your pack weight breaks down by category." PackLight uses bar graphs. GearBox can do this per-setup with a modern donut chart that also shows base/worn/consumable breakdown -- a combination no competitor offers cleanly. | MEDIUM | Totals data (already computed server-side per category), weight classification (new), a chart library (react-minimal-pie-chart at 2kB or Recharts). |
|
||||
| Candidate status tracking (researching/ordered/arrived) | No competitor has this. Research confirmed: the specific workflow of tracking purchase status through stages does not exist in any gear management app. This is unique to GearBox's planning thread concept. It makes threads a living document of the purchase lifecycle, not just a comparison tool. | LOW | `thread_candidates` table (needs new `status` column), CandidateCard, CandidateForm. Simple text field migration. |
|
||||
| Planning category filter with icon-aware dropdown | Already partially built as a plain `<select>` in PlanningView. Upgrading to show Lucide icons alongside category names makes filtering feel polished and consistent with the icon picker UX. | LOW | Existing CategoryPicker component pattern, existing category filter state in PlanningView. |
|
||||
| Weight classification shown per-setup (not global) | In LighterPack, worn/consumable flags are per-item within a list. In GearBox, items exist in a global collection and appear in multiple setups. The same jacket might be "worn" in a summer bikepacking setup but "base weight" (packed in panniers) in a winter setup. Classification belongs on the setup_items join, not on the item itself. This is architecturally superior to competitors. | MEDIUM | `setup_items` table schema, setup sync endpoint, setup detail UI. |
|
||||
## Feature Landscape
|
||||
|
||||
## Anti-Features
|
||||
### Table Stakes (Users Expect These)
|
||||
|
||||
Features to explicitly NOT build in this milestone.
|
||||
Features users assume exist in any comparison or decision tool. Missing these makes the thread
|
||||
detail page feel incomplete as a decision workspace.
|
||||
|
||||
| Anti-Feature | Why Avoid | What to Do Instead |
|
||||
|--------------|-----------|-------------------|
|
||||
| Per-item weight input in multiple units | Packrat lets you enter "2 lb 3 oz" per item. This adds parsing complexity, ambiguous storage, and conversion bugs. | Store grams internally (already done). Convert for display only. Users enter grams; if they want oz input, they convert mentally or we add a unit toggle on the input field later. |
|
||||
| Interactive chart drill-down (click to zoom) | LighterPack lets you click pie slices to zoom into category breakdowns. Adds significant interaction complexity. | Static donut chart with hover tooltips. Drill-down is a future enhancement. |
|
||||
| Weight goals / targets ("your target base weight is X") | Some apps show ultralight thresholds. Adds opinionated norms that conflict with hobby-agnostic design. | Show the numbers. Let users interpret them. |
|
||||
| Custom weight classification labels | Beyond base/worn/consumable. Some users want "luxury" or "shared" categories. | Three classifications cover 95% of use cases. The notes field handles edge cases. |
|
||||
| Server-side search / full-text search | SQLite FTS5 or similar. Premature for a single-user app with <1000 items. | Client-side filtering of the already-fetched items array. Simpler, faster for the expected data scale. |
|
||||
| Worn/consumable at the global item level | Tempting to add a classification column to the `items` table. | Classification varies by setup context. A rain shell is "worn" on a day hike but "base weight" (packed) on a bike tour. The join table `setup_items` is the correct location. |
|
||||
| Feature | Why Expected | Complexity | Notes |
|
||||
|---------|--------------|------------|-------|
|
||||
| Side-by-side comparison view | Any comparison tool in any domain shows attributes aligned per-column. Card grid (current) forces mental juggling between candidates. E-commerce, spec sheets, gear apps — all use tabular layout for comparison. | MEDIUM | Rows = attributes (image, name, weight, price, status, notes, link), columns = candidates. Sticky attribute-label column during horizontal scroll. Max 3–4 candidates usable on desktop; 2 on mobile. Toggle between grid view (current) and table view. |
|
||||
| Weight delta per candidate | Gear apps (LighterPack, GearGrams) display weight totals prominently. Users replacing an item need the delta, not just the raw weight of the candidate. | LOW | Pure client-side computation: `candidate.weightGrams - existingItemWeight`. No API call needed if setup data already loaded via `useSetup`. |
|
||||
| Cost delta per candidate | Same reasoning as weight delta. A purchase decision is always the weight vs. cost tradeoff. | LOW | Same pattern as weight delta. Color-coded: green for savings/lighter, red for more expensive/heavier. |
|
||||
| Setup selector for impact preview | User needs to pick which setup to compute deltas against — not all setups contain the same category of item being replaced. | MEDIUM | Dropdown of setup names populated from `useSetups()`. When selected, loads setup via `useSetup(id)`. "No setup selected" state shows raw candidate values only, no delta. |
|
||||
|
||||
## Feature Details
|
||||
### Differentiators (Competitive Advantage)
|
||||
|
||||
### 1. Search and Filter
|
||||
Features not found in LighterPack, GearGrams, or any other gear app. Directly serve the
|
||||
"decide between candidates" workflow that is unique to GearBox.
|
||||
|
||||
**What users expect:** A text input that filters visible items by name as you type. A category dropdown or pill selector to filter by category. Both should work together (search within a category).
|
||||
| Feature | Value Proposition | Complexity | Notes |
|
||||
|---------|-------------------|------------|-------|
|
||||
| Drag-to-rank ordering | Makes priority explicit without a numeric input. Ranking communicates "this is my current top pick." Maps to how users mentally stack-rank options during research. No competitor has this in the gear domain. | MEDIUM | `@dnd-kit/sortable` is the current standard (actively maintained; `react-beautiful-dnd` is abandoned as of 2025). Requires new `rank` integer column on `threadCandidates`. Persist order via PATCH endpoint. |
|
||||
| Per-candidate pros/cons fields | Freeform text capturing the reasoning behind ranking. LighterPack and GearGrams have notes per item but no structured decision rationale. Differentiates GearBox as a decision tool, not just a list tracker. | LOW | Two textarea fields per candidate. New `pros` and `cons` text columns on `threadCandidates`. Visible in comparison view rows and candidate edit panel. |
|
||||
| Impact preview with category-matched delta | Setup items have a category. The most meaningful delta is weight saved within the same category (e.g., comparing sleeping pads, subtract current sleeping pad weight from setup total). More actionable than comparing against the entire setup total. | MEDIUM | Use `candidate.categoryId` to find matching setup items and compute delta. Edge case: no item of that category in the setup → show "not in setup." Data already available from `getSetupWithItems`. |
|
||||
|
||||
**Domain patterns observed:**
|
||||
- Hikt: Searchable gear closet with category and specification filters
|
||||
- PackLight: Inventory search (premium feature) with category organization
|
||||
- Backpacking Light Calculator: Search filter in gear locker and within packs
|
||||
- LighterPack: No text search -- widely considered a gap
|
||||
### Anti-Features (Commonly Requested, Often Problematic)
|
||||
|
||||
**Recommended implementation:**
|
||||
- Sticky search bar above the collection grid with a text input and category filter dropdown
|
||||
- Client-side filtering using `Array.filter()` on the items array from `useItems()`
|
||||
- Case-insensitive substring match on item name
|
||||
- Category filter as pills or dropdown (reuse the pattern from PlanningView)
|
||||
- URL search params for filter state (shareable filtered views, consistent with existing `?tab=` pattern)
|
||||
- Clear filters button when any filter is active
|
||||
- Result count displayed ("showing 12 of 47 items")
|
||||
| Feature | Why Requested | Why Problematic | Alternative |
|
||||
|---------|---------------|-----------------|-------------|
|
||||
| Custom comparison attributes | "I want to compare battery life, durability, color..." | PROJECT.md explicitly rejects this as a complexity trap. Custom attributes require schema generalization, dynamic rendering, and data entry friction for every candidate. | Notes field and pros/cons fields cover the remaining use cases. |
|
||||
| Score/rating calculation | Automatically rank candidates by computed score | Score algorithms require encoding the user's weight-vs-price preference — personalization complexity. Users distrust opaque scores. | Manual drag-to-rank expresses the user's own weighting without encoding it in an algorithm. |
|
||||
| Side-by-side comparison across threads | Compare candidates from different research threads | Candidates belong to different purchase decisions — mixing them is conceptually incoherent. Different categories are never apples-to-apples. | Thread remains the scope boundary. Cross-thread planning is what setups are for. |
|
||||
| Comparison permalink/share | Share a comparison view URL | GearBox is single-user, no auth for v1. Sharing requires auth, user management, public/private visibility. | Out of scope for v1 per PROJECT.md. Future feature. |
|
||||
| Classification-aware impact preview as MVP requirement | Show delta broken down by base/worn/consumable | While data is available, the classification breakdown adds significant UI complexity. The flat delta answers "will this make my setup lighter?" which is 90% of the use case. | Flat delta for MVP. Classification-aware breakdown as a follow-up enhancement (P2). |
|
||||
|
||||
**Complexity:** LOW. Pure client-side. No API changes. ~100 lines of new component code plus minor state management.
|
||||
|
||||
### 2. Weight Classification (Base/Worn/Consumable)
|
||||
|
||||
**What users expect:** Every item in a setup can be marked as one of three types:
|
||||
- **Base weight**: Items carried in the pack. The fixed weight of your loadout. This is the primary metric ultralight hikers optimize.
|
||||
- **Worn weight**: Items on your body while hiking (shoes, primary clothing, watch, sunglasses). Not counted toward pack weight but tracked as part of "skin-out" weight.
|
||||
- **Consumable weight**: Items that deplete during a trip (food, water, fuel, sunscreen). Variable weight not counted toward base weight.
|
||||
|
||||
**Domain patterns observed:**
|
||||
- LighterPack: Per-item icons (shirt icon = worn, flame icon = consumable). Default = base weight. Totals show base/worn/consumable/total separately.
|
||||
- Packstack: "Separates base weight, worn weight, and consumables so you always know exactly what your pack weighs."
|
||||
- HikeLite: "Mark heavy clothing as worn to see your true base weight."
|
||||
- 99Boulders spreadsheet: Column with dropdown: WORN / CONSUMABLE / - (dash = base).
|
||||
|
||||
**Critical design decision -- classification scope:**
|
||||
In LighterPack, items only exist within lists, so the flag is per-item-per-list inherently. In GearBox, items live in a global collection and are referenced by setups. The classification MUST live on the `setup_items` join table, not on the `items` table. Reason: the same item can have different classifications in different setups (a puffy jacket is "worn" on a cold-weather hike but "base weight" in a three-season setup where it stays packed).
|
||||
|
||||
**Recommended implementation:**
|
||||
- Add `classification TEXT NOT NULL DEFAULT 'base'` column to `setup_items` table
|
||||
- Valid values: `"base"`, `"worn"`, `"consumable"`
|
||||
- Default to `"base"` (most items are base weight; this matches user expectation)
|
||||
- UI: Small segmented control or icon toggle on each item within the setup detail view
|
||||
- LighterPack-style icons: backpack icon (base), shirt icon (worn), flame/droplet icon (consumable)
|
||||
- Setup totals recalculated: show base weight, worn weight, consumable weight, and total (skin-out) as four separate numbers
|
||||
- SQL aggregation update: `SUM(CASE WHEN classification = 'base' THEN weight_grams ELSE 0 END)` etc.
|
||||
|
||||
**Complexity:** MEDIUM. Requires schema migration, API changes (sync endpoint must accept classification), service layer updates, and UI for per-item classification within setup views.
|
||||
|
||||
### 3. Weight Distribution Visualization
|
||||
|
||||
**What users expect:** A chart showing where the weight is. By category is standard. By classification (base/worn/consumable) is a bonus.
|
||||
|
||||
**Domain patterns observed:**
|
||||
- LighterPack: Color-coded pie chart by category, click to drill down. "As you enter each piece of equipment, a pie chart immediately displays a breakdown of where your weight is appropriated." Colors are customizable per category.
|
||||
- PackLight: Bar graph comparing category weights
|
||||
- OutPack: Category breakdown graph
|
||||
|
||||
**Two chart contexts:**
|
||||
1. **Collection-level**: Weight by category across the whole collection. Uses existing `useTotals()` data.
|
||||
2. **Setup-level**: Weight by category AND by classification within a specific setup. More useful because setups represent actual loadouts.
|
||||
|
||||
**Recommended implementation:**
|
||||
- Donut chart (modern feel, consistent with GearBox's minimalist aesthetic)
|
||||
- Library: `react-minimal-pie-chart` (2kB gzipped, zero dependencies, SVG-based) over Recharts (40kB+). GearBox only needs pie/donut -- no line charts, bar charts, etc.
|
||||
- Setup detail view: Donut chart showing weight by category, with center text showing total base weight
|
||||
- Optional toggle: switch between "by category" and "by classification" views
|
||||
- Color assignment: Derive from category or classification type (base = neutral gray, worn = blue, consumable = amber)
|
||||
- Hover tooltips showing category name, weight, and percentage
|
||||
- Responsive: Chart should work on mobile viewports
|
||||
|
||||
**Complexity:** MEDIUM. New dependency, new component, integration with totals data. The chart itself is straightforward; the data aggregation for per-setup-per-category-per-classification is the main work.
|
||||
|
||||
### 4. Candidate Status Tracking
|
||||
|
||||
**What users expect:** This is novel -- no competitor has it. The workflow mirrors real purchase behavior:
|
||||
1. **Researching** (default): You found this product, added it to a thread, and are evaluating it
|
||||
2. **Ordered**: You decided to buy it and placed an order
|
||||
3. **Arrived**: The product has been delivered. Ready for thread resolution.
|
||||
|
||||
**Why this matters:** Without status tracking, threads are a flat list of candidates. With it, threads become a living purchase tracker. A user can see at a glance "I ordered the Nemo Tensor, still researching two other pads."
|
||||
|
||||
**Recommended implementation:**
|
||||
- Add `status TEXT NOT NULL DEFAULT 'researching'` column to `thread_candidates` table
|
||||
- Valid values: `"researching"`, `"ordered"`, `"arrived"`
|
||||
- UI: Status badge on CandidateCard (small colored pill, similar to existing weight/price badges)
|
||||
- Color scheme: researching = gray/neutral, ordered = amber/yellow, arrived = green
|
||||
- Status change: Dropdown or simple click-to-cycle on the candidate card
|
||||
- Thread-level summary: Show count by status ("2 researching, 1 ordered")
|
||||
- When resolving a thread, only candidates with status "arrived" should be selectable as winners (soft constraint -- show a warning, not a hard block, since users may resolve with a "researching" candidate they just bought in-store)
|
||||
|
||||
**Complexity:** LOW. Simple column addition, enum-like text field, badge rendering, optional status transition UI.
|
||||
|
||||
### 5. Weight Unit Selection
|
||||
|
||||
**What users expect:** Choose a preferred unit (grams, ounces, pounds, kilograms) and have ALL weight displays in the app use that unit. LighterPack toggles between g/oz/lb/kg at the top level. The BPL Calculator app lets you "enter item weights in grams and switch the entire display to lbs & oz."
|
||||
|
||||
**Domain patterns observed:**
|
||||
- LighterPack: Toggle at list level between lb/oz/g/kg. Only changes summary display, not per-item display.
|
||||
- Packrat: "Input items in different units, choose how they're displayed, and freely convert between them."
|
||||
- BPL Calculator: Global settings change, applied to all displays
|
||||
- WeighMyGear: Input locked to grams, less intuitive
|
||||
|
||||
**Recommended implementation:**
|
||||
- Store preference in existing `settings` table as `{ key: "weightUnit", value: "g" }` (default: grams)
|
||||
- Supported units: `g` (grams), `oz` (ounces), `lb` (pounds + ounces), `kg` (kilograms)
|
||||
- Conversion constants: 1 oz = 28.3495g, 1 lb = 453.592g, 1 kg = 1000g
|
||||
- Display format per unit:
|
||||
- `g`: "450g" (round to integer)
|
||||
- `oz`: "15.9 oz" (one decimal)
|
||||
- `lb`: "2 lb 3 oz" (pounds + remainder ounces, traditional format)
|
||||
- `kg`: "1.45 kg" (two decimals)
|
||||
- Update `formatWeight()` to accept unit parameter or read from a React context/hook
|
||||
- Settings UI: Simple dropdown or segmented control, accessible from a settings page or inline in the TotalsBar
|
||||
- Internal storage stays as grams (already the case with `weight_grams` column)
|
||||
- Affects: TotalsBar, ItemCard, CandidateCard, SetupCard, CategoryHeader, setup detail view, chart tooltips
|
||||
|
||||
**Complexity:** LOW. No schema changes. Update the `formatWeight()` function, add a settings hook, propagate the unit to all display points. The main effort is touching every component that displays weight (there are ~6-8 call sites).
|
||||
|
||||
### 6. Planning Category Filter with Icon-Aware Dropdown
|
||||
|
||||
**What users expect:** The existing category filter in PlanningView is a plain `<select>` without icons. Since categories now have Lucide icons (v1.1), the filter should show them.
|
||||
|
||||
**Recommended implementation:**
|
||||
- Replace the native `<select>` with a custom dropdown component that renders `<LucideIcon>` alongside category names
|
||||
- Match the visual style of the CategoryPicker used in thread creation
|
||||
- Same functionality, better visual consistency
|
||||
|
||||
**Complexity:** LOW. UI-only change. Replace ~20 lines of `<select>` with a custom dropdown component.
|
||||
---
|
||||
|
||||
## Feature Dependencies
|
||||
|
||||
```
|
||||
[Weight Unit Selection] --independent-- (affects all displays, no schema changes)
|
||||
|
|
||||
+-- should ship first (all other features benefit from correct unit display)
|
||||
[Side-by-side comparison view]
|
||||
└──requires──> [All candidate fields visible in UI]
|
||||
(weightGrams, priceCents, notes, productUrl already in schema)
|
||||
└──enhances──> [Pros/cons fields] (displayed as comparison rows)
|
||||
└──enhances──> [Drag-to-rank] (rank number shown as position in comparison columns)
|
||||
└──enhances──> [Impact preview] (delta displayed per-column inline)
|
||||
|
||||
[Search & Filter] --independent-- (pure client-side, no schema changes)
|
||||
|
|
||||
+-- no dependencies on other v1.2 features
|
||||
[Impact preview (weight + cost delta)]
|
||||
└──requires──> [Setup selector] (user picks which setup to compute delta against)
|
||||
└──requires──> [Setup data client-side] (useSetup hook already exists, no new API)
|
||||
└──requires──> [Candidate weight/price data] (already in threadCandidates schema)
|
||||
|
||||
[Candidate Status Tracking] --independent-- (schema change on thread_candidates only)
|
||||
|
|
||||
+-- no dependencies on other v1.2 features
|
||||
[Setup selector]
|
||||
└──requires──> [useSetups() hook] (already exists in src/client/hooks/useSetups.ts)
|
||||
└──requires──> [useSetup(id) hook] (already exists, loads items with classification)
|
||||
|
||||
[Weight Classification] --depends-on--> [existing setup_items table]
|
||||
|
|
||||
+-- schema migration on setup_items
|
||||
+-- enables [Weight Distribution Visualization]
|
||||
[Drag-to-rank]
|
||||
└──requires──> [rank INTEGER column on threadCandidates] (new — schema migration)
|
||||
└──requires──> [PATCH /api/threads/:id/candidates/rank endpoint] (new API endpoint)
|
||||
└──enhances──> [Side-by-side comparison] (rank visible as position indicator)
|
||||
└──enhances──> [Card grid view] (rank badge on each CandidateCard)
|
||||
|
||||
[Weight Distribution Visualization] --depends-on--> [Weight Classification]
|
||||
|
|
||||
+-- needs classification data to show base/worn/consumable breakdown
|
||||
+-- can show by-category chart without classification (partial value)
|
||||
+-- new dependency: react-minimal-pie-chart
|
||||
|
||||
[Planning Category Filter Icons] --depends-on--> [existing CategoryPicker pattern]
|
||||
|
|
||||
+-- pure UI enhancement
|
||||
[Pros/cons fields]
|
||||
└──requires──> [pros TEXT column on threadCandidates] (new — schema migration)
|
||||
└──requires──> [cons TEXT column on threadCandidates] (new — schema migration)
|
||||
└──requires──> [updateCandidateSchema extended] (add pros/cons to Zod schema)
|
||||
└──enhances──> [CandidateForm edit panel] (new textarea fields)
|
||||
└──enhances──> [Side-by-side comparison] (pros/cons rows in comparison table)
|
||||
```
|
||||
|
||||
### Implementation Order Rationale
|
||||
### Dependency Notes
|
||||
|
||||
1. **Weight Unit Selection** first -- touches formatting everywhere, foundational for all subsequent weight displays
|
||||
2. **Search & Filter** second -- standalone, immediately useful, low risk
|
||||
3. **Candidate Status Tracking** third -- standalone schema change, simple
|
||||
4. **Planning Category Filter** fourth -- quick UI polish
|
||||
5. **Weight Classification** fifth -- most complex schema change, affects setup data model
|
||||
6. **Weight Distribution Visualization** last -- depends on classification, needs chart library, highest UI complexity
|
||||
- **Side-by-side comparison is independent of schema changes.** It can be built using
|
||||
existing candidate data. No migrations required. Delivers value immediately.
|
||||
- **Impact preview is independent of schema changes.** Uses existing `useSetups` and
|
||||
`useSetup` hooks client-side. Delta computation is pure math in the component.
|
||||
No new API endpoint needed for MVP.
|
||||
- **Drag-to-rank requires schema migration.** `rank` column must be added to
|
||||
`threadCandidates`. Default ordering on migration = `createdAt` ascending.
|
||||
- **Pros/cons requires schema migration.** Two nullable `text` columns on
|
||||
`threadCandidates`. Low risk — nullable, backwards compatible.
|
||||
- **Comparison view enhances everything.** Best delivered after rank and pros/cons
|
||||
schema work is done so the full table is useful from day one.
|
||||
|
||||
## Complexity Summary
|
||||
---
|
||||
|
||||
| Feature | Schema Change | API Change | New Dependency | UI Scope | Overall |
|
||||
|---------|---------------|------------|----------------|----------|---------|
|
||||
| Search & Filter | None | None | None | Collection view only | LOW |
|
||||
| Weight Unit Selection | None (uses settings) | None (settings API exists) | None | All weight displays (~8 components) | LOW |
|
||||
| Candidate Status Tracking | `thread_candidates.status` column | Update candidate CRUD | None | CandidateCard, CandidateForm | LOW |
|
||||
| Planning Category Filter | None | None | None | PlanningView dropdown | LOW |
|
||||
| Weight Classification | `setup_items.classification` column | Update setup sync + detail endpoints | None | Setup detail view | MEDIUM |
|
||||
| Weight Distribution Chart | None | Possibly new totals endpoint | react-minimal-pie-chart (~2kB) | New chart component | MEDIUM |
|
||||
## MVP Definition
|
||||
|
||||
### Launch With (v1.3 milestone)
|
||||
|
||||
- [ ] **Side-by-side comparison view** — Core deliverable. Replace mental juggling of the card
|
||||
grid with a scannable table. No schema changes. Highest ROI, lowest risk.
|
||||
- [ ] **Impact preview: flat weight + cost delta per candidate** — Shows `+/- X g` and
|
||||
`+/- $Y` vs. the selected setup. Pure client-side math. No schema changes.
|
||||
- [ ] **Setup selector** — Dropdown of user's setups. Required for impact preview. One
|
||||
interaction: pick a setup, see deltas update.
|
||||
- [ ] **Drag-to-rank** — Requires `rank` column migration. `@dnd-kit/sortable` handles
|
||||
the drag UX. Persist via new PATCH endpoint.
|
||||
- [ ] **Pros/cons text fields** — Requires `pros` + `cons` column migration. Trivially low
|
||||
implementation complexity once schema is in place.
|
||||
|
||||
### Add After Validation (v1.x)
|
||||
|
||||
- [ ] **Classification-aware impact preview** — Delta broken down by base/worn/consumable.
|
||||
Higher complexity UI. Add once flat delta is validated as useful.
|
||||
Trigger: user feedback requests "which classification does this affect?"
|
||||
- [ ] **Rank indicator on card grid** — Small "1st", "2nd" badge on CandidateCard.
|
||||
Trigger: users express confusion about which candidate is ranked first without entering
|
||||
comparison view.
|
||||
- [ ] **Comparison view on mobile** — Horizontal scroll works but is not ideal. Consider
|
||||
attribute-focus swipe view. Trigger: usage data shows mobile traffic on thread pages.
|
||||
|
||||
### Future Consideration (v2+)
|
||||
|
||||
- [ ] **Comparison permalink** — Requires auth/multi-user work first.
|
||||
- [ ] **Auto-fill from product URL** — Fragile scraping, rejected in PROJECT.md.
|
||||
- [ ] **Custom comparison attributes** — Explicitly rejected in PROJECT.md.
|
||||
|
||||
---
|
||||
|
||||
## Feature Prioritization Matrix
|
||||
|
||||
| Feature | User Value | Implementation Cost | Priority |
|
||||
|---------|------------|---------------------|----------|
|
||||
| Side-by-side comparison view | HIGH | MEDIUM | P1 |
|
||||
| Setup impact preview (flat delta) | HIGH | LOW | P1 |
|
||||
| Setup selector for impact preview | HIGH | LOW | P1 |
|
||||
| Drag-to-rank ordering | MEDIUM | MEDIUM | P1 |
|
||||
| Pros/cons text fields | MEDIUM | LOW | P1 |
|
||||
| Classification-aware impact preview | MEDIUM | HIGH | P2 |
|
||||
| Rank indicator on card grid | LOW | LOW | P2 |
|
||||
| Mobile-optimized comparison view | LOW | MEDIUM | P3 |
|
||||
|
||||
**Priority key:**
|
||||
- P1: Must have for this milestone launch
|
||||
- P2: Should have, add when possible
|
||||
- P3: Nice to have, future consideration
|
||||
|
||||
---
|
||||
|
||||
## Competitor Feature Analysis
|
||||
|
||||
| Feature | LighterPack | GearGrams | OutPack | Our Approach |
|
||||
|---------|-------------|-----------|---------|--------------|
|
||||
| Side-by-side candidate comparison | None (list only) | None (library + trip list) | None | Inline comparison table on thread detail page, toggle from grid view |
|
||||
| Impact preview / weight delta | None (duplicate lists manually to compare) | None (no delta concept) | None | Per-candidate delta vs. selected setup, computed client-side |
|
||||
| Candidate ranking | None | None | None | Drag-to-rank with persisted `rank` column |
|
||||
| Pros/cons annotation | None (notes field only) | None (notes field only) | None | Dedicated `pros` and `cons` fields separate from general notes |
|
||||
| Status tracking | None | "wish list" item flag only | None | Already built in v1.2 (researching/ordered/arrived) |
|
||||
|
||||
**Key insight:** No existing gear management tool has a comparison view, delta preview, or
|
||||
ranking system for candidates within a research thread. This is an unmet-need gap.
|
||||
The features are adapted from general product comparison UX (e-commerce) to the gear domain.
|
||||
|
||||
---
|
||||
|
||||
## Implementation Notes by Feature
|
||||
|
||||
### Side-by-side Comparison View
|
||||
|
||||
- Rendered as a transposed table: rows = attribute labels, columns = candidates.
|
||||
- Rows: Image (thumbnail), Name, Weight, Price, Status, Notes, Link, Pros, Cons, Rank, Impact Delta (weight), Impact Delta (cost).
|
||||
- Sticky first column (attribute label) while candidate columns scroll horizontally for 3+.
|
||||
- Candidate images at reduced aspect ratio (square thumbnail ~80px).
|
||||
- Weight/price cells use existing `formatWeight` / `formatPrice` formatters with the user's preferred unit.
|
||||
- Status cell reuses existing `StatusBadge` component.
|
||||
- "Pick as winner" action available per column (reuses existing `openResolveDialog`).
|
||||
- Toggle between grid view (current) and table view. Preserve both modes. Default to grid;
|
||||
user activates comparison mode explicitly.
|
||||
- Comparison mode is a UI state only (Zustand or local component state) — no URL change needed.
|
||||
|
||||
### Impact Preview
|
||||
|
||||
- Setup selector: `<select>` or custom dropdown populated from `useSetups()`.
|
||||
- On selection: load setup via `useSetup(id)`. Compute delta per candidate:
|
||||
`candidate.weightGrams - matchingCategoryWeight` where `matchingCategoryWeight` is the
|
||||
sum of setup item weights in the same category as the thread.
|
||||
- Delta display: colored pill on each candidate column in comparison view:
|
||||
- Negative delta (lighter) = green, prefixed with "−"
|
||||
- Positive delta (heavier) = red, prefixed with "+"
|
||||
- Zero = neutral gray
|
||||
- Same pattern for cost delta.
|
||||
- "No setup selected" state = no delta row shown.
|
||||
- "Category not in setup" state = "not in setup" label instead of delta.
|
||||
- No new API endpoints required. All data is client-side once setups are loaded.
|
||||
|
||||
### Drag-to-Rank
|
||||
|
||||
- `@dnd-kit/sortable` with `SortableContext` wrapping the candidate list.
|
||||
- `useSortable` hook per candidate with a drag handle (Lucide `grip-vertical` icon).
|
||||
- Drag handle visible always (not hover-only) so the affordance is clear.
|
||||
- On `onDragEnd`: recompute ranks using `arrayMove()`, call
|
||||
`PATCH /api/threads/:threadId/candidates/rank` with `{ orderedIds: number[] }`.
|
||||
- Server endpoint: bulk update `rank` for each candidate ID in the thread atomically.
|
||||
- `rank` column: `INTEGER` nullable. Null = unranked (treated as lowest rank). Default to
|
||||
`createdAt` order on first explicit rank save.
|
||||
- Rank number badge: displayed on each CandidateCard corner (small gray circle, "1", "2", "3").
|
||||
- Works in both grid view and comparison view.
|
||||
|
||||
### Pros/Cons Fields
|
||||
|
||||
- Two `textarea` inputs added to the existing `CandidateForm` (slide-out panel).
|
||||
- Labels: "Pros" and "Cons" — plain text, no icons.
|
||||
- Displayed below the existing Notes field in the form.
|
||||
- Extend `updateCandidateSchema` with `pros: z.string().optional()` and `cons: z.string().optional()`.
|
||||
- In comparison table: pros and cons rows display as plain text, line-wrapped.
|
||||
- In card grid: pros/cons not shown on card surface (too much density). Visible only in edit
|
||||
panel and comparison view.
|
||||
|
||||
### Schema Changes Required
|
||||
|
||||
Two schema migrations needed for this milestone:
|
||||
|
||||
```sql
|
||||
-- Migration 1: Candidate rank
|
||||
ALTER TABLE thread_candidates ADD COLUMN rank INTEGER;
|
||||
|
||||
-- Migration 2: Candidate pros/cons
|
||||
ALTER TABLE thread_candidates ADD COLUMN pros TEXT;
|
||||
ALTER TABLE thread_candidates ADD COLUMN cons TEXT;
|
||||
```
|
||||
|
||||
Both columns are nullable and backwards compatible. Existing candidates get `NULL` values.
|
||||
UI treats `NULL` rank as unranked, `NULL` pros/cons as empty string.
|
||||
|
||||
---
|
||||
|
||||
## Sources
|
||||
|
||||
- [LighterPack](https://lighterpack.com/) -- weight classification and pie chart visualization patterns
|
||||
- [LighterPack Tutorial (99Boulders)](https://www.99boulders.com/lighterpack-tutorial) -- detailed feature walkthrough
|
||||
- [LighterPack Tutorial (Backpackers.com)](https://backpackers.com/blog/how-to-calculate-backpack-weight-with-lighterpack/) -- base/worn/consumable definitions
|
||||
- [Hikt](https://hikt.app/) -- searchable gear closet, base vs worn weight display
|
||||
- [PackLight (iOS)](https://apps.apple.com/us/app/packlight-for-backpacking/id1054845207) -- search, custom categories, bar graph visualization
|
||||
- [Packstack](https://www.packstack.io/) -- base/worn/consumable weight separation
|
||||
- [HikeLite](https://hikeliteapp.com/) -- worn weight marking, CSV import format
|
||||
- [Packrat](https://www.packrat.app/) -- flexible weight unit input and display conversion
|
||||
- [BPL Calculator Forum Discussion](https://backpackinglight.com/forums/topic/new-backpacking-hiking-weight-calculator-app/) -- unit conversion UX, search filter patterns
|
||||
- [react-minimal-pie-chart (GitHub)](https://github.com/toomuchdesign/react-minimal-pie-chart) -- 2kB lightweight chart library
|
||||
- [Best React Chart Libraries 2025 (LogRocket)](https://blog.logrocket.com/best-react-chart-libraries-2025/) -- chart library comparison
|
||||
- [LighterPack GitHub Issues](https://github.com/galenmaly/lighterpack/issues) -- user feature requests
|
||||
- [OutPack](https://outpack.app/) -- modern LighterPack alternative with category breakdown graphs
|
||||
- [Pack Weight Calculator Guide (BackpackPeek)](https://backpackpeek.com/blog/pack-weight-calculator-base-weight-guide) -- base weight calculation methodology
|
||||
- [Designing The Perfect Feature Comparison Table — Smashing Magazine](https://www.smashingmagazine.com/2017/08/designing-perfect-feature-comparison-table/) — table layout patterns, sticky headers, progressive disclosure (HIGH confidence)
|
||||
- [Comparison Tables for Products, Services, and Features — Nielsen Norman Group](https://www.nngroup.com/articles/comparison-tables/) — information architecture for comparison, anti-patterns (HIGH confidence)
|
||||
- [The Ultimate Drag-and-Drop Toolkit for React: @dnd-kit — BrightCoding (2025)](https://www.blog.brightcoding.dev/2025/08/21/the-ultimate-drag-and-drop-toolkit-for-react-a-deep-dive-into-dnd-kit/) — confirmed dnd-kit as current standard, react-beautiful-dnd abandoned (HIGH confidence)
|
||||
- [dnd-kit Sortable Docs](https://docs.dndkit.com/presets/sortable) — SortableContext, useSortable, arrayMove patterns (HIGH confidence)
|
||||
- [Ultralight: The Gear Tracking App I'm Leaving LighterPack For — TrailsMag](https://trailsmag.net/blogs/hiker-box/ultralight-the-gear-tracking-app-i-m-leaving-lighterpack-for) — LighterPack feature gap analysis (MEDIUM confidence)
|
||||
- [Comparing products: UX design best practices — Contentsquare](https://contentsquare.com/blog/comparing-products-design-practices-to-help-your-users-avoid-fragmented-comparison-7/) — fragmented comparison UX pitfalls (HIGH confidence)
|
||||
- [Drag and drop UI examples and UX tips — Eleken](https://www.eleken.co/blog-posts/drag-and-drop-ui) — drag affordance and visual feedback patterns (MEDIUM confidence)
|
||||
- GearBox codebase analysis (src/db/schema.ts, src/server/services/, src/client/hooks/) — confirmed existing data model, no rank/pros/cons columns present (HIGH confidence)
|
||||
|
||||
---
|
||||
*Feature research for: v1.2 Collection Power-Ups (search/filter, weight classification, visualization, candidate status, weight units)*
|
||||
*Feature research for: GearBox v1.3 — candidate comparison, setup impact preview, candidate ranking*
|
||||
*Researched: 2026-03-16*
|
||||
|
||||
Reference in New Issue
Block a user