feat(analyticsDashboard): finished sessions view dashboard
Build and Push Docker image / build-and-push (push) Failing after 3m41s Details

This commit is contained in:
Valentin Kolb 2024-11-08 18:35:55 +01:00
parent d605d17d06
commit 8a7dc4869b
14 changed files with 273 additions and 219 deletions

View File

@ -1,4 +1,4 @@
import {Alert, Tooltip} from "@mantine/core"; import {Alert, AlertProps, Tooltip} from "@mantine/core";
import {IconBug} from "@tabler/icons-react"; import {IconBug} from "@tabler/icons-react";
import {useLocalStorage} from "@mantine/hooks"; import {useLocalStorage} from "@mantine/hooks";
import {ReactNode} from "react"; import {ReactNode} from "react";
@ -34,8 +34,9 @@ export const useShowDebug = (): {
* Shows a help dialog if the user has not disabled it * Shows a help dialog if the user has not disabled it
* @see useShowHelp * @see useShowHelp
* @param children - the content of the help dialog * @param children - the content of the help dialog
* @param props - the props of the alert component
*/ */
export default function ShowDebug({children}: { children: ReactNode }) { export default function ShowDebug({children, ...props}: { children: ReactNode } & Omit<AlertProps, "children">) {
const {showDebug} = useShowDebug() const {showDebug} = useShowDebug()
@ -54,7 +55,9 @@ export default function ShowDebug({children}: { children: ReactNode }) {
> >
<IconBug/> <IconBug/>
</Tooltip> </Tooltip>
}> }
{...props}
>
{children} {children}
</Alert> </Alert>
} }

View File

@ -1,12 +1,22 @@
import classes from "./index.module.css"; import classes from "./index.module.css";
import {ReactNode} from "react"; import {ReactNode} from "react";
import {ThemeIcon, ThemeIconProps} from "@mantine/core";
export default function TextWithIcon({icon, children}: { export default function TextWithIcon({icon, children, className, themeIconProps}: {
icon: ReactNode, icon: ReactNode,
children: ReactNode children: ReactNode,
className?: string,
themeIconProps?: ThemeIconProps
}) { }) {
return <div className={classes.container}> return <div className={`${classes.container} ${className}`}>
{icon} <ThemeIcon
variant={"transparent"}
size={"xs"}
color={"gray"}
{...themeIconProps}
>
{icon}
</ThemeIcon>
<span>{children}</span> <span>{children}</span>
</div> </div>
} }

View File

@ -134,12 +134,18 @@ export function formatDateForExcel(date: string | Date | Dayjs): string {
* Example: "3 Wo, 1 Tag, 2 Std, 53 Min" * Example: "3 Wo, 1 Tag, 2 Std, 53 Min"
* @param date1 * @param date1
* @param date2 * @param date2
* @param includeSeconds - If true, include seconds in the output.
*/ */
export function formatDuration(date1: string | Date | Dayjs, date2: string | Date | Dayjs) { export function formatDuration(date1: string | Date | Dayjs, date2: string | Date | Dayjs, includeSeconds = false) {
// ignore seconds and milliseconds if (!includeSeconds) {
date1 = dayjs(date1).startOf('minute'); // ignore seconds and milliseconds
date2 = dayjs(date2).startOf('minute'); date1 = dayjs(date1).startOf('minute');
date2 = dayjs(date2).startOf('minute');
} else {
date1 = dayjs(date1);
date2 = dayjs(date2);
}
// get the difference in milliseconds // get the difference in milliseconds
const diff = dayjs(date2).diff(dayjs(date1)); const diff = dayjs(date2).diff(dayjs(date1));
@ -150,6 +156,7 @@ export function formatDuration(date1: string | Date | Dayjs, date2: string | Dat
const days = durationObj.days(); const days = durationObj.days();
const hours = durationObj.hours(); const hours = durationObj.hours();
const minutes = durationObj.minutes(); const minutes = durationObj.minutes();
const seconds = durationObj.seconds();
// create a string array with the duration parts // create a string array with the duration parts
const result: string[] = []; const result: string[] = [];
@ -157,6 +164,8 @@ export function formatDuration(date1: string | Date | Dayjs, date2: string | Dat
if (days > 0) result.push(`${days} Tag${days > 1 ? 'e' : ''}`); if (days > 0) result.push(`${days} Tag${days > 1 ? 'e' : ''}`);
if (hours > 0) result.push(`${hours} Std`); if (hours > 0) result.push(`${hours} Std`);
if (minutes > 0) result.push(`${minutes} Min`); if (minutes > 0) result.push(`${minutes} Min`);
if (includeSeconds && seconds > 0) result.push(`${seconds} Sek`);
return result.join(', '); // join the parts with a comma and space
return result.length ? result.join(', ') : `0 ${includeSeconds ? 'Sek' : 'Min'}`;
} }

View File

@ -17,7 +17,6 @@ export type AnalyticsSessionModel = {
preferred_language?: string preferred_language?: string
expand?: { expand?: {
visitor?: AnalyticsVisitorsModel, visitor?: AnalyticsVisitorsModel,
analyticsErrors_via_session?: AnalyticsErrorModel[],
analyticsPageViews_via_session?: AnalyticsPageViewModel[] analyticsPageViews_via_session?: AnalyticsPageViewModel[]
} }
} & RecordModel } & RecordModel
@ -38,18 +37,6 @@ export type AnalyticsPageViewModel = {
} }
} & RecordModel } & RecordModel
export type AnalyticsErrorModel = {
session: string
error_message?: string
error_type?: string
stack_trace?: string
path?: string
expand?: {
session?: AnalyticsSessionModel
}
} & RecordModel
export type AnalyticsIpApiResult = { export type AnalyticsIpApiResult = {
ip: string ip: string
country_code: string country_code: string

View File

@ -0,0 +1,48 @@
.mainGrid {
display: grid;
grid-template-columns: auto auto auto auto auto auto;
background-color: var(--mantine-color-body);
border: var(--border);
border-radius: var(--border-radius);
}
.subGrid {
display: grid;
grid-template-columns: subgrid;
grid-column: span 6;
gap: var(--gap);
font-size: var(--mantine-font-size-sm);
padding: var(--padding);
border-bottom: var(--border);
&:last-of-type {
border-bottom: none;
}
@media (max-width: $mantine-breakpoint-sm) {
font-size: var(--mantine-font-size-sm);
display: flex;
flex-direction: column;
gap: var(--gap);
}
}
.gridCell {
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) {
}
}
.debugContainer {
grid-column: span 6; /* Spans all columns of the main grid */
}

View File

@ -0,0 +1,104 @@
import {AnalyticsPageViewModel, AnalyticsSessionModel} from "@/models/AnalyticsTypes.ts";
import classes from "./index.module.css";
import {formatDuration, pprintDateTime, pprintTime} from "@/lib/datetime.ts";
import ShowDebug from "@/components/ShowDebug.tsx";
import {Code} from "@mantine/core";
import TextWithIcon from "@/components/layout/TextWithIcon";
import {IconBug, IconClick, IconClock, IconHourglass, IconInfoCircle, IconStack3Filled} from "@tabler/icons-react";
export default function AnalyticsSessionDetail({session}: { session: AnalyticsSessionModel }) {
return <div className={classes.mainGrid}>
<ShowDebug className={classes.debugContainer}>
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>
{
session.expand?.analyticsPageViews_via_session?.sort((a, b) => {
return a.created < b.created ? -1 : 1
}).map((pageView, index, array) => {
return (
<PageViewRow
key={index}
pageView={pageView}
nextPageView={array[index + 1]}
/>
)
})
}
</div>
}
function PageViewRow({pageView, nextPageView}: {
pageView: AnalyticsPageViewModel,
nextPageView?: AnalyticsPageViewModel
}) {
return (
<div className={classes.subGrid}>
<TextWithIcon
className={classes.gridCell}
icon={<IconClock/>}
>
{pprintTime(pageView.created)}
</TextWithIcon>
<TextWithIcon
className={classes.gridCell}
icon={<IconHourglass/>}
>
{nextPageView ?
formatDuration(pageView.created, nextPageView.created, true)
:
"End"
}
</TextWithIcon>
<TextWithIcon
className={classes.gridCell}
icon={<IconClick/>}
>
{pageView.path}
</TextWithIcon>
{
pageView.error && <>
<TextWithIcon
className={classes.gridCell}
themeIconProps={{color: "red"}}
icon={<IconBug/>}
>
{pageView.error.error_type}
</TextWithIcon>
<TextWithIcon
className={classes.gridCell}
themeIconProps={{color: "red"}}
icon={<IconInfoCircle/>}
>
{pageView.error.error_message}
</TextWithIcon>
<TextWithIcon
className={classes.gridCell}
themeIconProps={{color: "red"}}
icon={<IconStack3Filled/>}
>
<Code>
{pageView.error.stack_trace}
</Code>
</TextWithIcon>
</>
}
</div>
)
}

View File

@ -1,5 +1,5 @@
import classes from "./index.module.css"; import classes from "./index.module.css";
import {ActionIcon, Code, Collapse, ThemeIcon, Tooltip} from "@mantine/core"; import {ActionIcon, Collapse, Tooltip} from "@mantine/core";
import { import {
IconBrandAndroid, IconBrandAndroid,
IconBrandApple, IconBrandApple,
@ -31,9 +31,9 @@ import TextWithIcon from "@/components/layout/TextWithIcon";
import {useDisclosure} from "@mantine/hooks"; import {useDisclosure} from "@mantine/hooks";
import ShowDebug from "@/components/ShowDebug.tsx";
import {pprintDateTime} from "@/lib/datetime.ts"; import {pprintDateTime} from "@/lib/datetime.ts";
import {AnalyticsSessionModel} from "@/models/AnalyticsTypes.ts"; import {AnalyticsSessionModel} from "@/models/AnalyticsTypes.ts";
import AnalyticsSessionDetail from "@/pages/admin/AnalyticsDashboard/AnalyticsSessions/AnalyticsSessionDetail";
export const DeviceTypeIcon = ({deviceType}: { deviceType: string }) => { export const DeviceTypeIcon = ({deviceType}: { deviceType: string }) => {
if (deviceType === "mobile") { if (deviceType === "mobile") {
@ -102,75 +102,54 @@ export default function AnalyticsSessionRow({session}: {
return <> return <>
<div className={classes.subgrid} data-error={!!errorCount}> <div className={classes.subgrid} data-error={!!errorCount}>
<div className={classes.child}> <TextWithIcon
<TextWithIcon icon={ className={classes.child}
<ThemeIcon variant={"transparent"} size={"xs"} color={"gray"}> icon={<IconCalendarClock/>}
<IconCalendarClock/> >
</ThemeIcon> {pprintDateTime(session.created)}
}> </TextWithIcon>
{pprintDateTime(session.created)}
</TextWithIcon>
</div>
<div className={classes.child}> <TextWithIcon
<TextWithIcon icon={ className={classes.child}
<ThemeIcon variant={"transparent"} size={"xs"} color={"gray"}> icon={<BrowserIcon browser={session.browser_name ?? ""}/>}
<BrowserIcon browser={session.browser_name ?? ""}/> >
</ThemeIcon> {session.browser_name} {session.browser_version}
}> </TextWithIcon>
{session.browser_name} {session.browser_version}
</TextWithIcon>
</div>
<div className={classes.child}> <TextWithIcon
<TextWithIcon icon={ className={classes.child}
<ThemeIcon variant={"transparent"} size={"xs"} color={"gray"}> icon={<OsIcon os={session.operating_system ?? ""}/>}
<OsIcon os={session.operating_system ?? ""}/> >
</ThemeIcon> {session.operating_system} {session.operating_system_version}
}> </TextWithIcon>
{session.operating_system} {session.operating_system_version}
</TextWithIcon>
</div>
<div className={classes.child}> <TextWithIcon
<TextWithIcon icon={ className={classes.child}
<ThemeIcon variant={"transparent"} size={"xs"} color={"gray"}> icon={<IconGlobe/>}
<IconGlobe/> >
</ThemeIcon> {session.geo_country_code || "--"}
}> </TextWithIcon>
{session.geo_country_code || "--"}
</TextWithIcon>
</div>
<div className={classes.child}> <TextWithIcon
<TextWithIcon icon={ className={classes.child}
<ThemeIcon variant={"transparent"} size={"xs"} color={"gray"}> icon={<IconLanguage/>}
<IconLanguage/> >
</ThemeIcon> {session.preferred_language || "--"}
}> </TextWithIcon>
{session.preferred_language || "--"}
</TextWithIcon>
</div>
<div className={classes.child}> <TextWithIcon
<TextWithIcon icon={ className={classes.child}
<ThemeIcon variant={"transparent"} size={"xs"} color={"gray"}> icon={<IconClick/>}
<IconClick/> >
</ThemeIcon> {pageViewCount ?? '--'}
}> </TextWithIcon>
{pageViewCount ?? '--'}
</TextWithIcon>
</div>
<div className={classes.child}> <TextWithIcon
<TextWithIcon icon={ className={classes.child}
<ThemeIcon variant={"transparent"} size={"xs"} color={errorCount ? "red" : "gray"}> icon={<IconBug/>}
<IconBug/> >
</ThemeIcon> {errorCount ?? '--'}
}> </TextWithIcon>
{errorCount ?? '--'}
</TextWithIcon>
</div>
<div className={`${classes.child} ${classes.alignEnd}`}> <div className={`${classes.child} ${classes.alignEnd}`}>
<Tooltip label={expanded ? "Details verbergen" : "Details anzeigen"} withArrow> <Tooltip label={expanded ? "Details verbergen" : "Details anzeigen"} withArrow>
@ -182,21 +161,7 @@ export default function AnalyticsSessionRow({session}: {
</div> </div>
<Collapse in={expanded} className={classes.detailsContainer}> <Collapse in={expanded} className={classes.detailsContainer}>
<ShowDebug> <AnalyticsSessionDetail session={session}/>
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> </Collapse>
</> </>
} }

View File

@ -52,7 +52,7 @@ export default function AnalyticsSessions({lastNDays}: { lastNDays?: number }) {
filter.push('analyticsPageViews_via_session.session=id&&analyticsPageViews_via_session.error?!=null') filter.push('analyticsPageViews_via_session.session=id&&analyticsPageViews_via_session.error?!=null')
} }
return await pb.collection("analyticsSessions").getList(pageParam, 500, { return await pb.collection("analyticsSessions").getList(pageParam, 100, {
filter: filter.join("&&"), filter: filter.join("&&"),
expand: 'analyticsErrors_via_session,analyticsPageViews_via_session', expand: 'analyticsErrors_via_session,analyticsPageViews_via_session',
sort: '-created' sort: '-created'
@ -67,6 +67,7 @@ export default function AnalyticsSessions({lastNDays}: { lastNDays?: number }) {
const sessionAnalytics = useMemo(() => { const sessionAnalytics = useMemo(() => {
return countByAllKeys(sessions) return countByAllKeys(sessions)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [query.data]) }, [query.data])
return <> return <>
@ -102,7 +103,8 @@ export default function AnalyticsSessions({lastNDays}: { lastNDays?: number }) {
<div className={"section-transparent center"}> <div className={"section-transparent center"}>
<Text size={"xs"} c={"dimmed"}> <Text size={"xs"} c={"dimmed"}>
Datenvisualisierung der letzten {sessions.length} Sessions ({query.data?.pages[0].totalItems} insgesamt) Datenvisualisierung der neuesten {sessions.length} Sessions
({query.data?.pages[0].totalItems} insgesamt)
{query.hasNextPage && <> {query.hasNextPage && <>
{" • "} {" • "}

View File

@ -100,16 +100,16 @@ export default function EventListRouter({event}: { event: EventModel }) {
</ShowDebug> </ShowDebug>
<Alert color={list.open ? "green" : "red"}> <Alert color={list.open ? "green" : "red"}>
<TextWithIcon icon={list.open ? <IconLockOpen size={16}/> : <IconLock size={16}/>}> <TextWithIcon icon={list.open ? <IconLockOpen/> : <IconLock/>}>
Liste ist für Anmeldungen <b>{list.open ? "geöffnet" : "geschlossen"}</b> Liste ist für Anmeldungen <b>{list.open ? "geöffnet" : "geschlossen"}</b>
</TextWithIcon> </TextWithIcon>
<br/> <br/>
<TextWithIcon icon={<IconUserCog size={16}/>}> <TextWithIcon icon={<IconUserCog/>}>
Anmeldung für Personen mit {list.onlyStuVeAccounts ? Anmeldung für Personen mit {list.onlyStuVeAccounts ?
<><b>StuVe</b> Account</> : <>StuVe <b>und</b> Gast Account</>} <><b>StuVe</b> Account</> : <>StuVe <b>und</b> Gast Account</>}
</TextWithIcon> </TextWithIcon>
<br/> <br/>
<TextWithIcon icon={<IconClockCog size={16}/>}> <TextWithIcon icon={<IconClockCog/>}>
Überlappende Einträge sind {!list.allowOverlappingEntries && <b>nicht</b>} erlaubt Überlappende Einträge sind {!list.allowOverlappingEntries && <b>nicht</b>} erlaubt
</TextWithIcon> </TextWithIcon>
</Alert> </Alert>

View File

@ -1,7 +1,7 @@
import {areDatesSame, formatDuration} from "@/lib/datetime.ts"; import {areDatesSame, formatDuration} from "@/lib/datetime.ts";
import {Group, ThemeIcon} from "@mantine/core"; import {Group} from "@mantine/core";
import {IconCalendar, IconCalendarClock, IconClock, IconHourglass} from "@tabler/icons-react"; import {IconCalendar, IconCalendarClock, IconClock, IconHourglass} from "@tabler/icons-react";
import dayjs from "dayjs"; import dayjs, {Dayjs} from "dayjs";
import TextWithIcon from "@/components/layout/TextWithIcon"; import TextWithIcon from "@/components/layout/TextWithIcon";
/** /**
@ -11,68 +11,47 @@ import TextWithIcon from "@/components/layout/TextWithIcon";
* @param start - start date * @param start - start date
* @param end - end date * @param end - end date
*/ */
export const RenderDateRange = ({start, end}: { start: Date, end: Date }) => { export const RenderDateRange = ({start, end}: { start: string | Date | Dayjs, end: string | Date | Dayjs }) => {
const duration = formatDuration(start, end) const duration = formatDuration(start, end)
start = dayjs(start)
end = dayjs(end)
// case for same date // case for same date
if (areDatesSame(start, end)) { if (areDatesSame(start, end)) {
return <Group gap={"xs"}> return <Group gap={"xs"}>
<TextWithIcon icon={ <TextWithIcon icon={<IconClock/>}>
<ThemeIcon variant={"transparent"} size={"xs"} color={"gray"}>
<IconClock/>
</ThemeIcon>
}>
{dayjs(start).format("HH:mm")} {dayjs(start).format("HH:mm")}
{"-"} {"-"}
{dayjs(end).format("HH:mm")} {dayjs(end).format("HH:mm")}
</TextWithIcon> </TextWithIcon>
<TextWithIcon icon={ <TextWithIcon icon={<IconCalendar/>}>
<ThemeIcon variant={"transparent"} size={"xs"} color={"gray"}>
<IconCalendar/>
</ThemeIcon>
}>
{dayjs(start).format("DD.MM.YY")} {dayjs(start).format("DD.MM.YY")}
</TextWithIcon> </TextWithIcon>
<TextWithIcon icon={ <TextWithIcon icon={<IconHourglass/>}>
<ThemeIcon variant={"transparent"} size={"xs"} color={"gray"}>
<IconHourglass/>
</ThemeIcon>
}>
{duration} {duration}
</TextWithIcon> </TextWithIcon>
</Group> </Group>
} }
// case both dates start at 00:00:00 // case both dates start at 00:00:00
if (start.getHours() === 0 && start.getMinutes() === 0 && start.getSeconds() === 0 && if (start.hour() === 0 && start.minute() === 0 && start.second() === 0 &&
end.getHours() === 0 && end.getMinutes() === 0 && end.getSeconds() === 0) { end.hour() === 0 && end.minute() === 0 && end.second() === 0) {
return <Group gap={'xs'}> return <Group gap={'xs'}>
<TextWithIcon icon={ <TextWithIcon icon={<IconCalendar/>}>
<ThemeIcon variant={"transparent"} size={"xs"} color={"gray"}>
<IconCalendar/>
</ThemeIcon>
}>
{dayjs(start).format("DD.MM.YY")} {dayjs(start).format("DD.MM.YY")}
</TextWithIcon> </TextWithIcon>
{"bis"} {"bis"}
<TextWithIcon icon={ <TextWithIcon icon={<IconCalendar/>}>
<ThemeIcon variant={"transparent"} size={"xs"} color={"gray"}>
<IconCalendar/>
</ThemeIcon>
}>
{dayjs(end).format("DD.MM.YY")} {dayjs(end).format("DD.MM.YY")}
</TextWithIcon> </TextWithIcon>
<TextWithIcon icon={ <TextWithIcon icon={<IconHourglass/>}>
<ThemeIcon variant={"transparent"} size={"xs"} color={"gray"}>
<IconHourglass/>
</ThemeIcon>
}>
{duration} {duration}
</TextWithIcon> </TextWithIcon>
</Group> </Group>
@ -80,29 +59,17 @@ export const RenderDateRange = ({start, end}: { start: Date, end: Date }) => {
// case different dates and times // case different dates and times
return <Group gap={'xs'}> return <Group gap={'xs'}>
<TextWithIcon icon={ <TextWithIcon icon={<IconCalendarClock/>}>
<ThemeIcon variant={"transparent"} size={"xs"} color={"gray"}>
<IconCalendarClock/>
</ThemeIcon>
}>
{dayjs(start).format("HH:mm DD.MM.YY")} {dayjs(start).format("HH:mm DD.MM.YY")}
</TextWithIcon> </TextWithIcon>
{"bis"} {"bis"}
<TextWithIcon icon={ <TextWithIcon icon={<IconCalendarClock/>}>
<ThemeIcon variant={"transparent"} size={"xs"} color={"gray"}>
<IconCalendarClock/>
</ThemeIcon>
}>
{dayjs(end).format("HH:mm DD.MM.YY")} {dayjs(end).format("HH:mm DD.MM.YY")}
</TextWithIcon> </TextWithIcon>
<TextWithIcon icon={ <TextWithIcon icon={<IconHourglass/>}>
<ThemeIcon variant={"transparent"} size={"xs"} color={"gray"}>
<IconHourglass/>
</ThemeIcon>
}>
{duration} {duration}
</TextWithIcon> </TextWithIcon>
</Group> </Group>

View File

@ -1,6 +1,6 @@
import {EventListSlotEntriesWithUserModel, EventModel} from "@/models/EventTypes.ts"; import {EventListSlotEntriesWithUserModel, EventModel} from "@/models/EventTypes.ts";
import classes from "./EventEntries.module.css"; import classes from "./EventEntries.module.css";
import {ActionIcon, Code, Collapse, ThemeIcon, Tooltip} from "@mantine/core"; import {ActionIcon, Code, Collapse, Tooltip} from "@mantine/core";
import {IconEye, IconEyeOff, IconList, IconUser} from "@tabler/icons-react"; import {IconEye, IconEyeOff, IconList, IconUser} from "@tabler/icons-react";
import TextWithIcon from "@/components/layout/TextWithIcon"; import TextWithIcon from "@/components/layout/TextWithIcon";
@ -26,22 +26,15 @@ function EventEntry({entry, refetch, event}: {
return <> return <>
<div className={classes.subgrid}> <div className={classes.subgrid}>
<div className={classes.child}> <TextWithIcon
<TextWithIcon icon={ className={classes.child}
<ThemeIcon variant={"transparent"} size={"xs"} color={"gray"}> icon={<IconUser/>}
<IconUser/> >
</ThemeIcon> <RenderUserName user={entry.expand?.user}/>
}> </TextWithIcon>
<RenderUserName user={entry.expand?.user}/>
</TextWithIcon>
</div>
<Link to={`/events/e/${entry.event}/lists/overview/${entry.eventList}`} className={classes.child}> <Link to={`/events/e/${entry.event}/lists/overview/${entry.eventList}`} className={classes.child}>
<TextWithIcon icon={ <TextWithIcon icon={<IconList/>}>
<ThemeIcon variant={"transparent"} size={"xs"} color={"gray"}>
<IconList/>
</ThemeIcon>
}>
{entry.listName} {entry.listName}
</TextWithIcon> </TextWithIcon>
</Link> </Link>

View File

@ -1,15 +1,5 @@
import {EventListSlotEntriesWithUserModel} from "@/models/EventTypes.ts"; import {EventListSlotEntriesWithUserModel} from "@/models/EventTypes.ts";
import { import {ActionIcon, Collapse, Group, Modal, Text, Tooltip, useMantineColorScheme, useMantineTheme} from "@mantine/core";
ActionIcon,
Collapse,
Group,
Modal,
Text,
ThemeIcon,
Tooltip,
useMantineColorScheme,
useMantineTheme
} from "@mantine/core";
import { import {
IconChevronDown, IconChevronDown,
IconChevronRight, IconChevronRight,
@ -107,19 +97,11 @@ export default function UserEntryRow({entry, refetch}: {
</Tooltip> </Tooltip>
<div className={classes.entryInfo}> <div className={classes.entryInfo}>
<TextWithIcon icon={ <TextWithIcon icon={<IconConfetti/>}>
<ThemeIcon variant={"transparent"} size={"xs"} color={"gray"}>
<IconConfetti/>
</ThemeIcon>
}>
{entry.eventName} {entry.eventName}
</TextWithIcon> </TextWithIcon>
<TextWithIcon icon={ <TextWithIcon icon={<IconList/>}>
<ThemeIcon variant={"transparent"} size={"xs"} color={"gray"}>
<IconList/>
</ThemeIcon>
}>
<Link to={`/events/s/${entry.event}?lists=${entry.eventList}`}> <Link to={`/events/s/${entry.event}?lists=${entry.eventList}`}>
{entry.listName} {entry.listName}
</Link> </Link>

View File

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

View File

@ -3,6 +3,8 @@ import {Link} from "react-router-dom";
import CollectedAnalyticsData from "@/pages/util/whatWeKnowAboutYou/CollectedAnalyticsData.tsx"; import CollectedAnalyticsData from "@/pages/util/whatWeKnowAboutYou/CollectedAnalyticsData.tsx";
import CollectedUserData from "@/pages/util/whatWeKnowAboutYou/CollectedUserData.tsx"; import CollectedUserData from "@/pages/util/whatWeKnowAboutYou/CollectedUserData.tsx";
import SVG from "@/illustrations/chart-circle.svg?react"
import classes from "./index.module.css";
export default function WhatWeKnowAboutYou() { export default function WhatWeKnowAboutYou() {
@ -10,6 +12,8 @@ export default function WhatWeKnowAboutYou() {
<div className={"section-transparent stack"}> <div className={"section-transparent stack"}>
<Center mt={"xl"} className={"stack"}> <Center mt={"xl"} className={"stack"}>
<SVG className={classes.svg} aria-label={"data analysis image"}/>
<Title order={1} c={"blue"}>Welche Daten werden gesammelt?</Title> <Title order={1} c={"blue"}>Welche Daten werden gesammelt?</Title>
<Anchor <Anchor