diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..ec79d9c
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,4 @@
+node_modules/
+.next/
+.env*
+.DS_Store
diff --git a/app/page.jsx b/app/page.jsx
index e87415c..9efaff8 100644
--- a/app/page.jsx
+++ b/app/page.jsx
@@ -8,6 +8,7 @@ import { Badge } from "@/components/ui/badge";
import { PortfolioOverview } from "@/components/PortfolioOverview";
import { ExchangeCard } from "@/components/ExchangeCard";
import { AssetList } from "@/components/AssetList";
+import { calculatePortfolioStats } from "@/lib/utils";
const mockExchanges = [
{
@@ -53,24 +54,14 @@ const mockAssets = [
];
export default function HomePage() {
- const { totalValue, totalChange } = useMemo(() => {
- const totalBalance = mockAssets.reduce((acc, asset) => acc + asset.value, 0);
- const change =
- mockExchanges
- .filter((exchange) => exchange.connected)
- .reduce((acc, exchange) => acc + exchange.change24h, 0) /
- Math.max(mockExchanges.filter((exchange) => exchange.connected).length, 1);
+ const { totalValue, weightedChange, bestPerformer, worstPerformer, allocation } =
+ useMemo(() => calculatePortfolioStats(mockAssets), []);
- return {
- totalValue: totalBalance,
- totalChange: change
- };
- }, []);
-
- const connectedCount = useMemo(
- () => mockExchanges.filter((exchange) => exchange.connected).length,
+ const connectedExchanges = useMemo(
+ () => mockExchanges.filter((exchange) => exchange.connected),
[]
);
+ const connectedCount = connectedExchanges.length;
return (
@@ -98,9 +89,12 @@ export default function HomePage() {
diff --git a/components/AssetList.jsx b/components/AssetList.jsx
index fae42b7..00f2381 100644
--- a/components/AssetList.jsx
+++ b/components/AssetList.jsx
@@ -1,12 +1,6 @@
import { TrendingDown, TrendingUp } from "lucide-react";
import { Card } from "@/components/ui/card";
-
-const formatCurrency = (value) =>
- new Intl.NumberFormat("en-US", {
- style: "currency",
- currency: "USD",
- maximumFractionDigits: 2
- }).format(value);
+import { formatCurrency, formatPercentage } from "@/lib/utils";
export const AssetList = ({ assets }) => {
return (
@@ -26,6 +20,7 @@ export const AssetList = ({ assets }) => {
{assets.map((asset) => {
const isPositive = asset.change24h >= 0;
const ChangeIcon = isPositive ? TrendingUp : TrendingDown;
+ const changeDisplay = formatPercentage(Math.abs(asset.change24h));
return (
|
@@ -44,8 +39,8 @@ export const AssetList = ({ assets }) => {
|
- {isPositive ? "+" : ""}
- {asset.change24h.toFixed(2)}%
+ {isPositive ? "+" : "-"}
+ {changeDisplay}%
|
{formatCurrency(asset.value)} |
diff --git a/components/ExchangeCard.jsx b/components/ExchangeCard.jsx
index d0a1099..1944aca 100644
--- a/components/ExchangeCard.jsx
+++ b/components/ExchangeCard.jsx
@@ -1,16 +1,11 @@
import { Wallet, CheckCircle2, XCircle } from "lucide-react";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
-
-const formatCurrency = (value) =>
- new Intl.NumberFormat("en-US", {
- style: "currency",
- currency: "USD",
- maximumFractionDigits: 2
- }).format(value);
+import { formatCurrency, formatPercentage } from "@/lib/utils";
export const ExchangeCard = ({ exchange }) => {
const isPositive = exchange.change24h >= 0;
+ const changeDisplay = formatPercentage(Math.abs(exchange.change24h));
return (
@@ -41,8 +36,8 @@ export const ExchangeCard = ({ exchange }) => {
24h Change
- {isPositive ? "+" : ""}
- {exchange.change24h.toFixed(2)}%
+ {isPositive ? "+" : "-"}
+ {changeDisplay}%
diff --git a/components/PortfolioOverview.jsx b/components/PortfolioOverview.jsx
index 11bf6b5..f5ed49b 100644
--- a/components/PortfolioOverview.jsx
+++ b/components/PortfolioOverview.jsx
@@ -1,22 +1,29 @@
import { TrendingDown, TrendingUp, Wallet } from "lucide-react";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
-
-const formatCurrency = (value) =>
- new Intl.NumberFormat("en-US", {
- style: "currency",
- currency: "USD",
- maximumFractionDigits: 2
- }).format(value);
+import { formatCurrency, formatPercentage } from "@/lib/utils";
export const PortfolioOverview = ({
totalValue,
totalChange,
connectedExchanges,
- totalAssets
+ totalAssets,
+ bestPerformer,
+ worstPerformer,
+ allocation
}) => {
const isPositive = totalChange >= 0;
const ChangeIcon = isPositive ? TrendingUp : TrendingDown;
+ const changeDisplay = formatPercentage(Math.abs(totalChange));
+ const bestPerformerLabel = bestPerformer
+ ? `${bestPerformer.name} (${bestPerformer.symbol})`
+ : "No assets tracked";
+ const worstPerformerLabel = worstPerformer
+ ? `${worstPerformer.name} (${worstPerformer.symbol})`
+ : "No assets tracked";
+ const allocationSummary = allocation?.length
+ ? allocation.map((item) => `${item.percentage}% ${item.symbol}`).join(" / ")
+ : "No tracked assets";
return (
@@ -37,8 +44,8 @@ export const PortfolioOverview = ({
24h
- {isPositive ? "+" : ""}
- {totalChange.toFixed(2)}%
+ {isPositive ? "+" : "-"}
+ {changeDisplay}%
@@ -80,15 +87,15 @@ export const PortfolioOverview = ({
Best Performer
- Solana (SOL)
+ {bestPerformerLabel}
Worst Performer
- Ethereum (ETH)
+ {worstPerformerLabel}
Allocation
- 45% BTC / 32% ALT / 23% DeFi
+ {allocationSummary}
diff --git a/docs/integration-guide.md b/docs/integration-guide.md
new file mode 100644
index 0000000..fcc5d8c
--- /dev/null
+++ b/docs/integration-guide.md
@@ -0,0 +1,83 @@
+# Exchange & Wallet Integration Guide
+
+This guide walks you through preparing API credentials or wallet connections for the data sources represented in the CryptoTracker Pro dashboard.
+
+> **Security first.** Never commit API keys or seed phrases to source control. Store them in `.env.local` for local development and in your deployment platform's secret manager in production.
+
+## 1. Binance
+
+1. Log into your [Binance](https://www.binance.com/) account and open **Profile → API Management**.
+2. Create a new API key and label it (for example, `CryptoTrackerPro`).
+3. Complete Binance's security verifications to reveal the key and secret.
+4. Under **API Restrictions**:
+ - Enable **Read Only** access. _Do not enable withdrawal permissions._
+ - If you plan to use IP restrictions, add the server's public IP and click **Confirm**.
+5. Store the credentials locally in `.env.local`:
+ ```bash
+ BINANCE_API_KEY=your_key_here
+ BINANCE_API_SECRET=your_secret_here
+ ```
+6. Restart the Next.js dev server so the new environment variables are loaded.
+7. In your data-fetching service (e.g., `/lib/integrations/binance.js`), use the credentials to call the [`/api/v3/account`](https://binance-docs.github.io/apidocs/spot/en/#account-information-user_data) endpoint for balances and recent 24h change stats.
+
+## 2. Coinbase Advanced Trade (Coinbase Pro)
+
+1. Visit [Coinbase Advanced Trade](https://advanced.trade.coinbase.com/) → **API** and click **Create API key**.
+2. Choose the portfolio that contains the assets you want to track.
+3. Give the key a descriptive name and set **Permissions → View** only.
+4. Download the `API Key`, `API Secret`, and `Passphrase` that Coinbase provides.
+5. Save them in `.env.local`:
+ ```bash
+ COINBASE_API_KEY=your_key_here
+ COINBASE_API_SECRET=your_secret_here
+ COINBASE_API_PASSPHRASE=your_passphrase_here
+ ```
+6. Use the official REST client or signed fetch calls against [`/accounts`](https://docs.cloud.coinbase.com/advanced-trade-api/reference/retailbrokerageapi_getaccounts) to retrieve balances and product tickers for pricing data.
+7. When deploying, add the same values to your hosting provider's secret store.
+
+## 3. MetaMask Wallet
+
+MetaMask exposes assets directly from the browser wallet, so no API keys are needed.
+
+1. Install the [MetaMask extension](https://metamask.io/download/) and import or create your wallet.
+2. In the CryptoTracker Pro UI, prompt the connection with `window.ethereum.request({ method: "eth_requestAccounts" })`.
+3. Store the selected account address in application state (for example, in a React context) and pass it to your balances hook.
+4. Use an on-chain data provider such as [Etherscan](https://docs.etherscan.io/) or [Alchemy](https://docs.alchemy.com/) to fetch token balances for the connected address. Persist the provider keys in `.env.local` (e.g., `ALCHEMY_API_KEY`).
+5. Refresh holdings on an interval or with a "Refresh" button to keep the dashboard in sync.
+
+## 4. Kraken
+
+1. Log into [Kraken](https://www.kraken.com/) and go to **Security → API**.
+2. Click **Add Key**, select a descriptive name, and enable the following permissions: **Query Funds**, **Query Ledgers**, and **Query Closed Orders**. Leave withdrawal permissions disabled.
+3. After creating the key, copy the **Key** and **Private Key** values.
+4. Add them to `.env.local`:
+ ```bash
+ KRAKEN_API_KEY=your_key_here
+ KRAKEN_API_SECRET=your_secret_here
+ ```
+5. Use Kraken's [REST API](https://docs.kraken.com/rest/) endpoints such as `Balance` and `TradeBalance` to obtain holdings and valuation.
+6. If Kraken is currently disconnected in the UI, surface actionable messaging (e.g., "Reconnect" button) once credentials are added.
+
+## Environment Variable Checklist
+
+Ensure the following keys are defined locally (and in production secrets) before fetching live data:
+
+```
+BINANCE_API_KEY=
+BINANCE_API_SECRET=
+COINBASE_API_KEY=
+COINBASE_API_SECRET=
+COINBASE_API_PASSPHRASE=
+KRAKEN_API_KEY=
+KRAKEN_API_SECRET=
+ALCHEMY_API_KEY= # Optional, for MetaMask balance lookups
+```
+
+## Next Steps
+
+- Build dedicated integration modules under `lib/integrations` that encapsulate request signing for each exchange.
+- Create an API route (e.g., `/api/portfolio`) that aggregates balances, normalises everything to USD, and feeds the `mockAssets` shape currently used in `app/page.jsx`.
+- Replace the mocked arrays with responses from the aggregation route, then persist the raw data in a lightweight database (Supabase, PlanetScale, etc.) if you need history.
+- Add UX affordances such as status badges (`Connected`, `Action Required`) that react to integration health checks.
+
+Following these steps will let you progressively replace the mocked data with real balances from each provider while keeping secrets out of your client bundle.
diff --git a/lib/utils.js b/lib/utils.js
index d8b4cf5..6f8a39d 100644
--- a/lib/utils.js
+++ b/lib/utils.js
@@ -4,3 +4,119 @@ import { twMerge } from "tailwind-merge";
export function cn(...inputs) {
return twMerge(clsx(inputs));
}
+
+export function formatCurrency(value, { currency = "USD", maximumFractionDigits = 2 } = {}) {
+ return new Intl.NumberFormat("en-US", {
+ style: "currency",
+ currency,
+ maximumFractionDigits,
+ minimumFractionDigits: maximumFractionDigits
+ }).format(typeof value === "number" ? value : 0);
+}
+
+export function formatPercentage(value, { maximumFractionDigits = 2 } = {}) {
+ return Number.parseFloat(value ?? 0).toLocaleString("en-US", {
+ minimumFractionDigits: 0,
+ maximumFractionDigits
+ });
+}
+
+function normalizePercentages(items) {
+ if (!items.length) return [];
+
+ const working = items.map((item, index) => ({
+ symbol: item.symbol,
+ raw: item.percentage,
+ percentage: Math.floor(item.percentage),
+ index
+ }));
+
+ let remainder = Math.round(
+ 100 - working.reduce((sum, item) => sum + item.percentage, 0)
+ );
+
+ if (remainder > 0) {
+ const prioritised = [...working].sort(
+ (a, b) => (b.raw - b.percentage) - (a.raw - a.percentage)
+ );
+ let cursor = 0;
+ const length = prioritised.length;
+ while (remainder > 0 && length > 0) {
+ prioritised[cursor % length].percentage += 1;
+ remainder -= 1;
+ cursor += 1;
+ }
+ }
+
+ return working
+ .sort((a, b) => a.index - b.index)
+ .map((item) => ({ symbol: item.symbol, percentage: item.percentage }));
+}
+
+export function calculatePortfolioStats(assets = []) {
+ if (!Array.isArray(assets) || assets.length === 0) {
+ return {
+ totalValue: 0,
+ weightedChange: 0,
+ bestPerformer: null,
+ worstPerformer: null,
+ allocation: []
+ };
+ }
+
+ const totalValue = assets.reduce((sum, asset) => sum + (asset.value ?? 0), 0);
+
+ const weightedChange =
+ totalValue > 0
+ ? assets.reduce(
+ (sum, asset) => sum + (asset.value ?? 0) * (asset.change24h ?? 0),
+ 0
+ ) / totalValue
+ : 0;
+
+ const bestPerformer = assets.reduce((best, asset) => {
+ if (!best) return asset;
+ return (asset.change24h ?? -Infinity) > (best.change24h ?? -Infinity)
+ ? asset
+ : best;
+ }, null);
+
+ const worstPerformer = assets.reduce((worst, asset) => {
+ if (!worst) return asset;
+ return (asset.change24h ?? Infinity) < (worst.change24h ?? Infinity)
+ ? asset
+ : worst;
+ }, null);
+
+ const sortedByValue = [...assets].sort(
+ (a, b) => (b.value ?? 0) - (a.value ?? 0)
+ );
+ const primaryHoldings = sortedByValue.slice(0, 3).map((asset) => ({
+ symbol: asset.symbol,
+ value: asset.value ?? 0
+ }));
+ const otherValue = sortedByValue
+ .slice(3)
+ .reduce((sum, asset) => sum + (asset.value ?? 0), 0);
+ if (otherValue > 0) {
+ primaryHoldings.push({ symbol: "Other", value: otherValue });
+ }
+
+ const allocation =
+ totalValue > 0
+ ? normalizePercentages(
+ primaryHoldings.map((holding) => ({
+ symbol: holding.symbol,
+ percentage: (holding.value / totalValue) * 100
+ }))
+ )
+ : [];
+
+ return {
+ totalValue,
+ weightedChange,
+ bestPerformer,
+ worstPerformer,
+ allocation
+ };
+}
diff --git a/readme.MD b/readme.MD
index f8607b4..123bd54 100644
--- a/readme.MD
+++ b/readme.MD
@@ -16,9 +16,14 @@ A Next.js dashboard experience for monitoring cryptocurrency holdings across exc
## Features
-- Portfolio overview with total balance and 24h performance snapshot
+- Portfolio overview with total balance and 24h performance snapshot derived from your holdings data
+- Automatic best/worst performer highlights and allocation breakdown
- Exchange connectivity cards with status indicators
- Asset holdings table showing balances, prices, and change percentages
- Tailwind CSS design tokens for a cohesive, glassmorphism-inspired aesthetic
Mock data is provided in `app/page.jsx` to demonstrate the layout.
+
+## Connecting Real Data
+
+When you're ready to replace the mock arrays, follow the detailed steps in [`docs/integration-guide.md`](docs/integration-guide.md) to configure Binance, Coinbase Advanced Trade, MetaMask, and Kraken credentials.