Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions app/(dashboard)/events/event-page/EventDetailsClient.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ import { Separator } from "@/components/ui/separator";
import { Clock, DollarSign, Users, BarChart2, Loader2, Share2 } from "lucide-react";
import { formatDistanceToNowStrict, parseISO, isValid } from "date-fns";
import { MarketDetailTabs } from "@/components/market/MarketDetailTabs";
import { ResolutionPreview } from "@/components/market/ResolutionPreview";
import { AboutMarketModal } from "@/app/components/AboutMarketModal";
import { ShareSheet } from "@/app/components/ShareSheet";
import { useMediaQuery } from "@/hooks/use-media-query";
import {
Expand Down Expand Up @@ -222,6 +224,15 @@ export default function EventDetailsClient() {
: undefined;
const potentialPayout =
currentOdds && betAmount ? parseFloat(betAmount || "0") * currentOdds : 0;
const parsedDeadline = parseISO(eventData.deadline);
const deadlineLabel = isValid(parsedDeadline)
? parsedDeadline.toLocaleDateString(undefined, {
year: "numeric",
month: "short",
day: "numeric",
})
: "No deadline set";
const resolutionCriteria = `This market resolves after the betting deadline using verified public outcome sources. The winning outcome must match the final result described by the market premise: ${eventData.description}`;

const overviewTab = (
<div className="space-y-6">
Expand Down Expand Up @@ -467,6 +478,13 @@ export default function EventDetailsClient() {
</h1>
<div className="flex items-center gap-2">
<Badge variant="outline">{eventData.category}</Badge>
<AboutMarketModal
marketTitle={eventData.title}
category={eventData.category}
description={eventData.description}
resolutionCriteria={resolutionCriteria}
deadlineLabel={deadlineLabel}
/>
<ShareSheet
title={eventData.title}
text={`Check out "${eventData.title}" on Predictify!`}
Expand Down
107 changes: 107 additions & 0 deletions app/components/AboutMarketModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
"use client"

import * as React from "react"
import { CalendarCheck, ClipboardCheck, Info } from "lucide-react"

import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog"
import { cn } from "@/lib/utils"

interface AboutMarketModalProps {
marketTitle: string
category: string
description: string
resolutionCriteria: string
deadlineLabel?: string
className?: string
}

export function AboutMarketModal({
marketTitle,
category,
description,
resolutionCriteria,
deadlineLabel,
className,
}: AboutMarketModalProps) {
const summaryId = React.useId()

return (
<Dialog>
<DialogTrigger asChild>
<Button
type="button"
variant="outline"
size="sm"
className={cn("gap-2", className)}
aria-label={`About this market: ${marketTitle}`}
>
<Info className="h-4 w-4" aria-hidden="true" />
About this market
</Button>
</DialogTrigger>
<DialogContent
className="max-h-[90vh] overflow-y-auto sm:max-w-xl"
aria-describedby={summaryId}
>
<DialogHeader>
<DialogTitle>About this market</DialogTitle>
<DialogDescription>
Review the market premise, key details, and resolution criteria.
</DialogDescription>
</DialogHeader>

<p id={summaryId} className="sr-only">
Screen reader summary: {marketTitle} is a {category} market.
{deadlineLabel ? ` It closes on ${deadlineLabel}.` : ""}{" "}
{resolutionCriteria}
</p>

<div className="space-y-5">
<section className="space-y-3 rounded-lg border bg-muted/30 p-4">
<div className="flex flex-wrap items-center gap-2">
<Badge variant="outline">{category}</Badge>
{deadlineLabel ? (
<span className="inline-flex items-center gap-1.5 text-xs text-muted-foreground">
<CalendarCheck className="h-3.5 w-3.5" aria-hidden="true" />
{deadlineLabel}
</span>
) : null}
</div>
<div className="space-y-1">
<h3 className="text-base font-semibold text-foreground">
{marketTitle}
</h3>
<p className="text-sm leading-relaxed text-muted-foreground">
{description}
</p>
</div>
</section>

<section className="space-y-2 rounded-lg border p-4">
<div className="flex items-center gap-2">
<ClipboardCheck
className="h-4 w-4 text-primary"
aria-hidden="true"
/>
<h3 className="text-sm font-semibold text-foreground">
Resolution criteria
</h3>
</div>
<p className="text-sm leading-relaxed text-muted-foreground">
{resolutionCriteria}
</p>
</section>
</div>
</DialogContent>
</Dialog>
)
}
31 changes: 31 additions & 0 deletions app/components/__tests__/AboutMarketModal.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import React from "react"
import { render, screen, fireEvent } from "@testing-library/react"
import { AboutMarketModal } from "@/app/components/AboutMarketModal"

const defaultProps = {
marketTitle: "Super Bowl Winner 2025",
category: "Sports",
description:
"Predict which team will win the Super Bowl LIX scheduled to be played on February 9, 2025.",
resolutionCriteria:
"The market resolves to the official championship winner after the final whistle and source verification.",
deadlineLabel: "Feb 9, 2025",
}

describe("AboutMarketModal", () => {
it("opens an accessible dialog with market context and resolution criteria", () => {
render(<AboutMarketModal {...defaultProps} />)

fireEvent.click(
screen.getByRole("button", { name: /about this market/i })
)

expect(
screen.getByRole("dialog", { name: /about this market/i })
).toBeInTheDocument()
expect(screen.getByText("Super Bowl Winner 2025")).toBeInTheDocument()
expect(screen.getByText("Sports")).toBeInTheDocument()
expect(screen.getAllByText(/official championship winner/i)).toHaveLength(2)
expect(screen.getByText(/screen reader summary/i)).toBeInTheDocument()
})
})
4 changes: 2 additions & 2 deletions components/ui/dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ const DialogContentWithFocusReturn = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
>(({ className, children, onOpenAutoFocus, ...props }, ref) => {
const triggerRef = useRef<HTMLElement | null>(null);
const triggerRef = React.useRef<HTMLElement | null>(null);

// Store the trigger element when the dialog opens
const handleOpenAutoFocus = (event: Event) => {
Expand Down Expand Up @@ -197,4 +197,4 @@ export {
DialogFooter,
DialogTitle,
DialogDescription,
};
};
18 changes: 18 additions & 0 deletions docs/about-market-modal.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# About Market Modal

The market detail page includes an `About this market` action beside the market category and share action. It opens `AboutMarketModal`, which summarizes the market premise, category, deadline, and resolution criteria without moving users away from the detail page.

## Accessibility

- The trigger is a keyboard-focusable button with a market-specific accessible label.
- The dialog uses Radix Dialog for focus trapping, Escape dismissal, and focus return.
- A screen-reader-only summary connects the dialog content to the market title, category, deadline, and resolution criteria.
- Icons are decorative and marked with `aria-hidden`.

## Verification

Run the focused component test:

```bash
node node_modules/jest/bin/jest.js app/components/__tests__/AboutMarketModal.test.tsx --runInBand
```