feat(analytics): analytics dashboard and whatWeKnowAboutYou Seite
Build and Push Docker image / build-and-push (push) Successful in 4m47s
Details
Build and Push Docker image / build-and-push (push) Successful in 4m47s
Details
This commit is contained in:
parent
2ace56cfab
commit
29c52d2638
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 |
|
@ -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/>
|
||||
}
|
||||
]
|
||||
},
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
}
|
|
@ -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])
|
||||
}
|
|
@ -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])
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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]
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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";
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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/>
|
||||
</>
|
||||
}
|
|
@ -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>
|
||||
</>
|
||||
}
|
|
@ -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 */
|
||||
}
|
||||
|
|
@ -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>
|
||||
</>
|
||||
}
|
|
@ -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>
|
||||
)
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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/>
|
||||
</>
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
}
|
||||
</>
|
||||
}
|
|
@ -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
498
yarn.lock
|
@ -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"
|
||||
|
||||
|
|
Loading…
Reference in New Issue