feat: modernize dashboard widgets
This commit is contained in:
1 parent
1a5e083fd0
commit
6f2cd7fc2b
270 files changed
+10189
-3544
No files matched your search
@@ -0,0 +1,18 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { getStockSummary } from "./component";
|
||||
|
||||
describe("stock summary", () => {
|
||||
it("returns no summary for an empty series", () => {
|
||||
expect(getStockSummary([], 10)).toBeNull();
|
||||
});
|
||||
|
||||
it("does not divide by a zero previous close", () => {
|
||||
expect(getStockSummary([0, 5], 0)).toMatchObject({
|
||||
currentPrice: 5,
|
||||
change: 5,
|
||||
changePercentage: null,
|
||||
graphValues: [50, 55],
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -19,10 +19,31 @@ function calculateChange(currentPrice: number, previousClose: number) {
|
||||
}
|
||||
|
||||
function calculateChangePercentage(currentPrice: number, previousClose: number) {
|
||||
if (previousClose === 0) return null;
|
||||
return 100 * ((currentPrice - previousClose) / previousClose);
|
||||
}
|
||||
|
||||
export default function StockPriceWidget({ options, width, height }: WidgetComponentProps<"stockPrice">) {
|
||||
export interface StockSummary {
|
||||
currentPrice: number;
|
||||
change: number;
|
||||
changePercentage: number | null;
|
||||
graphValues: number[];
|
||||
}
|
||||
|
||||
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,
|
||||
change: round(calculateChange(currentPrice, previousClose)),
|
||||
changePercentage: calculateChangePercentage(currentPrice, previousClose),
|
||||
graphValues: priceHistory.map((value) => value - minimum + 50),
|
||||
};
|
||||
}
|
||||
|
||||
export default function StockPriceWidget({ options, width, height, displayMode }: WidgetComponentProps<"stockPrice">) {
|
||||
const t = useScopedI18n("widget.stockPrice");
|
||||
const theme = useMantineTheme();
|
||||
const { data: result } = clientApi.widget.stockPrice.getPriceHistory.useQuery(options);
|
||||
@@ -30,13 +51,11 @@ export default function StockPriceWidget({ options, width, height }: WidgetCompo
|
||||
if (!result) return <WidgetEmptyState />;
|
||||
const { data } = result;
|
||||
|
||||
const stockValuesChange = round(calculateChange(data.priceHistory.at(-1) ?? 0, data.previousClose));
|
||||
const stockValuesChangePercentage = round(
|
||||
calculateChangePercentage(data.priceHistory.at(-1) ?? 0, data.previousClose),
|
||||
);
|
||||
|
||||
const stockValuesMin = Math.min(...data.priceHistory);
|
||||
const stockGraphValues = data.priceHistory.map((value) => value - stockValuesMin + 50);
|
||||
const summary = getStockSummary(data.priceHistory, data.previousClose);
|
||||
if (!summary) return <WidgetEmptyState />;
|
||||
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";
|
||||
|
||||
return (
|
||||
@@ -45,7 +64,7 @@ export default function StockPriceWidget({ options, width, height }: WidgetCompo
|
||||
pos="absolute"
|
||||
bottom={10}
|
||||
w="100%"
|
||||
h={height > 280 ? "75%" : "50%"}
|
||||
h={displayMode === "advanced" || height > 280 ? "75%" : "50%"}
|
||||
data={stockGraphValues}
|
||||
curveType="linear"
|
||||
color={trendColor}
|
||||
@@ -62,7 +81,7 @@ export default function StockPriceWidget({ options, width, height }: WidgetCompo
|
||||
)}
|
||||
{data.symbol}
|
||||
</Text>
|
||||
{width > 280 && height > 280 && (
|
||||
{(displayMode === "advanced" || (width > 280 && height > 280)) && (
|
||||
<Text size="md" lh="1">
|
||||
{data.shortName}
|
||||
</Text>
|
||||
@@ -70,37 +89,23 @@ export default function StockPriceWidget({ options, width, height }: WidgetCompo
|
||||
</Stack>
|
||||
|
||||
<Title pos="absolute" bottom={10} right={10} order={width > 280 ? 1 : 2} fw={700}>
|
||||
{new Intl.NumberFormat().format(round(data.priceHistory.at(-1) ?? 0))}
|
||||
{new Intl.NumberFormat().format(round(summary.currentPrice))}
|
||||
</Title>
|
||||
|
||||
{width > 280 && (
|
||||
{(displayMode === "advanced" || width > 280) && (
|
||||
<Text pos="absolute" top={10} right={10} size="xl" fw={700}>
|
||||
{new Intl.NumberFormat().format(stockValuesChange)} ({stockValuesChange > 0 ? "+" : ""}
|
||||
{new Intl.NumberFormat().format(stockValuesChangePercentage)}%)
|
||||
{new Intl.NumberFormat().format(stockValuesChange)}
|
||||
{stockValuesChangePercentage === null
|
||||
? null
|
||||
: ` (${stockValuesChange > 0 ? "+" : ""}${new Intl.NumberFormat().format(stockValuesChangePercentage)}%)`}
|
||||
</Text>
|
||||
)}
|
||||
|
||||
{width > 280 && (
|
||||
{(displayMode === "advanced" || width > 280) && (
|
||||
<Text pos="absolute" bottom={10} left={10} fw={700}>
|
||||
{t(`option.timeRange.option.${options.timeRange}.label`)}
|
||||
</Text>
|
||||
)}
|
||||
|
||||
<Stack pos="absolute" top={10} left={10}>
|
||||
<Text size="xl" fw={700} lh="0.715">
|
||||
{stockValuesChange > 0 ? (
|
||||
<IconTrendingUp size="1.5rem" color={theme.colors.green[7]} />
|
||||
) : (
|
||||
<IconTrendingDown size="1.5rem" color={theme.colors.red[7]} />
|
||||
)}
|
||||
{data.symbol}
|
||||
</Text>
|
||||
{width > 280 && height > 280 && (
|
||||
<Text size="md" lh="1">
|
||||
{data.shortName}
|
||||
</Text>
|
||||
)}
|
||||
</Stack>
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
@@ -13,6 +13,7 @@ const timeIntervalOptions = stockPriceTimeFrames.interval;
|
||||
|
||||
export const { definition, componentLoader } = createWidgetDefinition("stockPrice", {
|
||||
icon: IconBuildingBank,
|
||||
queryKey: [["widget", "stockPrice"]],
|
||||
refetchInterval: null,
|
||||
createOptions() {
|
||||
return optionsBuilder.from((factory) => ({
|
||||
|
||||
Reference in new issue
Block a user