├── .eslintrc.json ├── .gitignore ├── README.md ├── components.json ├── next.config.ts ├── package-lock.json ├── package.json ├── postcss.config.mjs ├── public ├── file.svg ├── globe.svg ├── next.svg ├── vercel.svg └── window.svg ├── src ├── app │ ├── api │ │ └── claimToken │ │ │ └── route.ts │ ├── client.ts │ ├── favicon.ico │ ├── fonts │ │ ├── GeistMonoVF.woff │ │ └── GeistVF.woff │ ├── globals.css │ ├── layout.tsx │ └── page.tsx ├── components │ ├── enhanced-prediction-market-dashboard.tsx │ ├── footer.tsx │ ├── market-buy-interface.tsx │ ├── market-card-skeleton.tsx │ ├── market-pending.tsx │ ├── market-progress.tsx │ ├── market-resolved.tsx │ ├── market-shares-display.tsx │ ├── market-time.tsx │ ├── marketCard.tsx │ ├── navbar.tsx │ └── ui │ │ ├── badge.tsx │ │ ├── button.tsx │ │ ├── card.tsx │ │ ├── input.tsx │ │ ├── progress.tsx │ │ ├── tabs.tsx │ │ ├── toast.tsx │ │ ├── toaster.tsx │ │ └── use-toast.ts ├── constants │ └── contract.ts ├── hooks │ └── use-toast.ts ├── lib │ └── utils.ts └── types │ └── types.ts ├── tailwind.config.ts └── tsconfig.json /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["next/core-web-vitals", "next/typescript"] 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.* 7 | .yarn/* 8 | !.yarn/patches 9 | !.yarn/plugins 10 | !.yarn/releases 11 | !.yarn/versions 12 | 13 | # testing 14 | /coverage 15 | 16 | # next.js 17 | /.next/ 18 | /out/ 19 | 20 | # production 21 | /build 22 | 23 | # misc 24 | .DS_Store 25 | *.pem 26 | 27 | # debug 28 | npm-debug.log* 29 | yarn-debug.log* 30 | yarn-error.log* 31 | 32 | # env files (can opt-in for commiting if needed) 33 | .env* 34 | 35 | # vercel 36 | .vercel 37 | 38 | # typescript 39 | *.tsbuildinfo 40 | next-env.d.ts 41 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app). 2 | 3 | ## Getting Started 4 | 5 | First, run the development server: 6 | 7 | ```bash 8 | npm run dev 9 | # or 10 | yarn dev 11 | # or 12 | pnpm dev 13 | # or 14 | bun dev 15 | ``` 16 | 17 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 18 | 19 | You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. 20 | 21 | This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel. 22 | 23 | ## Learn More 24 | 25 | To learn more about Next.js, take a look at the following resources: 26 | 27 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 28 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 29 | 30 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome! 31 | 32 | ## Deploy on Vercel 33 | 34 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. 35 | 36 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details. 37 | -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "default", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.ts", 8 | "css": "src/app/globals.css", 9 | "baseColor": "neutral", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils", 16 | "ui": "@/components/ui", 17 | "lib": "@/lib", 18 | "hooks": "@/hooks" 19 | } 20 | } -------------------------------------------------------------------------------- /next.config.ts: -------------------------------------------------------------------------------- 1 | import type { NextConfig } from "next"; 2 | 3 | const nextConfig: NextConfig = { 4 | /* config options here */ 5 | }; 6 | 7 | export default nextConfig; 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "simplepredictionmarket", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@radix-ui/react-progress": "^1.1.0", 13 | "@radix-ui/react-slot": "^1.1.0", 14 | "@radix-ui/react-tabs": "^1.1.1", 15 | "@radix-ui/react-toast": "^1.2.2", 16 | "class-variance-authority": "^0.7.0", 17 | "clsx": "^2.1.1", 18 | "lucide-react": "^0.453.0", 19 | "next": "15.0.1", 20 | "react": "19.0.0-rc-69d4b800-20241021", 21 | "react-dom": "19.0.0-rc-69d4b800-20241021", 22 | "tailwind-merge": "^2.5.4", 23 | "tailwindcss-animate": "^1.0.7", 24 | "thirdweb": "^5.64.2" 25 | }, 26 | "devDependencies": { 27 | "@types/node": "^20", 28 | "@types/react": "^18", 29 | "@types/react-dom": "^18", 30 | "eslint": "^8", 31 | "eslint-config-next": "15.0.1", 32 | "postcss": "^8", 33 | "tailwindcss": "^3.4.1", 34 | "typescript": "^5" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /postcss.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('postcss-load-config').Config} */ 2 | const config = { 3 | plugins: { 4 | tailwindcss: {}, 5 | }, 6 | }; 7 | 8 | export default config; 9 | -------------------------------------------------------------------------------- /public/file.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/globe.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/window.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/api/claimToken/route.ts: -------------------------------------------------------------------------------- 1 | import { tokenAddress } from "@/constants/contract"; 2 | import { NextResponse } from "next/server"; 3 | 4 | const { 5 | BACKEND_WALLET_ADDRESS, 6 | ENGINE_URL, 7 | THIRDWEB_SECRET_KEY, 8 | } = process.env; 9 | 10 | async function checkTransactionStatus(queueId: string): Promise { 11 | const statusResponse = await fetch(`${ENGINE_URL}/transaction/status/${queueId}`, { 12 | headers: { 13 | Authorization: `Bearer ${THIRDWEB_SECRET_KEY}`, 14 | }, 15 | }); 16 | 17 | if (statusResponse.ok) { 18 | const statusData = await statusResponse.json(); 19 | return statusData.result.status === 'mined'; 20 | } 21 | return false; 22 | } 23 | 24 | async function pollTransactionStatus(queueId: string, maxAttempts = 15, interval = 3000): Promise { 25 | for (let attempt = 0; attempt < maxAttempts; attempt++) { 26 | const isMined = await checkTransactionStatus(queueId); 27 | if (isMined) return true; 28 | await new Promise(resolve => setTimeout(resolve, interval)); 29 | } 30 | return false; 31 | } 32 | 33 | export async function POST(request: Request) { 34 | if ( 35 | !BACKEND_WALLET_ADDRESS || 36 | !ENGINE_URL || 37 | !THIRDWEB_SECRET_KEY 38 | ) { 39 | throw 'Server misconfigured. Did you forget to add a ".env.local" file?'; 40 | } 41 | 42 | const { address } = await request.json(); 43 | 44 | const resp = await fetch( 45 | `${ENGINE_URL}/contract/84532/${tokenAddress}/erc20/mint-to`, 46 | { 47 | method: "POST", 48 | headers: { 49 | "Content-Type": "application/json", 50 | Authorization: `Bearer ${THIRDWEB_SECRET_KEY}`, 51 | "x-backend-wallet-address": BACKEND_WALLET_ADDRESS, 52 | }, 53 | body: JSON.stringify({ 54 | "toAddress": address as string, 55 | "amount": "100" 56 | }), 57 | } 58 | ); 59 | 60 | if (resp.ok) { 61 | const data = await resp.json(); 62 | const queueId = data.result.queueId; 63 | 64 | const isMined = await pollTransactionStatus(queueId); 65 | 66 | if (isMined) { 67 | return NextResponse.json({ message: "Transaction mined successfully!", queueId }); 68 | } else { 69 | return NextResponse.json({ message: "Transaction not mined within the timeout period.", queueId }, { status: 408 }); 70 | } 71 | } else { 72 | const errorText = await resp.text(); 73 | console.error("[DEBUG] not ok", errorText); 74 | return NextResponse.json({ message: "Failed to initiate transaction", error: errorText }, { status: 500 }); 75 | } 76 | } -------------------------------------------------------------------------------- /src/app/client.ts: -------------------------------------------------------------------------------- 1 | // src/client.ts 2 | import { createThirdwebClient } from "thirdweb"; 3 | 4 | export const client = createThirdwebClient({ 5 | clientId: process.env.NEXT_PUBLIC_THIRDWEB_CLIENT_ID as string, 6 | }); 7 | -------------------------------------------------------------------------------- /src/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thirdweb-example/simple-prediction-market-youtube/2f413939fe720480023f2b9a18fc4765fc1d6edb/src/app/favicon.ico -------------------------------------------------------------------------------- /src/app/fonts/GeistMonoVF.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thirdweb-example/simple-prediction-market-youtube/2f413939fe720480023f2b9a18fc4765fc1d6edb/src/app/fonts/GeistMonoVF.woff -------------------------------------------------------------------------------- /src/app/fonts/GeistVF.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thirdweb-example/simple-prediction-market-youtube/2f413939fe720480023f2b9a18fc4765fc1d6edb/src/app/fonts/GeistVF.woff -------------------------------------------------------------------------------- /src/app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | body { 6 | font-family: Arial, Helvetica, sans-serif; 7 | } 8 | 9 | @layer base { 10 | :root { 11 | --background: 0 0% 100%; 12 | --foreground: 0 0% 3.9%; 13 | --card: 0 0% 100%; 14 | --card-foreground: 0 0% 3.9%; 15 | --popover: 0 0% 100%; 16 | --popover-foreground: 0 0% 3.9%; 17 | --primary: 0 0% 9%; 18 | --primary-foreground: 0 0% 98%; 19 | --secondary: 0 0% 96.1%; 20 | --secondary-foreground: 0 0% 9%; 21 | --muted: 0 0% 96.1%; 22 | --muted-foreground: 0 0% 45.1%; 23 | --accent: 0 0% 96.1%; 24 | --accent-foreground: 0 0% 9%; 25 | --destructive: 0 84.2% 60.2%; 26 | --destructive-foreground: 0 0% 98%; 27 | --border: 0 0% 89.8%; 28 | --input: 0 0% 89.8%; 29 | --ring: 0 0% 3.9%; 30 | --chart-1: 12 76% 61%; 31 | --chart-2: 173 58% 39%; 32 | --chart-3: 197 37% 24%; 33 | --chart-4: 43 74% 66%; 34 | --chart-5: 27 87% 67%; 35 | --radius: 0.5rem; 36 | } 37 | .dark { 38 | --background: 0 0% 3.9%; 39 | --foreground: 0 0% 98%; 40 | --card: 0 0% 3.9%; 41 | --card-foreground: 0 0% 98%; 42 | --popover: 0 0% 3.9%; 43 | --popover-foreground: 0 0% 98%; 44 | --primary: 0 0% 98%; 45 | --primary-foreground: 0 0% 9%; 46 | --secondary: 0 0% 14.9%; 47 | --secondary-foreground: 0 0% 98%; 48 | --muted: 0 0% 14.9%; 49 | --muted-foreground: 0 0% 63.9%; 50 | --accent: 0 0% 14.9%; 51 | --accent-foreground: 0 0% 98%; 52 | --destructive: 0 62.8% 30.6%; 53 | --destructive-foreground: 0 0% 98%; 54 | --border: 0 0% 14.9%; 55 | --input: 0 0% 14.9%; 56 | --ring: 0 0% 83.1%; 57 | --chart-1: 220 70% 50%; 58 | --chart-2: 160 60% 45%; 59 | --chart-3: 30 80% 55%; 60 | --chart-4: 280 65% 60%; 61 | --chart-5: 340 75% 55%; 62 | } 63 | } 64 | 65 | @layer base { 66 | * { 67 | @apply border-border; 68 | } 69 | body { 70 | @apply bg-background text-foreground; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next"; 2 | import localFont from "next/font/local"; 3 | import "./globals.css"; 4 | import { ThirdwebProvider } from "thirdweb/react"; 5 | import { Toaster } from "@/components/ui/toaster"; 6 | 7 | const geistSans = localFont({ 8 | src: "./fonts/GeistVF.woff", 9 | variable: "--font-geist-sans", 10 | weight: "100 900", 11 | }); 12 | const geistMono = localFont({ 13 | src: "./fonts/GeistMonoVF.woff", 14 | variable: "--font-geist-mono", 15 | weight: "100 900", 16 | }); 17 | 18 | export const metadata: Metadata = { 19 | title: "Create Next App", 20 | description: "Generated by create next app", 21 | }; 22 | 23 | export default function RootLayout({ 24 | children, 25 | }: Readonly<{ 26 | children: React.ReactNode; 27 | }>) { 28 | return ( 29 | 30 | 33 | 34 | {children} 35 | 36 | 37 | 38 | 39 | ); 40 | } 41 | -------------------------------------------------------------------------------- /src/app/page.tsx: -------------------------------------------------------------------------------- 1 | import { EnhancedPredictionMarketDashboard } from "@/components/enhanced-prediction-market-dashboard"; 2 | 3 | export default function Home() { 4 | return ( 5 | 6 | ); 7 | } 8 | -------------------------------------------------------------------------------- /src/components/enhanced-prediction-market-dashboard.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { useReadContract } from 'thirdweb/react' 4 | import { contract } from '@/constants/contract' 5 | import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" 6 | import { MarketCard } from './marketCard' 7 | import { Navbar } from './navbar' 8 | import { MarketCardSkeleton } from './market-card-skeleton' 9 | import { Footer } from "./footer" 10 | 11 | export function EnhancedPredictionMarketDashboard() { 12 | const { data: marketCount, isLoading: isLoadingMarketCount } = useReadContract({ 13 | contract, 14 | method: "function marketCount() view returns (uint256)", 15 | params: [] 16 | }); 17 | 18 | // Show 6 skeleton cards while loading 19 | const skeletonCards = Array.from({ length: 6 }, (_, i) => ( 20 | 21 | )); 22 | 23 | return ( 24 |
25 |
26 | 27 |
28 | Placeholder Banner 33 |
34 | 35 | 36 | Active 37 | Pending Resolution 38 | Resolved 39 | 40 | 41 | {isLoadingMarketCount ? ( 42 | 43 |
44 | {skeletonCards} 45 |
46 |
47 | ) : ( 48 | <> 49 | 50 |
51 | {Array.from({ length: Number(marketCount) }, (_, index) => ( 52 | 57 | ))} 58 |
59 |
60 | 61 | 62 |
63 | {Array.from({ length: Number(marketCount) }, (_, index) => ( 64 | 69 | ))} 70 |
71 |
72 | 73 | 74 |
75 | {Array.from({ length: Number(marketCount) }, (_, index) => ( 76 | 81 | ))} 82 |
83 |
84 | 85 | )} 86 |
87 |
88 |
89 |
90 | ); 91 | } 92 | -------------------------------------------------------------------------------- /src/components/footer.tsx: -------------------------------------------------------------------------------- 1 | import { Github } from "lucide-react" 2 | import Link from "next/link" 3 | 4 | export function Footer() { 5 | return ( 6 |
7 |
8 |
9 |

10 | Built by{" "} 11 | 17 | your-name 18 | 19 | . The source code is available on{" "} 20 | 26 | GitHub 27 | 28 | . 29 |

30 |
31 |
32 | 37 | 38 | 39 |
40 |
41 |
42 | ) 43 | } 44 | -------------------------------------------------------------------------------- /src/components/market-buy-interface.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from "./ui/button"; 2 | import { Input } from "./ui/input"; 3 | import { useState, useRef, useEffect } from "react"; 4 | import { useActiveAccount, useSendAndConfirmTransaction } from "thirdweb/react"; 5 | import { prepareContractCall, readContract, toWei } from "thirdweb"; 6 | import { contract, tokenContract } from "@/constants/contract"; 7 | import { approve } from "thirdweb/extensions/erc20"; 8 | import { Loader2 } from "lucide-react"; 9 | import { cn } from "@/lib/utils"; 10 | import { useToast } from "@/components/ui/use-toast" 11 | 12 | // Types for the component props 13 | interface MarketBuyInterfaceProps { 14 | marketId: number; 15 | market: { 16 | optionA: string; 17 | optionB: string; 18 | question: string; 19 | }; 20 | } 21 | 22 | // Type aliases for better readability 23 | type BuyingStep = 'initial' | 'allowance' | 'confirm'; 24 | type Option = 'A' | 'B' | null; 25 | 26 | export function MarketBuyInterface({ marketId, market }: MarketBuyInterfaceProps) { 27 | // Blockchain interactions 28 | const account = useActiveAccount(); 29 | const { mutateAsync: mutateTransaction } = useSendAndConfirmTransaction(); 30 | const { toast } = useToast() 31 | 32 | // UI state management 33 | const [isBuying, setIsBuying] = useState(false); 34 | const [isVisible, setIsVisible] = useState(true); 35 | const [containerHeight, setContainerHeight] = useState('auto'); 36 | const contentRef = useRef(null); 37 | 38 | // Transaction state 39 | const [selectedOption, setSelectedOption] = useState