diff --git a/components/active-bets/ActiveBetCard.tsx b/components/active-bets/ActiveBetCard.tsx index 5442d85c..4c156bff 100644 --- a/components/active-bets/ActiveBetCard.tsx +++ b/components/active-bets/ActiveBetCard.tsx @@ -1,7 +1,8 @@ -'use client'; +'Use client'; import React from 'react'; import Image from 'next/image'; + import { cn } from '@/lib/utils'; import { ActiveBetCardProps } from '@/lib/types'; import { categoryColors } from '@/lib/mock-data'; @@ -30,9 +31,7 @@ export const ActiveBetCard: React.FC { if (!timeString) return ''; @@ -72,31 +71,29 @@ export const ActiveBetCard: React.FC - {/* Skeleton Overlay */} - - {/* Content Container */}
{ + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + e.currentTarget.click(); + } + }} > - {/* Thumbnail and Category */} -
+
{thumbnail && ( {title} )} -
+
- {/* Category Chip */} {bet?.category?.name && (
)}
- - {/* Title */} -

+

{title}

- - {/* Start/End Dates — hidden in ultra */} {t.showDates && bet && (
-
+
-
+
)} - - {/* Progress Bar and Time Remaining */}
- {/* Progress Bar */} -
-
+
- - {/* Time Remaining */}
{bet && ( - + )} - + Time remaining: {formatTimeRemaining(timeRemaining)}
@@ -206,4 +193,4 @@ export const ActiveBetCard: React.FC
); -}; +}; \ No newline at end of file diff --git a/components/claims/ClaimEligibilityStatus.tsx b/components/claims/ClaimEligibilityStatus.tsx index fd85f5c0..a3b31689 100644 --- a/components/claims/ClaimEligibilityStatus.tsx +++ b/components/claims/ClaimEligibilityStatus.tsx @@ -6,15 +6,15 @@ * Displays whether the connected user is eligible to claim winnings for a market * based on authoritative evidence. It models every required state explicitly: * - * - loading — accessible skeleton + screen-reader announcement; the + * - loading -- accessible skeleton + screen-reader announcement; the * previous (last-good) result is preserved while reloading so * the user never loses data they were viewing. - * - success — a colour-blind-safe badge (pattern + text) describing the + * - success - a colour-blind-safe badge (pattern + text) describing the * decision, plus a stale-evidence warning when applicable. - * - error — an actionable alert with a Retry button when retryable. - * - permission — a non-retryable alert prompting the user to connect a + * - error - an actionable alert with a Retry button when retryable. + * - permission - a non-retryable alert prompting the user to connect a * wallet (the authorization / permission state). - * - not_found — a neutral "unavailable" empty state. + * - not_found - a neutral "unavailable" empty state. * * WCAG 2.1 AA: * - role="status" + aria-live="polite" for dynamic updates. @@ -23,7 +23,8 @@ */ import React from "react"; -import { AlertCircle, Loader2, Lock, RefreshCw, ShieldCheck } from "lucide-react"; +import { AlertCircle, AlertTriangle, CheckCircle2, Clock, HelpCircle, Loader2, Lock, RefreshCw, ShieldCheck, XCircle } from "lucide-react"; +import type { LucideIcon } from "lucide-react"; import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; import { Button } from "@/components/ui/button"; import { Skeleton } from "@/components/ui/skeleton"; @@ -39,6 +40,9 @@ import type { } from "@/types/claim-eligibility"; import { cn } from "@/lib/utils"; +// Imported CSSProperties for inline patterns that guarantee non-color distinction. +import type { CSSProperties } from "react"; + export interface ClaimEligibilityStatusProps { marketId: string; account?: string; @@ -66,7 +70,7 @@ const DECISION_STYLES: Record = { }, already_claimed: { label: "Already claimed", - badgeClass: "bg-muted text-muted-foreground pattern-crosshatch", + badgeClass: "bg-muted text-foreground pattern-crosshatch", tone: "neutral", }, pending: { @@ -81,37 +85,81 @@ const DECISION_STYLES: Record = { }, ineligible_wrong_outcome: { label: "Not eligible", - badgeClass: "bg-muted text-muted-foreground pattern-crosshatch", + badgeClass: "bg-muted text-foreground pattern-crosshatch", tone: "neutral", }, ineligible_unresolved: { label: "Outcome pending", - badgeClass: "bg-muted text-muted-foreground pattern-horizontal", + badgeClass: "bg-muted text-foreground pattern-horizontal", tone: "neutral", }, unknown: { label: "Eligibility unknown", - badgeClass: "bg-muted text-muted-foreground pattern-crosshatch", + badgeClass: "bg-muted text-foreground pattern-crosshatch", tone: "warning", }, }; +const DECISION_ICONS: Record = { + eligible: CheckCircle2, + already_claimed: ShieldCheck, + pending: Clock, + disputed: AlertTriangle, + ineligible_wrong_outcome: XCircle, + ineligible_unresolved: Clock, + unknown: HelpCircle, +}; + +// Inline background patterns ensure the visual distinction is not dependent on +// external CSS (SC 1.4.1). Each pattern uses a subtle, high-contrast stripe/ +// geometry that is visible on the decision-specific background class. +const DECISION_PATTERNS: Record = { + eligible: { + backgroundImage: + "repeating-linear-gradient(45deg, rgba(255,255,255,0.18) 0, rgba(255,255,255,0.18) 6px, transparent 6px, transparent 12px)", + }, + already_claimed: { + backgroundImage: + "repeating-linear-gradient(90deg, rgba(0,0,0,0.08) 0, rgba(0,0,0,0.08) 4px, transparent 4px, transparent 8px)", + }, + pending: { + backgroundImage: + "repeating-linear-gradient(0deg, rgba(0,0,0,0.08) 0, rgba(0,0,0,0.08) 4px, transparent 4px, transparent 8px)", + }, + disputed: { + backgroundImage: + "repeating-linear-gradient(90deg, rgba(0,0,0,0.12) 0, rgba(0,0,0,0.12) 6px, transparent 6px, transparent 12px)", + }, + ineligible_wrong_outcome: { + backgroundImage: + "repeating-linear-gradient(45deg, rgba(0,0,0,0.08) 0, rgba(0,0,0,0.08) 4px, transparent 4px, transparent 8px)", + }, + ineligible_unresolved: { + backgroundImage: + "repeating-linear-gradient(0deg, rgba(0,0,0,0.08) 0, rgba(0,0,0,0.08) 4px, transparent 4px, transparent 8px)", + }, + unknown: { + backgroundImage: + "repeating-linear-gradient(135deg, rgba(0,0,0,0.08) 0, rgba(0,0,0,0.08) 4px, transparent 4px, transparent 8px)", + }, +}; + function EligibilityBadge({ decision, }: { decision: ClaimEligibilityDecision; }) { const style = DECISION_STYLES[decision]; + const Icon = DECISION_ICONS[decision]; return ( - + {style.label} ); @@ -131,7 +179,7 @@ function ErrorAlert({ if (isPermission) { return ( - + Authorization required Connect your wallet to view claim eligibility for this market. @@ -143,7 +191,7 @@ function ErrorAlert({ if (isNotFound) { return ( - + Eligibility unavailable Authoritative claim evidence is not available for this market. @@ -154,11 +202,12 @@ function ErrorAlert({ return ( - - Couldn't load eligibility + + Couldn't load eligibility {error.message} {canRetry && ( - )} + ))} ); } @@ -243,6 +292,7 @@ export const ClaimEligibilityStatus: React.FC = ({ aria-live="polite" aria-atomic="true" aria-busy={phase === "loading"} + aria-label={`Claim eligibility for market ${marketId}`} data-testid={`claim-eligibility-${marketId}`} > {content} diff --git a/components/events/events-table.tsx b/components/events/events-table.tsx index 47e75838..726b55a7 100644 --- a/components/events/events-table.tsx +++ b/components/events/events-table.tsx @@ -3,7 +3,7 @@ import * as React from "react" import Link from "next/link" /* NEW: Added lucide icons for row actions and compare */ -import { Edit, MoreHorizontal, Trash2, Users, Calendar, Trophy, Building2, CircleDollarSign, LineChart, TrendingUp, GitCompareArrows } from "lucide-react" +import { Edit, MoreHorizontal, Trash2, Users, Calendar, Trophy, Building2, CircleDollarSign, LineChart, TrendingUp, GitCompareArrows, ShieldCheck, Clock, AlertTriangle } from "lucide-react" import { HoverTooltip } from "@/components/HoverTooltip" import { cn } from "@/lib/utils" import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table" @@ -94,10 +94,19 @@ function TimeRemainingProgress({ event }: { event: Event }) { return () => clearInterval(interval) }, []) - if (!event.timeRemainingMs) { + if (typeof event.timeRemainingMs !== "number" || !Number.isFinite(event.timeRemainingMs)) { return - } + if (event.timeRemainingMs <= 0) { + return ( + + + ) + } + const color = getTimeRemainingColor(event.timeRemainingMs) const timeString = formatTimeRemaining(event.timeRemainingMs) @@ -106,19 +115,31 @@ function TimeRemainingProgress({ event }: { event: Event }) { const currentDays = event.timeRemainingMs / (24 * 60 * 60 * 1000) const progressValue = Math.max(0, Math.min(100, (currentDays / maxDays) * 100)) - const urgencyLabel = { green: "Low urgency", orange: "Medium urgency", red: "High urgency" }[color] + const urgencyLabels: Record = { + green: "Low urgency", + orange: "Medium urgency", + red: "High urgency", + } + const urgencyLabel = urgencyLabels[color] ?? "Unknown urgency" + const urgencyIcons: Record = { + green: