"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}
);
}