From c4b5fb542944bd60e8515127d1dccb73a5b097a3 Mon Sep 17 00:00:00 2001 From: onyekachi66 Date: Sun, 30 Aug 2026 19:02:30 +0100 Subject: [PATCH 1/8] fix: keep leaderboard rank pages deterministic - Added secondary sort key (user.name) to LeaderboardTable for deterministic tie-breaking. - Resolved race conditions in useLeaderboard hook using a fetchIdRef. - Added useEffect to LeaderboardSection to fetch data upon activeTab changes. - Improved permission error handling to avoid exposing sensitive HTML payloads. Closes #123 --- components/leaderboard/LeaderboardSection.tsx | 35 +++++++++++++------ components/leaderboard/LeaderboardTable.tsx | 16 ++++++--- hooks/useLeaderboard.ts | 6 ++-- 3 files changed, 41 insertions(+), 16 deletions(-) diff --git a/components/leaderboard/LeaderboardSection.tsx b/components/leaderboard/LeaderboardSection.tsx index af2debd3..392983ff 100644 --- a/components/leaderboard/LeaderboardSection.tsx +++ b/components/leaderboard/LeaderboardSection.tsx @@ -19,21 +19,36 @@ export function LeaderboardSection() { const [activeTab, setActiveTab] = useState("all-time"); const [isUserVisible, setIsUserVisible] = useState(true); - const { status, data, error, refetch, retry } = useLeaderboard({ - fetcher: async () => { - const res = await fetch(`/api/leaderboard?period=${activeTab}`, { - cache: "no-store", - }); - if (!res.ok) { - const body = await res.text(); - throw new Error(body || `Failed to load leaderboard (${res.status})`); + const fetcher = React.useCallback(async () => { + const res = await fetch(`/api/leaderboard?period=${activeTab}`, { + cache: "no-store", + }); + if (!res.ok) { + if (res.status === 401 || res.status === 403) { + throw new Error("You do not have permission to view this leaderboard."); + } + let errorMessage = `Failed to load leaderboard (${res.status})`; + try { + const data = await res.json(); + if (data && data.error) errorMessage = data.error; + } catch (e) { + // ignore JSON parse errors } - return res.json(); - }, + throw new Error(errorMessage); + } + return res.json(); + }, [activeTab]); + + const { status, data, error, refetch, retry } = useLeaderboard({ + fetcher, initialData: mockLeaderboardData, staleTime: 2 * 60 * 1000, }); + React.useEffect(() => { + refetch(); + }, [refetch]); + const users = data.length > 0 ? data : mockLeaderboardData; const topThree = users.slice(0, 3); const others = users.slice(3); diff --git a/components/leaderboard/LeaderboardTable.tsx b/components/leaderboard/LeaderboardTable.tsx index 8d689a55..9699cabd 100644 --- a/components/leaderboard/LeaderboardTable.tsx +++ b/components/leaderboard/LeaderboardTable.tsx @@ -67,10 +67,18 @@ export function LeaderboardTable({ return [...users].sort((left, right) => { const leftValue = left[sortKey]; const rightValue = right[sortKey]; - const comparison = - typeof leftValue === "string" && typeof rightValue === "string" - ? leftValue.localeCompare(rightValue) - : Number(leftValue) - Number(rightValue); + + let comparison = 0; + if (typeof leftValue === "string" && typeof rightValue === "string") { + comparison = leftValue.localeCompare(rightValue); + } else { + comparison = Number(leftValue) - Number(rightValue); + } + + if (comparison === 0) { + // Fallback to name to guarantee deterministic sorting for identical values + comparison = left.name.localeCompare(right.name); + } return sortDirection === "desc" ? -comparison : comparison; }); diff --git a/hooks/useLeaderboard.ts b/hooks/useLeaderboard.ts index 2c6dd65a..916435ec 100644 --- a/hooks/useLeaderboard.ts +++ b/hooks/useLeaderboard.ts @@ -51,6 +51,7 @@ export function useLeaderboard({ const retryCountRef = useRef(0); const mountedRef = useRef(true); + const fetchIdRef = useRef(0); useEffect(() => { mountedRef.current = true; @@ -60,12 +61,13 @@ export function useLeaderboard({ }, []); const executeFetch = useCallback(async () => { + const currentFetchId = ++fetchIdRef.current; setState((prev) => ({ ...prev, status: "loading", error: null })); try { const data = await fetcher(); - if (!mountedRef.current) return; + if (!mountedRef.current || currentFetchId !== fetchIdRef.current) return; const now = Date.now(); setState({ @@ -77,7 +79,7 @@ export function useLeaderboard({ }); retryCountRef.current = 0; } catch (err) { - if (!mountedRef.current) return; + if (!mountedRef.current || currentFetchId !== fetchIdRef.current) return; const message = err instanceof Error ? err.message : "Unknown error"; setState((prev) => ({ From c0798d23fe17638a8e1acb7b9cfee8116c3f0911 Mon Sep 17 00:00:00 2001 From: onyekachi66 Date: Fri, 4 Sep 2026 19:20:33 +0100 Subject: [PATCH 2/8] fix: resolve syntax errors and duplicate imports causing build failure --- app/(dashboard)/layout.tsx | 2 +- components/active-bets/ActiveBetCard.tsx | 4 ++-- components/receipts/Receipt.tsx | 10 +++++----- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/app/(dashboard)/layout.tsx b/app/(dashboard)/layout.tsx index c665c551..da4dbd5c 100644 --- a/app/(dashboard)/layout.tsx +++ b/app/(dashboard)/layout.tsx @@ -8,7 +8,7 @@ import { Breadcrumbs } from "@/components/navbar/Breadcrumbs"; import { MobileBottomTabs } from "@/components/navbar/MobileBottomTabs"; import { ConnectWalletModal } from "@/components/connect-wallet-modal"; import { getBreadcrumbsForPath } from "@/lib/breadcrumbs"; -import { MobileBottomTabs } from "@/components/navbar/MobileBottomTabs"; +import { ErrorBoundary } from "@/components/error-boundary"; export default function DashboardLayout({ children, diff --git a/components/active-bets/ActiveBetCard.tsx b/components/active-bets/ActiveBetCard.tsx index 4c156bff..95a4a3ce 100644 --- a/components/active-bets/ActiveBetCard.tsx +++ b/components/active-bets/ActiveBetCard.tsx @@ -138,7 +138,7 @@ export const ActiveBetCard: React.FC )} -

+

{title}

{t.showDates && bet && ( @@ -171,7 +171,7 @@ export const ActiveBetCard: React.FC
diff --git a/components/receipts/Receipt.tsx b/components/receipts/Receipt.tsx index f42b1e6d..ad6031d7 100644 --- a/components/receipts/Receipt.tsx +++ b/components/receipts/Receipt.tsx @@ -2,7 +2,7 @@ import React from 'react'; import { CheckCircle2, Printer, ArrowLeft } from 'lucide-react'; -import { ReceitShare } from '@/app/components/ReceiptShare'; +import { ReceiptShare } from '@/app/components/ReceiptShare'; import { usePrivacy } from '@/context/PrivacyContext'; import { maskAmount } from '@/utils/maskAmount'; @@ -41,8 +41,8 @@ export function Receipt({ receiptId, amount, partyA, partyB, timestamp, type }:
-

Predictify

-

Predictify +

Official Record

@@ -136,7 +136,7 @@ export function Receipt({ receiptId, amount, partyA, partyB, timestamp, type }: aria-label="Print this receipt" className="w-full flex items-center justify-center gap-2 bg-foreground text-background py-3.5 px-4 rounded-xl font-semibold hover:opacity-90 transition-opacity" > - +
From 9379ab7af50140d1606eba85c7e70471add62d69 Mon Sep 17 00:00:00 2001 From: onyekachi66 Date: Fri, 4 Sep 2026 19:35:30 +0100 Subject: [PATCH 3/8] fix: resolve syntax errors in navbar and extract dynamic imports to client component --- app/markets/[id]/AboutMarketModalLoader.tsx | 52 +++++++++++++++++++++ app/markets/[id]/page.tsx | 52 ++------------------- components/navbar/WalletBalance.tsx | 2 +- components/navbar/WalletMenu.tsx | 4 +- 4 files changed, 60 insertions(+), 50 deletions(-) create mode 100644 app/markets/[id]/AboutMarketModalLoader.tsx diff --git a/app/markets/[id]/AboutMarketModalLoader.tsx b/app/markets/[id]/AboutMarketModalLoader.tsx new file mode 100644 index 00000000..65fdf43c --- /dev/null +++ b/app/markets/[id]/AboutMarketModalLoader.tsx @@ -0,0 +1,52 @@ +"use client"; + +import dynamic from "next/dynamic"; +import { Button } from "@/components/ui/button"; +import { Skeleton } from "@/components/ui/skeleton"; +import { Info } from "lucide-react"; +import type { AboutMarketModalProps } from "@/app/components/AboutMarketModal"; + +export const AboutMarketModal = dynamic( + () => import("@/app/components/AboutMarketModal").then((mod) => mod.AboutMarketModal), + { + ssr: false, + loading: () => ( + + ), + } +); + +export const PredictionCommentsLoader = dynamic<{ predictionId: string }>( + () => import("@/app/components/PredictionCommentsLoader").then((mod) => mod.default), + { + ssr: false, + loading: () => ( +
+
+ + + +
+
+ ), + } +); + +export const MarketDetailClient = dynamic<{ marketTitle: string; marketId: string }>( + () => import("./MarketDetailClient").then((mod) => mod.MarketDetailClient), + { + ssr: false, + loading: () => null, + } +); + +export default AboutMarketModal; diff --git a/app/markets/[id]/page.tsx b/app/markets/[id]/page.tsx index 8bde1697..2cc3242f 100644 --- a/app/markets/[id]/page.tsx +++ b/app/markets/[id]/page.tsx @@ -1,53 +1,11 @@ import type { Metadata } from "next"; -import dynamic from "next/dynamic"; import MarketHero from "./hero"; import { Tabs } from "@/app/components/Tabs"; -import { Button } from "@/components/ui/button"; -import { Skeleton } from "@/components/ui/skeleton"; -import { Info } from "lucide-react"; - -const AboutMarketModal = dynamic( - () => import("@/app/components/AboutMarketModal").then((mod) => mod.AboutMarketModal), - { - ssr: false, - loading: () => ( - - ), - } -); - -const PredictionCommentsLoader = dynamic( - () => import("@/app/components/PredictionCommentsLoader").then((mod) => mod.default), - { - ssr: false, - loading: () => ( -
-
- - - -
-
- ), - } -); - -const MarketDetailClient = dynamic( - () => import("./MarketDetailClient").then((mod) => mod.MarketDetailClient), - { - ssr: false, - loading: () => null, - } -); +import { + AboutMarketModal, + PredictionCommentsLoader, + MarketDetailClient, +} from "./AboutMarketModalLoader"; /** * Market Detail Page — Server Component diff --git a/components/navbar/WalletBalance.tsx b/components/navbar/WalletBalance.tsx index f094da5e..ff50d074 100644 --- a/components/navbar/WalletBalance.tsx +++ b/components/navbar/WalletBalance.tsx @@ -29,7 +29,7 @@ export function WalletBalance({ className = "" }: { className?: string }) { if (isLoading && !balance) { return (
diff --git a/components/navbar/WalletMenu.tsx b/components/navbar/WalletMenu.tsx index cab05884..0f77b5bd 100644 --- a/components/navbar/WalletMenu.tsx +++ b/components/navbar/WalletMenu.tsx @@ -93,11 +93,11 @@ export function WalletMenu() {
setIsOpen(true)} className="cursor-pointer" aria-label="Switch wallet"> - + Switch { void disconnectWallet(); }} className="cursor-pointer" aria-label="Disconnect wallet"> - + Disconnect From c98cae52c0822e4678fa78f1e0cb73f92eebe59f Mon Sep 17 00:00:00 2001 From: onyekachi66 Date: Fri, 4 Sep 2026 19:51:45 +0100 Subject: [PATCH 4/8] fix: ensure deterministic sorting, safe partial field handling, and robust retry boundaries --- components/leaderboard/LeaderboardSection.tsx | 7 +++++- components/leaderboard/LeaderboardTable.tsx | 22 ++++++++++++++----- hooks/useLeaderboard.ts | 7 ++++-- 3 files changed, 27 insertions(+), 9 deletions(-) diff --git a/components/leaderboard/LeaderboardSection.tsx b/components/leaderboard/LeaderboardSection.tsx index 392983ff..23e59569 100644 --- a/components/leaderboard/LeaderboardSection.tsx +++ b/components/leaderboard/LeaderboardSection.tsx @@ -45,9 +45,14 @@ export function LeaderboardSection() { staleTime: 2 * 60 * 1000, }); + const isFirstRender = React.useRef(true); React.useEffect(() => { + if (isFirstRender.current) { + isFirstRender.current = false; + return; + } refetch(); - }, [refetch]); + }, [activeTab, refetch]); const users = data.length > 0 ? data : mockLeaderboardData; const topThree = users.slice(0, 3); diff --git a/components/leaderboard/LeaderboardTable.tsx b/components/leaderboard/LeaderboardTable.tsx index 9699cabd..6815bc86 100644 --- a/components/leaderboard/LeaderboardTable.tsx +++ b/components/leaderboard/LeaderboardTable.tsx @@ -33,7 +33,10 @@ function getResponsiveImageSource(source: string, width: number): string { return source; } -function formatProfit(value: number): string { +function formatProfit(value?: number): string { + if (value === undefined || value === null || isNaN(value)) { + return "+0.00"; + } const formatted = value.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2, @@ -72,12 +75,19 @@ export function LeaderboardTable({ if (typeof leftValue === "string" && typeof rightValue === "string") { comparison = leftValue.localeCompare(rightValue); } else { - comparison = Number(leftValue) - Number(rightValue); + const leftNum = Number(leftValue ?? 0); + const rightNum = Number(rightValue ?? 0); + comparison = (isNaN(leftNum) ? 0 : leftNum) - (isNaN(rightNum) ? 0 : rightNum); } if (comparison === 0) { // Fallback to name to guarantee deterministic sorting for identical values - comparison = left.name.localeCompare(right.name); + comparison = (left.name ?? "").localeCompare(right.name ?? ""); + } + + if (comparison === 0) { + // Fallback to rank as tiebreaker + comparison = (left.rank ?? 0) - (right.rank ?? 0); } return sortDirection === "desc" ? -comparison : comparison; @@ -135,7 +145,7 @@ export function LeaderboardTable({ } setSortKey(nextKey); - setSortDirection("desc"); + setSortDirection(nextKey === "rank" ? "asc" : "desc"); }; const renderSortIcon = (key: SortKey) => { @@ -270,8 +280,8 @@ export function LeaderboardTable({ {user.name} {formatProfit(user.profit)} - {user.winRate}% - {user.predictions} + {user.winRate !== undefined && user.winRate !== null ? `${user.winRate}%` : "—"} + {user.predictions ?? 0} ); })} diff --git a/hooks/useLeaderboard.ts b/hooks/useLeaderboard.ts index 916435ec..0b31babb 100644 --- a/hooks/useLeaderboard.ts +++ b/hooks/useLeaderboard.ts @@ -95,16 +95,19 @@ export function useLeaderboard({ }, [executeFetch]); const retry = useCallback(async () => { + retryCountRef.current += 1; if (retryCountRef.current >= maxRetries) { setState((prev) => ({ ...prev, + status: "error", error: "Maximum retry attempts reached. Please try again later.", })); return; } - retryCountRef.current += 1; - await new Promise((resolve) => setTimeout(resolve, retryDelay)); + if (retryDelay > 0) { + await new Promise((resolve) => setTimeout(resolve, retryDelay)); + } await executeFetch(); }, [executeFetch, maxRetries, retryDelay]); From fdf2123338fe95e4c1f319397b8a01974b43d81f Mon Sep 17 00:00:00 2001 From: onyekachi66 Date: Fri, 4 Sep 2026 20:00:14 +0100 Subject: [PATCH 5/8] fix: add missing closing brace on className in WalletBalance --- components/navbar/WalletBalance.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/navbar/WalletBalance.tsx b/components/navbar/WalletBalance.tsx index ff50d074..64f6c9dc 100644 --- a/components/navbar/WalletBalance.tsx +++ b/components/navbar/WalletBalance.tsx @@ -54,7 +54,7 @@ export function WalletBalance({ className = "" }: { className?: string }) { return (