feat: stock price widget (#2579)
* feat: added stock price widget * fix: formatting * fix: broken lock file * fix: requested changes * fix: added parsing schema * fix: improve time range and interval inputs * fix: only return required data * fix: formatting * fix: deepsource tests * fix: moved all time frames into one location * fix: formatting * fix: requested changes * fix: formatting * fix: parse response data * fix: update packages * fix: typescript issues * fix: formatting * fix: broken lockfile --------- Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
This commit is contained in:
@@ -29,6 +29,7 @@ import type { WidgetOptionDefinition } from "./options";
|
||||
import * as rssFeed from "./rssFeed";
|
||||
import * as smartHomeEntityState from "./smart-home/entity-state";
|
||||
import * as smartHomeExecuteAutomation from "./smart-home/execute-automation";
|
||||
import * as stockPrice from "./stocks";
|
||||
import * as video from "./video";
|
||||
import * as weather from "./weather";
|
||||
|
||||
@@ -46,6 +47,7 @@ export const widgetImports = {
|
||||
dnsHoleControls,
|
||||
"smartHome-entityState": smartHomeEntityState,
|
||||
"smartHome-executeAutomation": smartHomeExecuteAutomation,
|
||||
stockPrice,
|
||||
mediaServer,
|
||||
calendar,
|
||||
downloads,
|
||||
|
||||
102
packages/widgets/src/stocks/component.tsx
Normal file
102
packages/widgets/src/stocks/component.tsx
Normal file
@@ -0,0 +1,102 @@
|
||||
"use client";
|
||||
|
||||
import { Sparkline } from "@mantine/charts";
|
||||
import { Flex, Stack, Text, Title, useMantineTheme } from "@mantine/core";
|
||||
import { IconTrendingDown, IconTrendingUp } from "@tabler/icons-react";
|
||||
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
import { useScopedI18n } from "@homarr/translation/client";
|
||||
|
||||
import type { WidgetComponentProps } from "../definition";
|
||||
|
||||
function round(value: number) {
|
||||
return Math.round(value * 100) / 100;
|
||||
}
|
||||
|
||||
function calculateChange(valueA: number, valueB: number) {
|
||||
return valueA - valueB;
|
||||
}
|
||||
|
||||
function calculateChangePercentage(valueA: number, valueB: number) {
|
||||
return 100 * ((valueA - valueB) / valueA);
|
||||
}
|
||||
|
||||
export default function StockPriceWidget({ options, width, height }: WidgetComponentProps<"stockPrice">) {
|
||||
const t = useScopedI18n("widget.stockPrice");
|
||||
const theme = useMantineTheme();
|
||||
const [{ data }] = clientApi.widget.stockPrice.getPriceHistory.useSuspenseQuery(options);
|
||||
|
||||
const stockValues = data.indicators.quote[0]?.close ?? [];
|
||||
|
||||
const stockValuesChange = round(calculateChange(stockValues[stockValues.length - 1] ?? 0, stockValues[0] ?? 0));
|
||||
const stockValuesChangePercentage = round(
|
||||
calculateChangePercentage(stockValues[stockValues.length - 1] ?? 0, stockValues[0] ?? 0),
|
||||
);
|
||||
|
||||
const stockValuesMin = Math.min(...stockValues);
|
||||
const stockGraphValues = stockValues.map((value) => value - stockValuesMin + 50);
|
||||
|
||||
return (
|
||||
<Flex h="100%" w="100%">
|
||||
<Sparkline
|
||||
pos="absolute"
|
||||
bottom={10}
|
||||
w="100%"
|
||||
h={height > 280 ? "75%" : "50%"}
|
||||
data={stockGraphValues}
|
||||
curveType="linear"
|
||||
trendColors={{ positive: "green.7", negative: "red.7", neutral: "gray.6" }}
|
||||
fillOpacity={0.6}
|
||||
strokeWidth={2.5}
|
||||
/>
|
||||
|
||||
<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.meta.symbol}
|
||||
</Text>
|
||||
{width > 280 && height > 280 && (
|
||||
<Text size="md" lh="1">
|
||||
{data.meta.shortName}
|
||||
</Text>
|
||||
)}
|
||||
</Stack>
|
||||
|
||||
<Title pos="absolute" bottom={10} right={10} order={width > 280 ? 1 : 2} fw={700}>
|
||||
{round(stockValues[stockValues.length - 1] ?? 0)}
|
||||
</Title>
|
||||
|
||||
{width > 280 && (
|
||||
<Text pos="absolute" top={10} right={10} size="xl" fw={700}>
|
||||
{Math.abs(stockValuesChange)} ({Math.abs(stockValuesChangePercentage)}%)
|
||||
</Text>
|
||||
)}
|
||||
|
||||
{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.meta.symbol}
|
||||
</Text>
|
||||
{width > 280 && height > 280 && (
|
||||
<Text size="md" lh="1">
|
||||
{data.meta.shortName}
|
||||
</Text>
|
||||
)}
|
||||
</Stack>
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
37
packages/widgets/src/stocks/index.ts
Normal file
37
packages/widgets/src/stocks/index.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { IconBuildingBank } from "@tabler/icons-react";
|
||||
|
||||
import { createWidgetDefinition } from "../definition";
|
||||
import { optionsBuilder } from "../options";
|
||||
|
||||
export const stockPriceTimeFrames = {
|
||||
range: ["1d", "5d", "1mo", "3mo", "6mo", "ytd", "1y", "2y", "5y", "10y", "max"] as const,
|
||||
interval: ["5m", "15m", "30m", "1h", "1d", "5d", "1wk", "1mo"] as const,
|
||||
};
|
||||
|
||||
const timeRangeOptions = stockPriceTimeFrames.range;
|
||||
const timeIntervalOptions = stockPriceTimeFrames.interval;
|
||||
|
||||
export const { definition, componentLoader } = createWidgetDefinition("stockPrice", {
|
||||
icon: IconBuildingBank,
|
||||
createOptions() {
|
||||
return optionsBuilder.from((factory) => ({
|
||||
stock: factory.text({
|
||||
defaultValue: "AAPL",
|
||||
}),
|
||||
timeRange: factory.select({
|
||||
defaultValue: "1mo",
|
||||
options: timeRangeOptions.map((value) => ({
|
||||
value,
|
||||
label: (t) => t(`widget.stockPrice.option.timeRange.option.${value}.label`),
|
||||
})),
|
||||
}),
|
||||
timeInterval: factory.select({
|
||||
defaultValue: "1d",
|
||||
options: timeIntervalOptions.map((value) => ({
|
||||
value,
|
||||
label: (t) => t(`widget.stockPrice.option.timeInterval.option.${value}.label`),
|
||||
})),
|
||||
}),
|
||||
}));
|
||||
},
|
||||
}).withDynamicImport(() => import("./component"));
|
||||
Reference in New Issue
Block a user