From 021d38fe9b3d113efc91e5a0dd169895b1c2fe55 Mon Sep 17 00:00:00 2001 From: kingsley Date: Thu, 23 Jul 2026 18:53:40 +0100 Subject: [PATCH 1/2] feat: receipt share card --- README.md | 1 + app/components/ReceiptShare.tsx | 157 ++++++++++++++++++ .../__tests__/ReceiptShare.test.tsx | 44 +++++ app/design/share-cards/page.tsx | 19 ++- components/receipts/Receipt.tsx | 10 ++ 5 files changed, 229 insertions(+), 2 deletions(-) create mode 100644 app/components/ReceiptShare.tsx create mode 100644 app/components/__tests__/ReceiptShare.test.tsx 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/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 +