docs: add UX/UI audit with a marketplace-flow focus

Full app audit (UX_AUDIT_2026-10-09.md), heavy on the marketplace flow
sequence. Headline verified findings: useMarketplaceItem does not filter
admin-hidden or exclude showcase-only listings (a hidden/not-for-sale
listing is reachable by URL); the buyer CTAs compete on fixed-price
listings and Make an offer is shown to logged-out users; the grid's Mark
as sold skips MarkSoldDialog so no trade attestation is recorded; the hub
tabs are not URL-addressable; nav.marketplace and nav.trades are
duplicate labels; and the nav customizer Reset hardcodes 3 tabs against
MIDDLE_SLOTS 4. Includes a recommended fix order. Audit only, no code
changes.
This commit is contained in:
SkateSpots 2026-10-09 19:11:05 +07:00
parent dde52eb1a7
commit 88a2a2ca15
2 changed files with 173 additions and 1 deletions

View file

@ -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)

170
UX_AUDIT_2026-10-09.md Normal file
View file

@ -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<TabId>('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/<npub>` 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.