tangled
alpha
login
or
join now
ciaran.co.za
/
cumulus
0
fork
atom
A Prediction Market on the AT Protocol
0
fork
atom
overview
issues
pulls
pipelines
feat(web): more client mvp
Ciaran
1 week ago
7610a131
0f49807c
+24
-36
4 changed files
expand all
collapse all
unified
split
src
web
app.tsx
components
shared
avatar.tsx
lib
lmsr.ts
providers
auth-context-provider.tsx
+14
-21
src/web/app.tsx
···
1
1
import { useCumulus } from "./providers/useCumulus";
2
2
-
import { formatDistance, getUnixTime } from "date-fns"
2
2
+
import { formatDistance } from "date-fns"
3
3
import { Spinner } from "./components/ui/spinner";
4
4
-
import { LineChart, Line, XAxis, CartesianGrid } from "recharts";
5
5
-
import { ChartContainer, ChartTooltip } from "./components/ui/chart";
4
4
+
import { LineChart, Line, Tooltip } from "recharts";
5
5
+
import { ChartContainer } from "./components/ui/chart";
6
6
import { noPrice, yesPrice } from "./lib/lmsr";
7
7
8
8
export default function App() {
···
18
18
.map(bet => {
19
19
if (bet.position === "yes") yes++;
20
20
if (bet.position === "no") no++;
21
21
-
return {
22
22
-
...bet,
23
23
-
createdAt: formatDistance(bet.createdAt, new Date(), { addSuffix: true }),
24
24
-
timestamp: getUnixTime(bet.createdAt),
25
25
-
yes,
26
26
-
no,
27
27
-
yesPrice: yesPrice(yes, no, market.liquidity),
28
28
-
noPrice: noPrice(yes, no, market.liquidity),
29
29
-
}
21
21
+
return { ...bet, yes, no, }
30
22
})
31
31
-
32
32
-
return <div key={market.cid} className="space-y-2">
33
33
-
<h2 className="text-3xl font-medium">{market.question}</h2>
34
34
-
<p className="uppercase text-sm font-bold">Closes {formatDistance(new Date(market.closesAt), new Date(), { addSuffix: true })} | {market.bets?.length} Positions</p>
23
23
+
return <div key={market.cid} className="relative uppercase bg-radial-[at_80%_200%] from-coral-500 via-coral-50">
24
24
+
<div className="absolute inset-0 p-2">
25
25
+
<h2 className="text-xl font-bold flex gap-1 items-center">{market.question}</h2>
26
26
+
<p>Closes: {formatDistance(new Date(market.closesAt), new Date(), { addSuffix: true })}</p>
27
27
+
<p>Positions: {market.bets?.length}</p>
28
28
+
<p>Yes Price: {yesPrice(yes, no, market.liquidity)}</p>
29
29
+
<p>No Price: {noPrice(yes, no, market.liquidity)}</p>
30
30
+
</div>
35
31
<ChartContainer
36
36
-
config={{
37
37
-
yes: { label: "Yes" }, no: { label: "No" }
38
38
-
}}>
32
32
+
config={{ yes: { label: "Yes" }, no: { label: "No" } }}>
39
33
<LineChart data={mappedBets}>
40
40
-
<ChartTooltip />
34
34
+
<Tooltip />
41
35
<Line dataKey="yes" stroke="var(--color-shell-600)" />
42
36
<Line dataKey="no" stroke="var(--color-coral-600)" />
43
43
-
<Line dataKey="yesPrice" />
44
37
</LineChart>
45
38
</ChartContainer>
46
39
</div>
+3
-4
src/web/components/shared/avatar.tsx
···
1
1
-
import { Badge } from "../ui/badge";
2
1
import type { AppBskyActorDefs } from "@atcute/bluesky";
3
2
4
3
export default function Avatar({ profile }: { profile?: AppBskyActorDefs.ProfileViewDetailed }) {
5
4
if (!profile) return null;
6
6
-
return <Badge variant="link">
7
7
-
<img src={profile.avatar} className="h-4 rounded-full" />
5
5
+
return <div className="flex items-center gap-2 text-sm text-coral-500 tracking-tight">
6
6
+
<img src={profile.avatar} className="h-5 rounded-full border border-coral-500" />
8
7
<a href={`https://bsky.app/profile/${profile.handle}`} target="_blank">@{profile.handle}</a>
9
9
-
</Badge>
8
8
+
</div>
10
9
}
+2
-2
src/web/lib/lmsr.ts
···
1
1
export function yesPrice(yes: number, no: number, liquidity: number) {
2
2
-
return 1 / (1 + Math.exp((no - yes) / liquidity));
2
2
+
return (1 / (1 + Math.exp((no - yes) / liquidity))).toFixed(2)
3
3
}
4
4
5
5
export function noPrice(yes: number, no: number, liquidity: number) {
6
6
-
return 1 / (1 + Math.exp((yes - no) / liquidity));
6
6
+
return (1 / (1 + Math.exp((yes - no) / liquidity))).toFixed(2)
7
7
}
+5
-9
src/web/providers/auth-context-provider.tsx
···
53
53
const showLoginForm = !showLoader && !data && !isLoading;
54
54
const showAppContent = !showLoginForm && data && !isLoading;
55
55
56
56
-
return <main className="text-shell-900 flex flex-col min-h-screen bg-shell-50">
57
57
-
<header className="bg-shell-100 p-2 h-10 flex justify-between gap-2 items-center">
58
58
-
<h1 className="justify-self-start uppercase text-xs font-medium">Cumulus</h1>
59
59
-
<div className="flex items-center gap-2">
60
60
-
{showLoader ? <Spinner /> : <Avatar profile={data?.profile} />}
61
61
-
</div>
56
56
+
return <main className="subpixel-antialiased text-shell-900 bg-shell-50 flex flex-col min-h-screen">
57
57
+
<header className="bg-shell-900 text-shell-50 p-2 h-10 flex justify-between gap-2 items-center">
58
58
+
<h1 className="text-coral-500 justify-self-start uppercase text-xs font-extrabold">Cumulus</h1>
59
59
+
<div className="flex items-center gap-2">{showLoader ? <Spinner /> : <Avatar profile={data?.profile} />}</div>
62
60
</header>
63
63
-
<div className="flex-1 p-2">
64
64
-
61
61
+
<div className="flex-1">
65
62
{showLoginForm && <form onSubmit={handleSubmit} className="mt-2 max-w-sm mx-auto flex flex-col gap-2">
66
63
<Input value={identifier} onChange={(e) => setIdentifier(e.target.value)} autoComplete="username" placeholder="username.com" />
67
64
<Button disabled={isLoginLoading} size="sm" type="submit">
···
73
70
{showAppContent && <AuthContext.Provider value={{ profile: data.profile, client: data.client }}>
74
71
{children}
75
72
</AuthContext.Provider>}
76
76
-
77
73
</div>
78
74
</main>
79
75
}