Skip to content

Commit b4667e5

Browse files
committed
feat: migrate frontend to Supabase and enable Realtime sync
1 parent ca532c1 commit b4667e5

10 files changed

Lines changed: 516 additions & 475 deletions

File tree

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { createClient } from '@/utils/supabase/server'
2+
import { cookies } from 'next/headers'
3+
4+
export default async function Page() {
5+
const cookieStore = await cookies()
6+
const supabase = createClient(cookieStore)
7+
8+
const { data: markets } = await supabase.from('markets').select('*').limit(10)
9+
10+
return (
11+
<div className="p-8">
12+
<h1 className="text-2xl font-bold mb-4">Supabase Migrated Markets</h1>
13+
<ul className="list-disc pl-5">
14+
{markets?.map((market) => (
15+
<li key={market.market_id}>
16+
<strong>{market.title}</strong> ({market.category}) - {market.status}
17+
</li>
18+
))}
19+
</ul>
20+
{(!markets || markets.length === 0) && <p>No markets found. Check the migration logs.</p>}
21+
</div>
22+
)
23+
}

packages/nextjs/components/markets/MarketDetail.tsx

Lines changed: 69 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -256,21 +256,57 @@ export const MarketDetail = ({ market }: MarketDetailProps) => {
256256

257257
useEffect(() => {
258258
let active = true;
259+
const supabase = createClient();
259260

260261
const loadComments = async () => {
261262
try {
262263
setCommentsMessage("");
263-
const backendComments = await darkonnetApi.listComments(market.id);
264-
if (active) setComments(flattenComments(backendComments, activeCommentWalletAddress));
264+
const { data: backendComments, error: fetchError } = await supabase
265+
.from("comments")
266+
.select("*")
267+
.eq("market_id", market.id)
268+
.order("created_at", { ascending: true });
269+
270+
if (fetchError) throw fetchError;
271+
if (active && backendComments) {
272+
const mapped: MarketComment[] = backendComments.map(c => ({
273+
id: c.id,
274+
author: c.display_name,
275+
walletAddress: c.wallet_address,
276+
createdAt: new Date(c.created_at).getTime(),
277+
time: relativeTime(c.created_at),
278+
text: c.body,
279+
likes: c.liked_by?.length || 0,
280+
liked: Boolean(activeCommentWalletAddress && c.liked_by?.includes(activeCommentWalletAddress)),
281+
parentId: c.parent_id,
282+
}));
283+
setComments(mapped);
284+
}
265285
} catch (err) {
266286
if (active) setCommentsMessage(err instanceof Error ? err.message : "Unable to load backend comments.");
267287
}
268288
};
269289

270290
loadComments();
271291

292+
const channel = supabase
293+
.channel(`market-detail:${market.id}`)
294+
.on(
295+
"postgres_changes",
296+
{ event: "*", schema: "public", table: "comments", filter: `market_id=eq.${market.id}` },
297+
() => {
298+
loadComments();
299+
},
300+
)
301+
.on("postgres_changes", { event: "UPDATE", schema: "public", table: "markets", filter: `market_id=eq.${market.id}` }, payload => {
302+
// Handle market status/metadata updates if needed
303+
console.log("Market updated:", payload.new);
304+
})
305+
.subscribe();
306+
272307
return () => {
273308
active = false;
309+
supabase.removeChannel(channel);
274310
};
275311
}, [activeCommentWalletAddress, market.id]);
276312

@@ -315,14 +351,17 @@ export const MarketDetail = ({ market }: MarketDetailProps) => {
315351
return;
316352
}
317353

354+
const supabase = createClient();
318355
try {
319-
await darkonnetApi.createComment({
320-
marketId: market.id,
321-
walletAddress: commentWalletAddress,
322-
displayName: currentProfileName,
356+
const { error: postError } = await supabase.from("comments").insert({
357+
market_id: market.id,
358+
wallet_address: commentWalletAddress.toLowerCase(),
359+
display_name: currentProfileName,
323360
body: text,
324361
});
325-
setComments(flattenComments(await darkonnetApi.listComments(market.id), activeCommentWalletAddress));
362+
363+
if (postError) throw postError;
364+
326365
setCommentDraft("");
327366
setCommentSort("new");
328367
setCommentsMessage("");
@@ -346,15 +385,18 @@ export const MarketDetail = ({ market }: MarketDetailProps) => {
346385
const parentId = parentComment?.parentId ?? targetId;
347386
const replyAuthor = currentProfileName;
348387

388+
const supabase = createClient();
349389
try {
350-
await darkonnetApi.createComment({
351-
marketId: market.id,
352-
walletAddress: commentWalletAddress,
353-
displayName: replyAuthor,
390+
const { error: postError } = await supabase.from("comments").insert({
391+
market_id: market.id,
392+
wallet_address: commentWalletAddress.toLowerCase(),
393+
display_name: replyAuthor,
354394
body: text,
355-
parentId,
395+
parent_id: parentId,
356396
});
357397

398+
if (postError) throw postError;
399+
358400
if (parentAuthor === currentProfileName) {
359401
addNotification({
360402
title: "Reply On Your Comment",
@@ -363,7 +405,6 @@ export const MarketDetail = ({ market }: MarketDetailProps) => {
363405
});
364406
}
365407

366-
setComments(flattenComments(await darkonnetApi.listComments(market.id), activeCommentWalletAddress));
367408
setReplyDrafts(prev => ({ ...prev, [targetId]: "" }));
368409
setReplyingTo(null);
369410
setExpandedThreads(prev => ({ ...prev, [parentId]: true }));
@@ -383,39 +424,25 @@ export const MarketDetail = ({ market }: MarketDetailProps) => {
383424
}
384425

385426
const nextLiked = !comment.liked;
386-
setComments(prev =>
387-
prev.map(comment =>
388-
comment.id === commentId
389-
? {
390-
...comment,
391-
liked: nextLiked,
392-
likes: nextLiked ? comment.likes + 1 : Math.max(0, comment.likes - 1),
393-
}
394-
: comment,
395-
),
396-
);
427+
const supabase = createClient();
397428

398429
try {
399-
await darkonnetApi.setCommentLike({
400-
marketId: market.id,
401-
commentId,
402-
walletAddress: activeCommentWalletAddress,
403-
liked: nextLiked,
404-
});
405-
setComments(flattenComments(await darkonnetApi.listComments(market.id), activeCommentWalletAddress));
430+
const { data: currentComment } = await supabase.from("comments").select("liked_by").eq("id", commentId).single();
431+
432+
let likedBy = currentComment?.liked_by || [];
433+
if (nextLiked) {
434+
if (!likedBy.includes(activeCommentWalletAddress)) {
435+
likedBy.push(activeCommentWalletAddress);
436+
}
437+
} else {
438+
likedBy = likedBy.filter((w: string) => w !== activeCommentWalletAddress);
439+
}
440+
441+
const { error: updateError } = await supabase.from("comments").update({ liked_by: likedBy }).eq("id", commentId);
442+
443+
if (updateError) throw updateError;
406444
setCommentsMessage("");
407445
} catch (err) {
408-
setComments(prev =>
409-
prev.map(comment =>
410-
comment.id === commentId
411-
? {
412-
...comment,
413-
liked: !nextLiked,
414-
likes: nextLiked ? Math.max(0, comment.likes - 1) : comment.likes + 1,
415-
}
416-
: comment,
417-
),
418-
);
419446
setCommentsMessage(err instanceof Error ? err.message : "Unable to update comment like.");
420447
}
421448
};

packages/nextjs/components/markets/MarketGrid.tsx

Lines changed: 54 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -147,27 +147,78 @@ export const MarketGrid = ({ source = "platform" }: MarketGridProps) => {
147147

148148
useEffect(() => {
149149
let active = true;
150+
const supabase = createClient();
151+
152+
const mapSupabaseMarket = (m: any): Market => ({
153+
id: m.market_id,
154+
slug: m.slug,
155+
onchainMarketId: m.onchain_market_id,
156+
question: m.title,
157+
description: m.description,
158+
category: m.category,
159+
yesProbability: m.metadata?.yesProbability || 0.5,
160+
encryptedVolumeLabel: m.metadata?.encryptedVolumeLabel || "Encrypted",
161+
tradingVolume: m.metadata?.tradingVolume || 0,
162+
endsAt: m.metadata?.endsAt || m.starts_at || new Date().toISOString(),
163+
signalLabel: m.metadata?.signalLabel || "Awaiting activity",
164+
sentimentSignals: m.metadata?.sentimentSignals || { news: 50, volume: 50, crowd: 50 },
165+
trending: m.metadata?.trending || false,
166+
rules: m.metadata?.rules || "",
167+
sources: m.metadata?.sources || [],
168+
coverImageDataUrl: m.metadata?.coverImageDataUrl,
169+
homeLogoUrl: m.home_logo_url,
170+
awayLogoUrl: m.away_logo_url,
171+
homeName: m.home_name,
172+
awayName: m.away_name,
173+
creatorKey: m.creator_wallet_address,
174+
status: m.status,
175+
resolution: m.resolution,
176+
resolvedAt: m.resolved_at,
177+
});
150178

151179
const syncMarkets = async () => {
152180
setIsLoading(true);
153181
setError("");
154182
try {
155-
const nextMarkets = await darkonnetApi.listMarkets();
183+
const { data, error: fetchError } = await supabase
184+
.from('markets')
185+
.select('*')
186+
.order('created_at', { ascending: false });
187+
188+
if (fetchError) throw fetchError;
156189
if (!active) return;
157-
setMarkets(nextMarkets);
190+
setMarkets(data.map(mapSupabaseMarket));
158191
} catch (err) {
159192
if (!active) return;
160193
setMarkets([]);
161-
setError(err instanceof Error ? err.message : "Unable to load markets from the DarkONNET backend.");
194+
setError(err instanceof Error ? err.message : "Unable to load markets from Supabase.");
162195
} finally {
163196
if (active) setIsLoading(false);
164197
}
165198
};
166199

167200
syncMarkets();
168201

202+
const channel = supabase
203+
.channel('markets-realtime')
204+
.on(
205+
'postgres_changes',
206+
{ event: '*', schema: 'public', table: 'markets' },
207+
(payload) => {
208+
if (payload.eventType === 'INSERT') {
209+
setMarkets(prev => [mapSupabaseMarket(payload.new), ...prev]);
210+
} else if (payload.eventType === 'UPDATE') {
211+
setMarkets(prev => prev.map(m => m.id === payload.new.market_id ? mapSupabaseMarket(payload.new) : m));
212+
} else if (payload.eventType === 'DELETE') {
213+
setMarkets(prev => prev.filter(m => m.id === payload.old.market_id));
214+
}
215+
}
216+
)
217+
.subscribe();
218+
169219
return () => {
170220
active = false;
221+
supabase.removeChannel(channel);
171222
};
172223
}, []);
173224

0 commit comments

Comments
 (0)