A Prediction Market on the AT Protocol

feat(web): more client mvp

Ciaran 7610a131 0f49807c

+24 -36
+14 -21
src/web/app.tsx
··· 1 1 import { useCumulus } from "./providers/useCumulus"; 2 - import { formatDistance, getUnixTime } from "date-fns" 2 + import { formatDistance } from "date-fns" 3 3 import { Spinner } from "./components/ui/spinner"; 4 - import { LineChart, Line, XAxis, CartesianGrid } from "recharts"; 5 - import { ChartContainer, ChartTooltip } from "./components/ui/chart"; 4 + import { LineChart, Line, Tooltip } from "recharts"; 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 - return { 22 - ...bet, 23 - createdAt: formatDistance(bet.createdAt, new Date(), { addSuffix: true }), 24 - timestamp: getUnixTime(bet.createdAt), 25 - yes, 26 - no, 27 - yesPrice: yesPrice(yes, no, market.liquidity), 28 - noPrice: noPrice(yes, no, market.liquidity), 29 - } 21 + return { ...bet, yes, no, } 30 22 }) 31 - 32 - return <div key={market.cid} className="space-y-2"> 33 - <h2 className="text-3xl font-medium">{market.question}</h2> 34 - <p className="uppercase text-sm font-bold">Closes {formatDistance(new Date(market.closesAt), new Date(), { addSuffix: true })} | {market.bets?.length} Positions</p> 23 + return <div key={market.cid} className="relative uppercase bg-radial-[at_80%_200%] from-coral-500 via-coral-50"> 24 + <div className="absolute inset-0 p-2"> 25 + <h2 className="text-xl font-bold flex gap-1 items-center">{market.question}</h2> 26 + <p>Closes: {formatDistance(new Date(market.closesAt), new Date(), { addSuffix: true })}</p> 27 + <p>Positions: {market.bets?.length}</p> 28 + <p>Yes Price: {yesPrice(yes, no, market.liquidity)}</p> 29 + <p>No Price: {noPrice(yes, no, market.liquidity)}</p> 30 + </div> 35 31 <ChartContainer 36 - config={{ 37 - yes: { label: "Yes" }, no: { label: "No" } 38 - }}> 32 + config={{ yes: { label: "Yes" }, no: { label: "No" } }}> 39 33 <LineChart data={mappedBets}> 40 - <ChartTooltip /> 34 + <Tooltip /> 41 35 <Line dataKey="yes" stroke="var(--color-shell-600)" /> 42 36 <Line dataKey="no" stroke="var(--color-coral-600)" /> 43 - <Line dataKey="yesPrice" /> 44 37 </LineChart> 45 38 </ChartContainer> 46 39 </div>
+3 -4
src/web/components/shared/avatar.tsx
··· 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 - return <Badge variant="link"> 7 - <img src={profile.avatar} className="h-4 rounded-full" /> 5 + return <div className="flex items-center gap-2 text-sm text-coral-500 tracking-tight"> 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 - </Badge> 8 + </div> 10 9 }
+2 -2
src/web/lib/lmsr.ts
··· 1 1 export function yesPrice(yes: number, no: number, liquidity: number) { 2 - return 1 / (1 + Math.exp((no - yes) / liquidity)); 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 - return 1 / (1 + Math.exp((yes - no) / liquidity)); 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 - return <main className="text-shell-900 flex flex-col min-h-screen bg-shell-50"> 57 - <header className="bg-shell-100 p-2 h-10 flex justify-between gap-2 items-center"> 58 - <h1 className="justify-self-start uppercase text-xs font-medium">Cumulus</h1> 59 - <div className="flex items-center gap-2"> 60 - {showLoader ? <Spinner /> : <Avatar profile={data?.profile} />} 61 - </div> 56 + return <main className="subpixel-antialiased text-shell-900 bg-shell-50 flex flex-col min-h-screen"> 57 + <header className="bg-shell-900 text-shell-50 p-2 h-10 flex justify-between gap-2 items-center"> 58 + <h1 className="text-coral-500 justify-self-start uppercase text-xs font-extrabold">Cumulus</h1> 59 + <div className="flex items-center gap-2">{showLoader ? <Spinner /> : <Avatar profile={data?.profile} />}</div> 62 60 </header> 63 - <div className="flex-1 p-2"> 64 - 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 - 77 73 </div> 78 74 </main> 79 75 }