From 08f039702967fcf055035ee7162ac7be481b1306 Mon Sep 17 00:00:00 2001 From: Froidefond Julien Date: Sun, 22 Mar 2026 06:26:45 +0100 Subject: [PATCH] feat: add reading stats and replace dashboard charts with recharts Add currently reading, recently read, and reading activity sections to the dashboard. Replace all custom SVG/CSS charts with recharts library (donut, area, stacked bar, horizontal bar). Reorganize layout: libraries and popular series side by side, books added chart full width below. Co-Authored-By: Claude Opus 4.6 --- apps/api/src/stats.rs | 109 +++++ .../app/components/DashboardCharts.tsx | 188 ++++++++ apps/backoffice/app/page.tsx | 330 +++++++------- apps/backoffice/lib/api.ts | 23 + apps/backoffice/lib/i18n/en.ts | 6 + apps/backoffice/lib/i18n/fr.ts | 6 + apps/backoffice/package-lock.json | 415 +++++++++++++++++- apps/backoffice/package.json | 1 + 8 files changed, 901 insertions(+), 177 deletions(-) create mode 100644 apps/backoffice/app/components/DashboardCharts.tsx diff --git a/apps/api/src/stats.rs b/apps/api/src/stats.rs index 2ada441..019b6ba 100644 --- a/apps/api/src/stats.rs +++ b/apps/api/src/stats.rs @@ -74,10 +74,36 @@ pub struct ProviderCount { pub count: i64, } +#[derive(Serialize, ToSchema)] +pub struct CurrentlyReadingItem { + pub book_id: String, + pub title: String, + pub series: Option, + pub current_page: i32, + pub page_count: i32, +} + +#[derive(Serialize, ToSchema)] +pub struct RecentlyReadItem { + pub book_id: String, + pub title: String, + pub series: Option, + pub last_read_at: String, +} + +#[derive(Serialize, ToSchema)] +pub struct MonthlyReading { + pub month: String, + pub books_read: i64, +} + #[derive(Serialize, ToSchema)] pub struct StatsResponse { pub overview: StatsOverview, pub reading_status: ReadingStatusStats, + pub currently_reading: Vec, + pub recently_read: Vec, + pub reading_over_time: Vec, pub by_format: Vec, pub by_language: Vec, pub by_library: Vec, @@ -327,9 +353,92 @@ pub async fn get_stats( by_provider, }; + // Currently reading books + let reading_rows = sqlx::query( + r#" + SELECT b.id AS book_id, b.title, b.series, brp.current_page, b.page_count + FROM book_reading_progress brp + JOIN books b ON b.id = brp.book_id + WHERE brp.status = 'reading' AND brp.current_page IS NOT NULL + ORDER BY brp.updated_at DESC + LIMIT 20 + "#, + ) + .fetch_all(&state.pool) + .await?; + + let currently_reading: Vec = reading_rows + .iter() + .map(|r| { + let id: uuid::Uuid = r.get("book_id"); + CurrentlyReadingItem { + book_id: id.to_string(), + title: r.get("title"), + series: r.get("series"), + current_page: r.get::, _>("current_page").unwrap_or(0), + page_count: r.get::, _>("page_count").unwrap_or(0), + } + }) + .collect(); + + // Recently read books + let recent_rows = sqlx::query( + r#" + SELECT b.id AS book_id, b.title, b.series, + TO_CHAR(brp.last_read_at, 'YYYY-MM-DD') AS last_read_at + FROM book_reading_progress brp + JOIN books b ON b.id = brp.book_id + WHERE brp.status = 'read' AND brp.last_read_at IS NOT NULL + ORDER BY brp.last_read_at DESC + LIMIT 10 + "#, + ) + .fetch_all(&state.pool) + .await?; + + let recently_read: Vec = recent_rows + .iter() + .map(|r| { + let id: uuid::Uuid = r.get("book_id"); + RecentlyReadItem { + book_id: id.to_string(), + title: r.get("title"), + series: r.get("series"), + last_read_at: r.get::, _>("last_read_at").unwrap_or_default(), + } + }) + .collect(); + + // Reading activity over time (last 12 months) + let reading_time_rows = sqlx::query( + r#" + SELECT + TO_CHAR(DATE_TRUNC('month', brp.last_read_at), 'YYYY-MM') AS month, + COUNT(*) AS books_read + FROM book_reading_progress brp + WHERE brp.status = 'read' + AND brp.last_read_at >= DATE_TRUNC('month', NOW()) - INTERVAL '11 months' + GROUP BY DATE_TRUNC('month', brp.last_read_at) + ORDER BY month ASC + "#, + ) + .fetch_all(&state.pool) + .await?; + + let reading_over_time: Vec = reading_time_rows + .iter() + .map(|r| MonthlyReading { + month: r.get::, _>("month").unwrap_or_default(), + books_read: r.get("books_read"), + }) + .collect(); + Ok(Json(StatsResponse { overview, reading_status, + currently_reading, + recently_read, + reading_over_time, by_format, by_language, by_library, diff --git a/apps/backoffice/app/components/DashboardCharts.tsx b/apps/backoffice/app/components/DashboardCharts.tsx new file mode 100644 index 0000000..146aa30 --- /dev/null +++ b/apps/backoffice/app/components/DashboardCharts.tsx @@ -0,0 +1,188 @@ +"use client"; + +import { + PieChart, Pie, Cell, ResponsiveContainer, Tooltip, + BarChart, Bar, XAxis, YAxis, CartesianGrid, + AreaChart, Area, + Legend, +} from "recharts"; + +// --------------------------------------------------------------------------- +// Donut +// --------------------------------------------------------------------------- + +export function RcDonutChart({ + data, + noDataLabel, +}: { + data: { name: string; value: number; color: string }[]; + noDataLabel?: string; +}) { + const total = data.reduce((s, d) => s + d.value, 0); + if (total === 0) return

{noDataLabel}

; + + return ( +
+ + + + {data.map((d, i) => ( + + ))} + + value} + contentStyle={{ backgroundColor: "var(--color-card)", border: "1px solid var(--color-border)", borderRadius: 8, fontSize: 12 }} + /> + + +
+ {data.map((d, i) => ( +
+ + {d.name} + {d.value} +
+ ))} +
+
+ ); +} + +// --------------------------------------------------------------------------- +// Bar chart +// --------------------------------------------------------------------------- + +export function RcBarChart({ + data, + color = "hsl(198 78% 37%)", + noDataLabel, +}: { + data: { label: string; value: number }[]; + color?: string; + noDataLabel?: string; +}) { + if (data.length === 0) return

{noDataLabel}

; + + return ( + + + + + + + + + + ); +} + +// --------------------------------------------------------------------------- +// Area / Line chart +// --------------------------------------------------------------------------- + +export function RcAreaChart({ + data, + color = "hsl(142 60% 45%)", + noDataLabel, +}: { + data: { label: string; value: number }[]; + color?: string; + noDataLabel?: string; +}) { + if (data.length === 0) return

{noDataLabel}

; + + return ( + + + + + + + + + + + + + + + + ); +} + +// --------------------------------------------------------------------------- +// Horizontal stacked bar (libraries breakdown) +// --------------------------------------------------------------------------- + +export function RcStackedBar({ + data, + labels, +}: { + data: { name: string; read: number; reading: number; unread: number; sizeLabel: string }[]; + labels: { read: string; reading: string; unread: string; books: string }; +}) { + if (data.length === 0) return null; + + return ( + + + + + + + {value}} + /> + + + + + + ); +} + +// --------------------------------------------------------------------------- +// Horizontal bar chart (top series) +// --------------------------------------------------------------------------- + +export function RcHorizontalBar({ + data, + color = "hsl(142 60% 45%)", + noDataLabel, +}: { + data: { name: string; value: number; subLabel: string }[]; + color?: string; + noDataLabel?: string; +}) { + if (data.length === 0) return

{noDataLabel}

; + + return ( + + + + + + + + + + ); +} diff --git a/apps/backoffice/app/page.tsx b/apps/backoffice/app/page.tsx index ddc68f9..e128cd6 100644 --- a/apps/backoffice/app/page.tsx +++ b/apps/backoffice/app/page.tsx @@ -1,6 +1,8 @@ import React from "react"; -import { fetchStats, StatsResponse } from "../lib/api"; +import { fetchStats, StatsResponse, getBookCoverUrl } from "../lib/api"; import { Card, CardContent, CardHeader, CardTitle } from "./components/ui"; +import { RcDonutChart, RcBarChart, RcAreaChart, RcStackedBar, RcHorizontalBar } from "./components/DashboardCharts"; +import Image from "next/image"; import Link from "next/link"; import { getServerTranslations } from "../lib/i18n/server"; import type { TranslateFunction } from "../lib/i18n/dictionaries"; @@ -19,84 +21,7 @@ function formatNumber(n: number, locale: string): string { return n.toLocaleString(locale === "fr" ? "fr-FR" : "en-US"); } -// Donut chart via SVG -function DonutChart({ data, colors, noDataLabel, locale = "fr" }: { data: { label: string; value: number; color: string }[]; colors?: string[]; noDataLabel?: string; locale?: string }) { - const total = data.reduce((sum, d) => sum + d.value, 0); - if (total === 0) return

{noDataLabel}

; - - const radius = 40; - const circumference = 2 * Math.PI * radius; - let offset = 0; - - return ( -
- - {data.map((d, i) => { - const pct = d.value / total; - const dashLength = pct * circumference; - const currentOffset = offset; - offset += dashLength; - return ( - - ); - })} - - {formatNumber(total, locale)} - - -
- {data.map((d, i) => ( -
- - {d.label} - {d.value} -
- ))} -
-
- ); -} - -// Bar chart via pure CSS -function BarChart({ data, color = "var(--color-primary)", noDataLabel }: { data: { label: string; value: number }[]; color?: string; noDataLabel?: string }) { - const max = Math.max(...data.map((d) => d.value), 1); - if (data.length === 0) return

{noDataLabel}

; - - return ( -
- {data.map((d, i) => ( -
- {d.value || ""} -
- - {d.label} - -
- ))} -
- ); -} - -// Horizontal progress bar for library breakdown +// Horizontal progress bar for metadata quality (stays server-rendered, no recharts needed) function HorizontalBar({ label, value, max, subLabel, color = "var(--color-primary)" }: { label: string; value: number; max: number; subLabel?: string; color?: string }) { const pct = max > 0 ? (value / max) * 100 : 0; return ( @@ -137,7 +62,7 @@ export default async function DashboardPage() { ); } - const { overview, reading_status, by_format, by_language, by_library, top_series, additions_over_time, metadata } = stats; + const { overview, reading_status, currently_reading = [], recently_read = [], reading_over_time = [], by_format, by_library, top_series, additions_over_time, metadata } = stats; const readingColors = ["hsl(220 13% 70%)", "hsl(45 93% 47%)", "hsl(142 60% 45%)"]; const formatColors = [ @@ -146,7 +71,6 @@ export default async function DashboardPage() { "hsl(170 60% 45%)", "hsl(220 60% 50%)", ]; - const maxLibBooks = Math.max(...by_library.map((l) => l.book_count), 1); const noDataLabel = t("common.noData"); return ( @@ -174,6 +98,98 @@ export default async function DashboardPage() {
+ {/* Currently reading + Recently read */} + {(currently_reading.length > 0 || recently_read.length > 0) && ( +
+ {/* Currently reading */} + + + {t("dashboard.currentlyReading")} + + + {currently_reading.length === 0 ? ( +

{t("dashboard.noCurrentlyReading")}

+ ) : ( +
+ {currently_reading.slice(0, 8).map((book) => { + const pct = book.page_count > 0 ? Math.round((book.current_page / book.page_count) * 100) : 0; + return ( + + {book.title} +
+

{book.title}

+ {book.series &&

{book.series}

} +
+
+
+
+ {pct}% +
+

{t("dashboard.pageProgress", { current: book.current_page, total: book.page_count })}

+
+ + ); + })} +
+ )} + + + + {/* Recently read */} + + + {t("dashboard.recentlyRead")} + + + {recently_read.length === 0 ? ( +

{t("dashboard.noRecentlyRead")}

+ ) : ( +
+ {recently_read.map((book) => ( + + {book.title} +
+

{book.title}

+ {book.series &&

{book.series}

} +
+ {book.last_read_at} + + ))} +
+ )} +
+
+
+ )} + + {/* Reading activity line chart */} + {reading_over_time.length > 0 && ( + + + {t("dashboard.readingActivity")} + + + ({ label: m.month.slice(5), value: m.books_read }))} + color="hsl(142 60% 45%)" + /> + + + )} + {/* Charts row */}
{/* Reading status donut */} @@ -182,13 +198,12 @@ export default async function DashboardPage() { {t("dashboard.readingStatus")} - @@ -200,11 +215,10 @@ export default async function DashboardPage() { {t("dashboard.byFormat")} - ({ - label: (f.format || t("dashboard.unknown")).toUpperCase(), + name: (f.format || t("dashboard.unknown")).toUpperCase(), value: f.count, color: formatColors[i % formatColors.length], }))} @@ -218,11 +232,10 @@ export default async function DashboardPage() { {t("dashboard.byLibrary")} - ({ - label: l.library_name, + name: l.library_name, value: l.book_count, color: formatColors[i % formatColors.length], }))} @@ -239,12 +252,11 @@ export default async function DashboardPage() { {t("dashboard.metadataCoverage")} - @@ -256,11 +268,10 @@ export default async function DashboardPage() { {t("dashboard.byProvider")} - ({ - label: p.provider.replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase()), + name: p.provider.replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase()), value: p.count, color: formatColors[i % formatColors.length], }))} @@ -294,24 +305,32 @@ export default async function DashboardPage() {
- {/* Second row */} + {/* Libraries breakdown + Top series */}
- {/* Monthly additions bar chart */} - - - {t("dashboard.booksAdded")} - - - ({ - label: m.month.slice(5), // "MM" from "YYYY-MM" - value: m.books_added, - }))} - color="hsl(198 78% 37%)" - /> - - + {by_library.length > 0 && ( + + + {t("dashboard.libraries")} + + + ({ + name: lib.library_name, + read: lib.read_count, + reading: lib.reading_count, + unread: lib.unread_count, + sizeLabel: formatBytes(lib.size_bytes), + }))} + labels={{ + read: t("status.read"), + reading: t("status.reading"), + unread: t("status.unread"), + books: t("dashboard.books"), + }} + /> + + + )} {/* Top series */} @@ -319,67 +338,32 @@ export default async function DashboardPage() { {t("dashboard.popularSeries")} -
- {top_series.slice(0, 8).map((s, i) => ( - - ))} - {top_series.length === 0 && ( -

{t("dashboard.noSeries")}

- )} -
+ ({ + name: s.series, + value: s.book_count, + subLabel: t("dashboard.readCount", { read: s.read_count, total: s.book_count }), + }))} + color="hsl(142 60% 45%)" + />
- {/* Libraries breakdown */} - {by_library.length > 0 && ( - - - {t("dashboard.libraries")} - - -
- {by_library.map((lib, i) => ( -
-
- {lib.library_name} - {formatBytes(lib.size_bytes)} -
-
-
-
-
-
-
- {lib.book_count} {t("dashboard.books").toLowerCase()} - {lib.read_count} {t("status.read").toLowerCase()} - {lib.reading_count} {t("status.reading").toLowerCase()} -
-
- ))} -
- - - )} + {/* Monthly additions line chart – full width */} + + + {t("dashboard.booksAdded")} + + + ({ label: m.month.slice(5), value: m.books_added }))} + color="hsl(198 78% 37%)" + /> + + {/* Quick links */} diff --git a/apps/backoffice/lib/api.ts b/apps/backoffice/lib/api.ts index a2dbc18..bffb448 100644 --- a/apps/backoffice/lib/api.ts +++ b/apps/backoffice/lib/api.ts @@ -550,9 +550,32 @@ export type MetadataStats = { by_provider: ProviderCount[]; }; +export type CurrentlyReadingItem = { + book_id: string; + title: string; + series: string | null; + current_page: number; + page_count: number; +}; + +export type RecentlyReadItem = { + book_id: string; + title: string; + series: string | null; + last_read_at: string; +}; + +export type MonthlyReading = { + month: string; + books_read: number; +}; + export type StatsResponse = { overview: StatsOverview; reading_status: ReadingStatusStats; + currently_reading: CurrentlyReadingItem[]; + recently_read: RecentlyReadItem[]; + reading_over_time: MonthlyReading[]; by_format: FormatCount[]; by_language: LanguageCount[]; by_library: LibraryStatsItem[]; diff --git a/apps/backoffice/lib/i18n/en.ts b/apps/backoffice/lib/i18n/en.ts index 8f18b82..3880106 100644 --- a/apps/backoffice/lib/i18n/en.ts +++ b/apps/backoffice/lib/i18n/en.ts @@ -82,6 +82,12 @@ const en: Record = { "dashboard.bookMetadata": "Book metadata", "dashboard.withSummary": "With summary", "dashboard.withIsbn": "With ISBN", + "dashboard.currentlyReading": "Currently reading", + "dashboard.recentlyRead": "Recently read", + "dashboard.readingActivity": "Reading activity (last 12 months)", + "dashboard.pageProgress": "p. {{current}} / {{total}}", + "dashboard.noCurrentlyReading": "No books in progress", + "dashboard.noRecentlyRead": "No books read recently", // Books page "books.title": "Books", diff --git a/apps/backoffice/lib/i18n/fr.ts b/apps/backoffice/lib/i18n/fr.ts index 32d59bf..342a1dd 100644 --- a/apps/backoffice/lib/i18n/fr.ts +++ b/apps/backoffice/lib/i18n/fr.ts @@ -80,6 +80,12 @@ const fr = { "dashboard.bookMetadata": "Métadonnées livres", "dashboard.withSummary": "Avec résumé", "dashboard.withIsbn": "Avec ISBN", + "dashboard.currentlyReading": "En cours de lecture", + "dashboard.recentlyRead": "Derniers livres lus", + "dashboard.readingActivity": "Activité de lecture (12 derniers mois)", + "dashboard.pageProgress": "p. {{current}} / {{total}}", + "dashboard.noCurrentlyReading": "Aucun livre en cours", + "dashboard.noRecentlyRead": "Aucun livre lu récemment", // Books page "books.title": "Livres", diff --git a/apps/backoffice/package-lock.json b/apps/backoffice/package-lock.json index 4c5bbfc..bf841d8 100644 --- a/apps/backoffice/package-lock.json +++ b/apps/backoffice/package-lock.json @@ -1,17 +1,18 @@ { "name": "stripstream-backoffice", - "version": "1.4.0", + "version": "1.23.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "stripstream-backoffice", - "version": "1.4.0", + "version": "1.23.0", "dependencies": { "next": "^16.1.6", "next-themes": "^0.4.6", "react": "19.0.0", "react-dom": "19.0.0", + "recharts": "^3.8.0", "sanitize-html": "^2.17.1" }, "devDependencies": { @@ -759,6 +760,54 @@ "node": ">= 10" } }, + "node_modules/@reduxjs/toolkit": { + "version": "2.11.2", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.11.2.tgz", + "integrity": "sha512-Kd6kAHTA6/nUpp8mySPqj3en3dm0tdMIgbttnQ1xFMVpufoj+ADi8pXLBsd4xzTRHQa7t/Jv8W5UnCuW4kuWMQ==", + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "@standard-schema/utils": "^0.3.0", + "immer": "^11.0.0", + "redux": "^5.0.1", + "redux-thunk": "^3.1.0", + "reselect": "^5.1.0" + }, + "peerDependencies": { + "react": "^16.9.0 || ^17.0.0 || ^18 || ^19", + "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-redux": { + "optional": true + } + } + }, + "node_modules/@reduxjs/toolkit/node_modules/immer": { + "version": "11.1.4", + "resolved": "https://registry.npmjs.org/immer/-/immer-11.1.4.tgz", + "integrity": "sha512-XREFCPo6ksxVzP4E0ekD5aMdf8WMwmdNaz6vuvxgI40UaEiu6q3p8X52aU6GdyvLY3XXX/8R7JOTXStz/nBbRw==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "license": "MIT" + }, + "node_modules/@standard-schema/utils": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz", + "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==", + "license": "MIT" + }, "node_modules/@swc/helpers": { "version": "0.5.15", "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", @@ -1051,6 +1100,69 @@ "tailwindcss": "4.2.1" } }, + "node_modules/@types/d3-array": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz", + "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==", + "license": "MIT" + }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "license": "MIT" + }, + "node_modules/@types/d3-ease": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", + "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", + "license": "MIT" + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "license": "MIT", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-path": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", + "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", + "license": "MIT" + }, + "node_modules/@types/d3-scale": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", + "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", + "license": "MIT", + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@types/d3-shape": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.8.tgz", + "integrity": "sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==", + "license": "MIT", + "dependencies": { + "@types/d3-path": "*" + } + }, + "node_modules/@types/d3-time": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", + "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", + "license": "MIT" + }, + "node_modules/@types/d3-timer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", + "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", + "license": "MIT" + }, "node_modules/@types/node": { "version": "22.13.14", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.14.tgz", @@ -1065,7 +1177,7 @@ "version": "19.0.12", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.0.12.tgz", "integrity": "sha512-V6Ar115dBDrjbtXSrS+/Oruobc+qVbbUxDFC1RSbRqLt5SYvxxyIDrSC85RWml54g+jfNeEMZhEj7wW07ONQhA==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "csstype": "^3.0.2" @@ -1124,6 +1236,12 @@ "entities": "^7.0.1" } }, + "node_modules/@types/use-sync-external-store": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", + "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==", + "license": "MIT" + }, "node_modules/autoprefixer": { "version": "10.4.27", "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.27.tgz", @@ -1233,11 +1351,147 @@ "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", "license": "MIT" }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/csstype": { "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "dev": true, + "devOptional": true, + "license": "MIT" + }, + "node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "license": "ISC", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.2.tgz", + "integrity": "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "license": "ISC", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "license": "ISC", + "dependencies": { + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "license": "ISC", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "license": "ISC", + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/decimal.js-light": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz", + "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==", "license": "MIT" }, "node_modules/deepmerge": { @@ -1347,6 +1601,16 @@ "url": "https://github.com/fb55/entities?sponsor=1" } }, + "node_modules/es-toolkit": { + "version": "1.45.1", + "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.45.1.tgz", + "integrity": "sha512-/jhoOj/Fx+A+IIyDNOvO3TItGmlMKhtX8ISAHKE90c4b/k1tqaqEZ+uUqfpU8DMnW5cgNJv606zS55jGvza0Xw==", + "license": "MIT", + "workspaces": [ + "docs", + "benchmarks" + ] + }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", @@ -1369,6 +1633,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/eventemitter3": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", + "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", + "license": "MIT" + }, "node_modules/fraction.js": { "version": "5.3.4", "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz", @@ -1409,6 +1679,25 @@ "entities": "^4.4.0" } }, + "node_modules/immer": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/immer/-/immer-10.2.0.tgz", + "integrity": "sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/is-plain-object": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", @@ -1895,6 +2184,87 @@ "react": "^19.0.0" } }, + "node_modules/react-is": { + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.4.tgz", + "integrity": "sha512-W+EWGn2v0ApPKgKKCy/7s7WHXkboGcsrXE+2joLyVxkbyVQfO3MUEaUQDHoSmb8TFFrSKYa9mw64WZHNHSDzYA==", + "license": "MIT", + "peer": true + }, + "node_modules/react-redux": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", + "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==", + "license": "MIT", + "dependencies": { + "@types/use-sync-external-store": "^0.0.6", + "use-sync-external-store": "^1.4.0" + }, + "peerDependencies": { + "@types/react": "^18.2.25 || ^19", + "react": "^18.0 || ^19", + "redux": "^5.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "redux": { + "optional": true + } + } + }, + "node_modules/recharts": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/recharts/-/recharts-3.8.0.tgz", + "integrity": "sha512-Z/m38DX3L73ExO4Tpc9/iZWHmHnlzWG4njQbxsF5aSjwqmHNDDIm0rdEBArkwsBvR8U6EirlEHiQNYWCVh9sGQ==", + "license": "MIT", + "workspaces": [ + "www" + ], + "dependencies": { + "@reduxjs/toolkit": "^1.9.0 || 2.x.x", + "clsx": "^2.1.1", + "decimal.js-light": "^2.5.1", + "es-toolkit": "^1.39.3", + "eventemitter3": "^5.0.1", + "immer": "^10.1.1", + "react-redux": "8.x.x || 9.x.x", + "reselect": "5.1.1", + "tiny-invariant": "^1.3.3", + "use-sync-external-store": "^1.2.2", + "victory-vendor": "^37.0.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-is": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/redux": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", + "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", + "license": "MIT" + }, + "node_modules/redux-thunk": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz", + "integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==", + "license": "MIT", + "peerDependencies": { + "redux": "^5.0.0" + } + }, + "node_modules/reselect": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz", + "integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==", + "license": "MIT" + }, "node_modules/sanitize-html": { "version": "2.17.1", "resolved": "https://registry.npmjs.org/sanitize-html/-/sanitize-html-2.17.1.tgz", @@ -2026,6 +2396,12 @@ "url": "https://opencollective.com/webpack" } }, + "node_modules/tiny-invariant": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", + "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", + "license": "MIT" + }, "node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", @@ -2083,6 +2459,37 @@ "peerDependencies": { "browserslist": ">= 4.21.0" } + }, + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/victory-vendor": { + "version": "37.3.6", + "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-37.3.6.tgz", + "integrity": "sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ==", + "license": "MIT AND ISC", + "dependencies": { + "@types/d3-array": "^3.0.3", + "@types/d3-ease": "^3.0.0", + "@types/d3-interpolate": "^3.0.1", + "@types/d3-scale": "^4.0.2", + "@types/d3-shape": "^3.1.0", + "@types/d3-time": "^3.0.0", + "@types/d3-timer": "^3.0.0", + "d3-array": "^3.1.6", + "d3-ease": "^3.0.1", + "d3-interpolate": "^3.0.1", + "d3-scale": "^4.0.2", + "d3-shape": "^3.1.0", + "d3-time": "^3.0.0", + "d3-timer": "^3.0.1" + } } } } diff --git a/apps/backoffice/package.json b/apps/backoffice/package.json index d49c691..dd454a7 100644 --- a/apps/backoffice/package.json +++ b/apps/backoffice/package.json @@ -12,6 +12,7 @@ "next-themes": "^0.4.6", "react": "19.0.0", "react-dom": "19.0.0", + "recharts": "^3.8.0", "sanitize-html": "^2.17.1" }, "devDependencies": {