diff --git a/src/components/CountdownTimer.tsx b/src/components/CountdownTimer.tsx index 852a763..1a4574d 100644 --- a/src/components/CountdownTimer.tsx +++ b/src/components/CountdownTimer.tsx @@ -31,10 +31,10 @@ export default function CountdownTimer({ endTime, className = "", initialSeconds return ( - {isExpired ? "Ended" : formattedTime} + {isExpired ? 'Ended' : formattedTime} ); } diff --git a/src/components/RoundCard.tsx b/src/components/RoundCard.tsx index d82957a..8fb75f0 100644 --- a/src/components/RoundCard.tsx +++ b/src/components/RoundCard.tsx @@ -1,17 +1,18 @@ - // ISSUE: Wire place_bet() to Xelma TypeScript bindings (xelma-contract) // ISSUE: Real-time round updates via Soroban event polling import { useEffect, useRef, useState } from 'react'; import type { MockRound } from '../types'; -import { useState } from "react"; - - import CountdownTimer from './CountdownTimer'; -import PanelHeader from './PanelHeader'; import { formatVXLM, formatPercent } from '../lib/utils'; import { TRANSITION } from '../utils/motion'; +const ASSET_ICONS: Record = { + BTC: '₿', + ETH: 'Ξ', + XLM: '✦', +}; + interface RoundCardProps { round: MockRound; onSubmitPrediction: (round: MockRound) => void; @@ -35,29 +36,25 @@ function poolSize(round: MockRound): number { } export default function RoundCard({ round, onSubmitPrediction }: RoundCardProps) { - - const [endTime] = useState(() => Date.now() + round.closesInSeconds * 1000); + const [endTime, setEndTime] = useState(() => new Date(Date.now() + round.closesInSeconds * 1000)); const total = poolSize(round); - const upRatio = round.mode === 'updown' && total > 0 ? (round.poolUp ?? 0) / total : 0; const upPct = Math.round(upRatio * 100); const downPct = round.mode === 'updown' ? 100 - upPct : 0; - const [endTime] = useState(() => new Date(Date.now() + round.closesInSeconds * 1000)); const statusMeta = getStatusMeta(round, round.closesInSeconds); const prevStatus = useRef(statusMeta.label); const [statusAnnouncement, setStatusAnnouncement] = useState(''); - const [endTime, setEndTime] = useState(() => new Date(Date.now() + round.closesInSeconds * 1000)); useEffect(() => { - const timer = setTimeout(() => { + const timer = window.setTimeout(() => { setEndTime(new Date(Date.now() + round.closesInSeconds * 1000)); }, 0); - return () => clearTimeout(timer); + return () => window.clearTimeout(timer); }, [round.closesInSeconds]); useEffect(() => { - const timer = setTimeout(() => { + const timer = window.setTimeout(() => { if (round.closesInSeconds <= 0) { setStatusAnnouncement('Round has ended'); } else if (prevStatus.current !== statusMeta.label) { @@ -65,30 +62,44 @@ export default function RoundCard({ round, onSubmitPrediction }: RoundCardProps) prevStatus.current = statusMeta.label; } }, 0); - return () => clearTimeout(timer); - }, [statusMeta.label, round.closesInSeconds]); + return () => window.clearTimeout(timer); + }, [round.closesInSeconds, statusMeta.label]); return (
{statusAnnouncement}
+
- {round.mode === "updown" ? "UP/DOWN" : "PRECISION"} + {ASSET_ICONS[round.asset]} - } - /> +
+

{round.asset}/USD

+

+ Reference ${round.startPrice.toLocaleString()} +

+
+
+ + + {round.mode === 'updown' ? 'UP/DOWN' : 'PRECISION'} + +
onSubmitPrediction(round)} - className="btn-primary mt-2 flex min-h-[44px] w-full items-center justify-center rounded-xl px-4 py-3 text-sm font-bold disabled:opacity-50 disabled:cursor-not-allowed" + className="btn-primary mt-2 flex min-h-[44px] w-full items-center justify-center rounded-xl px-4 py-3 text-sm font-bold disabled:cursor-not-allowed disabled:opacity-50" data-testid="round-card-submit" > Submit Prediction diff --git a/src/components/StatsCard.tsx b/src/components/StatsCard.tsx index 3975b72..1f27a6e 100644 --- a/src/components/StatsCard.tsx +++ b/src/components/StatsCard.tsx @@ -7,6 +7,7 @@ import { claim_winnings } from '../lib/xelma-contract'; import { toast } from 'sonner'; import { formatVXLM } from '../lib/utils'; import RankProgressBar from './RankProgressBar'; +import PanelHeader from './PanelHeader'; interface StatsCardProps { stats: MockUserStats; diff --git a/src/hooks/__tests__/useRoundCountdown.test.ts b/src/hooks/__tests__/useRoundCountdown.test.ts index f251038..9750914 100644 --- a/src/hooks/__tests__/useRoundCountdown.test.ts +++ b/src/hooks/__tests__/useRoundCountdown.test.ts @@ -1,5 +1,5 @@ -import { renderHook, act } from '@testing-library/react'; -import { beforeEach, afterEach, describe, expect, it, vi } from 'vitest'; +import { act, renderHook } from '@testing-library/react'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { useRoundCountdown } from '../../hooks/useRoundCountdown'; describe('useRoundCountdown Hook', () => { @@ -9,55 +9,42 @@ describe('useRoundCountdown Hook', () => { }); afterEach(() => { -import { renderHook, act } from "@testing-library/react"; -import { describe, test, expect, beforeAll, afterAll, vi } from "vitest"; -import { useRoundCountdown } from '../../hooks/useRoundCountdown'; - -// Helper to advance timers safely -function advance(ms: number) { - act(() => { - vi.advanceTimersByTime(ms); - }); -} - -describe('useRoundCountdown Hook', () => { - beforeAll(() => { - vi.useFakeTimers(); - vi.setSystemTime(new Date('2026-06-27T12:00:00Z')); // fixed now - }); - - afterAll(() => { vi.useRealTimers(); }); - it('Expired context returns isExpired true and 00:00', () => { + it('returns 00:00 and expires for past times', () => { const past = new Date('2026-06-27T11:00:00Z'); const { result } = renderHook(() => useRoundCountdown(past)); + expect(result.current.isExpired).toBe(true); expect(result.current.formattedTime).toBe('00:00'); expect(result.current.timeLeftMs).toBe(0); }); - it('Sub‑minute context shows mm:ss format', () => { + it('shows mm:ss formatting for sub-minute countdowns', () => { const future = new Date(Date.now() + 30 * 1000); const { result } = renderHook(() => useRoundCountdown(future)); + expect(result.current.isExpired).toBe(false); - expect(result.current.formattedTime).toBe('0:30'); + expect(result.current.formattedTime).toBe('00:30'); act(() => { vi.advanceTimersByTime(10 * 1000); }); - expect(result.current.formattedTime).toBe('0:20'); + + expect(result.current.formattedTime).toBe('00:20'); }); - it('Multi‑hour context formats HH:MM:SS', () => { + it('formats multi-hour countdowns as HH:MM:SS', () => { const future = new Date(Date.now() + (1 * 3600 + 2 * 60 + 3) * 1000); const { result } = renderHook(() => useRoundCountdown(future)); + expect(result.current.formattedTime).toBe('01:02:03'); act(() => { vi.advanceTimersByTime(62 * 1000); }); + expect(result.current.formattedTime).toBe('01:01:01'); }); }); diff --git a/src/hooks/useRoundCountdown.ts b/src/hooks/useRoundCountdown.ts index 3776c9b..b691883 100644 --- a/src/hooks/useRoundCountdown.ts +++ b/src/hooks/useRoundCountdown.ts @@ -35,8 +35,8 @@ export function useRoundCountdown( if (h > 0) { return `${pad(h)}:${pad(m)}:${pad(s)}`; } - // When less than an hour, omit hour component and do not pad minutes. - return `${m}:${pad(s)}`; + // When less than an hour, show a zero-padded minute value. + return `${pad(m)}:${pad(s)}`; }; useEffect(() => { diff --git a/src/pages/Dashboard.tsx b/src/pages/Dashboard.tsx index a12997f..226bb11 100644 --- a/src/pages/Dashboard.tsx +++ b/src/pages/Dashboard.tsx @@ -171,8 +171,8 @@ const Dashboard = () => { {!isLoading && !isRoundActive && (