From 318720e3967596fe3c53f52fe96eaaa64b4f4840 Mon Sep 17 00:00:00 2001 From: GiftedGiftB Date: Fri, 24 Jul 2026 01:38:03 +0100 Subject: [PATCH 1/4] fix: countdown reduced-motion --- components/disputes/shared/CountdownTimer.tsx | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/components/disputes/shared/CountdownTimer.tsx b/components/disputes/shared/CountdownTimer.tsx index 0b47aac6..e3a206ab 100644 --- a/components/disputes/shared/CountdownTimer.tsx +++ b/components/disputes/shared/CountdownTimer.tsx @@ -210,9 +210,12 @@ export function CountdownTimer({ deadline, label }: CountdownTimerProps) { > {visibleLabel} - - {announcement} - + {/* Avoid duplicate text and unnecessary live region updates when reduced motion is preferred */} + {!prefersReducedMotion && ( + + {announcement} + + )} ); } From e88631cbd4748eb5061973b43143bb739a914c4c Mon Sep 17 00:00:00 2001 From: GiftedGiftB Date: Fri, 24 Jul 2026 01:40:05 +0100 Subject: [PATCH 2/4] style: add minor whitespace for code readability in CountdownTimer and tests --- components/disputes/shared/CountdownTimer.tsx | 2 ++ components/disputes/shared/__tests__/CountdownTimer.test.tsx | 1 + 2 files changed, 3 insertions(+) diff --git a/components/disputes/shared/CountdownTimer.tsx b/components/disputes/shared/CountdownTimer.tsx index e3a206ab..26298b14 100644 --- a/components/disputes/shared/CountdownTimer.tsx +++ b/components/disputes/shared/CountdownTimer.tsx @@ -6,6 +6,7 @@ import { cn } from '@/lib/utils'; interface CountdownTimerProps { deadline: Date; label?: string; + } interface TimeLeft { @@ -13,6 +14,7 @@ interface TimeLeft { hours: number; minutes: number; seconds: number; + } function computeTimeLeft(deadline: Date): TimeLeft | null { diff --git a/components/disputes/shared/__tests__/CountdownTimer.test.tsx b/components/disputes/shared/__tests__/CountdownTimer.test.tsx index be349ca8..5a75fd8a 100644 --- a/components/disputes/shared/__tests__/CountdownTimer.test.tsx +++ b/components/disputes/shared/__tests__/CountdownTimer.test.tsx @@ -200,6 +200,7 @@ describe('CountdownTimer', () => { const liveRegion = container.querySelector('[aria-live="polite"]'); expect(liveRegion?.textContent).toBe('Deadline passed'); }); + }); describe('reduced motion', () => { From 1f96ecfaae1b23e6954cbc9454e28db397990678 Mon Sep 17 00:00:00 2001 From: GiftedGiftB Date: Fri, 24 Jul 2026 02:01:15 +0100 Subject: [PATCH 3/4] feat: live activity ticker --- app/(marketing)/page.tsx | 2 + app/components/LiveTicker.tsx | 93 ++++++++++++++++++++ app/components/__tests__/LiveTicker.test.tsx | 59 +++++++++++++ tailwind.config.ts | 7 +- 4 files changed, 160 insertions(+), 1 deletion(-) create mode 100644 app/components/LiveTicker.tsx create mode 100644 app/components/__tests__/LiveTicker.test.tsx diff --git a/app/(marketing)/page.tsx b/app/(marketing)/page.tsx index b2875c5b..e3ddd14c 100644 --- a/app/(marketing)/page.tsx +++ b/app/(marketing)/page.tsx @@ -17,6 +17,7 @@ import { HowItWorks } from "./_sections/how-it-works"; import { WalletsTokens } from "@/components/sections/wallets-token"; import { AnimatedBackground } from "@/components/ui/animated-background"; import Navbar from "./_components/navbar"; +import { LiveTicker } from "@/app/components/LiveTicker"; import KpiStrip from "./_sections/kpi-strip"; import { CTA } from "./_sections/cta"; import { useParallax } from "@/hooks/use-parallax"; @@ -34,6 +35,7 @@ export default function MarketingRoute() { + diff --git a/app/components/LiveTicker.tsx b/app/components/LiveTicker.tsx new file mode 100644 index 00000000..dbd579c0 --- /dev/null +++ b/app/components/LiveTicker.tsx @@ -0,0 +1,93 @@ +"use client"; + +import React, { useEffect, useState } from "react"; + +interface TickerEvent { + id: string; + text: string; + timestamp: string; +} + +const mockEvents: TickerEvent[] = [ + { id: "1", text: "User 0x4F... placed 500 XLM on 'ETH > $4K'", timestamp: "1m ago" }, + { id: "2", text: "New Market created: US Elections 2028", timestamp: "3m ago" }, + { id: "3", text: "Market 'Super Bowl LIX' resolved to 'Chiefs'", timestamp: "5m ago" }, + { id: "4", text: "User 0xA1... claimed 1,200 XLM reward", timestamp: "12m ago" }, + { id: "5", text: "Dispute opened on 'SpaceX Mars Mission'", timestamp: "15m ago" }, + { id: "6", text: "User 0x9B... predicted Yes on 'Stellar Protocol 22'", timestamp: "18m ago" }, +]; + +export function LiveTicker() { + const [prefersReducedMotion, setPrefersReducedMotion] = useState(false); + + useEffect(() => { + const mediaQuery = window.matchMedia("(prefers-reduced-motion: reduce)"); + setPrefersReducedMotion(mediaQuery.matches); + + const handler = (e: MediaQueryListEvent) => setPrefersReducedMotion(e.matches); + + if (typeof mediaQuery.addEventListener === 'function') { + mediaQuery.addEventListener("change", handler); + return () => mediaQuery.removeEventListener("change", handler); + } else { + mediaQuery.addListener(handler); + return () => mediaQuery.removeListener(handler); + } + }, []); + + if (prefersReducedMotion) { + return ( +
+
+ + + + Live Activity: + {mockEvents[0].text} + ({mockEvents[0].timestamp}) +
+
+ ); + } + + return ( + + ); +} diff --git a/app/components/__tests__/LiveTicker.test.tsx b/app/components/__tests__/LiveTicker.test.tsx new file mode 100644 index 00000000..0900fe75 --- /dev/null +++ b/app/components/__tests__/LiveTicker.test.tsx @@ -0,0 +1,59 @@ +import { render, screen } from "@testing-library/react"; +import { LiveTicker } from "../LiveTicker"; +import "@testing-library/jest-dom"; + +const setReducedMotion = (matches: boolean) => { + window.matchMedia = jest.fn().mockImplementation((query: string) => ({ + matches: query === "(prefers-reduced-motion: reduce)" ? matches : false, + media: query, + onchange: null, + addListener: jest.fn(), + removeListener: jest.fn(), + addEventListener: jest.fn(), + removeEventListener: jest.fn(), + dispatchEvent: jest.fn(), + })); +}; + +describe("LiveTicker", () => { + beforeEach(() => { + setReducedMotion(false); + }); + + describe("Normal Motion", () => { + it("renders the continuous marquee container when reduced motion is disabled", () => { + render(); + + const tickerContainer = screen.getByTestId("live-ticker-marquee"); + expect(tickerContainer).toBeInTheDocument(); + expect(tickerContainer).toHaveAttribute("aria-hidden", "true"); + + const marqueeElements = document.querySelectorAll(".animate-marquee"); + expect(marqueeElements.length).toBeGreaterThanOrEqual(2); + }); + + it("displays the mock events", () => { + render(); + const events = screen.getAllByText(/User 0x4F\.\.\. placed 500 XLM/i); + expect(events.length).toBeGreaterThanOrEqual(1); + }); + }); + + describe("Reduced Motion", () => { + it("falls back to a static accessible region when reduced motion is enabled", () => { + setReducedMotion(true); + render(); + + expect(screen.queryByTestId("live-ticker-marquee")).not.toBeInTheDocument(); + + const region = screen.getByRole("region", { name: "Recent Market Activity" }); + expect(region).toBeInTheDocument(); + + const eventText = screen.getByText(/User 0x4F\.\.\. placed 500 XLM/i); + expect(eventText).toBeInTheDocument(); + + const staticLabel = screen.getByText(/Live Activity:/i); + expect(staticLabel).toBeInTheDocument(); + }); + }); +}); diff --git a/tailwind.config.ts b/tailwind.config.ts index 1dacc435..e6b46d30 100644 --- a/tailwind.config.ts +++ b/tailwind.config.ts @@ -124,11 +124,16 @@ const config: Config = { to: { height: '0' } + }, + 'marquee': { + from: { transform: 'translateX(0)' }, + to: { transform: 'translateX(-100%)' } } }, animation: { 'accordion-down': 'accordion-down 0.2s ease-out', - 'accordion-up': 'accordion-up 0.2s ease-out' + 'accordion-up': 'accordion-up 0.2s ease-out', + 'marquee': 'marquee 30s linear infinite' } } }, From 29ca7b1ab1a3531584cfa07aedc72fcc14352b1c Mon Sep 17 00:00:00 2001 From: GiftedGiftB Date: Fri, 24 Jul 2026 02:03:28 +0100 Subject: [PATCH 4/4] refactor: clean up whitespace and formatting in CountdownTimer component and tests --- components/disputes/shared/CountdownTimer.tsx | 4 +++- components/disputes/shared/__tests__/CountdownTimer.test.tsx | 3 ++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/components/disputes/shared/CountdownTimer.tsx b/components/disputes/shared/CountdownTimer.tsx index 26298b14..d4f94221 100644 --- a/components/disputes/shared/CountdownTimer.tsx +++ b/components/disputes/shared/CountdownTimer.tsx @@ -14,7 +14,7 @@ interface TimeLeft { hours: number; minutes: number; seconds: number; - + } function computeTimeLeft(deadline: Date): TimeLeft | null { @@ -57,6 +57,7 @@ function buildAnnouncement(t: TimeLeft): string { if (parts.length === 0) return 'Less than one second remaining'; return `${parts.join(', ')} remaining`; + } /** @@ -121,6 +122,7 @@ export function CountdownTimer({ deadline, label }: CountdownTimerProps) { const [expired, setExpired] = useState(() => { if (!isValidDate(deadline)) return false; return deadline.getTime() <= Date.now(); + }); // The string the aria-live region currently shows. Updates only at coarse diff --git a/components/disputes/shared/__tests__/CountdownTimer.test.tsx b/components/disputes/shared/__tests__/CountdownTimer.test.tsx index 5a75fd8a..ffe8ff27 100644 --- a/components/disputes/shared/__tests__/CountdownTimer.test.tsx +++ b/components/disputes/shared/__tests__/CountdownTimer.test.tsx @@ -200,7 +200,7 @@ describe('CountdownTimer', () => { const liveRegion = container.querySelector('[aria-live="polite"]'); expect(liveRegion?.textContent).toBe('Deadline passed'); }); - + }); describe('reduced motion', () => { @@ -223,6 +223,7 @@ describe('CountdownTimer', () => { const countdownEl = screen.getByText(/seconds remaining/); expect(countdownEl).toHaveClass('text-destructive'); expect(countdownEl).not.toHaveClass('animate-pulse'); + }); }); });