diff --git a/README.md b/README.md index fb588ac3..bd4733b1 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,7 @@ Built with Next.js 15, React 19, TypeScript, and the Stellar Wallets Kit, Predic - **Instant Payouts**: Smart contracts automatically distribute winnings immediately after event resolution. No waiting periods. - **Multi-Wallet Support**: Connect with your preferred Stellar wallet (Freighter, LOBSTR, XBull, Albedo, Rabet). - **Real-Time Markets**: Access live prediction markets with real-time updates on odds, stakes, and participant activity. +- **Side-by-Side Market Comparison**: Compare up to two selected prediction markets in a responsive modal with accessible keyboard and focus support. - **Advanced Analytics**: Track your prediction performance with detailed analytics and insights. - **Create Your Markets**: Anyone can create prediction markets on virtually any verifiable future event. - **Verified Oracles**: Multiple oracle sources ensure accurate and tamper-proof event outcomes. diff --git a/app/components/CompareMarketsModal.tsx b/app/components/CompareMarketsModal.tsx new file mode 100644 index 00000000..cc5d77c4 --- /dev/null +++ b/app/components/CompareMarketsModal.tsx @@ -0,0 +1,184 @@ +"use client" + +import * as React from "react" +import { X, Users, Calendar, TrendingUp, Tag, Hash } from "lucide-react" +import { cn } from "@/lib/utils" +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogDescription, +} from "@/components/ui/dialog" +import { Badge } from "@/components/ui/badge" +import { Button } from "@/components/ui/button" +import { ScrollArea } from "@/components/ui/scroll-area" +import { useCompareStore } from "@/lib/compare-store" +import { useEventsStore } from "@/lib/events-store" +import type { Event } from "@/types/events" + +const categoryClass: Record = { + Football: "bg-[#EBE7F6] text-[#4400FF] border-0", + Politics: "bg-[#E7F6EC] text-[#036B26] border-0", + Crypto: "bg-[#FBF703] text-[#865503] border-0", + Stocks: "bg-[#03E6FB3B] text-[#035C86] border-0", +} + +const statusClass: Record = { + ongoing: "bg-emerald-100 text-emerald-700", + upcoming: "bg-blue-100 text-blue-700", + past: "bg-gray-100 text-gray-600", +} + +function MarketColumn({ event }: { event: Event }) { + return ( +
+

{event.title}

+ +
+ + {event.category} + + + {event.status} + +
+ +
+ } label="Odds" value={`${event.odds}x`} /> + } + label="Participants" + value={event.participants.toLocaleString()} + /> + } + label="Start date" + value={formatDate(event.startDate)} + /> + } label="End date" value={formatDate(event.endDate)} /> + {event.timeRemaining && ( + } label="Time remaining" value={event.timeRemaining} /> + )} + } + label="Tx hash" + value={ + + {event.txHash} + + } + /> +
+
+ ) +} + +function Row({ icon, label, value }: { icon: React.ReactNode; label: string; value: React.ReactNode }) { + return ( +
+
+ {icon} + {label} +
+
{value}
+
+ ) +} + +function formatDate(iso: string) { + try { + return new Date(iso).toLocaleDateString(undefined, { + year: "numeric", + month: "short", + day: "numeric", + }) + } catch { + return iso + } +} + +export function CompareMarketsModal() { + const { selectedIds, overlayOpen, closeOverlay, clear } = useCompareStore() + const { events } = useEventsStore() + + const selected = selectedIds + .map((id) => events.find((event) => event.id === id)) + .filter((event): event is Event => Boolean(event)) + + React.useEffect(() => { + if (!overlayOpen) return + + const handler = (event: KeyboardEvent) => { + if (event.key === "Backspace" && (event.metaKey || event.ctrlKey)) { + event.preventDefault() + clear() + } + } + + window.addEventListener("keydown", handler) + return () => window.removeEventListener("keydown", handler) + }, [overlayOpen, clear]) + + return ( + !open && closeOverlay()}> + + +
+ + Compare Markets + + + Side-by-side comparison of selected prediction markets. Press{" "} + + Esc + {" "} + to close or{" "} + + ⌘⌫ + {" "} + to clear. + +
+ +
+ + + {selected.length === 0 ? ( +

+ No markets selected for comparison. +

+ ) : ( +
+ {selected.map((event) => ( + + ))} +
+ )} +
+
+
+ ) +} diff --git a/components/events/events-section.tsx b/components/events/events-section.tsx index d028f37a..73a4f842 100644 --- a/components/events/events-section.tsx +++ b/components/events/events-section.tsx @@ -16,7 +16,7 @@ import { EventsTable } from "./events-table" import { EventsPagination } from "./pagination" import { useEventsStore, getEventCounts } from "@/lib/events-store" /* Compare feature */ -import { CompareOverlay } from "@/components/market/CompareOverlay" +import { CompareMarketsModal } from "@/app/components/CompareMarketsModal" import { CompareSelectionChip } from "@/components/market/CompareSelectionChip" interface EventsSectionProps { @@ -42,7 +42,7 @@ export function EventsSection({ className }: EventsSectionProps) { return (
{/* Compare overlay dialog (portal-rendered) */} - + {/* Floating selection chip */} {/* MODIFIED: Added Create Event button alongside the title */} diff --git a/components/market/__tests__/compare-overlay.test.tsx b/components/market/__tests__/compare-overlay.test.tsx index 3e776352..18bc34a3 100644 --- a/components/market/__tests__/compare-overlay.test.tsx +++ b/components/market/__tests__/compare-overlay.test.tsx @@ -4,6 +4,7 @@ */ import React from "react" import { render, screen, fireEvent, act } from "@testing-library/react" +import { CompareMarketsModal } from "@/app/components/CompareMarketsModal" import { CompareOverlay } from "@/components/market/CompareOverlay" import { CompareSelectionChip } from "@/components/market/CompareSelectionChip" import { useCompareStore } from "@/lib/compare-store" @@ -94,6 +95,20 @@ describe("CompareSelectionChip", () => { }) }) +// ── CompareMarketsModal ───────────────────────────────────────────────────── +describe("CompareMarketsModal", () => { + it("renders the compare modal header and content for two selected markets", () => { + act(() => useCompareStore.setState({ selectedIds: ["1", "2"], overlayOpen: true })) + render() + + expect(screen.getByRole("dialog")).toBeInTheDocument() + expect(screen.getByText("Compare Markets")).toBeInTheDocument() + expect(screen.getByText(/Side-by-side comparison/i)).toBeInTheDocument() + expect(screen.getByText("Arsenal vs Liverpool")).toBeInTheDocument() + expect(screen.getByText("Bitcoin Price")).toBeInTheDocument() + }) +}) + // ── CompareOverlay ─────────────────────────────────────────────────────────── describe("CompareOverlay", () => { it("is not visible when overlayOpen=false", () => {