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">
|
<html lang="de">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8"/>
|
<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"/>
|
<link rel="icon" href="/stuve-logo.svg"/>
|
||||||
<title>StuVe IT</title>
|
<title>StuVe IT</title>
|
||||||
|
<meta name="description" content="IT-Tools der StuVe Uni Ulm (K.d.ö.R)">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite --host",
|
||||||
"build": "export NODE_ENV=production && tsc && vite build",
|
"build": "export NODE_ENV=production && tsc && vite build",
|
||||||
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
|
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
|
||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
|
@ -35,6 +35,7 @@
|
||||||
"@tiptap/starter-kit": "^2.3.0",
|
"@tiptap/starter-kit": "^2.3.0",
|
||||||
"@tiptap/suggestion": "^2.4.0",
|
"@tiptap/suggestion": "^2.4.0",
|
||||||
"@types/react-big-calendar": "^1.8.9",
|
"@types/react-big-calendar": "^1.8.9",
|
||||||
|
"@yudiel/react-qr-scanner": "^2.0.2",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"dayjs": "^1.11.10",
|
"dayjs": "^1.11.10",
|
||||||
"jwt-decode": "^3.1.2",
|
"jwt-decode": "^3.1.2",
|
||||||
|
|
|
@ -1,22 +1,39 @@
|
||||||
import {useChangeEmail, useForgotPassword, useUserMenu} from "@/components/users/modals/hooks.ts";
|
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 {useShowHelp} from "@/components/ShowHelp.tsx";
|
||||||
import ShowDebug, {useShowDebug} from "@/components/ShowDebug.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 classes from "@/components/layout/nav/index.module.css";
|
||||||
import {
|
import {
|
||||||
IconAt,
|
IconAt,
|
||||||
IconCalendar,
|
IconCalendar,
|
||||||
|
IconDeviceFloppy,
|
||||||
IconLogout,
|
IconLogout,
|
||||||
IconMailCog,
|
IconMailCog,
|
||||||
IconPassword,
|
IconPassword,
|
||||||
IconRefresh,
|
IconRefresh,
|
||||||
IconServer,
|
IconServer,
|
||||||
IconServerOff,
|
IconServerOff
|
||||||
IconUser
|
|
||||||
} from "@tabler/icons-react";
|
} from "@tabler/icons-react";
|
||||||
import LdapGroupsDisplay from "@/components/users/LdapGroupsDisplay.tsx";
|
import LdapGroupsDisplay from "@/components/users/LdapGroupsDisplay.tsx";
|
||||||
import ColorSchemeSwitch from "@/components/input/ColorSchemeSwitch.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() {
|
export default function UserMenuModal() {
|
||||||
const {value, handler} = useUserMenu()
|
const {value, handler} = useUserMenu()
|
||||||
|
@ -25,22 +42,58 @@ export default function UserMenuModal() {
|
||||||
|
|
||||||
const {handler: changeEmailHandler} = useChangeEmail()
|
const {handler: changeEmailHandler} = useChangeEmail()
|
||||||
|
|
||||||
const {logout, apiIsHealthy, user, refreshUser} = usePB()
|
const {logout, apiIsHealthy, pb, user, refreshUser} = usePB()
|
||||||
|
|
||||||
const {showHelp, toggleShowHelp} = useShowHelp()
|
const {showHelp, toggleShowHelp} = useShowHelp()
|
||||||
|
|
||||||
const {showDebug, toggleShowDebug} = useShowDebug()
|
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 <>
|
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}>
|
<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>
|
<ShowDebug>
|
||||||
Datenbank ID: <Code>{user?.id}</Code>
|
User ID: <Code>{user?.id}</Code>
|
||||||
|
|
||||||
<br/>
|
<br/>
|
||||||
|
|
||||||
REALM: <Code>{user?.REALM}</Code>
|
REALM: <Code>{user?.REALM}</Code>
|
||||||
|
|
||||||
{user?.objectGUID && <>
|
{user?.objectGUID && <>
|
||||||
|
@ -50,43 +103,25 @@ export default function UserMenuModal() {
|
||||||
</ShowDebug>
|
</ShowDebug>
|
||||||
|
|
||||||
<div className={classes.row}>
|
<div className={classes.row}>
|
||||||
<ThemeIcon
|
<ThemeIcon variant={"transparent"}>
|
||||||
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"}
|
|
||||||
>
|
|
||||||
<IconAt/>
|
<IconAt/>
|
||||||
</ThemeIcon>
|
</ThemeIcon>
|
||||||
|
|
||||||
<Tooltip label={`Dein Email ist ${user?.emailVisibility ? "sichtbar" : "nicht sichtbar"}`}>
|
<Tooltip
|
||||||
<Text>
|
label={`Dein Email ist ${user?.emailVisibility ? "für niemanden sichtbar" : "für eingeloggte Personen sichtbar"}`}
|
||||||
|
>
|
||||||
|
<Text size={"sm"}>
|
||||||
{user?.email}
|
{user?.email}
|
||||||
</Text>
|
</Text>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={classes.row}>
|
<div className={classes.row}>
|
||||||
<ThemeIcon
|
<ThemeIcon variant={"transparent"}>
|
||||||
variant={"transparent"}
|
|
||||||
size={"xl"}
|
|
||||||
>
|
|
||||||
<IconCalendar/>
|
<IconCalendar/>
|
||||||
</ThemeIcon>
|
</ThemeIcon>
|
||||||
|
|
||||||
<Text>
|
<Text size={"sm"}>
|
||||||
{user?.accountExpires ? (
|
{user?.accountExpires ? (
|
||||||
|
|
||||||
new Date(user?.accountExpires).getTime() > Date.now() ? (
|
new Date(user?.accountExpires).getTime() > Date.now() ? (
|
||||||
|
@ -105,7 +140,6 @@ export default function UserMenuModal() {
|
||||||
<ThemeIcon
|
<ThemeIcon
|
||||||
variant={"transparent"}
|
variant={"transparent"}
|
||||||
color={"green"}
|
color={"green"}
|
||||||
size={"xl"}
|
|
||||||
>
|
>
|
||||||
<IconServer/>
|
<IconServer/>
|
||||||
</ThemeIcon>
|
</ThemeIcon>
|
||||||
|
@ -113,13 +147,12 @@ export default function UserMenuModal() {
|
||||||
<ThemeIcon
|
<ThemeIcon
|
||||||
variant={"transparent"}
|
variant={"transparent"}
|
||||||
color={"red"}
|
color={"red"}
|
||||||
size={"xl"}
|
|
||||||
>
|
>
|
||||||
<IconServerOff/>
|
<IconServerOff/>
|
||||||
</ThemeIcon>
|
</ThemeIcon>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Text>
|
<Text size={"sm"}>
|
||||||
{apiIsHealthy ? "Das Backend ist erreichbar" : "Das Backend ist nicht erreichbar"}
|
{apiIsHealthy ? "Das Backend ist erreichbar" : "Das Backend ist nicht erreichbar"}
|
||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
|
@ -147,6 +180,7 @@ export default function UserMenuModal() {
|
||||||
<Switch
|
<Switch
|
||||||
checked={showDebug}
|
checked={showDebug}
|
||||||
onChange={toggleShowDebug}
|
onChange={toggleShowDebug}
|
||||||
|
color={"orange"}
|
||||||
label={"Debug Modus aktivieren"}
|
label={"Debug Modus aktivieren"}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
@ -159,9 +193,47 @@ export default function UserMenuModal() {
|
||||||
label={"Account"}
|
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" && <>
|
{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"}>
|
<Tooltip label={"Email ändern"}>
|
||||||
<ActionIcon
|
<ActionIcon
|
||||||
variant={"transparent"}
|
variant={"transparent"}
|
||||||
|
@ -189,18 +261,20 @@ export default function UserMenuModal() {
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</>}
|
</>}
|
||||||
|
|
||||||
<Tooltip label={"Anmeldedaten neu laden"}>
|
{showDebug &&
|
||||||
<ActionIcon
|
<Tooltip color={"orange"} label={"Debug: Anmeldedaten neu laden"}>
|
||||||
variant={"transparent"}
|
<ActionIcon
|
||||||
color={"orange"}
|
variant={"transparent"}
|
||||||
aria-label={"logout"}
|
color={"orange"}
|
||||||
onClick={() => {
|
aria-label={"logout"}
|
||||||
refreshUser()
|
onClick={() => {
|
||||||
}}
|
refreshUser()
|
||||||
>
|
}}
|
||||||
<IconRefresh/>
|
>
|
||||||
</ActionIcon>
|
<IconRefresh/>
|
||||||
</Tooltip>
|
</ActionIcon>
|
||||||
|
</Tooltip>
|
||||||
|
}
|
||||||
|
|
||||||
<Tooltip label={"Ausloggen"}>
|
<Tooltip label={"Ausloggen"}>
|
||||||
<ActionIcon
|
<ActionIcon
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import {PB_BASE_URL} from "../../config.ts";
|
import {PB_BASE_URL} from "../../config.ts";
|
||||||
|
import {useLocalStorage} from "@mantine/hooks";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This function creates a query string from an object.
|
* This function creates a query string from an object.
|
||||||
|
@ -31,15 +32,51 @@ export type ErrorCorrectionLevel = "L" | "M" | "Q" | "H"
|
||||||
*/
|
*/
|
||||||
export const createQRCodeUrl = (options: {
|
export const createQRCodeUrl = (options: {
|
||||||
data: string,
|
data: string,
|
||||||
ecc: ErrorCorrectionLevel,
|
ecc?: ErrorCorrectionLevel,
|
||||||
scale: number,
|
scale?: number,
|
||||||
border: number,
|
border?: number,
|
||||||
color: string,
|
color?: string,
|
||||||
colorBackground: string
|
colorBackground?: string
|
||||||
}): string => {
|
}): string => {
|
||||||
return `${PB_BASE_URL}/api/qr/v1?${createQueryParams(options)}`
|
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[] => {
|
export const flattenReducer = <T>(acc: T[], val: T | T[]): T[] => {
|
||||||
if (Array.isArray(val)) {
|
if (Array.isArray(val)) {
|
||||||
return [...acc, ...val]
|
return [...acc, ...val]
|
||||||
|
|
|
@ -5,6 +5,7 @@ import EventView from "./s/EventView.tsx";
|
||||||
import EventNavigate from "@/pages/events/EventNavigate.tsx";
|
import EventNavigate from "@/pages/events/EventNavigate.tsx";
|
||||||
import EditEventRouter from "@/pages/events/e/:eventId/EditEventRouter.tsx";
|
import EditEventRouter from "@/pages/events/e/:eventId/EditEventRouter.tsx";
|
||||||
import UserEntries from "@/pages/events/entries/UserEntries.tsx";
|
import UserEntries from "@/pages/events/entries/UserEntries.tsx";
|
||||||
|
import StatusEditor from "@/pages/events/StatusEditor/StatusEditor.tsx";
|
||||||
|
|
||||||
|
|
||||||
export default function EventsRouter() {
|
export default function EventsRouter() {
|
||||||
|
@ -14,6 +15,7 @@ export default function EventsRouter() {
|
||||||
<Route path={":eventId"} element={<EventNavigate/>}/>
|
<Route path={":eventId"} element={<EventNavigate/>}/>
|
||||||
<Route path={"s/:eventId/*"} element={<EventView/>}/>
|
<Route path={"s/:eventId/*"} element={<EventView/>}/>
|
||||||
<Route path={"e/:eventId/*"} element={<EditEventRouter/>}/>
|
<Route path={"e/:eventId/*"} element={<EditEventRouter/>}/>
|
||||||
|
<Route path={"e/:eventId/lists/status/*"} element={<StatusEditor/>}/>
|
||||||
<Route path={"entries"} element={<UserEntries/>}/>
|
<Route path={"entries"} element={<UserEntries/>}/>
|
||||||
<Route path={"*"} element={<NotFound/>}/>
|
<Route path={"*"} element={<NotFound/>}/>
|
||||||
</Routes>
|
</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}/>
|
<EventFavourites event={event}/>
|
||||||
</div>
|
</div>
|
||||||
</Grid.Col>
|
</Grid.Col>
|
||||||
|
|
||||||
<Grid.Col span={{base: 12, sm: 9}} className={"stack"}>
|
<Grid.Col span={{base: 12, sm: 9}} className={"stack"}>
|
||||||
<Group gap={"sm"} align={"center"}>
|
<Group gap={"sm"} align={"center"}>
|
||||||
<NavLink to={`/events/e/${eventId}/description`} replace className={classes.navLink}>
|
<NavLink to={`/events/e/${eventId}/description`} replace className={classes.navLink}>
|
||||||
|
|
|
@ -19,15 +19,20 @@ const viewNav = [
|
||||||
{
|
{
|
||||||
label: "Listenaktionen",
|
label: "Listenaktionen",
|
||||||
children: [
|
children: [
|
||||||
{
|
|
||||||
title: "Listen und Slots",
|
|
||||||
icon: <IconList size={16}/>,
|
|
||||||
to: "overview"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
title: "Anmeldungen",
|
title: "Anmeldungen",
|
||||||
icon: <IconUserSearch size={16}/>,
|
icon: <IconUserSearch size={16}/>,
|
||||||
to: "search"
|
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}'`]
|
const filter: string[] = [`event='${event.id}'`]
|
||||||
|
|
||||||
// filter for user by name
|
// 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
|
// filter for lists
|
||||||
filterValues.selectedLists.length > 0 && filter.push(`(${filterValues.selectedLists.map(l => `eventList='${l.id}'`).join(" || ")})`)
|
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,
|
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 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) ?? []
|
const entries = query.data?.pages.flatMap(p => p.items) ?? []
|
||||||
|
|
|
@ -1,6 +1,24 @@
|
||||||
import {EventListSlotEntriesWithUserModel} from "@/models/EventTypes.ts";
|
import {EventListSlotEntriesWithUserModel} from "@/models/EventTypes.ts";
|
||||||
import {ActionIcon, Collapse, Group, Text, ThemeIcon, Tooltip} from "@mantine/core";
|
import {
|
||||||
import {IconChevronDown, IconChevronRight, IconConfetti, IconForms, IconList, IconTrash} from "@tabler/icons-react";
|
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";
|
import TextWithIcon from "@/components/layout/TextWithIcon";
|
||||||
|
|
||||||
|
|
||||||
|
@ -19,6 +37,9 @@ import {
|
||||||
EntryQuestionAndStatusData
|
EntryQuestionAndStatusData
|
||||||
} from "@/pages/events/e/:eventId/EventLists/EventListComponents/EntryQuestionAndStatusData.tsx";
|
} from "@/pages/events/e/:eventId/EventLists/EventListComponents/EntryQuestionAndStatusData.tsx";
|
||||||
import {Link} from "react-router-dom";
|
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}: {
|
export default function UserEntryRow({entry, refetch}: {
|
||||||
|
@ -29,6 +50,8 @@ export default function UserEntryRow({entry, refetch}: {
|
||||||
|
|
||||||
const [expanded, expandedHandler] = useDisclosure(false)
|
const [expanded, expandedHandler] = useDisclosure(false)
|
||||||
|
|
||||||
|
const [showQrCode, showQrCodeHandler] = useDisclosure(false)
|
||||||
|
|
||||||
const [showEditFormModal, showEditFormModalHandler] = useDisclosure(false)
|
const [showEditFormModal, showEditFormModalHandler] = useDisclosure(false)
|
||||||
|
|
||||||
const delta = humanDeltaFromNow(entry.slotStartDate, entry.slotEndDate)
|
const delta = humanDeltaFromNow(entry.slotStartDate, entry.slotEndDate)
|
||||||
|
@ -49,6 +72,15 @@ export default function UserEntryRow({entry, refetch}: {
|
||||||
onConfirm: () => deleteEntryMutation.mutate()
|
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}>
|
return <div className={classes.container}>
|
||||||
|
|
||||||
<ConfirmModal/>
|
<ConfirmModal/>
|
||||||
|
@ -60,6 +92,13 @@ export default function UserEntryRow({entry, refetch}: {
|
||||||
entry={entry}
|
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}>
|
<div className={classes.row}>
|
||||||
<Tooltip label={expanded ? "Details verbergen" : "Details anzeigen"} withArrow>
|
<Tooltip label={expanded ? "Details verbergen" : "Details anzeigen"} withArrow>
|
||||||
<ActionIcon onClick={expandedHandler.toggle} variant={"light"} size={"xs"}>
|
<ActionIcon onClick={expandedHandler.toggle} variant={"light"} size={"xs"}>
|
||||||
|
@ -81,7 +120,7 @@ export default function UserEntryRow({entry, refetch}: {
|
||||||
<IconList/>
|
<IconList/>
|
||||||
</ThemeIcon>
|
</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>
|
||||||
</TextWithIcon>
|
</TextWithIcon>
|
||||||
|
@ -95,6 +134,16 @@ export default function UserEntryRow({entry, refetch}: {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Group gap={"xs"}>
|
<Group gap={"xs"}>
|
||||||
|
|
||||||
|
<ActionIcon
|
||||||
|
variant={"light"}
|
||||||
|
size={"xs"}
|
||||||
|
onClick={showQrCodeHandler.toggle}
|
||||||
|
aria-label={"show qr code"}
|
||||||
|
>
|
||||||
|
<IconQrcode/>
|
||||||
|
</ActionIcon>
|
||||||
|
|
||||||
<Tooltip
|
<Tooltip
|
||||||
label={delta.delta === "PAST" ? "Vergangene Anmeldungen können nicht bearbeiten werden" : "Eintrag bearbeiten"}
|
label={delta.delta === "PAST" ? "Vergangene Anmeldungen können nicht bearbeiten werden" : "Eintrag bearbeiten"}
|
||||||
withArrow>
|
withArrow>
|
||||||
|
|
|
@ -6,6 +6,7 @@ import {NavLink} from "react-router-dom";
|
||||||
import Announcements from "@/pages/chat/components/Announcements.tsx";
|
import Announcements from "@/pages/chat/components/Announcements.tsx";
|
||||||
import classes from './index.module.css'
|
import classes from './index.module.css'
|
||||||
import {ReactNode} from "react";
|
import {ReactNode} from "react";
|
||||||
|
import {getUserName} from "@/components/users/modals/util.tsx";
|
||||||
|
|
||||||
const NavButtons = ({buttons}: {
|
const NavButtons = ({buttons}: {
|
||||||
buttons: { title: string, icon: ReactNode, to: string }[]
|
buttons: { title: string, icon: ReactNode, to: string }[]
|
||||||
|
@ -44,7 +45,7 @@ export default function HomePage() {
|
||||||
</ThemeIcon>
|
</ThemeIcon>
|
||||||
Hallo
|
Hallo
|
||||||
|
|
||||||
{user && `, ${user.username}`}
|
{user && `, ${getUserName(user)}`}
|
||||||
</Title>
|
</Title>
|
||||||
|
|
||||||
{!user && <>
|
{!user && <>
|
||||||
|
|
|
@ -67,10 +67,12 @@
|
||||||
|
|
||||||
a {
|
a {
|
||||||
text-decoration: unset; /* Removes default underline */
|
text-decoration: unset; /* Removes default underline */
|
||||||
|
color: revert;
|
||||||
}
|
}
|
||||||
|
|
||||||
a:hover, a:active, a:visited {
|
a:hover, a:active, a:visited {
|
||||||
text-decoration: unset; /* Removes default underline */
|
text-decoration: unset; /* Removes default underline */
|
||||||
|
color: unset;
|
||||||
}
|
}
|
||||||
|
|
||||||
.section-icon {
|
.section-icon {
|
||||||
|
|
45
yarn.lock
45
yarn.lock
|
@ -989,6 +989,16 @@
|
||||||
dependencies:
|
dependencies:
|
||||||
"@types/ms" "*"
|
"@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":
|
"@types/estree-jsx@^1.0.0":
|
||||||
version "1.0.5"
|
version "1.0.5"
|
||||||
resolved "https://registry.yarnpkg.com/@types/estree-jsx/-/estree-jsx-1.0.5.tgz#858a88ea20f34fe65111f005a689fa1ebf70dc18"
|
resolved "https://registry.yarnpkg.com/@types/estree-jsx/-/estree-jsx-1.0.5.tgz#858a88ea20f34fe65111f005a689fa1ebf70dc18"
|
||||||
|
@ -1217,6 +1227,14 @@
|
||||||
dependencies:
|
dependencies:
|
||||||
"@swc/core" "^1.3.85"
|
"@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:
|
acorn-jsx@^5.3.2:
|
||||||
version "5.3.2"
|
version "5.3.2"
|
||||||
resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937"
|
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"
|
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee"
|
||||||
integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==
|
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:
|
binary-extensions@^2.0.0:
|
||||||
version "2.3.0"
|
version "2.3.0"
|
||||||
resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.3.0.tgz#f6e14a97858d327252200242d4ccfe522c445522"
|
resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.3.0.tgz#f6e14a97858d327252200242d4ccfe522c445522"
|
||||||
|
@ -3512,6 +3538,11 @@ scheduler@^0.23.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
loose-envify "^1.1.0"
|
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:
|
semver@^6.3.1:
|
||||||
version "6.3.1"
|
version "6.3.1"
|
||||||
resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4"
|
resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4"
|
||||||
|
@ -3924,6 +3955,13 @@ warning@^4.0.3:
|
||||||
dependencies:
|
dependencies:
|
||||||
loose-envify "^1.0.0"
|
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:
|
which@^2.0.1:
|
||||||
version "2.0.2"
|
version "2.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1"
|
resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1"
|
||||||
|
@ -3960,3 +3998,10 @@ zwitch@^2.0.0:
|
||||||
version "2.0.4"
|
version "2.0.4"
|
||||||
resolved "https://registry.yarnpkg.com/zwitch/-/zwitch-2.0.4.tgz#c827d4b0acb76fc3e685a4c6ec2902d51070e9d7"
|
resolved "https://registry.yarnpkg.com/zwitch/-/zwitch-2.0.4.tgz#c827d4b0acb76fc3e685a4c6ec2902d51070e9d7"
|
||||||
integrity sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==
|
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