diff --git a/README.md b/README.md index fb588ac3..821b02e5 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,7 @@ Built with Next.js 15, React 19, TypeScript, and the Stellar Wallets Kit, Predic ## Features - **Decentralized & Transparent**: All predictions are recorded on-chain with complete transparency. No central authority controls the outcomes. +- **Shareable Prediction Receipts**: Completed predictions can now be shared with a polished receipt summary for campaigns such as GrantFox FWC26. - **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. diff --git a/app/a11y-audit/page.test.tsx b/app/a11y-audit/page.test.tsx new file mode 100644 index 00000000..9bb43574 --- /dev/null +++ b/app/a11y-audit/page.test.tsx @@ -0,0 +1,23 @@ +import { render, screen } from '@testing-library/react' +import '@testing-library/jest-dom' +import A11yAuditPage from './page' + +describe('A11y audit page', () => { + it('renders the board summary and status table', () => { + render() + + expect(screen.getByRole('heading', { name: /accessibility audit board/i })).toBeInTheDocument() + expect(screen.getByRole('region', { name: /board summary/i })).toBeInTheDocument() + expect(screen.getByRole('table', { name: /grantfox accessibility audit board/i })).toBeInTheDocument() + expect(screen.getByText('ConnectWalletModal')).toBeInTheDocument() + expect(screen.getByText('New event form focus order')).toBeInTheDocument() + }) + + it('shows the expected status counts', () => { + render() + + expect(screen.getByText('6')).toBeInTheDocument() + expect(screen.getByText('1')).toBeInTheDocument() + expect(screen.getByText('0')).toBeInTheDocument() + }) +}) diff --git a/app/a11y-audit/page.tsx b/app/a11y-audit/page.tsx new file mode 100644 index 00000000..bd8e45ce --- /dev/null +++ b/app/a11y-audit/page.tsx @@ -0,0 +1,194 @@ +import type { Metadata } from 'next' + +type AuditItem = { + component: string + status: 'Verified' | 'Partial' | 'Needs follow-up' + summary: string + evidence: string[] +} + +const auditItems: AuditItem[] = [ + { + component: 'ConnectWalletModal', + status: 'Verified', + summary: + 'Provider labels, announced badge state, and recovery messaging are covered for assistive technologies.', + evidence: ['components/connect-wallet-modal.tsx', 'components/__tests__/connect-wallet-modal.test.tsx'], + }, + { + component: 'Outcome icons and dispute voting states', + status: 'Verified', + summary: + 'Outcome affordances rely on text and shape cues rather than color alone, with decorative icon handling verified.', + evidence: [ + 'components/icons/OutcomeIcons.tsx', + 'components/icons/__tests__/OutcomeIcons.test.tsx', + 'components/disputes/shared/TallyBar.tsx', + ], + }, + { + component: 'DisputeOutcomeExplainer', + status: 'Verified', + summary: + 'Dialog flow, math steps, and tally values are structured so the explanation remains readable at larger zoom levels.', + evidence: [ + 'components/disputes/DisputeOutcomeExplainer.tsx', + 'components/disputes/__tests__/DisputeOutcomeExplainer.test.tsx', + ], + }, + { + component: 'ErrorRecoveryScreen', + status: 'Verified', + summary: + 'Recovery actions, incident copy, and escape paths are all available through keyboard and screen-reader friendly patterns.', + evidence: [ + 'components/error/ErrorRecoveryScreen.tsx', + 'components/error/ErrorRecoveryScreen.test.tsx', + 'components/error-boundary.tsx', + ], + }, + { + component: 'VirtualizedEventsList', + status: 'Verified', + summary: + 'Keyboard focus visibility, loading messaging, and scroll restoration remain intact during large-list navigation.', + evidence: [ + 'components/events/virtualized-events-list.tsx', + 'components/events/__tests__/virtualized-events-list.integration.test.tsx', + ], + }, + { + component: 'New event form focus order', + status: 'Partial', + summary: + 'Early tab order is covered, but the full form sequence still needs a broader review for keyboard continuity.', + evidence: ['app/(dashboard)/events/new/page.tsx', 'app/(dashboard)/events/new/page.test.tsx'], + }, + { + component: 'Focus-visible CSS layer', + status: 'Verified', + summary: + 'Global focus rings remain visible in dark mode and are paired with strong contrast for interactive surfaces.', + evidence: ['app/styles/focus.css', 'app/globals.css', 'app/__tests__/focus-visible.test.js'], + }, +] + +const statusStyles: Record = { + Verified: 'bg-emerald-100 text-emerald-800 dark:bg-emerald-900/30 dark:text-emerald-200', + Partial: 'bg-amber-100 text-amber-800 dark:bg-amber-900/30 dark:text-amber-200', + 'Needs follow-up': 'bg-rose-100 text-rose-800 dark:bg-rose-900/30 dark:text-rose-200', +} + +export const metadata: Metadata = { + title: 'Accessibility audit board', + description: 'Internal GrantFox accessibility audit board for WCAG 2.1 AA review status.', +} + +export default function A11yAuditPage() { + const verifiedCount = auditItems.filter((item) => item.status === 'Verified').length + const partialCount = auditItems.filter((item) => item.status === 'Partial').length + const followUpCount = auditItems.filter((item) => item.status === 'Needs follow-up').length + + return ( +
+
+
+
+ Internal accessibility board +
+
+

+ Accessibility audit board +

+

+ GrantFox-facing surfaces are tracked here for WCAG 2.1 AA readiness, with focused evidence for each component. +

+
+
+ +
+
+

Verified

+

{verifiedCount}

+
+
+

Partial

+

{partialCount}

+
+
+

Needs follow-up

+

{followUpCount}

+
+
+ +
+
+

AA status by component

+

+ Status is based on implementation evidence and targeted audit coverage. +

+
+
+ + + + + + + + + + + + {auditItems.map((item) => ( + + + + + + + ))} + +
+ GrantFox accessibility audit board +
+ Component + + Status + + What was verified + + Evidence +
+ {item.component} + + + {item.status} + + + {item.summary} + +
    + {item.evidence.map((piece) => ( +
  • {piece}
  • + ))} +
+
+
+
+ +
+

How to use this board

+
    +
  • Use the status values as a quick review summary before shipping a GrantFox-facing change.
  • +
  • Pair any “Partial” item with a follow-up pass for keyboard and screen-reader behavior.
  • +
  • Keep the evidence list in sync with the implementation and test files when the component changes.
  • +
+
+
+
+ ) +} diff --git a/app/components/ReceiptShare.tsx b/app/components/ReceiptShare.tsx new file mode 100644 index 00000000..89301421 --- /dev/null +++ b/app/components/ReceiptShare.tsx @@ -0,0 +1,157 @@ +"use client" + +import { useMemo, useState } from "react" +import { Check, Copy, Download, ExternalLink, Share2 } from "lucide-react" +import { Button } from "@/components/ui/button" +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog" +import { cn } from "@/lib/utils" + +interface ReceiptShareProps { + receiptId: string + marketTitle: string + outcome: string + amount: string + timestamp: string + campaign?: string + className?: string +} + +function formatTimestamp(timestamp: string) { + const date = new Date(timestamp) + + if (Number.isNaN(date.getTime())) { + return "Pending confirmation" + } + + return date.toLocaleString("en-US", { + month: "short", + day: "numeric", + year: "numeric", + hour: "numeric", + minute: "2-digit", + }) +} + +export function ReceiptShare({ + receiptId, + marketTitle, + outcome, + amount, + timestamp, + campaign = "GrantFox FWC26", + className, +}: ReceiptShareProps) { + const [open, setOpen] = useState(false) + const [copied, setCopied] = useState(false) + + const shareUrl = useMemo( + () => `https://predictify.app/receipts/${receiptId}`, + [receiptId], + ) + + const handleCopy = async () => { + if (typeof navigator === "undefined" || !navigator.clipboard?.writeText) { + return + } + + await navigator.clipboard.writeText(shareUrl) + setCopied(true) + window.setTimeout(() => setCopied(false), 1800) + } + + const handleDownload = () => { + const payload = [ + `Receipt: ${receiptId}`, + `Market: ${marketTitle}`, + `Outcome: ${outcome}`, + `Amount: ${amount}`, + `Confirmed: ${formatTimestamp(timestamp)}`, + `Campaign: ${campaign}`, + ].join("\n") + + const blob = new Blob([payload], { type: "text/plain;charset=utf-8" }) + const url = URL.createObjectURL(blob) + const link = document.createElement("a") + link.href = url + link.download = `receipt-${receiptId}.txt` + link.click() + URL.revokeObjectURL(url) + } + + return ( + + + + + + + Share your prediction receipt + + Share a polished receipt card for this completed prediction with your audience. + + + +
+
+
+
+
+ {campaign} +
+
+

+ Prediction confirmed +

+

{marketTitle}

+
+
+
+

Outcome

+

{outcome}

+
+
+

Amount

+

{amount}

+
+
+
+ +
+

Receipt ID

+

{receiptId}

+

{formatTimestamp(timestamp)}

+
+
+
+ +
+ + + +
+
+
+
+ ) +} diff --git a/app/components/__tests__/ReceiptShare.test.tsx b/app/components/__tests__/ReceiptShare.test.tsx new file mode 100644 index 00000000..1cc4d9e6 --- /dev/null +++ b/app/components/__tests__/ReceiptShare.test.tsx @@ -0,0 +1,44 @@ +import React from "react" +import { render, screen, fireEvent, waitFor } from "@testing-library/react" +import { ReceiptShare } from "../ReceiptShare" + +const defaultProps = { + receiptId: "RCP-2026-001", + marketTitle: "Will the GrantFox campaign hit 10k signups?", + outcome: "Yes", + amount: "12.50 XLM", + timestamp: "2026-07-23T17:30:00.000Z", + campaign: "GrantFox FWC26", +} + +describe("ReceiptShare", () => { + it("opens the share preview and renders the receipt details", async () => { + render() + + fireEvent.click(screen.getByRole("button", { name: /share receipt/i })) + + expect(await screen.findByRole("dialog")).toBeInTheDocument() + expect(screen.getByText(defaultProps.marketTitle)).toBeInTheDocument() + expect(screen.getByText(defaultProps.outcome)).toBeInTheDocument() + expect(screen.getByText(defaultProps.amount)).toBeInTheDocument() + expect(screen.getByText(defaultProps.receiptId)).toBeInTheDocument() + }) + + it("copies the share link when the copy action is selected", async () => { + const writeTextMock = jest.fn().mockResolvedValue(undefined) + Object.defineProperty(navigator, "clipboard", { + value: { writeText: writeTextMock }, + configurable: true, + writable: true, + }) + + render() + fireEvent.click(screen.getByRole("button", { name: /share receipt/i })) + + fireEvent.click(await screen.findByRole("button", { name: /copy link/i })) + + await waitFor(() => { + expect(writeTextMock).toHaveBeenCalledWith(expect.stringContaining("predictify.app/receipts/")) + }) + }) +}) diff --git a/app/design/share-cards/page.tsx b/app/design/share-cards/page.tsx index 8a8e93bd..57846cd9 100644 --- a/app/design/share-cards/page.tsx +++ b/app/design/share-cards/page.tsx @@ -1,13 +1,28 @@ import MarketShareCard from "@/components/market/MarketShareCard"; +import { ReceiptShare } from "@/app/components/ReceiptShare"; export default function ShareCardsPreview() { return (
-

Market Share Cards

-

Social preview layouts (1200x630)

+

Share Cards

+

Social preview layouts and shareable prediction receipts (1200x630)

+
+

Prediction Receipt Share Card

+
+ +
+
+

Active Market

diff --git a/components/receipts/Receipt.tsx b/components/receipts/Receipt.tsx index 1348f504..9374def8 100644 --- a/components/receipts/Receipt.tsx +++ b/components/receipts/Receipt.tsx @@ -2,6 +2,7 @@ import React from 'react'; import { CheckCircle2, Download, ArrowLeft } from 'lucide-react'; +import { ReceiptShare } from '@/app/components/ReceiptShare'; interface ReceiptProps { receiptId: string; @@ -129,6 +130,15 @@ export function Receipt({ receiptId, amount, partyA, partyB, timestamp, type }: Download Receipt +