feat(statusEditor): added StatusEditor.tsx with mobile first design
Build and Push Docker image / build-and-push (push) Successful in 5m36s
Details
Build and Push Docker image / build-and-push (push) Successful in 5m36s
Details
This commit is contained in:
parent
4ea290ddf4
commit
7cef873acd
|
@ -2,9 +2,11 @@
|
|||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"/>
|
||||
<meta name="theme-color" content="#339af0">
|
||||
<link rel="icon" href="/stuve-logo.svg"/>
|
||||
<title>StuVe IT</title>
|
||||
<meta name="description" content="IT-Tools der StuVe Uni Ulm (K.d.ö.R)">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"dev": "vite --host",
|
||||
"build": "export NODE_ENV=production && tsc && vite build",
|
||||
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
|
||||
"preview": "vite preview"
|
||||
|
@ -35,6 +35,7 @@
|
|||
"@tiptap/starter-kit": "^2.3.0",
|
||||
"@tiptap/suggestion": "^2.4.0",
|
||||
"@types/react-big-calendar": "^1.8.9",
|
||||
"@yudiel/react-qr-scanner": "^2.0.2",
|
||||
"clsx": "^2.1.1",
|
||||
"dayjs": "^1.11.10",
|
||||
"jwt-decode": "^3.1.2",
|
||||
|
|
|
@ -1,22 +1,39 @@
|
|||
import {useChangeEmail, useForgotPassword, useUserMenu} from "@/components/users/modals/hooks.ts";
|
||||
import {usePB} from "@/lib/pocketbase.tsx";
|
||||
import {PocketBaseErrorAlert, usePB} from "@/lib/pocketbase.tsx";
|
||||
import {useShowHelp} from "@/components/ShowHelp.tsx";
|
||||
import ShowDebug, {useShowDebug} from "@/components/ShowDebug.tsx";
|
||||
import {ActionIcon, Code, Divider, Group, Modal, Switch, Text, ThemeIcon, Title, Tooltip,} from "@mantine/core";
|
||||
import {
|
||||
ActionIcon,
|
||||
Alert,
|
||||
Code,
|
||||
Divider,
|
||||
Group,
|
||||
Modal,
|
||||
Switch,
|
||||
Text,
|
||||
TextInput,
|
||||
ThemeIcon,
|
||||
Title,
|
||||
Tooltip,
|
||||
} from "@mantine/core";
|
||||
import classes from "@/components/layout/nav/index.module.css";
|
||||
import {
|
||||
IconAt,
|
||||
IconCalendar,
|
||||
IconDeviceFloppy,
|
||||
IconLogout,
|
||||
IconMailCog,
|
||||
IconPassword,
|
||||
IconRefresh,
|
||||
IconServer,
|
||||
IconServerOff,
|
||||
IconUser
|
||||
IconServerOff
|
||||
} from "@tabler/icons-react";
|
||||
import LdapGroupsDisplay from "@/components/users/LdapGroupsDisplay.tsx";
|
||||
import ColorSchemeSwitch from "@/components/input/ColorSchemeSwitch.tsx";
|
||||
import {getUserName} from "@/components/users/modals/util.tsx";
|
||||
import {isNotEmpty, useForm} from "@mantine/form";
|
||||
import {useMutation} from "@tanstack/react-query";
|
||||
import {showSuccessNotification} from "@/components/util.tsx";
|
||||
|
||||
export default function UserMenuModal() {
|
||||
const {value, handler} = useUserMenu()
|
||||
|
@ -25,22 +42,58 @@ export default function UserMenuModal() {
|
|||
|
||||
const {handler: changeEmailHandler} = useChangeEmail()
|
||||
|
||||
const {logout, apiIsHealthy, user, refreshUser} = usePB()
|
||||
const {logout, apiIsHealthy, pb, user, refreshUser} = usePB()
|
||||
|
||||
const {showHelp, toggleShowHelp} = useShowHelp()
|
||||
|
||||
const {showDebug, toggleShowDebug} = useShowDebug()
|
||||
|
||||
const formValues = useForm({
|
||||
initialValues: {
|
||||
sn: user?.sn ?? "",
|
||||
givenName: user?.givenName ?? "",
|
||||
},
|
||||
validate: {
|
||||
sn: isNotEmpty('Bitte gebe deinen Nachnamen ein'),
|
||||
givenName: isNotEmpty('Bitte gebe deinen Vornamen ein'),
|
||||
}
|
||||
})
|
||||
|
||||
const mutation = useMutation({
|
||||
mutationFn: async () => {
|
||||
if (formValues.validate().hasErrors || !user) {
|
||||
throw new Error("Validation failed")
|
||||
}
|
||||
|
||||
return await pb.collection("users").update(user!.id, {
|
||||
...formValues.values
|
||||
})
|
||||
},
|
||||
onSuccess: () => {
|
||||
showSuccessNotification("Dein Name wurde erfolgreich gespeichert")
|
||||
refreshUser()
|
||||
formValues.reset()
|
||||
}
|
||||
})
|
||||
|
||||
// modal must be open if the user has no givenName or sn
|
||||
const userHasNoName = (!user?.sn || !user?.givenName) && !!user
|
||||
|
||||
return <>
|
||||
<Modal opened={value && !!user} onClose={handler.close} withCloseButton={false} size={"md"}>
|
||||
<Modal opened={(value && !!user) || userHasNoName} onClose={handler.close} withCloseButton={false} size={"md"}>
|
||||
<div className={classes.stack}>
|
||||
<Title order={3}>Hallo {user?.username}</Title>
|
||||
|
||||
{userHasNoName && (
|
||||
<Alert color={"orange"} title={"Gebe deinen Vor- und Nachnamen ein"}>
|
||||
Bitte vervollständige deinen Account, um alle Funktionen nutzen zu können.
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<Title order={3}>Hallo {getUserName(user)}</Title>
|
||||
|
||||
<ShowDebug>
|
||||
Datenbank ID: <Code>{user?.id}</Code>
|
||||
|
||||
User ID: <Code>{user?.id}</Code>
|
||||
<br/>
|
||||
|
||||
REALM: <Code>{user?.REALM}</Code>
|
||||
|
||||
{user?.objectGUID && <>
|
||||
|
@ -50,43 +103,25 @@ export default function UserMenuModal() {
|
|||
</ShowDebug>
|
||||
|
||||
<div className={classes.row}>
|
||||
<ThemeIcon
|
||||
variant={"transparent"}
|
||||
size={"xl"}
|
||||
>
|
||||
<IconUser/>
|
||||
</ThemeIcon>
|
||||
|
||||
|
||||
<Text>
|
||||
{user?.REALM === "LDAP" ? "StuVe IT Account" : "Gast Account"}
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
<div className={classes.row}>
|
||||
<ThemeIcon
|
||||
variant={"transparent"}
|
||||
size={"xl"}
|
||||
>
|
||||
<ThemeIcon variant={"transparent"}>
|
||||
<IconAt/>
|
||||
</ThemeIcon>
|
||||
|
||||
<Tooltip label={`Dein Email ist ${user?.emailVisibility ? "sichtbar" : "nicht sichtbar"}`}>
|
||||
<Text>
|
||||
<Tooltip
|
||||
label={`Dein Email ist ${user?.emailVisibility ? "für niemanden sichtbar" : "für eingeloggte Personen sichtbar"}`}
|
||||
>
|
||||
<Text size={"sm"}>
|
||||
{user?.email}
|
||||
</Text>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
<div className={classes.row}>
|
||||
<ThemeIcon
|
||||
variant={"transparent"}
|
||||
size={"xl"}
|
||||
>
|
||||
<ThemeIcon variant={"transparent"}>
|
||||
<IconCalendar/>
|
||||
</ThemeIcon>
|
||||
|
||||
<Text>
|
||||
<Text size={"sm"}>
|
||||
{user?.accountExpires ? (
|
||||
|
||||
new Date(user?.accountExpires).getTime() > Date.now() ? (
|
||||
|
@ -105,7 +140,6 @@ export default function UserMenuModal() {
|
|||
<ThemeIcon
|
||||
variant={"transparent"}
|
||||
color={"green"}
|
||||
size={"xl"}
|
||||
>
|
||||
<IconServer/>
|
||||
</ThemeIcon>
|
||||
|
@ -113,13 +147,12 @@ export default function UserMenuModal() {
|
|||
<ThemeIcon
|
||||
variant={"transparent"}
|
||||
color={"red"}
|
||||
size={"xl"}
|
||||
>
|
||||
<IconServerOff/>
|
||||
</ThemeIcon>
|
||||
)}
|
||||
|
||||
<Text>
|
||||
<Text size={"sm"}>
|
||||
{apiIsHealthy ? "Das Backend ist erreichbar" : "Das Backend ist nicht erreichbar"}
|
||||
</Text>
|
||||
</div>
|
||||
|
@ -147,6 +180,7 @@ export default function UserMenuModal() {
|
|||
<Switch
|
||||
checked={showDebug}
|
||||
onChange={toggleShowDebug}
|
||||
color={"orange"}
|
||||
label={"Debug Modus aktivieren"}
|
||||
/>
|
||||
|
||||
|
@ -159,9 +193,47 @@ export default function UserMenuModal() {
|
|||
label={"Account"}
|
||||
/>
|
||||
|
||||
<Group justify={"center"}>
|
||||
{userHasNoName &&
|
||||
<Alert> Bitte vervollständige deinen Account, um alle Funktionen nutzen zu können. </Alert>}
|
||||
|
||||
{
|
||||
user?.REALM === "GUEST" && <>
|
||||
<PocketBaseErrorAlert error={mutation.error}/>
|
||||
|
||||
<TextInput
|
||||
label={"Vorname"}
|
||||
description={"Dein Vorname, alle Personen mit Account können diesen sehen"}
|
||||
placeholder={"Vorname"}
|
||||
{...formValues.getInputProps("givenName")}
|
||||
required
|
||||
/>
|
||||
|
||||
<TextInput
|
||||
label={"Nachname"}
|
||||
description={"Dein Nachname, alle Personen mit Account können diesen sehen"}
|
||||
placeholder={"Nachname"}
|
||||
{...formValues.getInputProps("sn")}
|
||||
required
|
||||
/>
|
||||
</>
|
||||
}
|
||||
|
||||
<Group justify={"center"}>
|
||||
{user?.REALM === "GUEST" && <>
|
||||
<Tooltip label={"Vor- und Nachname speichern"}>
|
||||
<ActionIcon
|
||||
variant={"transparent"}
|
||||
aria-label={"change email"}
|
||||
onClick={() => {
|
||||
mutation.mutate()
|
||||
}}
|
||||
color={"green"}
|
||||
disabled={!formValues.isTouched()}
|
||||
>
|
||||
<IconDeviceFloppy/>
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip label={"Email ändern"}>
|
||||
<ActionIcon
|
||||
variant={"transparent"}
|
||||
|
@ -189,7 +261,8 @@ export default function UserMenuModal() {
|
|||
</Tooltip>
|
||||
</>}
|
||||
|
||||
<Tooltip label={"Anmeldedaten neu laden"}>
|
||||
{showDebug &&
|
||||
<Tooltip color={"orange"} label={"Debug: Anmeldedaten neu laden"}>
|
||||
<ActionIcon
|
||||
variant={"transparent"}
|
||||
color={"orange"}
|
||||
|
@ -201,6 +274,7 @@ export default function UserMenuModal() {
|
|||
<IconRefresh/>
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
}
|
||||
|
||||
<Tooltip label={"Ausloggen"}>
|
||||
<ActionIcon
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import {PB_BASE_URL} from "../../config.ts";
|
||||
import {useLocalStorage} from "@mantine/hooks";
|
||||
|
||||
/**
|
||||
* This function creates a query string from an object.
|
||||
|
@ -31,15 +32,51 @@ export type ErrorCorrectionLevel = "L" | "M" | "Q" | "H"
|
|||
*/
|
||||
export const createQRCodeUrl = (options: {
|
||||
data: string,
|
||||
ecc: ErrorCorrectionLevel,
|
||||
scale: number,
|
||||
border: number,
|
||||
color: string,
|
||||
colorBackground: string
|
||||
ecc?: ErrorCorrectionLevel,
|
||||
scale?: number,
|
||||
border?: number,
|
||||
color?: string,
|
||||
colorBackground?: string
|
||||
}): string => {
|
||||
return `${PB_BASE_URL}/api/qr/v1?${createQueryParams(options)}`
|
||||
}
|
||||
|
||||
/**
|
||||
* The settings for the QR code.
|
||||
* @typedef {Object} QRCodeScannerSettings
|
||||
* @property {boolean} torch - If true, the torch is on. Otherwise, it's off.
|
||||
* @property {boolean} sound - If true, the sound is on. Otherwise, it's off.
|
||||
*/
|
||||
export type QRCodeScannerSettings = {
|
||||
torch: boolean,
|
||||
sound: boolean,
|
||||
}
|
||||
|
||||
/**
|
||||
* A hook that provides a way to get and set QR code settings.
|
||||
* The settings are stored in local storage under the key 'stuve-it-qr-code-settings'.
|
||||
* The default settings are { torch: false, sound: true }.
|
||||
* @returns {Array} A tuple where the first element is the current settings and the second element is a function to update the settings.
|
||||
* The update function takes a partial settings object and merges it with the current settings.
|
||||
* @example
|
||||
* const [settings, setSettings] = useQRCodeSettings();
|
||||
* setSettings({ torch: true }); // turns the torch on
|
||||
*/
|
||||
export const useQRCodeScannerSettings = (): [QRCodeScannerSettings, (s: Partial<QRCodeScannerSettings>) => void] => {
|
||||
const [_value, _setValue] = useLocalStorage({
|
||||
key: 'stuve-it-qr-code-settings',
|
||||
defaultValue: JSON.stringify({
|
||||
torch: false,
|
||||
sound: true,
|
||||
} as QRCodeScannerSettings),
|
||||
})
|
||||
|
||||
const value = JSON.parse(_value)
|
||||
const setValue = (settings: Partial<QRCodeScannerSettings>) => _setValue(JSON.stringify({...value, ...settings}))
|
||||
|
||||
return [value, setValue]
|
||||
}
|
||||
|
||||
export const flattenReducer = <T>(acc: T[], val: T | T[]): T[] => {
|
||||
if (Array.isArray(val)) {
|
||||
return [...acc, ...val]
|
||||
|
|
|
@ -5,6 +5,7 @@ import EventView from "./s/EventView.tsx";
|
|||
import EventNavigate from "@/pages/events/EventNavigate.tsx";
|
||||
import EditEventRouter from "@/pages/events/e/:eventId/EditEventRouter.tsx";
|
||||
import UserEntries from "@/pages/events/entries/UserEntries.tsx";
|
||||
import StatusEditor from "@/pages/events/StatusEditor/StatusEditor.tsx";
|
||||
|
||||
|
||||
export default function EventsRouter() {
|
||||
|
@ -14,6 +15,7 @@ export default function EventsRouter() {
|
|||
<Route path={":eventId"} element={<EventNavigate/>}/>
|
||||
<Route path={"s/:eventId/*"} element={<EventView/>}/>
|
||||
<Route path={"e/:eventId/*"} element={<EditEventRouter/>}/>
|
||||
<Route path={"e/:eventId/lists/status/*"} element={<StatusEditor/>}/>
|
||||
<Route path={"entries"} element={<UserEntries/>}/>
|
||||
<Route path={"*"} element={<NotFound/>}/>
|
||||
</Routes>
|
||||
|
|
|
@ -0,0 +1,48 @@
|
|||
import {EventListSlotEntriesWithUserModel} from "@/models/EventTypes.ts";
|
||||
import {useMutation} from "@tanstack/react-query";
|
||||
import {FieldEntries} from "@/components/formUtil/FromInput/types.ts";
|
||||
import {showSuccessNotification} from "@/components/util.tsx";
|
||||
import {getUserName} from "@/components/users/modals/util.tsx";
|
||||
import {PocketBaseErrorAlert, usePB} from "@/lib/pocketbase.tsx";
|
||||
import FormInput from "@/components/formUtil/FromInput";
|
||||
import {getListSchemas} from "@/pages/events/util.ts";
|
||||
import {useNavigate, useParams} from "react-router-dom";
|
||||
|
||||
export default function EditStatus({entry, refetch}: {
|
||||
entry: EventListSlotEntriesWithUserModel
|
||||
refetch: () => void
|
||||
}) {
|
||||
const {pb} = usePB()
|
||||
const {eventId} = useParams() as { entryId: string, eventId: string }
|
||||
const navigate = useNavigate()
|
||||
|
||||
const mutation = useMutation({
|
||||
mutationFn: async (values: FieldEntries) => {
|
||||
return await pb.collection("eventListSlotEntries").update(entry!.id, {
|
||||
entryStatusData: values
|
||||
})
|
||||
},
|
||||
onSuccess: () => {
|
||||
showSuccessNotification(`Status von ${getUserName(entry!.expand?.user)} erfolgreich aktualisiert`)
|
||||
navigate(`/events/e/${eventId}/lists/status/${entry.id}/s`, {replace: true})
|
||||
refetch()
|
||||
}
|
||||
})
|
||||
|
||||
const {statusSchema} = getListSchemas(entry)
|
||||
|
||||
return <>
|
||||
<div className={"section-transparent"}>
|
||||
<PocketBaseErrorAlert error={mutation.error}/>
|
||||
</div>
|
||||
|
||||
<div className={"section-transparent"}>
|
||||
<FormInput
|
||||
schema={statusSchema}
|
||||
onSubmit={mutation.mutateAsync}
|
||||
onAbort={() => navigate(`/events/e/${eventId}/lists/status`, {replace: true})}
|
||||
initialData={entry.entryStatusData ?? undefined}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
import {EventListSlotEntriesWithUserModel} from "@/models/EventTypes.ts";
|
||||
import EntryStatusSpoiler from "@/pages/events/e/:eventId/EventLists/EventListComponents/EntryStatusSpoiler.tsx";
|
||||
import {Button, Group} from "@mantine/core";
|
||||
import {IconCheck, IconCheckupList} from "@tabler/icons-react";
|
||||
import {Link} from "react-router-dom";
|
||||
|
||||
export default function ShowStatus({entry}: {
|
||||
entry: EventListSlotEntriesWithUserModel
|
||||
}) {
|
||||
return <>
|
||||
<div className={"section-transparent"}>
|
||||
<EntryStatusSpoiler entry={entry}/>
|
||||
</div>
|
||||
|
||||
<div className={"section-transparent"}>
|
||||
<Group>
|
||||
|
||||
<Button
|
||||
leftSection={<IconCheckupList/>}
|
||||
component={Link}
|
||||
to={`/events/e/${entry.event}/lists/status/${entry.id}/e`}
|
||||
>
|
||||
Bearbeiten
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
leftSection={<IconCheck/>}
|
||||
component={Link}
|
||||
to={`/events/e/${entry.event}/lists/status`}
|
||||
>
|
||||
Fertig
|
||||
</Button>
|
||||
|
||||
</Group>
|
||||
</div>
|
||||
</>
|
||||
}
|
|
@ -0,0 +1,84 @@
|
|||
import {Navigate, Route, Routes, useParams} from "react-router-dom";
|
||||
import {useQuery} from "@tanstack/react-query";
|
||||
import {PocketBaseErrorAlert, usePB} from "@/lib/pocketbase.tsx";
|
||||
import {Button, Center, Collapse, Divider, Group, Loader, Text, Title} from "@mantine/core";
|
||||
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 {humanDeltaFromNow} from "@/lib/datetime.ts";
|
||||
import InnerHtml from "@/components/InnerHtml";
|
||||
import {useDisclosure} from "@mantine/hooks";
|
||||
import {IconEye, IconEyeOff} from "@tabler/icons-react";
|
||||
import ShowStatus from "@/pages/events/StatusEditor/EditEntryStatus/ShowStatus.tsx";
|
||||
|
||||
export default function Index() {
|
||||
const {entryId} = useParams() as { entryId: string, eventId: string }
|
||||
const {pb} = usePB()
|
||||
|
||||
const [showDescription, {toggle}] = useDisclosure(false);
|
||||
|
||||
const entryQuery = useQuery({
|
||||
queryKey: ["entry", entryId],
|
||||
queryFn: async () => (await pb.collection("eventListSlotEntriesWithUser").getOne(entryId, {
|
||||
expand: "user"
|
||||
}))
|
||||
})
|
||||
|
||||
const entry = entryQuery.data
|
||||
|
||||
if (entryQuery.isPending) {
|
||||
return <Center>
|
||||
<Loader/>
|
||||
</Center>
|
||||
}
|
||||
|
||||
if (entryQuery.isError || !entry) {
|
||||
return <PocketBaseErrorAlert error={entryQuery.error}/>
|
||||
}
|
||||
|
||||
return <>
|
||||
<div className={"section stack"}>
|
||||
<Title c={"blue"} order={1} size={"md"}>
|
||||
{getUserName(entry.expand?.user)}
|
||||
</Title>
|
||||
|
||||
<Divider/>
|
||||
|
||||
<Text>{entry.listName}</Text>
|
||||
<Group>
|
||||
<RenderDateRange start={new Date(entry.slotStartDate)} end={new Date(entry.slotEndDate)}/>
|
||||
({humanDeltaFromNow(entry.slotStartDate, entry.slotEndDate).message})
|
||||
</Group>
|
||||
|
||||
{
|
||||
(entry.listDescription || entry.slotDescription) && <>
|
||||
<Group>
|
||||
<Button
|
||||
size={"xs"}
|
||||
variant={"light"}
|
||||
onClick={toggle}
|
||||
leftSection={showDescription ? <IconEyeOff/> : <IconEye/>}
|
||||
>
|
||||
Beschreibung
|
||||
</Button>
|
||||
</Group>
|
||||
|
||||
<Collapse in={showDescription}>
|
||||
<>
|
||||
{entry.listDescription && <InnerHtml html={entry.listDescription}/>}
|
||||
{entry.slotDescription && <InnerHtml html={entry.slotDescription}/>}
|
||||
</>
|
||||
</Collapse>
|
||||
</>
|
||||
}
|
||||
</div>
|
||||
|
||||
<Routes>
|
||||
<Route index element={<Navigate to={"e"} replace/>}/>
|
||||
<Route path={"e"} element={<EditStatus entry={entry} refetch={entryQuery.refetch}/>}/>
|
||||
<Route path={"s"} element={<ShowStatus entry={entry}/>}/>
|
||||
<Route path={"*"} element={<NotFound/>}/>
|
||||
</Routes>
|
||||
</>
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
import QRCodeModal from "@/pages/events/StatusEditor/QrCodeModal.tsx";
|
||||
import EntrySearchModal from "@/pages/events/StatusEditor/EntrySearchModal.tsx";
|
||||
import {ActionIcon, Center, Group, Text} from "@mantine/core";
|
||||
import {IconQrcode, IconUserSearch} from "@tabler/icons-react";
|
||||
import {useDisclosure} from "@mantine/hooks";
|
||||
import SearchSVG from "@/illustrations/search.svg?react"
|
||||
|
||||
|
||||
export default function EntrySearch() {
|
||||
const [showQrCodeModal, showQrCodeModalHandler] = useDisclosure(false)
|
||||
const [showEntrySearchModal, showEntrySearchModalHandler] = useDisclosure(false)
|
||||
|
||||
return <>
|
||||
<QRCodeModal opened={showQrCodeModal} onClose={showQrCodeModalHandler.close}/>
|
||||
<EntrySearchModal opened={showEntrySearchModal} onClose={showEntrySearchModalHandler.close}/>
|
||||
|
||||
<Center>
|
||||
<SearchSVG height={"300px"} width={"300px"}/>
|
||||
</Center>
|
||||
|
||||
<div className={" center"}>
|
||||
<Group justify={"center"}>
|
||||
<ActionIcon
|
||||
variant="light" size="xl" radius="xl"
|
||||
aria-label={"search for user"}
|
||||
onClick={showEntrySearchModalHandler.open}
|
||||
>
|
||||
<IconUserSearch/>
|
||||
</ActionIcon>
|
||||
|
||||
<Text size={"xs"} c={"dimmed"}>
|
||||
oder
|
||||
</Text>
|
||||
|
||||
<ActionIcon
|
||||
variant="light" size="xl" radius="xl"
|
||||
aria-label={"scan qr code"}
|
||||
onClick={showQrCodeModalHandler.open}
|
||||
>
|
||||
<IconQrcode/>
|
||||
</ActionIcon>
|
||||
</Group>
|
||||
</div>
|
||||
</>
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
.container {
|
||||
transition: height 200ms ease, width 200ms ease, padding 200ms ease, border 200ms ease, border-radius 200ms ease, background-color 200ms ease, max-width 200ms ease;
|
||||
max-height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.content {
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.body {
|
||||
padding: 0;
|
||||
max-height: 100%;
|
||||
overflow: hidden;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
|
||||
.entry {
|
||||
padding: var(--mantine-spacing-xs);
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.entryDetails {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: calc(var(--mantine-spacing-xs) / 2);
|
||||
flex: 1;
|
||||
}
|
|
@ -0,0 +1,141 @@
|
|||
import {ActionIcon, Kbd, Loader, Modal, Text, TextInput} from "@mantine/core";
|
||||
import {getHotkeyHandler, useDebouncedValue} from "@mantine/hooks";
|
||||
import {useQuery} from "@tanstack/react-query";
|
||||
import {IconUserSearch, IconX} from "@tabler/icons-react";
|
||||
import {PocketBaseErrorAlert, usePB} from "@/lib/pocketbase.tsx";
|
||||
import {Link, useNavigate, useParams} from "react-router-dom";
|
||||
import {useState} from "react";
|
||||
import {getUserName} from "@/components/users/modals/util.tsx";
|
||||
|
||||
import classes from "./EntrySearchModal.module.css";
|
||||
import {humanDeltaFromNow, pprintDateRange} from "@/lib/datetime.ts";
|
||||
|
||||
export default function EntrySearchModal({opened, onClose}: {
|
||||
opened: boolean,
|
||||
onClose: () => void,
|
||||
}) {
|
||||
|
||||
const {pb} = usePB()
|
||||
|
||||
const {eventId} = useParams() as { eventId: string }
|
||||
const navigate = useNavigate()
|
||||
|
||||
const [searchValue, setSearchValue] = useState('');
|
||||
const [debouncedSearchValue] = useDebouncedValue(searchValue, 300);
|
||||
|
||||
const userQuery = useQuery({
|
||||
queryKey: ["eventListSlotEntriesWithUser", debouncedSearchValue],
|
||||
queryFn: async () => {
|
||||
const filter = [] as string[]
|
||||
|
||||
const usernameQuery = debouncedSearchValue.trim()
|
||||
const givenName = debouncedSearchValue.split(" ")[1]?.trim() ?? null
|
||||
const surname = debouncedSearchValue.split(" ")[0]?.trim() ?? givenName
|
||||
|
||||
usernameQuery && filter.push(`user.username~'${usernameQuery}'`)
|
||||
givenName && filter.push(`user.givenName~'${givenName}'`)
|
||||
surname && filter.push(`user.sn~'${surname}'`)
|
||||
|
||||
if (filter.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
return await pb.collection("eventListSlotEntriesWithUser").getList(1, 40, {
|
||||
filter: `(${filter.join("||")}) && event~'${eventId}'`,
|
||||
expand: "user",
|
||||
sort: "-slotStartDate"
|
||||
})
|
||||
},
|
||||
retry: false,
|
||||
enabled: opened
|
||||
})
|
||||
|
||||
return <Modal
|
||||
size={"xl"}
|
||||
opened={opened}
|
||||
onClose={onClose}
|
||||
withCloseButton={false}
|
||||
centered
|
||||
classNames={classes}
|
||||
styles={{}}
|
||||
>
|
||||
<div className={classes.container}>
|
||||
<TextInput
|
||||
size={"lg"}
|
||||
variant={"unstyled"}
|
||||
leftSection={<IconUserSearch/>}
|
||||
rightSection={userQuery.isLoading ? <Loader size={"xs"}/> : searchValue.length !== 0 ? (
|
||||
<ActionIcon
|
||||
variant={"transparent"}
|
||||
onClick={() => setSearchValue('')}
|
||||
aria-label={"Clear"}
|
||||
>
|
||||
<IconX/>
|
||||
</ActionIcon>
|
||||
) : undefined}
|
||||
placeholder={`Nach Anmeldungen suchen...`}
|
||||
value={searchValue}
|
||||
onChange={(e) => setSearchValue(e.target.value)}
|
||||
onKeyDown={getHotkeyHandler(
|
||||
(userQuery.data?.items ?? []).map((entry, i) => ([
|
||||
`ctrl+${i + 1}`,
|
||||
(e: KeyboardEvent) => {
|
||||
console.log("entry", entry)
|
||||
e.stopPropagation()
|
||||
onClose()
|
||||
setSearchValue("")
|
||||
navigate(`/events/e/${eventId}/lists/status/${entry.id}`, {replace: true})
|
||||
}
|
||||
])
|
||||
)
|
||||
)}
|
||||
/>
|
||||
<PocketBaseErrorAlert error={userQuery.error}/>
|
||||
<div className={"scrollbar scrollable"}>
|
||||
{userQuery.data?.items.map((e, i) => (
|
||||
<Link
|
||||
to={`/events/e/${eventId}/lists/status/${e.id}`}
|
||||
key={e.id}
|
||||
className={`${classes.entry} hover`}
|
||||
onClick={() => {
|
||||
setSearchValue("")
|
||||
onClose()
|
||||
}}
|
||||
replace={true}
|
||||
>
|
||||
<div className={classes.entryDetails}>
|
||||
<Text>{getUserName(e.expand?.user)}</Text>
|
||||
<Text size={"xs"} c={"dimmed"}>{e.listName}</Text>
|
||||
<Text size={"xs"} c={"dimmed"}>
|
||||
{pprintDateRange(e.slotStartDate, e.slotEndDate)}
|
||||
{" • "}
|
||||
{humanDeltaFromNow(e.slotStartDate, e.slotEndDate).message}
|
||||
</Text>
|
||||
</div>
|
||||
<div>
|
||||
<Kbd size={"xs"} c={"dimmed"}>
|
||||
CTRL + {i + 1}
|
||||
</Kbd>
|
||||
</div>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
{debouncedSearchValue.length > 0 &&
|
||||
(
|
||||
<Text
|
||||
p={"sm"}
|
||||
size={"xs"}
|
||||
ta={"center"}
|
||||
c={"dimmed"}
|
||||
>
|
||||
{
|
||||
!userQuery.data?.totalItems ?
|
||||
userQuery.isPending ? "Suche..." :
|
||||
"Keine Anmeldungen gefunden" :
|
||||
`${userQuery.data?.totalItems} Anmeldung(en) gefunden`
|
||||
}
|
||||
</Text>
|
||||
)}
|
||||
</div>
|
||||
</Modal>
|
||||
}
|
|
@ -0,0 +1,170 @@
|
|||
import {ActionIcon, Group, Modal, useMantineTheme} from "@mantine/core";
|
||||
import {IDetectedBarcode, IPoint, Scanner} from "@yudiel/react-qr-scanner";
|
||||
import {IconBulb, IconBulbOff, IconVolume, IconVolumeOff, IconX} from "@tabler/icons-react";
|
||||
import {useNavigate} from "react-router-dom";
|
||||
import {useQRCodeScannerSettings} from "@/lib/util.ts";
|
||||
import {useEffect} from "react";
|
||||
|
||||
const REGEX = /\/events\/e\/(?<event_id>[a-zA-Z0-9]+)\/lists\/status\/(?<status_id>[a-zA-Z0-9]+)/;
|
||||
|
||||
|
||||
/**
|
||||
* Draws text in the middle of a rectangle defined by an array of IPoint.
|
||||
* @param ctx - The canvas rendering context.
|
||||
* @param points - The array of points defining the rectangle.
|
||||
* @param text - The text to draw.
|
||||
* @param textColor - The color of the text.
|
||||
* @param backgroundColor - The background color behind the text.
|
||||
*/
|
||||
function drawCenteredText(ctx: CanvasRenderingContext2D, points: IPoint[], text: string, textColor: string, backgroundColor: string): void {
|
||||
// Calculate the bounding box
|
||||
const xValues = points.map(point => point.x);
|
||||
const yValues = points.map(point => point.y);
|
||||
|
||||
const minX = Math.min(...xValues);
|
||||
const maxX = Math.max(...xValues);
|
||||
const minY = Math.min(...yValues);
|
||||
const maxY = Math.max(...yValues);
|
||||
|
||||
const centerX = (minX + maxX) / 2;
|
||||
const centerY = (minY + maxY) / 2;
|
||||
|
||||
// Set text properties to measure text width and height
|
||||
ctx.font = '10px Overpass';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.textBaseline = 'middle';
|
||||
|
||||
// Measure text width and height
|
||||
const textMetrics = ctx.measureText(text);
|
||||
const textWidth = textMetrics.width;
|
||||
const textHeight = textMetrics.actualBoundingBoxAscent + textMetrics.actualBoundingBoxDescent;
|
||||
|
||||
// Draw the background rectangle
|
||||
ctx.fillStyle = backgroundColor;
|
||||
ctx.fillRect(centerX - textWidth / 2 - 5, centerY - textHeight / 2 - 5, textWidth + 10, textHeight + 10);
|
||||
|
||||
// Draw the text
|
||||
ctx.fillStyle = textColor;
|
||||
ctx.fillText(text, centerX, centerY);
|
||||
}
|
||||
|
||||
function outlineQrCodes(detectedCodes: IDetectedBarcode[], ctx: CanvasRenderingContext2D) {
|
||||
for (const detectedCode of detectedCodes) {
|
||||
const [firstPoint, ...otherPoints] = detectedCode.cornerPoints;
|
||||
ctx.lineWidth = 1.5;
|
||||
|
||||
// only paint qr codes that match the regex
|
||||
if (detectedCode.rawValue.match(REGEX)) {
|
||||
ctx.strokeStyle = '#4dabf7';
|
||||
} else {
|
||||
ctx.strokeStyle = '#ff6b6b';
|
||||
drawCenteredText(ctx, otherPoints, "Invalide", "#ff6b6b", "#fff");
|
||||
}
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(firstPoint.x, firstPoint.y);
|
||||
for (const {x, y} of otherPoints) {
|
||||
ctx.lineTo(x, y);
|
||||
}
|
||||
ctx.lineTo(firstPoint.x, firstPoint.y);
|
||||
ctx.closePath();
|
||||
ctx.stroke();
|
||||
}
|
||||
}
|
||||
|
||||
export default function QRCodeModal({opened, onClose}: {
|
||||
opened: boolean,
|
||||
onClose: () => void,
|
||||
}) {
|
||||
|
||||
|
||||
const [settings, setSettings] = useQRCodeScannerSettings()
|
||||
useEffect(() => undefined, [settings]);
|
||||
|
||||
const navigate = useNavigate()
|
||||
|
||||
const theme = useMantineTheme()
|
||||
return <Modal
|
||||
size={"xl"}
|
||||
opened={opened}
|
||||
onClose={onClose}
|
||||
withCloseButton={false}
|
||||
centered
|
||||
styles={{
|
||||
content: {
|
||||
background: "none",
|
||||
boxShadow: "none"
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className={"stack"}>
|
||||
<Scanner
|
||||
key={JSON.stringify(settings)}
|
||||
paused={!opened}
|
||||
onScan={(result) => {
|
||||
if (result?.[0]?.rawValue) {
|
||||
const match = result[0].rawValue.match(REGEX);
|
||||
if (match) {
|
||||
const event_id = match.groups?.event_id;
|
||||
const status_id = match.groups?.status_id;
|
||||
|
||||
onClose()
|
||||
navigate(`/events/e/${event_id}/lists/status/${status_id}`, {replace: true});
|
||||
}
|
||||
}
|
||||
}}
|
||||
components={{
|
||||
audio: settings.sound,
|
||||
onOff: settings.sound,
|
||||
torch: settings.torch,
|
||||
tracker: outlineQrCodes,
|
||||
finder: false
|
||||
}}
|
||||
styles={{
|
||||
container: {
|
||||
borderRadius: theme.radius.md,
|
||||
overflow: "hidden",
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
margin: 'auto'
|
||||
}
|
||||
}}
|
||||
allowMultiple={true}
|
||||
/>
|
||||
|
||||
<Group justify="center">
|
||||
<ActionIcon
|
||||
onClick={() => {
|
||||
setSettings({
|
||||
torch: !settings.torch
|
||||
})
|
||||
}}
|
||||
color={settings.torch ? "yellow" : "gray"}
|
||||
aria-label={"flash"}
|
||||
>
|
||||
{settings.torch ? <IconBulb/> : <IconBulbOff/>}
|
||||
</ActionIcon>
|
||||
|
||||
<ActionIcon
|
||||
onClick={() => {
|
||||
setSettings({
|
||||
sound: !settings.sound
|
||||
})
|
||||
}}
|
||||
color={settings.sound ? "blue" : "gray"}
|
||||
aria-label={"scan sound"}
|
||||
>
|
||||
{settings.sound ? <IconVolume/> : <IconVolumeOff/>}
|
||||
</ActionIcon>
|
||||
|
||||
<ActionIcon
|
||||
onClick={onClose}
|
||||
aria-label={"close"}
|
||||
color={"gray"}
|
||||
>
|
||||
<IconX/>
|
||||
</ActionIcon>
|
||||
</Group>
|
||||
</div>
|
||||
</Modal>
|
||||
}
|
|
@ -0,0 +1,58 @@
|
|||
import {usePB} from "@/lib/pocketbase.tsx";
|
||||
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 Index from "@/pages/events/StatusEditor/EditEntryStatus";
|
||||
import EntrySearch from "@/pages/events/StatusEditor/EntrySearch.tsx";
|
||||
|
||||
export default function StatusEditor() {
|
||||
|
||||
const {pb} = usePB()
|
||||
const {eventId} = useParams() as { eventId: string }
|
||||
|
||||
const eventQuery = useQuery({
|
||||
queryKey: ["event", eventId],
|
||||
queryFn: async () => (await pb.collection("events").getOne(eventId, {
|
||||
expand: "eventAdmins, privilegedLists, privilegedLists.eventListSlots_via_eventList.eventListSlotEntries_via_eventListsSlot"
|
||||
}))
|
||||
})
|
||||
|
||||
const {isPrivilegedUser, canEditEvent} = useEventRights(eventQuery.data)
|
||||
|
||||
if (eventQuery.isLoading) {
|
||||
return <LoadingOverlay/>
|
||||
}
|
||||
|
||||
if (eventQuery.isError || !eventQuery.data) {
|
||||
return <NotFound/>
|
||||
}
|
||||
|
||||
if (!(canEditEvent || isPrivilegedUser)) {
|
||||
return <Navigate to={`/events/s/${eventId}`} replace/>
|
||||
}
|
||||
|
||||
const event = eventQuery.data
|
||||
|
||||
return <>
|
||||
<div className={"section-transparent"}>
|
||||
<Breadcrumbs>{[
|
||||
{title: "Home", to: "/"},
|
||||
{title: "Events", to: "/events"},
|
||||
{title: event.name, to: `/events/${event.id}`},
|
||||
{title: "Status Editor", to: `/events/e/${event.id}/lists/status`},
|
||||
].map(({title, to}) => (
|
||||
<Anchor component={Link} to={to} key={title}>
|
||||
{title}
|
||||
</Anchor>
|
||||
))}</Breadcrumbs>
|
||||
</div>
|
||||
|
||||
<Routes>
|
||||
<Route index element={<EntrySearch/>}/>
|
||||
<Route path={":entryId/*"} element={<Index/>}/>
|
||||
</Routes>
|
||||
<Outlet/>
|
||||
</>
|
||||
}
|
|
@ -163,7 +163,6 @@ export default function EditEventRouter() {
|
|||
<EventFavourites event={event}/>
|
||||
</div>
|
||||
</Grid.Col>
|
||||
|
||||
<Grid.Col span={{base: 12, sm: 9}} className={"stack"}>
|
||||
<Group gap={"sm"} align={"center"}>
|
||||
<NavLink to={`/events/e/${eventId}/description`} replace className={classes.navLink}>
|
||||
|
|
|
@ -19,15 +19,20 @@ const viewNav = [
|
|||
{
|
||||
label: "Listenaktionen",
|
||||
children: [
|
||||
{
|
||||
title: "Listen und Slots",
|
||||
icon: <IconList size={16}/>,
|
||||
to: "overview"
|
||||
},
|
||||
{
|
||||
title: "Anmeldungen",
|
||||
icon: <IconUserSearch size={16}/>,
|
||||
to: "search"
|
||||
},
|
||||
{
|
||||
title: "Status-Editor",
|
||||
icon: <IconCheckupList size={16}/>,
|
||||
to: "status"
|
||||
},
|
||||
{
|
||||
title: "Listen und Slots",
|
||||
icon: <IconList size={16}/>,
|
||||
to: "overview"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
|
|
@ -66,7 +66,19 @@ export default function ListSearch({event}: { event: EventModel }) {
|
|||
const filter: string[] = [`event='${event.id}'`]
|
||||
|
||||
// filter for user by name
|
||||
filterValues.searchQueryString && filter.push(`user.username ~ '${filterValues.searchQueryString.trim().replace(" ", ".")}'`)
|
||||
if (filterValues.searchQueryString) {
|
||||
const usernameQuery = filterValues.searchQueryString.trim()
|
||||
const givenName = filterValues.searchQueryString.split(" ")[1]?.trim() ?? null
|
||||
const surname = filterValues.searchQueryString.split(" ")[0]?.trim() ?? givenName
|
||||
|
||||
const nameFilter = [] as string[]
|
||||
|
||||
usernameQuery && nameFilter.push(`user.username~'${usernameQuery}'`)
|
||||
surname && nameFilter.push(`user.sn~'${surname}'`)
|
||||
givenName && nameFilter.push(`user.givenName~'${givenName}'`)
|
||||
|
||||
filter.push(`(${nameFilter.join("||")})`)
|
||||
}
|
||||
|
||||
// filter for lists
|
||||
filterValues.selectedLists.length > 0 && filter.push(`(${filterValues.selectedLists.map(l => `eventList='${l.id}'`).join(" || ")})`)
|
||||
|
@ -100,7 +112,7 @@ export default function ListSearch({event}: { event: EventModel }) {
|
|||
initialPageParam: 1,
|
||||
})
|
||||
|
||||
// this string informs the user about the current list selection
|
||||
// this string informs the user about the current list selection
|
||||
const listInfoString = `${formValues.values.selectedLists.length > 0 ? ` in den Listen ${formValues.values.selectedLists.map(l => `'${l.name}'`).join(", ")}` : ""}`
|
||||
|
||||
const entries = query.data?.pages.flatMap(p => p.items) ?? []
|
||||
|
|
|
@ -1,6 +1,24 @@
|
|||
import {EventListSlotEntriesWithUserModel} from "@/models/EventTypes.ts";
|
||||
import {ActionIcon, Collapse, Group, Text, ThemeIcon, Tooltip} from "@mantine/core";
|
||||
import {IconChevronDown, IconChevronRight, IconConfetti, IconForms, IconList, IconTrash} from "@tabler/icons-react";
|
||||
import {
|
||||
ActionIcon,
|
||||
Collapse,
|
||||
Group,
|
||||
Modal,
|
||||
Text,
|
||||
ThemeIcon,
|
||||
Tooltip,
|
||||
useMantineColorScheme,
|
||||
useMantineTheme
|
||||
} from "@mantine/core";
|
||||
import {
|
||||
IconChevronDown,
|
||||
IconChevronRight,
|
||||
IconConfetti,
|
||||
IconForms,
|
||||
IconList,
|
||||
IconQrcode,
|
||||
IconTrash
|
||||
} from "@tabler/icons-react";
|
||||
import TextWithIcon from "@/components/layout/TextWithIcon";
|
||||
|
||||
|
||||
|
@ -19,6 +37,9 @@ import {
|
|||
EntryQuestionAndStatusData
|
||||
} from "@/pages/events/e/:eventId/EventLists/EventListComponents/EntryQuestionAndStatusData.tsx";
|
||||
import {Link} from "react-router-dom";
|
||||
import {createQRCodeUrl} from "@/lib/util.ts";
|
||||
import {APP_URL} from "../../../../config.ts";
|
||||
import ShowHelp from "@/components/ShowHelp.tsx";
|
||||
|
||||
|
||||
export default function UserEntryRow({entry, refetch}: {
|
||||
|
@ -29,6 +50,8 @@ export default function UserEntryRow({entry, refetch}: {
|
|||
|
||||
const [expanded, expandedHandler] = useDisclosure(false)
|
||||
|
||||
const [showQrCode, showQrCodeHandler] = useDisclosure(false)
|
||||
|
||||
const [showEditFormModal, showEditFormModalHandler] = useDisclosure(false)
|
||||
|
||||
const delta = humanDeltaFromNow(entry.slotStartDate, entry.slotEndDate)
|
||||
|
@ -49,6 +72,15 @@ export default function UserEntryRow({entry, refetch}: {
|
|||
onConfirm: () => deleteEntryMutation.mutate()
|
||||
})
|
||||
|
||||
const theme = useMantineTheme()
|
||||
const {colorScheme} = useMantineColorScheme()
|
||||
|
||||
const qrCodeLink = createQRCodeUrl({
|
||||
data: `${APP_URL}/events/e/${entry.event}/lists/status/${entry.id}`,
|
||||
color: colorScheme === "dark" ? theme.white : theme.colors.dark[8],
|
||||
colorBackground: colorScheme === "dark" ? theme.colors.dark[7] : ""
|
||||
})
|
||||
|
||||
return <div className={classes.container}>
|
||||
|
||||
<ConfirmModal/>
|
||||
|
@ -60,6 +92,13 @@ export default function UserEntryRow({entry, refetch}: {
|
|||
entry={entry}
|
||||
/>
|
||||
|
||||
<Modal opened={showQrCode} onClose={showQrCodeHandler.close} title={"Status Code"}>
|
||||
<img src={qrCodeLink} alt={"QR Code Vorschau"}/>
|
||||
<ShowHelp>
|
||||
Mit diesem Code können die Veranstalter:innen des Events deinen Status bearbeiten.
|
||||
</ShowHelp>
|
||||
</Modal>
|
||||
|
||||
<div className={classes.row}>
|
||||
<Tooltip label={expanded ? "Details verbergen" : "Details anzeigen"} withArrow>
|
||||
<ActionIcon onClick={expandedHandler.toggle} variant={"light"} size={"xs"}>
|
||||
|
@ -81,7 +120,7 @@ export default function UserEntryRow({entry, refetch}: {
|
|||
<IconList/>
|
||||
</ThemeIcon>
|
||||
}>
|
||||
<Link to={`/events/s/${entry.event}?lists=${entry.eventList}`} >
|
||||
<Link to={`/events/s/${entry.event}?lists=${entry.eventList}`}>
|
||||
{entry.listName}
|
||||
</Link>
|
||||
</TextWithIcon>
|
||||
|
@ -95,6 +134,16 @@ export default function UserEntryRow({entry, refetch}: {
|
|||
</div>
|
||||
|
||||
<Group gap={"xs"}>
|
||||
|
||||
<ActionIcon
|
||||
variant={"light"}
|
||||
size={"xs"}
|
||||
onClick={showQrCodeHandler.toggle}
|
||||
aria-label={"show qr code"}
|
||||
>
|
||||
<IconQrcode/>
|
||||
</ActionIcon>
|
||||
|
||||
<Tooltip
|
||||
label={delta.delta === "PAST" ? "Vergangene Anmeldungen können nicht bearbeiten werden" : "Eintrag bearbeiten"}
|
||||
withArrow>
|
||||
|
|
|
@ -6,6 +6,7 @@ import {NavLink} from "react-router-dom";
|
|||
import Announcements from "@/pages/chat/components/Announcements.tsx";
|
||||
import classes from './index.module.css'
|
||||
import {ReactNode} from "react";
|
||||
import {getUserName} from "@/components/users/modals/util.tsx";
|
||||
|
||||
const NavButtons = ({buttons}: {
|
||||
buttons: { title: string, icon: ReactNode, to: string }[]
|
||||
|
@ -44,7 +45,7 @@ export default function HomePage() {
|
|||
</ThemeIcon>
|
||||
Hallo
|
||||
|
||||
{user && `, ${user.username}`}
|
||||
{user && `, ${getUserName(user)}`}
|
||||
</Title>
|
||||
|
||||
{!user && <>
|
||||
|
|
|
@ -67,10 +67,12 @@
|
|||
|
||||
a {
|
||||
text-decoration: unset; /* Removes default underline */
|
||||
color: revert;
|
||||
}
|
||||
|
||||
a:hover, a:active, a:visited {
|
||||
text-decoration: unset; /* Removes default underline */
|
||||
color: unset;
|
||||
}
|
||||
|
||||
.section-icon {
|
||||
|
|
45
yarn.lock
45
yarn.lock
|
@ -989,6 +989,16 @@
|
|||
dependencies:
|
||||
"@types/ms" "*"
|
||||
|
||||
"@types/dom-webcodecs@^0.1.11":
|
||||
version "0.1.11"
|
||||
resolved "https://registry.yarnpkg.com/@types/dom-webcodecs/-/dom-webcodecs-0.1.11.tgz#2e36e5cc71789551f107e2fe15d956845fa19567"
|
||||
integrity sha512-yPEZ3z7EohrmOxbk/QTAa0yonMFkNkjnVXqbGb7D4rMr+F1dGQ8ZUFxXkyLLJuiICPejZ0AZE9Rrk9wUCczx4A==
|
||||
|
||||
"@types/emscripten@^1.39.13":
|
||||
version "1.39.13"
|
||||
resolved "https://registry.yarnpkg.com/@types/emscripten/-/emscripten-1.39.13.tgz#afeb1648648dc096efe57983e20387627306e2aa"
|
||||
integrity sha512-cFq+fO/isvhvmuP/+Sl4K4jtU6E23DoivtbO4r50e3odaxAiVdbfSYRDdJ4gCdxx+3aRjhphS5ZMwIH4hFy/Cw==
|
||||
|
||||
"@types/estree-jsx@^1.0.0":
|
||||
version "1.0.5"
|
||||
resolved "https://registry.yarnpkg.com/@types/estree-jsx/-/estree-jsx-1.0.5.tgz#858a88ea20f34fe65111f005a689fa1ebf70dc18"
|
||||
|
@ -1217,6 +1227,14 @@
|
|||
dependencies:
|
||||
"@swc/core" "^1.3.85"
|
||||
|
||||
"@yudiel/react-qr-scanner@^2.0.2":
|
||||
version "2.0.2"
|
||||
resolved "https://registry.yarnpkg.com/@yudiel/react-qr-scanner/-/react-qr-scanner-2.0.2.tgz#0fa54e5f8365a69d1a5bd6f5fb024167bea9ab57"
|
||||
integrity sha512-62xxXQVGkh813MZ3aTQiHMP33CAUJicGb2aRFOt4Vtmna+SR67v1wX3ip7UVcoBxwjuwDTfVvPxWquCvjcBYQg==
|
||||
dependencies:
|
||||
barcode-detector "^2.2.6"
|
||||
webrtc-adapter "9.0.1"
|
||||
|
||||
acorn-jsx@^5.3.2:
|
||||
version "5.3.2"
|
||||
resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937"
|
||||
|
@ -1289,6 +1307,14 @@ balanced-match@^1.0.0:
|
|||
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee"
|
||||
integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==
|
||||
|
||||
barcode-detector@^2.2.6:
|
||||
version "2.2.7"
|
||||
resolved "https://registry.yarnpkg.com/barcode-detector/-/barcode-detector-2.2.7.tgz#ab47e3e36727a9f5d1e7183240f88831f2451efa"
|
||||
integrity sha512-+6PJNcMtdVehX5i2LQUE9L+mS6C3cG00Vsuc4Ynj3Mls5GNKIAFkE0IFGtw4s6vu8SXeogrzTj4btm44oD+gNw==
|
||||
dependencies:
|
||||
"@types/dom-webcodecs" "^0.1.11"
|
||||
zxing-wasm "1.2.11"
|
||||
|
||||
binary-extensions@^2.0.0:
|
||||
version "2.3.0"
|
||||
resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.3.0.tgz#f6e14a97858d327252200242d4ccfe522c445522"
|
||||
|
@ -3512,6 +3538,11 @@ scheduler@^0.23.0:
|
|||
dependencies:
|
||||
loose-envify "^1.1.0"
|
||||
|
||||
sdp@^3.2.0:
|
||||
version "3.2.0"
|
||||
resolved "https://registry.yarnpkg.com/sdp/-/sdp-3.2.0.tgz#8961420552b36663b4d13ddba6f478d1461896a5"
|
||||
integrity sha512-d7wDPgDV3DDiqulJjKiV2865wKsJ34YI+NDREbm+FySq6WuKOikwyNQcm+doLAZ1O6ltdO0SeKle2xMpN3Brgw==
|
||||
|
||||
semver@^6.3.1:
|
||||
version "6.3.1"
|
||||
resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4"
|
||||
|
@ -3924,6 +3955,13 @@ warning@^4.0.3:
|
|||
dependencies:
|
||||
loose-envify "^1.0.0"
|
||||
|
||||
webrtc-adapter@9.0.1:
|
||||
version "9.0.1"
|
||||
resolved "https://registry.yarnpkg.com/webrtc-adapter/-/webrtc-adapter-9.0.1.tgz#d4efa22ca9604cb2c8cdb9e492815ba37acfa0b2"
|
||||
integrity sha512-1AQO+d4ElfVSXyzNVTOewgGT/tAomwwztX/6e3totvyyzXPvXIIuUUjAmyZGbKBKbZOXauuJooZm3g6IuFuiNQ==
|
||||
dependencies:
|
||||
sdp "^3.2.0"
|
||||
|
||||
which@^2.0.1:
|
||||
version "2.0.2"
|
||||
resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1"
|
||||
|
@ -3960,3 +3998,10 @@ zwitch@^2.0.0:
|
|||
version "2.0.4"
|
||||
resolved "https://registry.yarnpkg.com/zwitch/-/zwitch-2.0.4.tgz#c827d4b0acb76fc3e685a4c6ec2902d51070e9d7"
|
||||
integrity sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==
|
||||
|
||||
zxing-wasm@1.2.11:
|
||||
version "1.2.11"
|
||||
resolved "https://registry.yarnpkg.com/zxing-wasm/-/zxing-wasm-1.2.11.tgz#8263ba27ea6d209d7a8440fac15a1fb6838d4630"
|
||||
integrity sha512-rNSMkIU310sK5cCPSjZA58FEhGZUtNx+f0CmtZ3SZzpdwZE6IzzKFdkbFkl8CFnxiUrx1VMVl/2WULDnwwJbfg==
|
||||
dependencies:
|
||||
"@types/emscripten" "^1.39.13"
|
||||
|
|
Loading…
Reference in New Issue