diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..30cf57e --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,10 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Ignored default folder with query files +/queries/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 0000000..03d9549 --- /dev/null +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..4b151ab --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..2bd0a5c --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/stellarflow-frontend.iml b/.idea/stellarflow-frontend.iml new file mode 100644 index 0000000..d6ebd48 --- /dev/null +++ b/.idea/stellarflow-frontend.iml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..35eb1dd --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/src/app/components/PriceFeedCard.tsx b/src/app/components/PriceFeedCard.tsx index edc1db8..5cfea5c 100644 --- a/src/app/components/PriceFeedCard.tsx +++ b/src/app/components/PriceFeedCard.tsx @@ -108,6 +108,7 @@ function formatTime(iso: string): string { const PriceFeedCard: React.FC = ({ refreshInterval = 30_000, enableWebSocket = true, + assetId, }) => { const mounted = useMounted(); const [data, setData] = useState(null); @@ -123,7 +124,6 @@ const PriceFeedCard: React.FC = ({ // Granular context subscriptions — each hook only re-renders this component // when its specific slice changes, not on every unrelated socket event. const { isConnected, error: wsError } = useSocketConnection(); - const { lastUpdate: wsUpdate } = useSocketData(); const isPageVisible = usePageVisibility(); diff --git a/src/app/components/client/LivePrices.tsx b/src/app/components/client/LivePrices.tsx index b33a3e1..282f37a 100644 --- a/src/app/components/client/LivePrices.tsx +++ b/src/app/components/client/LivePrices.tsx @@ -1,7 +1,8 @@ "use client" -import React, { useEffect, useState, memo } from 'react' -import { useSocket } from '../../hooks/useSocket' +import React, { memo } from "react" +import { useAssetPrice } from "../../hooks/useAssetPrice" +import { useSocketConnection } from "../providers/SocketProvider" const CHART_HISTORY_LIMIT = 150; @@ -50,18 +51,19 @@ function LivePrices({ initialData }: any) { return (
-

Live Prices

-
- {isConnected ? 'WebSocket Connected' : 'WebSocket Disconnected'} +

Live Prices

+
+ {isConnected ? "WebSocket Connected" : "WebSocket Disconnected"}
{error &&
Error: {error}
} - {data?.map((p: PriceData) => ( -
- {p.symbol}: {p.price} -
- ))} + +
+ {initialAssets.map((id: string) => ( + + ))} +
) } -export default memo(LivePrices); \ No newline at end of file +export default memo(LivePrices) \ No newline at end of file diff --git a/src/app/components/providers/QueryProvider.tsx b/src/app/components/providers/QueryProvider.tsx index 3384db2..9eead3c 100644 --- a/src/app/components/providers/QueryProvider.tsx +++ b/src/app/components/providers/QueryProvider.tsx @@ -1,3 +1,5 @@ +"use client" + import { QueryClientProvider } from '@tanstack/react-query' import { queryClient } from '../../lib/queryClient' diff --git a/src/app/globals.css b/src/app/globals.css index a3fcba3..a3121fc 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -3,6 +3,8 @@ :root { --background: #ffffff; --foreground: #171717; + --font-geist-sans: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; + --font-geist-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; } /* Dark theme — class applied by next-themes */ diff --git a/src/app/hooks/useAssetPrice.ts b/src/app/hooks/useAssetPrice.ts new file mode 100644 index 0000000..8ab41b9 --- /dev/null +++ b/src/app/hooks/useAssetPrice.ts @@ -0,0 +1,39 @@ +"use client" + +import { useEffect, useState } from "react" +import { useSocketActions, useSocketData } from "../components/providers/SocketProvider" + +interface AssetPrice { + price?: number + timestamp?: number +} + +/** + * Subscribe to a single asset and expose a localized price state. + * This hook only updates its internal state when the incoming WS tick + * targets `assetId`, avoiding re-renders when unrelated assets change. + */ +export function useAssetPrice(assetId: string): AssetPrice { + const { subscribeToAsset, unsubscribeFromAsset } = useSocketActions() + const { lastUpdate } = useSocketData() + + const [state, setState] = useState({ price: undefined, timestamp: undefined }) + + // Subscribe once on mount, and unsubscribe on unmount. + useEffect(() => { + subscribeToAsset(assetId) + return () => { + unsubscribeFromAsset(assetId) + } + }, [assetId, subscribeToAsset, unsubscribeFromAsset]) + + // Update local state only when the global `lastUpdate` refers to our asset. + useEffect(() => { + if (!lastUpdate) return + if (lastUpdate.assetPair === assetId) { + setState({ price: lastUpdate.price, timestamp: lastUpdate.timestamp }) + } + }, [assetId, lastUpdate]) + + return state +} diff --git a/src/app/layout.tsx b/src/app/layout.tsx index aabe25f..73ef790 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,6 +1,5 @@ import "@/config/env"; import type { Metadata } from "next"; -import { Geist, Geist_Mono } from "next/font/google"; import "./globals.css"; import { ThemeProvider } from "./components/ThemeProvider"; import { ProgressBarProvider } from "./components/TopLoadingBar"; @@ -10,20 +9,6 @@ import { QueryProvider } from "./components/providers/QueryProvider"; import Script from "next/script"; import { SvgSprite } from "@/components/icons"; -const geistSans = Geist({ - variable: "--font-geist-sans", - subsets: ["latin"], - display: "swap", - weight: ["400", "700"] -}); - -const geistMono = Geist_Mono({ - variable: "--font-geist-mono", - subsets: ["latin"], - display: "swap", - weight: ["400", "700"] -}); - export const metadata: Metadata = { title: "Create Next App", description: "Generated by create next app",