Skip to content

Commit eae642e

Browse files
feat: implement markets frontend
1 parent 143e991 commit eae642e

16 files changed

Lines changed: 711 additions & 48 deletions

.env.example

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
NEXT_PUBLIC_STELLAR_NETWORK=testnet
2-
NEXT_PUBLIC_STELLAR_RPC_URL=https://soroban-testnet.stellar.org
3-
NEXT_PUBLIC_PREDICTX_CONTRACT_ID=
1+
# Stellar Network Configuration
2+
# Testnet Soroban RPC endpoint (automatically configured in code)
3+
# NEXT_PUBLIC_SOROBAN_RPC_URL=https://soroban-testnet.stellar.org
44

5+
# Contract ID on Testnet
6+
NEXT_PUBLIC_CONTRACT_ID=CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABSC4

frontend/app/layout.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { Metadata } from "next";
2+
import { WalletProvider } from "@/lib/wallet/wallet-context";
23
import "./globals.css";
34

45
export const metadata: Metadata = {
@@ -13,7 +14,9 @@ export default function RootLayout({
1314
}>) {
1415
return (
1516
<html lang="en">
16-
<body>{children}</body>
17+
<body>
18+
<WalletProvider>{children}</WalletProvider>
19+
</body>
1720
</html>
1821
);
1922
}

frontend/app/markets/page.tsx

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { MarketsGrid } from "@/components/markets-grid";
2+
3+
export default function MarketsPage() {
4+
return (
5+
<main className="min-h-screen bg-slate-50">
6+
<section className="mx-auto w-full max-w-6xl px-6 py-10">
7+
<div className="mb-8 flex flex-col gap-2">
8+
<h1 className="text-3xl font-semibold text-ink">Markets</h1>
9+
<p className="text-slate-600">
10+
Explore available prediction markets and place your bets
11+
</p>
12+
</div>
13+
14+
<MarketsGrid />
15+
</section>
16+
</main>
17+
);
18+
}

frontend/app/page.tsx

Lines changed: 22 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,30 @@
11
import { WalletPanel } from "@/components/wallet-panel";
2+
import { Header } from "@/components/header";
23

34
export default function Home() {
45
return (
5-
<main className="min-h-screen">
6-
<section className="mx-auto flex w-full max-w-6xl flex-col gap-8 px-6 py-10">
7-
<div className="flex flex-col gap-3">
8-
<p className="text-sm font-semibold uppercase tracking-wide text-stellar">
9-
Stellar Soroban Scaffold
10-
</p>
11-
<h1 className="max-w-3xl text-4xl font-semibold text-ink">
12-
PredictX
13-
</h1>
14-
<p className="max-w-2xl text-base leading-7 text-slate-600">
15-
Open-source prediction marketplace foundation with contract,
16-
wallet, deployment, and documentation placeholders ready for MVP
17-
implementation.
18-
</p>
19-
</div>
6+
<>
7+
<Header />
8+
<main className="min-h-screen bg-slate-50">
9+
<section className="mx-auto flex w-full max-w-6xl flex-col gap-8 px-6 py-10">
10+
<div className="flex flex-col gap-3">
11+
<p className="text-sm font-semibold uppercase tracking-wide text-stellar">
12+
Stellar Soroban Scaffold
13+
</p>
14+
<h1 className="max-w-3xl text-4xl font-semibold text-ink">
15+
PredictX
16+
</h1>
17+
<p className="max-w-2xl text-base leading-7 text-slate-600">
18+
Open-source prediction marketplace foundation with contract,
19+
wallet, deployment, and documentation placeholders ready for MVP
20+
implementation.
21+
</p>
22+
</div>
2023

21-
<WalletPanel />
22-
</section>
23-
</main>
24+
<WalletPanel />
25+
</section>
26+
</main>
27+
</>
2428
);
2529
}
2630

frontend/src/components/header.tsx

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
"use client";
2+
3+
import Link from "next/link";
4+
import { usePathname } from "next/navigation";
5+
import { useWallet } from "@/lib/wallet/use-wallet";
6+
import clsx from "clsx";
7+
8+
export function Header() {
9+
const { isConnected, publicKey } = useWallet();
10+
const pathname = usePathname();
11+
12+
const isActive = (path: string) => pathname === path;
13+
14+
return (
15+
<header className="border-b border-slate-200 bg-white">
16+
<div className="mx-auto flex w-full max-w-6xl items-center justify-between px-6 py-4">
17+
<div className="flex items-center gap-8">
18+
<Link href="/" className="flex items-center gap-2 hover:opacity-80">
19+
<h1 className="text-xl font-bold text-ink">PredictX</h1>
20+
<span className="text-sm text-slate-500">Testnet</span>
21+
</Link>
22+
23+
<nav className="flex gap-6">
24+
<Link
25+
href="/"
26+
className={clsx(
27+
"text-sm font-medium transition-colors",
28+
isActive("/")
29+
? "text-ink border-b-2 border-ink"
30+
: "text-slate-600 hover:text-ink"
31+
)}
32+
>
33+
Home
34+
</Link>
35+
<Link
36+
href="/markets"
37+
className={clsx(
38+
"text-sm font-medium transition-colors",
39+
isActive("/markets")
40+
? "text-ink border-b-2 border-ink"
41+
: "text-slate-600 hover:text-ink"
42+
)}
43+
>
44+
Markets
45+
</Link>
46+
</nav>
47+
</div>
48+
49+
{isConnected && publicKey && (
50+
<div className="flex items-center gap-2 rounded-md bg-slate-50 px-3 py-2">
51+
<div className="h-2 w-2 rounded-full bg-green-500" />
52+
<span className="text-xs font-medium text-slate-600">
53+
{`${publicKey.slice(0, 8)}...${publicKey.slice(-8)}`}
54+
</span>
55+
</div>
56+
)}
57+
</div>
58+
</header>
59+
);
60+
}
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
"use client";
2+
3+
import { useEffect, useState } from "react";
4+
import clsx from "clsx";
5+
import { Market, MarketStatus } from "@/lib/contract/types";
6+
import {
7+
getMarketStatus,
8+
getTimeUntilResolution,
9+
} from "@/lib/contract/market-service";
10+
11+
interface MarketCardProps {
12+
market: Market;
13+
}
14+
15+
export function MarketCard({ market }: MarketCardProps) {
16+
const [status, setStatus] = useState<MarketStatus | null>(null);
17+
const [timeRemaining, setTimeRemaining] = useState<string>("");
18+
19+
useEffect(() => {
20+
const updateStatus = () => {
21+
const now = Math.floor(Date.now() / 1000);
22+
const marketStatus = getMarketStatus(market, now);
23+
setStatus(marketStatus);
24+
25+
const timeData = getTimeUntilResolution(market.end_time, now);
26+
if (timeData.isExpired) {
27+
setTimeRemaining("Resolution ended");
28+
} else {
29+
const parts = [];
30+
if (timeData.days > 0) parts.push(`${timeData.days}d`);
31+
if (timeData.hours > 0) parts.push(`${timeData.hours}h`);
32+
if (timeData.minutes > 0) parts.push(`${timeData.minutes}m`);
33+
setTimeRemaining(parts.join(" ") || "< 1 minute");
34+
}
35+
};
36+
37+
updateStatus();
38+
const interval = setInterval(updateStatus, 60000); // Update every minute
39+
return () => clearInterval(interval);
40+
}, [market]);
41+
42+
const getStatusColor = (s: MarketStatus | null) => {
43+
switch (s) {
44+
case MarketStatus.Active:
45+
return "bg-green-50 text-green-700 border-green-200";
46+
case MarketStatus.Ended:
47+
return "bg-yellow-50 text-yellow-700 border-yellow-200";
48+
case MarketStatus.Resolved:
49+
return "bg-blue-50 text-blue-700 border-blue-200";
50+
default:
51+
return "bg-slate-50 text-slate-700 border-slate-200";
52+
}
53+
};
54+
55+
const getStatusDot = (s: MarketStatus | null) => {
56+
switch (s) {
57+
case MarketStatus.Active:
58+
return "bg-green-500";
59+
case MarketStatus.Ended:
60+
return "bg-yellow-500";
61+
case MarketStatus.Resolved:
62+
return "bg-blue-500";
63+
default:
64+
return "bg-slate-500";
65+
}
66+
};
67+
68+
return (
69+
<div className="rounded-lg border border-slate-200 bg-white p-6 shadow-sm hover:shadow-md transition-shadow">
70+
<div className="mb-4 flex items-start justify-between">
71+
<div className="flex-1">
72+
<h3 className="text-lg font-semibold text-ink line-clamp-2">
73+
{market.title}
74+
</h3>
75+
<p className="mt-2 text-sm text-slate-600 line-clamp-2">
76+
{market.description}
77+
</p>
78+
</div>
79+
{status && (
80+
<div
81+
className={clsx(
82+
"ml-4 flex items-center gap-2 rounded-full border px-3 py-1 text-xs font-medium whitespace-nowrap",
83+
getStatusColor(status)
84+
)}
85+
>
86+
<div className={clsx("h-2 w-2 rounded-full", getStatusDot(status))} />
87+
{status}
88+
</div>
89+
)}
90+
</div>
91+
92+
<div className="mb-4 space-y-2">
93+
<div className="text-sm text-slate-600">
94+
<span className="font-medium text-ink">Outcomes:</span>
95+
<div className="mt-1 flex flex-wrap gap-2">
96+
{market.outcomes.map((outcome, idx) => (
97+
<span
98+
key={idx}
99+
className="inline-block rounded-md bg-slate-100 px-2 py-1 text-xs text-slate-700"
100+
>
101+
{outcome}
102+
</span>
103+
))}
104+
</div>
105+
</div>
106+
</div>
107+
108+
<div className="flex items-center justify-between border-t border-slate-200 pt-4">
109+
<div>
110+
<p className="text-xs text-slate-500">Time to resolution</p>
111+
<p className="mt-1 font-mono text-sm font-medium text-ink">
112+
{timeRemaining}
113+
</p>
114+
</div>
115+
<button className="rounded-md bg-ink px-4 py-2 text-sm font-medium text-white hover:bg-slate-900 transition-colors">
116+
Place Bet
117+
</button>
118+
</div>
119+
</div>
120+
);
121+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
"use client";
2+
3+
import { useMarkets } from "@/lib/hooks/use-markets";
4+
import { MarketCard } from "./market-card";
5+
6+
export function MarketsGrid() {
7+
const { markets, isLoading, error, refetch } = useMarkets();
8+
9+
if (isLoading) {
10+
return (
11+
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
12+
{Array.from({ length: 6 }).map((_, idx) => (
13+
<div
14+
key={idx}
15+
className="h-64 animate-pulse rounded-lg border border-slate-200 bg-slate-100"
16+
/>
17+
))}
18+
</div>
19+
);
20+
}
21+
22+
if (error) {
23+
return (
24+
<div className="rounded-lg border border-red-200 bg-red-50 p-6">
25+
<h3 className="font-semibold text-red-900">Failed to load markets</h3>
26+
<p className="mt-2 text-sm text-red-700">{error}</p>
27+
<button
28+
onClick={refetch}
29+
className="mt-4 rounded-md bg-red-600 px-4 py-2 text-sm font-medium text-white hover:bg-red-700 transition-colors"
30+
>
31+
Try Again
32+
</button>
33+
</div>
34+
);
35+
}
36+
37+
if (markets.length === 0) {
38+
return (
39+
<div className="rounded-lg border border-slate-200 bg-slate-50 p-12 text-center">
40+
<p className="text-slate-600">No markets available yet</p>
41+
<p className="mt-2 text-sm text-slate-500">
42+
Check back later or create a new market
43+
</p>
44+
</div>
45+
);
46+
}
47+
48+
return (
49+
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
50+
{markets.map((market) => (
51+
<MarketCard key={market.id} market={market} />
52+
))}
53+
</div>
54+
);
55+
}

0 commit comments

Comments
 (0)