Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions src/app/dashboard/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { LuArrowDownRight, LuArrowUpRight } from "react-icons/lu";
import { useQuery } from "@tanstack/react-query";
import { queryKeys } from "@/lib/query-keys";
import { RewardsChart } from "@/components/RewardsChart";
import BtcPriceTicker from "@/components/BtcPriceTicker";

interface DCSummary {
dca: {
Expand Down Expand Up @@ -281,6 +282,11 @@ export default function WalletPage() {
</div>
</div>

{/* BTC Price Ticker */}
<div className="mb-4">
<BtcPriceTicker />
</div>

{/* Balance Card */}
{isLoading ? (
<BalanceCardSkeleton />
Expand Down
72 changes: 72 additions & 0 deletions src/components/BtcPriceTicker.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
"use client";

import { useEffect, useRef, useState } from "react";
import { motion } from "framer-motion";

export default function BtcPriceTicker() {
const [price, setPrice] = useState<number | null>(null);
const [prevPrice, setPrevPrice] = useState<number | null>(null);
const [trend, setTrend] = useState<"up" | "down" | null>(null);
const priceRef = useRef<number>(0);
const [rate, setRate] = useState<number | null>(null);

useEffect(() => {
const fetchRate = async () => {
try {
const res = await fetch("https://open.er-api.com/v6/latest/USD");
const data = await res.json();
setRate(data.rates.LKR);
} catch (err) {
console.error("Failed to fetch LKR rate", err);
}
};
fetchRate();
}, []);

useEffect(() => {
if (!rate) return;
const ws = new WebSocket("wss://ws.coincap.io/prices?assets=bitcoin");
ws.onmessage = (event) => {
const data = JSON.parse(event.data);
if (data.bitcoin) {
priceRef.current = parseFloat(data.bitcoin) * rate;
}
};
const interval = setInterval(() => {
setPrevPrice((p) => price);
setPrice(priceRef.current);
}, 1000);
return () => {
ws.close();
clearInterval(interval);
};
}, [rate, price]);

Comment on lines +26 to +44

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

⚠️ Potential issue

Fix WebSocket re-creation loop and incorrect prevPrice update

Including price in the effect’s dependency array recreates the WebSocket and timer on every update (roughly every second), causing churn and potential rate limits. Also, setPrevPrice((p) => price) ignores the functional param and does not reliably capture the previously displayed price.

  • Remove price from deps.
  • Track the last displayed price with a ref and update prevPrice before setting the new price.
  • Add basic ws.onerror logging and safer cleanup.

Add a ref for the displayed price somewhere near existing refs:

-    const priceRef = useRef<number>(0);
+    const priceRef = useRef<number>(0); // latest incoming LKR price
+    const displayedPriceRef = useRef<number | null>(null); // last displayed LKR price

Then replace the effect as below:

-useEffect(() => {
-    if (!rate) return;
-    const ws = new WebSocket("wss://ws.coincap.io/prices?assets=bitcoin");
-    ws.onmessage = (event) => {
-        const data = JSON.parse(event.data);
-        if (data.bitcoin) {
-            priceRef.current = parseFloat(data.bitcoin) * rate;
-        }
-    };
-    const interval = setInterval(() => {
-        setPrevPrice((p) => price);
-        setPrice(priceRef.current);
-    }, 1000);
-    return () => {
-        ws.close();
-        clearInterval(interval);
-    };
-}, [rate, price]);
+useEffect(() => {
+    if (!rate) return;
+    const ws = new WebSocket("wss://ws.coincap.io/prices?assets=bitcoin");
+    ws.onmessage = (event: MessageEvent) => {
+        const data = JSON.parse(event.data);
+        if (data.bitcoin) {
+            priceRef.current = parseFloat(data.bitcoin) * rate;
+        }
+    };
+    ws.onerror = (e) => {
+        console.error("CoinCap WebSocket error", e);
+    };
+    const interval = setInterval(() => {
+        const next = priceRef.current;
+        if (typeof next === "number" && next > 0) {
+            setPrevPrice(displayedPriceRef.current);
+            setPrice(next);
+            displayedPriceRef.current = next;
+        }
+    }, 1000);
+    return () => {
+        try {
+            ws.close();
+        } finally {
+            clearInterval(interval);
+        }
+    };
+}, [rate]);

Also applies to: 45-51

🤖 Prompt for AI Agents
In src/components/BtcPriceTicker.tsx around lines 26 to 44 (and similarly
45-51), the effect is recreating the WebSocket and timer every second because
price is in the dependency array and prevPrice is set incorrectly; remove price
from the deps so the effect only depends on rate, add a ref (e.g.,
displayedPriceRef) to hold the last displayed price, update
displayedPriceRef.current before calling setPrice and use that value to
setPrevPrice (instead of setPrevPrice(() => price)), add ws.onerror logging, and
make cleanup robust by checking ws.readyState before closing and always clearing
the interval. Ensure the effect returns the cleanup that closes the socket and
clears the interval and that any refs are updated inside the interval callback
so state updates are stable.

useEffect(() => {
if (prevPrice === null || price === null) return;
if (price === prevPrice) return;
setTrend(price > prevPrice ? "up" : "down");
const timeout = setTimeout(() => setTrend(null), 500);
return () => clearTimeout(timeout);
}, [price, prevPrice]);

if (price === null) {
return <div className="text-sm text-gray-400">Loading BTC price...</div>;
}

const trendClass =
trend === "up" ? "text-green-400" : trend === "down" ? "text-red-400" : "text-white";

return (
<motion.div
key={price}
initial={{ scale: 1 }}
animate={{ scale: [1, 1.05, 1] }}
transition={{ duration: 0.5 }}
className={`text-sm font-medium ${trendClass}`}
>
BTC Price: රු {price.toLocaleString("en-LK", { maximumFractionDigits: 0 })}
</motion.div>
);
}