diff --git a/AGENTS.md b/AGENTS.md index ab56ca5..5e5da30 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -2102,7 +2102,9 @@ Rules: - `/src/contexts/`: React context providers (AppContext, LanguageContext, NWCContext) - `NIP.md`: **source of truth** for every custom/adopted Nostr kind and its schema - `AGENTS.md` (this file): the only cross-session memory — keep it current -- `UX_AUDIT.md`, `GROUP_MESSAGING.md`, `ESCROW.md`: feature decision/backlog docs +- `UX_AUDIT.md` (pre-refactor, partly stale) and `UX_AUDIT_2026-10-09.md` + (current app-wide + marketplace UI/UX audit, with a recommended fix order), + `GROUP_MESSAGING.md`, `ESCROW.md`: feature decision/backlog docs - `/src/test/`: Testing utilities including TestApp component - `/public/`: Static assets - `App.tsx`: Main app component with provider setup (**CRITICAL**: this file is **already configured** with `QueryClientProvider`, `NostrProvider`, `UnheadProvider` and other important providers - **read this file before making changes**. Changes are usually not necessary unless adding new providers. Changing this file may break the application) diff --git a/UX_AUDIT_2026-10-09.md b/UX_AUDIT_2026-10-09.md new file mode 100644 index 0000000..4b0a3ee --- /dev/null +++ b/UX_AUDIT_2026-10-09.md @@ -0,0 +1,170 @@ +# UI/UX audit — 2026-10-09 + +Scope: whole app, with a deep focus on the marketplace and its flow sequence. +Method: two exploration passes (marketplace flow; app-wide cross-cutting) plus +direct verification of the high-impact findings in the source. Items marked +**[verified]** were checked by hand; items marked **[reported]** come from the +exploration passes and should be confirmed before acting. + +> Note: the older `UX_AUDIT.md` is pre-refactor and partly stale (it still +> flags `/marketplace-listings`, but several of its line references no longer +> apply). Treat this file as current. + +--- + +## 1. Marketplace flow — the sequence and where it breaks + +Entry points today: + +- Nav "Marketplace" -> `/marketplace` -> `MarketplaceHubPage` + (`src/AppRouter.tsx:140`). The hub has `Browse / Selling / My Trades / + Reputation` tabs. +- Deep links: `/listing/:pubkey/:identifier` (detail), `/trades` (TradesPage), + and the orphan `/marketplace-listings` (see below). + +### Step 1 — Browse +`MarketplaceHubPage` Browse tab renders `GearSwapPage` +(`src/pages/MarketplaceHubPage.tsx:59`), whose grid uses `useMarketplaceItems` +and `GearSwapItemCard`. + +- **[verified] The hub tabs are local state, not the URL** — `const [tab, setTab] + = useState('browse')` (`MarketplaceHubPage.tsx:29`). Refreshing resets + to Browse, tabs cannot be deep-linked or shared, and the separate `/trades` + route is the only URL-addressable trades view. Set the tab from + `useSearchParams`. +- **[verified] Two parallel listing cards exist.** `GearSwapItemCard` (grid, + `GearSwapPage.tsx:464`) and `MarketplaceItemCard` (`SpotDetailPage.tsx:828` + per-spot gear). Only the latter mounts `ListingTradeActions` + (`MarketplaceItemCard.tsx`), so buy/offer/escrow behaviour differs by surface + — contradicting the "shared by both card and detail" design note. +- **[reported] Anonymous dead ends:** the card's Contact button renders when + logged out and links to the auth-gated `/messages/` with no login + affordance (`GearSwapItemCard.tsx:414`). +- **[reported] `isFollowing` is a declared but unused prop** + (`GearSwapItemCard.tsx:32`), still passed by the grid (`GearSwapPage.tsx:468`). + +### Step 2 — Listing detail +Seller block (`ListingDetailPage.tsx:272-301`) then actions. + +- **[verified] A fixed-price listing shows two competing primary CTAs at once:** + "Buy with sats" (inside `ListingTradeActions`) and "Make an offer" + (`ListingDetailPage.tsx:339-347`). Only one should be primary per price type. +- **[verified] "Make an offer" is shown to logged-out users.** The buyer branch + renders for any non-owner regardless of auth; `ListingOffersPanel` returns null + when `!user` (`ListingOffersPanel.tsx:76`), so an anonymous visitor sees the + offer button, opens it, and the submit throws "Log in to make an offer" + (`useMarketplaceOffers.ts`). Needs a login guard/affordance. +- **[verified] Detail page omits badges the cards show** — fulfilment, shipping, + escrow-required, distance, PoW, verified-trades (`ListingDetailPage.tsx:214` + shows only condition/size/category). +- **[verified] Back always goes to `/marketplace`** + (`ListingDetailPage.tsx:155`), dropping the tab/context you came from. +- **[reported] No edit-listing path exists at all** (no `/edit-listing` route); + an owner can only Mark sold / Relist / Delete (`ListingDetailPage.tsx:304`). +- **[reported] Escrow is unreachable on web** (native-only gate) while the + "ships + escrow" hint still shows (`ListingTradeActions.tsx:229,242-251`). + +### Step 3 — Offers and trade completion +`ListingOffersPanel` is the negotiation thread; accepted offers offer a +"Pay with sats" button. + +- **[verified] The offers panel disappears entirely when the query has no data** + (`ListingOffersPanel.tsx:76`) — on a relay error, or for logged-out users, there + is no panel, no error, and no retry. +- **[reported] "Pay with sats" is offered for accepted offers whose seller + pointer kind is unsupported** (creq/clink), which then throws + (`usePayPaymentPointer.ts:54,59`). +- **[reported] Shipment status can dead-end:** "I've shipped" / "I received" + silently error until the seller records the buyer + (`ShippingStatusCard.tsx:46`). + +### Step 4 — Selling +`SellerDashboard` (hub Selling tab) and the grid. + +- **[verified] The grid's owner "Mark as sold" skips the trade flow.** + `GearSwapItemCard.handleMarkSold` calls only `markSold` (`GearSwapItemCard.tsx:113-115`); + the detail/dashboard paths open `MarkSoldDialog`, which also publishes the + kind-31871 attestation (buyer + method + rating). Same user action, no + reputation recorded, depending on where it is clicked. +- **[verified] A hidden or showcase-only listing is still reachable by URL.** + `useMarketplaceItem` (`useMarketplace.ts:96-115`) does not apply + `filterAdminHidden` and does not exclude `showcase`-only items, unlike the list + hooks (`useMarketplace.ts:19,58`). An admin-hidden or not-for-sale listing still + renders at `/listing/...` (moderation gap). +- **[reported] `SellerDashboard` has no error state** — a relay outage looks like + "No listings" (`SellerDashboard.tsx:76`). + +--- + +## 2. Cross-cutting bugs / inconsistencies + +- **[verified] Duplicate/conflicting nav labels.** `nav.marketplace` and + `nav.trades` are both "Marketplace" (`src/i18n/en.ts:75,79`); the catalog entry + `{ id: 'trades', href: '/marketplace', labelKey: 'nav.trades' }` + (`useNavPreferences.ts:67`) has an id/label/href mismatch, and the real + `/trades` route has no nav entry at all. Pick one word per destination. +- **[verified] Orphan "Marketplace" page.** `/marketplace-listings` -> + `MarketplacePage` (`AppRouter.tsx:193`) is an external-product promo page + (Plebeian Market) with no in-app link — but its name collides with the real + marketplace. Rename or remove. +- **[verified] Nav-consistency bug in the nav customizer.** `MIDDLE_SLOTS = 4` + (`useNavPreferences.ts:76`) but the Reset action hardcodes three tabs + (`CustomizeNavSheet.tsx:72`), diverging from `DEFAULT_TAB_IDS`. +- **[reported] Desktop "More" active state only checks `/stats` and `/about`** + (`Layout.tsx:595`), so many destinations never highlight. +- **[reported] Inconsistent 404 presentation:** unknown single-segment paths hit + `/:nip19` (NotFound inside Layout); unknown multi-segment paths hit `*` + (NotFound with no chrome) (`AppRouter.tsx:199,206`). +- **[reported] Orphan routes with no in-app link:** `/home`, `/coaches`, + `/pyramid`, `/food` (`AppRouter.tsx:116,120,139,148`). +- **[reported] Missing error/outage states** on TradesPage, TradeReputationPanel, + LabPage, CrewsPage, SkatersPage, SpotCollectionsPage, FoodPage, NewsletterPage, + GroupConversationPage, ContactsPage — a relay outage renders the empty state as + truth. `TradesPage.tsx:59` confirmed by the "no error read" pattern. +- **[reported] Destructive-confirmation inconsistency:** `window.confirm` + (`DeckCostCalculator.tsx:1144`), plain `Dialog` (`TradesPage.tsx:319`, + `MyDataTab.tsx:420`), no confirmation (`MapPage.tsx:305,323`), versus the + app-standard `AlertDialog` (Stash/Collections/SpotDetail). +- **[reported] Icon-only buttons missing `aria-label`:** SpotCollectionsPage, + ShopDetailPage, CollectionDetailPage, SpotCard, EventCard, TourCard, + MarketplaceItemCard (report menu). +- **[reported] Hardcoded English left in shared UI:** `DeleteEntityDialog` + (used by listings/crews/events/tours/coaches), `CrossLinkCards`, + `FounderFollow`, `CommunityStatsPage` heading, `SkateGlobe`, `SkatersPage` + heading. Plus marketplace strings: listing status rendered raw + (`MarketplaceItemCard.tsx`, `ListingDetailPage.tsx:203`), the shipping value + raw (`GearSwapItemCard.tsx:334`), seller-dashboard price raw + (`SellerDashboard.tsx:19`). +- **[reported] A legacy `marketplace` i18n namespace runs in parallel to + `marketUi`** (`en.ts:763` vs `1452`); only a few legacy keys are still used, + the rest is dead. +- **[reported] Per-row trust query still exists on PyramidPage** + (`PyramidPage.tsx:19`) — the same heavy pattern we removed from the marketplace + card; every other list uses `useBatchTrustScores`. +- **[reported] `ListingTradeActions` mounts `useCashuEscrow` per card** on the + per-spot gear list (`SpotDetailPage.tsx:828`), duplicating wallet hooks across + items. + +--- + +## Recommended order of work + +1. **Marketplace correctness (P0):** + - Filter `useMarketplaceItem` through `filterAdminHidden` and exclude + `showcase`-only (moderation + "not for sale" reachable by URL). + - Gate "Make an offer" behind login and show only one primary CTA per price + type. + - Make the grid's "Mark as sold" go through `MarkSoldDialog` (record the + attestation) or remove the quick action. +2. **Flow integrity (P1):** URL-address the hub tabs; fix the back target; + give `ListingOffersPanel` an error/empty state; add the missing outage states. +3. **Navigation + terminology (P1):** collapse `nav.marketplace`/`nav.trades`, + fix the catalog id/href, retire `/marketplace-listings`, fix the customizer + Reset. +4. **Polish (P2):** i18n the raw status/currency/shipping strings, standardize + destructive confirms on `AlertDialog`, add `aria-label`s, and move PyramidPage + to the batch trust hook. + +Nothing above was changed in this pass — this is the audit only. Items marked +**[reported]** should be re-checked in the source before a fix, since explorer +line references can drift.