SkateSpots/UX_AUDIT_2026-10-09.md
SkateSpots 88a2a2ca15 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.
2026-10-09 19:11:05 +07:00

9.2 KiB

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.

  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-labels, 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.