feat(analyticsDashboard): finished sessions view dashboard
Build and Push Docker image / build-and-push (push) Failing after 3m41s
Details
Build and Push Docker image / build-and-push (push) Failing after 3m41s
Details
This commit is contained in:
parent
d605d17d06
commit
8a7dc4869b
|
@ -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>
|
||||||
}
|
}
|
|
@ -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>
|
||||||
}
|
}
|
|
@ -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'}`;
|
||||||
}
|
}
|
|
@ -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
|
||||||
|
|
|
@ -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 */
|
||||||
|
}
|
|
@ -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>
|
||||||
|
)
|
||||||
|
}
|
|
@ -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>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
|
@ -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 && <>
|
||||||
{" • "}
|
{" • "}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
}
|
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue