Compare commits

...

6 commits

Author SHA1 Message Date
Nathan Lamy
b9773de805 feat: add open action
All checks were successful
Deploy to Netlify / Deploy to Netlify (push) Successful in 2m10s
2025-08-21 11:36:37 +02:00
Nathan Lamy
7ccd36a7c4 feat: add WIP repas route 2025-08-21 11:33:43 +02:00
Nathan Lamy
3e01fa47cd feat: add radar chart 2025-08-20 18:17:55 +02:00
Nathan Lamy
e6ba7e0d9a feat: add line chart 2025-08-20 18:02:22 +02:00
Nathan Lamy
dc5e10efd1 feat: add grades & averages 2025-08-20 16:09:06 +02:00
Nathan Lamy
eb8bb8331f chore: change icon 2025-08-20 13:54:53 +02:00
13 changed files with 1124 additions and 4 deletions

View file

@ -123,6 +123,24 @@
} }
} }
@layer base {
:root {
--chart-1: oklch(0.646 0.222 41.116);
--chart-2: oklch(0.6 0.118 184.704);
--chart-3: oklch(0.398 0.07 227.392);
--chart-4: oklch(0.828 0.189 84.429);
--chart-5: oklch(0.769 0.188 70.08);
}
.dark {
--chart-1: oklch(0.488 0.243 264.376);
--chart-2: oklch(0.696 0.17 162.48);
--chart-3: oklch(0.769 0.188 70.08);
--chart-4: oklch(0.627 0.265 303.9);
--chart-5: oklch(0.645 0.246 16.439);
}
}
html, html,
body { body {
@apply w-full h-full; @apply w-full h-full;
@ -135,3 +153,26 @@ body {
[data-sonner-toaster][data-sonner-theme='dark'] [data-description] { [data-sonner-toaster][data-sonner-theme='dark'] [data-description] {
color: var(--primary) !important; color: var(--primary) !important;
} }
:root {
--color-red-800: theme('colors.red.800');
--color-orange-800: theme('colors.orange.800');
--color-amber-800: theme('colors.amber.800');
--color-yellow-800: theme('colors.yellow.800');
--color-lime-800: theme('colors.lime.800');
--color-green-800: theme('colors.green.800');
--color-emerald-800: theme('colors.emerald.800');
--color-teal-800: theme('colors.teal.800');
--color-cyan-800: theme('colors.cyan.800');
--color-sky-800: theme('colors.sky.800');
--color-blue-800: theme('colors.blue.800');
--color-indigo-800: theme('colors.indigo.800');
--color-violet-800: theme('colors.violet.800');
--color-purple-800: theme('colors.purple.800');
--color-fuchsia-800: theme('colors.fuchsia.800');
--color-pink-800: theme('colors.pink.800');
--color-rose-800: theme('colors.rose.800');
--color-slate-800: theme('colors.slate.800');
--color-gray-800: theme('colors.gray.800');
--color-zinc-800: theme('colors.zinc.800');
}

View file

@ -8,7 +8,7 @@ export default function AttachmentItem({ attachment }: { attachment: Attachment
target="_blank" target="_blank"
className="flex items-center gap-2 p-3 border rounded-md hover:bg-muted transition-colors cursor-pointer" className="flex items-center gap-2 p-3 border rounded-md hover:bg-muted transition-colors cursor-pointer"
> >
{getIcon(attachment.name)} {getIcon(attachment.path)}
<span className="font-medium truncate"> <span className="font-medium truncate">
{attachment.name} {attachment.name}
</span> </span>

View file

@ -0,0 +1,255 @@
import {
CartesianGrid,
Legend,
Line,
LineChart,
PolarAngleAxis,
PolarGrid,
PolarRadiusAxis,
Radar,
RadarChart,
ResponsiveContainer,
XAxis,
YAxis,
} from "recharts";
import { useAverages, useGrades, type User } from "~/lib/api";
import { ChartContainer, ChartTooltip, ChartTooltipContent } from "../ui/chart";
import { Award } from "lucide-react";
import { getSubjectColor, getSubjectEmoji } from "~/lib/utils";
import { useEffect, useState } from "react";
import { Tabs, TabsList, tabsStyle, TabsTrigger } from "../ui/tabs";
const periods = [
{
id: "YEAR",
label: "Cette année",
},
{
id: "TRIMESTER",
label: "Ce trimestre",
},
{
id: "MONTH",
label: "Ce mois",
},
];
export default function GradesPage({ user }: { user: User }) {
const [period, setPeriod] = useState(periods[0].id);
const { grades, subjects, isLoading, isError } = useGrades(period);
const {
subjectAverages,
globalAverage,
isLoading: loading,
isError: error,
} = useAverages(period);
const [hiddenSubjects, setHiddenSubjects] = useState<string[]>([]);
const toggleSubjectVisibility = (subject: string) => {
setHiddenSubjects((prev) =>
prev.includes(subject)
? prev.filter((s) => s !== subject)
: [...prev, subject]
);
};
const [min, setMin] = useState(0);
const [max, setMax] = useState(20);
useEffect(() => {
if (grades.length > 0) {
const filteredGrades = grades.map((o) =>
Object.entries(o)
.filter(([key, value]) => {
return !hiddenSubjects.includes(key) && !isNaN(parseFloat(value));
})
.map(([_, v]) => parseFloat(v))
.filter(Boolean)
);
setMin(getMin(filteredGrades));
setMax(getMax(filteredGrades));
}
}, [grades, hiddenSubjects]);
return (
<div className="space-y-6 pb-20 md:pb-0">
{/* Tabs */}
<Tabs
defaultValue={periods[0].id}
value={period}
onValueChange={setPeriod}
className="max-w-md w-full"
>
<TabsList className="w-full p-0 bg-background justify-start border-b rounded-none">
{periods.map((period) => (
<TabsTrigger
key={period.id}
value={period.id}
className={tabsStyle}
>
{period.label}
</TabsTrigger>
))}
</TabsList>
</Tabs>
{isNaN(globalAverage) ? (
<div className="text-center text-muted-foreground">
Aucune note disponible pour cette période.
</div>
) : isLoading || loading ? (
<div className="flex items-center justify-center h-64">
<span className="text-muted-foreground">Chargement...</span>
</div>
) : isError || error ? (
<div className="text-center text-red-500">
Une erreur est survenue lors de la récupération des données.
</div>
) : (
<>
{/* Global average */}
<div className="bg-muted/50 rounded-lg p-4 space-y-2">
<div className="flex items-center justify-between">
<span className="text-xs md:text-sm font-medium text-muted-foreground">
Moyenne générale
</span>
<Award className="h-4 w-4 text-muted-foreground" />
</div>
<div className="text-xl md:text-2xl font-bold">
{globalAverage} / 20
</div>
</div>
{/* Charts - Mobile First Layout */}
<div className="space-y-8">
{/* Grade Trends */}
<div className="space-y-4">
<div className="flex items-center justify-between">
<div>
<h2 className="text-lg md:text-xl font-semibold">
Votre progression
</h2>
<p className="text-sm text-muted-foreground">
Suivez vos notes au fil du temps
</p>
</div>
</div>
<div className="bg-muted/50 rounded-lg py-4">
<ChartContainer
config={{}}
className="h-full w-full -ml-6 max-w-2xl"
>
<LineChart data={grades}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="period" />
<YAxis domain={[min, max]} />
<ChartTooltip content={<ChartTooltipContent />} />
<Legend
onClick={(e) => {
if (e.dataKey) {
toggleSubjectVisibility(e.dataKey as string);
}
}}
/>
{subjects.map((subject, index) => (
<Line
key={index}
type="monotone"
dataKey={subject}
name={
subject +
" " +
getSubjectEmoji(subject, user.preferences)
}
hide={hiddenSubjects.includes(subject)}
stroke={`var(--color-${getSubjectColor(
subject,
user.preferences
)}-800`}
dot={{
fill: `var(--color-${getSubjectColor(
subject,
user.preferences
)}-800`,
stroke: `var(--color-${getSubjectColor(
subject,
user.preferences
)}-800)`,
}}
/>
))}
<Line
type="monotone"
dataKey="average"
name="Moyenne"
stroke="var(--primary)"
strokeWidth={2}
dot={false}
strokeDasharray="5 5"
hide={hiddenSubjects.includes("average")}
/>
</LineChart>
</ChartContainer>
</div>
</div>
{/* Radar chart */}
<div className="space-y-4">
<div className="flex items-center justify-between">
<div>
<h2 className="text-lg md:text-xl font-semibold">
Vos points forts
</h2>
<p className="text-sm text-muted-foreground">
Visualisez vos performances par matière
</p>
</div>
</div>
<div className="bg-muted/50 rounded-lg py-4">
<ChartContainer
config={{}}
className="h-full w-full -ml-6 max-w-2xl"
>
<RadarChart
data={subjectAverages}
margin={{ top: 10, right: 10, bottom: 10, left: 10 }}
>
<PolarGrid />
<PolarAngleAxis dataKey="subject" />
<PolarRadiusAxis angle={90} domain={[0, 20]} />
<Radar
name="Moyenne"
dataKey="average"
stroke="var(--chart-2)"
fill="var(--chart-2)"
fillOpacity={0.3}
/>
<ChartTooltip content={<ChartTooltipContent />} />
</RadarChart>
</ChartContainer>
</div>
</div>
</div>
</>
)}
</div>
);
}
function getMin(grades: number[][]) {
return Math.max(
Math.round(
Math.min(...grades.map((g) => Math.min(...g)).filter(Boolean)) - 1
),
0
);
}
function getMax(grades: number[][]) {
return Math.min(
Math.round(
Math.max(...grades.map((g) => Math.max(...g)).filter(Boolean)) + 1
),
20
);
}

View file

@ -17,6 +17,7 @@ import {
Send, Send,
AlertTriangle, AlertTriangle,
Loader, Loader,
Save,
} from "lucide-react"; } from "lucide-react";
import InstallApp, { isInstalled } from "./install-app"; import InstallApp, { isInstalled } from "./install-app";
import { import {
@ -280,7 +281,7 @@ export default function NotificationSettings() {
}) })
} }
> >
<Send className="h-4 w-4 mr-2" /> <Save className="h-4 w-4 mr-2" />
Enregistrer Enregistrer
</Button> </Button>
</div> </div>

353
app/components/ui/chart.tsx Normal file
View file

@ -0,0 +1,353 @@
"use client";
import * as React from "react";
import * as RechartsPrimitive from "recharts";
import { cn } from "~/lib/utils";
// Format: { THEME_NAME: CSS_SELECTOR }
const THEMES = { light: "", dark: ".dark" } as const;
export type ChartConfig = {
[k in string]: {
label?: React.ReactNode;
icon?: React.ComponentType;
} & (
| { color?: string; theme?: never }
| { color?: never; theme: Record<keyof typeof THEMES, string> }
);
};
type ChartContextProps = {
config: ChartConfig;
};
const ChartContext = React.createContext<ChartContextProps | null>(null);
function useChart() {
const context = React.useContext(ChartContext);
if (!context) {
throw new Error("useChart must be used within a <ChartContainer />");
}
return context;
}
function ChartContainer({
id,
className,
children,
config,
...props
}: React.ComponentProps<"div"> & {
config: ChartConfig;
children: React.ComponentProps<
typeof RechartsPrimitive.ResponsiveContainer
>["children"];
}) {
const uniqueId = React.useId();
const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`;
return (
<ChartContext.Provider value={{ config }}>
<div
data-slot="chart"
data-chart={chartId}
className={cn(
"[&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground [&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-muted [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-reference-line_[stroke='#ccc']]:stroke-border flex aspect-video justify-center text-xs [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-hidden [&_.recharts-sector]:outline-hidden [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-surface]:outline-hidden",
className
)}
{...props}
>
<ChartStyle id={chartId} config={config} />
<RechartsPrimitive.ResponsiveContainer>
{children}
</RechartsPrimitive.ResponsiveContainer>
</div>
</ChartContext.Provider>
);
}
const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
const colorConfig = Object.entries(config).filter(
([, config]) => config.theme || config.color
);
if (!colorConfig.length) {
return null;
}
return (
<style
dangerouslySetInnerHTML={{
__html: Object.entries(THEMES)
.map(
([theme, prefix]) => `
${prefix} [data-chart=${id}] {
${colorConfig
.map(([key, itemConfig]) => {
const color =
itemConfig.theme?.[theme as keyof typeof itemConfig.theme] ||
itemConfig.color;
return color ? ` --color-${key}: ${color};` : null;
})
.join("\n")}
}
`
)
.join("\n"),
}}
/>
);
};
const ChartTooltip = RechartsPrimitive.Tooltip;
function ChartTooltipContent({
active,
payload,
className,
indicator = "dot",
hideLabel = false,
hideIndicator = false,
label,
labelFormatter,
labelClassName,
formatter,
color,
nameKey,
labelKey,
}: React.ComponentProps<typeof RechartsPrimitive.Tooltip> &
React.ComponentProps<"div"> & {
hideLabel?: boolean;
hideIndicator?: boolean;
indicator?: "line" | "dot" | "dashed";
nameKey?: string;
labelKey?: string;
}) {
const { config } = useChart();
const tooltipLabel = React.useMemo(() => {
if (hideLabel || !payload?.length) {
return null;
}
const [item] = payload;
const key = `${labelKey || item?.dataKey || item?.name || "value"}`;
const itemConfig = getPayloadConfigFromPayload(config, item, key);
const value =
!labelKey && typeof label === "string"
? config[label as keyof typeof config]?.label || label
: itemConfig?.label;
if (labelFormatter) {
return (
<div className={cn("font-medium", labelClassName)}>
{labelFormatter(value, payload)}
</div>
);
}
if (!value) {
return null;
}
return <div className={cn("font-medium", labelClassName)}>{value}</div>;
}, [
label,
labelFormatter,
payload,
hideLabel,
labelClassName,
config,
labelKey,
]);
if (!active || !payload?.length) {
return null;
}
const nestLabel = payload.length === 1 && indicator !== "dot";
return (
<div
className={cn(
"border-border/50 bg-background grid min-w-[8rem] items-start gap-1.5 rounded-lg border px-2.5 py-1.5 text-xs shadow-xl",
className
)}
>
{!nestLabel ? tooltipLabel : null}
<div className="grid gap-1.5">
{payload.map((item, index) => {
const key = `${nameKey || item.name || item.dataKey || "value"}`;
const itemConfig = getPayloadConfigFromPayload(config, item, key);
const indicatorColor = color || item.payload.fill || item.color;
return (
<div
key={item.dataKey}
className={cn(
"[&>svg]:text-muted-foreground flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5",
indicator === "dot" && "items-center"
)}
>
{formatter && item?.value !== undefined && item.name ? (
formatter(item.value, item.name, item, index, item.payload)
) : (
<>
{itemConfig?.icon ? (
<itemConfig.icon />
) : (
!hideIndicator && (
<div
className={cn(
"shrink-0 rounded-[2px] border-(--color-border) bg-(--color-bg)",
{
"h-2.5 w-2.5": indicator === "dot",
"w-1": indicator === "line",
"w-0 border-[1.5px] border-dashed bg-transparent":
indicator === "dashed",
"my-0.5": nestLabel && indicator === "dashed",
}
)}
style={
{
"--color-bg": indicatorColor,
"--color-border": indicatorColor,
} as React.CSSProperties
}
/>
)
)}
<div
className={cn(
"flex flex-1 justify-between leading-none",
nestLabel ? "items-end" : "items-center"
)}
>
<div className="grid gap-1.5">
{nestLabel ? tooltipLabel : null}
<span className="text-muted-foreground">
{itemConfig?.label || item.name}
</span>
</div>
{item.value && (
<span className="text-foreground font-mono font-medium tabular-nums">
{item.value.toLocaleString()}
</span>
)}
</div>
</>
)}
</div>
);
})}
</div>
</div>
);
}
const ChartLegend = RechartsPrimitive.Legend;
function ChartLegendContent({
className,
hideIcon = false,
payload,
verticalAlign = "bottom",
nameKey,
}: React.ComponentProps<"div"> &
Pick<RechartsPrimitive.LegendProps, "payload" | "verticalAlign"> & {
hideIcon?: boolean;
nameKey?: string;
}) {
const { config } = useChart();
if (!payload?.length) {
return null;
}
return (
<div
className={cn(
"flex items-center justify-center gap-4",
verticalAlign === "top" ? "pb-3" : "pt-3",
className
)}
>
{payload.map((item) => {
const key = `${nameKey || item.dataKey || "value"}`;
const itemConfig = getPayloadConfigFromPayload(config, item, key);
return (
<div
key={item.value}
className={cn(
"[&>svg]:text-muted-foreground flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3"
)}
>
{itemConfig?.icon && !hideIcon ? (
<itemConfig.icon />
) : (
<div
className="h-2 w-2 shrink-0 rounded-[2px]"
style={{
backgroundColor: item.color,
}}
/>
)}
{itemConfig?.label}
</div>
);
})}
</div>
);
}
// Helper to extract item config from a payload.
function getPayloadConfigFromPayload(
config: ChartConfig,
payload: unknown,
key: string
) {
if (typeof payload !== "object" || payload === null) {
return undefined;
}
const payloadPayload =
"payload" in payload &&
typeof payload.payload === "object" &&
payload.payload !== null
? payload.payload
: undefined;
let configLabelKey: string = key;
if (
key in payload &&
typeof payload[key as keyof typeof payload] === "string"
) {
configLabelKey = payload[key as keyof typeof payload] as string;
} else if (
payloadPayload &&
key in payloadPayload &&
typeof payloadPayload[key as keyof typeof payloadPayload] === "string"
) {
configLabelKey = payloadPayload[
key as keyof typeof payloadPayload
] as string;
}
return configLabelKey in config
? config[configLabelKey]
: config[key as keyof typeof config];
}
export {
ChartContainer,
ChartTooltip,
ChartTooltipContent,
ChartLegend,
ChartLegendContent,
ChartStyle,
};

View file

@ -383,3 +383,68 @@ export const testNotification = async (id: string) => {
"Échec de l'envoi de la notification de test" "Échec de l'envoi de la notification de test"
); );
}; };
/**
* === GRADES API ===
*/
export const getGrades = async (period: string) => {
return makeRequest(
`/grades?period=${encodeURIComponent(period)}`,
"Échec de la récupération des notes"
);
};
export const useGrades = (period: string) => {
const { data, ...props } = useQuery({
queryKey: ["grades", period],
queryFn: () => getGrades(period),
staleTime: Duration.fromObject({
hours: 0, // 1 hour
}).toMillis(),
gcTime: Duration.fromObject({
days: 3, // 3 days
}).toMillis(),
});
const grades = (data?.grades as PeriodResult[]) || [];
const subjects = (data?.subjects as string[]) || [];
return {
grades,
subjects,
...props,
};
};
interface SubjectPerformance {
subject: string;
average: number;
}
interface PeriodResult {
period: string;
average: number;
}
export const getAverages = async (period: string) => {
return makeRequest(
`/averages?period=${encodeURIComponent(period)}`,
"Échec de la récupération des moyennes"
);
};
export const useAverages = (period: string) => {
const { data, ...props } = useQuery({
queryKey: ["averages", period],
queryFn: () => getAverages(period),
staleTime: Duration.fromObject({
hours: 0, // 1 hour
}).toMillis(),
gcTime: Duration.fromObject({
days: 3, // 3 days
}).toMillis(),
});
const subjectAverages = (data?.subjectAverages as SubjectPerformance[]) || [];
const globalAverage = data?.globalAverage || 0;
return {
subjectAverages,
globalAverage,
...props,
};
};

View file

@ -7,4 +7,6 @@ export default [
route("/register", "routes/register.tsx"), route("/register", "routes/register.tsx"),
route("/colles/:colleId", "routes/colles.tsx"), route("/colles/:colleId", "routes/colles.tsx"),
route("/settings", "routes/settings.tsx"), route("/settings", "routes/settings.tsx"),
route("/grades", "routes/grades.tsx"),
route("/repas", "routes/repas.tsx"),
] satisfies RouteConfig; ] satisfies RouteConfig;

35
app/routes/grades.tsx Normal file
View file

@ -0,0 +1,35 @@
import { Navigate } from "react-router";
import BottomNavigation from "~/components/bottom-nav";
import Error from "~/components/error";
import GradesPage from "~/components/grades";
import Loader from "~/components/loader";
import { MainLayout } from "~/layout";
import { AUTH_ERROR, useUser } from "~/lib/api";
import { forceReload } from "~/lib/utils";
export default function Grade() {
const { user, isLoading, error } = useUser();
if (isLoading) {
return <Loader />;
}
if (error?.message === AUTH_ERROR) {
return <Navigate to="/login" replace />;
}
if (error) {
return <Error message={error.message} />;
}
return (
<MainLayout
header={
<h1 className="text-2xl font-bold" onClick={forceReload}>
Khollis&eacute; - {user.className}
</h1>
}
>
<GradesPage user={user} />
<BottomNavigation activeId="grades" />
</MainLayout>
);
}

53
app/routes/repas.tsx Normal file
View file

@ -0,0 +1,53 @@
import { Navigate } from "react-router";
import Error from "~/components/error";
import Loader from "~/components/loader";
import { MainLayout } from "~/layout";
import { AUTH_ERROR, useUser } from "~/lib/api";
import { forceReload } from "~/lib/utils";
import { Button } from "~/components/ui/button";
import { Github } from "lucide-react";
import BottomNavigation from "~/components/bottom-nav";
export default function Repas() {
const { user, isLoading, error } = useUser();
if (isLoading) {
return <Loader />;
}
if (error?.message === AUTH_ERROR) {
return <Navigate to="/login" replace />;
}
if (error) {
return <Error message={error.message} />;
}
return (
<MainLayout
header={
<h1 className="text-2xl font-bold" onClick={forceReload}>
Khollis&eacute; - {user.className}
</h1>
}
>
{/* Under construction message */}
<div className="text-center mt-10">
<h2 className="text-xl font-semibold">
Cette fonctionnalité nest pas encore implémentée.
</h2>
<p className="mt-4">
Vous pouvez contribuer au développement du projet ici :
</p>
<Button
className="mt-4"
variant="default"
onClick={() => window.open(import.meta.env.VITE_GITHUB_URL, "_blank")}
>
<Github />
Contribuer sur GitHub
</Button>
</div>
<BottomNavigation activeId="repas" />
</MainLayout>
);
}

View file

@ -6,7 +6,7 @@ import { MainLayout } from "~/layout";
import { AUTH_ERROR, useUser } from "~/lib/api"; import { AUTH_ERROR, useUser } from "~/lib/api";
import { forceReload } from "~/lib/utils"; import { forceReload } from "~/lib/utils";
export default function Home() { export default function Settings() {
const { user, isLoading, error } = useUser(); const { user, isLoading, error } = useUser();
if (isLoading) { if (isLoading) {

View file

@ -46,6 +46,7 @@
"react-dom": "^19.1.0", "react-dom": "^19.1.0",
"react-latex-next": "^3.0.0", "react-latex-next": "^3.0.0",
"react-router": "^7.5.3", "react-router": "^7.5.3",
"recharts": "^3.1.2",
"sonner": "^2.0.6", "sonner": "^2.0.6",
"tailwind-merge": "^3.3.0", "tailwind-merge": "^3.3.0",
"tw-animate-css": "^1.3.0" "tw-animate-css": "^1.3.0"

298
pnpm-lock.yaml generated
View file

@ -116,6 +116,9 @@ importers:
react-router: react-router:
specifier: ^7.5.3 specifier: ^7.5.3
version: 7.7.1(react-dom@19.1.1(react@19.1.1))(react@19.1.1) version: 7.7.1(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
recharts:
specifier: ^3.1.2
version: 3.1.2(@types/react@19.1.8)(react-dom@19.1.1(react@19.1.1))(react-is@19.1.1)(react@19.1.1)(redux@5.0.1)
sonner: sonner:
specifier: ^2.0.6 specifier: ^2.0.6
version: 2.0.6(react-dom@19.1.1(react@19.1.1))(react@19.1.1) version: 2.0.6(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
@ -2173,6 +2176,17 @@ packages:
peerDependencies: peerDependencies:
react-router: 7.7.1 react-router: 7.7.1
'@reduxjs/toolkit@2.8.2':
resolution: {integrity: sha512-MYlOhQ0sLdw4ud48FoC5w0dH9VfWQjtCjreKwYTT3l+r427qYC5Y8PihNutepr8XrNaBUDQo9khWUwQxZaqt5A==}
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
'@rollup/plugin-babel@5.3.1': '@rollup/plugin-babel@5.3.1':
resolution: {integrity: sha512-WFfdLWU/xVWKeRQnKmIAQULUI7Il0gZnBIH/ZFO069wYIfPu+8zrfp/KMW0atmELoRDq8FbiP3VCss9MhCut7Q==} resolution: {integrity: sha512-WFfdLWU/xVWKeRQnKmIAQULUI7Il0gZnBIH/ZFO069wYIfPu+8zrfp/KMW0atmELoRDq8FbiP3VCss9MhCut7Q==}
engines: {node: '>= 10.0.0'} engines: {node: '>= 10.0.0'}
@ -2341,6 +2355,12 @@ packages:
resolution: {integrity: sha512-doH1gimEu3A46VX6aVxpHTeHrytJAG6HgdxntYnCFiIFHEM/ZGpG8KiZGBChchjQmG0XFIBL552kBTjVcMZXwQ==} resolution: {integrity: sha512-doH1gimEu3A46VX6aVxpHTeHrytJAG6HgdxntYnCFiIFHEM/ZGpG8KiZGBChchjQmG0XFIBL552kBTjVcMZXwQ==}
engines: {node: '>=12'} engines: {node: '>=12'}
'@standard-schema/spec@1.0.0':
resolution: {integrity: sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==}
'@standard-schema/utils@0.3.0':
resolution: {integrity: sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==}
'@surma/rollup-plugin-off-main-thread@2.2.3': '@surma/rollup-plugin-off-main-thread@2.2.3':
resolution: {integrity: sha512-lR8q/9W7hZpMWweNiAKU7NQerBnzQQLvi8qnTDU/fxItPhtZVMbPV3lbCwjhIlNBe9Bbr5V+KHshvWmVSG9cxQ==} resolution: {integrity: sha512-lR8q/9W7hZpMWweNiAKU7NQerBnzQQLvi8qnTDU/fxItPhtZVMbPV3lbCwjhIlNBe9Bbr5V+KHshvWmVSG9cxQ==}
@ -2473,6 +2493,33 @@ packages:
'@tsconfig/node16@1.0.4': '@tsconfig/node16@1.0.4':
resolution: {integrity: sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==} resolution: {integrity: sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==}
'@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/estree@0.0.39': '@types/estree@0.0.39':
resolution: {integrity: sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==} resolution: {integrity: sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==}
@ -2514,6 +2561,9 @@ packages:
'@types/trusted-types@2.0.7': '@types/trusted-types@2.0.7':
resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==}
'@types/use-sync-external-store@0.0.6':
resolution: {integrity: sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==}
'@types/yauzl@2.10.3': '@types/yauzl@2.10.3':
resolution: {integrity: sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==} resolution: {integrity: sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==}
@ -3197,6 +3247,50 @@ packages:
cyclist@1.0.2: cyclist@1.0.2:
resolution: {integrity: sha512-0sVXIohTfLqVIW3kb/0n6IiWF3Ifj5nm2XaSrLq2DI6fKIGa2fYAZdk917rUneaeLVpYfFcyXE2ft0fe3remsA==} resolution: {integrity: sha512-0sVXIohTfLqVIW3kb/0n6IiWF3Ifj5nm2XaSrLq2DI6fKIGa2fYAZdk917rUneaeLVpYfFcyXE2ft0fe3remsA==}
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'}
data-uri-to-buffer@4.0.1: data-uri-to-buffer@4.0.1:
resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==} resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==}
engines: {node: '>= 12'} engines: {node: '>= 12'}
@ -3239,6 +3333,9 @@ packages:
decache@4.6.2: decache@4.6.2:
resolution: {integrity: sha512-2LPqkLeu8XWHU8qNCS3kcF6sCcb5zIzvWaAHYSvPfwhdd7mHuah29NssMzrTYyHN4F5oFy2ko9OBYxegtU0FEw==} resolution: {integrity: sha512-2LPqkLeu8XWHU8qNCS3kcF6sCcb5zIzvWaAHYSvPfwhdd7mHuah29NssMzrTYyHN4F5oFy2ko9OBYxegtU0FEw==}
decimal.js-light@2.5.1:
resolution: {integrity: sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==}
decompress-response@6.0.0: decompress-response@6.0.0:
resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==} resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==}
engines: {node: '>=10'} engines: {node: '>=10'}
@ -3493,6 +3590,9 @@ packages:
resolution: {integrity: sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==} resolution: {integrity: sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
es-toolkit@1.39.10:
resolution: {integrity: sha512-E0iGnTtbDhkeczB0T+mxmoVlT4YNweEKBLq7oaU4p11mecdsZpNWOglI4895Vh4usbQ+LsJiuLuI2L0Vdmfm2w==}
esbuild@0.25.6: esbuild@0.25.6:
resolution: {integrity: sha512-GVuzuUwtdsghE3ocJ9Bs8PNoF13HNQ5TXbEi2AhvVb8xU1Iwt9Fos9FEamfoee+u/TOsn7GUWc04lz46n2bbTg==} resolution: {integrity: sha512-GVuzuUwtdsghE3ocJ9Bs8PNoF13HNQ5TXbEi2AhvVb8xU1Iwt9Fos9FEamfoee+u/TOsn7GUWc04lz46n2bbTg==}
engines: {node: '>=18'} engines: {node: '>=18'}
@ -3561,6 +3661,9 @@ packages:
eventemitter3@4.0.7: eventemitter3@4.0.7:
resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==} resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==}
eventemitter3@5.0.1:
resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==}
events@3.3.0: events@3.3.0:
resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==}
engines: {node: '>=0.8.x'} engines: {node: '>=0.8.x'}
@ -4044,6 +4147,9 @@ packages:
engines: {node: '>=16.x'} engines: {node: '>=16.x'}
hasBin: true hasBin: true
immer@10.1.1:
resolution: {integrity: sha512-s2MPrmjovJcoMaHtx6K11Ra7oD05NT97w1IC5zpMkT6Atjr7H8LjaDd81iIxUYpMKSRRNMJE703M1Fhr/TctHw==}
imurmurhash@0.1.4: imurmurhash@0.1.4:
resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==}
engines: {node: '>=0.8.19'} engines: {node: '>=0.8.19'}
@ -4093,6 +4199,10 @@ packages:
resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==} resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
internmap@2.0.3:
resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==}
engines: {node: '>=12'}
ipaddr.js@1.9.1: ipaddr.js@1.9.1:
resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==}
engines: {node: '>= 0.10'} engines: {node: '>= 0.10'}
@ -5300,6 +5410,9 @@ packages:
peerDependencies: peerDependencies:
react: ^19.1.1 react: ^19.1.1
react-is@19.1.1:
resolution: {integrity: sha512-tr41fA15Vn8p4X9ntI+yCyeGSf1TlYaY5vlTZfQmeLBrFo3psOPX6HhTDnFNL9uj3EhP0KAQ80cugCl4b4BERA==}
react-latex-next@3.0.0: react-latex-next@3.0.0:
resolution: {integrity: sha512-x70f1b1G7TronVigsRgKHKYYVUNfZk/3bciFyYX1lYLQH2y3/TXku3+5Vap8MDbJhtopePSYBsYWS6jhzIdz+g==} resolution: {integrity: sha512-x70f1b1G7TronVigsRgKHKYYVUNfZk/3bciFyYX1lYLQH2y3/TXku3+5Vap8MDbJhtopePSYBsYWS6jhzIdz+g==}
engines: {node: '>=12', npm: '>=5'} engines: {node: '>=12', npm: '>=5'}
@ -5307,6 +5420,18 @@ packages:
react: ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 react: ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0
react-dom: ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 react-dom: ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0
react-redux@9.2.0:
resolution: {integrity: sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==}
peerDependencies:
'@types/react': ^18.2.25 || ^19
react: ^18.0 || ^19
redux: ^5.0.0
peerDependenciesMeta:
'@types/react':
optional: true
redux:
optional: true
react-refresh@0.14.2: react-refresh@0.14.2:
resolution: {integrity: sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==} resolution: {integrity: sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
@ -5389,6 +5514,22 @@ packages:
resolution: {integrity: sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==} resolution: {integrity: sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==}
engines: {node: '>= 12.13.0'} engines: {node: '>= 12.13.0'}
recharts@3.1.2:
resolution: {integrity: sha512-vhNbYwaxNbk/IATK0Ki29k3qvTkGqwvCgyQAQ9MavvvBwjvKnMTswdbklJpcOAoMPN/qxF3Lyqob0zO+ZXkZ4g==}
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
redux-thunk@3.1.0:
resolution: {integrity: sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==}
peerDependencies:
redux: ^5.0.0
redux@5.0.1:
resolution: {integrity: sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==}
reflect.getprototypeof@1.0.10: reflect.getprototypeof@1.0.10:
resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==} resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
@ -5444,6 +5585,9 @@ packages:
requires-port@1.0.0: requires-port@1.0.0:
resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==} resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==}
reselect@5.1.1:
resolution: {integrity: sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==}
resolve-alpn@1.2.1: resolve-alpn@1.2.1:
resolution: {integrity: sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==} resolution: {integrity: sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==}
@ -5890,6 +6034,9 @@ packages:
through@2.3.8: through@2.3.8:
resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==} resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==}
tiny-invariant@1.3.3:
resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==}
tinyglobby@0.2.14: tinyglobby@0.2.14:
resolution: {integrity: sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==} resolution: {integrity: sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==}
engines: {node: '>=12.0.0'} engines: {node: '>=12.0.0'}
@ -6236,6 +6383,9 @@ packages:
resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==}
engines: {node: '>= 0.8'} engines: {node: '>= 0.8'}
victory-vendor@37.3.6:
resolution: {integrity: sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ==}
vite-node@3.2.4: vite-node@3.2.4:
resolution: {integrity: sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==} resolution: {integrity: sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==}
engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0}
@ -8780,6 +8930,18 @@ snapshots:
- supports-color - supports-color
- typescript - typescript
'@reduxjs/toolkit@2.8.2(react-redux@9.2.0(@types/react@19.1.8)(react@19.1.1)(redux@5.0.1))(react@19.1.1)':
dependencies:
'@standard-schema/spec': 1.0.0
'@standard-schema/utils': 0.3.0
immer: 10.1.1
redux: 5.0.1
redux-thunk: 3.1.0(redux@5.0.1)
reselect: 5.1.1
optionalDependencies:
react: 19.1.1
react-redux: 9.2.0(@types/react@19.1.8)(react@19.1.1)(redux@5.0.1)
'@rollup/plugin-babel@5.3.1(@babel/core@7.28.0)(rollup@2.79.2)': '@rollup/plugin-babel@5.3.1(@babel/core@7.28.0)(rollup@2.79.2)':
dependencies: dependencies:
'@babel/core': 7.28.0 '@babel/core': 7.28.0
@ -8903,6 +9065,10 @@ snapshots:
dependencies: dependencies:
escape-string-regexp: 5.0.0 escape-string-regexp: 5.0.0
'@standard-schema/spec@1.0.0': {}
'@standard-schema/utils@0.3.0': {}
'@surma/rollup-plugin-off-main-thread@2.2.3': '@surma/rollup-plugin-off-main-thread@2.2.3':
dependencies: dependencies:
ejs: 3.1.10 ejs: 3.1.10
@ -9016,6 +9182,30 @@ snapshots:
'@tsconfig/node16@1.0.4': {} '@tsconfig/node16@1.0.4': {}
'@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/estree@0.0.39': {} '@types/estree@0.0.39': {}
'@types/estree@1.0.8': {} '@types/estree@1.0.8': {}
@ -9050,6 +9240,8 @@ snapshots:
'@types/trusted-types@2.0.7': {} '@types/trusted-types@2.0.7': {}
'@types/use-sync-external-store@0.0.6': {}
'@types/yauzl@2.10.3': '@types/yauzl@2.10.3':
dependencies: dependencies:
'@types/node': 20.19.9 '@types/node': 20.19.9
@ -9834,6 +10026,44 @@ snapshots:
cyclist@1.0.2: {} cyclist@1.0.2: {}
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: {}
data-uri-to-buffer@4.0.1: {} data-uri-to-buffer@4.0.1: {}
data-view-buffer@1.0.2: data-view-buffer@1.0.2:
@ -9872,6 +10102,8 @@ snapshots:
dependencies: dependencies:
callsite: 1.0.0 callsite: 1.0.0
decimal.js-light@2.5.1: {}
decompress-response@6.0.0: decompress-response@6.0.0:
dependencies: dependencies:
mimic-response: 3.1.0 mimic-response: 3.1.0
@ -10151,6 +10383,8 @@ snapshots:
is-date-object: 1.1.0 is-date-object: 1.1.0
is-symbol: 1.1.1 is-symbol: 1.1.1
es-toolkit@1.39.10: {}
esbuild@0.25.6: esbuild@0.25.6:
optionalDependencies: optionalDependencies:
'@esbuild/aix-ppc64': 0.25.6 '@esbuild/aix-ppc64': 0.25.6
@ -10245,6 +10479,8 @@ snapshots:
eventemitter3@4.0.7: {} eventemitter3@4.0.7: {}
eventemitter3@5.0.1: {}
events@3.3.0: {} events@3.3.0: {}
execa@5.1.1: execa@5.1.1:
@ -10816,6 +11052,8 @@ snapshots:
image-size@2.0.2: {} image-size@2.0.2: {}
immer@10.1.1: {}
imurmurhash@0.1.4: {} imurmurhash@0.1.4: {}
indent-string@5.0.0: {} indent-string@5.0.0: {}
@ -10875,6 +11113,8 @@ snapshots:
hasown: 2.0.2 hasown: 2.0.2
side-channel: 1.1.0 side-channel: 1.1.0
internmap@2.0.3: {}
ipaddr.js@1.9.1: {} ipaddr.js@1.9.1: {}
ipx@3.1.1(@netlify/blobs@10.0.7)(idb-keyval@6.2.2): ipx@3.1.1(@netlify/blobs@10.0.7)(idb-keyval@6.2.2):
@ -12128,12 +12368,23 @@ snapshots:
react: 19.1.1 react: 19.1.1
scheduler: 0.26.0 scheduler: 0.26.0
react-is@19.1.1: {}
react-latex-next@3.0.0(react-dom@19.1.1(react@19.1.1))(react@19.1.1): react-latex-next@3.0.0(react-dom@19.1.1(react@19.1.1))(react@19.1.1):
dependencies: dependencies:
katex: 0.16.22 katex: 0.16.22
react: 19.1.1 react: 19.1.1
react-dom: 19.1.1(react@19.1.1) react-dom: 19.1.1(react@19.1.1)
react-redux@9.2.0(@types/react@19.1.8)(react@19.1.1)(redux@5.0.1):
dependencies:
'@types/use-sync-external-store': 0.0.6
react: 19.1.1
use-sync-external-store: 1.5.0(react@19.1.1)
optionalDependencies:
'@types/react': 19.1.8
redux: 5.0.1
react-refresh@0.14.2: {} react-refresh@0.14.2: {}
react-remove-scroll-bar@2.3.8(@types/react@19.1.8)(react@19.1.1): react-remove-scroll-bar@2.3.8(@types/react@19.1.8)(react@19.1.1):
@ -12223,6 +12474,32 @@ snapshots:
real-require@0.2.0: {} real-require@0.2.0: {}
recharts@3.1.2(@types/react@19.1.8)(react-dom@19.1.1(react@19.1.1))(react-is@19.1.1)(react@19.1.1)(redux@5.0.1):
dependencies:
'@reduxjs/toolkit': 2.8.2(react-redux@9.2.0(@types/react@19.1.8)(react@19.1.1)(redux@5.0.1))(react@19.1.1)
clsx: 2.1.1
decimal.js-light: 2.5.1
es-toolkit: 1.39.10
eventemitter3: 5.0.1
immer: 10.1.1
react: 19.1.1
react-dom: 19.1.1(react@19.1.1)
react-is: 19.1.1
react-redux: 9.2.0(@types/react@19.1.8)(react@19.1.1)(redux@5.0.1)
reselect: 5.1.1
tiny-invariant: 1.3.3
use-sync-external-store: 1.5.0(react@19.1.1)
victory-vendor: 37.3.6
transitivePeerDependencies:
- '@types/react'
- redux
redux-thunk@3.1.0(redux@5.0.1):
dependencies:
redux: 5.0.1
redux@5.0.1: {}
reflect.getprototypeof@1.0.10: reflect.getprototypeof@1.0.10:
dependencies: dependencies:
call-bind: 1.0.8 call-bind: 1.0.8
@ -12284,6 +12561,8 @@ snapshots:
requires-port@1.0.0: {} requires-port@1.0.0: {}
reselect@5.1.1: {}
resolve-alpn@1.2.1: {} resolve-alpn@1.2.1: {}
resolve-from@5.0.0: {} resolve-from@5.0.0: {}
@ -12823,6 +13102,8 @@ snapshots:
through@2.3.8: {} through@2.3.8: {}
tiny-invariant@1.3.3: {}
tinyglobby@0.2.14: tinyglobby@0.2.14:
dependencies: dependencies:
fdir: 6.4.6(picomatch@4.0.3) fdir: 6.4.6(picomatch@4.0.3)
@ -13093,6 +13374,23 @@ snapshots:
vary@1.1.2: {} vary@1.1.2: {}
victory-vendor@37.3.6:
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
vite-node@3.2.4(@types/node@20.19.9)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.0): vite-node@3.2.4(@types/node@20.19.9)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.0):
dependencies: dependencies:
cac: 6.7.14 cac: 6.7.14

View file

@ -17,4 +17,20 @@ async function onPush(event) {
} }
} }
// self.addEventListener("notificationclick", function (event) {}); const BASE_URL = "https://khollise.fr";
self.addEventListener("notificationclick", function (event) {
const clickedNotification = event.notification;
clickedNotification.close();
if (event.action == "open" && event.data.id) {
const promiseChain = clients.openWindow(
BASE_URL + "/colles/" + event.data.id
);
event.waitUntil(promiseChain);
return;
}
const promiseChain = clients.openWindow(BASE_URL);
event.waitUntil(promiseChain);
});