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:
parent
dde52eb1a7
commit
88a2a2ca15
2 changed files with 173 additions and 1 deletions
|
|
@ -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
170
UX_AUDIT_2026-10-09.md
Normal 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.
|
||||
Loading…
Reference in a new issue