diff --git a/frontend/app/components/ByCustomerChart.tsx b/frontend/app/components/ByCustomerChart.tsx index d64af25..fe9ccbc 100644 --- a/frontend/app/components/ByCustomerChart.tsx +++ b/frontend/app/components/ByCustomerChart.tsx @@ -26,29 +26,31 @@ export default function ByCustomerChart({ data }: Props) { ); } return ( - - - - {data.map((entry, index) => ( - - ))} - - } /> - - - +
+ + + + {data.map((entry, index) => ( + + ))} + + } /> + + + +
); } diff --git a/frontend/app/components/ChartTooltip.test.tsx b/frontend/app/components/ChartTooltip.test.tsx new file mode 100644 index 0000000..c22ed52 --- /dev/null +++ b/frontend/app/components/ChartTooltip.test.tsx @@ -0,0 +1,39 @@ +import { render, screen } from "@testing-library/react"; +import { describe, expect, it } from "vitest"; +import ChartTooltip from "./ChartTooltip"; + +describe("ChartTooltip", () => { + it("renders nothing when inactive", () => { + const { container } = render( + , + ); + expect(container).toBeEmptyDOMElement(); + }); + + it("formats a single-series value with the label", () => { + render( + , + ); + expect(screen.getByText("Café Molido")).toBeInTheDocument(); + expect(screen.getByText("1,234.5")).toBeInTheDocument(); + }); + + it("lists each series for a multi-series payload", () => { + render( + , + ); + expect(screen.getByText(/Ingresos:/)).toBeInTheDocument(); + expect(screen.getByText(/Unidades:/)).toBeInTheDocument(); + }); +}); diff --git a/frontend/app/components/Dashboard.test.tsx b/frontend/app/components/Dashboard.test.tsx index a3f6089..180cb71 100644 --- a/frontend/app/components/Dashboard.test.tsx +++ b/frontend/app/components/Dashboard.test.tsx @@ -1,4 +1,4 @@ -import { render, screen } from "@testing-library/react"; +import { fireEvent, render, screen } from "@testing-library/react"; import { describe, expect, it, vi } from "vitest"; import Dashboard from "./Dashboard"; import type { Sale } from "../lib/dashboard"; @@ -34,4 +34,16 @@ describe("Dashboard", () => { expect(screen.getByTestId("by-product")).toHaveTextContent("2"); expect(screen.getByTestId("by-customer")).toHaveTextContent("2"); }); + + it("shows the empty-filter state when the date range excludes all sales", () => { + render(); + + fireEvent.change(screen.getByLabelText("Desde"), { + target: { value: "2027-01-01" }, + }); + + expect( + screen.getByText(/No hay ventas para los filtros seleccionados/i), + ).toBeInTheDocument(); + }); }); diff --git a/frontend/app/components/Dashboard.tsx b/frontend/app/components/Dashboard.tsx index f624a71..7952dc6 100644 --- a/frontend/app/components/Dashboard.tsx +++ b/frontend/app/components/Dashboard.tsx @@ -75,15 +75,15 @@ export default function Dashboard({ sales }: Props) { salesTrend={salesCount.map((d) => d.count)} /> - +
- + - +
diff --git a/frontend/app/components/FilterBar.test.tsx b/frontend/app/components/FilterBar.test.tsx new file mode 100644 index 0000000..059ebdd --- /dev/null +++ b/frontend/app/components/FilterBar.test.tsx @@ -0,0 +1,47 @@ +import { fireEvent, render, screen } from "@testing-library/react"; +import { describe, expect, it, vi } from "vitest"; +import FilterBar from "./FilterBar"; +import { EMPTY_FILTERS } from "../lib/analytics"; + +const baseProps = { + filters: EMPTY_FILTERS, + range: { min: "2026-01-01", max: "2026-01-31" }, + productOptions: ["A", "B"], + customerOptions: ["C1", "C2"], +}; + +describe("FilterBar", () => { + it("disables the reset button when there are no active filters", () => { + render( + , + ); + expect(screen.getByRole("button", { name: /Limpiar filtros/i })).toBeDisabled(); + }); + + it("calls onReset when active and the reset button is clicked", () => { + const onReset = vi.fn(); + render( + , + ); + fireEvent.click(screen.getByRole("button", { name: /Limpiar filtros/i })); + expect(onReset).toHaveBeenCalled(); + }); + + it("propagates a date-range change via onChange", () => { + const onChange = vi.fn(); + render( + , + ); + fireEvent.change(screen.getByLabelText("Desde"), { + target: { value: "2026-01-10" }, + }); + expect(onChange).toHaveBeenCalledWith( + expect.objectContaining({ from: "2026-01-10" }), + ); + }); +}); diff --git a/frontend/app/components/KpiCards.test.tsx b/frontend/app/components/KpiCards.test.tsx index 88789c1..54e4fb9 100644 --- a/frontend/app/components/KpiCards.test.tsx +++ b/frontend/app/components/KpiCards.test.tsx @@ -23,10 +23,10 @@ describe("KpiCards", () => { />, ); - expect(screen.getByText("Total revenue")).toBeInTheDocument(); + expect(screen.getByText("Ingresos totales")).toBeInTheDocument(); expect(screen.getByText("1,234.5")).toBeInTheDocument(); expect(screen.getByText("42")).toBeInTheDocument(); - expect(screen.getByText("Customers")).toBeInTheDocument(); - expect(screen.getByText("Products")).toBeInTheDocument(); + expect(screen.getByText("Clientes")).toBeInTheDocument(); + expect(screen.getByText("Productos")).toBeInTheDocument(); }); }); diff --git a/frontend/app/components/KpiCards.tsx b/frontend/app/components/KpiCards.tsx index 5974e04..affd695 100644 --- a/frontend/app/components/KpiCards.tsx +++ b/frontend/app/components/KpiCards.tsx @@ -19,30 +19,30 @@ type Item = { export default function KpiCards({ kpis, revenueTrend, salesTrend }: Props) { const items: Item[] = [ { - label: "Total revenue", + label: "Ingresos totales", value: formatAmountFull(kpis.totalRevenue), trend: revenueTrend, color: "var(--chart-1)", }, { - label: "Sales", + label: "Ventas", value: formatInt(kpis.transactions), trend: salesTrend, color: "var(--chart-2)", }, - { label: "Avg ticket", value: formatAmountFull(kpis.avgTicket) }, - { label: "Units sold", value: formatInt(kpis.totalUnits) }, - { label: "Customers", value: formatInt(kpis.distinctCustomers) }, - { label: "Products", value: formatInt(kpis.distinctProducts) }, + { label: "Ticket medio", value: formatAmountFull(kpis.avgTicket) }, + { label: "Unidades", value: formatInt(kpis.totalUnits) }, + { label: "Clientes", value: formatInt(kpis.distinctCustomers) }, + { label: "Productos", value: formatInt(kpis.distinctProducts) }, { - label: "Best day", + label: "Mejor día", value: kpis.bestDayDate ? formatAmountFull(kpis.bestDayTotal) : "—", sub: kpis.bestDayDate ? formatDateShort(kpis.bestDayDate) : undefined, }, ]; return ( -
+
{items.map((item) => (
{item.value} diff --git a/frontend/app/components/MultiSelect.tsx b/frontend/app/components/MultiSelect.tsx index 08567a5..9e2592c 100644 --- a/frontend/app/components/MultiSelect.tsx +++ b/frontend/app/components/MultiSelect.tsx @@ -1,6 +1,6 @@ "use client"; -import { useEffect, useRef, useState } from "react"; +import { useEffect, useId, useRef, useState } from "react"; type Props = { label: string; @@ -13,6 +13,13 @@ export default function MultiSelect({ label, options, selected, onChange }: Prop const [open, setOpen] = useState(false); const [query, setQuery] = useState(""); const ref = useRef(null); + const buttonRef = useRef(null); + const panelId = useId(); + + const close = () => { + setOpen(false); + buttonRef.current?.focus(); + }; useEffect(() => { if (!open) return; @@ -20,7 +27,7 @@ export default function MultiSelect({ label, options, selected, onChange }: Prop if (ref.current && !ref.current.contains(event.target as Node)) setOpen(false); }; const onKey = (event: KeyboardEvent) => { - if (event.key === "Escape") setOpen(false); + if (event.key === "Escape") close(); }; document.addEventListener("pointerdown", onPointerDown); document.addEventListener("keydown", onKey); @@ -45,9 +52,12 @@ export default function MultiSelect({ label, options, selected, onChange }: Prop return (
{open && ( -
+
- - - - - - } /> - - - - - +
+ + + + + + + } /> + + + + + +
); } diff --git a/frontend/app/components/RevenueOverTimeChart.tsx b/frontend/app/components/RevenueOverTimeChart.tsx index 4fe9c04..822aabf 100644 --- a/frontend/app/components/RevenueOverTimeChart.tsx +++ b/frontend/app/components/RevenueOverTimeChart.tsx @@ -28,41 +28,43 @@ export default function RevenueOverTimeChart({ data }: Props) { } return ( - - - - - - - - - - - - formatDateShort(String(l))} />} - /> - - - +
+ + + + + + + + + + + + formatDateShort(String(l))} />} + /> + + + +
); } diff --git a/frontend/app/globals.css b/frontend/app/globals.css index 06f5f70..a14931e 100644 --- a/frontend/app/globals.css +++ b/frontend/app/globals.css @@ -52,6 +52,12 @@ padding: 0; } +:focus-visible { + outline: 2px solid var(--accent); + outline-offset: 2px; + border-radius: 4px; +} + body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; background: var(--bg); diff --git a/frontend/app/layout.tsx b/frontend/app/layout.tsx index 15e0de5..d48b54f 100644 --- a/frontend/app/layout.tsx +++ b/frontend/app/layout.tsx @@ -3,7 +3,7 @@ import "./globals.css"; export const metadata: Metadata = { title: "Connect Analyzer", - description: "Sales data analysis — prototype with simulated data", + description: "Análisis de datos de ventas — prototipo con datos simulados", }; export default function RootLayout({ @@ -12,7 +12,7 @@ export default function RootLayout({ children: React.ReactNode; }>) { return ( - + {children} ); diff --git a/frontend/app/lib/dashboard.test.ts b/frontend/app/lib/dashboard.test.ts new file mode 100644 index 0000000..e5d92c7 --- /dev/null +++ b/frontend/app/lib/dashboard.test.ts @@ -0,0 +1,30 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; +import { fetchDashboard } from "./dashboard"; + +afterEach(() => { + vi.restoreAllMocks(); +}); + +describe("fetchDashboard", () => { + it("returns the sales on a successful response", async () => { + vi.spyOn(globalThis, "fetch").mockResolvedValue({ + ok: true, + json: async () => [ + { date: "2026-01-01", customerId: "C1", productName: "A", quantity: 1, amount: 10 }, + ], + } as Response); + + const { sales } = await fetchDashboard(); + + expect(sales).toHaveLength(1); + }); + + it("throws when the backend responds with an error", async () => { + vi.spyOn(globalThis, "fetch").mockResolvedValue({ + ok: false, + status: 502, + } as Response); + + await expect(fetchDashboard()).rejects.toThrow(/502/); + }); +}); diff --git a/frontend/app/page.tsx b/frontend/app/page.tsx index ca99772..04c17c4 100644 --- a/frontend/app/page.tsx +++ b/frontend/app/page.tsx @@ -8,7 +8,7 @@ export default async function Page() {

Connect Analyzer

-

Prototype with simulated sales data.

+

Prototipo con datos de ventas simulados.

diff --git a/frontend/next.config.ts b/frontend/next.config.ts index e9ffa30..075ba9b 100644 --- a/frontend/next.config.ts +++ b/frontend/next.config.ts @@ -1,7 +1,36 @@ import type { NextConfig } from "next"; +// Security headers for the public demo. The frontend fetches the backend server-side, so +// the browser only talks to its own origin → connect-src 'self'. 'unsafe-inline' is kept +// for script/style because Next (App Router hydration) and Recharts emit inline ones and +// this demo doesn't wire up CSP nonces. +const csp = [ + "default-src 'self'", + "script-src 'self' 'unsafe-inline'", + "style-src 'self' 'unsafe-inline'", + "img-src 'self' data:", + "font-src 'self'", + "connect-src 'self'", + "base-uri 'self'", + "form-action 'self'", + "frame-ancestors 'none'", +].join("; "); + +const securityHeaders = [ + { key: "Content-Security-Policy", value: csp }, + { key: "X-Content-Type-Options", value: "nosniff" }, + { key: "X-Frame-Options", value: "DENY" }, + { key: "Referrer-Policy", value: "strict-origin-when-cross-origin" }, + { + key: "Permissions-Policy", + value: "camera=(), microphone=(), geolocation=()", + }, +]; + const nextConfig: NextConfig = { - /* config options here */ + async headers() { + return [{ source: "/:path*", headers: securityHeaders }]; + }, }; export default nextConfig; diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 9769ebc..2f5d3f3 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -8,9 +8,9 @@ "name": "frontend", "version": "0.1.0", "dependencies": { - "next": "16.2.6", - "react": "19.2.4", - "react-dom": "19.2.4", + "next": "16.2.7", + "react": "19.2.7", + "react-dom": "19.2.7", "recharts": "^2.15.0" }, "devDependencies": { @@ -22,7 +22,7 @@ "@types/react-dom": "^19", "@vitejs/plugin-react": "^4.3.4", "eslint": "^9", - "eslint-config-next": "16.2.6", + "eslint-config-next": "16.2.7", "jsdom": "^25.0.1", "typescript": "^5", "vitest": "^2.1.8" @@ -1632,15 +1632,15 @@ } }, "node_modules/@next/env": { - "version": "16.2.6", - "resolved": "https://registry.npmjs.org/@next/env/-/env-16.2.6.tgz", - "integrity": "sha512-gd8HoHN4ufj73WmR3JmVolrpJR47ILK6LouP5xElPglaVxir6e1a7VzvTvDWkOoPXT9rkkTzyCxBu4yeZfZwcw==", + "version": "16.2.7", + "resolved": "https://registry.npmjs.org/@next/env/-/env-16.2.7.tgz", + "integrity": "sha512-tMJizPlj6ZYpBMMdK8S0LJufrP4QTdR6pcv9KQ/bVETPAmg0j1mlHE9G2c38UyGHxoBapgwuj7XjbGJ2RcDFOg==", "license": "MIT" }, "node_modules/@next/eslint-plugin-next": { - "version": "16.2.6", - "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-16.2.6.tgz", - "integrity": "sha512-Z8l6o4JWKUl755x4R+wogD86KPeU+Ckw4K+SYG4kHeOJtRenDeK+OSbGcqZpDtbwn9DsJVdir2UxmwXuinUbUw==", + "version": "16.2.7", + "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-16.2.7.tgz", + "integrity": "sha512-VbS+QgMHqvIDMTIqD2xMBKK1otIpdAUKA8VLHFwR9h6OfU/mOm7w/69nQcvdmI8hCk99Wr2AsGLn/PJ/tMHw1w==", "dev": true, "license": "MIT", "dependencies": { @@ -1648,9 +1648,9 @@ } }, "node_modules/@next/swc-darwin-arm64": { - "version": "16.2.6", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.2.6.tgz", - "integrity": "sha512-ZJGkkcNfYgrrMkqOdZ7zoLa1TOy0qpcMfk/z4Mh/FKUz40gVO+HNQWqmLxf67Z5WB64DRp0dhEbyHfel+6sJUg==", + "version": "16.2.7", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.2.7.tgz", + "integrity": "sha512-vm1EDI/pVaBNNiychmxk3fft+OhQPVD9cIM/tReLZIQ3TfQ4kqI9DwKk00dzuS1ulC7icbrzCFrmRRlk9PfNdw==", "cpu": [ "arm64" ], @@ -1664,9 +1664,9 @@ } }, "node_modules/@next/swc-darwin-x64": { - "version": "16.2.6", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.2.6.tgz", - "integrity": "sha512-v/YLBHIY132Ced3puBJ7YJKw1lqsCrgcNo2aRJlCEyQrrCeRJlvGlnmxhPxNQI3KE3N1DN5r9TPNPvka3nq5RQ==", + "version": "16.2.7", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.2.7.tgz", + "integrity": "sha512-O3IRSv1ZBL1zs0WrIgefTEcTKFVn+ryxBNe54erJ6KsD+2f/Mmt7g2jOYh8PSBdUwPtKQJuCsTMlZ7tIu2AcsQ==", "cpu": [ "x64" ], @@ -1680,9 +1680,9 @@ } }, "node_modules/@next/swc-linux-arm64-gnu": { - "version": "16.2.6", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.2.6.tgz", - "integrity": "sha512-RPOvqlYBbcQjkz9VQQDZ2T2bARIjXZV1KFlt+V2Mr6SW/e4I9fcKsaA0hdyf2FHoTlsV2xnBd5Y912rP/1Ce6w==", + "version": "16.2.7", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.2.7.tgz", + "integrity": "sha512-Re6PZtjBDd0aMU+VcZcC/PrIvj4WhrjDYtMhhCVQamWN4L90EVP0pcEOBQD25prSlw7OzNw5QpHLWMilRLsRNw==", "cpu": [ "arm64" ], @@ -1696,9 +1696,9 @@ } }, "node_modules/@next/swc-linux-arm64-musl": { - "version": "16.2.6", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.2.6.tgz", - "integrity": "sha512-URUTu1+dMkxJsPFgm+OeEvq9wf5sujw0EvgYy80TDGHTSLTnIHeqb0Eu8A3sC95IRgjejQL+kC4mw+4yPxiAXA==", + "version": "16.2.7", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.2.7.tgz", + "integrity": "sha512-qyogG9QtBzWxgJfeGBvOEHI3851gTfCF3wLZ5RDLTBJGAmE9p1qDwKCOdrBrvBzRvYDT+gUDp72pzlSEfAXgNA==", "cpu": [ "arm64" ], @@ -1712,9 +1712,9 @@ } }, "node_modules/@next/swc-linux-x64-gnu": { - "version": "16.2.6", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.2.6.tgz", - "integrity": "sha512-DOj182mPV8G3UkrayLoREM5YEYI+Dk5wv7Ox9xl1fFibAELEsFD0lDPfHIeILlutMMfdyhlzYPELG3peuKaurw==", + "version": "16.2.7", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.2.7.tgz", + "integrity": "sha512-Vhe4ZDuBpmMogrGi5D4R2Kq4JAQlj6+wvgaFYy31zfES0zPmt6TLA+cuYpM/OLrPZjo2MYQTHVqNUSCR6+fDZQ==", "cpu": [ "x64" ], @@ -1728,9 +1728,9 @@ } }, "node_modules/@next/swc-linux-x64-musl": { - "version": "16.2.6", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.2.6.tgz", - "integrity": "sha512-HKQ5SP/V/ub73UvF7n/zeJlxk2kLmtL7Wzrg4WfmkjmNos5onJ2tKu7yZOPdL18A6Svfn3max29ym+ry7NkK4g==", + "version": "16.2.7", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.2.7.tgz", + "integrity": "sha512-srvian89JahFLw1YLBEuhvPJ0DO5lpUeJQMXy4xYo7g628ZlNgXdNkqoxSAv9OYrBfByh6vxISMwW/mRbzCY+g==", "cpu": [ "x64" ], @@ -1744,9 +1744,9 @@ } }, "node_modules/@next/swc-win32-arm64-msvc": { - "version": "16.2.6", - "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.2.6.tgz", - "integrity": "sha512-LZXpTlPyS5v7HhSmnvsLGP3iIYgYOBnc8r8ArlT55sGHV89bR2HlDdBjWQ+PY6SJMmk8TuVGFuxalnP3k/0Dwg==", + "version": "16.2.7", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.2.7.tgz", + "integrity": "sha512-GX3wvLpULFuRFJzwHaKfm7QZJ18F4ZSuxlPJ96BoBglCzBmdSjyeBKF+ZhWhvL/ckxNfLnNa7bsObO2ipYpszw==", "cpu": [ "arm64" ], @@ -1760,9 +1760,9 @@ } }, "node_modules/@next/swc-win32-x64-msvc": { - "version": "16.2.6", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.2.6.tgz", - "integrity": "sha512-F0+4i0h9J6C4eE3EAPWsoCk7UW/dbzOjyzxY0qnDUOYFu6FFmdZ6l97/XdV3/Nz3VYyO7UWjyEJUXkGqcoXfMA==", + "version": "16.2.7", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.2.7.tgz", + "integrity": "sha512-J4WlM72NMk076Qsg0jTdK3SNXatlSdnjW7L7oNGLst1tAGjHrJh/FYi+pw9wyIjEtGRKDNzD0zuiY16oWYWVaw==", "cpu": [ "x64" ], @@ -4534,13 +4534,13 @@ } }, "node_modules/eslint-config-next": { - "version": "16.2.6", - "resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-16.2.6.tgz", - "integrity": "sha512-z2ELYSkyrrJ6cuunTU8vhsT/RpouPkjaSah06nVW6Rg2Hpg0Vs8s497/e5s8G8qtdp4ccsiovz5P1rv+5VSW2Q==", + "version": "16.2.7", + "resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-16.2.7.tgz", + "integrity": "sha512-CQ2aNXkrsjaGA2oJBE1LYnlRdphIAQE9ZQfX9hSv1PNGPyiOMSaVeBfTIO29QxYz+ij/hZudK0cfpCG1HXWstg==", "dev": true, "license": "MIT", "dependencies": { - "@next/eslint-plugin-next": "16.2.6", + "@next/eslint-plugin-next": "16.2.7", "eslint-import-resolver-node": "^0.3.6", "eslint-import-resolver-typescript": "^3.5.2", "eslint-plugin-import": "^2.32.0", @@ -6348,12 +6348,12 @@ "license": "MIT" }, "node_modules/next": { - "version": "16.2.6", - "resolved": "https://registry.npmjs.org/next/-/next-16.2.6.tgz", - "integrity": "sha512-qOVgKJg1+At15NpeUP+eJgCHvTCgXsogweq87Ri/Ix7PkqQHg4sdaXmSFqKlgaIXE4kW0g25LE68W87UANlHtw==", + "version": "16.2.7", + "resolved": "https://registry.npmjs.org/next/-/next-16.2.7.tgz", + "integrity": "sha512-eMJxgjRzBaj3olkP4cBamHDXL79A8FC6u1GcsO1D1Tsx8bw/LLXUJCaoajVxtnhD3A1IJqIT8IcRJjgBIPJq4w==", "license": "MIT", "dependencies": { - "@next/env": "16.2.6", + "@next/env": "16.2.7", "@swc/helpers": "0.5.15", "baseline-browser-mapping": "^2.9.19", "caniuse-lite": "^1.0.30001579", @@ -6367,14 +6367,14 @@ "node": ">=20.9.0" }, "optionalDependencies": { - "@next/swc-darwin-arm64": "16.2.6", - "@next/swc-darwin-x64": "16.2.6", - "@next/swc-linux-arm64-gnu": "16.2.6", - "@next/swc-linux-arm64-musl": "16.2.6", - "@next/swc-linux-x64-gnu": "16.2.6", - "@next/swc-linux-x64-musl": "16.2.6", - "@next/swc-win32-arm64-msvc": "16.2.6", - "@next/swc-win32-x64-msvc": "16.2.6", + "@next/swc-darwin-arm64": "16.2.7", + "@next/swc-darwin-x64": "16.2.7", + "@next/swc-linux-arm64-gnu": "16.2.7", + "@next/swc-linux-arm64-musl": "16.2.7", + "@next/swc-linux-x64-gnu": "16.2.7", + "@next/swc-linux-x64-musl": "16.2.7", + "@next/swc-win32-arm64-msvc": "16.2.7", + "@next/swc-win32-x64-msvc": "16.2.7", "sharp": "^0.34.5" }, "peerDependencies": { @@ -6841,24 +6841,24 @@ "license": "MIT" }, "node_modules/react": { - "version": "19.2.4", - "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", - "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", + "version": "19.2.7", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.7.tgz", + "integrity": "sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==", "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/react-dom": { - "version": "19.2.4", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz", - "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==", + "version": "19.2.7", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.7.tgz", + "integrity": "sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ==", "license": "MIT", "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { - "react": "^19.2.4" + "react": "^19.2.7" } }, "node_modules/react-is": { diff --git a/frontend/package.json b/frontend/package.json index 211a060..226f661 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -11,9 +11,9 @@ "test:run": "vitest run" }, "dependencies": { - "next": "16.2.6", - "react": "19.2.4", - "react-dom": "19.2.4", + "next": "16.2.7", + "react": "19.2.7", + "react-dom": "19.2.7", "recharts": "^2.15.0" }, "devDependencies": { @@ -25,7 +25,7 @@ "@types/react-dom": "^19", "@vitejs/plugin-react": "^4.3.4", "eslint": "^9", - "eslint-config-next": "16.2.6", + "eslint-config-next": "16.2.7", "jsdom": "^25.0.1", "typescript": "^5", "vitest": "^2.1.8"