fix(trust): H15 — relay outage must not read as 'not found'/'no events'

- useMarketplaceItem / useSkateEvents now pass requireAny, so a total outage
  throws (isError) instead of silently returning [].
- ListingDetailPage and EventsPage render the shared ErrorState with a retry
  instead of a false 'Listing not found' / 'No upcoming events'.
This commit is contained in:
SkateSpots 2026-10-02 21:36:44 +07:00
parent 9189fc6584
commit 64ba187ad4
4 changed files with 19 additions and 5 deletions

View file

@ -106,7 +106,7 @@ export function useMarketplaceItem(pubkey?: string, identifier?: string) {
const signal = AbortSignal.timeout(TIMEOUT); const signal = AbortSignal.timeout(TIMEOUT);
const events = await querySkateContent( const events = await querySkateContent(
[{ kinds: [MARKETPLACE_ITEM_KIND], authors: [pubkey], '#d': [identifier], limit: 5 }], [{ kinds: [MARKETPLACE_ITEM_KIND], authors: [pubkey], '#d': [identifier], limit: 5 }],
{ signal }, { signal, requireAny: true },
); );
const newest = [...events].sort((a, b) => b.created_at - a.created_at)[0]; const newest = [...events].sort((a, b) => b.created_at - a.created_at)[0];
return newest ? parseMarketplaceItem(newest) : null; return newest ? parseMarketplaceItem(newest) : null;

View file

@ -24,7 +24,7 @@ export function useSkateEvents(limit = 50, options: { includeHidden?: boolean }
limit, limit,
}, },
], ],
{ signal } { signal, requireAny: true }
); );
const skateEvents: SkateEvent[] = []; const skateEvents: SkateEvent[] = [];

View file

@ -46,6 +46,7 @@ import {
} from 'lucide-react'; } from 'lucide-react';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { EmptyState as SharedEmptyState, type EmptyStateAction } from '@/components/EmptyState'; import { EmptyState as SharedEmptyState, type EmptyStateAction } from '@/components/EmptyState';
import { ErrorState } from '@/components/ErrorState';
// ─── Accent colours ─────────────────────────────────────────────────────────── // ─── Accent colours ───────────────────────────────────────────────────────────
@ -822,8 +823,8 @@ export default function EventsPage() {
const followCount = followingList?.length ?? 0; const followCount = followingList?.length ?? 0;
const smartDefault: TrustFilterLevel = (user && followCount >= 5) ? 'trusted' : 'all'; const smartDefault: TrustFilterLevel = (user && followCount >= 5) ? 'trusted' : 'all';
const { data: events, isLoading: eventsLoading } = useSkateEvents(100); const { data: events, isLoading: eventsLoading, isError: eventsError, refetch: refetchEvents } = useSkateEvents(100);
const { data: tours, isLoading: toursLoading } = useTours(100); const { data: tours, isLoading: toursLoading, isError: toursError, refetch: refetchTours } = useTours(100);
// ── View state ────────────────────────────────────────────────────────────── // ── View state ──────────────────────────────────────────────────────────────
const [mainView, setMainView] = useState<MainView>('events'); const [mainView, setMainView] = useState<MainView>('events');
@ -1228,6 +1229,8 @@ export default function EventsPage() {
{mainView === 'events' && layout === 'list' && ( {mainView === 'events' && layout === 'list' && (
eventsLoading ? ( eventsLoading ? (
<SkeletonGrid /> <SkeletonGrid />
) : eventsError ? (
<ErrorState onRetry={() => { void refetchEvents(); }} />
) : activeTab === 'upcoming' ? ( ) : activeTab === 'upcoming' ? (
filteredEvents.upcoming.length > 0 ? ( filteredEvents.upcoming.length > 0 ? (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 stagger-children"> <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 stagger-children">
@ -1280,6 +1283,8 @@ export default function EventsPage() {
{mainView === 'tours' && layout === 'list' && ( {mainView === 'tours' && layout === 'list' && (
toursLoading ? ( toursLoading ? (
<TourSkeletonGrid /> <TourSkeletonGrid />
) : toursError ? (
<ErrorState onRetry={() => { void refetchTours(); }} />
) : filteredTours.length > 0 ? ( ) : filteredTours.length > 0 ? (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 stagger-children"> <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 stagger-children">
{filteredTours.map((tour) => ( {filteredTours.map((tour) => (

View file

@ -22,6 +22,7 @@ import { useCurrentUser } from '@/hooks/useCurrentUser';
import { useAuthor } from '@/hooks/useAuthor'; import { useAuthor } from '@/hooks/useAuthor';
import { useSellerInfo } from '@/hooks/useSellerInfo'; import { useSellerInfo } from '@/hooks/useSellerInfo';
import { useToast } from '@/hooks/useToast'; import { useToast } from '@/hooks/useToast';
import { ErrorState } from '@/components/ErrorState';
import { formatPrice, GEAR_CONDITIONS, GEAR_CATEGORIES } from '@/lib/types'; import { formatPrice, GEAR_CONDITIONS, GEAR_CATEGORIES } from '@/lib/types';
import { GEAR_CATEGORY_KEYS, GEAR_CONDITION_KEYS } from '@/lib/labelKeys'; import { GEAR_CATEGORY_KEYS, GEAR_CONDITION_KEYS } from '@/lib/labelKeys';
import { genUserName } from '@/lib/genUserName'; import { genUserName } from '@/lib/genUserName';
@ -54,7 +55,7 @@ export default function ListingDetailPage() {
const { user } = useCurrentUser(); const { user } = useCurrentUser();
const { toast } = useToast(); const { toast } = useToast();
const { data: item, isLoading } = useMarketplaceItem(pubkey, identifier); const { data: item, isLoading, isError, refetch } = useMarketplaceItem(pubkey, identifier);
const author = useAuthor(pubkey); const author = useAuthor(pubkey);
const metadata = author.data?.metadata; const metadata = author.data?.metadata;
const { data: sellerInfo } = useSellerInfo(pubkey); const { data: sellerInfo } = useSellerInfo(pubkey);
@ -83,6 +84,14 @@ export default function ListingDetailPage() {
); );
} }
if (isError) {
return (
<div className="container mx-auto max-w-md px-4 py-16">
<ErrorState onRetry={() => { void refetch(); }} />
</div>
);
}
if (!item) { if (!item) {
return ( return (
<> <>