Merge branch 'worktree-agent-adbc35a5' into Develop
# Conflicts: # .planning/STATE.md # drizzle-pg/meta/0002_snapshot.json # drizzle-pg/meta/_journal.json # src/db/schema.ts
This commit is contained in:
@@ -2,16 +2,16 @@
|
||||
gsd_state_version: 1.0
|
||||
milestone: v1.3
|
||||
milestone_name: Research & Decision Tools
|
||||
status: executing
|
||||
stopped_at: Phase 20 context gathered
|
||||
last_updated: "2026-04-06T05:52:12.097Z"
|
||||
last_activity: 2026-04-06 -- Phase 20 execution started
|
||||
status: planning
|
||||
stopped_at: Completed 20-01-PLAN.md
|
||||
last_updated: "2026-04-06T05:59:26.689Z"
|
||||
last_activity: 2026-04-05
|
||||
progress:
|
||||
total_phases: 14
|
||||
completed_phases: 12
|
||||
total_plans: 38
|
||||
completed_plans: 34
|
||||
percent: 3
|
||||
total_phases: 13
|
||||
completed_phases: 11
|
||||
total_plans: 33
|
||||
completed_plans: 32
|
||||
percent: 0
|
||||
---
|
||||
|
||||
# Project State
|
||||
@@ -21,16 +21,16 @@ progress:
|
||||
See: .planning/PROJECT.md (updated 2026-04-03)
|
||||
|
||||
**Core value:** Help people make better gear decisions — discover what others use, compare real-world data, and see how a potential buy affects your setup before committing.
|
||||
**Current focus:** Phase 20 — fab-full-screen-catalog-search
|
||||
**Current focus:** v2.0 Platform Foundation — Phase 14 (PostgreSQL Migration)
|
||||
|
||||
## Current Position
|
||||
|
||||
Phase: 20 (fab-full-screen-catalog-search) — EXECUTING
|
||||
Plan: 1 of 2
|
||||
Status: Executing Phase 20
|
||||
Last activity: 2026-04-06 -- Phase 20 execution started
|
||||
Phase: 18 of 18 (PostgreSQL Migration)
|
||||
Plan: Not started
|
||||
Status: Ready to plan
|
||||
Last activity: 2026-04-05
|
||||
|
||||
Progress: [#---------] 3% (v2.0 milestone)
|
||||
Progress: [----------] 0% (v2.0 milestone)
|
||||
|
||||
## Performance Metrics
|
||||
|
||||
@@ -55,11 +55,7 @@ Key decisions made during v2.0 planning:
|
||||
- Separate globalItems table — not a flag on user items table
|
||||
- Single-user SQLite mode diverges at v2.0 boundary
|
||||
- [Phase 18]: Profile data loaded via usePublicProfile(userId) not /auth/me extension
|
||||
- [Phase 19]: Direct globalItemId FK on items replaces itemGlobalLinks junction table
|
||||
- [Phase 19]: Data migration SQL: UPDATE items before DROP TABLE item_global_links
|
||||
- [Phase 19]: Flat tags system without type categorization per D-16
|
||||
- [Phase 19-reference-item-model-tags-schema]: COALESCE merge pattern for transparent reference item data in item/thread services
|
||||
- [Phase 19-reference-item-model-tags-schema]: COALESCE merge pattern propagated to all secondary services (setup, totals, profile, CSV)
|
||||
- [Phase 20]: Created tags table in schema (was missing, needed for GET /api/tags endpoint)
|
||||
|
||||
### Pending Todos
|
||||
|
||||
@@ -72,6 +68,6 @@ None active.
|
||||
|
||||
## Session Continuity
|
||||
|
||||
Last session: 2026-04-06T05:38:18.950Z
|
||||
Stopped at: Phase 20 context gathered
|
||||
Resume file: .planning/phases/20-fab-full-screen-catalog-search/20-CONTEXT.md
|
||||
Last session: 2026-04-06T05:59:26.687Z
|
||||
Stopped at: Completed 20-01-PLAN.md
|
||||
Resume file: None
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
---
|
||||
phase: 20-fab-full-screen-catalog-search
|
||||
plan: 01
|
||||
subsystem: api, ui
|
||||
tags: [hono, zustand, tanstack-query, drizzle, tags, global-items]
|
||||
|
||||
requires:
|
||||
- phase: 19-reference-item-model-tags-schema
|
||||
provides: global-items service and route, schema foundation
|
||||
provides:
|
||||
- GET /api/tags endpoint returning all tags
|
||||
- GET /api/global-items route registration in index.ts
|
||||
- UIStore FAB menu and catalog search state slices
|
||||
- useTags hook with 5-min stale cache
|
||||
- useGlobalItems hook with optional tags parameter
|
||||
affects: [20-02-PLAN, phase-21]
|
||||
|
||||
tech-stack:
|
||||
added: []
|
||||
patterns: [public-read auth skip for new GET endpoints]
|
||||
|
||||
key-files:
|
||||
created:
|
||||
- src/server/services/tag.service.ts
|
||||
- src/server/routes/tags.ts
|
||||
- src/client/hooks/useTags.ts
|
||||
- tests/services/tag.service.test.ts
|
||||
- tests/routes/tags.test.ts
|
||||
- drizzle-pg/0002_square_pyro.sql
|
||||
modified:
|
||||
- src/db/schema.ts
|
||||
- src/server/index.ts
|
||||
- src/client/stores/uiStore.ts
|
||||
- src/client/hooks/useGlobalItems.ts
|
||||
|
||||
key-decisions:
|
||||
- "Created tags table in schema (was missing, needed for GET /api/tags)"
|
||||
- "Tags endpoint is public-read (no auth), consistent with global-items"
|
||||
|
||||
patterns-established:
|
||||
- "Tag service pattern: select specific columns (id, name) not full row"
|
||||
|
||||
requirements-completed: [CATFLOW-01, CATFLOW-02]
|
||||
|
||||
duration: 5min
|
||||
completed: 2026-04-06
|
||||
---
|
||||
|
||||
# Phase 20 Plan 01: Tags API, Route Registration, and UI State Summary
|
||||
|
||||
**Tags endpoint with alphabetical ordering, global-items route registration, UIStore FAB/catalog-search state, and tag-aware useGlobalItems hook**
|
||||
|
||||
## Performance
|
||||
|
||||
- **Duration:** 5 min
|
||||
- **Started:** 2026-04-06T05:53:35Z
|
||||
- **Completed:** 2026-04-06T05:58:27Z
|
||||
- **Tasks:** 2
|
||||
- **Files modified:** 10
|
||||
|
||||
## Accomplishments
|
||||
- Created tags table, service, and route with full test coverage (4 tests)
|
||||
- Registered previously unregistered global-items route in index.ts
|
||||
- Added public-read auth skips for both /api/tags and /api/global-items
|
||||
- Extended UIStore with FAB menu state (open/close) and catalog search overlay state (open with mode, close)
|
||||
- Created useTags hook with 5-minute staleTime caching
|
||||
- Updated useGlobalItems hook to accept optional tags array for filtering
|
||||
|
||||
## Task Commits
|
||||
|
||||
Each task was committed atomically:
|
||||
|
||||
1. **Task 1 (RED): Tag service and route tests** - `6f07e87` (test)
|
||||
2. **Task 1 (GREEN): Tags table, service, route, registrations** - `2ec1276` (feat)
|
||||
3. **Task 2: UIStore extension, useTags hook, useGlobalItems update** - `67facea` (feat)
|
||||
|
||||
## Files Created/Modified
|
||||
- `src/db/schema.ts` - Added tags table definition
|
||||
- `drizzle-pg/0002_square_pyro.sql` - Migration for tags table
|
||||
- `src/server/services/tag.service.ts` - getAllTags function (id+name, alphabetical)
|
||||
- `src/server/routes/tags.ts` - GET / handler returning all tags
|
||||
- `src/server/index.ts` - Registered global-items and tags routes, added auth skips
|
||||
- `src/client/stores/uiStore.ts` - Added FAB menu and catalog search state slices
|
||||
- `src/client/hooks/useTags.ts` - Tag fetching hook with staleTime cache
|
||||
- `src/client/hooks/useGlobalItems.ts` - Added optional tags parameter
|
||||
- `tests/services/tag.service.test.ts` - Service-level tests for getAllTags
|
||||
- `tests/routes/tags.test.ts` - Route-level tests for GET /api/tags
|
||||
|
||||
## Decisions Made
|
||||
- Created tags table in schema since it was referenced by the plan but didn't exist yet (Rule 3 deviation)
|
||||
- Made both /api/tags and /api/global-items public-read (GET requests skip auth middleware)
|
||||
|
||||
## Deviations from Plan
|
||||
|
||||
### Auto-fixed Issues
|
||||
|
||||
**1. [Rule 3 - Blocking] Created missing tags table in schema**
|
||||
- **Found during:** Task 1 (Tag service implementation)
|
||||
- **Issue:** Plan referenced `tags` table from schema.ts but no such table existed in the database schema
|
||||
- **Fix:** Added tags table definition to schema.ts and generated migration (0002_square_pyro.sql)
|
||||
- **Files modified:** src/db/schema.ts, drizzle-pg/0002_square_pyro.sql, drizzle-pg/meta/
|
||||
- **Verification:** Migration generated successfully, tests pass with PGlite
|
||||
- **Committed in:** 2ec1276 (Task 1 GREEN commit)
|
||||
|
||||
---
|
||||
|
||||
**Total deviations:** 1 auto-fixed (1 blocking)
|
||||
**Impact on plan:** Essential for task completion. Tags table is required by the entire phase. No scope creep.
|
||||
|
||||
## Issues Encountered
|
||||
- Pre-existing global-items route test failures (9 of 10 tests fail) due to async/sync mismatch in test helper usage. Out of scope for this plan.
|
||||
|
||||
## User Setup Required
|
||||
None - no external service configuration required.
|
||||
|
||||
## Known Stubs
|
||||
None - all functionality is fully wired.
|
||||
|
||||
## Next Phase Readiness
|
||||
- Tags endpoint and UIStore state ready for Plan 02's UI components (FabMenu, CatalogSearchOverlay, TagChips)
|
||||
- useTags and useGlobalItems hooks ready for consumption by overlay components
|
||||
|
||||
---
|
||||
*Phase: 20-fab-full-screen-catalog-search*
|
||||
*Completed: 2026-04-06*
|
||||
@@ -0,0 +1,145 @@
|
||||
<h2>Catalog Browse & Search Experience</h2>
|
||||
<p class="subtitle">How should users find gear in the global catalog?</p>
|
||||
|
||||
<div class="options">
|
||||
<div class="option" data-choice="a" onclick="toggleSelect(this)">
|
||||
<div class="letter">A</div>
|
||||
<div class="content">
|
||||
<h3>Search-First</h3>
|
||||
<p style="margin-bottom:12px;">Big search bar dominates. Tags below as quick filters. Type naturally: "waterproof handlebar bag under 200g". Results appear as you type.</p>
|
||||
<div class="mockup">
|
||||
<div class="mockup-header">Global Catalog — Search-First</div>
|
||||
<div class="mockup-body" style="padding: 20px;">
|
||||
<div style="display:flex; gap:8px; margin-bottom:16px;">
|
||||
<input class="mock-input" placeholder="Search gear... e.g. 'waterproof handlebar bag under 200g'" style="flex:1; font-size:15px; padding:10px 14px;">
|
||||
</div>
|
||||
<div style="display:flex; gap:8px; margin-bottom:20px; flex-wrap:wrap;">
|
||||
<span style="padding:4px 12px; border-radius:99px; background:#f3f4f6; font-size:13px; color:#6b7280; cursor:pointer;">Bikepacking</span>
|
||||
<span style="padding:4px 12px; border-radius:99px; background:#f3f4f6; font-size:13px; color:#6b7280; cursor:pointer;">Bags</span>
|
||||
<span style="padding:4px 12px; border-radius:99px; background:#f3f4f6; font-size:13px; color:#6b7280; cursor:pointer;">Waterproof</span>
|
||||
<span style="padding:4px 12px; border-radius:99px; background:#f3f4f6; font-size:13px; color:#6b7280; cursor:pointer;">Ultralight</span>
|
||||
<span style="padding:4px 12px; border-radius:99px; background:#f3f4f6; font-size:13px; color:#6b7280; cursor:pointer;">Shelter</span>
|
||||
<span style="padding:4px 12px; border-radius:99px; background:#f3f4f6; font-size:13px; color:#6b7280; cursor:pointer;">Cooking</span>
|
||||
</div>
|
||||
<div style="color:#9ca3af; font-size:13px; margin-bottom:16px;">Type to search or click tags to filter</div>
|
||||
<div style="display:grid; grid-template-columns: 1fr 1fr; gap:12px;">
|
||||
<div style="border:1px solid #e5e7eb; border-radius:12px; padding:14px;">
|
||||
<div style="display:flex; justify-content:space-between; align-items:start;">
|
||||
<div>
|
||||
<div style="font-weight:600; font-size:14px;">Revelate Designs Pika</div>
|
||||
<div style="color:#6b7280; font-size:12px;">Handlebar Bag</div>
|
||||
</div>
|
||||
<div style="background:#fef3c7; color:#92400e; font-size:11px; padding:2px 8px; border-radius:99px;">★ 4.6</div>
|
||||
</div>
|
||||
<div style="display:flex; gap:16px; margin-top:10px; font-size:12px; color:#6b7280;">
|
||||
<span>128g</span>
|
||||
<span>$85</span>
|
||||
<span>24 owners</span>
|
||||
</div>
|
||||
<div style="display:flex; gap:4px; margin-top:8px; flex-wrap:wrap;">
|
||||
<span style="padding:2px 8px; border-radius:99px; background:#eff6ff; font-size:11px; color:#3b82f6;">bikepacking</span>
|
||||
<span style="padding:2px 8px; border-radius:99px; background:#eff6ff; font-size:11px; color:#3b82f6;">handlebar</span>
|
||||
<span style="padding:2px 8px; border-radius:99px; background:#eff6ff; font-size:11px; color:#3b82f6;">waterproof</span>
|
||||
</div>
|
||||
</div>
|
||||
<div style="border:1px solid #e5e7eb; border-radius:12px; padding:14px;">
|
||||
<div style="display:flex; justify-content:space-between; align-items:start;">
|
||||
<div>
|
||||
<div style="font-weight:600; font-size:14px;">Apidura Racing HB Pack</div>
|
||||
<div style="color:#6b7280; font-size:12px;">Handlebar Bag</div>
|
||||
</div>
|
||||
<div style="background:#fef3c7; color:#92400e; font-size:11px; padding:2px 8px; border-radius:99px;">★ 4.4</div>
|
||||
</div>
|
||||
<div style="display:flex; gap:16px; margin-top:10px; font-size:12px; color:#6b7280;">
|
||||
<span>105g</span>
|
||||
<span>$72</span>
|
||||
<span>18 owners</span>
|
||||
</div>
|
||||
<div style="display:flex; gap:4px; margin-top:8px; flex-wrap:wrap;">
|
||||
<span style="padding:2px 8px; border-radius:99px; background:#eff6ff; font-size:11px; color:#3b82f6;">bikepacking</span>
|
||||
<span style="padding:2px 8px; border-radius:99px; background:#eff6ff; font-size:11px; color:#3b82f6;">handlebar</span>
|
||||
<span style="padding:2px 8px; border-radius:99px; background:#eff6ff; font-size:11px; color:#3b82f6;">ultralight</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pros-cons" style="margin-top:12px;">
|
||||
<div class="pros"><h4>Pros</h4><ul><li>Natural language friendly</li><li>Fast for users who know what they want</li><li>Tags as quick filters below search</li></ul></div>
|
||||
<div class="cons"><h4>Cons</h4><ul><li>Empty state before typing</li><li>Browsing requires typing first</li></ul></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="option" data-choice="b" onclick="toggleSelect(this)">
|
||||
<div class="letter">B</div>
|
||||
<div class="content">
|
||||
<h3>Browse-First with Search</h3>
|
||||
<p style="margin-bottom:12px;">Category tiles for discovery with trending items. Search bar prominent but browsing is the default. Great for "I don't know exactly what I need yet."</p>
|
||||
<div class="mockup">
|
||||
<div class="mockup-header">Global Catalog — Browse-First</div>
|
||||
<div class="mockup-body" style="padding: 20px;">
|
||||
<div style="display:flex; gap:8px; margin-bottom:16px;">
|
||||
<input class="mock-input" placeholder="Search gear..." style="flex:1; font-size:15px; padding:10px 14px;">
|
||||
</div>
|
||||
<div style="margin-bottom:20px;">
|
||||
<div style="font-size:13px; font-weight:600; color:#374151; margin-bottom:8px;">Popular Categories</div>
|
||||
<div style="display:grid; grid-template-columns: repeat(3, 1fr); gap:8px;">
|
||||
<div style="border:1px solid #e5e7eb; border-radius:10px; padding:12px; text-align:center; cursor:pointer;">
|
||||
<div style="font-size:20px; margin-bottom:4px;">🎒</div>
|
||||
<div style="font-size:13px; font-weight:500;">Bags</div>
|
||||
<div style="font-size:11px; color:#9ca3af;">342 items</div>
|
||||
</div>
|
||||
<div style="border:1px solid #e5e7eb; border-radius:10px; padding:12px; text-align:center; cursor:pointer;">
|
||||
<div style="font-size:20px; margin-bottom:4px;">🏕️</div>
|
||||
<div style="font-size:13px; font-weight:500;">Shelter</div>
|
||||
<div style="font-size:11px; color:#9ca3af;">128 items</div>
|
||||
</div>
|
||||
<div style="border:1px solid #e5e7eb; border-radius:10px; padding:12px; text-align:center; cursor:pointer;">
|
||||
<div style="font-size:20px; margin-bottom:4px;">🍳</div>
|
||||
<div style="font-size:13px; font-weight:500;">Cooking</div>
|
||||
<div style="font-size:11px; color:#9ca3af;">94 items</div>
|
||||
</div>
|
||||
<div style="border:1px solid #e5e7eb; border-radius:10px; padding:12px; text-align:center; cursor:pointer;">
|
||||
<div style="font-size:20px; margin-bottom:4px;">🚲</div>
|
||||
<div style="font-size:13px; font-weight:500;">Bike Parts</div>
|
||||
<div style="font-size:11px; color:#9ca3af;">215 items</div>
|
||||
</div>
|
||||
<div style="border:1px solid #e5e7eb; border-radius:10px; padding:12px; text-align:center; cursor:pointer;">
|
||||
<div style="font-size:20px; margin-bottom:4px;">💡</div>
|
||||
<div style="font-size:13px; font-weight:500;">Lights</div>
|
||||
<div style="font-size:11px; color:#9ca3af;">67 items</div>
|
||||
</div>
|
||||
<div style="border:1px solid #e5e7eb; border-radius:10px; padding:12px; text-align:center; cursor:pointer;">
|
||||
<div style="font-size:20px; margin-bottom:4px;">🧭</div>
|
||||
<div style="font-size:13px; font-weight:500;">Navigation</div>
|
||||
<div style="font-size:11px; color:#9ca3af;">43 items</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div style="font-size:13px; font-weight:600; color:#374151; margin-bottom:8px;">Trending</div>
|
||||
<div style="border:1px solid #e5e7eb; border-radius:12px; padding:12px; display:flex; justify-content:space-between; align-items:center; margin-bottom:6px;">
|
||||
<div>
|
||||
<div style="font-weight:600; font-size:14px;">Revelate Designs Pika</div>
|
||||
<div style="font-size:12px; color:#6b7280;">128g · $85 · ★ 4.6</div>
|
||||
</div>
|
||||
<div style="font-size:12px; color:#6b7280;">24 owners</div>
|
||||
</div>
|
||||
<div style="border:1px solid #e5e7eb; border-radius:12px; padding:12px; display:flex; justify-content:space-between; align-items:center;">
|
||||
<div>
|
||||
<div style="font-weight:600; font-size:14px;">Ortlieb Fork-Pack</div>
|
||||
<div style="font-size:12px; color:#6b7280;">310g · $55 · ★ 4.3</div>
|
||||
</div>
|
||||
<div style="font-size:12px; color:#6b7280;">19 owners</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pros-cons" style="margin-top:12px;">
|
||||
<div class="pros"><h4>Pros</h4><ul><li>Great for discovery and browsing</li><li>Never an empty state</li><li>Shows what's popular in the community</li></ul></div>
|
||||
<div class="cons"><h4>Cons</h4><ul><li>Categories need curation</li><li>Slower for targeted searches</li></ul></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,128 @@
|
||||
<h2>Catalog Browse & Search Experience</h2>
|
||||
<p class="subtitle">How should users find gear in the global catalog?</p>
|
||||
|
||||
<div class="options">
|
||||
<div class="option" data-choice="a" onclick="toggleSelect(this)">
|
||||
<div class="letter">A</div>
|
||||
<div class="content">
|
||||
<h3>Search-First</h3>
|
||||
<div class="mockup" style="margin-top: 12px;">
|
||||
<div class="mockup-header">Global Catalog</div>
|
||||
<div class="mockup-body" style="padding: 20px;">
|
||||
<div style="display:flex; gap:8px; margin-bottom:16px;">
|
||||
<input class="mock-input" placeholder="Search gear... e.g. 'waterproof handlebar bag under 200g'" style="flex:1; font-size:15px; padding:10px 14px;">
|
||||
</div>
|
||||
<div style="display:flex; gap:8px; margin-bottom:20px; flex-wrap:wrap;">
|
||||
<span style="padding:4px 12px; border-radius:99px; background:#f3f4f6; font-size:13px; color:#6b7280;">Bikepacking</span>
|
||||
<span style="padding:4px 12px; border-radius:99px; background:#f3f4f6; font-size:13px; color:#6b7280;">Bags</span>
|
||||
<span style="padding:4px 12px; border-radius:99px; background:#f3f4f6; font-size:13px; color:#6b7280;">Waterproof</span>
|
||||
<span style="padding:4px 12px; border-radius:99px; background:#f3f4f6; font-size:13px; color:#6b7280;">Ultralight</span>
|
||||
<span style="padding:4px 12px; border-radius:99px; background:#f3f4f6; font-size:13px; color:#6b7280;">Sim Racing</span>
|
||||
<span style="padding:4px 12px; border-radius:99px; background:#f3f4f6; font-size:13px; color:#6b7280;">Wheels</span>
|
||||
</div>
|
||||
<div style="color:#9ca3af; font-size:13px; margin-bottom:12px;">Type to search or click tags to filter</div>
|
||||
<div style="display:grid; grid-template-columns: 1fr 1fr; gap:12px;">
|
||||
<div style="border:1px solid #e5e7eb; border-radius:12px; padding:14px;">
|
||||
<div style="display:flex; justify-content:space-between; align-items:start;">
|
||||
<div>
|
||||
<div style="font-weight:600; font-size:14px;">Revelate Designs Pika</div>
|
||||
<div style="color:#6b7280; font-size:12px;">Handlebar Bag</div>
|
||||
</div>
|
||||
<div style="background:#fef3c7; color:#92400e; font-size:11px; padding:2px 8px; border-radius:99px;">★ 4.6</div>
|
||||
</div>
|
||||
<div style="display:flex; gap:16px; margin-top:10px; font-size:12px; color:#6b7280;">
|
||||
<span>128g</span>
|
||||
<span>$85</span>
|
||||
<span>24 owners</span>
|
||||
</div>
|
||||
<div style="display:flex; gap:4px; margin-top:8px; flex-wrap:wrap;">
|
||||
<span style="padding:2px 8px; border-radius:99px; background:#eff6ff; font-size:11px; color:#3b82f6;">bikepacking</span>
|
||||
<span style="padding:2px 8px; border-radius:99px; background:#eff6ff; font-size:11px; color:#3b82f6;">handlebar</span>
|
||||
<span style="padding:2px 8px; border-radius:99px; background:#eff6ff; font-size:11px; color:#3b82f6;">waterproof</span>
|
||||
</div>
|
||||
</div>
|
||||
<div style="border:1px solid #e5e7eb; border-radius:12px; padding:14px;">
|
||||
<div style="display:flex; justify-content:space-between; align-items:start;">
|
||||
<div>
|
||||
<div style="font-weight:600; font-size:14px;">Apidura Racing HB Pack</div>
|
||||
<div style="color:#6b7280; font-size:12px;">Handlebar Bag</div>
|
||||
</div>
|
||||
<div style="background:#fef3c7; color:#92400e; font-size:11px; padding:2px 8px; border-radius:99px;">★ 4.4</div>
|
||||
</div>
|
||||
<div style="display:flex; gap:16px; margin-top:10px; font-size:12px; color:#6b7280;">
|
||||
<span>105g</span>
|
||||
<span>$72</span>
|
||||
<span>18 owners</span>
|
||||
</div>
|
||||
<div style="display:flex; gap:4px; margin-top:8px; flex-wrap:wrap;">
|
||||
<span style="padding:2px 8px; border-radius:99px; background:#eff6ff; font-size:11px; color:#3b82f6;">bikepacking</span>
|
||||
<span style="padding:2px 8px; border-radius:99px; background:#eff6ff; font-size:11px; color:#3b82f6;">handlebar</span>
|
||||
<span style="padding:2px 8px; border-radius:99px; background:#eff6ff; font-size:11px; color:#3b82f6;">ultralight</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pros-cons" style="margin-top:12px;">
|
||||
<div class="pros"><h4>Pros</h4><ul><li>Natural language friendly</li><li>Fast for users who know what they want</li><li>Tags as quick filters below search</li></ul></div>
|
||||
<div class="cons"><h4>Cons</h4><ul><li>Empty state can feel blank</li><li>Browsing requires typing first</li></ul></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="option" data-choice="b" onclick="toggleSelect(this)">
|
||||
<div class="letter">B</div>
|
||||
<div class="content">
|
||||
<h3>Browse-First with Search</h3>
|
||||
<div class="mockup" style="margin-top: 12px;">
|
||||
<div class="mockup-header">Global Catalog</div>
|
||||
<div class="mockup-body" style="padding: 20px;">
|
||||
<div style="display:flex; gap:8px; margin-bottom:16px;">
|
||||
<input class="mock-input" placeholder="Search gear..." style="flex:1; font-size:15px; padding:10px 14px;">
|
||||
</div>
|
||||
<div style="margin-bottom:20px;">
|
||||
<div style="font-size:13px; font-weight:600; color:#374151; margin-bottom:8px;">Popular Categories</div>
|
||||
<div style="display:grid; grid-template-columns: repeat(3, 1fr); gap:8px;">
|
||||
<div style="border:1px solid #e5e7eb; border-radius:10px; padding:12px; text-align:center; cursor:pointer;">
|
||||
<div style="font-size:20px; margin-bottom:4px;">🎒</div>
|
||||
<div style="font-size:13px; font-weight:500;">Bags</div>
|
||||
<div style="font-size:11px; color:#9ca3af;">342 items</div>
|
||||
</div>
|
||||
<div style="border:1px solid #e5e7eb; border-radius:10px; padding:12px; text-align:center; cursor:pointer;">
|
||||
<div style="font-size:20px; margin-bottom:4px;">🏕️</div>
|
||||
<div style="font-size:13px; font-weight:500;">Shelter</div>
|
||||
<div style="font-size:11px; color:#9ca3af;">128 items</div>
|
||||
</div>
|
||||
<div style="border:1px solid #e5e7eb; border-radius:10px; padding:12px; text-align:center; cursor:pointer;">
|
||||
<div style="font-size:20px; margin-bottom:4px;">🎮</div>
|
||||
<div style="font-size:13px; font-weight:500;">Sim Gear</div>
|
||||
<div style="font-size:11px; color:#9ca3af;">89 items</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div style="font-size:13px; font-weight:600; color:#374151; margin-bottom:8px;">Trending</div>
|
||||
<div style="border:1px solid #e5e7eb; border-radius:12px; padding:12px; display:flex; justify-content:space-between; align-items:center; margin-bottom:6px;">
|
||||
<div>
|
||||
<div style="font-weight:600; font-size:14px;">Revelate Designs Pika</div>
|
||||
<div style="font-size:12px; color:#6b7280;">128g · $85 · ★ 4.6</div>
|
||||
</div>
|
||||
<div style="font-size:12px; color:#6b7280;">24 owners</div>
|
||||
</div>
|
||||
<div style="border:1px solid #e5e7eb; border-radius:12px; padding:12px; display:flex; justify-content:space-between; align-items:center;">
|
||||
<div>
|
||||
<div style="font-weight:600; font-size:14px;">Fanatec DD Pro</div>
|
||||
<div style="font-size:12px; color:#6b7280;">2.4kg · $350 · ★ 4.8</div>
|
||||
</div>
|
||||
<div style="font-size:12px; color:#6b7280;">31 owners</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pros-cons" style="margin-top:12px;">
|
||||
<div class="pros"><h4>Pros</h4><ul><li>Great for discovery — see what's popular</li><li>Categories give structure without complexity</li><li>Never an empty state</li></ul></div>
|
||||
<div class="cons"><h4>Cons</h4><ul><li>More complex landing page</li><li>Categories need curation</li></ul></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
172
.superpowers/brainstorm/1268660-1775403447/content/fab-flow.html
Normal file
172
.superpowers/brainstorm/1268660-1775403447/content/fab-flow.html
Normal file
@@ -0,0 +1,172 @@
|
||||
<h2>Add Item Flow — From FAB to Search to Results</h2>
|
||||
<p class="subtitle">The journey from clicking + to finding gear</p>
|
||||
|
||||
<div class="section">
|
||||
<h3>Step 1: FAB Menu</h3>
|
||||
<p class="subtitle">Click + → two options appear above the button</p>
|
||||
<div class="mockup">
|
||||
<div class="mockup-header">Collection View</div>
|
||||
<div class="mockup-body" style="padding:20px; min-height:280px; position:relative; background:#f9fafb;">
|
||||
<!-- Fake collection content -->
|
||||
<div style="opacity:0.4;">
|
||||
<div style="border:1px solid #e5e7eb; border-radius:12px; padding:12px; margin-bottom:8px; background:white;">
|
||||
<div style="font-weight:600; font-size:14px;">Revelate Designs Pika</div>
|
||||
<div style="font-size:12px; color:#6b7280;">128g · Bags</div>
|
||||
</div>
|
||||
<div style="border:1px solid #e5e7eb; border-radius:12px; padding:12px; margin-bottom:8px; background:white;">
|
||||
<div style="font-weight:600; font-size:14px;">MSR Hubba NX</div>
|
||||
<div style="font-size:12px; color:#6b7280;">1,540g · Shelter</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- FAB with popup menu -->
|
||||
<div style="position:absolute; bottom:20px; right:20px; text-align:right;">
|
||||
<div style="background:white; border:1px solid #e5e7eb; border-radius:12px; padding:6px; margin-bottom:10px; box-shadow:0 4px 12px rgba(0,0,0,0.1); display:inline-block;">
|
||||
<div style="padding:10px 16px; border-radius:8px; cursor:pointer; font-size:14px; font-weight:500; display:flex; align-items:center; gap:8px; color:#374151;">
|
||||
<span style="font-size:16px;">📦</span> Add to Collection
|
||||
</div>
|
||||
<div style="height:1px; background:#e5e7eb; margin:2px 0;"></div>
|
||||
<div style="padding:10px 16px; border-radius:8px; cursor:pointer; font-size:14px; font-weight:500; display:flex; align-items:center; gap:8px; color:#374151;">
|
||||
<span style="font-size:16px;">🔍</span> Start New Thread
|
||||
</div>
|
||||
</div>
|
||||
<div style="width:56px; height:56px; background:#374151; border-radius:50%; display:flex; align-items:center; justify-content:center; color:white; font-size:24px; box-shadow:0 2px 8px rgba(0,0,0,0.2); margin-left:auto;">+</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h3>Step 2: Search Overlay</h3>
|
||||
<p class="subtitle">After choosing an action, a search overlay appears</p>
|
||||
<div class="options">
|
||||
<div class="option" data-choice="a" onclick="toggleSelect(this)">
|
||||
<div class="letter">A</div>
|
||||
<div class="content">
|
||||
<h3>Floating Search Bar</h3>
|
||||
<p style="margin-bottom:12px;">Compact overlay centered on screen. Feels quick and light.</p>
|
||||
<div class="mockup">
|
||||
<div class="mockup-header">Search Overlay — Floating</div>
|
||||
<div class="mockup-body" style="padding:20px; min-height:220px; position:relative; background:#f9fafb;">
|
||||
<!-- Dimmed background -->
|
||||
<div style="position:absolute; inset:0; background:rgba(0,0,0,0.3); border-radius:0 0 8px 8px;"></div>
|
||||
<!-- Floating search -->
|
||||
<div style="position:relative; max-width:480px; margin:40px auto 0; background:white; border-radius:16px; padding:20px; box-shadow:0 8px 30px rgba(0,0,0,0.15);">
|
||||
<div style="font-size:13px; font-weight:600; color:#6b7280; margin-bottom:10px;">Adding to Collection</div>
|
||||
<div style="display:flex; gap:8px;">
|
||||
<input class="mock-input" placeholder="Search gear..." style="flex:1; font-size:15px; padding:10px 14px;">
|
||||
<button class="mock-button" style="padding:10px 20px;">Search</button>
|
||||
</div>
|
||||
<div style="margin-top:12px; font-size:12px; color:#9ca3af;">Search the catalog or press Enter to see results</div>
|
||||
<div style="margin-top:10px; display:flex; gap:6px; flex-wrap:wrap;">
|
||||
<span style="padding:3px 10px; border-radius:99px; background:#f3f4f6; font-size:12px; color:#6b7280; cursor:pointer;">Bags</span>
|
||||
<span style="padding:3px 10px; border-radius:99px; background:#f3f4f6; font-size:12px; color:#6b7280; cursor:pointer;">Shelter</span>
|
||||
<span style="padding:3px 10px; border-radius:99px; background:#f3f4f6; font-size:12px; color:#6b7280; cursor:pointer;">Lights</span>
|
||||
<span style="padding:3px 10px; border-radius:99px; background:#f3f4f6; font-size:12px; color:#6b7280; cursor:pointer;">Cooking</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="option" data-choice="b" onclick="toggleSelect(this)">
|
||||
<div class="letter">B</div>
|
||||
<div class="content">
|
||||
<h3>Full-Screen Search</h3>
|
||||
<p style="margin-bottom:12px;">Takes over the screen. More room for instant suggestions and recent searches.</p>
|
||||
<div class="mockup">
|
||||
<div class="mockup-header">Search Overlay — Full Screen</div>
|
||||
<div class="mockup-body" style="padding:0; min-height:280px; background:white;">
|
||||
<div style="padding:16px 20px; border-bottom:1px solid #e5e7eb; display:flex; align-items:center; gap:12px;">
|
||||
<span style="color:#9ca3af; cursor:pointer; font-size:18px;">←</span>
|
||||
<input class="mock-input" placeholder="Search gear..." style="flex:1; font-size:16px; padding:10px 14px; border:none; background:#f9fafb;">
|
||||
</div>
|
||||
<div style="padding:16px 20px;">
|
||||
<div style="font-size:12px; font-weight:600; color:#9ca3af; text-transform:uppercase; letter-spacing:0.5px; margin-bottom:10px;">Quick Tags</div>
|
||||
<div style="display:flex; gap:6px; flex-wrap:wrap; margin-bottom:20px;">
|
||||
<span style="padding:6px 14px; border-radius:99px; background:#f3f4f6; font-size:13px; color:#6b7280; cursor:pointer;">Bags</span>
|
||||
<span style="padding:6px 14px; border-radius:99px; background:#f3f4f6; font-size:13px; color:#6b7280; cursor:pointer;">Shelter</span>
|
||||
<span style="padding:6px 14px; border-radius:99px; background:#f3f4f6; font-size:13px; color:#6b7280; cursor:pointer;">Lights</span>
|
||||
<span style="padding:6px 14px; border-radius:99px; background:#f3f4f6; font-size:13px; color:#6b7280; cursor:pointer;">Cooking</span>
|
||||
<span style="padding:6px 14px; border-radius:99px; background:#f3f4f6; font-size:13px; color:#6b7280; cursor:pointer;">Bike Parts</span>
|
||||
<span style="padding:6px 14px; border-radius:99px; background:#f3f4f6; font-size:13px; color:#6b7280; cursor:pointer;">Waterproof</span>
|
||||
</div>
|
||||
<div style="font-size:12px; font-weight:600; color:#9ca3af; text-transform:uppercase; letter-spacing:0.5px; margin-bottom:10px;">Adding to Collection</div>
|
||||
<div style="font-size:13px; color:#6b7280;">Type to search the gear catalog, or <span style="color:#3b82f6; cursor:pointer;">add manually</span> if your item isn't listed.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h3>Step 3: Results (always full screen)</h3>
|
||||
<p class="subtitle">After hitting Enter/Search, full results page with actions</p>
|
||||
<div class="mockup">
|
||||
<div class="mockup-header">Search Results — "handlebar bag"</div>
|
||||
<div class="mockup-body" style="padding:0; background:white;">
|
||||
<div style="padding:12px 20px; border-bottom:1px solid #e5e7eb; display:flex; align-items:center; gap:12px;">
|
||||
<span style="color:#9ca3af; cursor:pointer; font-size:18px;">←</span>
|
||||
<div style="flex:1; background:#f3f4f6; border-radius:8px; padding:8px 14px; font-size:14px; color:#374151;">handlebar bag</div>
|
||||
</div>
|
||||
<div style="padding:12px 20px;">
|
||||
<div style="display:flex; gap:6px; flex-wrap:wrap; margin-bottom:16px;">
|
||||
<span style="padding:4px 12px; border-radius:99px; background:#dbeafe; font-size:12px; color:#2563eb;">handlebar</span>
|
||||
<span style="padding:4px 12px; border-radius:99px; background:#f3f4f6; font-size:12px; color:#6b7280; cursor:pointer;">+ waterproof</span>
|
||||
<span style="padding:4px 12px; border-radius:99px; background:#f3f4f6; font-size:12px; color:#6b7280; cursor:pointer;">+ ultralight</span>
|
||||
</div>
|
||||
<div style="font-size:12px; color:#9ca3af; margin-bottom:12px;">12 results</div>
|
||||
|
||||
<!-- Result cards -->
|
||||
<div style="border:1px solid #e5e7eb; border-radius:12px; padding:14px; margin-bottom:8px; display:flex; justify-content:space-between; align-items:center;">
|
||||
<div style="flex:1;">
|
||||
<div style="display:flex; align-items:center; gap:8px;">
|
||||
<div style="font-weight:600; font-size:14px;">Revelate Designs Pika</div>
|
||||
<div style="background:#fef3c7; color:#92400e; font-size:11px; padding:2px 8px; border-radius:99px;">★ 4.6</div>
|
||||
</div>
|
||||
<div style="display:flex; gap:16px; margin-top:4px; font-size:12px; color:#6b7280;">
|
||||
<span>128g</span><span>$85</span><span>24 owners</span>
|
||||
</div>
|
||||
</div>
|
||||
<button class="mock-button" style="padding:6px 14px; font-size:13px; background:#374151; color:white;">+ Add</button>
|
||||
</div>
|
||||
|
||||
<div style="border:1px solid #e5e7eb; border-radius:12px; padding:14px; margin-bottom:8px; display:flex; justify-content:space-between; align-items:center;">
|
||||
<div style="flex:1;">
|
||||
<div style="display:flex; align-items:center; gap:8px;">
|
||||
<div style="font-weight:600; font-size:14px;">Apidura Racing HB Pack</div>
|
||||
<div style="background:#fef3c7; color:#92400e; font-size:11px; padding:2px 8px; border-radius:99px;">★ 4.4</div>
|
||||
</div>
|
||||
<div style="display:flex; gap:16px; margin-top:4px; font-size:12px; color:#6b7280;">
|
||||
<span>105g</span><span>$72</span><span>18 owners</span>
|
||||
</div>
|
||||
</div>
|
||||
<button class="mock-button" style="padding:6px 14px; font-size:13px; background:#374151; color:white;">+ Add</button>
|
||||
</div>
|
||||
|
||||
<div style="border:1px solid #e5e7eb; border-radius:12px; padding:14px; margin-bottom:8px; display:flex; justify-content:space-between; align-items:center;">
|
||||
<div style="flex:1;">
|
||||
<div style="display:flex; align-items:center; gap:8px;">
|
||||
<div style="font-weight:600; font-size:14px;">Ortlieb Handlebar-Pack QR</div>
|
||||
<div style="background:#fef3c7; color:#92400e; font-size:11px; padding:2px 8px; border-radius:99px;">★ 4.2</div>
|
||||
</div>
|
||||
<div style="display:flex; gap:16px; margin-top:4px; font-size:12px; color:#6b7280;">
|
||||
<span>340g</span><span>$95</span><span>31 owners</span>
|
||||
</div>
|
||||
</div>
|
||||
<button class="mock-button" style="padding:6px 14px; font-size:13px; background:#374151; color:white;">+ Add</button>
|
||||
</div>
|
||||
|
||||
<!-- Not found option -->
|
||||
<div style="border:2px dashed #d1d5db; border-radius:12px; padding:16px; margin-top:16px; text-align:center;">
|
||||
<div style="font-size:14px; font-weight:500; color:#6b7280;">Can't find what you're looking for?</div>
|
||||
<div style="font-size:13px; color:#9ca3af; margin-top:4px;">Add it manually and submit it to the catalog for review</div>
|
||||
<button class="mock-button" style="margin-top:10px; padding:8px 20px; font-size:13px;">Add Manually</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,3 @@
|
||||
<div style="display:flex;align-items:center;justify-content:center;min-height:60vh">
|
||||
<p class="subtitle">Continuing in terminal...</p>
|
||||
</div>
|
||||
@@ -0,0 +1,3 @@
|
||||
<div style="display:flex;align-items:center;justify-content:center;min-height:60vh">
|
||||
<p class="subtitle">Continuing in terminal...</p>
|
||||
</div>
|
||||
@@ -0,0 +1 @@
|
||||
{"reason":"idle timeout","timestamp":1775407587614}
|
||||
6
drizzle-pg/0002_square_pyro.sql
Normal file
6
drizzle-pg/0002_square_pyro.sql
Normal file
@@ -0,0 +1,6 @@
|
||||
CREATE TABLE "tags" (
|
||||
"id" serial PRIMARY KEY NOT NULL,
|
||||
"name" text NOT NULL,
|
||||
"created_at" timestamp DEFAULT now() NOT NULL,
|
||||
CONSTRAINT "tags_name_unique" UNIQUE("name")
|
||||
);
|
||||
@@ -1,5 +1,9 @@
|
||||
{
|
||||
<<<<<<< HEAD
|
||||
"id": "1c8fbda2-e486-4f57-a6d5-1a2e7042e413",
|
||||
=======
|
||||
"id": "4b01f839-a5ff-416c-826c-1e37e76d0a78",
|
||||
>>>>>>> worktree-agent-adbc35a5
|
||||
"prevId": "8fb47390-ff75-41f7-aa35-fad97b1a097e",
|
||||
"version": "7",
|
||||
"dialect": "postgresql",
|
||||
@@ -136,6 +140,7 @@
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
<<<<<<< HEAD
|
||||
"public.global_item_tags": {
|
||||
"name": "global_item_tags",
|
||||
"schema": "",
|
||||
@@ -196,6 +201,8 @@
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
=======
|
||||
>>>>>>> worktree-agent-adbc35a5
|
||||
"public.global_items": {
|
||||
"name": "global_items",
|
||||
"schema": "",
|
||||
@@ -264,6 +271,75 @@
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
<<<<<<< HEAD
|
||||
=======
|
||||
"public.item_global_links": {
|
||||
"name": "item_global_links",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "serial",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"item_id": {
|
||||
"name": "item_id",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"global_item_id": {
|
||||
"name": "global_item_id",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"item_global_links_item_id_items_id_fk": {
|
||||
"name": "item_global_links_item_id_items_id_fk",
|
||||
"tableFrom": "item_global_links",
|
||||
"tableTo": "items",
|
||||
"columnsFrom": [
|
||||
"item_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
},
|
||||
"item_global_links_global_item_id_global_items_id_fk": {
|
||||
"name": "item_global_links_global_item_id_global_items_id_fk",
|
||||
"tableFrom": "item_global_links",
|
||||
"tableTo": "global_items",
|
||||
"columnsFrom": [
|
||||
"global_item_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {
|
||||
"item_global_links_item_id_unique": {
|
||||
"name": "item_global_links_item_id_unique",
|
||||
"nullsNotDistinct": false,
|
||||
"columns": [
|
||||
"item_id"
|
||||
]
|
||||
}
|
||||
},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
>>>>>>> worktree-agent-adbc35a5
|
||||
"public.items": {
|
||||
"name": "items",
|
||||
"schema": "",
|
||||
@@ -335,6 +411,7 @@
|
||||
"notNull": true,
|
||||
"default": 1
|
||||
},
|
||||
<<<<<<< HEAD
|
||||
"global_item_id": {
|
||||
"name": "global_item_id",
|
||||
"type": "integer",
|
||||
@@ -347,6 +424,8 @@
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
=======
|
||||
>>>>>>> worktree-agent-adbc35a5
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp",
|
||||
@@ -389,6 +468,7 @@
|
||||
],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
<<<<<<< HEAD
|
||||
},
|
||||
"items_global_item_id_global_items_id_fk": {
|
||||
"name": "items_global_item_id_global_items_id_fk",
|
||||
@@ -402,6 +482,8 @@
|
||||
],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
=======
|
||||
>>>>>>> worktree-agent-adbc35a5
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
@@ -938,12 +1020,15 @@
|
||||
"notNull": true,
|
||||
"default": 0
|
||||
},
|
||||
<<<<<<< HEAD
|
||||
"global_item_id": {
|
||||
"name": "global_item_id",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
=======
|
||||
>>>>>>> worktree-agent-adbc35a5
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp",
|
||||
@@ -986,6 +1071,7 @@
|
||||
],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
<<<<<<< HEAD
|
||||
},
|
||||
"thread_candidates_global_item_id_global_items_id_fk": {
|
||||
"name": "thread_candidates_global_item_id_global_items_id_fk",
|
||||
@@ -999,6 +1085,8 @@
|
||||
],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
=======
|
||||
>>>>>>> worktree-agent-adbc35a5
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
|
||||
@@ -23,13 +23,16 @@ interface ItemGlobalLink {
|
||||
globalItemId: number;
|
||||
}
|
||||
|
||||
export function useGlobalItems(query?: string) {
|
||||
export function useGlobalItems(query?: string, tags?: string[]) {
|
||||
const params = new URLSearchParams();
|
||||
if (query) params.set("q", query);
|
||||
if (tags && tags.length > 0) params.set("tags", tags.join(","));
|
||||
const qs = params.toString();
|
||||
|
||||
return useQuery({
|
||||
queryKey: ["global-items", query ?? ""],
|
||||
queryKey: ["global-items", query ?? "", tags ?? []],
|
||||
queryFn: () =>
|
||||
apiGet<GlobalItem[]>(
|
||||
`/api/global-items${query ? `?q=${encodeURIComponent(query)}` : ""}`,
|
||||
),
|
||||
apiGet<GlobalItem[]>(`/api/global-items${qs ? `?${qs}` : ""}`),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
15
src/client/hooks/useTags.ts
Normal file
15
src/client/hooks/useTags.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { apiGet } from "../lib/api";
|
||||
|
||||
export interface Tag {
|
||||
id: number;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export function useTags() {
|
||||
return useQuery({
|
||||
queryKey: ["tags"],
|
||||
queryFn: () => apiGet<Tag[]>("/api/tags"),
|
||||
staleTime: 5 * 60 * 1000,
|
||||
});
|
||||
}
|
||||
@@ -56,6 +56,17 @@ interface UIState {
|
||||
// Setup impact preview
|
||||
selectedSetupId: number | null;
|
||||
setSelectedSetupId: (id: number | null) => void;
|
||||
|
||||
// FAB menu
|
||||
fabMenuOpen: boolean;
|
||||
openFabMenu: () => void;
|
||||
closeFabMenu: () => void;
|
||||
|
||||
// Catalog search overlay
|
||||
catalogSearchOpen: boolean;
|
||||
catalogSearchMode: "collection" | "thread" | null;
|
||||
openCatalogSearch: (mode: "collection" | "thread") => void;
|
||||
closeCatalogSearch: () => void;
|
||||
}
|
||||
|
||||
export const useUIStore = create<UIState>((set) => ({
|
||||
@@ -119,4 +130,21 @@ export const useUIStore = create<UIState>((set) => ({
|
||||
// Setup impact preview
|
||||
selectedSetupId: null,
|
||||
setSelectedSetupId: (id) => set({ selectedSetupId: id }),
|
||||
|
||||
// FAB menu
|
||||
fabMenuOpen: false,
|
||||
openFabMenu: () => set({ fabMenuOpen: true }),
|
||||
closeFabMenu: () => set({ fabMenuOpen: false }),
|
||||
|
||||
// Catalog search overlay
|
||||
catalogSearchOpen: false,
|
||||
catalogSearchMode: null,
|
||||
openCatalogSearch: (mode) =>
|
||||
set({
|
||||
catalogSearchOpen: true,
|
||||
catalogSearchMode: mode,
|
||||
fabMenuOpen: false,
|
||||
}),
|
||||
closeCatalogSearch: () =>
|
||||
set({ catalogSearchOpen: false, catalogSearchMode: null }),
|
||||
}));
|
||||
|
||||
@@ -12,12 +12,14 @@ import { mcpRoutes } from "./mcp/index.ts";
|
||||
import { requireAuth } from "./middleware/auth.ts";
|
||||
import { authRoutes } from "./routes/auth.ts";
|
||||
import { categoryRoutes } from "./routes/categories.ts";
|
||||
import { globalItemRoutes } from "./routes/global-items.ts";
|
||||
import { imageRoutes } from "./routes/images.ts";
|
||||
import { itemRoutes } from "./routes/items.ts";
|
||||
import { oauthRoutes, wellKnownRoute } from "./routes/oauth.ts";
|
||||
import { profileRoutes } from "./routes/profiles.ts";
|
||||
import { settingsRoutes } from "./routes/settings.ts";
|
||||
import { setupRoutes } from "./routes/setups.ts";
|
||||
import { tagRoutes } from "./routes/tags.ts";
|
||||
import { threadRoutes } from "./routes/threads.ts";
|
||||
import { totalRoutes } from "./routes/totals.ts";
|
||||
|
||||
@@ -51,10 +53,7 @@ if (process.env.NODE_ENV !== "production") {
|
||||
if (setCookies.length > 0) {
|
||||
c.res.headers.delete("Set-Cookie");
|
||||
for (const cookie of setCookies) {
|
||||
c.res.headers.append(
|
||||
"Set-Cookie",
|
||||
cookie.replace(/;\s*Secure/gi, ""),
|
||||
);
|
||||
c.res.headers.append("Set-Cookie", cookie.replace(/;\s*Secure/gi, ""));
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -98,6 +97,12 @@ app.use("/api/*", async (c, next) => {
|
||||
// Skip public setup view (GET /api/setups/:id/public)
|
||||
if (/^\/api\/setups\/\d+\/public$/.test(c.req.path) && c.req.method === "GET")
|
||||
return next();
|
||||
// Skip public tags endpoint (GET /api/tags)
|
||||
if (c.req.path.startsWith("/api/tags") && c.req.method === "GET")
|
||||
return next();
|
||||
// Skip public global-items endpoint (GET /api/global-items)
|
||||
if (c.req.path.startsWith("/api/global-items") && c.req.method === "GET")
|
||||
return next();
|
||||
// All other methods require auth for userId resolution
|
||||
return requireAuth(c, next);
|
||||
});
|
||||
@@ -112,6 +117,8 @@ app.route("/api/settings", settingsRoutes);
|
||||
app.route("/api/threads", threadRoutes);
|
||||
app.route("/api/users", profileRoutes);
|
||||
app.route("/api/setups", setupRoutes);
|
||||
app.route("/api/global-items", globalItemRoutes);
|
||||
app.route("/api/tags", tagRoutes);
|
||||
|
||||
// MCP server (conditionally mounted)
|
||||
if (process.env.GEARBOX_MCP !== "false") {
|
||||
|
||||
14
src/server/routes/tags.ts
Normal file
14
src/server/routes/tags.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { Hono } from "hono";
|
||||
import { getAllTags } from "../services/tag.service.ts";
|
||||
|
||||
type Env = { Variables: { db?: any } };
|
||||
|
||||
const app = new Hono<Env>();
|
||||
|
||||
app.get("/", async (c) => {
|
||||
const db = c.get("db");
|
||||
const allTags = await getAllTags(db);
|
||||
return c.json(allTags);
|
||||
});
|
||||
|
||||
export { app as tagRoutes };
|
||||
12
src/server/services/tag.service.ts
Normal file
12
src/server/services/tag.service.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { asc } from "drizzle-orm";
|
||||
import { db as prodDb } from "../../db/index.ts";
|
||||
import { tags } from "../../db/schema.ts";
|
||||
|
||||
type Db = typeof prodDb;
|
||||
|
||||
export async function getAllTags(db: Db = prodDb) {
|
||||
return db
|
||||
.select({ id: tags.id, name: tags.name })
|
||||
.from(tags)
|
||||
.orderBy(asc(tags.name));
|
||||
}
|
||||
52
tests/routes/tags.test.ts
Normal file
52
tests/routes/tags.test.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import { beforeEach, describe, expect, it } from "bun:test";
|
||||
import { Hono } from "hono";
|
||||
import { tags } from "../../src/db/schema.ts";
|
||||
import { tagRoutes } from "../../src/server/routes/tags.ts";
|
||||
import { createTestDb } from "../helpers/db.ts";
|
||||
|
||||
function createTestApp(db: any) {
|
||||
const app = new Hono();
|
||||
|
||||
app.use("*", async (c, next) => {
|
||||
c.set("db", db);
|
||||
await next();
|
||||
});
|
||||
|
||||
app.route("/api/tags", tagRoutes);
|
||||
return app;
|
||||
}
|
||||
|
||||
describe("Tag Routes", () => {
|
||||
let app: Hono;
|
||||
let db: Awaited<ReturnType<typeof createTestDb>>["db"];
|
||||
|
||||
beforeEach(async () => {
|
||||
const testDb = await createTestDb();
|
||||
db = testDb.db;
|
||||
app = createTestApp(db);
|
||||
});
|
||||
|
||||
describe("GET /api/tags", () => {
|
||||
it("returns 200 with empty array when no tags", async () => {
|
||||
const res = await app.request("/api/tags");
|
||||
expect(res.status).toBe(200);
|
||||
|
||||
const body = await res.json();
|
||||
expect(body).toEqual([]);
|
||||
});
|
||||
|
||||
it("returns 200 with tag objects after seeding", async () => {
|
||||
await db
|
||||
.insert(tags)
|
||||
.values([{ name: "bikepacking" }, { name: "ultralight" }]);
|
||||
|
||||
const res = await app.request("/api/tags");
|
||||
expect(res.status).toBe(200);
|
||||
|
||||
const body = await res.json();
|
||||
expect(body).toHaveLength(2);
|
||||
expect(body[0]).toHaveProperty("id");
|
||||
expect(body[0]).toHaveProperty("name");
|
||||
});
|
||||
});
|
||||
});
|
||||
36
tests/services/tag.service.test.ts
Normal file
36
tests/services/tag.service.test.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { beforeEach, describe, expect, it } from "bun:test";
|
||||
import { tags } from "../../src/db/schema.ts";
|
||||
import { getAllTags } from "../../src/server/services/tag.service.ts";
|
||||
import { createTestDb } from "../helpers/db.ts";
|
||||
|
||||
describe("Tag Service", () => {
|
||||
let db: Awaited<ReturnType<typeof createTestDb>>["db"];
|
||||
|
||||
beforeEach(async () => {
|
||||
const testDb = await createTestDb();
|
||||
db = testDb.db;
|
||||
});
|
||||
|
||||
it("returns empty array when no tags exist", async () => {
|
||||
const result = await getAllTags(db);
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it("returns all tags as { id, name } ordered alphabetically", async () => {
|
||||
await db
|
||||
.insert(tags)
|
||||
.values([
|
||||
{ name: "bikepacking" },
|
||||
{ name: "ultralight" },
|
||||
{ name: "accessories" },
|
||||
]);
|
||||
|
||||
const result = await getAllTags(db);
|
||||
expect(result).toHaveLength(3);
|
||||
expect(result[0].name).toBe("accessories");
|
||||
expect(result[1].name).toBe("bikepacking");
|
||||
expect(result[2].name).toBe("ultralight");
|
||||
// Should NOT include createdAt
|
||||
expect(result[0]).toEqual({ id: expect.any(Number), name: "accessories" });
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user