{/* Filter Panel */}
diff --git a/frontend/hooks/__tests__/useTradeList.test.ts b/frontend/hooks/__tests__/useTradeList.test.ts
index e193fac..327d843 100644
--- a/frontend/hooks/__tests__/useTradeList.test.ts
+++ b/frontend/hooks/__tests__/useTradeList.test.ts
@@ -145,4 +145,53 @@ describe("useTradeList Hook", () => {
expect.stringContaining("maxAmount=5000")
);
});
+
+ it("exposes isEmpty=true only on a successful empty response", async () => {
+ mockedApiFetch.mockResolvedValueOnce({ data: [], total: 0 });
+
+ const { result } = renderHook(() => useTradeList({ page: 1, limit: 10 }));
+
+ await waitFor(() => expect(result.current.isLoading).toBe(false));
+
+ expect(result.current.error).toBeNull();
+ expect(result.current.isEmpty).toBe(true);
+ });
+
+ it("keeps isEmpty=false (and sets error) when the fetch fails", async () => {
+ const apiError = new ApiError("marketplace unavailable", 500);
+ mockedApiFetch.mockRejectedValueOnce(apiError);
+
+ const { result } = renderHook(() => useTradeList({ page: 1, limit: 10 }));
+
+ await waitFor(() => expect(result.current.isLoading).toBe(false));
+
+ expect(result.current.error).toBe(apiError);
+ expect(result.current.isEmpty).toBe(false);
+ });
+
+ it("keeps isEmpty=false on a successful non-empty response", async () => {
+ mockedApiFetch.mockResolvedValueOnce({
+ data: [
+ {
+ id: "trade-1",
+ sellerId: "user-1",
+ assetCode: "MTN_AIRTIME",
+ amount: 5000,
+ price: 5000,
+ currency: "NGN",
+ status: TradeStatus.ACTIVE,
+ createdAt: new Date(),
+ updatedAt: new Date(),
+ },
+ ],
+ total: 1,
+ });
+
+ const { result } = renderHook(() => useTradeList({ page: 1, limit: 10 }));
+
+ await waitFor(() => expect(result.current.isLoading).toBe(false));
+
+ expect(result.current.error).toBeNull();
+ expect(result.current.isEmpty).toBe(false);
+ });
});
diff --git a/frontend/hooks/useTradeList.ts b/frontend/hooks/useTradeList.ts
index dbca0ab..8a62552 100644
--- a/frontend/hooks/useTradeList.ts
+++ b/frontend/hooks/useTradeList.ts
@@ -18,6 +18,12 @@ export interface UseTradeListReturn {
trades: TradeOffer[];
total: number;
isLoading: boolean;
+ /**
+ * `true` only when the latest load **succeeded** and returned zero trades.
+ * Never `true` while loading or when `error` is set — so a caller can tell
+ * "no listings available" apart from "failed to load listings".
+ */
+ isEmpty: boolean;
error: ApiError | Error | null;
refetch: () => Promise
;
}
@@ -47,6 +53,7 @@ export function useTradeList(options: UseTradeListOptions): UseTradeListReturn {
const [trades, setTrades] = useState([]);
const [total, setTotal] = useState(0);
const [isLoading, setIsLoading] = useState(true);
+ const [isEmpty, setIsEmpty] = useState(false);
const [error, setError] = useState(null);
const queryParams = new URLSearchParams();
@@ -69,6 +76,7 @@ export function useTradeList(options: UseTradeListOptions): UseTradeListReturn {
setTotal(cached.data.total);
setIsLoading(false);
setError(null);
+ setIsEmpty(cached.data.trades.length === 0);
return;
}
}
@@ -77,6 +85,7 @@ export function useTradeList(options: UseTradeListOptions): UseTradeListReturn {
setIsLoading(true);
}
setError(null);
+ setIsEmpty(false);
try {
let promise = inFlightPromises.get(cacheKey);
@@ -98,12 +107,14 @@ export function useTradeList(options: UseTradeListOptions): UseTradeListReturn {
setTrades(result.trades);
setTotal(result.total);
setIsLoading(false);
+ setIsEmpty(result.trades.length === 0);
}
} catch (err) {
inFlightPromises.delete(cacheKey);
if (isMounted.current) {
const errorObj = err instanceof ApiError ? err : (err as Error);
setError(errorObj);
+ setIsEmpty(false);
setIsLoading(false);
}
}
@@ -127,6 +138,7 @@ export function useTradeList(options: UseTradeListOptions): UseTradeListReturn {
trades,
total,
isLoading,
+ isEmpty,
error,
refetch: () => fetchData(true),
};