Compare commits
6 commits
aa88fbda44
...
b9773de805
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b9773de805 | ||
|
|
7ccd36a7c4 | ||
|
|
3e01fa47cd | ||
|
|
e6ba7e0d9a | ||
|
|
dc5e10efd1 | ||
|
|
eb8bb8331f |
13 changed files with 1124 additions and 4 deletions
41
app/app.css
41
app/app.css
|
|
@ -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,
|
||||
body {
|
||||
@apply w-full h-full;
|
||||
|
|
@ -135,3 +153,26 @@ body {
|
|||
[data-sonner-toaster][data-sonner-theme='dark'] [data-description] {
|
||||
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');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ export default function AttachmentItem({ attachment }: { attachment: Attachment
|
|||
target="_blank"
|
||||
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">
|
||||
{attachment.name}
|
||||
</span>
|
||||
|
|
|
|||
255
app/components/grades/index.tsx
Normal file
255
app/components/grades/index.tsx
Normal 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
|
||||
);
|
||||
}
|
||||
|
|
@ -17,6 +17,7 @@ import {
|
|||
Send,
|
||||
AlertTriangle,
|
||||
Loader,
|
||||
Save,
|
||||
} from "lucide-react";
|
||||
import InstallApp, { isInstalled } from "./install-app";
|
||||
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
|
||||
</Button>
|
||||
</div>
|
||||
|
|
|
|||
353
app/components/ui/chart.tsx
Normal file
353
app/components/ui/chart.tsx
Normal 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,
|
||||
};
|
||||
|
|
@ -383,3 +383,68 @@ export const testNotification = async (id: string) => {
|
|||
"É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,
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -7,4 +7,6 @@ export default [
|
|||
route("/register", "routes/register.tsx"),
|
||||
route("/colles/:colleId", "routes/colles.tsx"),
|
||||
route("/settings", "routes/settings.tsx"),
|
||||
route("/grades", "routes/grades.tsx"),
|
||||
route("/repas", "routes/repas.tsx"),
|
||||
] satisfies RouteConfig;
|
||||
|
|
|
|||
35
app/routes/grades.tsx
Normal file
35
app/routes/grades.tsx
Normal 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é - {user.className} ⚔️
|
||||
</h1>
|
||||
}
|
||||
>
|
||||
<GradesPage user={user} />
|
||||
<BottomNavigation activeId="grades" />
|
||||
</MainLayout>
|
||||
);
|
||||
}
|
||||
53
app/routes/repas.tsx
Normal file
53
app/routes/repas.tsx
Normal 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é - {user.className} ⚔️
|
||||
</h1>
|
||||
}
|
||||
>
|
||||
{/* Under construction message */}
|
||||
<div className="text-center mt-10">
|
||||
<h2 className="text-xl font-semibold">
|
||||
⚠️ Cette fonctionnalité n’est 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>
|
||||
);
|
||||
}
|
||||
|
|
@ -6,7 +6,7 @@ import { MainLayout } from "~/layout";
|
|||
import { AUTH_ERROR, useUser } from "~/lib/api";
|
||||
import { forceReload } from "~/lib/utils";
|
||||
|
||||
export default function Home() {
|
||||
export default function Settings() {
|
||||
const { user, isLoading, error } = useUser();
|
||||
|
||||
if (isLoading) {
|
||||
|
|
|
|||
|
|
@ -46,6 +46,7 @@
|
|||
"react-dom": "^19.1.0",
|
||||
"react-latex-next": "^3.0.0",
|
||||
"react-router": "^7.5.3",
|
||||
"recharts": "^3.1.2",
|
||||
"sonner": "^2.0.6",
|
||||
"tailwind-merge": "^3.3.0",
|
||||
"tw-animate-css": "^1.3.0"
|
||||
|
|
|
|||
298
pnpm-lock.yaml
generated
298
pnpm-lock.yaml
generated
|
|
@ -116,6 +116,9 @@ importers:
|
|||
react-router:
|
||||
specifier: ^7.5.3
|
||||
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:
|
||||
specifier: ^2.0.6
|
||||
version: 2.0.6(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
|
||||
|
|
@ -2173,6 +2176,17 @@ packages:
|
|||
peerDependencies:
|
||||
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':
|
||||
resolution: {integrity: sha512-WFfdLWU/xVWKeRQnKmIAQULUI7Il0gZnBIH/ZFO069wYIfPu+8zrfp/KMW0atmELoRDq8FbiP3VCss9MhCut7Q==}
|
||||
engines: {node: '>= 10.0.0'}
|
||||
|
|
@ -2341,6 +2355,12 @@ packages:
|
|||
resolution: {integrity: sha512-doH1gimEu3A46VX6aVxpHTeHrytJAG6HgdxntYnCFiIFHEM/ZGpG8KiZGBChchjQmG0XFIBL552kBTjVcMZXwQ==}
|
||||
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':
|
||||
resolution: {integrity: sha512-lR8q/9W7hZpMWweNiAKU7NQerBnzQQLvi8qnTDU/fxItPhtZVMbPV3lbCwjhIlNBe9Bbr5V+KHshvWmVSG9cxQ==}
|
||||
|
||||
|
|
@ -2473,6 +2493,33 @@ packages:
|
|||
'@tsconfig/node16@1.0.4':
|
||||
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':
|
||||
resolution: {integrity: sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==}
|
||||
|
||||
|
|
@ -2514,6 +2561,9 @@ packages:
|
|||
'@types/trusted-types@2.0.7':
|
||||
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':
|
||||
resolution: {integrity: sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==}
|
||||
|
||||
|
|
@ -3197,6 +3247,50 @@ packages:
|
|||
cyclist@1.0.2:
|
||||
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:
|
||||
resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==}
|
||||
engines: {node: '>= 12'}
|
||||
|
|
@ -3239,6 +3333,9 @@ packages:
|
|||
decache@4.6.2:
|
||||
resolution: {integrity: sha512-2LPqkLeu8XWHU8qNCS3kcF6sCcb5zIzvWaAHYSvPfwhdd7mHuah29NssMzrTYyHN4F5oFy2ko9OBYxegtU0FEw==}
|
||||
|
||||
decimal.js-light@2.5.1:
|
||||
resolution: {integrity: sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==}
|
||||
|
||||
decompress-response@6.0.0:
|
||||
resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==}
|
||||
engines: {node: '>=10'}
|
||||
|
|
@ -3493,6 +3590,9 @@ packages:
|
|||
resolution: {integrity: sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
es-toolkit@1.39.10:
|
||||
resolution: {integrity: sha512-E0iGnTtbDhkeczB0T+mxmoVlT4YNweEKBLq7oaU4p11mecdsZpNWOglI4895Vh4usbQ+LsJiuLuI2L0Vdmfm2w==}
|
||||
|
||||
esbuild@0.25.6:
|
||||
resolution: {integrity: sha512-GVuzuUwtdsghE3ocJ9Bs8PNoF13HNQ5TXbEi2AhvVb8xU1Iwt9Fos9FEamfoee+u/TOsn7GUWc04lz46n2bbTg==}
|
||||
engines: {node: '>=18'}
|
||||
|
|
@ -3561,6 +3661,9 @@ packages:
|
|||
eventemitter3@4.0.7:
|
||||
resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==}
|
||||
|
||||
eventemitter3@5.0.1:
|
||||
resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==}
|
||||
|
||||
events@3.3.0:
|
||||
resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==}
|
||||
engines: {node: '>=0.8.x'}
|
||||
|
|
@ -4044,6 +4147,9 @@ packages:
|
|||
engines: {node: '>=16.x'}
|
||||
hasBin: true
|
||||
|
||||
immer@10.1.1:
|
||||
resolution: {integrity: sha512-s2MPrmjovJcoMaHtx6K11Ra7oD05NT97w1IC5zpMkT6Atjr7H8LjaDd81iIxUYpMKSRRNMJE703M1Fhr/TctHw==}
|
||||
|
||||
imurmurhash@0.1.4:
|
||||
resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==}
|
||||
engines: {node: '>=0.8.19'}
|
||||
|
|
@ -4093,6 +4199,10 @@ packages:
|
|||
resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
internmap@2.0.3:
|
||||
resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
ipaddr.js@1.9.1:
|
||||
resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==}
|
||||
engines: {node: '>= 0.10'}
|
||||
|
|
@ -5300,6 +5410,9 @@ packages:
|
|||
peerDependencies:
|
||||
react: ^19.1.1
|
||||
|
||||
react-is@19.1.1:
|
||||
resolution: {integrity: sha512-tr41fA15Vn8p4X9ntI+yCyeGSf1TlYaY5vlTZfQmeLBrFo3psOPX6HhTDnFNL9uj3EhP0KAQ80cugCl4b4BERA==}
|
||||
|
||||
react-latex-next@3.0.0:
|
||||
resolution: {integrity: sha512-x70f1b1G7TronVigsRgKHKYYVUNfZk/3bciFyYX1lYLQH2y3/TXku3+5Vap8MDbJhtopePSYBsYWS6jhzIdz+g==}
|
||||
engines: {node: '>=12', npm: '>=5'}
|
||||
|
|
@ -5307,6 +5420,18 @@ packages:
|
|||
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-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:
|
||||
resolution: {integrity: sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
|
@ -5389,6 +5514,22 @@ packages:
|
|||
resolution: {integrity: sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==}
|
||||
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:
|
||||
resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
|
@ -5444,6 +5585,9 @@ packages:
|
|||
requires-port@1.0.0:
|
||||
resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==}
|
||||
|
||||
reselect@5.1.1:
|
||||
resolution: {integrity: sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==}
|
||||
|
||||
resolve-alpn@1.2.1:
|
||||
resolution: {integrity: sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==}
|
||||
|
||||
|
|
@ -5890,6 +6034,9 @@ packages:
|
|||
through@2.3.8:
|
||||
resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==}
|
||||
|
||||
tiny-invariant@1.3.3:
|
||||
resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==}
|
||||
|
||||
tinyglobby@0.2.14:
|
||||
resolution: {integrity: sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==}
|
||||
engines: {node: '>=12.0.0'}
|
||||
|
|
@ -6236,6 +6383,9 @@ packages:
|
|||
resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==}
|
||||
engines: {node: '>= 0.8'}
|
||||
|
||||
victory-vendor@37.3.6:
|
||||
resolution: {integrity: sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ==}
|
||||
|
||||
vite-node@3.2.4:
|
||||
resolution: {integrity: sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==}
|
||||
engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0}
|
||||
|
|
@ -8780,6 +8930,18 @@ snapshots:
|
|||
- supports-color
|
||||
- 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)':
|
||||
dependencies:
|
||||
'@babel/core': 7.28.0
|
||||
|
|
@ -8903,6 +9065,10 @@ snapshots:
|
|||
dependencies:
|
||||
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':
|
||||
dependencies:
|
||||
ejs: 3.1.10
|
||||
|
|
@ -9016,6 +9182,30 @@ snapshots:
|
|||
|
||||
'@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@1.0.8': {}
|
||||
|
|
@ -9050,6 +9240,8 @@ snapshots:
|
|||
|
||||
'@types/trusted-types@2.0.7': {}
|
||||
|
||||
'@types/use-sync-external-store@0.0.6': {}
|
||||
|
||||
'@types/yauzl@2.10.3':
|
||||
dependencies:
|
||||
'@types/node': 20.19.9
|
||||
|
|
@ -9834,6 +10026,44 @@ snapshots:
|
|||
|
||||
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-view-buffer@1.0.2:
|
||||
|
|
@ -9872,6 +10102,8 @@ snapshots:
|
|||
dependencies:
|
||||
callsite: 1.0.0
|
||||
|
||||
decimal.js-light@2.5.1: {}
|
||||
|
||||
decompress-response@6.0.0:
|
||||
dependencies:
|
||||
mimic-response: 3.1.0
|
||||
|
|
@ -10151,6 +10383,8 @@ snapshots:
|
|||
is-date-object: 1.1.0
|
||||
is-symbol: 1.1.1
|
||||
|
||||
es-toolkit@1.39.10: {}
|
||||
|
||||
esbuild@0.25.6:
|
||||
optionalDependencies:
|
||||
'@esbuild/aix-ppc64': 0.25.6
|
||||
|
|
@ -10245,6 +10479,8 @@ snapshots:
|
|||
|
||||
eventemitter3@4.0.7: {}
|
||||
|
||||
eventemitter3@5.0.1: {}
|
||||
|
||||
events@3.3.0: {}
|
||||
|
||||
execa@5.1.1:
|
||||
|
|
@ -10816,6 +11052,8 @@ snapshots:
|
|||
|
||||
image-size@2.0.2: {}
|
||||
|
||||
immer@10.1.1: {}
|
||||
|
||||
imurmurhash@0.1.4: {}
|
||||
|
||||
indent-string@5.0.0: {}
|
||||
|
|
@ -10875,6 +11113,8 @@ snapshots:
|
|||
hasown: 2.0.2
|
||||
side-channel: 1.1.0
|
||||
|
||||
internmap@2.0.3: {}
|
||||
|
||||
ipaddr.js@1.9.1: {}
|
||||
|
||||
ipx@3.1.1(@netlify/blobs@10.0.7)(idb-keyval@6.2.2):
|
||||
|
|
@ -12128,12 +12368,23 @@ snapshots:
|
|||
react: 19.1.1
|
||||
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):
|
||||
dependencies:
|
||||
katex: 0.16.22
|
||||
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-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: {}
|
||||
|
||||
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:
|
||||
dependencies:
|
||||
call-bind: 1.0.8
|
||||
|
|
@ -12284,6 +12561,8 @@ snapshots:
|
|||
|
||||
requires-port@1.0.0: {}
|
||||
|
||||
reselect@5.1.1: {}
|
||||
|
||||
resolve-alpn@1.2.1: {}
|
||||
|
||||
resolve-from@5.0.0: {}
|
||||
|
|
@ -12823,6 +13102,8 @@ snapshots:
|
|||
|
||||
through@2.3.8: {}
|
||||
|
||||
tiny-invariant@1.3.3: {}
|
||||
|
||||
tinyglobby@0.2.14:
|
||||
dependencies:
|
||||
fdir: 6.4.6(picomatch@4.0.3)
|
||||
|
|
@ -13093,6 +13374,23 @@ snapshots:
|
|||
|
||||
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):
|
||||
dependencies:
|
||||
cac: 6.7.14
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue