feat(analytics): added analytics
Build and Push Docker image / build-and-push (push) Successful in 4m55s Details

This commit is contained in:
Valentin Kolb 2024-11-01 21:01:12 +01:00
parent 50a22f6187
commit 2ace56cfab
32 changed files with 578 additions and 68 deletions

View File

@ -11,3 +11,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"
// analytics
export const ANALYTICS_COOKIE_NAME = "stuve-it-analytics"
export const ANALYTICS_IP_API = "https://backend.stuve-it.de/api/ip/json"

View File

@ -42,12 +42,16 @@
"jwt-decode": "^3.1.2",
"ms": "^2.1.3",
"nanoid": "^5.0.7",
"ofetch": "^1.4.1",
"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-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",
@ -57,6 +61,7 @@
"sanitize-html": "^2.13.0",
"tippy.js": "^6.3.7",
"tiptap": "^1.32.2",
"ua-parser-js": "^2.0.0-rc.1",
"zod": "^3.22.4"
},
"devDependencies": {

View File

@ -1,17 +1,22 @@
import {createBrowserRouter, RouterProvider} from "react-router-dom";
import HomePage from "./pages/home/index.page.tsx";
import NotFound from "./pages/not-found/index.page.tsx";
import NotFound from "@/pages/error-pages/NotFound.tsx";
import Layout from "@/components/layout";
import QRCodeGenerator from "./pages/util/qr/index.page.tsx";
import EventsRouter from "./pages/events/EventsRouter.tsx";
import LegalPage from "@/pages/LegalPage.tsx";
import DebugPage from "@/pages/test/DebugPage.tsx";
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";
const router = createBrowserRouter([
{
path: "/",
element: <Layout/>,
element: <>
<Analytics/>
<Layout/>
</>,
children: [
{
index: true,
@ -29,6 +34,10 @@ const router = createBrowserRouter([
path: "email/*",
element: <EmailRouter/>,
},
{
path: "admin/*",
element: <AdminRouter/>,
},
{
path: "debug",
element: <DebugPage/>

View File

@ -0,0 +1,60 @@
import {Button, Dialog, Text} from "@mantine/core";
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";
/**
* Component to handle analytics and display a dialog for cookie consent.
*
* @returns The rendered Analytics component.
*/
export default function Analytics() {
// Initialize visitor analytics
const {visitorId, initVisitor} = useAnalyticsVisitor()
// Initialize session analytics with the visitor ID
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>
<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>
</div>
</Dialog>
</>
)
}

View File

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

View File

@ -0,0 +1,36 @@
import {useEffect} from "react";
import {useLocation} from "react-router-dom";
import {useMutation} from "@tanstack/react-query";
import {usePB} from "@/lib/pocketbase.tsx";
/**
* Custom hook to track page views for analytics.
*
* @param {string} [sessionId] - The ID of the current analytics session.
*/
export default function useAnalyticsPageView(sessionId?: string) {
const location = useLocation()
const {pb} = usePB()
/**
* Mutation to log a page view in the analytics system.
*
* @param {string} path - The path of the page being viewed.
*/
const pageViewMutation = useMutation({
mutationFn: async (path: string) => {
await pb.collection("analyticsPageViews").create({
path,
session: sessionId,
})
}
})
// Effect to log the page view when the session ID or location changes
useEffect(() => {
if (!sessionId) return
pageViewMutation.mutate(location.pathname)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [sessionId, location])
}

View File

@ -0,0 +1,64 @@
import {useEffect, useState} from "react";
import {useMutation} from "@tanstack/react-query";
import {usePB} from "@/lib/pocketbase.tsx";
import {nanoid} from "nanoid/non-secure";
import {UAParser} from "ua-parser-js";
import {AnalyticsIpApiResult} from "@/models/AnalyticsTypes.ts";
import {ANALYTICS_IP_API} from "../../../config.ts";
import {ofetch} from "ofetch";
/**
* Custom hook to manage an analytics session.
*
* @param {string} [visitorId] - The ID of the visitor.
* @returns {Object} - An object containing the session ID.
*/
export default function useAnalyticsSession(visitorId?: string) {
// State to store the session ID
const [sessionId, setSessionId] = useState<string | undefined>(undefined)
const {pb} = usePB()
// Mutation to initialize the analytics session
const initSessionMutation = useMutation({
mutationFn: async () => {
if (!visitorId) return
// Parse the user agent to get device and browser information
const parser = new UAParser()
const result = parser.getResult()
// Fetch IP and country code from the analytics IP API
const {
ip,
country_code
} = await ofetch<AnalyticsIpApiResult>(ANALYTICS_IP_API, {ignoreResponseError: true})
// Create a new analytics session in the PocketBase collection
return await pb.collection("analyticsSessions").create({
id: nanoid(15),
visitor: visitorId,
device_type: result.device.type || 'desktop',
browser_name: result.browser.name,
browser_version: result.browser.version,
operating_system: result.os.name,
operating_system_version: result.os.version,
user_agent: navigator.userAgent,
ip_address: ip,
geo_country_code: country_code
})
},
onSuccess: (data) => {
// Set the session ID on successful creation
setSessionId(data?.id)
}
})
// Effect to initialize the session when the visitor ID changes
useEffect(() => {
initSessionMutation.mutate()
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [visitorId])
return {sessionId}
}

View File

@ -0,0 +1,62 @@
import {useCookies} from "react-cookie";
import {ANALYTICS_COOKIE_NAME} 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";
/**
* This hook is used to manage the analytics visitor id and the cookie.
*
* If the cookie is not set, the returned visitorId will be undefined.
* To set the cookie and create the visitor in the database, call the initVisitor function.
*/
export default function useAnalyticsVisitor() {
const [cookies, setCookie] = useCookies([ANALYTICS_COOKIE_NAME])
const {pb} = usePB()
const initVisitor = () => {
// create 15 chars long random string
const visitorId = nanoid(15)
// create visitor in database
upsertVisitorMutation.mutateAsync(visitorId).then(() => {
// set cookie
setCookie(ANALYTICS_COOKIE_NAME, visitorId, {path: "/"})
})
}
const cookieValue = cookies[ANALYTICS_COOKIE_NAME]
const cookieIsSet = cookieValue !== undefined
const upsertVisitorMutation = useMutation({
mutationFn: async (visitorId: string) => {
try {
// set visitor updated date to now
await pb.collection("analyticsVisitors").update(visitorId, {})
} catch (e) {
if ((e as ClientResponseError).status === 404) {
// visitor does not exist, create it
await pb.collection("analyticsVisitors").create({id: visitorId})
}
}
}
})
// this effect will run when ever the cookie is set or the page reloads
useEffect(() => {
if (cookieIsSet) {
upsertVisitorMutation.mutate(cookieValue)
}
// eslint-disable-next-line
}, [cookieIsSet, cookieValue])
return {
visitorId: cookieValue as string | undefined,
initVisitor
}
}

View File

@ -8,9 +8,6 @@
@media (max-width: $mantine-breakpoint-sm) {
padding: calc(var(--padding)/2) var(--mantine-spacing-xl);
}
/*noinspection CssInvalidFunction*/
//box-shadow: 0 0 5px 10px light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-9));;
}
.bottomContainer {

View File

@ -11,6 +11,7 @@ import {
IconSectionSign
} from "@tabler/icons-react";
import {useShowDebug} from "@/components/ShowDebug.tsx";
import {usePB} from "@/lib/pocketbase.tsx";
const NavItems = [
@ -101,16 +102,34 @@ const DebugMenuItems = [
}
]
const AdminMenuItems = [
{
section: "Admin",
items: [
{
title: "Dashboard",
icon: IconBug,
description: "Dashboard für Admins",
link: "/admin",
color: "blue"
}
]
}
]
export default function MenuItems() {
const {showDebug} = useShowDebug()
const {user} = usePB()
let nav
let nav = NavItems
if (showDebug) {
nav = [...NavItems, ...DebugMenuItems]
} else {
nav = NavItems
nav = [...nav, ...DebugMenuItems]
}
if (user?.isAdmin) {
nav = [...nav, ...AdminMenuItems]
}
return <>

View File

@ -47,8 +47,6 @@ ReactDOM.createRoot(document.getElementById('root')!).render(
<Router/>
</PocketBaseProvider>
<ReactQueryDevtools initialIsOpen={false}/>
{/*
*/}
</QueryClientProvider>
</ModalsProvider>
</MantineProvider>

View File

@ -0,0 +1,50 @@
import {RecordModel} from "pocketbase";
export type AnalyticsVisitorsModel = {
meta: object;
} & RecordModel
export type AnalyticsSessionModel = {
visitor: string
device_type?: string
browser_name?: string
browser_version?: string
operating_system?: string
operating_system_version?: string
user_agent?: string
ip_address?: string
geo_country_code?: string
expand: {
visitor: AnalyticsVisitorsModel
}
} & RecordModel
export type AnalyticsPageViewModel = {
session: string
path: string
expand?: {
session: AnalyticsSessionModel
}
} & RecordModel
export type AnalyticsErrorModel = {
session: string
error_message?: string
error_type?: string
stack_trace?: string
path?: string
expand?: {
session: AnalyticsSessionModel
}
} & RecordModel
export type AnalyticsIpApiResult = {
ip: string
country_code: string
country_name: string
asn: string
as_desc: string
user_agent: string
}

View File

@ -5,6 +5,7 @@ export type UserModel = {
verified: boolean;
email: string;
emailVisibility: boolean;
isAdmin: boolean;
sn: string | null;
givenName: string | null;

View File

@ -1,5 +1,5 @@
import PocketBase, {RecordModel, RecordService} from "pocketbase";
import {LdapGroupModel, LdapSyncLogModel, UserModel} from "./AuthTypes.ts";
import PocketBase, {RecordModel, RecordService} from "pocketbase"
import {LdapGroupModel, LdapSyncLogModel, UserModel} from "./AuthTypes.ts"
import {
EventListModel,
EventListSlotEntriesWithUserModel,
@ -8,8 +8,14 @@ import {
EventListSlotsWithEntriesCountModel,
EventModel
} from "./EventTypes.ts";
import {MessagesModel} from "@/models/MessageTypes.ts";
import {EmailModel} from "@/models/EmailTypes.ts";
import {MessagesModel} from "@/models/MessageTypes.ts"
import {EmailModel} from "@/models/EmailTypes.ts"
import {
AnalyticsErrorModel,
AnalyticsPageViewModel,
AnalyticsSessionModel,
AnalyticsVisitorsModel
} from "@/models/AnalyticsTypes.ts"
export type SettingsModel = {
key: ['privacyPolicy', 'agb', 'stexGroup', 'stuveEventQuestions']
@ -53,4 +59,12 @@ export interface TypedPocketBase extends PocketBase {
collection(idOrName: 'eventListSlotEntriesWithUser'): RecordService<EventListSlotEntriesWithUserModel>
collection(idOrName: 'emails'): RecordService<EmailModel>
collection(idOrName: 'analyticsVisitors'): RecordService<AnalyticsVisitorsModel>
collection(idOrName: 'analyticsSessions'): RecordService<AnalyticsSessionModel>
collection(idOrName: 'analyticsPageViews'): RecordService<AnalyticsPageViewModel>
collection(idOrName: 'analyticsErrors'): RecordService<AnalyticsErrorModel>
}

View File

@ -4,7 +4,7 @@ import {pprintDate} from "@/lib/datetime.ts";
import {useParams} from "react-router-dom";
import {useQuery} from "@tanstack/react-query";
import {usePB} from "@/lib/pocketbase.tsx";
import NotFound from "@/pages/not-found/index.page.tsx";
import NotFound from "@/pages/error-pages/NotFound.tsx";
export default function LegalPage() {
const {page} = useParams() as { page: string }

View File

@ -0,0 +1,18 @@
import {usePB} from "@/lib/pocketbase.tsx";
import NotAllowed from "@/pages/error-pages/NotAllowed.tsx";
export default function AdminRouter() {
const {user} = usePB()
if (!user?.isAdmin) {
return <NotAllowed/>
}
return (
<div>
<h1>Admin Panel</h1>
</div>
)
}

View File

@ -1,16 +1,5 @@
import ShowDebug, {useShowDebug} from "@/components/ShowDebug.tsx";
import {
ActionIcon,
Alert,
Button,
Divider,
Group,
LoadingOverlay,
Pagination,
Text,
TextInput,
Title
} from "@mantine/core";
import {ActionIcon, Alert, Divider, Group, LoadingOverlay, Pagination, Text, TextInput, Title} from "@mantine/core";
import {useForm} from "@mantine/form";
import {useQuery} from "@tanstack/react-query";
import {PocketBaseErrorAlert, usePB} from "@/lib/pocketbase.tsx";
@ -18,19 +7,12 @@ import {CodeHighlight} from "@mantine/code-highlight";
import {IconRefresh} from "@tabler/icons-react";
import {RecordListOptions} from "pocketbase";
import {useState} from "react";
import {useDisclosure} from "@mantine/hooks";
import {EmailModal} from "@/components/EmailModal";
import {UserModel} from "@/models/AuthTypes.ts";
import UserInput from "@/components/users/UserInput.tsx";
export default function DebugPage() {
const {showDebug} = useShowDebug()
const [users, setUsers] = useState<UserModel[]>([])
const {pb} = usePB()
const [opened, handler] = useDisclosure()
const [page, setPage] = useState(1)
@ -70,24 +52,6 @@ export default function DebugPage() {
const result = formValues.values.select ? debugQuery.data?.items.map(i => i[formValues.values.select]) : debugQuery.data?.items
return <>
<UserInput
required
placeholder={"Empfängerinnen"}
variant={"filled"}
selectedRecords={users}
setSelectedRecords={setUsers}
error={formValues.errors.recipients}
/>
<EmailModal
opened={opened}
onClose={handler.close}
recipients={users}
description={"Hier kannst du eine Email an einen oder mehrere Empfänger senden."}
/>
<Button onClick={handler.toggle}>Open Email Modal</Button>
<div className={"section"}>
<Title c={"orange"} order={1}>Debug</Title>
</div>

View File

@ -8,7 +8,7 @@ import SentEmailsNavigation from "@/pages/email/SentEmailsNavigation.tsx";
import {usePB} from "@/lib/pocketbase.tsx";
import {useLogin} from "@/components/users/modals/hooks.ts";
import {useEffect} from "react";
import NotFound from "@/pages/not-found/index.page.tsx";
import NotFound from "@/pages/error-pages/NotFound.tsx";
import EmailView from "@/pages/email/EmailView";
const EmailIndex = () => {

View File

@ -54,7 +54,6 @@
}
.emailListContainer {
//flex-grow: 1;
overflow: auto;
border: var(--border);
border-radius: var(--border-radius);

View File

@ -0,0 +1,20 @@
import classes from "./NotFound.module.css"
import ErrorSVG from "@/illustrations/error.svg?react"
export default function ErrorPage() {
return (
<div className={classes.container}>
<ErrorSVG className={classes.svg} aria-label={"error image"}/>
<h1 className={classes.status}>S*!:#!</h1>
<div>
<p>
Irgendwas ist schiefgelaufen. Bitte versuche es später erneut.
</p>
</div>
</div>
)
}

View File

@ -0,0 +1,24 @@
import classes from "./NotFound.module.css"
import ErrorSVG from "@/illustrations/error.svg?react"
export default function NotAllowed() {
return (
<div className={classes.container}>
<ErrorSVG className={classes.svg} aria-label={"not allowed image"}/>
<h1 className={classes.status}>Nein!</h1>
<div>
<p>
Du hast nicht die nötigen Berechtigungen, um diese Seite zu nutzen.
</p>
<p>
Nutze die Navigation, um zurück in vertraute Dimensionen zu gelangen.
</p>
</div>
</div>
)
}

View File

@ -1,4 +1,4 @@
import classes from "./index.module.css"
import classes from "./NotFound.module.css"
import NotFoundSvg from "@/illustrations/not-found.svg?react"

View File

@ -2,7 +2,7 @@ import {Navigate, useParams} from "react-router-dom";
import {usePB} from "@/lib/pocketbase.tsx";
import {useQuery} from "@tanstack/react-query";
import {LoadingOverlay} from "@mantine/core";
import NotFound from "@/pages/not-found/index.page.tsx";
import NotFound from "@/pages/error-pages/NotFound.tsx";
import {useEventRights} from "@/pages/events/util.ts";
/**

View File

@ -1,5 +1,5 @@
import {Outlet, Route, Routes} from "react-router-dom";
import NotFound from "../not-found/index.page.tsx";
import NotFound from "@/pages/error-pages/NotFound.tsx";
import EventOverview from "@/pages/events/EventOverview/index.page.tsx";
import EventView from "./s/EventView.tsx";
import EventNavigate from "@/pages/events/EventNavigate.tsx";

View File

@ -5,7 +5,7 @@ import {Button, Center, Collapse, Divider, Group, Loader, Text, Title} from "@ma
import {getUserName} from "@/components/users/modals/util.tsx";
import {RenderDateRange} from "@/pages/events/e/:eventId/EventLists/EventListComponents/RenderDateRange.tsx";
import EditStatus from "@/pages/events/StatusEditor/EditEntryStatus/EditStatus.tsx";
import NotFound from "@/pages/not-found/index.page.tsx";
import NotFound from "@/pages/error-pages/NotFound.tsx";
import {humanDeltaFromNow} from "@/lib/datetime.ts";
import InnerHtml from "@/components/InnerHtml";
import {useDisclosure} from "@mantine/hooks";

View File

@ -3,7 +3,7 @@ import {Link, Navigate, Outlet, Route, Routes, useParams} from "react-router-dom
import {useQuery} from "@tanstack/react-query";
import {useEventRights} from "@/pages/events/util.ts";
import {Anchor, Breadcrumbs, LoadingOverlay} from "@mantine/core";
import NotFound from "@/pages/not-found/index.page.tsx";
import NotFound from "@/pages/error-pages/NotFound.tsx";
import Index from "@/pages/events/StatusEditor/EditEntryStatus";
import EntrySearch from "@/pages/events/StatusEditor/EntrySearch.tsx";

View File

@ -29,7 +29,7 @@ import {
IconSectionSign,
IconSettings
} from "@tabler/icons-react";
import NotFound from "@/pages/not-found/index.page.tsx";
import NotFound from "@/pages/error-pages/NotFound.tsx";
import EventDescription from "./EventComponents/EventDescription.tsx";
import EventAGB from "./EventComponents/EventAGB.tsx";
import EventData from "./EventComponents/EventData.tsx";

View File

@ -23,7 +23,7 @@ import ListEntryStatusSettings
import ShowDebug from "@/components/ShowDebug.tsx";
import {pprintDateTime} from "@/lib/datetime.ts";
import {useEventRights} from "@/pages/events/util.ts";
import NotFound from "@/pages/not-found/index.page.tsx";
import NotFound from "@/pages/error-pages/NotFound.tsx";
export default function EventListRouter({event}: { event: EventModel }) {

View File

@ -12,7 +12,7 @@ import EditDefaultEntryStatusSchema
import EditDefaultEntryQuestionSchema
from "@/pages/events/e/:eventId/EventLists/GeneralListSettings/EditDefaultEntryQuestionSchema.tsx";
import {useEventRights} from "@/pages/events/util.ts";
import NotFound from "@/pages/not-found/index.page.tsx";
import NotFound from "@/pages/error-pages/NotFound.tsx";
const viewNav = [

View File

@ -1,7 +1,7 @@
import {Link, useParams, useSearchParams} from "react-router-dom";
import {usePB} from "@/lib/pocketbase.tsx";
import {useQuery} from "@tanstack/react-query";
import NotFound from "../../not-found/index.page.tsx";
import NotFound from "@/pages/error-pages/NotFound.tsx";
import {Accordion, Alert, Anchor, Breadcrumbs, Button, Center, Group, Loader, Title} from "@mantine/core";
import PBAvatar from "@/components/PBAvatar.tsx";
import InnerHtml from "@/components/InnerHtml";

View File

@ -168,6 +168,13 @@
dependencies:
regenerator-runtime "^0.14.0"
"@babel/runtime@^7.12.5":
version "7.26.0"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.26.0.tgz#8600c2f595f277c60815256418b85356a65173c1"
integrity sha512-FDSOghenHTiToteC/QRlv2q3DhPZ/oOXTBoirfWNx1Cx3TMVcGWQtMMmQcSvb/JjpNeGzx8Pq/b4fKEJuWm1sw==
dependencies:
regenerator-runtime "^0.14.0"
"@babel/runtime@^7.20.13", "@babel/runtime@^7.5.5", "@babel/runtime@^7.8.7":
version "7.23.2"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.23.2.tgz#062b0ac103261d68a966c4c7baf2ae3e62ec3885"
@ -1072,6 +1079,11 @@
resolved "https://registry.yarnpkg.com/@tiptap/suggestion/-/suggestion-2.4.0.tgz#1926cde5f197d116baf7794f55bd971245540e5c"
integrity sha512-6dCkjbL8vIzcLWtS6RCBx0jlYPKf2Beuyq5nNLrDDZZuyJow5qJAY0eGu6Xomp9z0WDK/BYOxT4hHNoGMDkoAg==
"@types/cookie@^0.6.0":
version "0.6.0"
resolved "https://registry.yarnpkg.com/@types/cookie/-/cookie-0.6.0.tgz#eac397f28bf1d6ae0ae081363eca2f425bedf0d5"
integrity sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==
"@types/date-arithmetic@*":
version "4.1.4"
resolved "https://registry.yarnpkg.com/@types/date-arithmetic/-/date-arithmetic-4.1.4.tgz#bdb441f61a916f11af1874a8c2cf787f77ffcb94"
@ -1123,7 +1135,7 @@
dependencies:
"@types/unist" "*"
"@types/hoist-non-react-statics@^3.3.1":
"@types/hoist-non-react-statics@^3.3.1", "@types/hoist-non-react-statics@^3.3.5":
version "3.3.5"
resolved "https://registry.yarnpkg.com/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.5.tgz#dab7867ef789d87e2b4b0003c9d65c49cc44a494"
integrity sha512-SbcrWzkKBw2cdwRTwQAswfpB9g9LJWfjtUeW/jvNwbhC8cpmmNYVePa+ncbUe0rGTQ7G3Ff6mYUN2VMfLVr+Sg==
@ -1595,6 +1607,11 @@ convert-source-map@^2.0.0:
resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-2.0.0.tgz#4b560f649fc4e918dd0ab75cf4961e8bc882d82a"
integrity sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==
cookie@^0.7.2:
version "0.7.2"
resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.7.2.tgz#556369c472a2ba910f2979891b526b3436237ed7"
integrity sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==
cosmiconfig@^8.1.3:
version "8.3.6"
resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-8.3.6.tgz#060a2b871d66dba6c8538ea1118ba1ac16f5fae3"
@ -1675,6 +1692,16 @@ dequal@^2.0.0, dequal@^2.0.3:
resolved "https://registry.yarnpkg.com/dequal/-/dequal-2.0.3.tgz#2644214f1997d39ed0ee0ece72335490a7ac67be"
integrity sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==
destr@^2.0.3:
version "2.0.3"
resolved "https://registry.yarnpkg.com/destr/-/destr-2.0.3.tgz#7f9e97cb3d16dbdca7be52aca1644ce402cfe449"
integrity sha512-2N3BOUU4gYMpTP24s5rF5iP7BDr7uNTCs4ozw3kf/eKfvWSIu93GEBi5m427YoyJoeOzQ5smuu4nNAPGb8idSQ==
detect-europe-js@^0.1.1:
version "0.1.1"
resolved "https://registry.yarnpkg.com/detect-europe-js/-/detect-europe-js-0.1.1.tgz#b12eda6a32cf701c0c9a5a3895818a12ad790e12"
integrity sha512-+bUXDf+tI3L4dcEuRdAFa44Amx9aEaJzoZssx7Xis4H1bXWc5fAcOP850BOj0wJPRzOdovOuOVEvrg6T+GflZA==
detect-node-es@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/detect-node-es/-/detect-node-es-1.1.0.tgz#163acdf643330caa0b4cd7c21e7ee7755d6fa493"
@ -2973,6 +3000,11 @@ no-case@^3.0.4:
lower-case "^2.0.2"
tslib "^2.0.3"
node-fetch-native@^1.6.4:
version "1.6.4"
resolved "https://registry.yarnpkg.com/node-fetch-native/-/node-fetch-native-1.6.4.tgz#679fc8fd8111266d47d7e72c379f1bed9acff06e"
integrity sha512-IhOigYzAKHd244OC0JIMIUrjzctirCmPkaIfhDeGcEETWof5zKYUW7e7MYvChGWh/4CJeXEgsRyGzuF334rOOQ==
node-releases@^2.0.14:
version "2.0.14"
resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.14.tgz#2ffb053bceb8b2be8495ece1ab6ce600c4461b0b"
@ -2988,6 +3020,15 @@ object-assign@^4.1.1:
resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==
ofetch@^1.4.1:
version "1.4.1"
resolved "https://registry.yarnpkg.com/ofetch/-/ofetch-1.4.1.tgz#b6bf6b0d75ba616cef6519dd8b6385a8bae480ec"
integrity sha512-QZj2DfGplQAr2oj9KzceK9Hwz6Whxazmn85yYeVuS3u9XTMOGMRx0kO95MQ+vLsj/S/NwBDMMLU5hpxvI6Tklw==
dependencies:
destr "^2.0.3"
node-fetch-native "^1.6.4"
ufo "^1.5.4"
once@^1.3.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"
@ -3371,6 +3412,11 @@ 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"
@ -3393,6 +3439,15 @@ react-big-calendar@^1.11.3:
react-overlays "^5.2.1"
uncontrollable "^7.2.1"
react-cookie@^7.2.2:
version "7.2.2"
resolved "https://registry.yarnpkg.com/react-cookie/-/react-cookie-7.2.2.tgz#a7559e552ea9cca39a4b3686723a5acf504b8f84"
integrity sha512-e+hi6axHcw9VODoeVu8WyMWyoosa1pzpyjfvrLdF7CexfU+WSGZdDuRfHa4RJgTpfv3ZjdIpHE14HpYBieHFhg==
dependencies:
"@types/hoist-non-react-statics" "^3.3.5"
hoist-non-react-statics "^3.3.2"
universal-cookie "^7.0.0"
react-dom@^18.2.0:
version "18.2.0"
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.2.0.tgz#22aaf38708db2674ed9ada224ca4aa708d821e3d"
@ -3410,6 +3465,13 @@ react-dropzone@^14.2.3:
file-selector "^0.6.0"
prop-types "^15.8.1"
react-error-boundary@^4.1.2:
version "4.1.2"
resolved "https://registry.yarnpkg.com/react-error-boundary/-/react-error-boundary-4.1.2.tgz#bc750ad962edb8b135d6ae922c046051eb58f289"
integrity sha512-GQDxZ5Jd+Aq/qUxbCm1UtzmL/s++V7zKgE8yMktJiCQXCCFZnMZh9ng+6/Ne6PjNSXH0L9CjeOEREfRnq6Duag==
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"
@ -3939,11 +4001,23 @@ typescript@^5.0.2:
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.2.2.tgz#5ebb5e5a5b75f085f22bc3f8460fba308310fa78"
integrity sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==
ua-parser-js@^2.0.0-rc.1:
version "2.0.0-rc.1"
resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-2.0.0-rc.1.tgz#e1d3e71a144fd56abb81e195a9006c040c4ea646"
integrity sha512-H+kTJv7j04XbMuASIIrp7TwKj8rVHfGLR9dI36FAWneGHyQ6JZVwFukFgxSyM/OChmk7dxsV82R/tnEQxb1EXg==
dependencies:
detect-europe-js "^0.1.1"
uc.micro@^2.0.0, uc.micro@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/uc.micro/-/uc.micro-2.1.0.tgz#f8d3f7d0ec4c3dea35a7e3c8efa4cb8b45c9e7ee"
integrity sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==
ufo@^1.5.4:
version "1.5.4"
resolved "https://registry.yarnpkg.com/ufo/-/ufo-1.5.4.tgz#16d6949674ca0c9e0fbbae1fa20a71d7b1ded754"
integrity sha512-UsUk3byDzKd04EyoZ7U4DOlxQaD14JUKQl6/P7wiX4FNvUfm3XL246n9W5AmqwW5RSFJ27NAuM0iLscAOYUiGQ==
uncontrollable@^7.2.1:
version "7.2.1"
resolved "https://registry.yarnpkg.com/uncontrollable/-/uncontrollable-7.2.1.tgz#1fa70ba0c57a14d5f78905d533cf63916dc75738"
@ -4018,6 +4092,14 @@ unist-util-visit@^5.0.0:
unist-util-is "^6.0.0"
unist-util-visit-parents "^6.0.0"
universal-cookie@^7.0.0:
version "7.2.2"
resolved "https://registry.yarnpkg.com/universal-cookie/-/universal-cookie-7.2.2.tgz#93ae9ec55baab89b24300473543170bb8112773c"
integrity sha512-fMiOcS3TmzP2x5QV26pIH3mvhexLIT0HmPa3V7Q7knRfT9HG6kTwq02HZGLPw0sAOXrAmotElGRvTLCMbJsvxQ==
dependencies:
"@types/cookie" "^0.6.0"
cookie "^0.7.2"
update-browserslist-db@^1.0.13:
version "1.0.15"
resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.0.15.tgz#60ed9f8cba4a728b7ecf7356f641a31e3a691d97"