Skip to content

Commit f25651d

Browse files
committed
refactor: pagination and socket hook
1 parent 58ac76f commit f25651d

3 files changed

Lines changed: 147 additions & 174 deletions

File tree

components/CoinsPagination.tsx

Lines changed: 5 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import {
99
PaginationNext,
1010
PaginationPrevious,
1111
} from '@/components/ui/pagination';
12-
import { cn } from '@/lib/utils';
12+
import { buildPageNumbers, cn, ELLIPSIS } from '@/lib/utils';
1313

1414
export default function CoinsPagination({
1515
currentPage,
@@ -22,45 +22,7 @@ export default function CoinsPagination({
2222
router.push(`/coins?page=${page}`);
2323
};
2424

25-
// Generate page numbers to display
26-
const getPageNumbers = () => {
27-
const pages: (number | string)[] = [];
28-
const showPages = 5; // Number of page buttons to show
29-
30-
if (totalPages <= showPages) {
31-
// Show all pages if total is less than or equal to showPages
32-
for (let i = 1; i <= totalPages; i++) {
33-
pages.push(i);
34-
}
35-
} else {
36-
pages.push(1);
37-
38-
// Calculate start and end of middle pages
39-
const start = Math.max(2, currentPage - 1);
40-
const end = Math.min(totalPages - 1, currentPage + 1);
41-
42-
// Add ellipsis after first page if needed
43-
if (start > 2) {
44-
pages.push('...');
45-
}
46-
47-
// Add middle pages
48-
for (let i = start; i <= end; i++) {
49-
pages.push(i);
50-
}
51-
52-
// Add ellipsis before last page if needed
53-
if (end < totalPages - 1) {
54-
pages.push('...');
55-
}
56-
57-
pages.push(totalPages);
58-
}
59-
60-
return pages;
61-
};
62-
63-
const pageNumbers = getPageNumbers();
25+
const pageNumbers = buildPageNumbers(currentPage, totalPages);
6426
const isLastPage = !hasMorePages || currentPage === totalPages;
6527

6628
return (
@@ -80,13 +42,13 @@ export default function CoinsPagination({
8042
<div className='flex flex-1 justify-center gap-2'>
8143
{pageNumbers.map((page, index) => (
8244
<PaginationItem key={index}>
83-
{page === '...' ? (
45+
{page === ELLIPSIS ? (
8446
<span className='px-3 py-2 text-base'>...</span>
8547
) : (
8648
<PaginationLink
87-
onClick={() => handlePageChange(page as number)}
49+
onClick={() => handlePageChange(page)}
8850
className={cn(
89-
`hover:bg-dark-400! rounded-sm text-base cursor-pointer`,
51+
'hover:bg-dark-400! rounded-sm text-base cursor-pointer',
9052
{
9153
'bg-green-500! text-dark-900 font-semibold':
9254
currentPage === page,

hooks/useCoinGeckoWebSocket.ts

Lines changed: 97 additions & 124 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
/* eslint-disable @typescript-eslint/no-explicit-any */
2-
import { useEffect, useRef, useState, useCallback } from 'react';
1+
import { useEffect, useRef, useState } from 'react';
32

43
const WS_BASE = `${process.env.NEXT_PUBLIC_COINGECKO_WEBSOCKET_URL}?x_cg_pro_api_key=${process.env.NEXT_PUBLIC_COINGECKO_API_KEY}`;
54

@@ -13,157 +12,131 @@ export function useCoinGeckoWebSocket({
1312
const [price, setPrice] = useState<ExtendedPriceData | null>(null);
1413
const [trades, setTrades] = useState<Trade[]>([]);
1514
const [ohlcv, setOhlcv] = useState<OHLCData | null>(null);
16-
const lastOhlcvTimestamp = useRef<number>(0);
1715

1816
const [isWsReady, setIsWsReady] = useState(false);
1917

20-
const handleMessage = useCallback((event: MessageEvent) => {
21-
const ws = wsRef.current;
22-
const msg: WebSocketMessage = JSON.parse(event.data);
23-
24-
// Ping/Pong to keep connection alive
25-
if (msg.type === 'ping') return ws?.send(JSON.stringify({ type: 'pong' }));
26-
27-
// Confirm subscription
28-
if (msg.type === 'confirm_subscription') {
29-
const { channel } = JSON.parse(msg?.identifier ?? '');
30-
subscribed.current.add(channel);
31-
return;
32-
}
33-
34-
// C1: Price updates
35-
if (msg.c === 'C1') {
36-
setPrice({
37-
usd: msg.p ?? 0,
38-
coin: msg.i,
39-
price: msg.p,
40-
change24h: msg.pp,
41-
marketCap: msg.m,
42-
volume24h: msg.v,
43-
timestamp: msg.t,
44-
});
45-
}
46-
47-
// G2: Trade updates
48-
if (msg.c === 'G2') {
49-
const newTrade: Trade = {
50-
price: msg.pu,
51-
value: msg.vo,
52-
timestamp: msg.t ?? 0,
53-
type: msg.ty,
54-
amount: msg.to,
55-
};
56-
57-
setTrades((prev) => [newTrade, ...prev].slice(0, 7));
58-
}
59-
// G3: OHLCV updates
60-
if (msg.ch === 'G3') {
61-
const timestamp = msg.t || 0; // already in seconds
62-
const newCandle: OHLCData = [
63-
timestamp,
64-
Number(msg.o ?? 0),
65-
Number(msg.h ?? 0),
66-
Number(msg.l ?? 0),
67-
Number(msg.c ?? 0),
68-
];
69-
70-
// Always update with the latest candle - chart will handle deduplication
71-
setOhlcv(newCandle);
72-
lastOhlcvTimestamp.current = timestamp;
73-
}
74-
}, []);
75-
7618
// WebSocket connection
7719
useEffect(() => {
7820
const ws = new WebSocket(WS_BASE);
7921
wsRef.current = ws;
22+
const send = (payload: Record<string, unknown>) =>
23+
ws.send(JSON.stringify(payload));
8024

81-
ws.onopen = () => setIsWsReady(true);
82-
ws.onmessage = handleMessage;
83-
ws.onclose = () => setIsWsReady(false);
25+
const handleMessage = (event: MessageEvent) => {
26+
const msg: WebSocketMessage = JSON.parse(event.data);
8427

85-
return () => ws.close();
86-
}, [handleMessage]);
28+
if (msg.type === 'ping') {
29+
send({ type: 'pong' });
30+
return;
31+
}
8732

88-
// Subscribe helper
89-
const subscribe = useCallback(
90-
(channel: string, data?: Record<string, any>) => {
91-
const ws = wsRef.current;
92-
if (!ws || !isWsReady || subscribed.current.has(channel)) return;
33+
if (msg.type === 'confirm_subscription') {
34+
const { channel } = JSON.parse(msg?.identifier ?? '');
35+
subscribed.current.add(channel);
36+
return;
37+
}
9338

94-
ws.send(
95-
JSON.stringify({
96-
command: 'subscribe',
97-
identifier: JSON.stringify({ channel }),
98-
})
99-
);
39+
if (msg.c === 'C1') {
40+
setPrice({
41+
usd: msg.p ?? 0,
42+
coin: msg.i,
43+
price: msg.p,
44+
change24h: msg.pp,
45+
marketCap: msg.m,
46+
volume24h: msg.v,
47+
timestamp: msg.t,
48+
});
49+
}
10050

101-
if (data) {
102-
ws.send(
103-
JSON.stringify({
104-
command: 'message',
105-
identifier: JSON.stringify({ channel }),
106-
data: JSON.stringify(data),
107-
})
108-
);
51+
if (msg.c === 'G2') {
52+
const newTrade: Trade = {
53+
price: msg.pu,
54+
value: msg.vo,
55+
timestamp: msg.t ?? 0,
56+
type: msg.ty,
57+
amount: msg.to,
58+
};
59+
60+
setTrades((prev) => [newTrade, ...prev].slice(0, 7));
10961
}
110-
},
111-
[isWsReady]
112-
);
11362

114-
const unsubscribeAll = useCallback(() => {
115-
const ws = wsRef.current;
116-
subscribed.current.forEach((channel) => {
117-
ws?.send(
118-
JSON.stringify({
119-
command: 'unsubscribe',
120-
identifier: JSON.stringify({ channel }),
121-
})
122-
);
123-
});
124-
subscribed.current.clear();
63+
if (msg.ch === 'G3') {
64+
const timestamp = msg.t || 0;
65+
const newCandle: OHLCData = [
66+
timestamp,
67+
Number(msg.o ?? 0),
68+
Number(msg.h ?? 0),
69+
Number(msg.l ?? 0),
70+
Number(msg.c ?? 0),
71+
];
72+
73+
setOhlcv(newCandle);
74+
}
75+
};
76+
77+
ws.onopen = () => setIsWsReady(true);
78+
ws.onmessage = handleMessage;
79+
ws.onclose = () => setIsWsReady(false);
80+
81+
return () => ws.close();
12582
}, []);
12683

12784
// Subscribe on connection ready
12885
useEffect(() => {
12986
if (!isWsReady) return;
87+
const ws = wsRef.current;
88+
if (!ws) return;
89+
const send = (payload: Record<string, unknown>) =>
90+
ws.send(JSON.stringify(payload));
13091

131-
let active = true;
92+
const unsubscribeAll = () => {
93+
subscribed.current.forEach((channel) => {
94+
send({
95+
command: 'unsubscribe',
96+
identifier: JSON.stringify({ channel }),
97+
});
98+
});
99+
subscribed.current.clear();
100+
};
132101

133-
(async () => {
134-
if (!active) return;
102+
const subscribe = (channel: string, data?: Record<string, unknown>) => {
103+
if (subscribed.current.has(channel)) return;
135104

136-
// Reset state
105+
send({ command: 'subscribe', identifier: JSON.stringify({ channel }) });
106+
107+
if (data) {
108+
send({
109+
command: 'message',
110+
identifier: JSON.stringify({ channel }),
111+
data: JSON.stringify(data),
112+
});
113+
}
114+
};
115+
116+
queueMicrotask(() => {
137117
setPrice(null);
138118
setTrades([]);
139119
setOhlcv(null);
140-
lastOhlcvTimestamp.current = 0;
141-
142-
unsubscribeAll();
143-
144-
// Subscribe channels
145-
subscribe('CGSimplePrice', { coin_id: [coinId], action: 'set_tokens' });
120+
});
146121

147-
const wsPools = [poolId.replace('_', ':')];
122+
unsubscribeAll();
148123

149-
if (wsPools.length) {
150-
subscribe('OnchainTrade', {
151-
'network_id:pool_addresses': wsPools,
152-
action: 'set_pools',
153-
});
124+
subscribe('CGSimplePrice', { coin_id: [coinId], action: 'set_tokens' });
154125

155-
subscribe('OnchainOHLCV', {
156-
'network_id:pool_addresses': wsPools,
157-
interval: '1s',
158-
action: 'set_pools',
159-
});
160-
}
161-
})();
126+
const poolAddress = poolId.replace('_', ':');
127+
if (poolAddress) {
128+
subscribe('OnchainTrade', {
129+
'network_id:pool_addresses': [poolAddress],
130+
action: 'set_pools',
131+
});
162132

163-
return () => {
164-
active = false;
165-
};
166-
}, [coinId, poolId, isWsReady, subscribe, unsubscribeAll]);
133+
subscribe('OnchainOHLCV', {
134+
'network_id:pool_addresses': [poolAddress],
135+
interval: '1s',
136+
action: 'set_pools',
137+
});
138+
}
139+
}, [coinId, poolId, isWsReady]);
167140

168141
return {
169142
price,

0 commit comments

Comments
 (0)