Skip to content

Commit 70c29db

Browse files
feat: implement market details and betting UI
1 parent edb0543 commit 70c29db

7 files changed

Lines changed: 639 additions & 3 deletions

File tree

frontend/app/markets/[id]/page.tsx

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
"use client";
2+
3+
import { useParams } from "next/navigation";
4+
import Link from "next/link";
5+
import { useMarket } from "@/lib/hooks/use-market";
6+
import { MarketDetails } from "@/components/market-details";
7+
8+
export default function MarketPage() {
9+
const params = useParams();
10+
const marketId = parseInt(params.id as string);
11+
12+
const { market, pool, isLoading, error, refetch } = useMarket(marketId);
13+
14+
if (isLoading) {
15+
return (
16+
<main className="min-h-screen bg-slate-50">
17+
<section className="mx-auto w-full max-w-4xl px-6 py-10">
18+
<div className="mb-8">
19+
<Link
20+
href="/markets"
21+
className="text-sm font-medium text-ink hover:underline"
22+
>
23+
← Back to Markets
24+
</Link>
25+
</div>
26+
27+
<div className="space-y-6">
28+
<div className="h-12 w-3/4 animate-pulse rounded-lg bg-slate-200" />
29+
<div className="h-32 w-full animate-pulse rounded-lg bg-slate-200" />
30+
<div className="grid gap-4 md:grid-cols-4">
31+
{Array.from({ length: 4 }).map((_, idx) => (
32+
<div
33+
key={idx}
34+
className="h-24 animate-pulse rounded-lg bg-slate-200"
35+
/>
36+
))}
37+
</div>
38+
</div>
39+
</section>
40+
</main>
41+
);
42+
}
43+
44+
if (error || !market) {
45+
return (
46+
<main className="min-h-screen bg-slate-50">
47+
<section className="mx-auto w-full max-w-4xl px-6 py-10">
48+
<div className="mb-8">
49+
<Link
50+
href="/markets"
51+
className="text-sm font-medium text-ink hover:underline"
52+
>
53+
← Back to Markets
54+
</Link>
55+
</div>
56+
57+
<div className="rounded-lg border border-red-200 bg-red-50 p-6">
58+
<h2 className="font-semibold text-red-900">Market Not Found</h2>
59+
<p className="mt-2 text-sm text-red-700">
60+
{error || "Could not load market details"}
61+
</p>
62+
<Link
63+
href="/markets"
64+
className="mt-4 inline-block rounded-md bg-red-600 px-4 py-2 text-sm font-medium text-white hover:bg-red-700"
65+
>
66+
Return to Markets
67+
</Link>
68+
</div>
69+
</section>
70+
</main>
71+
);
72+
}
73+
74+
return (
75+
<main className="min-h-screen bg-slate-50">
76+
<section className="mx-auto w-full max-w-4xl px-6 py-10">
77+
<div className="mb-8">
78+
<Link
79+
href="/markets"
80+
className="text-sm font-medium text-ink hover:underline"
81+
>
82+
← Back to Markets
83+
</Link>
84+
</div>
85+
86+
<MarketDetails market={market} pool={pool} onRefetch={refetch} />
87+
</section>
88+
</main>
89+
);
90+
}

frontend/src/components/market-card.tsx

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
"use client";
22

3+
import Link from "next/link";
34
import { useEffect, useState } from "react";
45
import clsx from "clsx";
56
import { Market, MarketStatus } from "@/lib/contract/types";
@@ -112,9 +113,12 @@ export function MarketCard({ market }: MarketCardProps) {
112113
{timeRemaining}
113114
</p>
114115
</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>
116+
<Link
117+
href={`/markets/${market.id}`}
118+
className="rounded-md bg-ink px-4 py-2 text-sm font-medium text-white hover:bg-slate-900 transition-colors"
119+
>
120+
View Details
121+
</Link>
118122
</div>
119123
</div>
120124
);
Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
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+
import { PlaceBetForm } from "./place-bet-form";
11+
12+
interface MarketDetailsProps {
13+
market: Market;
14+
pool: number;
15+
onRefetch: () => void;
16+
}
17+
18+
export function MarketDetails({
19+
market,
20+
pool,
21+
onRefetch,
22+
}: MarketDetailsProps) {
23+
const [status, setStatus] = useState<MarketStatus | null>(null);
24+
const [timeRemaining, setTimeRemaining] = useState<string>("");
25+
26+
useEffect(() => {
27+
const updateStatus = () => {
28+
const now = Math.floor(Date.now() / 1000);
29+
const marketStatus = getMarketStatus(market, now);
30+
setStatus(marketStatus);
31+
32+
const timeData = getTimeUntilResolution(market.end_time, now);
33+
if (timeData.isExpired) {
34+
setTimeRemaining("Resolution ended");
35+
} else {
36+
const parts = [];
37+
if (timeData.days > 0) parts.push(`${timeData.days}d`);
38+
if (timeData.hours > 0) parts.push(`${timeData.hours}h`);
39+
if (timeData.minutes > 0) parts.push(`${timeData.minutes}m`);
40+
setTimeRemaining(parts.join(" ") || "< 1 minute");
41+
}
42+
};
43+
44+
updateStatus();
45+
const interval = setInterval(updateStatus, 60000);
46+
return () => clearInterval(interval);
47+
}, [market]);
48+
49+
const getStatusColor = (s: MarketStatus | null) => {
50+
switch (s) {
51+
case MarketStatus.Active:
52+
return "bg-green-50 text-green-700 border-green-200";
53+
case MarketStatus.Ended:
54+
return "bg-yellow-50 text-yellow-700 border-yellow-200";
55+
case MarketStatus.Resolved:
56+
return "bg-blue-50 text-blue-700 border-blue-200";
57+
default:
58+
return "bg-slate-50 text-slate-700 border-slate-200";
59+
}
60+
};
61+
62+
const getStatusDot = (s: MarketStatus | null) => {
63+
switch (s) {
64+
case MarketStatus.Active:
65+
return "bg-green-500";
66+
case MarketStatus.Ended:
67+
return "bg-yellow-500";
68+
case MarketStatus.Resolved:
69+
return "bg-blue-500";
70+
default:
71+
return "bg-slate-500";
72+
}
73+
};
74+
75+
const formatDate = (timestamp: number) => {
76+
return new Date(timestamp * 1000).toLocaleString();
77+
};
78+
79+
const formatPool = (amount: number) => {
80+
return (amount / 10000000).toFixed(7);
81+
};
82+
83+
return (
84+
<div className="space-y-6">
85+
<div>
86+
<div className="mb-4 flex items-start justify-between gap-4">
87+
<div className="flex-1">
88+
<h1 className="text-3xl font-semibold text-ink">{market.title}</h1>
89+
<p className="mt-2 text-slate-600">{market.description}</p>
90+
</div>
91+
{status && (
92+
<div
93+
className={clsx(
94+
"flex items-center gap-2 rounded-full border px-3 py-1 text-sm font-medium whitespace-nowrap",
95+
getStatusColor(status)
96+
)}
97+
>
98+
<div
99+
className={clsx(
100+
"h-2 w-2 rounded-full",
101+
getStatusDot(status)
102+
)}
103+
/>
104+
{status}
105+
</div>
106+
)}
107+
</div>
108+
</div>
109+
110+
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
111+
<div className="rounded-lg border border-slate-200 bg-slate-50 p-4">
112+
<p className="text-xs font-medium uppercase text-slate-600">
113+
Total Pool
114+
</p>
115+
<p className="mt-2 font-mono text-lg font-semibold text-ink">
116+
{formatPool(pool)} XLM
117+
</p>
118+
</div>
119+
120+
<div className="rounded-lg border border-slate-200 bg-slate-50 p-4">
121+
<p className="text-xs font-medium uppercase text-slate-600">
122+
Time Remaining
123+
</p>
124+
<p className="mt-2 font-mono text-lg font-semibold text-ink">
125+
{timeRemaining}
126+
</p>
127+
</div>
128+
129+
<div className="rounded-lg border border-slate-200 bg-slate-50 p-4">
130+
<p className="text-xs font-medium uppercase text-slate-600">
131+
End Time
132+
</p>
133+
<p className="mt-2 text-sm text-ink break-words">
134+
{formatDate(market.end_time)}
135+
</p>
136+
</div>
137+
138+
<div className="rounded-lg border border-slate-200 bg-slate-50 p-4">
139+
<p className="text-xs font-medium uppercase text-slate-600">
140+
Outcomes
141+
</p>
142+
<p className="mt-2 text-lg font-semibold text-ink">
143+
{market.outcomes.length}
144+
</p>
145+
</div>
146+
</div>
147+
148+
<div>
149+
<h2 className="mb-4 text-xl font-semibold text-ink">Outcomes</h2>
150+
<div className="grid gap-3 md:grid-cols-2">
151+
{market.outcomes.map((outcome, idx) => (
152+
<div
153+
key={idx}
154+
className="rounded-lg border border-slate-200 bg-white p-4"
155+
>
156+
<p className="text-sm font-medium text-slate-600">
157+
Outcome {idx + 1}
158+
</p>
159+
<p className="mt-1 text-lg font-semibold text-ink">{outcome}</p>
160+
</div>
161+
))}
162+
</div>
163+
</div>
164+
165+
<div className="border-t border-slate-200 pt-6">
166+
<h2 className="mb-4 text-xl font-semibold text-ink">Place Your Bet</h2>
167+
<div className="rounded-lg border border-slate-200 bg-white p-6">
168+
<PlaceBetForm market={market} onBetSuccess={onRefetch} />
169+
</div>
170+
</div>
171+
</div>
172+
);
173+
}

0 commit comments

Comments
 (0)