feat(analytics): analytics dashboard and whatWeKnowAboutYou Seite
Build and Push Docker image / build-and-push (push) Successful in 4m47s Details

This commit is contained in:
Valentin Kolb 2024-11-04 19:31:08 +01:00
parent 2ace56cfab
commit 29c52d2638
26 changed files with 1637 additions and 343 deletions

View File

@ -1,6 +1,7 @@
/**
* @description Global configuration file for the application
*/
import * as process from "node:process";
// POCKETBASE
export const PB_USER_COLLECTION = "users"
@ -11,6 +12,7 @@ export const PB_STORAGE_KEY = "stuve-it-login-record"
export const APP_NAME = "StuVe IT"
export const APP_VERSION = "0.9.8 (beta)"
export const APP_URL = "https://it.stuve.uni-ulm.de"
export const LOCAL_DEV_MODE = process?.env?.NODE_ENV === "development" || window?.location?.hostname === "localhost"
// analytics
export const ANALYTICS_COOKIE_NAME = "stuve-it-analytics"

View File

@ -13,6 +13,7 @@
"@fontsource/fira-code": "^5.0.15",
"@fontsource/overpass": "^5.0.15",
"@hello-pangea/dnd": "^16.6.0",
"@mantine/charts": "^7.13.4",
"@mantine/code-highlight": "^7.10.0",
"@mantine/core": "^7.10.0",
"@mantine/dates": "^7.10.0",
@ -35,6 +36,7 @@
"@tiptap/starter-kit": "^2.3.0",
"@tiptap/suggestion": "^2.4.0",
"@types/react-big-calendar": "^1.8.9",
"@uidotdev/usehooks": "^2.4.1",
"@yudiel/react-qr-scanner": "^2.0.2",
"clsx": "^2.1.1",
"dayjs": "^1.11.10",
@ -46,16 +48,16 @@
"papaparse": "^5.4.1",
"pocketbase": "^0.19.0",
"react": "^18.2.0",
"react-beforeunload": "^2.6.0",
"react-big-calendar": "^1.11.3",
"react-cookie": "^7.2.2",
"react-countup": "^6.5.3",
"react-dom": "^18.2.0",
"react-dropzone": "^14.2.3",
"react-error-boundary": "^4.1.2",
"react-infinite-scroll-component": "^6.1.0",
"react-intersection-observer": "^9.10.2",
"react-markdown": "^9.0.1",
"react-router-dom": "^6.20.0",
"recharts": "2",
"recoil": "^0.7.7",
"remark-gfm": "^4.0.0",
"sanitize-html": "^2.13.0",
@ -84,7 +86,7 @@
"postcss-simple-vars": "^7.0.1",
"sass": "^1.75.0",
"typescript": "^5.0.2",
"vite": "5.1.0-beta.2",
"vite": "5.4.6",
"vite-plugin-svgr": "^4.2.0"
}
}

View File

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 100 100" version="1.1" xmlns="http://www.w3.org/2000/svg"
xml:space="preserve"
style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<g transform="matrix(1.02041,0,0,1.02041,-197.959,0)">
<rect id="Artboard1" x="194" y="0" width="98" height="98" style="fill:none;"/>
<g id="Artboard11">
<g transform="matrix(0.129488,0,0,0.128633,216.823,22.7241)">
<g transform="matrix(7.56828,0,0,7.61857,-176.258,-188.086)">
<path d="M95.493,95.155L4.5,95.155C4.5,95.155 4.726,76.243 5.56,72.26C5.961,70.348 5.798,55.477 5.798,55.477C5.798,55.477 5.802,55.187 5.8,55.048C5.795,54.633 6.268,53.615 6.72,53.294C7.011,53.086 7.343,52.821 7.343,52.419C7.345,49.181 7.341,34.535 7.341,34.535L7.339,34.302C7.329,33.742 7.68,32.975 8.093,32.614C8.55,32.214 9.207,31.923 9.441,31.656C9.589,31.487 9.709,31.294 9.815,30.999C9.9,30.761 16.837,4.845 16.837,4.845C16.837,4.845 23.776,30.762 23.861,30.999C23.967,31.294 24.087,31.487 24.234,31.656C24.469,31.923 25.126,32.214 25.583,32.614C25.996,32.975 26.347,33.742 26.337,34.302L26.334,34.535C26.334,34.535 26.331,49.181 26.333,52.419C26.333,52.821 26.665,53.086 26.956,53.294C27.408,53.615 27.88,54.633 27.876,55.048C27.874,55.187 27.878,55.477 27.878,55.477L27.863,65.838L73.37,65.833L76.83,49.964L78.56,57.898L80.294,49.942L83.757,65.832L83.757,71.587L90.119,71.584C90.119,71.584 91.628,71.592 91.636,71.588C92.215,71.34 94.358,74.803 94.912,76.282C95.606,78.131 95.493,95.155 95.493,95.155Z" style="fill:none;"/>
</g>
<g transform="matrix(7.56828,0,0,7.61857,-176.28,-188.086)">
<path d="M94.912,76.282C95.606,78.131 95.5,98.155 95.5,98.155L4.5,98.155C4.5,98.155 4.726,76.243 5.56,72.26C5.961,70.348 5.798,55.477 5.798,55.477C5.798,55.477 5.802,55.187 5.8,55.048C5.795,54.633 6.268,53.615 6.72,53.294C7.011,53.086 7.343,52.821 7.343,52.419C7.345,49.181 7.341,34.535 7.341,34.535L7.339,34.302C7.329,33.742 7.68,32.975 8.093,32.614C8.55,32.214 9.207,31.923 9.441,31.656C9.589,31.487 9.709,31.294 9.815,30.999C9.9,30.761 16.837,4.845 16.837,4.845C16.837,4.845 23.776,30.762 23.861,30.999C23.967,31.294 24.087,31.487 24.234,31.656C24.469,31.923 25.126,32.214 25.583,32.614C25.996,32.975 26.347,33.742 26.337,34.302L26.334,34.535C26.334,34.535 26.331,49.181 26.333,52.419C26.333,52.821 26.665,53.086 26.956,53.294C27.408,53.615 27.88,54.633 27.876,55.048C27.874,55.187 27.878,55.477 27.878,55.477L27.863,65.838L73.37,65.833L76.83,49.964L78.56,57.898L80.294,49.942L83.757,65.832L83.757,71.587L90.119,71.584C90.119,71.584 91.628,71.592 91.636,71.588C92.215,71.34 94.358,74.803 94.912,76.282Z" style="fill:#fd7e14;"/>
</g>
<g transform="matrix(3.31017,0,0,3.33216,-123.695,-80.204)">
<path d="M47.904,125.072L82.509,185.184L13.3,185.184L47.904,125.072Z" style="fill:#ffd8a8;"/>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.1 KiB

View File

@ -9,14 +9,17 @@ import DebugPage from "@/pages/debug/DebugPage.tsx";
import EmailRouter from "@/pages/email/EmailRouter.tsx";
import AdminRouter from "@/pages/admin/AdminRouter.tsx";
import Analytics from "@/components/analytics";
import {ErrorBoundary} from "react-error-boundary";
import ErrorPage from "@/pages/error-pages/ErrorPage.tsx";
import WhatWeKnowAboutYou from "@/pages/util/whatWeKnowAboutYou";
const router = createBrowserRouter([
{
path: "/",
element: <>
element: <ErrorBoundary fallback={<ErrorPage/>}>
<Analytics/>
<Layout/>
</>,
</ErrorBoundary>,
children: [
{
index: true,
@ -48,6 +51,10 @@ const router = createBrowserRouter([
{
path: "qr",
element: <QRCodeGenerator/>
},
{
path : "whatWeKnowAboutYou",
element: <WhatWeKnowAboutYou/>
}
]
},

View File

@ -3,7 +3,6 @@ import {IconThumbUp} from "@tabler/icons-react";
import {Link} from "react-router-dom";
import useAnalyticsVisitor from "@/components/analytics/useAnalyticsVisitor.ts";
import useAnalyticsSession from "@/components/analytics/useAnalyticsSession.ts";
import useAnalyticsError from "@/components/analytics/useAnalyticsError.ts";
import useAnalyticsPageView from "@/components/analytics/useAnalyticsPageView.ts";
/**
@ -19,42 +18,38 @@ export default function Analytics() {
const {sessionId} = useAnalyticsSession(visitorId)
// Track page views with the session ID
useAnalyticsPageView(sessionId)
// Track errors
useAnalyticsError(sessionId)
return (
<>
<Dialog opened={!visitorId} radius="md">
<div className={"stack"}>
<Text size={"sm"} c={"dimmed"}>
Wir nutzen Cookies um diese Seite zu verbessern.
</Text>
<Text size={"xs"} c={"dimmed"}>
Mit der Nutzung unserer Dienste erklärst du dich damit einverstanden,
dass wir Cookies verwenden und anonymisierte Nutzungsdaten erheben.
</Text>
<Dialog opened={!visitorId} radius="md">
<div className={"stack"}>
<Text size={"sm"} c={"dimmed"}>
Wir nutzen Cookies um diese Seite zu verbessern.
</Text>
<Text size={"xs"} c={"dimmed"}>
Mit der Nutzung unserer Dienste erklärst du dich damit einverstanden,
dass wir Cookies verwenden und anonymisierte Nutzungsdaten erheben.
</Text>
<div className={"group"}>
<Button
component={Link}
to={"/legal/privacy-policy"}
size={"xs"}
color={"gray"}
>
Infos
</Button>
<div className={"group"}>
<Button
component={Link}
to={"/legal/privacy-policy"}
size={"xs"}
color={"gray"}
>
Infos
</Button>
<Button
size={"xs"}
color={"green"}
onClick={initVisitor}
leftSection={<IconThumbUp size={16}/>}
>
Alles klar
</Button>
</div>
<Button
size={"xs"}
color={"green"}
onClick={initVisitor}
leftSection={<IconThumbUp size={16}/>}
>
Alles klar
</Button>
</div>
</Dialog>
</>
</div>
</Dialog>
)
}

View File

@ -1,84 +0,0 @@
import {useEffect} from "react";
import {usePB} from "@/lib/pocketbase.tsx";
import {useMutation} from "@tanstack/react-query";
import {useLocation} from "react-router-dom";
/**
* Custom hook to track and log errors for analytics.
*
* @param {string} [sessionId] - The ID of the current analytics session.
*/
export default function useAnalyticsError(sessionId?: string) {
const {pb} = usePB()
const location = useLocation()
/**
* Mutation to log an error in the analytics system.
*
* @param {Object} error - The error object containing error details.
* @param {string} error.session - The session ID.
* @param {string} error.error_message - The error message.
* @param {string} error.error_type - The type of error.
* @param {string} error.stack_trace - The stack trace of the error.
*/
const trackErrorMutation = useMutation({
mutationFn: async (error: {
session: string;
error_message: string;
error_type: string;
stack_trace: string;
}) => {
if (!sessionId) return
return await pb.collection('analyticsErrors').create({
...error,
path: location.pathname
})
}
})
useEffect(() => {
// Check if current session is available
if (!sessionId) return;
/**
* Global error handler to track errors.
*
* @param {ErrorEvent} event - The error event.
*/
const errorHandler = function (event: ErrorEvent) {
trackErrorMutation.mutate({
session: sessionId,
error_message: event.message || 'Unknown error',
error_type: 'Global Error',
stack_trace: event.error?.stack || '',
})
}
/**
* Unhandled rejection handler to track promise rejections.
*
* @param {PromiseRejectionEvent} event - The promise rejection event.
*/
const unhandledRejectionHandler = function (event: PromiseRejectionEvent) {
trackErrorMutation.mutate({
session: sessionId,
error_message: event.reason?.message || 'Unhandled Rejection',
error_type: 'Promise Rejection',
stack_trace: event.reason?.stack || '',
})
}
// Add event listeners
window.addEventListener('error', errorHandler)
window.addEventListener('unhandledrejection', unhandledRejectionHandler)
// Cleanup function in useEffect
return () => {
window.removeEventListener('error', errorHandler)
window.removeEventListener('unhandledrejection', unhandledRejectionHandler)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [sessionId])
}

View File

@ -2,35 +2,81 @@ import {useEffect} from "react";
import {useLocation} from "react-router-dom";
import {useMutation} from "@tanstack/react-query";
import {usePB} from "@/lib/pocketbase.tsx";
import {AnalyticsErrorType} from "@/models/AnalyticsTypes.ts";
/**
* Custom hook to track page views for analytics.
* Custom hook to track page views and errors for analytics.
*
* @param {string} [sessionId] - The ID of the current analytics session.
*/
export default function useAnalyticsPageView(sessionId?: string) {
const location = useLocation()
const {pb} = usePB()
const location = useLocation()
/**
* Mutation to log a page view in the analytics system.
*
* @param {string} path - The path of the page being viewed.
* Mutation to log an error in the analytics system.
*/
const pageViewMutation = useMutation({
mutationFn: async (path: string) => {
await pb.collection("analyticsPageViews").create({
path,
mutationFn: async (error?: {
error_type: AnalyticsErrorType
stack_trace: string
error_message: string
}) => {
if (!sessionId) return
return await pb.collection('analyticsPageViews').create({
session: sessionId,
path: location.pathname,
error: error
})
}
})
useEffect(() => {
/**
* Global error handler to track errors.
*
* @param {ErrorEvent} event - The error event.
*/
const errorHandler = function (event: ErrorEvent) {
pageViewMutation.mutate({
error_type: 'Error',
stack_trace: event.error?.stack || '',
error_message: event.message || 'Unknown error',
})
}
/**
* Unhandled rejection handler to track promise rejections.
*
* @param {PromiseRejectionEvent} event - The promise rejection event.
*/
const unhandledRejectionHandler = function (event: PromiseRejectionEvent) {
pageViewMutation.mutate({
error_type: 'Promise Rejection',
stack_trace: event.reason?.stack || '',
error_message: event.reason?.message || 'Unhandled Rejection',
})
}
// Add event listeners
window.addEventListener('error', errorHandler)
window.addEventListener('unhandledrejection', unhandledRejectionHandler)
// Cleanup function in useEffect
return () => {
window.removeEventListener('error', errorHandler)
window.removeEventListener('unhandledrejection', unhandledRejectionHandler)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [sessionId])
// Effect to log the page view when the session ID or location changes
useEffect(() => {
if (!sessionId) return
pageViewMutation.mutate(location.pathname)
pageViewMutation.mutate(undefined)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [sessionId, location])
}

View File

@ -6,6 +6,7 @@ import {UAParser} from "ua-parser-js";
import {AnalyticsIpApiResult} from "@/models/AnalyticsTypes.ts";
import {ANALYTICS_IP_API} from "../../../config.ts";
import {ofetch} from "ofetch";
import {usePreferredLanguage} from "@uidotdev/usehooks";
/**
* Custom hook to manage an analytics session.
@ -18,10 +19,12 @@ export default function useAnalyticsSession(visitorId?: string) {
// State to store the session ID
const [sessionId, setSessionId] = useState<string | undefined>(undefined)
const {pb} = usePB()
const preferredLanguage = usePreferredLanguage()
// Mutation to initialize the analytics session
const initSessionMutation = useMutation({
mutationFn: async () => {
// prevent initialization if the visitor ID is not set or if debug mode is enabled
if (!visitorId) return
// Parse the user agent to get device and browser information
@ -45,10 +48,12 @@ export default function useAnalyticsSession(visitorId?: string) {
operating_system_version: result.os.version,
user_agent: navigator.userAgent,
ip_address: ip,
geo_country_code: country_code
geo_country_code: country_code,
preferred_language: preferredLanguage
})
},
onSuccess: (data) => {
if (!data) return
// Set the session ID on successful creation
setSessionId(data?.id)
}

View File

@ -1,10 +1,11 @@
import {useCookies} from "react-cookie";
import {ANALYTICS_COOKIE_NAME} from "../../../config.ts";
import {ANALYTICS_COOKIE_NAME, LOCAL_DEV_MODE} from "../../../config.ts";
import {usePB} from "@/lib/pocketbase.tsx";
import {nanoid} from "nanoid/non-secure";
import {useMutation} from "@tanstack/react-query";
import {ClientResponseError} from "pocketbase";
import {useEffect} from "react";
import dayjs from "dayjs";
/**
@ -18,6 +19,14 @@ export default function useAnalyticsVisitor() {
const {pb} = usePB()
const storeVisitorInCookie = (visitorId: string) => {
setCookie(ANALYTICS_COOKIE_NAME, visitorId, {
path: "/",
expires: dayjs().add(1, "year").toDate(),
sameSite: "strict"
})
}
const initVisitor = () => {
// create 15 chars long random string
const visitorId = nanoid(15)
@ -25,7 +34,7 @@ export default function useAnalyticsVisitor() {
// create visitor in database
upsertVisitorMutation.mutateAsync(visitorId).then(() => {
// set cookie
setCookie(ANALYTICS_COOKIE_NAME, visitorId, {path: "/"})
storeVisitorInCookie(visitorId)
})
}
@ -36,13 +45,25 @@ export default function useAnalyticsVisitor() {
mutationFn: async (visitorId: string) => {
try {
// set visitor updated date to now
await pb.collection("analyticsVisitors").update(visitorId, {})
await pb.collection("analyticsVisitors").update(visitorId, {
meta: {
localDevMode: LOCAL_DEV_MODE
}
})
} catch (e) {
if ((e as ClientResponseError).status === 404) {
// visitor does not exist, create it
await pb.collection("analyticsVisitors").create({id: visitorId})
await pb.collection("analyticsVisitors").create({
id: visitorId,
meta: {
localDevMode: LOCAL_DEV_MODE
}
})
}
}
},
onSuccess: () => {
storeVisitorInCookie(cookieValue as string)
}
})
@ -54,7 +75,6 @@ export default function useAnalyticsVisitor() {
// eslint-disable-next-line
}, [cookieIsSet, cookieValue])
return {
visitorId: cookieValue as string | undefined,
initVisitor

View File

@ -4,13 +4,12 @@ import {Fragment} from "react";
import {
IconBug,
IconConfetti,
IconDatabaseSmile,
IconHome,
IconList,
IconMailShare,
IconQrcode,
IconSectionSign
IconQrcode
} from "@tabler/icons-react";
import {useShowDebug} from "@/components/ShowDebug.tsx";
import {usePB} from "@/lib/pocketbase.tsx";
@ -58,45 +57,12 @@ const NavItems = [
icon: IconQrcode,
description: "Generiere einen QR Code",
link: "/util/qr"
}
]
},
{
section: "Rechtliches",
items: [
{
title: "Impressum",
icon: IconSectionSign,
description: "Impressum",
link: "/legal/imprint"
},
{
title: "Datenschutzerklärung",
icon: IconSectionSign,
description: "Datenschutzerklärung",
link: "/legal/privacy-policy"
},
{
title: "AGB",
icon: IconSectionSign,
description: "AGB",
link: "/legal/terms-and-conditions"
}
]
}
]
const DebugMenuItems = [
{
section: "Debug",
items: [
{
title: "Debug Seite",
icon: IconBug,
description: "Debug",
link: "/debug",
color: "orange"
title: "Datenauskunft",
icon: IconDatabaseSmile,
description: "Was wissen wir über dich?",
link: "/util/whatWeKnowAboutYou"
}
]
}
@ -119,15 +85,10 @@ const AdminMenuItems = [
export default function MenuItems() {
const {showDebug} = useShowDebug()
const {user} = usePB()
let nav = NavItems
if (showDebug) {
nav = [...nav, ...DebugMenuItems]
}
if (user?.isAdmin) {
nav = [...nav, ...AdminMenuItems]
}

View File

@ -1,14 +1,18 @@
import {usePB} from "@/lib/pocketbase.tsx";
import classes from "./index.module.css";
import {ActionIcon, Image, Menu, ThemeIcon} from "@mantine/core";
import {IconChevronDown, IconLogin, IconUserStar} from "@tabler/icons-react";
import {ActionIcon, Badge, Image, Menu, ThemeIcon} from "@mantine/core";
import {IconBug, IconChevronDown, IconLogin, IconUserStar} from "@tabler/icons-react";
import MenuItems from "./MenuItems.tsx";
import {useLogin, useUserMenu} from "@/components/users/modals/hooks.ts";
import {useShowDebug} from "@/components/ShowDebug.tsx";
import {Link} from "react-router-dom";
export default function NavBar() {
const {user} = usePB()
const {showDebug} = useShowDebug()
const {handler: userMenuHandler} = useUserMenu()
const {handler: loginHandler} = useLogin()
@ -25,7 +29,7 @@ export default function NavBar() {
<Image
h={30}
w={30}
src={"/stuve-logo.svg"}
src={showDebug ? "/stuve-logo-debug.svg" : "/stuve-logo.svg"}
alt={"StuVe IT Logo"}
/>
@ -36,14 +40,29 @@ export default function NavBar() {
<ThemeIcon variant={"transparent"} size={"sm"}>
<IconChevronDown/>
</ThemeIcon>
</div>
</Menu.Target>
<Menu.Dropdown>
<MenuItems/>
</Menu.Dropdown>
</Menu>
<div className={classes.actionIcons}>
{showDebug && (
<Badge
className={"cursor-pointer"}
color={"orange"}
component={Link}
to={"/debug"}
leftSection={<IconBug size={12}/>}
>
DEBUG MODUS
</Badge>
)}
{user ?
<>
<ActionIcon

View File

@ -3,6 +3,7 @@ import {PocketBaseErrorAlert, usePB} from "@/lib/pocketbase.tsx";
import {useForm} from "@mantine/form";
import {useMutation} from "@tanstack/react-query";
import {
Alert,
Anchor,
Button,
Center,
@ -83,8 +84,31 @@ export default function LoginModal() {
</Group>
</Radio.Group>
<Alert>
{
formValues.values.authMethod === "ldap" ?
<>
Mitglieder der Verfassten Studierendenschaft können sich mit ihrem StuVe IT Account
anmelden.
<br/>
Falls du du keinen StuVe IT Account hast, kannst du einen
<Anchor size={"sm"} c={"blue"}
onClick={() => formValues.setFieldValue("authMethod", "guest")}>
{""} Gast Account {" "}
</Anchor>
nutzen.
</> : <>
Hier kannst du dich mit deinem Gast Account anmelden.
</>
}
</Alert>
<TextInput
label={"Anmeldename"}
description={
formValues.values.authMethod === "ldap" ?
undefined : "Dein selbstgewählter Anmeldename für den Gäste Account"
}
placeholder={
formValues.values.authMethod === "ldap" ?
"vorname.nachname" : "Anmeldename"

40
src/lib/datasience.ts Normal file
View File

@ -0,0 +1,40 @@
export function countByAllKeys<T extends Record<string, any>>(
data: T[]
): Record<string, Array<{ key: any; count: number }>> {
const result: Record<string, Array<{ key: any; count: number }>> = {};
// Get all unique keys from the data
const keysSet = new Set<string>();
data.forEach((obj) => {
Object.keys(obj).forEach((key) => keysSet.add(key));
});
const keys = Array.from(keysSet);
// Initialize a Map for each key to store counts
const countsMaps: Record<string, Map<any, number>> = {};
keys.forEach((key) => {
countsMaps[key] = new Map<any, number>();
});
// Count occurrences
data.forEach((item) => {
keys.forEach((key) => {
const value = item.hasOwnProperty(key) ? item[key] : undefined;
const countsMap = countsMaps[key];
countsMap.set(value, (countsMap.get(value) || 0) + 1);
});
});
// Convert Maps to arrays in the desired format
keys.forEach((key) => {
const countsArray = Array.from(countsMaps[key].entries()).map(
([value, count]) => ({
key: value,
count,
})
);
result[key] = countsArray;
});
return result;
}

View File

@ -6,6 +6,7 @@ import '@mantine/code-highlight/styles.layer.css';
import '@mantine/dates/styles.layer.css';
import '@mantine/tiptap/styles.layer.css';
import '@mantine/notifications/styles.layer.css';
import '@mantine/charts/styles.css';
import {Alert, createTheme, DEFAULT_THEME, MantineProvider, mergeMantineTheme} from "@mantine/core";
import {QueryClient, QueryClientProvider} from "@tanstack/react-query";
import {PocketBaseProvider} from "@/lib/pocketbase.tsx";

View File

@ -14,17 +14,27 @@ export type AnalyticsSessionModel = {
user_agent?: string
ip_address?: string
geo_country_code?: string
expand: {
visitor: AnalyticsVisitorsModel
preferred_language?: string
expand?: {
visitor?: AnalyticsVisitorsModel,
analyticsErrors_via_session?: AnalyticsErrorModel[],
analyticsPageViews_via_session?: AnalyticsPageViewModel[]
}
} & RecordModel
export type AnalyticsErrorType = 'Error' | 'Promise Rejection'
export type AnalyticsPageViewModel = {
session: string
path: string
error?: {
error_message?: string
error_type?: AnalyticsErrorType
stack_trace?: string
}
expand?: {
session: AnalyticsSessionModel
session?: AnalyticsSessionModel
}
} & RecordModel
@ -36,7 +46,7 @@ export type AnalyticsErrorModel = {
path?: string
expand?: {
session: AnalyticsSessionModel
session?: AnalyticsSessionModel
}
} & RecordModel

View File

@ -1,5 +1,8 @@
import {usePB} from "@/lib/pocketbase.tsx";
import NotAllowed from "@/pages/error-pages/NotAllowed.tsx";
import {Navigate, Outlet, Route, Routes} from "react-router-dom";
import NotFound from "@/pages/error-pages/NotFound.tsx";
import AnalyticsDashboard from "@/pages/admin/AnalyticsDashboard";
export default function AdminRouter() {
@ -9,10 +12,12 @@ export default function AdminRouter() {
return <NotAllowed/>
}
return (
<div>
<h1>Admin Panel</h1>
</div>
)
return <>
<Routes>
<Route index element={<Navigate to={"analytics"}/>}/>
<Route path={"analytics/*"} element={<AnalyticsDashboard/>}/>
<Route path={"*"} element={<NotFound/>}/>
</Routes>
<Outlet/>
</>
}

View File

@ -0,0 +1,202 @@
import classes from "./index.module.css";
import {ActionIcon, Code, Collapse, ThemeIcon, Tooltip} from "@mantine/core";
import {
IconBrandAndroid,
IconBrandApple,
IconBrandChrome,
IconBrandEdge,
IconBrandFirefox,
IconBrandOpera,
IconBrandSafari,
IconBrandWindows,
IconBrowser,
IconBug,
IconCalendarClock,
IconCar,
IconClick,
IconDeviceGamepad2,
IconDeviceIpad,
IconDeviceLaptop,
IconDevices,
IconDevices2,
IconDeviceTv,
IconDeviceWatch,
IconEye,
IconEyeOff,
IconGlobe,
IconLanguage,
IconStereoGlasses
} from "@tabler/icons-react";
import TextWithIcon from "@/components/layout/TextWithIcon";
import {useDisclosure} from "@mantine/hooks";
import ShowDebug from "@/components/ShowDebug.tsx";
import {pprintDateTime} from "@/lib/datetime.ts";
import {AnalyticsSessionModel} from "@/models/AnalyticsTypes.ts";
export const DeviceTypeIcon = ({deviceType}: { deviceType: string }) => {
if (deviceType === "mobile") {
return <IconBrandAndroid/>
} else if (deviceType === "tablet") {
return <IconDeviceIpad/>
} else if (deviceType === "desktop") {
return <IconDeviceLaptop/>
} else if (deviceType === "console") {
return <IconDeviceGamepad2/>
} else if (deviceType === "smarttv") {
return <IconDeviceTv/>
} else if (deviceType === "wearable") {
return <IconDeviceWatch/>
} else if (deviceType === "embedded") {
return <IconCar/>
} else if (deviceType === "xr") {
return <IconStereoGlasses/>
}
return <IconDevices/>
}
export const BrowserIcon = ({browser}: { browser: string }) => {
browser = browser.toLocaleLowerCase().trim()
if (browser.includes("safari")) {
return <IconBrandSafari/>
} else if (browser.includes("firefox")) {
return <IconBrandFirefox/>
} else if (browser.includes("chrome")) {
return <IconBrandChrome/>
} else if (browser.includes("edge")) {
return <IconBrandEdge/>
} else if (browser.includes("opera")) {
return <IconBrandOpera/>
}
return <IconBrowser/>
}
export const OsIcon = ({os}: { os: string }) => {
os = os.toLocaleLowerCase().trim()
if (os.includes("macos") || os.includes("ios")) {
return <IconBrandApple/>
} else if (os.includes("windows")) {
return <IconBrandWindows/>
} else if (os.includes("android")) {
return <IconBrandAndroid/>
} else if (os.includes("chrome")) {
return <IconBrandChrome/>
}
return <IconDevices2/>
}
export default function AnalyticsSessionRow({session}: {
session: AnalyticsSessionModel,
}) {
const [expanded, expandedHandler] = useDisclosure(false)
const errorCount = session.expand?.analyticsPageViews_via_session?.filter(pv => pv.error).length
const pageViewCount = session.expand?.analyticsPageViews_via_session?.length
return <>
<div className={classes.subgrid} data-error={!!errorCount}>
<div className={classes.child}>
<TextWithIcon icon={
<ThemeIcon variant={"transparent"} size={"xs"} color={"gray"}>
<IconCalendarClock/>
</ThemeIcon>
}>
{pprintDateTime(session.created)}
</TextWithIcon>
</div>
<div className={classes.child}>
<TextWithIcon icon={
<ThemeIcon variant={"transparent"} size={"xs"} color={"gray"}>
<BrowserIcon browser={session.browser_name ?? ""}/>
</ThemeIcon>
}>
{session.browser_name} {session.browser_version}
</TextWithIcon>
</div>
<div className={classes.child}>
<TextWithIcon icon={
<ThemeIcon variant={"transparent"} size={"xs"} color={"gray"}>
<OsIcon os={session.operating_system ?? ""}/>
</ThemeIcon>
}>
{session.operating_system} {session.operating_system_version}
</TextWithIcon>
</div>
<div className={classes.child}>
<TextWithIcon icon={
<ThemeIcon variant={"transparent"} size={"xs"} color={"gray"}>
<IconGlobe/>
</ThemeIcon>
}>
{session.geo_country_code || "--"}
</TextWithIcon>
</div>
<div className={classes.child}>
<TextWithIcon icon={
<ThemeIcon variant={"transparent"} size={"xs"} color={"gray"}>
<IconLanguage/>
</ThemeIcon>
}>
{session.preferred_language || "--"}
</TextWithIcon>
</div>
<div className={classes.child}>
<TextWithIcon icon={
<ThemeIcon variant={"transparent"} size={"xs"} color={"gray"}>
<IconClick/>
</ThemeIcon>
}>
{pageViewCount ?? '--'}
</TextWithIcon>
</div>
<div className={classes.child}>
<TextWithIcon icon={
<ThemeIcon variant={"transparent"} size={"xs"} color={errorCount ? "red" : "gray"}>
<IconBug/>
</ThemeIcon>
}>
{errorCount ?? '--'}
</TextWithIcon>
</div>
<div className={`${classes.child} ${classes.alignEnd}`}>
<Tooltip label={expanded ? "Details verbergen" : "Details anzeigen"} withArrow>
<ActionIcon onClick={expandedHandler.toggle} variant={"light"} size={"md"}>
{expanded ? <IconEyeOff/> : <IconEye/>}
</ActionIcon>
</Tooltip>
</div>
</div>
<Collapse in={expanded} className={classes.detailsContainer}>
<ShowDebug>
Session Id - <Code>{session.id}</Code>
<br/>
created - <Code>{pprintDateTime(session.created)}</Code>
<br/>
updated - <Code>{pprintDateTime(session.updated)}</Code>
<br/>
<br/>
IP - <Code>{session.ip_address}</Code>
<br/>
User Agent - <Code>{session.user_agent}</Code>
<br/>
Visitor Id - <Code>{session.visitor}</Code>
</ShowDebug>
{/*<EntryQuestionAndStatusData entry={entry}/>*/}
</Collapse>
</>
}

View File

@ -0,0 +1,58 @@
.charts {
max-width: var(--max-content-width);
padding: var(--padding) 0;
}
.mainGrid {
display: grid;
grid-template-columns: auto auto auto auto auto auto auto 0.1fr;
gap: var(--gap);
}
.subgrid {
display: grid;
grid-template-columns: subgrid;
grid-column: span 8;
background-color: var(--mantine-color-body);
border: var(--border);
border-radius: var(--border-radius);
padding: var(--mantine-spacing-sm);
font-size: var(--mantine-font-size-sm);
@media (max-width: $mantine-breakpoint-sm) {
font-size: var(--mantine-font-size-sm);
display: flex;
flex-direction: column;
gap: var(--gap);
}
&[data-error="true"] {
border-color: var(--mantine-color-error);
}
}
.child {
display: flex;
justify-content: start;
align-items: center;
text-align: start;
gap: var(--gap);
word-wrap: break-word; /* Ensures text wraps within the cell */
overflow-wrap: break-word; /* Ensures text wraps within the cell */
hyphens: auto;
@media (max-width: $mantine-breakpoint-sm) {
}
}
.alignEnd {
@media (min-width: $mantine-breakpoint-sm) {
justify-content: end;
}
}
.detailsContainer {
grid-column: span 8; /* Spans all columns of the main grid */
}

View File

@ -0,0 +1,197 @@
import {useInfiniteQuery} from "@tanstack/react-query";
import {usePB} from "@/lib/pocketbase.tsx";
import dayjs from "dayjs";
import {ActionIcon, Anchor, Button, Center, SimpleGrid, Switch, Text, TextInput, Tooltip} from "@mantine/core";
import {IconArrowDown, IconBug, IconZoomReset} from "@tabler/icons-react";
import {countByAllKeys} from "@/lib/datasience.ts";
import {useMemo} from "react";
import {RadarChart} from "@mantine/charts";
import classes from "./index.module.css";
import AnalyticsSessionRow from "@/pages/admin/AnalyticsDashboard/AnalyticsSessions/AnalyticsSessionRow.tsx";
import {useForm} from "@mantine/form";
export default function AnalyticsSessions({lastNDays}: { lastNDays?: number }) {
const {pb} = usePB()
const formValues = useForm({
mode: "uncontrolled",
initialValues: {
browser: '',
os: '',
country: '',
language: '',
hasErrors: false
}
})
const query = useInfiniteQuery({
queryKey: ["sessions", {lastNDays}],
queryFn: async ({pageParam}) => {
const filter = ['visitor.meta.localDevMode!=true'] as string[]
if (lastNDays !== undefined) {
filter.push(`created>='${dayjs().startOf("days").subtract(lastNDays, "days").toISOString()}'`)
}
const filterValues = formValues.getValues()
if (filterValues.browser) {
filter.push(`browser_name~'${filterValues.browser}'`)
}
if (filterValues.os) {
filter.push(`operating_system~'${filterValues.os}'`)
}
if (filterValues.country) {
filter.push(`geo_country_code~'${filterValues.country}'`)
}
if (filterValues.language) {
filter.push(`preferred_language~'${filterValues.language}'`)
}
if (filterValues.hasErrors) {
filter.push('analyticsPageViews_via_session.session=id&&analyticsPageViews_via_session.error?!=null')
}
return await pb.collection("analyticsSessions").getList(pageParam, 500, {
filter: filter.join("&&"),
expand: 'analyticsErrors_via_session,analyticsPageViews_via_session',
sort: '-created'
})
},
getNextPageParam: (lastPage) =>
lastPage.page >= lastPage.totalPages ? undefined : lastPage.page + 1,
initialPageParam: 1,
})
const sessions = query.data?.pages.flatMap(p => p.items) ?? []
const sessionAnalytics = useMemo(() => {
return countByAllKeys(sessions)
}, [query.data])
return <>
<SimpleGrid className={classes.charts} cols={{base: 2, xs: 4}}>
<RadarChart
h={{base: 120, md: 200}}
data={sessionAnalytics["browser_name"]}
dataKey="key"
series={[{name: 'count', color: 'blue.4'}]}
/>
<RadarChart
h={{base: 120, md: 200}}
data={sessionAnalytics["device_type"]}
dataKey="key"
series={[{name: 'count', color: 'blue.4'}]}
/>
<RadarChart
h={{base: 120, md: 200}}
data={sessionAnalytics["operating_system"]}
dataKey="key"
series={[{name: 'count', color: 'blue.4'}]}
/>
<RadarChart
h={{base: 120, md: 200}}
data={sessionAnalytics["preferred_language"]}
dataKey="key"
series={[{name: 'count', color: 'blue.4'}]}
/>
</SimpleGrid>
<div className={"section-transparent center"}>
<Text size={"xs"} c={"dimmed"}>
Datenvisualisierung der letzten {sessions.length} Sessions ({query.data?.pages[0].totalItems} insgesamt)
{query.hasNextPage && <>
{" • "}
<Anchor
variant={"transparent"}
size={"xs"}
onClick={() => query.fetchNextPage()}
>
Mehr laden
</Anchor>
</>}
</Text>
</div>
<div className={"section-transparent"}>
<div className={classes.mainGrid}>
<div className={classes.subgrid}>
<div className={classes.child}/>
<div className={classes.child}>
<TextInput
variant={"unstyled"}
placeholder={"Browser"}
{...formValues.getInputProps("browser")}
/>
</div>
<div className={classes.child}>
<TextInput
variant={"unstyled"}
placeholder={"OS"}
{...formValues.getInputProps("os")}
/>
</div>
<div className={classes.child}>
<TextInput
variant={"unstyled"}
placeholder={"Land"}
{...formValues.getInputProps("country")}
/>
</div>
<div className={classes.child}>
<TextInput
variant={"unstyled"}
placeholder={"Sprache"}
{...formValues.getInputProps("language")}
/>
</div>
<div className={classes.child}/>
<div className={classes.child}>
<Switch
size="sm"
color="gray"
onLabel={<IconBug size={16}/>}
{...formValues.getInputProps("hasErrors", {type: "checkbox"})}
/>
</div>
<div className={`${classes.child} ${classes.alignEnd}`}>
<Tooltip label={"Suchen & Daten neu laden"}>
<ActionIcon
aria-label={"search"}
variant={"transparent"}
onClick={() => query.refetch()}
disabled={query.isPending}
>
<IconZoomReset/>
</ActionIcon>
</Tooltip>
</div>
</div>
{sessions.map(session => <AnalyticsSessionRow session={session} key={session.id}/>)}
</div>
{query.hasNextPage && (
<Center p={"xs"}>
<Button
disabled={query.isFetchingNextPage || !query.hasNextPage}
loading={query.isFetchingNextPage}
variant={"transparent"}
size={"compact-xs"}
leftSection={<IconArrowDown/>}
onClick={() => query.fetchNextPage()}
>
Mehr laden
</Button>
</Center>
)}
</div>
</>
}

View File

@ -0,0 +1,115 @@
import {Center, Group, RingProgress, Text} from "@mantine/core";
import {useQuery} from "@tanstack/react-query";
import {usePB} from "@/lib/pocketbase.tsx";
import CountUp from "react-countup";
import classes from "./index.module.css"
import {ReactNode, useMemo} from "react"
import dayjs from "dayjs";
export default function CountStat({lastNDays, collection, label, icon, filter}: {
collection: string
filter?: string
label: string
icon: ReactNode
lastNDays?: number
}) {
const {pb} = usePB()
const dataOverviewQuery = useQuery({
queryKey: ['dataOverview', {collection}, {lastNDays}, {filter}],
queryFn: async () => {
// get oldest entry
const res = await pb.collection(collection).getList(1, 1, {
sort: 'created',
filter
})
return {
oldestDate: dayjs(res.items[0].created).startOf("day") || dayjs(),
totalItems: res.totalItems
}
},
// only run when startDate is set
enabled: lastNDays !== undefined
})
const countQuery = useQuery({
queryKey: ['countStat', {collection}, {lastNDays}, {filter}],
queryFn: async () => {
const _filter = [] as string[]
if (filter) {
_filter.push(filter)
}
if (lastNDays !== undefined) {
_filter.push(`updated>='${dayjs().startOf("days").subtract(lastNDays, "days").toISOString()}'`)
}
const res = await pb.collection(collection).getList(1, 0, {
filter: _filter.join('&&')
})
return res.totalItems
}
})
// when startDate is set, calculate the average using the dataOverviewQuery
const data = useMemo(() => {
if (!dataOverviewQuery.data) return {
expectedCount: 0,
count: countQuery.data ?? 0,
reachedPercentage: 0
}
const {oldestDate, totalItems} = dataOverviewQuery.data
const days = dayjs().diff(oldestDate, 'day')
const dailyAverage = totalItems / days
// calculate the expected count according to the average and the timespan since the startDate
const expectedCount = Math.round(dailyAverage * (lastNDays ?? 0))
const count = countQuery.data ?? 0
const reachedPercentage = (count / expectedCount) * 100
console.log({collection, days, dailyAverage, expectedCount, reachedPercentage})
return {
expectedCount,
count,
reachedPercentage
}
}, [dataOverviewQuery.data, countQuery.data])
return (
<div className={classes.statsContainer}>
<Group>
<RingProgress
size={80}
roundCaps
thickness={8}
sections={[{value: data.reachedPercentage, color: "blue"}]}
label={
<Center>
{icon}
</Center>
}
/>
<div>
<Text c="dimmed" size="xs" tt="uppercase" fw={700}>
{label}
</Text>
<Text fw={700} size="xl">
<CountUp end={data.count}/>
</Text>
{!!lastNDays && <>
<Text size="xs" c={"dimmed"}>
<CountUp end={data.expectedCount}/> erwartet (<CountUp end={data.reachedPercentage}/>%)
</Text>
</>}
</div>
</Group>
</div>
)
}

View File

@ -0,0 +1,10 @@
.grid {
max-width: var(--max-content-width);
}
.statsContainer {
border-radius: var(--border-radius);
border: var(--border);
padding: var(--padding);
background-color: var(--mantine-color-body);
}

View File

@ -0,0 +1,130 @@
import {Anchor, Breadcrumbs, Button, Group, Select, SimpleGrid, Title} from "@mantine/core";
import {Link, Navigate, NavLink, Outlet, Route, Routes} from "react-router-dom";
import classes from './index.module.css'
import CountStat from "@/pages/admin/AnalyticsDashboard/CountStat.tsx";
import {IconBug, IconCalendarStats, IconEye, IconTimeline, IconUsers, IconZoomExclamation} from "@tabler/icons-react";
import {useMemo} from "react";
import {useForm} from "@mantine/form";
import NotFound from "@/pages/error-pages/NotFound.tsx";
import Sessions from "@/pages/admin/AnalyticsDashboard/AnalyticsSessions";
export default function AnalyticsDashboard() {
const dateSelect = useMemo(() => ([
{
label: "letztes Jahr",
value: '365'
},
{
label: "letzte 30 Tage",
value: '30'
},
{
label: "letzte 7 Tage",
value: '7'
},
{
label: "heute",
value: '1'
}
]), [])
const formValues = useForm({
initialValues: {
dateFilter: undefined as string | undefined
}
})
const dateFilter = formValues.values.dateFilter ? parseInt(formValues.values.dateFilter) : undefined
return <>
<div className={"section-transparent stack"}>
<Breadcrumbs>{[
{title: "Home", to: "/"},
{title: "Admin Dashboard", to: "/admin"},
].map(({title, to}) => (
<Anchor className={"wrapWords"} component={Link} to={to} key={title}>
{title}
</Anchor>
))}</Breadcrumbs>
</div>
<div className={"section-transparent"}>
<Group justify={"space-between"}>
<Title order={1} c={"dimmed"}>
Analytics
</Title>
<Group>
{
[
{
label: "Sessions",
path: "/admin/analytics/sessions",
icon: <IconTimeline/>
},
{
label: "Fehler",
path: "/admin/analytics/error",
icon: <IconBug/>
}
].map((e, i) => (
<NavLink key={i} to={e.path} replace>
{({isActive}) => (
<Button
variant={isActive ? "filled" : "light"}
component={"span"}
leftSection={e.icon}
>
{e.label}
</Button>
)}
</NavLink>
))
}
<Select
leftSection={<IconCalendarStats/>}
placeholder="Zeitraum"
clearable
data={dateSelect}
{...formValues.getInputProps("dateFilter")}
/>
</Group>
</Group>
</div>
<SimpleGrid className={classes.grid} cols={{base: 1, sm: 3}}>
<CountStat
lastNDays={dateFilter}
collection={"analyticsVisitors"}
filter={'meta.localDevMode!=true'}
label={"Besucher"}
icon={<IconUsers/>}
/>
<CountStat
lastNDays={dateFilter}
collection={"analyticsPageViews"}
filter={'session.visitor.meta.localDevMode!=true'}
label={"Seitenaufrufe"}
icon={<IconEye/>}
/>
<CountStat
lastNDays={dateFilter}
collection={"analyticsPageViews"}
filter={'(session.visitor.meta.localDevMode!=true)&&(error!=null)'}
label={"Fehler"}
icon={<IconZoomExclamation/>}
/>
</SimpleGrid>
<Routes>
<Route index element={<Navigate to={"sessions"}/>}/>
<Route path={"sessions/*"} element={<Sessions lastNDays={dateFilter}/>}/>
<Route path={"*"} element={<NotFound/>}/>
</Routes>
<Outlet/>
</>
}

View File

@ -0,0 +1,24 @@
.wrapper {
padding-top: calc(var(--mantine-spacing-xl) * 4);
padding-bottom: calc(var(--mantine-spacing-xl) * 4);
}
.title {
font-family: var(--mantine-font-family), sans-serif;
font-weight: 900;
margin-bottom: var(--mantine-spacing-md);
text-align: center;
@media (max-width: $mantine-breakpoint-sm) {
font-size: var(--mantine-font-size-lg);
text-align: left;
}
}
.description {
text-align: center;
@media (max-width: $mantine-breakpoint-sm) {
text-align: left;
}
}

View File

@ -0,0 +1,260 @@
import {Alert, Anchor, Center, Code, rem, SimpleGrid, Text, ThemeIcon, Title} from "@mantine/core";
import {usePB} from "@/lib/pocketbase.tsx";
import {FC, ReactNode} from "react";
import {
IconBug,
IconCalendarClock,
IconGlobe,
IconId,
IconPassword,
IconSignature,
IconSlash,
IconUser,
IconUsers,
IconWorldWww
} from "@tabler/icons-react";
import {useCookies} from "react-cookie";
import {ANALYTICS_COOKIE_NAME, ANALYTICS_IP_API} from "../../../../config.ts";
import {Link, useLocation} from "react-router-dom";
import {ofetch} from "ofetch";
import {AnalyticsIpApiResult} from "@/models/AnalyticsTypes.ts";
import {useQuery} from "@tanstack/react-query";
import {UAParser} from "ua-parser-js";
import {
BrowserIcon,
DeviceTypeIcon,
OsIcon
} from "@/pages/admin/AnalyticsDashboard/AnalyticsSessions/AnalyticsSessionRow.tsx";
import {usePreferredLanguage} from "@uidotdev/usehooks";
import {getUserName} from "@/components/users/modals/util.tsx";
import {LdapGroupModel} from "@/models/AuthTypes.ts";
interface FeatureProps {
icon: FC<any>;
data: ReactNode;
title: ReactNode;
description: ReactNode;
}
function DataItem({icon: Icon, data, title, description}: FeatureProps) {
return (
<div className={"paper"}>
<ThemeIcon variant="light" size={40} radius={40}>
<Icon style={{width: rem(18), height: rem(18)}} stroke={1.5}/>
</ThemeIcon>
<Text mt="sm" mb={7}>
{title}
</Text>
<Text mt="sm" mb={7}>
<Code>
{data}
</Code>
</Text>
<Text size="sm" c="dimmed" lh={1.6}>
{description}
</Text>
</div>
)
}
export default function WhatWeKnowAboutYou() {
const {user} = usePB()
const location = useLocation()
const [cookies] = useCookies([ANALYTICS_COOKIE_NAME])
const cookieValue = cookies[ANALYTICS_COOKIE_NAME]
const parserResult = new UAParser().getResult()
const preferredLanguage = usePreferredLanguage()
const ipQuery = useQuery({
queryKey: ['ip'],
queryFn: async () => {
return await ofetch<AnalyticsIpApiResult>(ANALYTICS_IP_API, {ignoreResponseError: true})
}
})
return <>
<div className={"section-transparent stack"}>
<Center mt={"xl"} className={"stack"}>
<Title order={1} c={"blue"}>Welche Daten werden gesammelt?</Title>
<Anchor
td={"underline"}
component={Link}
to={"/legal/privacy-policy"}
>
Datenschutzerklärung
</Anchor>
</Center>
</div>
<div className={"section-transparent stack"}>
<Center mt={"xl"}>
<Title order={2}>Anonyme Analysedaten</Title>
</Center>
<Center>
<Text ta={"center"} c={"dimmed"}>
Alle Analysedaten, die wir sammeln, sind anonymisiert und dienen dazu, diesen Dienst zu verbessern
und zu personalisieren.
<br/>
Wir geben keine Daten an Dritte weiter. Die Daten liegen auf unseren eigenen Servern in Deutschland.
<br/>
Nur Systemadministratoren haben Zugriff auf die Daten. Wir sammeln diese Daten erst, nachdem Cookies
akzeptiert wurden.
</Text>
</Center>
{!cookieValue && (
<Alert ta={"center"} fw={"600"} color={"green"}>
Solange du keine Cookies akzeptierst, sammeln wir keine Analysedaten.
</Alert>
)}
<SimpleGrid
cols={{base: 1, sm: 2, md: 3}}
>
{
[
{
icon: IconId,
title: "Analyse-ID",
data: cookieValue || "-",
description: "Diese ID wird genutzt, um nachzuvollziehen, wie du über die Zeit verteilt die StuVe IT Tools nutzt."
},
{
icon: IconSlash,
title: "Aktuelle Seite",
data: location.pathname,
description: "Der Pfad der aktuellen Seite, die du besuchst. Wir speichern diese Pfade um zu analysieren, welche Seiten am häufigsten besucht werden und wie Nutzende navigieren."
},
{
icon: IconBug,
title: "Fehler",
data: "-",
description: "Falls ein Fehler auftritt speichern wir die Fehlermeldung und den Zeitpunkt und dem entsprechenden Pfad, um die Stabilität der Seite zu verbessern."
},
{
icon: IconWorldWww,
title: "IP Adresse",
data: ipQuery.data?.ip || "-",
description: "Deine IP Adresse wird genutzt, um das Land zu bestimmen, aus dem du die Seite besuchst."
},
{
icon: IconGlobe,
title: "Land",
data: ipQuery.data?.country_code || "-",
description: "Das Land, aus dem du die Seite besuchst. Diese Information wird genutzt, um die Seite zu personalisieren und um zu verstehen, woher die Nutzenden kommen."
},
{
icon: () => <BrowserIcon browser={parserResult.browser.name ?? ""}/>,
title: "Browser",
data: `${parserResult.browser.name} ${parserResult.browser.version}`,
description: "Dein Browser wird genutzt, um die Seite anzuzeigen. Diese Information wird genutzt, um die Seite zu optimieren und um zu verstehen, welche Technologien Nutzende verwenden."
},
{
icon: () => <OsIcon os={parserResult.os.name ?? ""}/>,
title: "Betriebssystem",
data: `${parserResult.os.name} ${parserResult.os.version}`,
description: "Dein Betriebssystem wird genutzt, um die Seite anzuzeigen. Diese Information wird genutzt, um die Seite zu optimieren und um zu verstehen, welche Technologien Nutzende verwenden."
},
{
icon: IconWorldWww,
title: "Bevorzugte Sprache",
data: preferredLanguage || "-",
description: "Deine bevorzugte Sprache wird genutzt, um zu verstehen, welche Sprachen Nutzende sprechen."
},
{
icon: () => <DeviceTypeIcon deviceType={parserResult.device.type ?? "desktop"}/>,
title: "Gerätetyp",
data: parserResult.device.type || "desktop",
description: "Dein Gerätetyp wird genutzt, um die Seite anzuzeigen. Diese Information wird genutzt, um die Seite zu optimieren und um zu verstehen, welche Technologien Nutzende verwenden."
}
].map((feature, index) => (
<DataItem key={index} {...feature}/>
))
}
</SimpleGrid>
</div>
{
user &&
<div className={"section-transparent stack"}>
<Center mt={"xl"}>
<Title order={2}>Accountdaten</Title>
</Center>
<Center>
<Text ta={"center"} c={"dimmed"}>
Wenn du einen Account hast speichern wir zusätzlich zu den anonymen Analysedaten auch deine
Accountdaten.
</Text>
</Center>
<SimpleGrid
cols={{base: 1, sm: 2, md: 3}}
>
{
[
{
icon: IconUser,
title: "Anmeldename und Realm",
data: `${user.username} (${user.REALM})`,
description: "Hiermit kannst du dich anmelden und deine Daten verwalten. Deine Realm wird genutzt, um zu bestimmen ob du einen StuVe-IT oder Gast-Account hast."
},
{
icon: IconPassword,
title: "Passwort",
data: '***********',
description: "Dein Passwort wird verschlüsselt auf unseren Servern gespeichert und kann nicht eingesehen werden."
},
{
icon: IconGlobe,
title: "E-Mail",
data: user.email,
description: "Deine E-Mail Adresse wird genutzt, um dich zu kontaktieren. Andere Nutzende können deine E-Mail Adresse nicht sehen."
},
{
icon: IconSignature,
title: "Name",
data: getUserName(user),
description: "Dein Name wird genutzt, um dich zu identifizieren. Andere Nutzende können deinen Namen sehen."
},
{
icon: IconCalendarClock,
title: "Account Ablaufdatum",
data: user?.accountExpires ? (
new Date(user?.accountExpires).getTime() > Date.now() ? (
"Account ist aktiv und läuft am " + new Date(user?.accountExpires).toLocaleDateString() + " ab"
) : (
"Account ist abgelaufen"
)
) : (
"Dein Account läuft nicht ab"
),
description: "Dein Account-Ablaufdatum wird genutzt, um zu bestimmen, ob dein Account noch aktiv ist."
},
{
icon: IconUsers,
title: "Gruppen",
data: user?.expand?.memberOf.map((group: LdapGroupModel) => group.cn).join(", ") || "Keine Gruppen",
description: "Dein Account-Ablaufdatum wird genutzt, um zu bestimmen, ob dein Account noch aktiv ist."
},
].map((feature, index) => (
<DataItem key={index} {...feature}/>
))
}
</SimpleGrid>
</div>
}
</>
}

View File

@ -1,5 +1,9 @@
@layer mantine, mantine-datatable;
.recharts-polar-angle-axis-tick-value {
font-size: calc(var(--mantine-font-size-xs) * 0.8);
}
* {
--gap: var(--mantine-spacing-sm);
--padding: var(--mantine-spacing-md);
@ -93,6 +97,19 @@ a:hover, a:active, a:visited {
}
}
.paper {
padding: var(--padding);
border: var(--border);
border-radius: var(--border-radius);
/*noinspection CssInvalidFunction*/
background-color: var(--mantine-color-body);
:first-child {
margin-top: 0;
}
}
.section {
padding: var(--padding);
border: var(--border);
@ -139,6 +156,10 @@ a:hover, a:active, a:visited {
gap: var(--gap);
}
.cursor-pointer {
cursor: pointer;
}
.center {
display: flex;
justify-content: center;

498
yarn.lock
View File

@ -216,120 +216,120 @@
"@babel/helper-validator-identifier" "^7.24.5"
to-fast-properties "^2.0.0"
"@esbuild/aix-ppc64@0.19.12":
version "0.19.12"
resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.19.12.tgz#d1bc06aedb6936b3b6d313bf809a5a40387d2b7f"
integrity sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA==
"@esbuild/aix-ppc64@0.21.5":
version "0.21.5"
resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz#c7184a326533fcdf1b8ee0733e21c713b975575f"
integrity sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==
"@esbuild/android-arm64@0.19.12":
version "0.19.12"
resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.19.12.tgz#7ad65a36cfdb7e0d429c353e00f680d737c2aed4"
integrity sha512-P0UVNGIienjZv3f5zq0DP3Nt2IE/3plFzuaS96vihvD0Hd6H/q4WXUGpCxD/E8YrSXfNyRPbpTq+T8ZQioSuPA==
"@esbuild/android-arm64@0.21.5":
version "0.21.5"
resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz#09d9b4357780da9ea3a7dfb833a1f1ff439b4052"
integrity sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==
"@esbuild/android-arm@0.19.12":
version "0.19.12"
resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.19.12.tgz#b0c26536f37776162ca8bde25e42040c203f2824"
integrity sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w==
"@esbuild/android-arm@0.21.5":
version "0.21.5"
resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.21.5.tgz#9b04384fb771926dfa6d7ad04324ecb2ab9b2e28"
integrity sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==
"@esbuild/android-x64@0.19.12":
version "0.19.12"
resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.19.12.tgz#cb13e2211282012194d89bf3bfe7721273473b3d"
integrity sha512-3k7ZoUW6Q6YqhdhIaq/WZ7HwBpnFBlW905Fa4s4qWJyiNOgT1dOqDiVAQFwBH7gBRZr17gLrlFCRzF6jFh7Kew==
"@esbuild/android-x64@0.21.5":
version "0.21.5"
resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.21.5.tgz#29918ec2db754cedcb6c1b04de8cd6547af6461e"
integrity sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==
"@esbuild/darwin-arm64@0.19.12":
version "0.19.12"
resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.19.12.tgz#cbee41e988020d4b516e9d9e44dd29200996275e"
integrity sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g==
"@esbuild/darwin-arm64@0.21.5":
version "0.21.5"
resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz#e495b539660e51690f3928af50a76fb0a6ccff2a"
integrity sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==
"@esbuild/darwin-x64@0.19.12":
version "0.19.12"
resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.19.12.tgz#e37d9633246d52aecf491ee916ece709f9d5f4cd"
integrity sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A==
"@esbuild/darwin-x64@0.21.5":
version "0.21.5"
resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz#c13838fa57372839abdddc91d71542ceea2e1e22"
integrity sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==
"@esbuild/freebsd-arm64@0.19.12":
version "0.19.12"
resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.12.tgz#1ee4d8b682ed363b08af74d1ea2b2b4dbba76487"
integrity sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA==
"@esbuild/freebsd-arm64@0.21.5":
version "0.21.5"
resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz#646b989aa20bf89fd071dd5dbfad69a3542e550e"
integrity sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==
"@esbuild/freebsd-x64@0.19.12":
version "0.19.12"
resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.19.12.tgz#37a693553d42ff77cd7126764b535fb6cc28a11c"
integrity sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg==
"@esbuild/freebsd-x64@0.21.5":
version "0.21.5"
resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz#aa615cfc80af954d3458906e38ca22c18cf5c261"
integrity sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==
"@esbuild/linux-arm64@0.19.12":
version "0.19.12"
resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.19.12.tgz#be9b145985ec6c57470e0e051d887b09dddb2d4b"
integrity sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA==
"@esbuild/linux-arm64@0.21.5":
version "0.21.5"
resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz#70ac6fa14f5cb7e1f7f887bcffb680ad09922b5b"
integrity sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==
"@esbuild/linux-arm@0.19.12":
version "0.19.12"
resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.19.12.tgz#207ecd982a8db95f7b5279207d0ff2331acf5eef"
integrity sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w==
"@esbuild/linux-arm@0.21.5":
version "0.21.5"
resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz#fc6fd11a8aca56c1f6f3894f2bea0479f8f626b9"
integrity sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==
"@esbuild/linux-ia32@0.19.12":
version "0.19.12"
resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.19.12.tgz#d0d86b5ca1562523dc284a6723293a52d5860601"
integrity sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA==
"@esbuild/linux-ia32@0.21.5":
version "0.21.5"
resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz#3271f53b3f93e3d093d518d1649d6d68d346ede2"
integrity sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==
"@esbuild/linux-loong64@0.19.12":
version "0.19.12"
resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.19.12.tgz#9a37f87fec4b8408e682b528391fa22afd952299"
integrity sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA==
"@esbuild/linux-loong64@0.21.5":
version "0.21.5"
resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz#ed62e04238c57026aea831c5a130b73c0f9f26df"
integrity sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==
"@esbuild/linux-mips64el@0.19.12":
version "0.19.12"
resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.19.12.tgz#4ddebd4e6eeba20b509d8e74c8e30d8ace0b89ec"
integrity sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w==
"@esbuild/linux-mips64el@0.21.5":
version "0.21.5"
resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz#e79b8eb48bf3b106fadec1ac8240fb97b4e64cbe"
integrity sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==
"@esbuild/linux-ppc64@0.19.12":
version "0.19.12"
resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.19.12.tgz#adb67dadb73656849f63cd522f5ecb351dd8dee8"
integrity sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg==
"@esbuild/linux-ppc64@0.21.5":
version "0.21.5"
resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz#5f2203860a143b9919d383ef7573521fb154c3e4"
integrity sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==
"@esbuild/linux-riscv64@0.19.12":
version "0.19.12"
resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.19.12.tgz#11bc0698bf0a2abf8727f1c7ace2112612c15adf"
integrity sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg==
"@esbuild/linux-riscv64@0.21.5":
version "0.21.5"
resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz#07bcafd99322d5af62f618cb9e6a9b7f4bb825dc"
integrity sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==
"@esbuild/linux-s390x@0.19.12":
version "0.19.12"
resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.19.12.tgz#e86fb8ffba7c5c92ba91fc3b27ed5a70196c3cc8"
integrity sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg==
"@esbuild/linux-s390x@0.21.5":
version "0.21.5"
resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz#b7ccf686751d6a3e44b8627ababc8be3ef62d8de"
integrity sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==
"@esbuild/linux-x64@0.19.12":
version "0.19.12"
resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.19.12.tgz#5f37cfdc705aea687dfe5dfbec086a05acfe9c78"
integrity sha512-B71g1QpxfwBvNrfyJdVDexenDIt1CiDN1TIXLbhOw0KhJzE78KIFGX6OJ9MrtC0oOqMWf+0xop4qEU8JrJTwCg==
"@esbuild/linux-x64@0.21.5":
version "0.21.5"
resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz#6d8f0c768e070e64309af8004bb94e68ab2bb3b0"
integrity sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==
"@esbuild/netbsd-x64@0.19.12":
version "0.19.12"
resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.19.12.tgz#29da566a75324e0d0dd7e47519ba2f7ef168657b"
integrity sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA==
"@esbuild/netbsd-x64@0.21.5":
version "0.21.5"
resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz#bbe430f60d378ecb88decb219c602667387a6047"
integrity sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==
"@esbuild/openbsd-x64@0.19.12":
version "0.19.12"
resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.19.12.tgz#306c0acbdb5a99c95be98bdd1d47c916e7dc3ff0"
integrity sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw==
"@esbuild/openbsd-x64@0.21.5":
version "0.21.5"
resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz#99d1cf2937279560d2104821f5ccce220cb2af70"
integrity sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==
"@esbuild/sunos-x64@0.19.12":
version "0.19.12"
resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.19.12.tgz#0933eaab9af8b9b2c930236f62aae3fc593faf30"
integrity sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA==
"@esbuild/sunos-x64@0.21.5":
version "0.21.5"
resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz#08741512c10d529566baba837b4fe052c8f3487b"
integrity sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==
"@esbuild/win32-arm64@0.19.12":
version "0.19.12"
resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.19.12.tgz#773bdbaa1971b36db2f6560088639ccd1e6773ae"
integrity sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A==
"@esbuild/win32-arm64@0.21.5":
version "0.21.5"
resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz#675b7385398411240735016144ab2e99a60fc75d"
integrity sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==
"@esbuild/win32-ia32@0.19.12":
version "0.19.12"
resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.19.12.tgz#000516cad06354cc84a73f0943a4aa690ef6fd67"
integrity sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ==
"@esbuild/win32-ia32@0.21.5":
version "0.21.5"
resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz#1bfc3ce98aa6ca9a0969e4d2af72144c59c1193b"
integrity sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==
"@esbuild/win32-x64@0.19.12":
version "0.19.12"
resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.19.12.tgz#c57c8afbb4054a3ab8317591a0b7320360b444ae"
integrity sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA==
"@esbuild/win32-x64@0.21.5":
version "0.21.5"
resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz#acad351d582d157bb145535db2a6ff53dd514b5c"
integrity sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==
"@eslint-community/eslint-utils@^4.2.0", "@eslint-community/eslint-utils@^4.4.0":
version "4.4.0"
@ -473,6 +473,11 @@
"@jridgewell/resolve-uri" "^3.1.0"
"@jridgewell/sourcemap-codec" "^1.4.14"
"@mantine/charts@^7.13.4":
version "7.13.4"
resolved "https://registry.yarnpkg.com/@mantine/charts/-/charts-7.13.4.tgz#cc775feaf436f12ffad031bb58a8e08b7aafa1c7"
integrity sha512-hQPqJ1wmKRbVSaoq/YDSKLP1F1YnW2DDgkMSPThFIjfiSblRGhHkwlPfPf844/jrw5px3Vor4jvTUf0bsUML9A==
"@mantine/code-highlight@^7.10.0":
version "7.10.0"
resolved "https://registry.yarnpkg.com/@mantine/code-highlight/-/code-highlight-7.10.0.tgz#189c0c109168a1b2aeab0069a6376b6538e827db"
@ -1084,6 +1089,57 @@
resolved "https://registry.yarnpkg.com/@types/cookie/-/cookie-0.6.0.tgz#eac397f28bf1d6ae0ae081363eca2f425bedf0d5"
integrity sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==
"@types/d3-array@^3.0.3":
version "3.2.1"
resolved "https://registry.yarnpkg.com/@types/d3-array/-/d3-array-3.2.1.tgz#1f6658e3d2006c4fceac53fde464166859f8b8c5"
integrity sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg==
"@types/d3-color@*":
version "3.1.3"
resolved "https://registry.yarnpkg.com/@types/d3-color/-/d3-color-3.1.3.tgz#368c961a18de721da8200e80bf3943fb53136af2"
integrity sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==
"@types/d3-ease@^3.0.0":
version "3.0.2"
resolved "https://registry.yarnpkg.com/@types/d3-ease/-/d3-ease-3.0.2.tgz#e28db1bfbfa617076f7770dd1d9a48eaa3b6c51b"
integrity sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==
"@types/d3-interpolate@^3.0.1":
version "3.0.4"
resolved "https://registry.yarnpkg.com/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz#412b90e84870285f2ff8a846c6eb60344f12a41c"
integrity sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==
dependencies:
"@types/d3-color" "*"
"@types/d3-path@*":
version "3.1.0"
resolved "https://registry.yarnpkg.com/@types/d3-path/-/d3-path-3.1.0.tgz#2b907adce762a78e98828f0b438eaca339ae410a"
integrity sha512-P2dlU/q51fkOc/Gfl3Ul9kicV7l+ra934qBFXCFhrZMOL6du1TM0pm1ThYvENukyOn5h9v+yMJ9Fn5JK4QozrQ==
"@types/d3-scale@^4.0.2":
version "4.0.8"
resolved "https://registry.yarnpkg.com/@types/d3-scale/-/d3-scale-4.0.8.tgz#d409b5f9dcf63074464bf8ddfb8ee5a1f95945bb"
integrity sha512-gkK1VVTr5iNiYJ7vWDI+yUFFlszhNMtVeneJ6lUTKPjprsvLLI9/tgEGiXJOnlINJA8FyA88gfnQsHbybVZrYQ==
dependencies:
"@types/d3-time" "*"
"@types/d3-shape@^3.1.0":
version "3.1.6"
resolved "https://registry.yarnpkg.com/@types/d3-shape/-/d3-shape-3.1.6.tgz#65d40d5a548f0a023821773e39012805e6e31a72"
integrity sha512-5KKk5aKGu2I+O6SONMYSNflgiP0WfZIQvVUMan50wHsLG1G94JlxEVnCpQARfTtzytuY0p/9PXXZb3I7giofIA==
dependencies:
"@types/d3-path" "*"
"@types/d3-time@*", "@types/d3-time@^3.0.0":
version "3.0.3"
resolved "https://registry.yarnpkg.com/@types/d3-time/-/d3-time-3.0.3.tgz#3c186bbd9d12b9d84253b6be6487ca56b54f88be"
integrity sha512-2p6olUZ4w3s+07q3Tm2dbiMZy5pCDfYwtLXXHUnVzXgQlZ/OyPtUz6OL382BkOuGlLXqfT+wqv8Fw2v8/0geBw==
"@types/d3-timer@^3.0.0":
version "3.0.2"
resolved "https://registry.yarnpkg.com/@types/d3-timer/-/d3-timer-3.0.2.tgz#70bbda77dc23aa727413e22e214afa3f0e852f70"
integrity sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==
"@types/date-arithmetic@*":
version "4.1.4"
resolved "https://registry.yarnpkg.com/@types/date-arithmetic/-/date-arithmetic-4.1.4.tgz#bdb441f61a916f11af1874a8c2cf787f77ffcb94"
@ -1351,6 +1407,11 @@
"@typescript-eslint/types" "6.9.0"
eslint-visitor-keys "^3.4.1"
"@uidotdev/usehooks@^2.4.1":
version "2.4.1"
resolved "https://registry.yarnpkg.com/@uidotdev/usehooks/-/usehooks-2.4.1.tgz#4b733eaeae09a7be143c6c9ca158b56cc1ea75bf"
integrity sha512-1I+RwWyS+kdv3Mv0Vmc+p0dPYH0DTRAo04HLyXReYBL9AeseDWUJyi4THuksBJcu9F0Pih69Ak150VDnqbVnXg==
"@ungap/structured-clone@^1.0.0", "@ungap/structured-clone@^1.2.0":
version "1.2.0"
resolved "https://registry.yarnpkg.com/@ungap/structured-clone/-/structured-clone-1.2.0.tgz#756641adb587851b5ccb3e095daf27ae581c8406"
@ -1563,7 +1624,7 @@ clsx@^1.2.1:
resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.2.1.tgz#0ddc4a20a549b59c93a4116bb26f5294ca17dc12"
integrity sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==
clsx@^2.1.1:
clsx@^2.0.0, clsx@^2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/clsx/-/clsx-2.1.1.tgz#eed397c9fd8bd882bfb18deab7102049a2f32999"
integrity sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==
@ -1622,6 +1683,11 @@ cosmiconfig@^8.1.3:
parse-json "^5.2.0"
path-type "^4.0.0"
countup.js@^2.8.0:
version "2.8.0"
resolved "https://registry.yarnpkg.com/countup.js/-/countup.js-2.8.0.tgz#64951f2df3ede28839413d654d8fef28251c32a8"
integrity sha512-f7xEhX0awl4NOElHulrl4XRfKoNH3rB+qfNSZZyjSZhaAoUk6elvhH+MNxMmlmuUJ2/QNTWPSA7U4mNtIAKljQ==
crelt@^1.0.0:
version "1.0.6"
resolved "https://registry.yarnpkg.com/crelt/-/crelt-1.0.6.tgz#7cc898ea74e190fb6ef9dae57f8f81cf7302df72"
@ -1653,6 +1719,77 @@ csstype@^3.0.2:
resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.2.tgz#1d4bf9d572f11c14031f0436e1c10bc1f571f50b"
integrity sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==
"d3-array@2 - 3", "d3-array@2.10.0 - 3", d3-array@^3.1.6:
version "3.2.4"
resolved "https://registry.yarnpkg.com/d3-array/-/d3-array-3.2.4.tgz#15fec33b237f97ac5d7c986dc77da273a8ed0bb5"
integrity sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==
dependencies:
internmap "1 - 2"
"d3-color@1 - 3":
version "3.1.0"
resolved "https://registry.yarnpkg.com/d3-color/-/d3-color-3.1.0.tgz#395b2833dfac71507f12ac2f7af23bf819de24e2"
integrity sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==
d3-ease@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/d3-ease/-/d3-ease-3.0.1.tgz#9658ac38a2140d59d346160f1f6c30fda0bd12f4"
integrity sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==
"d3-format@1 - 3":
version "3.1.0"
resolved "https://registry.yarnpkg.com/d3-format/-/d3-format-3.1.0.tgz#9260e23a28ea5cb109e93b21a06e24e2ebd55641"
integrity sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==
"d3-interpolate@1.2.0 - 3", d3-interpolate@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/d3-interpolate/-/d3-interpolate-3.0.1.tgz#3c47aa5b32c5b3dfb56ef3fd4342078a632b400d"
integrity sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==
dependencies:
d3-color "1 - 3"
d3-path@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/d3-path/-/d3-path-3.1.0.tgz#22df939032fb5a71ae8b1800d61ddb7851c42526"
integrity sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==
d3-scale@^4.0.2:
version "4.0.2"
resolved "https://registry.yarnpkg.com/d3-scale/-/d3-scale-4.0.2.tgz#82b38e8e8ff7080764f8dcec77bd4be393689396"
integrity sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==
dependencies:
d3-array "2.10.0 - 3"
d3-format "1 - 3"
d3-interpolate "1.2.0 - 3"
d3-time "2.1.1 - 3"
d3-time-format "2 - 4"
d3-shape@^3.1.0:
version "3.2.0"
resolved "https://registry.yarnpkg.com/d3-shape/-/d3-shape-3.2.0.tgz#a1a839cbd9ba45f28674c69d7f855bcf91dfc6a5"
integrity sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==
dependencies:
d3-path "^3.1.0"
"d3-time-format@2 - 4":
version "4.1.0"
resolved "https://registry.yarnpkg.com/d3-time-format/-/d3-time-format-4.1.0.tgz#7ab5257a5041d11ecb4fe70a5c7d16a195bb408a"
integrity sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==
dependencies:
d3-time "1 - 3"
"d3-time@1 - 3", "d3-time@2.1.1 - 3", d3-time@^3.0.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/d3-time/-/d3-time-3.1.0.tgz#9310db56e992e3c0175e1ef385e545e48a9bb5c7"
integrity sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==
dependencies:
d3-array "2 - 3"
d3-timer@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/d3-timer/-/d3-timer-3.0.1.tgz#6284d2a2708285b1abb7e201eda4380af35e63b0"
integrity sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==
date-arithmetic@^4.1.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/date-arithmetic/-/date-arithmetic-4.1.0.tgz#e5d6434e9deb71f79760a37b729e4a515e730ddf"
@ -1670,6 +1807,11 @@ debug@^4.0.0, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3
dependencies:
ms "2.1.2"
decimal.js-light@^2.4.1:
version "2.5.1"
resolved "https://registry.yarnpkg.com/decimal.js-light/-/decimal.js-light-2.5.1.tgz#134fd32508f19e208f4fb2f8dac0d2626a867934"
integrity sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==
decode-named-character-reference@^1.0.0:
version "1.0.2"
resolved "https://registry.yarnpkg.com/decode-named-character-reference/-/decode-named-character-reference-1.0.2.tgz#daabac9690874c394c81e4162a0304b35d824f0e"
@ -1791,34 +1933,34 @@ error-ex@^1.3.1:
dependencies:
is-arrayish "^0.2.1"
esbuild@^0.19.3:
version "0.19.12"
resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.19.12.tgz#dc82ee5dc79e82f5a5c3b4323a2a641827db3e04"
integrity sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg==
esbuild@^0.21.3:
version "0.21.5"
resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.21.5.tgz#9ca301b120922959b766360d8ac830da0d02997d"
integrity sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==
optionalDependencies:
"@esbuild/aix-ppc64" "0.19.12"
"@esbuild/android-arm" "0.19.12"
"@esbuild/android-arm64" "0.19.12"
"@esbuild/android-x64" "0.19.12"
"@esbuild/darwin-arm64" "0.19.12"
"@esbuild/darwin-x64" "0.19.12"
"@esbuild/freebsd-arm64" "0.19.12"
"@esbuild/freebsd-x64" "0.19.12"
"@esbuild/linux-arm" "0.19.12"
"@esbuild/linux-arm64" "0.19.12"
"@esbuild/linux-ia32" "0.19.12"
"@esbuild/linux-loong64" "0.19.12"
"@esbuild/linux-mips64el" "0.19.12"
"@esbuild/linux-ppc64" "0.19.12"
"@esbuild/linux-riscv64" "0.19.12"
"@esbuild/linux-s390x" "0.19.12"
"@esbuild/linux-x64" "0.19.12"
"@esbuild/netbsd-x64" "0.19.12"
"@esbuild/openbsd-x64" "0.19.12"
"@esbuild/sunos-x64" "0.19.12"
"@esbuild/win32-arm64" "0.19.12"
"@esbuild/win32-ia32" "0.19.12"
"@esbuild/win32-x64" "0.19.12"
"@esbuild/aix-ppc64" "0.21.5"
"@esbuild/android-arm" "0.21.5"
"@esbuild/android-arm64" "0.21.5"
"@esbuild/android-x64" "0.21.5"
"@esbuild/darwin-arm64" "0.21.5"
"@esbuild/darwin-x64" "0.21.5"
"@esbuild/freebsd-arm64" "0.21.5"
"@esbuild/freebsd-x64" "0.21.5"
"@esbuild/linux-arm" "0.21.5"
"@esbuild/linux-arm64" "0.21.5"
"@esbuild/linux-ia32" "0.21.5"
"@esbuild/linux-loong64" "0.21.5"
"@esbuild/linux-mips64el" "0.21.5"
"@esbuild/linux-ppc64" "0.21.5"
"@esbuild/linux-riscv64" "0.21.5"
"@esbuild/linux-s390x" "0.21.5"
"@esbuild/linux-x64" "0.21.5"
"@esbuild/netbsd-x64" "0.21.5"
"@esbuild/openbsd-x64" "0.21.5"
"@esbuild/sunos-x64" "0.21.5"
"@esbuild/win32-arm64" "0.21.5"
"@esbuild/win32-ia32" "0.21.5"
"@esbuild/win32-x64" "0.21.5"
escalade@^3.1.2:
version "3.1.2"
@ -1950,6 +2092,11 @@ esutils@^2.0.2:
resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64"
integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==
eventemitter3@^4.0.1:
version "4.0.7"
resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f"
integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==
extend@^3.0.0:
version "3.0.2"
resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa"
@ -1960,6 +2107,11 @@ fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3:
resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525"
integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==
fast-equals@^5.0.1:
version "5.0.1"
resolved "https://registry.yarnpkg.com/fast-equals/-/fast-equals-5.0.1.tgz#a4eefe3c5d1c0d021aeed0bc10ba5e0c12ee405d"
integrity sha512-WF1Wi8PwwSY7/6Kx0vKXtw8RwuSGoM1bvDaJbu7MxDlR1vovZjIAKrnzyrThgAjm6JDTu0fVgWXDlMGspodfoQ==
fast-glob@^3.2.11, fast-glob@^3.2.9:
version "3.3.1"
resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.1.tgz#784b4e897340f3dbbef17413b3f11acf03c874c4"
@ -2227,6 +2379,11 @@ inline-style-parser@0.2.3:
resolved "https://registry.yarnpkg.com/inline-style-parser/-/inline-style-parser-0.2.3.tgz#e35c5fb45f3a83ed7849fe487336eb7efa25971c"
integrity sha512-qlD8YNDqyTKTyuITrDOffsl6Tdhv+UC4hcdAVuQsK4IMQ99nSgd1MIA/Q+jQYoh9r3hVUXhYh7urSRmXPkW04g==
"internmap@1 - 2":
version "2.0.3"
resolved "https://registry.yarnpkg.com/internmap/-/internmap-2.0.3.tgz#6685f23755e43c524e251d29cbc97248e3061009"
integrity sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==
invariant@^2.2.4:
version "2.2.4"
resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6"
@ -3211,7 +3368,7 @@ postcss@^8.4.31:
picocolors "^1.0.0"
source-map-js "^1.0.2"
postcss@^8.4.33:
postcss@^8.4.43:
version "8.4.47"
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.47.tgz#5bf6c9a010f3e724c503bf03ef7947dcb0fea365"
integrity sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==
@ -3412,11 +3569,6 @@ raf-schd@^4.0.3:
resolved "https://registry.yarnpkg.com/raf-schd/-/raf-schd-4.0.3.tgz#5d6c34ef46f8b2a0e880a8fcdb743efc5bfdbc1a"
integrity sha512-tQkJl2GRWh83ui2DiPTJz9wEiMN20syf+5oKfB03yYP7ioZcJwsIK8FjrtLwH1m7C7e+Tt2yYBlrOpdT+dyeIQ==
react-beforeunload@^2.6.0:
version "2.6.0"
resolved "https://registry.yarnpkg.com/react-beforeunload/-/react-beforeunload-2.6.0.tgz#e7ead8dc39009a205b4249619050772595bfeefd"
integrity sha512-aKrGaRNc7fZQlDnmSYrXu4cbz9QEPhScA4A2mLxhjcULDy4VILLyLhSEjg2goIw3o5LQ1zss44kmQh5LXWYGCw==
react-big-calendar@^1.11.3:
version "1.11.3"
resolved "https://registry.yarnpkg.com/react-big-calendar/-/react-big-calendar-1.11.3.tgz#78f19f3acd11c8f8a7738ac836e83dc852fefdb9"
@ -3448,6 +3600,13 @@ react-cookie@^7.2.2:
hoist-non-react-statics "^3.3.2"
universal-cookie "^7.0.0"
react-countup@^6.5.3:
version "6.5.3"
resolved "https://registry.yarnpkg.com/react-countup/-/react-countup-6.5.3.tgz#e892aa3eab2d6ba9c3cdba30bf4ed6764826d848"
integrity sha512-udnqVQitxC7QWADSPDOxVWULkLvKUWrDapn5i53HE4DPRVgs+Y5rr4bo25qEl8jSh+0l2cToJgGMx+clxPM3+w==
dependencies:
countup.js "^2.8.0"
react-dom@^18.2.0:
version "18.2.0"
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.2.0.tgz#22aaf38708db2674ed9ada224ca4aa708d821e3d"
@ -3472,13 +3631,6 @@ react-error-boundary@^4.1.2:
dependencies:
"@babel/runtime" "^7.12.5"
react-infinite-scroll-component@^6.1.0:
version "6.1.0"
resolved "https://registry.yarnpkg.com/react-infinite-scroll-component/-/react-infinite-scroll-component-6.1.0.tgz#7e511e7aa0f728ac3e51f64a38a6079ac522407f"
integrity sha512-SQu5nCqy8DxQWpnUVLx7V7b7LcA37aM7tvoWjTLZp1dk6EJibM5/4EJKzOnl07/BsM1Y40sKLuqjCwwH/xV0TQ==
dependencies:
throttle-debounce "^2.1.0"
react-intersection-observer@^9.10.2:
version "9.10.2"
resolved "https://registry.yarnpkg.com/react-intersection-observer/-/react-intersection-observer-9.10.2.tgz#d5b14f80c9a6bed525becc228db7dccac5d0ec1c"
@ -3494,6 +3646,11 @@ react-is@^18.0.0:
resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.2.0.tgz#199431eeaaa2e09f86427efbb4f1473edb47609b"
integrity sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==
react-is@^18.3.1:
version "18.3.1"
resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.3.1.tgz#e83557dc12eae63a99e003a46388b1dcbb44db7e"
integrity sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==
react-lifecycles-compat@^3.0.4:
version "3.0.4"
resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362"
@ -3582,6 +3739,15 @@ react-router@6.20.0:
dependencies:
"@remix-run/router" "1.13.0"
react-smooth@^4.0.0:
version "4.0.1"
resolved "https://registry.yarnpkg.com/react-smooth/-/react-smooth-4.0.1.tgz#6200d8699bfe051ae40ba187988323b1449eab1a"
integrity sha512-OE4hm7XqR0jNOq3Qmk9mFLyd6p2+j6bvbPJ7qlB7+oo0eNcL2l7WQzG6MBnT3EXY6xzkLMUBec3AfewJdA0J8w==
dependencies:
fast-equals "^5.0.1"
prop-types "^15.8.1"
react-transition-group "^4.4.5"
react-style-singleton@^2.2.1:
version "2.2.1"
resolved "https://registry.yarnpkg.com/react-style-singleton/-/react-style-singleton-2.2.1.tgz#f99e420492b2d8f34d38308ff660b60d0b1205b4"
@ -3600,7 +3766,7 @@ react-textarea-autosize@8.5.3:
use-composed-ref "^1.3.0"
use-latest "^1.2.1"
react-transition-group@4.4.5:
react-transition-group@4.4.5, react-transition-group@^4.4.5:
version "4.4.5"
resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-4.4.5.tgz#e53d4e3f3344da8521489fbef8f2581d42becdd1"
integrity sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==
@ -3624,6 +3790,27 @@ readdirp@~3.6.0:
dependencies:
picomatch "^2.2.1"
recharts-scale@^0.4.4:
version "0.4.5"
resolved "https://registry.yarnpkg.com/recharts-scale/-/recharts-scale-0.4.5.tgz#0969271f14e732e642fcc5bd4ab270d6e87dd1d9"
integrity sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w==
dependencies:
decimal.js-light "^2.4.1"
recharts@2:
version "2.13.3"
resolved "https://registry.yarnpkg.com/recharts/-/recharts-2.13.3.tgz#a5ce61e493dff921a14a14a8f42a9f2d2bbefd5a"
integrity sha512-YDZ9dOfK9t3ycwxgKbrnDlRC4BHdjlY73fet3a0C1+qGMjXVZe6+VXmpOIIhzkje5MMEL8AN4hLIe4AMskBzlA==
dependencies:
clsx "^2.0.0"
eventemitter3 "^4.0.1"
lodash "^4.17.21"
react-is "^18.3.1"
react-smooth "^4.0.0"
recharts-scale "^0.4.4"
tiny-invariant "^1.3.1"
victory-vendor "^36.6.8"
recoil@^0.7.7:
version "0.7.7"
resolved "https://registry.yarnpkg.com/recoil/-/recoil-0.7.7.tgz#c5f2c843224384c9c09e4a62c060fb4c1454dc8e"
@ -3702,7 +3889,7 @@ rimraf@^3.0.2:
dependencies:
glob "^7.1.3"
rollup@^4.2.0:
rollup@^4.20.0:
version "4.24.3"
resolved "https://registry.yarnpkg.com/rollup/-/rollup-4.24.3.tgz#8b259063740af60b0030315f88665ba2041789b8"
integrity sha512-HBW896xR5HGmoksbi3JBDtmVzWiPAYqp7wip50hjQ67JbDz61nyoMPdqu1DvVW9asYb2M65Z20ZHsyJCMqMyDg==
@ -3892,12 +4079,7 @@ text-table@^0.2.0:
resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4"
integrity sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==
throttle-debounce@^2.1.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/throttle-debounce/-/throttle-debounce-2.3.0.tgz#fd31865e66502071e411817e241465b3e9c372e2"
integrity sha512-H7oLPV0P7+jgvrk+6mwwwBDmxTaxnu9HMXmloNLXwnNO0ZxZ31Orah2n8lU1eMPvsaowP2CX+USCgyovXfdOFQ==
tiny-invariant@^1.0.6:
tiny-invariant@^1.0.6, tiny-invariant@^1.3.1:
version "1.3.3"
resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.3.3.tgz#46680b7a873a0d5d10005995eb90a70d74d60127"
integrity sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==
@ -4179,6 +4361,26 @@ vfile@^6.0.0:
unist-util-stringify-position "^4.0.0"
vfile-message "^4.0.0"
victory-vendor@^36.6.8:
version "36.9.2"
resolved "https://registry.yarnpkg.com/victory-vendor/-/victory-vendor-36.9.2.tgz#668b02a448fa4ea0f788dbf4228b7e64669ff801"
integrity sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==
dependencies:
"@types/d3-array" "^3.0.3"
"@types/d3-ease" "^3.0.0"
"@types/d3-interpolate" "^3.0.1"
"@types/d3-scale" "^4.0.2"
"@types/d3-shape" "^3.1.0"
"@types/d3-time" "^3.0.0"
"@types/d3-timer" "^3.0.0"
d3-array "^3.1.6"
d3-ease "^3.0.1"
d3-interpolate "^3.0.1"
d3-scale "^4.0.2"
d3-shape "^3.1.0"
d3-time "^3.0.0"
d3-timer "^3.0.1"
vite-plugin-svgr@^4.2.0:
version "4.2.0"
resolved "https://registry.yarnpkg.com/vite-plugin-svgr/-/vite-plugin-svgr-4.2.0.tgz#9f3bf5206b0ec510287e56d16f1915e729bb4e6b"
@ -4188,14 +4390,14 @@ vite-plugin-svgr@^4.2.0:
"@svgr/core" "^8.1.0"
"@svgr/plugin-jsx" "^8.1.0"
vite@5.1.0-beta.2:
version "5.1.0-beta.2"
resolved "https://registry.yarnpkg.com/vite/-/vite-5.1.0-beta.2.tgz#ca2bf7504952f8e0384ed5d2ed2e13c0ab4fbdd8"
integrity sha512-FpzQ6WBc2x7F71QmLEP6nCaFy6IlhkfrzYuLEd4Ax8mGju+BncnggAe3e4j6wLnh8FA7GkCxJu1ds2ZY0+Ws4A==
vite@5.4.6:
version "5.4.6"
resolved "https://registry.yarnpkg.com/vite/-/vite-5.4.6.tgz#85a93a1228a7fb5a723ca1743e337a2588ed008f"
integrity sha512-IeL5f8OO5nylsgzd9tq4qD2QqI0k2CQLGrWD0rCN0EQJZpBK5vJAx0I+GDkMOXxQX/OfFHMuLIx6ddAxGX/k+Q==
dependencies:
esbuild "^0.19.3"
postcss "^8.4.33"
rollup "^4.2.0"
esbuild "^0.21.3"
postcss "^8.4.43"
rollup "^4.20.0"
optionalDependencies:
fsevents "~2.3.3"