From e25de028b94d70d2c5d34600014bba3754ce7bb5 Mon Sep 17 00:00:00 2001 From: alvaromaoc Date: Tue, 3 Jun 2025 09:46:08 +0200 Subject: [PATCH] First iteration cool UI (portfolio performance) --- src/pages/webapp/Dashboard.tsx | 159 +++++++++++++++++++++++++++++++- src/pages/webapp/Simulation.tsx | 1 - 2 files changed, 156 insertions(+), 4 deletions(-) diff --git a/src/pages/webapp/Dashboard.tsx b/src/pages/webapp/Dashboard.tsx index d38fa43..24b0b9c 100644 --- a/src/pages/webapp/Dashboard.tsx +++ b/src/pages/webapp/Dashboard.tsx @@ -18,7 +18,7 @@ import { DrawerTitle, DrawerTrigger, } from "@/components/ui/drawer" -import {Minus, Plus} from "lucide-react" +import {Minus, Plus, TrendingDown, TrendingUp} from "lucide-react" import { Select, @@ -36,11 +36,14 @@ import {DecisionHttpService} from "@/services/decision-http-service.ts"; import {UsersHttpService} from "@/services/users-http-service.ts"; import {Tooltip, TooltipContent, TooltipProvider, TooltipTrigger} from "@/components/ui/tooltip.tsx"; import {Badge} from "@/components/ui/badge.tsx"; -import {Card, CardContent, CardDescription, CardHeader, CardTitle} from "@/components/ui/card.tsx"; +import {Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle} from "@/components/ui/card.tsx"; import {NewsItem} from "@/model/NewsItem.ts"; import {Alert} from "@/model/Alert.ts"; import {NewsHttpService} from "@/services/news-http-service.ts"; import {AlertsHttpService} from "@/services/alerts-http-service.ts"; +import {ChartConfig, ChartContainer, ChartTooltip, ChartTooltipContent} from "@/components/ui/chart.tsx"; +import {CartesianGrid, Line, LineChart, XAxis, YAxis} from "recharts"; +import {addDays, startOfDay} from "date-fns"; const portfolioHttpService = new PortfolioHttpService(); const assetsHttpService = new AssetsHttpService(); @@ -135,6 +138,129 @@ function Summary() { ) } +interface ChartData { + date: string; + portfolio: number; +} + +const chartConfig = { + portfolio: { + label: "Portfolio", + color: "hsl(var(--chart-blue-1))", + }, +} satisfies ChartConfig + +function ChartLineDots({ chartData }: { chartData: ChartData[] }) { + const calculatePerformance: (arr: T[], key: K extends keyof T ? (T[K] extends number ? K : never) : never) => number = (arr, key) => { + const first = arr.length > 0 ? arr[0] : undefined; + const last = arr.length > 0 ? arr[arr.length - 1] : undefined; + if (!first || !last) { + return 0; + } + const firstValue = first[key]; + const lastValue = last[key]; + if (typeof firstValue !== 'number' || typeof lastValue !== 'number') { + return 0; + } + return parseFloat(((lastValue - firstValue) / firstValue * 100).toFixed(2)); + } + + return ( + + + Portfolio performance + Portfolio performance for the last week + + + + + + { + const date = new Date(value) + return date.toLocaleDateString("en-US", { + month: "short", + day: "numeric", + }) + }} + /> + { + const cents = Number(value); + return (cents / 100).toLocaleString("en-US", { + style: "currency", + currency: "USD" + }) + }} + /> + ( + <> +
+ {((chartConfig["name" as keyof typeof chartConfig]?.label || name) as string) + .replace(/^./, c => c.toUpperCase())} +
+ {(Number(value) / 100).toLocaleString("en-US", { + style: "currency", + currency: "USD" + })} +
+ + )} + />} + /> + + + + + +
+ Trending up by {calculatePerformance(chartData, "portfolio")}% this week { + calculatePerformance(chartData, "portfolio") > 0 + ? + : + } +
+
+ Showing total performance of the overall portfolio holdings +
+
+ + ) +} + + interface AssetHolding { assetId: string; mic: string; @@ -156,6 +282,7 @@ function toUSD(cents: number) { function Portfolio() { const [holdings, setHoldings] = useState([]); const [assetHoldings, setAssetHoldings] = useState([]); + const [performanceChartData, setPerformanceChartData] = useState([]); const percentageChange = (actual: number, original: number): string => { const numericChange = ((actual - original) / original) * 100; @@ -185,6 +312,30 @@ function Portfolio() { setHoldings(prevItems => prevItems.filter(item => item.assetId !== assetId)); } + const createPerformanceChartData = async (holdings: AssetHolding[]): Promise => { + const entryCollection: ChartData[] = []; + + const sevenDaysAgo = startOfDay(new Date(Date.now() - 7 * 24 * 60 * 60 * 1000)); + const end = startOfDay(new Date(Date.now())); + for ( + let current = new Date(sevenDaysAgo); + current <= end; + current = addDays(current, 1) + ) { + const values = await Promise.all(holdings.map(async holding => { + const price = await assetsHttpService.getPrice(holding.assetId, current) + return price.price * holding.shares; + })) + const totalValue = values.reduce((acc, curr) => acc + curr, 0); + entryCollection.push({ + date: current.toISOString().split('T')[0], + portfolio: totalValue + }) + } + + return entryCollection; + } + useEffect(() => { async function fetchInitialData(): Promise { const holdings = await portfolioHttpService.getPortfolioHoldings(); @@ -214,6 +365,7 @@ function Portfolio() { } as AssetHolding; })); setAssetHoldings(assets); + createPerformanceChartData(assets).then(setPerformanceChartData) } fetchInitialData().then(); @@ -221,7 +373,8 @@ function Portfolio() { return ( <> - + +
diff --git a/src/pages/webapp/Simulation.tsx b/src/pages/webapp/Simulation.tsx index 0732dd1..0a4f4f6 100644 --- a/src/pages/webapp/Simulation.tsx +++ b/src/pages/webapp/Simulation.tsx @@ -407,7 +407,6 @@ function DataChart tickLine={false} axisLine={false} tickMargin={8} - tickCount={3} tickFormatter={(value) => { const cents = Number(value); return (cents / 100).toLocaleString("en-US", {