diff --git a/packages/api/src/router/widgets/index.ts b/packages/api/src/router/widgets/index.ts index c2a8efc60..f3716459c 100644 --- a/packages/api/src/router/widgets/index.ts +++ b/packages/api/src/router/widgets/index.ts @@ -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, diff --git a/packages/api/src/router/widgets/stocks.ts b/packages/api/src/router/widgets/stocks.ts new file mode 100644 index 000000000..f9b583570 --- /dev/null +++ b/packages/api/src/router/widgets/stocks.ts @@ -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 }); + }), +}); diff --git a/packages/definitions/src/widget.ts b/packages/definitions/src/widget.ts index a829b9ae8..7f586358c 100644 --- a/packages/definitions/src/widget.ts +++ b/packages/definitions/src/widget.ts @@ -9,6 +9,7 @@ export const widgetKinds = [ "dnsHoleControls", "smartHome-entityState", "smartHome-executeAutomation", + "stockPrice", "mediaServer", "calendar", "downloads", diff --git a/packages/request-handler/src/stock-price.ts b/packages/request-handler/src/stock-price.ts new file mode 100644 index 000000000..8219881d9 --- /dev/null +++ b/packages/request-handler/src/stock-price.ts @@ -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(), + }), + }), + ), + }), + }), + ); diff --git a/packages/translation/src/lang/en.json b/packages/translation/src/lang/en.json index d11c8d7fe..54307c005 100644 --- a/packages/translation/src/lang/en.json +++ b/packages/translation/src/lang/en.json @@ -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", diff --git a/packages/widgets/package.json b/packages/widgets/package.json index 5896ebc69..7e9b57a8f 100644 --- a/packages/widgets/package.json +++ b/packages/widgets/package.json @@ -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" }, diff --git a/packages/widgets/src/index.tsx b/packages/widgets/src/index.tsx index e5ca8edf0..dab575965 100644 --- a/packages/widgets/src/index.tsx +++ b/packages/widgets/src/index.tsx @@ -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, diff --git a/packages/widgets/src/stocks/component.tsx b/packages/widgets/src/stocks/component.tsx new file mode 100644 index 000000000..326301d54 --- /dev/null +++ b/packages/widgets/src/stocks/component.tsx @@ -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 ( + + 280 ? "75%" : "50%"} + data={stockGraphValues} + curveType="linear" + trendColors={{ positive: "green.7", negative: "red.7", neutral: "gray.6" }} + fillOpacity={0.6} + strokeWidth={2.5} + /> + + + + {stockValuesChange > 0 ? ( + + ) : ( + + )} + {data.meta.symbol} + + {width > 280 && height > 280 && ( + + {data.meta.shortName} + + )} + + + 280 ? 1 : 2} fw={700}> + {round(stockValues[stockValues.length - 1] ?? 0)} + + + {width > 280 && ( + + {Math.abs(stockValuesChange)} ({Math.abs(stockValuesChangePercentage)}%) + + )} + + {width > 280 && ( + + {t(`option.timeRange.option.${options.timeRange}.label`)} + + )} + + + + {stockValuesChange > 0 ? ( + + ) : ( + + )} + {data.meta.symbol} + + {width > 280 && height > 280 && ( + + {data.meta.shortName} + + )} + + + ); +} diff --git a/packages/widgets/src/stocks/index.ts b/packages/widgets/src/stocks/index.ts new file mode 100644 index 000000000..fa64bf3a4 --- /dev/null +++ b/packages/widgets/src/stocks/index.ts @@ -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")); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a1864c8f2..2bb886b1c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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