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:
Jack Weller
2025-03-21 02:49:19 +10:00
committed by GitHub
parent e3fcfbe916
commit 91a69c162a
10 changed files with 547 additions and 0 deletions

View File

@@ -13,6 +13,7 @@ import { notebookRouter } from "./notebook";
import { optionsRouter } from "./options";
import { rssFeedRouter } from "./rssFeed";
import { smartHomeRouter } from "./smart-home";
import { stockPriceRouter } from "./stocks";
import { weatherRouter } from "./weather";
export const widgetRouter = createTRPCRouter({
@@ -21,6 +22,7 @@ export const widgetRouter = createTRPCRouter({
app: appRouter,
dnsHole: dnsHoleRouter,
smartHome: smartHomeRouter,
stockPrice: stockPriceRouter,
mediaServer: mediaServerRouter,
calendar: calendarRouter,
downloads: downloadsRouter,

View File

@@ -0,0 +1,23 @@
import { z } from "zod";
import { fetchStockPriceHandler } from "@homarr/request-handler/stock-price";
import { stockPriceTimeFrames } from "../../../../widgets/src/stocks";
import { createTRPCRouter, publicProcedure } from "../../trpc";
const stockPriceInputSchema = z.object({
stock: z.string().nonempty(),
timeRange: z.enum(stockPriceTimeFrames.range),
timeInterval: z.enum(stockPriceTimeFrames.interval),
});
export const stockPriceRouter = createTRPCRouter({
getPriceHistory: publicProcedure.input(stockPriceInputSchema).query(async ({ input }) => {
const innerHandler = fetchStockPriceHandler.handler({
stock: input.stock,
timeRange: input.timeRange,
timeInterval: input.timeInterval,
});
return await innerHandler.getCachedOrUpdatedDataAsync({ forceUpdate: false });
}),
});

View File

@@ -9,6 +9,7 @@ export const widgetKinds = [
"dnsHoleControls",
"smartHome-entityState",
"smartHome-executeAutomation",
"stockPrice",
"mediaServer",
"calendar",
"downloads",

View File

@@ -0,0 +1,58 @@
import dayjs from "dayjs";
import { z } from "zod";
import { fetchWithTimeout } from "@homarr/common";
import { createCachedWidgetRequestHandler } from "./lib/cached-widget-request-handler";
export const fetchStockPriceHandler = createCachedWidgetRequestHandler({
queryKey: "fetchStockPriceResult",
widgetKind: "stockPrice",
async requestAsync(input: { stock: string; timeRange: string; timeInterval: string }) {
const response = await fetchWithTimeout(
`https://query1.finance.yahoo.com/v8/finance/chart/${input.stock}?range=${input.timeRange}&interval=${input.timeInterval}`,
);
const data = dataSchema.parse(await response.json());
if ("error" in data) {
throw new Error(data.error.description);
}
if (data.chart.result.length !== 1) {
throw new Error("Received multiple results");
}
if (!data.chart.result[0]) {
throw new Error("Received invalid data");
}
return data.chart.result[0];
},
cacheDuration: dayjs.duration(5, "minutes"),
});
const dataSchema = z
.object({
error: z.object({
description: z.string(),
}),
})
.or(
z.object({
chart: z.object({
result: z.array(
z.object({
indicators: z.object({
quote: z.array(
z.object({
close: z.array(z.number()),
}),
),
}),
meta: z.object({
symbol: z.string(),
shortName: z.string(),
}),
}),
),
}),
}),
);

View File

@@ -1424,6 +1424,82 @@
"run": "Run {name}"
}
},
"stockPrice": {
"name": "Stock Price",
"description": "Displays the current stock price of a company",
"option": {
"stock": {
"label": "Stock symbol"
},
"timeRange": {
"label": "Time Range",
"option": {
"1d": {
"label": "1 Day"
},
"5d": {
"label": "5 Day"
},
"1mo": {
"label": "1 Month"
},
"3mo": {
"label": "3 Months"
},
"6mo": {
"label": "6 Months"
},
"ytd": {
"label": "Year to Date"
},
"1y": {
"label": "1 Year"
},
"2y": {
"label": "2 Years"
},
"5y": {
"label": "5 Years"
},
"10y": {
"label": "10 Years"
},
"max": {
"label": "Max"
}
}
},
"timeInterval": {
"label": "Time Interval",
"option": {
"5m": {
"label": "5 Minutes"
},
"15m": {
"label": "15 Minutes"
},
"30m": {
"label": "30 Minutes"
},
"1h": {
"label": "1 Hour"
},
"1d": {
"label": "1 Day"
},
"5d": {
"label": "5 Days"
},
"1wk": {
"label": "1 Week"
},
"1mo": {
"label": "1 Month"
}
}
}
}
},
"calendar": {
"name": "Calendar",
"description": "Display events from your integrations in a calendar view within a certain relative time period",

View File

@@ -44,6 +44,7 @@
"@homarr/translation": "workspace:^0.1.0",
"@homarr/ui": "workspace:^0.1.0",
"@homarr/validation": "workspace:^0.1.0",
"@mantine/charts": "^7.17.2",
"@mantine/core": "^7.17.2",
"@mantine/hooks": "^7.17.2",
"@tabler/icons-react": "^3.31.0",
@@ -68,6 +69,7 @@
"next": "15.1.7",
"react": "19.0.0",
"react-dom": "19.0.0",
"recharts": "^2.15.1",
"video.js": "^8.22.0",
"zod": "^3.24.2"
},

View File

@@ -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,

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

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

244
pnpm-lock.yaml generated
View File

@@ -2072,6 +2072,9 @@ importers:
'@homarr/validation':
specifier: workspace:^0.1.0
version: link:../validation
'@mantine/charts':
specifier: ^7.17.2
version: 7.17.2(@mantine/core@7.17.2(@mantine/hooks@7.17.2(react@19.0.0))(@types/react@19.0.12)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@mantine/hooks@7.17.2(react@19.0.0))(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(recharts@2.15.1(react-dom@19.0.0(react@19.0.0))(react@19.0.0))
'@mantine/core':
specifier: ^7.17.2
version: 7.17.2(@mantine/hooks@7.17.2(react@19.0.0))(@types/react@19.0.12)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
@@ -2144,6 +2147,9 @@ importers:
react-dom:
specifier: 19.0.0
version: 19.0.0(react@19.0.0)
recharts:
specifier: ^2.15.1
version: 2.15.1(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
video.js:
specifier: ^8.22.0
version: 8.22.0
@@ -3514,6 +3520,15 @@ packages:
'@libsql/core@0.14.0':
resolution: {integrity: sha512-nhbuXf7GP3PSZgdCY2Ecj8vz187ptHlZQ0VRc751oB2C1W8jQUXKKklvt7t1LJiUTQBVJuadF628eUk+3cRi4Q==}
'@mantine/charts@7.17.2':
resolution: {integrity: sha512-ckB23pIqRjzysUz2EiWZD9AVyf7t0r7o7zfJbl01nzOezFgYq5RGeRoxvpcsfBC+YoSbB/43rjNcXtYhtA7QzA==}
peerDependencies:
'@mantine/core': 7.17.2
'@mantine/hooks': 7.17.2
react: ^18.x || ^19.x
react-dom: ^18.x || ^19.x
recharts: ^2.13.3
'@mantine/colors-generator@7.17.2':
resolution: {integrity: sha512-wn4qmefWyQO9424nenN3k/zYcN4kPut1LVdv1ZyQ0Bz1giKc3PKTb96OOMzWlPCW08WjK/nwa2/VczC7YVKcQQ==}
peerDependencies:
@@ -4657,6 +4672,33 @@ packages:
'@types/css-modules@1.0.5':
resolution: {integrity: sha512-oeKafs/df9lwOvtfiXVliZsocFVOexK9PZtLQWuPeuVCFR7jwiqlg60lu80JTe5NFNtH3tnV6Fs/ySR8BUPHAw==}
'@types/d3-array@3.2.1':
resolution: {integrity: sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg==}
'@types/d3-color@3.1.3':
resolution: {integrity: sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==}
'@types/d3-ease@3.0.2':
resolution: {integrity: sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==}
'@types/d3-interpolate@3.0.4':
resolution: {integrity: sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==}
'@types/d3-path@3.1.1':
resolution: {integrity: sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==}
'@types/d3-scale@4.0.9':
resolution: {integrity: sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==}
'@types/d3-shape@3.1.7':
resolution: {integrity: sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==}
'@types/d3-time@3.0.4':
resolution: {integrity: sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==}
'@types/d3-timer@3.0.2':
resolution: {integrity: sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==}
'@types/docker-modem@3.0.6':
resolution: {integrity: sha512-yKpAGEuKRSS8wwx0joknWxsmLha78wNMe9R2S3UNsVOkZded8UqOrV8KoeDXoXsjndxwyF3eIhyClGbO1SEhEg==}
@@ -5766,6 +5808,50 @@ packages:
csstype@3.1.3:
resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==}
d3-array@3.2.4:
resolution: {integrity: sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==}
engines: {node: '>=12'}
d3-color@3.1.0:
resolution: {integrity: sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==}
engines: {node: '>=12'}
d3-ease@3.0.1:
resolution: {integrity: sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==}
engines: {node: '>=12'}
d3-format@3.1.0:
resolution: {integrity: sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==}
engines: {node: '>=12'}
d3-interpolate@3.0.1:
resolution: {integrity: sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==}
engines: {node: '>=12'}
d3-path@3.1.0:
resolution: {integrity: sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==}
engines: {node: '>=12'}
d3-scale@4.0.2:
resolution: {integrity: sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==}
engines: {node: '>=12'}
d3-shape@3.2.0:
resolution: {integrity: sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==}
engines: {node: '>=12'}
d3-time-format@4.1.0:
resolution: {integrity: sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==}
engines: {node: '>=12'}
d3-time@3.1.0:
resolution: {integrity: sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==}
engines: {node: '>=12'}
d3-timer@3.0.1:
resolution: {integrity: sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==}
engines: {node: '>=12'}
damerau-levenshtein@1.0.8:
resolution: {integrity: sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==}
@@ -5830,6 +5916,9 @@ packages:
supports-color:
optional: true
decimal.js-light@2.5.1:
resolution: {integrity: sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==}
decimal.js@10.4.3:
resolution: {integrity: sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==}
@@ -6399,6 +6488,9 @@ packages:
resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==}
engines: {node: '>=6'}
eventemitter3@4.0.7:
resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==}
events@3.3.0:
resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==}
engines: {node: '>=0.8.x'}
@@ -6437,6 +6529,10 @@ packages:
fast-deep-equal@3.1.3:
resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
fast-equals@5.2.2:
resolution: {integrity: sha512-V7/RktU11J3I36Nwq2JnZEM7tNm17eBJz+u25qdxBZeCKiX6BkVSZQjwWIr+IobgnZy+ag73tTZgZi7tr0LrBw==}
engines: {node: '>=6.0.0'}
fast-fifo@1.3.2:
resolution: {integrity: sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==}
@@ -6999,6 +7095,10 @@ packages:
resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==}
engines: {node: '>= 0.4'}
internmap@2.0.3:
resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==}
engines: {node: '>=12'}
intl-messageformat@10.7.1:
resolution: {integrity: sha512-xQuJW2WcyzNJZWUu5xTVPOmNSA1Sowuu/NKFdUid5Fxx/Yl6/s4DefTU/y7zy+irZLDmFGmTLtnM8FqpN05wlA==}
@@ -8711,6 +8811,9 @@ packages:
react-is@16.13.1:
resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
react-is@18.3.1:
resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==}
react-number-format@5.4.3:
resolution: {integrity: sha512-VCY5hFg/soBighAoGcdE+GagkJq0230qN6jcS5sp8wQX1qy1fYN/RX7/BXkrs0oyzzwqR8/+eSUrqXbGeywdUQ==}
peerDependencies:
@@ -8763,6 +8866,12 @@ packages:
react: '>=16.8.0'
react-dom: '>=16.8.0'
react-smooth@4.0.4:
resolution: {integrity: sha512-gnGKTpYwqL0Iii09gHobNolvX4Kiq4PKx6eWBCYYix+8cdw+cGo3do906l1NBPKkSWx1DghC1dlWG9L2uGd61Q==}
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
react-style-singleton@2.2.3:
resolution: {integrity: sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==}
engines: {node: '>=10'}
@@ -8824,6 +8933,16 @@ packages:
resolution: {integrity: sha512-YTUo+Flmw4ZXiWfQKGcwwc11KnoRAYgzAE2E7mXKCjSviTKShtxBsN6YUUBB2gtaBzKzeKunxhUwNHQuRryhWA==}
engines: {node: '>= 4'}
recharts-scale@0.4.5:
resolution: {integrity: sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w==}
recharts@2.15.1:
resolution: {integrity: sha512-v8PUTUlyiDe56qUj82w/EDVuzEFXwEHp9/xOowGAZwfLjB9uAy3GllQVIYMWF6nU+qibx85WF75zD7AjqoT54Q==}
engines: {node: '>=14'}
peerDependencies:
react: ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
react-dom: ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
redis-errors@1.2.0:
resolution: {integrity: sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==}
engines: {node: '>=4'}
@@ -10060,6 +10179,9 @@ packages:
resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==}
engines: {node: '>= 0.8'}
victory-vendor@36.9.2:
resolution: {integrity: sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==}
video.js@8.22.0:
resolution: {integrity: sha512-xge2kpjsvC0zgFJ1cqt+wTqsi21+huFswlonPFh7qiplypsb4FN/D2Rz6bWdG/S9eQaPHfWHsarmJL/7D3DHoA==}
@@ -11420,6 +11542,14 @@ snapshots:
js-base64: 3.7.7
optional: true
'@mantine/charts@7.17.2(@mantine/core@7.17.2(@mantine/hooks@7.17.2(react@19.0.0))(@types/react@19.0.12)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@mantine/hooks@7.17.2(react@19.0.0))(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(recharts@2.15.1(react-dom@19.0.0(react@19.0.0))(react@19.0.0))':
dependencies:
'@mantine/core': 7.17.2(@mantine/hooks@7.17.2(react@19.0.0))(@types/react@19.0.12)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
'@mantine/hooks': 7.17.2(react@19.0.0)
react: 19.0.0
react-dom: 19.0.0(react@19.0.0)
recharts: 2.15.1(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
'@mantine/colors-generator@7.17.2(chroma-js@3.1.2)':
dependencies:
chroma-js: 3.1.2
@@ -12906,6 +13036,30 @@ snapshots:
'@types/css-modules@1.0.5': {}
'@types/d3-array@3.2.1': {}
'@types/d3-color@3.1.3': {}
'@types/d3-ease@3.0.2': {}
'@types/d3-interpolate@3.0.4':
dependencies:
'@types/d3-color': 3.1.3
'@types/d3-path@3.1.1': {}
'@types/d3-scale@4.0.9':
dependencies:
'@types/d3-time': 3.0.4
'@types/d3-shape@3.1.7':
dependencies:
'@types/d3-path': 3.1.1
'@types/d3-time@3.0.4': {}
'@types/d3-timer@3.0.2': {}
'@types/docker-modem@3.0.6':
dependencies:
'@types/node': 22.13.10
@@ -14198,6 +14352,44 @@ snapshots:
csstype@3.1.3: {}
d3-array@3.2.4:
dependencies:
internmap: 2.0.3
d3-color@3.1.0: {}
d3-ease@3.0.1: {}
d3-format@3.1.0: {}
d3-interpolate@3.0.1:
dependencies:
d3-color: 3.1.0
d3-path@3.1.0: {}
d3-scale@4.0.2:
dependencies:
d3-array: 3.2.4
d3-format: 3.1.0
d3-interpolate: 3.0.1
d3-time: 3.1.0
d3-time-format: 4.1.0
d3-shape@3.2.0:
dependencies:
d3-path: 3.1.0
d3-time-format@4.1.0:
dependencies:
d3-time: 3.1.0
d3-time@3.1.0:
dependencies:
d3-array: 3.2.4
d3-timer@3.0.1: {}
damerau-levenshtein@1.0.8: {}
data-uri-to-buffer@6.0.2: {}
@@ -14257,6 +14449,8 @@ snapshots:
dependencies:
ms: 2.1.3
decimal.js-light@2.5.1: {}
decimal.js@10.4.3: {}
decompress-response@6.0.0:
@@ -15030,6 +15224,8 @@ snapshots:
event-target-shim@5.0.1: {}
eventemitter3@4.0.7: {}
events@3.3.0: {}
execa@5.1.1:
@@ -15089,6 +15285,8 @@ snapshots:
fast-deep-equal@3.1.3: {}
fast-equals@5.2.2: {}
fast-fifo@1.3.2: {}
fast-glob@3.3.1:
@@ -15707,6 +15905,8 @@ snapshots:
hasown: 2.0.2
side-channel: 1.1.0
internmap@2.0.3: {}
intl-messageformat@10.7.1:
dependencies:
'@formatjs/ecma402-abstract': 2.2.0
@@ -17404,6 +17604,8 @@ snapshots:
react-is@16.13.1: {}
react-is@18.3.1: {}
react-number-format@5.4.3(react-dom@19.0.0(react@19.0.0))(react@19.0.0):
dependencies:
react: 19.0.0
@@ -17452,6 +17654,14 @@ snapshots:
react: 19.0.0
react-dom: 19.0.0(react@19.0.0)
react-smooth@4.0.4(react-dom@19.0.0(react@19.0.0))(react@19.0.0):
dependencies:
fast-equals: 5.2.2
prop-types: 15.8.1
react: 19.0.0
react-dom: 19.0.0(react@19.0.0)
react-transition-group: 4.4.5(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
react-style-singleton@2.2.3(@types/react@19.0.12)(react@19.0.0):
dependencies:
get-nonce: 1.0.1
@@ -17542,6 +17752,23 @@ snapshots:
tiny-invariant: 1.3.3
tslib: 2.8.1
recharts-scale@0.4.5:
dependencies:
decimal.js-light: 2.5.1
recharts@2.15.1(react-dom@19.0.0(react@19.0.0))(react@19.0.0):
dependencies:
clsx: 2.1.1
eventemitter3: 4.0.7
lodash: 4.17.21
react: 19.0.0
react-dom: 19.0.0(react@19.0.0)
react-is: 18.3.1
react-smooth: 4.0.4(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
recharts-scale: 0.4.5
tiny-invariant: 1.3.3
victory-vendor: 36.9.2
redis-errors@1.2.0: {}
redis-parser@3.0.0:
@@ -19008,6 +19235,23 @@ snapshots:
vary@1.1.2: {}
victory-vendor@36.9.2:
dependencies:
'@types/d3-array': 3.2.1
'@types/d3-ease': 3.0.2
'@types/d3-interpolate': 3.0.4
'@types/d3-scale': 4.0.9
'@types/d3-shape': 3.1.7
'@types/d3-time': 3.0.4
'@types/d3-timer': 3.0.2
d3-array: 3.2.4
d3-ease: 3.0.1
d3-interpolate: 3.0.1
d3-scale: 4.0.2
d3-shape: 3.2.0
d3-time: 3.1.0
d3-timer: 3.0.1
video.js@8.22.0:
dependencies:
'@babel/runtime': 7.25.6