Skip to content
2 changes: 1 addition & 1 deletion app/(dashboard)/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
52 changes: 52 additions & 0 deletions app/markets/[id]/AboutMarketModalLoader.tsx
Original file line number Diff line number Diff line change
@@ -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<AboutMarketModalProps>(
() => import("@/app/components/AboutMarketModal").then((mod) => mod.AboutMarketModal),
{
ssr: false,
loading: () => (
<Button
variant="outline"
size="sm"
disabled
className="h-10 gap-2 px-4 opacity-80"
aria-label="Loading market details"
>
<Info className="h-4 w-4" aria-hidden="true" />
<span>About Market</span>
</Button>
),
}
);

export const PredictionCommentsLoader = dynamic<{ predictionId: string }>(
() => import("@/app/components/PredictionCommentsLoader").then((mod) => mod.default),
{
ssr: false,
loading: () => (
<div className="rounded-xl border border-border bg-card p-4">
<div className="space-y-2">
<Skeleton className="h-4 w-28 rounded-md" />
<Skeleton className="h-4 w-full rounded-md" />
<Skeleton className="h-4 w-5/6 rounded-md" />
</div>
</div>
),
}
);

export const MarketDetailClient = dynamic<{ marketTitle: string; marketId: string }>(
() => import("./MarketDetailClient").then((mod) => mod.MarketDetailClient),
{
ssr: false,
loading: () => null,
}
);

export default AboutMarketModal;
52 changes: 5 additions & 47 deletions app/markets/[id]/page.tsx
Original file line number Diff line number Diff line change
@@ -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: () => (
<Button
variant="outline"
size="sm"
disabled
className="h-10 gap-2 px-4 opacity-80"
aria-label="Loading market details"
>
<Info className="h-4 w-4" aria-hidden="true" />
<span>About Market</span>
</Button>
),
}
);

const PredictionCommentsLoader = dynamic(
() => import("@/app/components/PredictionCommentsLoader").then((mod) => mod.default),
{
ssr: false,
loading: () => (
<div className="rounded-xl border border-border bg-card p-4">
<div className="space-y-2">
<Skeleton className="h-4 w-28 rounded-md" />
<Skeleton className="h-4 w-full rounded-md" />
<Skeleton className="h-4 w-5/6 rounded-md" />
</div>
</div>
),
}
);

const MarketDetailClient = dynamic(
() => import("./MarketDetailClient").then((mod) => mod.MarketDetailClient),
{
ssr: false,
loading: () => null,
}
);
import {
AboutMarketModal,
PredictionCommentsLoader,
MarketDetailClient,
} from "./AboutMarketModalLoader";

/**
* Market Detail Page — Server Component
Expand Down
4 changes: 2 additions & 2 deletions components/active-bets/ActiveBetCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ export const ActiveBetCard: React.FC<ActiveBetCardProps & { isLoading?: boolean
</div>
)}
</div>
<h3 className={cn("font-semibold text-foreground leading-tight mb-3", t.titleSize)_>
<h3 className={cn("font-semibold text-foreground leading-tight mb-3", t.titleSize)}>
{title}
</h3>
{t.showDates && bet && (
Expand Down Expand Up @@ -171,7 +171,7 @@ export const ActiveBetCard: React.FC<ActiveBetCardProps & { isLoading?: boolean
'h-full rounded-full transition-all duration-300ms',
categoryStyle.progress
)}
style={{ width: `${progress}%``}}
style={{ width: `${progress}%` }}
/>
</div>
<div className="flex justify-end items-center gap-2">
Expand Down
4 changes: 2 additions & 2 deletions components/claims/ClaimEligibilityStatus.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,7 @@ function ErrorAlert({
}

return (
<Alert variant="destructive">
<Alert className="border-destructive/40 bg-destructive/10">
<AlertCircle className="h-4 w-4" aria-hidden={true} />
<AlertTitle>Couldn&#39;t load eligibility</AlertTitle>
<AlertDescription>{error.message}</AlertDescription>
Expand All @@ -217,7 +217,7 @@ function ErrorAlert({
<RefreshCw className="mr-2 h-3.5 w-3.5" aria-hidden={true} />
Retry
</Button>
))}
)}
</Alert>
);
}
Expand Down
40 changes: 30 additions & 10 deletions components/leaderboard/LeaderboardSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,21 +19,41 @@ 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,
});

const isFirstRender = React.useRef(true);
React.useEffect(() => {
if (isFirstRender.current) {
isFirstRender.current = false;
return;
}
refetch();
}, [activeTab, refetch]);

const users = data.length > 0 ? data : mockLeaderboardData;
const topThree = users.slice(0, 3);
const others = users.slice(3);
Expand Down
34 changes: 26 additions & 8 deletions components/leaderboard/LeaderboardTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -67,10 +70,25 @@ 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 {
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 ?? "");
}

if (comparison === 0) {
// Fallback to rank as tiebreaker
comparison = (left.rank ?? 0) - (right.rank ?? 0);
}

return sortDirection === "desc" ? -comparison : comparison;
});
Expand Down Expand Up @@ -127,7 +145,7 @@ export function LeaderboardTable({
}

setSortKey(nextKey);
setSortDirection("desc");
setSortDirection(nextKey === "rank" ? "asc" : "desc");
};

const renderSortIcon = (key: SortKey) => {
Expand Down Expand Up @@ -262,8 +280,8 @@ export function LeaderboardTable({
<span>{user.name}</span>
</td>
<td className="px-4 py-3">{formatProfit(user.profit)}</td>
<td className="px-4 py-3">{user.winRate}%</td>
<td className="px-4 py-3">{user.predictions}</td>
<td className="px-4 py-3">{user.winRate !== undefined && user.winRate !== null ? `${user.winRate}%` : "—"}</td>
<td className="px-4 py-3">{user.predictions ?? 0}</td>
</tr>
);
})}
Expand Down
4 changes: 2 additions & 2 deletions components/navbar/WalletBalance.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ export function WalletBalance({ className = "" }: { className?: string }) {
if (isLoading && !balance) {
return (
<div
className={`flex items-center gap-1.5 px-3 py-1.5 rounded-lg bg-slate-800/50 animate-pulse ${className} ${privacyClasses}`
className={`flex items-center gap-1.5 px-3 py-1.5 rounded-lg bg-slate-800/50 animate-pulse ${className} ${privacyClasses}`}
aria-label="Loading wallet balance"
>
<div className="h-3 w-16 bg-slate-700 rounded" />
Expand All @@ -54,7 +54,7 @@ export function WalletBalance({ className = "" }: { className?: string }) {

return (
<div
className={`flex items-center gap-1.5 px-3 py-1.5 rounded-lg bg-slate-800/50 text-slate-300 text-sm font-mono ${className} ${privacyClasses}`
className={`flex items-center gap-1.5 px-3 py-1.5 rounded-lg bg-slate-800/50 text-slate-300 text-sm font-mono ${className} ${privacyClasses}`}
aria-label={`Wallet balance: ${formatBalance(balance)} XLM`}
>
<Wallet className="h-3.5 w-3.5 text-cyan-400" aria-hidden="true" />
Expand Down
4 changes: 2 additions & 2 deletions components/navbar/WalletMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -93,11 +93,11 @@ export function WalletMenu() {
</div>
</DropdownMenuItem>
<DropdownMenuItem role="menuitem" onClick={() => setIsOpen(true)} className="cursor-pointer" aria-label="Switch wallet">
<RefreshCcw className="mr-2 h-4"w-4" />
<RefreshCcw className="mr-2 h-4 w-4" />
Switch
</DropdownMenuItem>
<DropdownMenuItem role="menuitem" onClick={() => { void disconnectWallet(); }} className="cursor-pointer" aria-label="Disconnect wallet">
<LogOutIcon className="mr-2 h-4"w-4" />
<LogOutIcon className="mr-2 h-4 w-4" />
Disconnect
</DropdownMenuItem>
<DropdownMenuItem asChild>
Expand Down
10 changes: 5 additions & 5 deletions components/receipts/Receipt.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -41,8 +41,8 @@ export function Receipt({ receiptId, amount, partyA, partyB, timestamp, type }:
<div className="receipt-wrapper flex flex-col items-center justify-center w-full min-h-[80vh] print:min-h-0 print:py-12">
<div className="receipt-container relative w-full max-w-lg bg-card print:bg-white rounded-[24px] print:rounded-none overflow-hidden pb-8 print:pb-0 shadow-sm print:shadow-none border border-border/40 print:border-none">
<div className="hidden print:flex flex-col items-center pt-8 pb-4 w-full text-center">
<h1 className="text-3 xl font-black tracking-tighter text-black">Predictify</h1>
<p className="text-xs text-gray-400 uppercase tracking-[5.3em] mt-2 Official Record</p>
<h1 className="text-3xl font-black tracking-tighter text-black">Predictify</h1>
<p className="text-xs text-gray-400 uppercase tracking-[0.3em] mt-2">Official Record</p>
</div>

<div className="flex flex-col items-center text-center pt-12 pb-8 px-8">
Expand Down Expand Up @@ -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"
>
<Printer className="w-4 h-4 aria-hidden="true" />
<Printer className="w-4 h-4" aria-hidden="true" />
Print Receipt
</button>
<ReceiptShare
Expand All @@ -154,7 +154,7 @@ export function Receipt({ receiptId, amount, partyA, partyB, timestamp, type }:
onClick={() => window.history.back()}
aria-label="Return to the previous page"
>
<ArrowLeft className="w-4 h-4 aria-hidden="true" />
<ArrowLeft className="w-4 h-4" aria-hidden="true" />
Back to Dashboard
</button>
</div>
Expand Down
Loading
Loading