"use client"; import { Sparkline } from "@mantine/charts"; import { Badge, Box, Flex, Group, Paper, SimpleGrid, Stack, Text, Title, useMantineTheme } from "@mantine/core"; import { IconMinus, IconTrendingDown, IconTrendingUp } from "@tabler/icons-react"; import { clientApi } from "@homarr/api/client"; import { useCurrentIntlLocale, useScopedI18n } from "@homarr/translation/client"; import { WidgetEmptyState } from "../common/empty-state"; import { getUsableWidgetQueryData, isInitialWidgetQueryPending } from "../common/query-state"; import { WidgetQueryErrorIndicator, WidgetQueryLoadingState } from "../common/query-state-indicator"; import type { WidgetComponentProps } from "../definition"; function round(value: number) { return Math.round(value * 100) / 100; } function calculateChange(currentPrice: number, previousClose: number) { return currentPrice - previousClose; } function calculateChangePercentage(currentPrice: number, previousClose: number) { if (previousClose === 0) return null; return 100 * ((currentPrice - previousClose) / previousClose); } export interface StockSummary { currentPrice: number; previousClose: number; change: number; changePercentage: number | null; minimum: number; maximum: number; graphValues: number[]; } export interface StockLayout { showName: boolean; showChange: boolean; showRange: boolean; graphHeight: "48%" | "68%" | "75%"; priceOrder: 1 | 2; } export function getStockLayout(width: number, height: number, showDetails: boolean): StockLayout { return { showName: showDetails && width >= 300 && height >= 190, showChange: showDetails && width >= 260 && height >= 130, showRange: showDetails && width >= 220 && height >= 130, graphHeight: height >= 320 ? "75%" : height >= 220 ? "68%" : "48%", priceOrder: width >= 280 && height >= 120 ? 1 : 2, }; } export function getStockSummary(priceHistory: number[], previousClose: number): StockSummary | null { const currentPrice = priceHistory.at(-1); if (currentPrice === undefined) return null; const minimum = Math.min(...priceHistory); return { currentPrice, previousClose, change: round(calculateChange(currentPrice, previousClose)), changePercentage: calculateChangePercentage(currentPrice, previousClose), minimum, maximum: Math.max(...priceHistory), graphValues: priceHistory.map((value) => value - minimum + 50), }; } export default function StockPriceWidget({ options, width, height, displayMode = "compact", }: WidgetComponentProps<"stockPrice">) { const t = useScopedI18n("widget.stockPrice"); const locale = useCurrentIntlLocale(); const numberFormatter = new Intl.NumberFormat(locale); const theme = useMantineTheme(); const stockQuery = clientApi.widget.stockPrice.getPriceHistory.useQuery(options); const result = getUsableWidgetQueryData(stockQuery); if (isInitialWidgetQueryPending(stockQuery)) return ; if (!result) return ; const { data } = result; const summary = getStockSummary(data.priceHistory, data.previousClose); if (!summary) return ; const stockValuesChange = summary.change; const stockValuesChangePercentage = summary.changePercentage === null ? null : round(summary.changePercentage); const stockGraphValues = summary.graphValues; const trendColor = stockValuesChange > 0 ? "green.7" : stockValuesChange < 0 ? "red.7" : "gray.6"; const layout = getStockLayout(width, height, options.showDetails); const formatValue = (value: number) => numberFormatter.format(round(value)); const formatSignedValue = (value: number) => `${value > 0 ? "+" : ""}${formatValue(value)}`; const content = displayMode === "advanced" ? ( {data.symbol} {data.shortName} {t("option.timeRange.label")}: {t(`option.timeRange.option.${options.timeRange}.label`)} {t("option.timeInterval.label")}: {t(`option.timeInterval.option.${options.timeInterval}.label`)} = 840 ? 6 : width >= 480 ? 3 : 2} spacing="xs"> ) : ( {stockValuesChange > 0 ? ( ) : stockValuesChange < 0 ? ( ) : ( )} {data.symbol} {layout.showName && ( {data.shortName} )} {formatValue(summary.currentPrice)} {layout.showChange && ( {numberFormatter.format(stockValuesChange)} {stockValuesChangePercentage === null ? null : ` (${stockValuesChange > 0 ? "+" : ""}${numberFormatter.format(stockValuesChangePercentage)}%)`} )} {layout.showRange && ( {t(`option.timeRange.option.${options.timeRange}.label`)} )} ); return ( {content} {stockQuery.error && ( )} ); } function StockMetric({ label, value, color }: { label: string; value: string; color?: string }) { return ( {label} {value} ); }