feat(app): added email send support
Build and Push Docker image / build-and-push (push) Has been cancelled
Details
Build and Push Docker image / build-and-push (push) Has been cancelled
Details
removed the in-app chat and added email send support in its place
This commit is contained in:
parent
3895b86515
commit
fc1103b61a
|
@ -5,8 +5,8 @@ import Layout from "@/components/layout";
|
|||
import QRCodeGenerator from "./pages/util/qr/index.page.tsx";
|
||||
import EventsRouter from "./pages/events/EventsRouter.tsx";
|
||||
import LegalPage from "@/pages/LegalPage.tsx";
|
||||
import ChatRouter from "@/pages/chat/ChatRouter.tsx";
|
||||
import DebugPage from "@/pages/test/DebugPage.tsx";
|
||||
import EmailRouter from "@/pages/email/EmailRouter.tsx";
|
||||
|
||||
const router = createBrowserRouter([
|
||||
{
|
||||
|
@ -26,8 +26,8 @@ const router = createBrowserRouter([
|
|||
element: <EventsRouter/>,
|
||||
},
|
||||
{
|
||||
path: "chat/*",
|
||||
element: <ChatRouter/>,
|
||||
path: "email/*",
|
||||
element: <EmailRouter/>,
|
||||
},
|
||||
{
|
||||
path: "debug",
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
.header {
|
||||
/*noinspection CssInvalidFunction*/
|
||||
background-color: light-dark(var(--mantine-color-blue-1), var(--mantine-color-dark-9));
|
||||
padding: var(--padding);
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.container {
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.inner {
|
||||
padding: var(--padding);
|
||||
height: 100%;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.editorContainer {
|
||||
flex-grow: 1;
|
||||
height: 50vh;
|
||||
}
|
|
@ -0,0 +1,244 @@
|
|||
import {ActionIcon, Alert, Button, Group, Modal, ModalProps, TextInput, Tooltip} from "@mantine/core";
|
||||
import {
|
||||
IconArrowsMaximize,
|
||||
IconArrowsMinimize,
|
||||
IconInfoSquareRounded,
|
||||
IconMailCheck,
|
||||
IconMailQuestion,
|
||||
IconMailStar,
|
||||
IconMailUp,
|
||||
IconX
|
||||
} from "@tabler/icons-react";
|
||||
import UserInput from "@/components/users/UserInput.tsx";
|
||||
import {hasLength, isNotEmpty, useForm} from "@mantine/form";
|
||||
import {UserModel} from "@/models/AuthTypes.ts";
|
||||
import {PocketBaseErrorAlert, usePB} from "@/lib/pocketbase.tsx";
|
||||
import TextEditor from "@/components/input/Editor";
|
||||
import {useMutation} from "@tanstack/react-query"
|
||||
import classes from "./index.module.css"
|
||||
import {useDisclosure} from "@mantine/hooks";
|
||||
import ShowHelp from "@/components/ShowHelp.tsx";
|
||||
import {useEffect, useState} from "react";
|
||||
import {showSuccessNotification} from "@/components/util.tsx";
|
||||
|
||||
|
||||
/**
|
||||
* This component provides an email modal that can be used to send emails.
|
||||
* It returns a function to toggle the modal and the modal component itself.
|
||||
* @param onSuccess - the function to call when the email was send (optional)
|
||||
* @param onCancel - the function to call when the user cancels (optional)
|
||||
* @param recipients - set the recipients of the email (optional)
|
||||
* @param subject - set the subject of the email (optional)
|
||||
* @param content - set the content of the email (optional)
|
||||
* @param disableUserInput - disable the user input (optional)
|
||||
* @param props - additional props for the modal
|
||||
*/
|
||||
export const EmailModal = ({onSuccess, onCancel, recipients, subject, content, disableUserInput, ...props}: {
|
||||
description?: string,
|
||||
onSuccess?: () => void,
|
||||
onCancel?: () => void,
|
||||
recipients?: UserModel[],
|
||||
subject?: string,
|
||||
content?: string,
|
||||
disableUserInput?: boolean
|
||||
} & ModalProps) => {
|
||||
|
||||
const {user, pb} = usePB()
|
||||
|
||||
const [fullscreen, fullscreenHandler] = useDisclosure(false)
|
||||
|
||||
const formValues = useForm({
|
||||
initialValues: {
|
||||
recipients: recipients ?? [] as UserModel[],
|
||||
subject: "",
|
||||
content: ""
|
||||
},
|
||||
validate: {
|
||||
recipients: hasLength({min: 1}, "Bitte wähle mindestens eine Empfängerin aus."),
|
||||
subject: isNotEmpty("Bitte gib einen Betreff ein."),
|
||||
content: isNotEmpty("Bitte gib einen Inhalt ein.")
|
||||
}
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
formValues.setFieldValue("recipients", recipients ?? [])
|
||||
}, [recipients])
|
||||
|
||||
useEffect(() => {
|
||||
formValues.setFieldValue("subject", subject ?? "")
|
||||
}, [subject])
|
||||
|
||||
useEffect(() => {
|
||||
formValues.setFieldValue("content", content ?? "")
|
||||
}, [content])
|
||||
|
||||
const [testEmailWasSent, setTestEmailWasSent] = useState(false)
|
||||
const sendTestEmailMutation = useMutation({
|
||||
mutationFn: async () => {
|
||||
await pb.collection("emails").create({
|
||||
sender: user?.id,
|
||||
recipients: user?.id,
|
||||
subject: formValues.values.subject.trim() + " (Test Email)",
|
||||
content: formValues.values.content
|
||||
})
|
||||
},
|
||||
onSuccess: () => {
|
||||
setTestEmailWasSent(true)
|
||||
setTimeout(() => {
|
||||
setTestEmailWasSent(false)
|
||||
}, 1000)
|
||||
showSuccessNotification("Test Email wurde gesendet.")
|
||||
}
|
||||
})
|
||||
|
||||
const sendEmailMutation = useMutation({
|
||||
mutationFn: async () => {
|
||||
await pb.collection("emails").create({
|
||||
sender: user?.id,
|
||||
recipients: formValues.values.recipients.map(r => r.id),
|
||||
subject: formValues.values.subject.trim(),
|
||||
content: formValues.values.content
|
||||
})
|
||||
},
|
||||
onSuccess: () => {
|
||||
formValues.reset()
|
||||
props.onClose()
|
||||
onSuccess?.()
|
||||
showSuccessNotification(
|
||||
"Du kannst deine gesendeten Emails auf der Email Seite einsehen."
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
if (!user) return <>
|
||||
<Modal
|
||||
size={"sm"}
|
||||
{...props}
|
||||
title={"Bitte logge dich ein"}
|
||||
>
|
||||
<Alert variant="light" color="blue" icon={<IconMailQuestion/>}>
|
||||
Bitte logge dich ein, um eine Email zu senden.
|
||||
</Alert>
|
||||
</Modal>
|
||||
</>
|
||||
|
||||
return <>
|
||||
<Modal
|
||||
size={"xl"}
|
||||
{...props}
|
||||
styles={{
|
||||
body: {
|
||||
padding: 0,
|
||||
},
|
||||
content: {
|
||||
marginTop: "auto",
|
||||
borderBottomRightRadius: 0,
|
||||
borderBottomLeftRadius: 0,
|
||||
}
|
||||
}}
|
||||
yOffset={0}
|
||||
transitionProps={{transition: 'slide-up'}}
|
||||
fullScreen={fullscreen}
|
||||
withCloseButton={false}
|
||||
>
|
||||
<div>
|
||||
<div className={classes.header}>
|
||||
<Button
|
||||
loading={sendEmailMutation.isPending}
|
||||
leftSection={<IconMailUp/>}
|
||||
color={"blue"}
|
||||
variant={"subtle"}
|
||||
onClick={() => {
|
||||
if (formValues.validate().hasErrors) return
|
||||
sendEmailMutation.mutate()
|
||||
}}
|
||||
>
|
||||
Email Senden
|
||||
</Button>
|
||||
|
||||
<Group>
|
||||
<Tooltip label={"Test Email an dich senden"}>
|
||||
<ActionIcon
|
||||
onClick={() => {
|
||||
if (formValues.validate().hasErrors) return
|
||||
sendTestEmailMutation.mutate()
|
||||
}}
|
||||
color={"gray"}
|
||||
variant={"subtle"}
|
||||
title={"Send test email"}
|
||||
>
|
||||
{
|
||||
testEmailWasSent ?
|
||||
<IconMailCheck color={"green"}/> :
|
||||
<IconMailStar/>
|
||||
}
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
|
||||
<ActionIcon
|
||||
onClick={() => {
|
||||
fullscreenHandler.toggle()
|
||||
}}
|
||||
color={"gray"}
|
||||
variant={"subtle"}
|
||||
title={"Fullscreen"}
|
||||
>
|
||||
{fullscreen ? <IconArrowsMinimize/> : <IconArrowsMaximize/>}
|
||||
</ActionIcon>
|
||||
|
||||
<ActionIcon
|
||||
onClick={() => {
|
||||
props.onClose()
|
||||
onCancel?.()
|
||||
}}
|
||||
color={"gray"}
|
||||
variant={"subtle"}
|
||||
title={"Close"}
|
||||
>
|
||||
<IconX/>
|
||||
</ActionIcon>
|
||||
</Group>
|
||||
</div>
|
||||
|
||||
<div className={`${classes.inner} stack`}>
|
||||
|
||||
<PocketBaseErrorAlert error={sendEmailMutation.error}/>
|
||||
<PocketBaseErrorAlert error={sendTestEmailMutation.error}/>
|
||||
|
||||
<ShowHelp>
|
||||
Die in deinem Account gespeicherte E-Mail-Adresse wird in der gesendeten Nachricht
|
||||
angezeigt, damit die Empfängerin weiß, von wem die Nachricht stammt.
|
||||
</ShowHelp>
|
||||
|
||||
<UserInput
|
||||
required
|
||||
disabled={disableUserInput}
|
||||
placeholder={"Empfängerinnen"}
|
||||
variant={"filled"}
|
||||
selectedRecords={formValues.values.recipients}
|
||||
setSelectedRecords={(val) => formValues.setFieldValue("recipients", val)}
|
||||
error={formValues.errors.recipients}
|
||||
/>
|
||||
|
||||
<TextInput
|
||||
leftSection={<IconInfoSquareRounded/>}
|
||||
variant={"filled"}
|
||||
placeholder={"Betreff"}
|
||||
required
|
||||
{...formValues.getInputProps("subject")}
|
||||
/>
|
||||
|
||||
<TextEditor
|
||||
noBorder
|
||||
minHeight={"40vh"}
|
||||
maxHeight={"50vh"}
|
||||
placeholder={"Inhalt der Email"}
|
||||
value={formValues.values.content}
|
||||
onChange={(value) => formValues.setFieldValue("content", value)}
|
||||
error={formValues.errors.content}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
</>
|
||||
}
|
|
@ -0,0 +1,71 @@
|
|||
import {ListResult} from "pocketbase";
|
||||
import {InfiniteData, UseInfiniteQueryResult} from "@tanstack/react-query";
|
||||
import {Modal, Progress, Text} from "@mantine/core";
|
||||
import {useEffect} from "react";
|
||||
|
||||
/**
|
||||
* A modal component that handles the infinite query loading process.
|
||||
* The modal will be opened when the start flag is set to true while the query is fetching data.
|
||||
* When the data fetching is completed, the onSuccess callback will be called.
|
||||
*
|
||||
* During fetching, the modal will show the progress of the fetching process.
|
||||
*
|
||||
* @param {Object} props - The properties object.
|
||||
* @param {boolean} props.start - A flag to start the loading process.
|
||||
* @param {Function} [props.onSuccess] - A callback function to be called when all pages are fetched successfully.
|
||||
* @param {Function} [props.onError] - A callback function to be called when an error occurs during fetching.
|
||||
* @param {UseInfiniteQueryResult<InfiniteData<ListResult, unknown>, Error>} props.query - The infinite query result object.
|
||||
*
|
||||
* @returns The modal component.
|
||||
*/
|
||||
export default function LoadInfinitQueryModal<T>({start, query, onSuccess, onError}: {
|
||||
start: boolean
|
||||
onSuccess?: () => void
|
||||
onError?: (error: Error) => void
|
||||
query: UseInfiniteQueryResult<InfiniteData<ListResult<T>, unknown>, Error>
|
||||
}) {
|
||||
|
||||
// Fetch all pages
|
||||
useEffect(() => {
|
||||
// Fetch next page if not fetching and hasNextPage
|
||||
if (start && query.hasNextPage && !query.isFetchingNextPage) {
|
||||
query.fetchNextPage().catch(onError)
|
||||
}
|
||||
|
||||
// Call onSuccess when all pages are fetched
|
||||
if (start && query.isFetched && !query.hasNextPage) {
|
||||
onSuccess?.()
|
||||
}
|
||||
}, [start, query, onSuccess])
|
||||
|
||||
const totalItems = query.data?.pages?.[0].totalItems || 0
|
||||
const fetchedItems = query.data?.pages?.reduce((acc, page) => acc + page.items.length, 0) || 0
|
||||
|
||||
const totalPages = query.data?.pages?.[0].totalPages || 0
|
||||
const fetchedPages = query.data?.pages?.length || 0
|
||||
|
||||
const progress = (fetchedPages / totalPages * 100) || 0
|
||||
const dataIsFetching = query.isFetching || query.isPending
|
||||
|
||||
return <Modal
|
||||
opened={start && progress < 100}
|
||||
onClose={() => null}
|
||||
size="xs"
|
||||
centered
|
||||
withCloseButton={false}
|
||||
>
|
||||
<div className={"stack"}>
|
||||
{dataIsFetching && <>
|
||||
<Text c={"dimmed"} size={"xs"}>
|
||||
Daten werden gesammelt ... {fetchedItems}/{totalItems} Einträge
|
||||
</Text>
|
||||
|
||||
<Progress.Root size="xl">
|
||||
<Progress.Section value={progress} animated>
|
||||
<Progress.Label>~{progress.toFixed(0)}%</Progress.Label>
|
||||
</Progress.Section>
|
||||
</Progress.Root>
|
||||
</>}
|
||||
</div>
|
||||
</Modal>
|
||||
}
|
|
@ -99,14 +99,17 @@ export default function FormFilter({schema, label, defaultValue, onChange}: {
|
|||
}
|
||||
})
|
||||
formValues.setValues(newValues)
|
||||
console.log("Schema changed", values, newValues)
|
||||
// eslint-disable-next-line
|
||||
}, [schema])
|
||||
|
||||
return <>
|
||||
<Popover width={300} position="bottom" withArrow shadow="md">
|
||||
<Popover.Target>
|
||||
<Button size={"xs"} leftSection={<IconFilterPlus size={16}/>}>
|
||||
<Button
|
||||
disabled={schema.fields.length === 0}
|
||||
size={"xs"}
|
||||
leftSection={<IconFilterPlus size={16}/>}
|
||||
>
|
||||
{label || "Nach Werten filter"}
|
||||
</Button>
|
||||
</Popover.Target>
|
||||
|
|
|
@ -63,6 +63,8 @@ const Toolbar = ({fullToolbar}: { fullToolbar: boolean, editor: Editor }) => (
|
|||
<RichTextEditor.Code/>
|
||||
</RichTextEditor.ControlsGroup>
|
||||
<RichTextEditor.ControlsGroup>
|
||||
<RichTextEditor.BulletList/>
|
||||
<RichTextEditor.OrderedList/>
|
||||
<RichTextEditor.Link/>
|
||||
<RichTextEditor.Unlink/>
|
||||
</RichTextEditor.ControlsGroup>
|
||||
|
@ -79,6 +81,9 @@ const Toolbar = ({fullToolbar}: { fullToolbar: boolean, editor: Editor }) => (
|
|||
* @param placeholder The placeholder text to show when the editor is empty.
|
||||
* @param fullToolbar Whether to show the full toolbar or not.
|
||||
* @param maxHeight The maximum height of the editor.
|
||||
* @param minHeight The minimum height of the editor.
|
||||
* @param disabled Whether the editor is disabled or not.
|
||||
* @param modEnter The callback to call when the user presses Mod+Enter.
|
||||
* @param hideToolbar Whether to hide the toolbar or not. If hidden a bubble menu will be shown instead.
|
||||
* @param noBorder shows no border if true
|
||||
* @param props The props to pass to the Mantine Input Wrapper component.
|
||||
|
@ -89,6 +94,7 @@ export default function TextEditor({
|
|||
placeholder,
|
||||
fullToolbar,
|
||||
maxHeight,
|
||||
minHeight,
|
||||
hideToolbar,
|
||||
noBorder,
|
||||
disabled,
|
||||
|
@ -100,6 +106,7 @@ export default function TextEditor({
|
|||
placeholder?: string;
|
||||
fullToolbar?: boolean;
|
||||
maxHeight?: number | string;
|
||||
minHeight?: number | string;
|
||||
hideToolbar?: boolean;
|
||||
noBorder?: boolean;
|
||||
disabled?: boolean;
|
||||
|
@ -161,7 +168,7 @@ export default function TextEditor({
|
|||
toolbar: classes.toolbar,
|
||||
}}
|
||||
>
|
||||
<RichTextEditorContent mah={maxHeight ?? "100px"}/>
|
||||
<RichTextEditorContent mih={minHeight} mah={maxHeight ?? "100px"}/>
|
||||
{hideToolbar ? <Bubble editor={editor}/> : <Toolbar editor={editor} fullToolbar={!!fullToolbar}/>}
|
||||
</Box>
|
||||
</Input.Wrapper>
|
||||
|
|
|
@ -1,6 +1,17 @@
|
|||
import {RecordModel} from "pocketbase";
|
||||
import {UseMutationResult} from "@tanstack/react-query";
|
||||
import {CheckIcon, Combobox, Group, Pill, PillsInput, PillsInputProps, Stack, Text, useCombobox} from "@mantine/core";
|
||||
import {
|
||||
CheckIcon,
|
||||
Combobox,
|
||||
Group,
|
||||
Pill,
|
||||
PillsInput,
|
||||
PillsInputProps,
|
||||
ScrollArea,
|
||||
Stack,
|
||||
Text,
|
||||
useCombobox
|
||||
} from "@mantine/core";
|
||||
import {useEffect, useState} from "react";
|
||||
|
||||
/*
|
||||
|
@ -17,7 +28,7 @@ export type GenericRecordSearchInputProps<T> = {
|
|||
selectedRecords: T[]
|
||||
setSelectedRecords: (records: T[]) => void
|
||||
placeholder?: string
|
||||
} & Pick<PillsInputProps, "label" | "description" | "leftSection" | "required" | "error">
|
||||
} & Pick<PillsInputProps, "label" | "description" | "leftSection" | "required" | "error" | "variant" | "disabled">
|
||||
|
||||
/**
|
||||
* RecordSearchInput is a generic component that can be used to create a searchable input field for selecting records from a database.
|
||||
|
@ -93,12 +104,16 @@ export default function RecordSearchInput<T extends RecordModel>(props: {
|
|||
leftSection={props.leftSection}
|
||||
required={props.required}
|
||||
error={props.error}
|
||||
variant={props.variant}
|
||||
disabled={props.disabled}
|
||||
>
|
||||
<ScrollArea.Autosize mah={100} scrollbarSize={8}>
|
||||
<Pill.Group>
|
||||
{
|
||||
props.selectedRecords.map((selectedRecord) => (
|
||||
props.selectedRecords.map((selectedRecord, index) => (
|
||||
<Pill
|
||||
key={selectedRecord.id}
|
||||
key={`${selectedRecord.id}-${index}`}
|
||||
disabled={props.disabled}
|
||||
withRemoveButton
|
||||
onRemove={() => handleValueRemove(selectedRecord.id)}
|
||||
>
|
||||
|
@ -109,6 +124,7 @@ export default function RecordSearchInput<T extends RecordModel>(props: {
|
|||
|
||||
<Combobox.EventsTarget>
|
||||
<PillsInput.Field
|
||||
disabled={props.disabled}
|
||||
onFocus={() => combobox.openDropdown()}
|
||||
onBlur={() => combobox.closeDropdown()}
|
||||
value={search}
|
||||
|
@ -127,6 +143,7 @@ export default function RecordSearchInput<T extends RecordModel>(props: {
|
|||
/>
|
||||
</Combobox.EventsTarget>
|
||||
</Pill.Group>
|
||||
</ScrollArea.Autosize>
|
||||
</PillsInput>
|
||||
</Combobox.DropdownTarget>
|
||||
|
||||
|
|
|
@ -6,10 +6,9 @@ import {
|
|||
IconConfetti,
|
||||
IconHome,
|
||||
IconList,
|
||||
IconMessageCircle,
|
||||
IconMailShare,
|
||||
IconQrcode,
|
||||
IconSectionSign,
|
||||
IconSpeakerphone
|
||||
IconSectionSign
|
||||
} from "@tabler/icons-react";
|
||||
import {useShowDebug} from "@/components/ShowDebug.tsx";
|
||||
|
||||
|
@ -25,16 +24,10 @@ const NavItems = [
|
|||
link: "/"
|
||||
},
|
||||
{
|
||||
title: "Nachrichten",
|
||||
icon: IconMessageCircle,
|
||||
description: "Nachrichten",
|
||||
link: "/chat"
|
||||
},
|
||||
{
|
||||
title: "Ankündigungen",
|
||||
icon: IconSpeakerphone,
|
||||
description: "Ankündigungen",
|
||||
link: "/chat/announcements"
|
||||
title: "Emails",
|
||||
icon: IconMailShare,
|
||||
description: "Emails",
|
||||
link: "/email"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
|
|
@ -4,7 +4,6 @@ import {ActionIcon, Image, Menu, ThemeIcon} from "@mantine/core";
|
|||
import {IconChevronDown, IconLogin, IconUserStar} from "@tabler/icons-react";
|
||||
import MenuItems from "./MenuItems.tsx";
|
||||
import {useLogin, useUserMenu} from "@/components/users/modals/hooks.ts";
|
||||
import ChatNavIcon from "@/pages/chat/components/ChatNavIcon.tsx";
|
||||
|
||||
export default function NavBar() {
|
||||
|
||||
|
@ -47,7 +46,6 @@ export default function NavBar() {
|
|||
<div className={classes.actionIcons}>
|
||||
{user ?
|
||||
<>
|
||||
<ChatNavIcon/>
|
||||
<ActionIcon
|
||||
variant={"transparent"}
|
||||
color={"gray"}
|
||||
|
|
|
@ -1,17 +1,17 @@
|
|||
import {useMutation} from "@tanstack/react-query";
|
||||
import {IconUsers} from "@tabler/icons-react";
|
||||
import {usePB} from "@/lib/pocketbase.tsx";
|
||||
import {UserModal} from "@/models/AuthTypes.ts";
|
||||
import {UserModel} from "@/models/AuthTypes.ts";
|
||||
import RecordSearchInput, {GenericRecordSearchInputProps} from "../input/RecordSearchInput.tsx";
|
||||
|
||||
|
||||
export default function UserInput(props: GenericRecordSearchInputProps<UserModal>) {
|
||||
export default function UserInput(props: GenericRecordSearchInputProps<UserModel>) {
|
||||
|
||||
const {pb} = usePB()
|
||||
|
||||
return (
|
||||
<RecordSearchInput
|
||||
<UserModal>
|
||||
<UserModel>
|
||||
recordToString={(user) => ({displayName: `${user.givenName} ${user.sn}`})}
|
||||
{...props}
|
||||
placeholder={props.placeholder || "Suche nach Personen..."}
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
import {UserModal} from "@/models/AuthTypes.ts";
|
||||
import {UserModel} from "@/models/AuthTypes.ts";
|
||||
import {List} from "@mantine/core";
|
||||
import {IconUser} from "@tabler/icons-react";
|
||||
import {usePB} from "@/lib/pocketbase.tsx";
|
||||
import {getUserName} from "@/components/users/modals/util.tsx";
|
||||
|
||||
export default function UsersDisplay({users}: { users: UserModal[] }) {
|
||||
export default function UsersDisplay({users}: { users: UserModel[] }) {
|
||||
|
||||
const {user} = usePB()
|
||||
|
||||
|
@ -12,7 +13,7 @@ export default function UsersDisplay({users}: { users: UserModal[] }) {
|
|||
{
|
||||
users.map((u) => (
|
||||
<List.Item key={u.id} fw={500} c={user && user.id == u.id ? "green" : ""}>
|
||||
{u.username}
|
||||
{getUserName(u)}
|
||||
</List.Item>
|
||||
))
|
||||
}
|
||||
|
|
|
@ -34,7 +34,6 @@ 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";
|
||||
import {CheckboxCard} from "@/components/input/CheckboxCard";
|
||||
import {useEffect} from "react";
|
||||
|
||||
export default function UserMenuModal() {
|
||||
|
@ -62,7 +61,6 @@ export default function UserMenuModal() {
|
|||
const values = {
|
||||
sn: user?.sn ?? "",
|
||||
givenName: user?.givenName ?? "",
|
||||
muteEmailNotifications: user?.muteEmailNotifications ?? false
|
||||
}
|
||||
formValues.setInitialValues(values)
|
||||
formValues.setValues(values)
|
||||
|
@ -197,15 +195,16 @@ export default function UserMenuModal() {
|
|||
sondern zeigt zusätzliche Informationen an.
|
||||
</ShowDebug>
|
||||
|
||||
<Divider
|
||||
label={"Account"}
|
||||
/>
|
||||
|
||||
{userHasNoName &&
|
||||
<Alert> Bitte vervollständige deinen Account, um alle Funktionen nutzen zu können. </Alert>}
|
||||
|
||||
{
|
||||
user?.REALM === "GUEST" && <>
|
||||
<Divider
|
||||
label={"Account"}
|
||||
/>
|
||||
|
||||
<PocketBaseErrorAlert error={mutation.error}/>
|
||||
|
||||
<TextInput
|
||||
|
@ -226,14 +225,6 @@ export default function UserMenuModal() {
|
|||
</>
|
||||
}
|
||||
|
||||
<CheckboxCard
|
||||
label={"Email Benachrichtigungen stummschalten"}
|
||||
description={"Du erhältst keine Benachrichtigungen per Email für neu Chatnachrichten. " +
|
||||
"Für Ankündigungen und wichtige Informationen bekommst du weiterhin Nachrichten."
|
||||
}
|
||||
{...formValues.getInputProps("muteEmailNotifications", {type: 'checkbox'})}
|
||||
/>
|
||||
|
||||
<Group justify={"center"}>
|
||||
<Tooltip label={"Account Einstellungen speichern"}>
|
||||
<ActionIcon
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import {UserModal} from "@/models/AuthTypes.ts";
|
||||
import {UserModel} from "@/models/AuthTypes.ts";
|
||||
import {Tooltip} from "@mantine/core";
|
||||
|
||||
/**
|
||||
|
@ -7,7 +7,7 @@ import {Tooltip} from "@mantine/core";
|
|||
* @param user
|
||||
*/
|
||||
// eslint-disable-next-line react-refresh/only-export-components
|
||||
export const getUserName = (user?: UserModal | null) => {
|
||||
export const getUserName = (user?: UserModel | null) => {
|
||||
if (!user) {
|
||||
return null
|
||||
}
|
||||
|
@ -24,7 +24,7 @@ export const getUserName = (user?: UserModal | null) => {
|
|||
* @param user
|
||||
* @constructor
|
||||
*/
|
||||
export const RenderUserName = ({user}: { user?: UserModal | null }) => {
|
||||
export const RenderUserName = ({user}: { user?: UserModel | null }) => {
|
||||
|
||||
if (!user) {
|
||||
return null
|
||||
|
|
|
@ -31,6 +31,16 @@ export const pprintDate = (date: string | Date | Dayjs): string => {
|
|||
return `${d.format('dd')} ${d.format('DD.MM.YY')}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Pretty print a time. The date is formatted as "HH:MM".
|
||||
* @param date - The date string to pretty print.
|
||||
* @return {string} The pretty printed time.
|
||||
*/
|
||||
export const pprintTime = (date: string | Date | Dayjs): string => {
|
||||
const d = dayjs(date)
|
||||
return `${d.format('HH:mm')}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Pretty print a date and time. The date is formatted as "HH:MM DAY DD.MM.YYYY".
|
||||
* Uses Dayjs
|
||||
|
|
|
@ -10,7 +10,7 @@ import ms from "ms";
|
|||
import {useQuery} from "@tanstack/react-query";
|
||||
import {TypedPocketBase} from "@/models";
|
||||
import {PB_BASE_URL, PB_STORAGE_KEY, PB_USER_COLLECTION} from "../../config.ts";
|
||||
import {UserModal} from "@/models/AuthTypes.ts";
|
||||
import {UserModel} from "@/models/AuthTypes.ts";
|
||||
import {Alert, List} from "@mantine/core";
|
||||
import {IconAlertTriangle} from "@tabler/icons-react";
|
||||
import {showSuccessNotification} from "@/components/util.tsx";
|
||||
|
@ -182,7 +182,7 @@ const PocketData = () => {
|
|||
ldapLogin,
|
||||
guestLogin,
|
||||
logout,
|
||||
user: pb.authStore.isValid ? user as UserModal | null : null,
|
||||
user: pb.authStore.isValid ? user as UserModel | null : null,
|
||||
pb,
|
||||
refreshUser: refreshUserQuery.refetch,
|
||||
useSubscription,
|
||||
|
|
|
@ -1,11 +1,10 @@
|
|||
import {RecordModel} from "pocketbase";
|
||||
|
||||
export type UserModal = {
|
||||
export type UserModel = {
|
||||
username: string;
|
||||
verified: boolean;
|
||||
email: string;
|
||||
emailVisibility: boolean;
|
||||
muteEmailNotifications: boolean;
|
||||
|
||||
sn: string | null;
|
||||
givenName: string | null;
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
import {UserModel} from "@/models/AuthTypes.ts";
|
||||
import {RecordModel} from "pocketbase";
|
||||
|
||||
export type EmailModel = {
|
||||
sender: string;
|
||||
recipients: string[];
|
||||
subject: string;
|
||||
content: string;
|
||||
meta: object | null;
|
||||
sentAt: string | null;
|
||||
sentTo: string[] | null;
|
||||
expand?: {
|
||||
sender: UserModel | null;
|
||||
recipients: UserModel[] | null;
|
||||
sentTo: UserModel[] | null;
|
||||
}
|
||||
} & RecordModel
|
|
@ -1,4 +1,4 @@
|
|||
import {UserModal} from "./AuthTypes.ts";
|
||||
import {UserModel} from "./AuthTypes.ts";
|
||||
import {RecordModel} from "pocketbase";
|
||||
import {FieldEntries} from "@/components/formUtil/FromInput/types.ts";
|
||||
import {FormSchema} from "@/components/formUtil/formBuilder/types.ts";
|
||||
|
@ -17,10 +17,8 @@ export type EventModel = {
|
|||
eventLinks: EventLink[];
|
||||
defaultEntryQuestionSchema: FormSchema | null;
|
||||
defaultEntryStatusSchema: FormSchema | null;
|
||||
privilegedLists: string[];
|
||||
expand?: {
|
||||
eventAdmins: UserModal[] | null;
|
||||
privilegedLists: EventListModel[] | null;
|
||||
eventAdmins: UserModel[] | null;
|
||||
}
|
||||
} & RecordModel
|
||||
|
||||
|
@ -47,7 +45,6 @@ export type EventListModel = {
|
|||
}
|
||||
} & RecordModel
|
||||
|
||||
|
||||
export type EventListSlotModel = {
|
||||
eventList: string;
|
||||
startDate: string;
|
||||
|
@ -71,7 +68,7 @@ export type EventListSlotEntryModel = {
|
|||
user: string | null
|
||||
expand?: {
|
||||
eventListsSlot: EventListSlotModel;
|
||||
user: UserModal | null;
|
||||
user: UserModel | null;
|
||||
}
|
||||
} & RecordModel
|
||||
|
||||
|
@ -90,7 +87,7 @@ export type EventListSlotEntriesWithUserModel =
|
|||
expand?: {
|
||||
event: EventModel;
|
||||
eventList: EventListModel;
|
||||
user: UserModal;
|
||||
user: UserModel;
|
||||
}
|
||||
}
|
||||
& Pick<EventModel, "defaultEntryQuestionSchema" | "defaultEntryStatusSchema">
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import {RecordModel} from "pocketbase";
|
||||
import {UserModal} from "@/models/AuthTypes.ts";
|
||||
import {UserModel} from "@/models/AuthTypes.ts";
|
||||
import {EventListModel} from "@/models/EventTypes.ts";
|
||||
|
||||
export type MessagesModel = {
|
||||
|
@ -20,8 +20,8 @@ export type MessagesModel = {
|
|||
} | null
|
||||
|
||||
expand: {
|
||||
sender: UserModal
|
||||
recipients: UserModal[]
|
||||
sender: UserModel
|
||||
recipients: UserModel[]
|
||||
eventList: EventListModel | null
|
||||
repliedTo: MessagesModel | null
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import PocketBase, {RecordModel, RecordService} from "pocketbase";
|
||||
import {LdapGroupModel, LdapSyncLogModel, UserModal} from "./AuthTypes.ts";
|
||||
import {LdapGroupModel, LdapSyncLogModel, UserModel} from "./AuthTypes.ts";
|
||||
import {
|
||||
EventListModel,
|
||||
EventListSlotEntriesWithUserModel,
|
||||
|
@ -9,6 +9,7 @@ import {
|
|||
EventModel
|
||||
} from "./EventTypes.ts";
|
||||
import {MessagesModel} from "@/models/MessageTypes.ts";
|
||||
import {EmailModel} from "@/models/EmailTypes.ts";
|
||||
|
||||
export type SettingsModel = {
|
||||
key: ['privacyPolicy', 'agb', 'stexGroup', 'stuveEventQuestions']
|
||||
|
@ -33,7 +34,7 @@ export interface TypedPocketBase extends PocketBase {
|
|||
|
||||
collection(idOrName: 'legalSettings'): RecordService<LegalSettingsModal>
|
||||
|
||||
collection(idOrName: 'users'): RecordService<UserModal>
|
||||
collection(idOrName: 'users'): RecordService<UserModel>
|
||||
|
||||
collection(idOrName: 'ldap_groups'): RecordService<LdapGroupModel>
|
||||
|
||||
|
@ -50,4 +51,6 @@ export interface TypedPocketBase extends PocketBase {
|
|||
collection(idOrName: 'eventListSlotsWithEntriesCount'): RecordService<EventListSlotsWithEntriesCountModel>
|
||||
|
||||
collection(idOrName: 'eventListSlotEntriesWithUser'): RecordService<EventListSlotEntriesWithUserModel>
|
||||
|
||||
collection(idOrName: 'emails'): RecordService<EmailModel>
|
||||
}
|
|
@ -1,127 +0,0 @@
|
|||
import {Button, Center, Divider, Loader, Stack, Text, TextInput, ThemeIcon, UnstyledButton} from "@mantine/core";
|
||||
import {NavLink} from "react-router-dom";
|
||||
import classes from "@/pages/chat/EventListMessagesList.module.css";
|
||||
import PBAvatar from "@/components/PBAvatar.tsx";
|
||||
import {IconArrowDown, IconEdit, IconListSearch, IconMoodPuzzled, IconSpeakerphone} from "@tabler/icons-react";
|
||||
import {usePB} from "@/lib/pocketbase.tsx";
|
||||
import {useDebouncedState} from "@mantine/hooks";
|
||||
import {useInfiniteQuery} from "@tanstack/react-query";
|
||||
import {EventListModel, EventModel} from "@/models/EventTypes.ts";
|
||||
|
||||
const ListMessagesLink = ({eventList, event}: { eventList?: EventListModel, event?: EventModel }) => {
|
||||
if (!eventList || !event) return null
|
||||
return <UnstyledButton component={NavLink} to={`/chat/${eventList.id}`} className={classes.listLink}>
|
||||
{
|
||||
({isActive}) => <>
|
||||
<PBAvatar model={eventList} name={eventList.name} img={eventList.img} size={"sm"}/>
|
||||
<div className={classes.listText} data-active={isActive}>
|
||||
{eventList.name} ({event.name})
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
</UnstyledButton>
|
||||
}
|
||||
|
||||
const AnnouncementsLink = () => {
|
||||
return <div className={classes.announcementLinkContainer}>
|
||||
<UnstyledButton component={NavLink} to={`/chat/announcements`} className={classes.announcementLink}>
|
||||
{
|
||||
({isActive}) => <>
|
||||
|
||||
<ThemeIcon variant="light" radius={"xl"} size="md" color="green">
|
||||
<IconSpeakerphone/>
|
||||
</ThemeIcon>
|
||||
|
||||
<div className={`${classes.listText} `} data-active={isActive}>
|
||||
Ankündigungen
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
</UnstyledButton>
|
||||
|
||||
<UnstyledButton component={NavLink} to={`/chat/send-announcements`} className={classes.announcementLink}>
|
||||
{
|
||||
({isActive}) => <>
|
||||
|
||||
<ThemeIcon variant="light" radius={"xl"} size="md" color="green">
|
||||
<IconEdit/>
|
||||
</ThemeIcon>
|
||||
|
||||
<div className={`${classes.listText} `} data-active={isActive}>
|
||||
Gesendete Ankündigungen
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
</UnstyledButton>
|
||||
</div>
|
||||
}
|
||||
|
||||
export default function EventListMessagesList() {
|
||||
const {user, pb} = usePB()
|
||||
|
||||
const [chatEntriesQuery, setChatEntriesQuery] = useDebouncedState("", 200)
|
||||
|
||||
const query = useInfiniteQuery({
|
||||
queryKey: ["chatEntries", chatEntriesQuery],
|
||||
queryFn: async ({pageParam}) => (
|
||||
await pb.collection("eventListSlotEntriesWithUser").getList(pageParam, 100, {
|
||||
filter: `user='${user?.id}'&&eventList.enableChat=true&&listName~'${chatEntriesQuery}'`,
|
||||
sort: "created",
|
||||
expand: "event,eventList"
|
||||
})
|
||||
),
|
||||
getNextPageParam: (lastPage) =>
|
||||
lastPage.page >= lastPage.totalPages ? undefined : lastPage.page + 1,
|
||||
initialPageParam: 1,
|
||||
enabled: !!user,
|
||||
})
|
||||
|
||||
const chatEntries = query.data?.pages.flatMap(t => t.items) || []
|
||||
|
||||
return <div className={`stack ${classes.container}`}>
|
||||
<AnnouncementsLink/>
|
||||
|
||||
<Divider label={"Chats"}/>
|
||||
|
||||
<TextInput
|
||||
leftSection={<IconListSearch/>}
|
||||
rightSection={query.isPending ? <Loader size={"xs"}/> : undefined}
|
||||
defaultValue={chatEntriesQuery}
|
||||
onChange={(e) => setChatEntriesQuery(e.currentTarget.value)}
|
||||
placeholder={"Nach Listen suchen..."}
|
||||
/>
|
||||
|
||||
{chatEntries.length === 0 ? <Stack gap={"xs"} align={"center"}>
|
||||
<ThemeIcon variant="transparent" size="xs" color="gray">
|
||||
<IconMoodPuzzled/>
|
||||
</ThemeIcon>
|
||||
|
||||
<Text size={"xs"} c={"dimmed"}>
|
||||
{chatEntriesQuery ? "Keine Listen gefunden" : "Keine Listen"}
|
||||
</Text>
|
||||
</Stack> : (
|
||||
<div className={`${classes.listsContainer} no-scrollbar`}>
|
||||
{chatEntries.map(entry => <ListMessagesLink
|
||||
key={entry.id}
|
||||
eventList={entry.expand?.eventList}
|
||||
event={entry.expand?.event}
|
||||
/>)}
|
||||
|
||||
{query.hasNextPage && (
|
||||
<Center p={"xs"}>
|
||||
<Button
|
||||
disabled={query.isFetchingNextPage || !query.hasNextPage}
|
||||
loading={query.isFetchingNextPage}
|
||||
variant={"transparent"}
|
||||
size={"compact-xs"}
|
||||
leftSection={<IconArrowDown/>}
|
||||
onClick={() => query.fetchNextPage()}
|
||||
>
|
||||
Mehr laden
|
||||
</Button>
|
||||
</Center>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
}
|
|
@ -1,5 +0,0 @@
|
|||
.backIcon {
|
||||
@media (min-width: 768px) {
|
||||
display: none;
|
||||
}
|
||||
}
|
|
@ -1,92 +0,0 @@
|
|||
import {Link, useParams} from "react-router-dom";
|
||||
import {useQuery} from "@tanstack/react-query";
|
||||
import {PocketBaseErrorAlert, usePB} from "@/lib/pocketbase.tsx";
|
||||
import {ActionIcon, Button, Center, Collapse, Group, Loader, Text} from "@mantine/core";
|
||||
import PBAvatar from "@/components/PBAvatar.tsx";
|
||||
import {IconArrowRight, IconChevronLeft, IconInfoCircle, IconInfoCircleFilled} from "@tabler/icons-react";
|
||||
import {useDisclosure} from "@mantine/hooks";
|
||||
import Messages from "@/pages/chat/components/Messages.tsx";
|
||||
import classes from './ListMessagesView.module.css';
|
||||
import InnerHtml from "@/components/InnerHtml";
|
||||
import {useEventRights} from "@/pages/events/util.ts";
|
||||
|
||||
export default function ListMessagesView() {
|
||||
const {listId} = useParams() as { listId: string }
|
||||
|
||||
const {pb} = usePB()
|
||||
|
||||
const [showListInfo, showListInfoHandler] = useDisclosure(false)
|
||||
|
||||
const query = useQuery({
|
||||
queryKey: ["eventListMessageView", listId],
|
||||
queryFn: async () => (
|
||||
await pb.collection("eventLists").getOne(listId, {expand: "event"})
|
||||
)
|
||||
})
|
||||
|
||||
const {canEditEvent} = useEventRights(query.data?.expand?.event)
|
||||
|
||||
if (query.isError) return (
|
||||
<PocketBaseErrorAlert error={query.error}/>
|
||||
)
|
||||
|
||||
if (query.isLoading || !query.data) return (
|
||||
<Center>
|
||||
<Loader/>
|
||||
</Center>
|
||||
)
|
||||
|
||||
const eventList = query.data
|
||||
|
||||
return <div className={"stack"}>
|
||||
<Group wrap={"nowrap"}>
|
||||
<ActionIcon
|
||||
component={Link}
|
||||
to={"/chat"}
|
||||
variant={"transparent"} color={"blue"}
|
||||
radius={"xl"}
|
||||
aria-label={"Back to Threads"}
|
||||
className={classes.backIcon}
|
||||
>
|
||||
<IconChevronLeft/>
|
||||
</ActionIcon>
|
||||
|
||||
<PBAvatar model={eventList} name={eventList.name} img={eventList.img} size={"lg"}/>
|
||||
<div>
|
||||
<Text size={"lg"} fw={600}>{eventList.name}</Text>
|
||||
<Text size={"xs"} c={"dimmed"}>{eventList.expand?.event.name}</Text>
|
||||
</div>
|
||||
<ActionIcon
|
||||
variant={"transparent"} color={"blue"}
|
||||
radius={"xl"} ms={"auto"}
|
||||
aria-label={"Thread Info"}
|
||||
onClick={showListInfoHandler.toggle}
|
||||
>
|
||||
{showListInfo ? <IconInfoCircleFilled/> : <IconInfoCircle/>}
|
||||
</ActionIcon>
|
||||
</Group>
|
||||
|
||||
<Collapse in={showListInfo}>
|
||||
<div className={"stack"}>
|
||||
<InnerHtml html={eventList.description ?? "Keine Beschreibung vorhanden"}/>
|
||||
{
|
||||
(canEditEvent || eventList.expand?.event.privilegedLists.includes(eventList.id))
|
||||
&&
|
||||
<Group>
|
||||
<Button
|
||||
rightSection={<IconArrowRight/>}
|
||||
variant={"light"} color={"blue"}
|
||||
size={"xs"}
|
||||
component={Link}
|
||||
to={`/events/e/${eventList.event}/lists/overview/${eventList.id}`}
|
||||
>
|
||||
Zur Liste
|
||||
</Button>
|
||||
</Group>
|
||||
}
|
||||
</div>
|
||||
</Collapse>
|
||||
|
||||
<Messages eventList={eventList}/>
|
||||
</div>
|
||||
}
|
|
@ -1,31 +0,0 @@
|
|||
.announcement {
|
||||
display: flex;
|
||||
position: relative;
|
||||
flex-direction: column;
|
||||
|
||||
background-color: var(--mantine-color-body);
|
||||
|
||||
padding: var(--padding);
|
||||
|
||||
border: var(--border);
|
||||
border-color: var(--mantine-primary-color-5);
|
||||
border-radius: var(--border-radius);
|
||||
|
||||
max-width: 100%;
|
||||
|
||||
word-break: break-all;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
.subject {
|
||||
font-size: var(--mantine-font-size-lg);
|
||||
font-weight: bold;
|
||||
color: var(--mantine-primary-color-5);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.subjectStack {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
|
@ -1,85 +0,0 @@
|
|||
import InnerHtml from "@/components/InnerHtml";
|
||||
import classes from './Announcement.module.css'
|
||||
import {Alert, Center, Group, List, Loader, Text, ThemeIcon, Tooltip} from "@mantine/core";
|
||||
import {IconSpeakerphone} from "@tabler/icons-react";
|
||||
import {MessagesModel} from "@/models/MessageTypes.ts";
|
||||
import {getUserName} from "@/components/users/modals/util.tsx";
|
||||
import {humanDeltaFromNow, pprintDate, pprintDateRange} from "@/lib/datetime.ts";
|
||||
import {useQuery} from "@tanstack/react-query";
|
||||
import {usePB} from "@/lib/pocketbase.tsx";
|
||||
import {Link} from "react-router-dom";
|
||||
|
||||
export default function Announcement({announcement}: {
|
||||
announcement: MessagesModel
|
||||
}) {
|
||||
const senderName = getUserName(announcement.expand.sender)
|
||||
const eventId = announcement.meta?.event
|
||||
const {pb, user} = usePB()
|
||||
const entriesQuery = useQuery({
|
||||
queryKey: ["eventListSlotEntries", "announcement", eventId],
|
||||
queryFn: async () => {
|
||||
return await pb.collection("eventListSlotEntriesWithUser").getList(1, 10, {
|
||||
filter: `event='${eventId}'&&user='${user!.id}'`,
|
||||
sort: "-created"
|
||||
})
|
||||
},
|
||||
enabled: !!eventId
|
||||
})
|
||||
|
||||
return <div className={classes.announcement}>
|
||||
<Group justify={"space-between"} wrap={"nowrap"} align={"top"} mb={"md"}>
|
||||
<div className={classes.subjectStack}>
|
||||
{announcement.subject && <div className={`${classes.subject} wrapWords`}>
|
||||
{announcement.subject}
|
||||
</div>}
|
||||
|
||||
<Text size={"xs"} c={"dimmed"} className={"wrapWords"}>
|
||||
von {senderName} • {pprintDate(announcement.created)} • {humanDeltaFromNow(announcement.created).message}
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
<Tooltip label={`Ankündigung von ${getUserName(announcement.expand.sender)}`} withArrow>
|
||||
<ThemeIcon
|
||||
className={classes.icon}
|
||||
variant={"transparent"} size={"sm"}
|
||||
>
|
||||
<IconSpeakerphone/>
|
||||
</ThemeIcon>
|
||||
</Tooltip>
|
||||
</Group>
|
||||
|
||||
{(eventId && entriesQuery.data?.totalItems !== 0) && (
|
||||
entriesQuery.isFetching ? <Center><Loader size={"xs"}/></Center> :
|
||||
<Alert mb="md">
|
||||
<Text>
|
||||
Du hast bei dem Event
|
||||
{" "}
|
||||
<Text td={"underline"} component={Link} to={`/events/e/${eventId}`}>{
|
||||
entriesQuery.data?.items[0].eventName
|
||||
}</Text>
|
||||
{" "}
|
||||
{
|
||||
entriesQuery.data?.totalItems === 1 ? "einen Eintrag" : `${entriesQuery.data?.totalItems} Einträge`
|
||||
}
|
||||
{"."}
|
||||
</Text>
|
||||
<List size={"sm"} c={"dimmed"}>
|
||||
{entriesQuery.data?.items.map((entry) => (
|
||||
<List.Item key={entry.id}>
|
||||
{entry.listName}
|
||||
{", Zeitslot "}
|
||||
{pprintDateRange(entry.slotStartDate, entry.slotEndDate)}
|
||||
{", "}
|
||||
{humanDeltaFromNow(entry.slotStartDate).message}
|
||||
</List.Item>
|
||||
))}
|
||||
</List>
|
||||
|
||||
<Text size={"sm"} td={"underline"} component={Link} to={"/events/entries"}>Hier</Text> kannst du
|
||||
alle deine Einträge einsehen und bearbeiten.
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<InnerHtml html={announcement.content}/>
|
||||
</div>
|
||||
}
|
|
@ -1,7 +0,0 @@
|
|||
.announcements {
|
||||
flex-grow: 1;
|
||||
overflow-y: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--gap);
|
||||
}
|
|
@ -1,67 +0,0 @@
|
|||
import {useInfiniteQuery} from "@tanstack/react-query";
|
||||
import {PocketBaseErrorAlert, usePB} from "@/lib/pocketbase.tsx";
|
||||
import {Button, Center, Loader, Text} from "@mantine/core";
|
||||
import classes from './Announcements.module.css'
|
||||
import {IconMessageCircleDown} from "@tabler/icons-react";
|
||||
import Announcement from "@/pages/chat/components/Announcement.tsx";
|
||||
|
||||
export default function Announcements() {
|
||||
|
||||
const {user, pb} = usePB()
|
||||
const query = useInfiniteQuery({
|
||||
queryKey: ["announcements"],
|
||||
queryFn: async ({pageParam}) => (
|
||||
await pb.collection("messages").getList(pageParam, 50, {
|
||||
filter: `isAnnouncement=true&&sender!='${user?.id}'`,
|
||||
sort: "-created",
|
||||
expand: "sender"
|
||||
})
|
||||
),
|
||||
getNextPageParam: (lastPage) =>
|
||||
lastPage.page >= lastPage.totalPages ? undefined : lastPage.page + 1,
|
||||
initialPageParam: 1,
|
||||
enabled: !!user,
|
||||
})
|
||||
|
||||
const announcements = query.data?.pages.flatMap(page => page.items) || []
|
||||
|
||||
if (query.isError) return (
|
||||
<PocketBaseErrorAlert error={query.error}/>
|
||||
)
|
||||
|
||||
if (query.isLoading || !query.data) return (
|
||||
<Center>
|
||||
<Loader/>
|
||||
</Center>
|
||||
)
|
||||
|
||||
return <div className={`stack `}>
|
||||
<div className={`scrollbar ${classes.announcements}`}>
|
||||
{announcements.map((announcement) => (
|
||||
<Announcement
|
||||
key={announcement.id}
|
||||
announcement={announcement}
|
||||
/>
|
||||
))}
|
||||
|
||||
{query.hasNextPage ? (
|
||||
<Center>
|
||||
<Button
|
||||
variant={"transparent"} color={"blue"}
|
||||
radius={"xl"}
|
||||
onClick={() => query.fetchNextPage()}
|
||||
leftSection={<IconMessageCircleDown/>}
|
||||
loading={query.isFetchingNextPage}
|
||||
>
|
||||
Mehr laden
|
||||
</Button>
|
||||
</Center>
|
||||
) : announcements.length === 0 ? <div className={classes.text}>
|
||||
<Text ta={"center"} size={"xs"} c={"dimmed"}>
|
||||
Noch keine Ankündigungen
|
||||
</Text>
|
||||
</div> : null
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
|
@ -1,96 +0,0 @@
|
|||
import {ActionIcon, Indicator} from "@mantine/core";
|
||||
import {Link, useMatch} from "react-router-dom";
|
||||
import {IconMessageCircle} from "@tabler/icons-react";
|
||||
import {usePB} from "@/lib/pocketbase.tsx";
|
||||
import {useEffect, useState} from "react";
|
||||
import {useTimeout} from "@mantine/hooks";
|
||||
import {MessagesModel} from "@/models/MessageTypes.ts";
|
||||
import {APP_NAME, APP_URL} from "../../../../config.ts";
|
||||
|
||||
/**
|
||||
* Parse HTML string and return text content
|
||||
* @param html - HTML string
|
||||
*/
|
||||
const parseHtmlString = (html: string): string => {
|
||||
const parser = new DOMParser();
|
||||
const doc = parser.parseFromString(html, 'text/html');
|
||||
return doc.body.textContent || '';
|
||||
}
|
||||
|
||||
const sendDesktopNotification = (body: string, url: string) => {
|
||||
if ("Notification" in window && Notification.permission === 'granted') {
|
||||
const notification = new Notification(APP_NAME, {
|
||||
body: body,
|
||||
icon: `${APP_URL}/stuve-logo.svg`,
|
||||
data: {url: url}
|
||||
})
|
||||
notification.onclick = () => {
|
||||
window.focus()
|
||||
window.open(notification.data.url);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default function ChatNavIcon() {
|
||||
|
||||
const [notificationUrl, setNotificationUrl] = useState<string | null>(null);
|
||||
const {start} = useTimeout(() => setNotificationUrl(null), 5000);
|
||||
|
||||
const {user, useSubscription} = usePB()
|
||||
|
||||
const match = useMatch("/chat/:listId")
|
||||
|
||||
useEffect(() => {
|
||||
// Request notification permission on component mount
|
||||
if ("Notification" in window && Notification.permission !== 'granted') {
|
||||
Notification.requestPermission().then(() => {
|
||||
sendDesktopNotification(
|
||||
"Willkommen bei StuVe! Du kannst jetzt Desktop Benachrichtigungen erhalten.",
|
||||
`${APP_URL}/chat`
|
||||
)
|
||||
})
|
||||
}
|
||||
}, [])
|
||||
|
||||
useSubscription<MessagesModel>({
|
||||
idOrName: "messages",
|
||||
topic: "*",
|
||||
callback: (event) => {
|
||||
if (event.action == "create") {
|
||||
// check if chat page is not already open and if sender is not the user
|
||||
if (match?.params.listId === event.record.eventList && event.record.sender === user?.id) {
|
||||
return
|
||||
}
|
||||
|
||||
const notificationUrl = `/chat/${event.record.isAnnouncement ? "announcements" : event.record.eventList}`
|
||||
|
||||
setNotificationUrl(notificationUrl)
|
||||
start()
|
||||
|
||||
sendDesktopNotification(
|
||||
parseHtmlString(event.record.content),
|
||||
`${APP_URL}${notificationUrl}`
|
||||
)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
if (!user) {
|
||||
return null
|
||||
}
|
||||
|
||||
return <>
|
||||
<Indicator inline processing disabled={notificationUrl === null} classNames={{
|
||||
root: "stack"
|
||||
}}>
|
||||
<ActionIcon
|
||||
component={Link}
|
||||
to={notificationUrl ?? "/chat"}
|
||||
variant={"transparent"}
|
||||
color={"gray"}
|
||||
>
|
||||
<IconMessageCircle/>
|
||||
</ActionIcon>
|
||||
</Indicator>
|
||||
</>
|
||||
}
|
|
@ -1,53 +0,0 @@
|
|||
.messageContainer {
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.messages {
|
||||
flex-grow: 1;
|
||||
overflow-y: auto;
|
||||
display: flex;
|
||||
flex-direction: column-reverse;
|
||||
}
|
||||
|
||||
.message {
|
||||
display: flex;
|
||||
position: relative;
|
||||
flex-direction: column;
|
||||
|
||||
background-color: var(--mantine-color-body);
|
||||
|
||||
padding: var(--padding);
|
||||
|
||||
border: var(--border);
|
||||
border-radius: var(--mantine-radius-xl);
|
||||
border-top-left-radius: var(--mantine-radius-sm);
|
||||
|
||||
margin: var(--gap);
|
||||
|
||||
max-width: fit-content;
|
||||
|
||||
word-break: break-all;
|
||||
overflow-wrap: break-word;
|
||||
|
||||
&[data-sender="true"] {
|
||||
align-self: flex-end;
|
||||
border-radius: var(--mantine-radius-xl);
|
||||
border-bottom-right-radius: var(--mantine-radius-sm);
|
||||
|
||||
& > .messageSender {
|
||||
color: var(--mantine-primary-color-4);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.messageSender {
|
||||
font-size: var(--mantine-font-size-xs);
|
||||
color: var(--mantine-color-dimmed)
|
||||
}
|
||||
|
||||
.text {
|
||||
height: 100vh;
|
||||
}
|
|
@ -1,154 +0,0 @@
|
|||
import {MessagesModel} from "@/models/MessageTypes.ts";
|
||||
import TextEditor from "@/components/input/Editor";
|
||||
import {useForm} from "@mantine/form";
|
||||
import {ActionIcon, Button, Center, Divider, Group, Text} from "@mantine/core";
|
||||
import {IconMessageCircleUp, IconSend} from "@tabler/icons-react";
|
||||
import classes from './Messages.module.css'
|
||||
import {useInfiniteQuery, useMutation} from "@tanstack/react-query";
|
||||
import {PocketBaseErrorAlert, usePB} from "@/lib/pocketbase.tsx";
|
||||
import InnerHtml from "@/components/InnerHtml";
|
||||
import {getUserName} from "@/components/users/modals/util.tsx";
|
||||
import {humanDeltaFromNow, pprintDateTime} from "@/lib/datetime.ts";
|
||||
import {EventListModel} from "@/models/EventTypes.ts";
|
||||
|
||||
export default function Messages({eventList}: {
|
||||
eventList: EventListModel
|
||||
}) {
|
||||
const {user, pb, useSubscription} = usePB()
|
||||
|
||||
const query = useInfiniteQuery({
|
||||
queryKey: ["messages", eventList],
|
||||
queryFn: async ({pageParam}) => (
|
||||
await pb.collection("messages").getList(pageParam, 100, {
|
||||
filter: `eventList='${eventList?.id}'`,
|
||||
sort: "-created",
|
||||
expand: "sender"
|
||||
})
|
||||
),
|
||||
getNextPageParam: (lastPage) =>
|
||||
lastPage.page >= lastPage.totalPages ? undefined : lastPage.page + 1,
|
||||
initialPageParam: 1,
|
||||
enabled: !!user,
|
||||
})
|
||||
|
||||
useSubscription<MessagesModel>({
|
||||
idOrName: "messages",
|
||||
topic: "*",
|
||||
callback: (event) => {
|
||||
if (event.action == "create" && event.record.eventList == eventList.id) {
|
||||
query.refetch()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const mutation = useMutation({
|
||||
mutationFn: async () => {
|
||||
await pb.collection("messages").create({
|
||||
...formValues.values,
|
||||
sender: user!.id,
|
||||
eventList: eventList.id,
|
||||
})
|
||||
},
|
||||
onSuccess: () => {
|
||||
formValues.reset()
|
||||
}
|
||||
})
|
||||
|
||||
const formValues = useForm({
|
||||
initialValues: {
|
||||
content: ""
|
||||
},
|
||||
validate: {
|
||||
content: (value) => {
|
||||
if (value.length < 1) {
|
||||
return "Bitte gib eine Nachricht ein"
|
||||
}
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
const messages = query.data?.pages.flatMap(page => page.items) || []
|
||||
|
||||
return <div className={`stack ${classes.messageContainer}`}>
|
||||
<PocketBaseErrorAlert error={query.error}/>
|
||||
|
||||
<div className={`${classes.messages} scrollbar`}>
|
||||
{messages.map((message) => (
|
||||
<div
|
||||
className={classes.message}
|
||||
key={message.id}
|
||||
data-sender={message.sender === user?.id}
|
||||
>
|
||||
|
||||
<div className={classes.messageSender}>
|
||||
{getUserName(message.expand?.sender)}
|
||||
</div>
|
||||
|
||||
<InnerHtml html={message.content}/>
|
||||
|
||||
{message.comment && <>
|
||||
<Divider mt={"sm"} mb={"sm"} label="Kommentar"/>
|
||||
<InnerHtml html={message.comment}/>
|
||||
</>}
|
||||
|
||||
<div className={classes.messageSender}>
|
||||
{humanDeltaFromNow(message.created).message} • {pprintDateTime(message.created)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{query.hasNextPage ? (
|
||||
<Center>
|
||||
<Button
|
||||
variant={"transparent"} color={"blue"}
|
||||
radius={"xl"}
|
||||
onClick={() => query.fetchNextPage()}
|
||||
leftSection={<IconMessageCircleUp/>}
|
||||
loading={query.isFetchingNextPage}
|
||||
>
|
||||
Mehr laden
|
||||
</Button>
|
||||
</Center>
|
||||
) : <div className={classes.text}>
|
||||
<Text ta={"center"} size={"xs"} c={"dimmed"}>
|
||||
{
|
||||
messages.length > 0 ?
|
||||
"Keine weiteren Nachrichten"
|
||||
: "Noch keine Nachrichten"
|
||||
}
|
||||
</Text>
|
||||
</div>}
|
||||
</div>
|
||||
|
||||
<PocketBaseErrorAlert error={mutation.error}/>
|
||||
|
||||
<form onSubmit={formValues.onSubmit(() => mutation.mutate())}>
|
||||
<Group wrap={"nowrap"} align={"center"}>
|
||||
<TextEditor
|
||||
placeholder={"Neue Nachricht ..."}
|
||||
style={{flexGrow: 1}}
|
||||
noBorder hideToolbar
|
||||
value={formValues.values.content}
|
||||
onChange={(value) => formValues.setFieldValue("content", value)}
|
||||
modEnter={() => {
|
||||
if (!formValues.validate().hasErrors) {
|
||||
mutation.mutate()
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<div className={"stack"}>
|
||||
<ActionIcon
|
||||
variant={"transparent"} color={"blue"}
|
||||
radius={"xl"} ms={"auto"}
|
||||
bg={"var(--mantine-color-body)"}
|
||||
size={"lg"}
|
||||
aria-label={"Send"}
|
||||
type={"submit"}
|
||||
>
|
||||
<IconSend size={16}/>
|
||||
</ActionIcon>
|
||||
</div>
|
||||
</Group>
|
||||
</form>
|
||||
</div>
|
||||
}
|
|
@ -1,67 +0,0 @@
|
|||
import {useInfiniteQuery} from "@tanstack/react-query";
|
||||
import {PocketBaseErrorAlert, usePB} from "@/lib/pocketbase.tsx";
|
||||
import {Button, Center, Loader, Text} from "@mantine/core";
|
||||
import classes from './Announcements.module.css'
|
||||
import {IconMessageCircleDown} from "@tabler/icons-react";
|
||||
import Announcement from "@/pages/chat/components/Announcement.tsx";
|
||||
|
||||
export default function SendAnnouncements() {
|
||||
|
||||
const {user, pb} = usePB()
|
||||
const query = useInfiniteQuery({
|
||||
queryKey: ["announcements"],
|
||||
queryFn: async ({pageParam}) => (
|
||||
await pb.collection("messages").getList(pageParam, 50, {
|
||||
filter: `isAnnouncement=true&&sender='${user?.id}'`,
|
||||
sort: "-created",
|
||||
expand: "sender"
|
||||
})
|
||||
),
|
||||
getNextPageParam: (lastPage) =>
|
||||
lastPage.page >= lastPage.totalPages ? undefined : lastPage.page + 1,
|
||||
initialPageParam: 1,
|
||||
enabled: !!user,
|
||||
})
|
||||
|
||||
const announcements = query.data?.pages.flatMap(page => page.items) || []
|
||||
|
||||
if (query.isError) return (
|
||||
<PocketBaseErrorAlert error={query.error}/>
|
||||
)
|
||||
|
||||
if (query.isLoading || !query.data) return (
|
||||
<Center>
|
||||
<Loader/>
|
||||
</Center>
|
||||
)
|
||||
|
||||
return <div className={`stack `}>
|
||||
<div className={`scrollbar ${classes.announcements}`}>
|
||||
{announcements.map((announcement) => (
|
||||
<Announcement
|
||||
key={announcement.id}
|
||||
announcement={announcement}
|
||||
/>
|
||||
))}
|
||||
|
||||
{query.hasNextPage ? (
|
||||
<Center>
|
||||
<Button
|
||||
variant={"transparent"} color={"blue"}
|
||||
radius={"xl"}
|
||||
onClick={() => query.fetchNextPage()}
|
||||
leftSection={<IconMessageCircleDown/>}
|
||||
loading={query.isFetchingNextPage}
|
||||
>
|
||||
Mehr laden
|
||||
</Button>
|
||||
</Center>
|
||||
) : announcements.length === 0 ? <div className={classes.text}>
|
||||
<Text ta={"center"} size={"xs"} c={"dimmed"}>
|
||||
Noch keine von dir gesendeten Ankündigungen
|
||||
</Text>
|
||||
</div> : null
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
|
@ -1,24 +1,23 @@
|
|||
import {Anchor, Breadcrumbs, Center} from "@mantine/core";
|
||||
import {Link, Outlet, Route, Routes} from "react-router-dom";
|
||||
import classes from "./ChatRouter.module.css";
|
||||
import classes from "./EmailRouter.module.css";
|
||||
|
||||
import ConversationSvg from "@/illustrations/conversation.svg?react";
|
||||
import EmailSVG from "@/illustrations/email.svg?react";
|
||||
import {useMediaQuery} from "@mantine/hooks";
|
||||
import EventListMessagesList from "@/pages/chat/EventListMessagesList.tsx";
|
||||
import ListMessagesView from "@/pages/chat/ListMessagesView.tsx";
|
||||
import SentEmailsNavigation from "@/pages/email/SentEmailsNavigation.tsx";
|
||||
import {usePB} from "@/lib/pocketbase.tsx";
|
||||
import {useLogin} from "@/components/users/modals/hooks.ts";
|
||||
import Announcements from "@/pages/chat/components/Announcements.tsx";
|
||||
import {useEffect} from "react";
|
||||
import SendAnnouncements from "@/pages/chat/components/SendAnnouncements.tsx";
|
||||
import NotFound from "@/pages/not-found/index.page.tsx";
|
||||
import EmailView from "@/pages/email/EmailView";
|
||||
|
||||
const ChatIndex = () => {
|
||||
const EmailIndex = () => {
|
||||
return <Center>
|
||||
<ConversationSvg width={"100%"} height={"100%"}/>
|
||||
<EmailSVG width={"100%"} height={"100%"}/>
|
||||
</Center>
|
||||
}
|
||||
|
||||
export default function ChatRouter() {
|
||||
export default function EmailRouter() {
|
||||
|
||||
const isMobile = useMediaQuery("(max-width: 768px)")
|
||||
const {user} = usePB()
|
||||
|
@ -38,7 +37,7 @@ export default function ChatRouter() {
|
|||
<div className={"section-transparent"}>
|
||||
<Breadcrumbs>{[
|
||||
{title: "Home", to: "/"},
|
||||
{title: "Nachrichten", to: "/chat"},
|
||||
{title: "Email", to: "/email"},
|
||||
].map(({title, to}) => (
|
||||
<Anchor component={Link} to={to} key={title} className={"wrapWords"}>
|
||||
{title}
|
||||
|
@ -50,14 +49,13 @@ export default function ChatRouter() {
|
|||
<Outlet/>
|
||||
|
||||
<Routes>
|
||||
<Route path={isMobile ? "/" : "*"} element={<EventListMessagesList/>}/>
|
||||
<Route path={isMobile ? "/" : "*"} element={<SentEmailsNavigation/>}/>
|
||||
</Routes>
|
||||
|
||||
<Routes>
|
||||
{!isMobile && <Route index element={<ChatIndex/>}/>}
|
||||
<Route path={":listId"} element={<ListMessagesView/>}/>
|
||||
<Route path={"announcements"} element={<Announcements/>}/>
|
||||
<Route path={"send-announcements"} element={<SendAnnouncements/>}/>
|
||||
{!isMobile && <Route index element={<EmailIndex/>}/>}
|
||||
<Route path={":emailId"} element={<EmailView/>}/>
|
||||
<Route path={"*"} element={<NotFound/>}/>
|
||||
</Routes>
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,7 @@
|
|||
.infoText {
|
||||
color: var(--mantine-color-dimmed);
|
||||
}
|
||||
|
||||
.container {
|
||||
overflow: auto !important;
|
||||
}
|
|
@ -0,0 +1,160 @@
|
|||
import {useParams} from "react-router-dom";
|
||||
import {PocketBaseErrorAlert, usePB} from "@/lib/pocketbase.tsx";
|
||||
import {useQuery} from "@tanstack/react-query";
|
||||
import {Center, Divider, List, Loader, rem, ScrollArea, ThemeIcon, Title, Tooltip} from "@mantine/core";
|
||||
import {pprintDate, pprintTime} from "@/lib/datetime.ts";
|
||||
import InnerHtml from "@/components/InnerHtml";
|
||||
import {IconCheck, IconChecks, IconCircleCheck, IconX} from "@tabler/icons-react";
|
||||
import TextWithIcon from "@/components/layout/TextWithIcon";
|
||||
import ShowHelp from "@/components/ShowHelp.tsx";
|
||||
import {getUserName} from "@/components/users/modals/util.tsx";
|
||||
import classes from "./index.module.css";
|
||||
|
||||
export default function EmailView() {
|
||||
|
||||
const {emailId} = useParams() as { emailId: string }
|
||||
|
||||
const {pb, useSubscription} = usePB()
|
||||
|
||||
const query = useQuery({
|
||||
queryKey: ["email", emailId],
|
||||
queryFn: async () => (
|
||||
await pb.collection("emails").getOne(emailId, {expand: "sender,recipients,sentTo"})
|
||||
)
|
||||
})
|
||||
|
||||
useSubscription({
|
||||
idOrName: "emails",
|
||||
topic: emailId,
|
||||
callback: () => query.refetch()
|
||||
}, [emailId])
|
||||
|
||||
if (query.isError) return (
|
||||
<PocketBaseErrorAlert error={query.error}/>
|
||||
)
|
||||
|
||||
if (query.isLoading || !query.data) return (
|
||||
<Center>
|
||||
<Loader/>
|
||||
</Center>
|
||||
)
|
||||
|
||||
const email = query.data
|
||||
|
||||
const wasSentToCount = email.recipients.filter(r => email.sentTo?.includes(r)).length
|
||||
|
||||
return <div className={`stack scrollable scrollbar ${classes.container}`}>
|
||||
<div className={"section"}>
|
||||
{
|
||||
email.sentAt && wasSentToCount === 0 ? <>
|
||||
<Tooltip
|
||||
label={"Nicht gesendet"}
|
||||
color={"red"}
|
||||
>
|
||||
<ThemeIcon
|
||||
className={"section-icon"}
|
||||
color={"red"}
|
||||
variant={"light"}
|
||||
radius={"xl"}
|
||||
>
|
||||
<IconX/>
|
||||
</ThemeIcon>
|
||||
</Tooltip>
|
||||
|
||||
</> : email.sentAt ? <>
|
||||
<Tooltip
|
||||
label={`Versendet am ${pprintDate(email.sentAt)} um ${pprintTime(email.sentAt)}`}
|
||||
color={"green"}
|
||||
>
|
||||
<ThemeIcon
|
||||
className={"section-icon"}
|
||||
color={"green"}
|
||||
variant={"light"}
|
||||
radius={"xl"}
|
||||
>
|
||||
<IconCheck/>
|
||||
</ThemeIcon>
|
||||
</Tooltip>
|
||||
</> : <>
|
||||
<Tooltip label={"Status: In Bearbeitung"}>
|
||||
<Loader className={"section-icon"} size={"xs"}/>
|
||||
</Tooltip>
|
||||
</>}
|
||||
|
||||
<div className={"stack"}>
|
||||
<Title order={1}>
|
||||
{email.subject}
|
||||
</Title>
|
||||
|
||||
<TextWithIcon icon={
|
||||
<Tooltip label={"Gesendet an"}>
|
||||
<IconChecks/>
|
||||
</Tooltip>
|
||||
}>
|
||||
{wasSentToCount} gesendet / {email.recipients.length} insgesamt
|
||||
</TextWithIcon>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={"section stack"}>
|
||||
<Title order={2}>
|
||||
Empfängerinnen
|
||||
</Title>
|
||||
<ShowHelp>
|
||||
Das versenden einer Email kann einige Zeit in Anspruch nehmen.
|
||||
<br/>
|
||||
Falls eine Email an einzelne Personen nicht gesendet wurde, kann es sein,
|
||||
dass deren Email-Adresse inkorrekt ist.
|
||||
</ShowHelp>
|
||||
|
||||
<ScrollArea.Autosize mah={300}>
|
||||
<List
|
||||
spacing="xs"
|
||||
size="sm"
|
||||
center
|
||||
icon={
|
||||
<ThemeIcon color="teal" size={24} radius="xl">
|
||||
<IconCircleCheck style={{width: rem(16), height: rem(16)}}/>
|
||||
</ThemeIcon>
|
||||
}
|
||||
>
|
||||
{email.expand?.recipients?.map(r => {
|
||||
|
||||
const wasSent = email.sentTo?.includes(r.id)
|
||||
const inProgress = !email.sentAt
|
||||
|
||||
return <>
|
||||
<List.Item
|
||||
key={r.id}
|
||||
icon={
|
||||
<Tooltip
|
||||
label={wasSent ? "Gesendet" : inProgress ? "Wird gesendet ..." : "Fehler: Konnte nicht gesendet werden"}>
|
||||
<ThemeIcon
|
||||
variant={"transparent"}
|
||||
color={wasSent ? "green" : inProgress ? "blue" : "red"}
|
||||
>
|
||||
{
|
||||
wasSent ? <IconCheck/> : inProgress ? <Loader size={"xs"}/> : <IconX/>
|
||||
}
|
||||
</ThemeIcon>
|
||||
</Tooltip>
|
||||
}
|
||||
>
|
||||
{getUserName(r)}
|
||||
</List.Item>
|
||||
|
||||
</>
|
||||
})}
|
||||
</List>
|
||||
</ScrollArea.Autosize>
|
||||
</div>
|
||||
|
||||
<div className={"section stack"}>
|
||||
<Title order={2}>
|
||||
Inhalt
|
||||
</Title>
|
||||
<Divider/>
|
||||
<InnerHtml html={email.content}/>
|
||||
</div>
|
||||
</div>
|
||||
}
|
|
@ -3,27 +3,30 @@
|
|||
transition: width 0.3s ease
|
||||
}
|
||||
|
||||
.announcementLinkContainer {
|
||||
.newEmailBtnContainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: calc(var(--padding) / 2);
|
||||
border: var(--border);
|
||||
border-radius: var(--border-radius);
|
||||
background-color: var(--mantine-color-body);
|
||||
|
||||
&[data-active="true"] {
|
||||
border-color: var(--mantine-primary-color-5);
|
||||
}
|
||||
}
|
||||
|
||||
.announcementLink {
|
||||
.newEmailBtn {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
padding: calc(var(--padding) / 2);
|
||||
gap: var(--gap);
|
||||
}
|
||||
|
||||
.listText {
|
||||
color: var(--mantine-color-dimmed);
|
||||
.emailSubject {
|
||||
|
||||
width: calc(100%);
|
||||
|
||||
width: calc(100% - 40px);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
|
@ -34,15 +37,23 @@
|
|||
}
|
||||
}
|
||||
|
||||
.listLink {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
padding: calc(var(--padding) / 2);
|
||||
gap: var(--gap);
|
||||
.emailSentDate {
|
||||
color: var(--mantine-color-dimmed);
|
||||
font-size: var(--mantine-font-size-xs);
|
||||
|
||||
&[data-active="true"] {
|
||||
color: var(--mantine-primary-color-5);
|
||||
}
|
||||
}
|
||||
|
||||
.listsContainer {
|
||||
.emailLink {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: start;
|
||||
padding: calc(var(--padding) / 2);
|
||||
}
|
||||
|
||||
.emailListContainer {
|
||||
//flex-grow: 1;
|
||||
overflow: auto;
|
||||
border: var(--border);
|
|
@ -0,0 +1,118 @@
|
|||
import {Button, Center, Divider, Loader, Stack, Text, TextInput, ThemeIcon, UnstyledButton} from "@mantine/core";
|
||||
import {NavLink} from "react-router-dom";
|
||||
import classes from "@/pages/email/SentEmailsNavigation.module.css";
|
||||
import {IconArrowDown, IconListSearch, IconMailPlus, IconMoodPuzzled} from "@tabler/icons-react";
|
||||
import {usePB} from "@/lib/pocketbase.tsx";
|
||||
import {useDebouncedState, useDisclosure} from "@mantine/hooks";
|
||||
import {useInfiniteQuery} from "@tanstack/react-query";
|
||||
import {EmailModal} from "@/components/EmailModal";
|
||||
import {EmailModel} from "@/models/EmailTypes.ts";
|
||||
import {pprintDateTime} from "@/lib/datetime.ts";
|
||||
|
||||
const EmailLink = ({email}: { email?: EmailModel }) => {
|
||||
if (!email) return null
|
||||
return <UnstyledButton component={NavLink} to={`/email/${email.id}`} className={classes.emailLink}>
|
||||
{
|
||||
({isActive}) => <>
|
||||
<div className={classes.emailSubject} data-active={isActive}>
|
||||
{email.subject}
|
||||
</div>
|
||||
<div className={classes.emailSentDate} data-active={isActive}>
|
||||
{pprintDateTime(email.created)}
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
</UnstyledButton>
|
||||
}
|
||||
|
||||
const NewEmailButton = () => {
|
||||
const [showModal, showModalHandler] = useDisclosure(false)
|
||||
|
||||
return <>
|
||||
<EmailModal opened={showModal} onClose={showModalHandler.close}/>
|
||||
<div className={classes.newEmailBtnContainer} data-active={showModal}>
|
||||
<UnstyledButton className={classes.newEmailBtn} onClick={showModalHandler.toggle}>
|
||||
<ThemeIcon
|
||||
variant="light"
|
||||
radius={"xl"}
|
||||
size="md"
|
||||
color={showModal ? "blue" : "green"}
|
||||
>
|
||||
<IconMailPlus/>
|
||||
</ThemeIcon>
|
||||
|
||||
<div className={`${classes.emailSubject} `}>
|
||||
Neue Email
|
||||
</div>
|
||||
</UnstyledButton>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
|
||||
export default function SentEmailsNavigation() {
|
||||
const {user, pb} = usePB()
|
||||
|
||||
const [emailQuery, setEmailQuery] = useDebouncedState("", 200)
|
||||
|
||||
const query = useInfiniteQuery({
|
||||
queryKey: ["emails", emailQuery],
|
||||
queryFn: async ({pageParam}) => (
|
||||
await pb.collection("emails").getList(pageParam, 100, {
|
||||
filter: `sender='${user?.id}'&&(content~'${emailQuery}' || subject~'${emailQuery}')`,
|
||||
sort: "-created",
|
||||
})
|
||||
),
|
||||
getNextPageParam: (lastPage) =>
|
||||
lastPage.page >= lastPage.totalPages ? undefined : lastPage.page + 1,
|
||||
initialPageParam: 1,
|
||||
enabled: !!user,
|
||||
})
|
||||
|
||||
const emails = query.data?.pages.flatMap(t => t.items) || []
|
||||
|
||||
return <div className={`stack ${classes.container}`}>
|
||||
<NewEmailButton/>
|
||||
|
||||
<Divider label={"Gesendete Emails"}/>
|
||||
|
||||
<TextInput
|
||||
leftSection={<IconListSearch/>}
|
||||
rightSection={query.isPending ? <Loader size={"xs"}/> : undefined}
|
||||
defaultValue={emailQuery}
|
||||
onChange={(e) => setEmailQuery(e.currentTarget.value)}
|
||||
placeholder={"Nach Emails suchen..."}
|
||||
/>
|
||||
|
||||
{emails.length === 0 ? <Stack gap={"xs"} align={"center"}>
|
||||
<ThemeIcon variant="transparent" size="xs" color="gray">
|
||||
<IconMoodPuzzled/>
|
||||
</ThemeIcon>
|
||||
|
||||
<Text size={"xs"} c={"dimmed"}>
|
||||
{emailQuery ? "Keine Emails gefunden" : "Du hast bisher keine Emails gesendet"}
|
||||
</Text>
|
||||
</Stack> : (
|
||||
<div className={`${classes.emailListContainer} no-scrollbar`}>
|
||||
{emails.map(entry => <EmailLink
|
||||
key={entry.id}
|
||||
email={entry}
|
||||
/>)}
|
||||
|
||||
{query.hasNextPage && (
|
||||
<Center p={"xs"}>
|
||||
<Button
|
||||
disabled={query.isFetchingNextPage || !query.hasNextPage}
|
||||
loading={query.isFetchingNextPage}
|
||||
variant={"transparent"}
|
||||
size={"compact-xs"}
|
||||
leftSection={<IconArrowDown/>}
|
||||
onClick={() => query.fetchNextPage()}
|
||||
>
|
||||
Mehr laden
|
||||
</Button>
|
||||
</Center>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
}
|
|
@ -19,11 +19,11 @@ export default function EventNavigate() {
|
|||
const eventQuery = useQuery({
|
||||
queryKey: ["event", eventId],
|
||||
queryFn: async () => (await pb.collection("events").getOne(eventId, {
|
||||
expand: "eventAdmins, privilegedLists, privilegedLists.eventListSlots_via_eventList.eventListSlotEntries_via_eventListsSlot"
|
||||
expand: "eventAdmins"
|
||||
}))
|
||||
})
|
||||
|
||||
const {isPrivilegedUser, canEditEvent} = useEventRights(eventQuery.data)
|
||||
const { canEditEvent} = useEventRights(eventQuery.data)
|
||||
|
||||
if (eventQuery.isLoading) {
|
||||
return <LoadingOverlay/>
|
||||
|
@ -37,9 +37,5 @@ export default function EventNavigate() {
|
|||
return <Navigate to={`/events/e/${eventId}`} replace/>
|
||||
}
|
||||
|
||||
if (isPrivilegedUser) {
|
||||
return <Navigate to={`/events/e/${eventId}/lists`} replace/>
|
||||
}
|
||||
|
||||
return <Navigate to={`/events/s/${eventId}`} replace/>
|
||||
}
|
|
@ -4,7 +4,7 @@ import {DateTimePicker} from "@mantine/dates";
|
|||
import {IconCheck, IconInfoCircle, IconX} from "@tabler/icons-react";
|
||||
import {useMutation} from "@tanstack/react-query";
|
||||
import dayjs from "dayjs";
|
||||
import {UserModal} from "@/models/AuthTypes.ts";
|
||||
import {UserModel} from "@/models/AuthTypes.ts";
|
||||
import {PocketBaseErrorAlert, usePB} from "@/lib/pocketbase.tsx";
|
||||
import UserInput from "@/components/users/UserInput.tsx";
|
||||
import {EventModel} from "@/models/EventTypes.ts";
|
||||
|
@ -33,13 +33,13 @@ export default function CreateEvent({onSuccess, onAbort}: {
|
|||
name: "",
|
||||
startDate: null,
|
||||
endDate: null,
|
||||
eventAdmins: [user] as UserModal[],
|
||||
eventAdmins: [user] as UserModel[],
|
||||
isStuveEvent: true,
|
||||
},
|
||||
validate: {
|
||||
name: hasLength({min: 4, max: 50}, 'Der Name muss zwischen 4 und 50 Zeichen lang sein.'),
|
||||
startDate: (value) => dayjs(value).isAfter(dayjs(), "day") ? null : "Das Startdatum muss in der Zukunft liegen.",
|
||||
endDate: (value, values) => dayjs(value).isAfter(dayjs(values.startDate), "day") ? null : "Das Enddatum muss nach dem Startdatum liegen.",
|
||||
startDate: (value) => dayjs(value).isAfter(dayjs(), "day") ? null : "Es können nur Events in der Zukunft erstellt werden.",
|
||||
endDate: (value, values) => dayjs(value).isAfter(dayjs(values.startDate), "day") || dayjs(value).isSame(dayjs(values.startDate), "day") ? null : "Ungültiges Enddatum.",
|
||||
eventAdmins: (value) => value.length > 0 ? null : "Es muss mindestens ein Admin ausgewählt werden.",
|
||||
}
|
||||
})
|
||||
|
|
|
@ -24,7 +24,6 @@ import {
|
|||
IconHistory,
|
||||
IconHistoryOff,
|
||||
IconInfoCircle,
|
||||
IconList,
|
||||
IconSearch,
|
||||
IconSortAscending,
|
||||
IconSortDescending,
|
||||
|
@ -43,7 +42,7 @@ import {useEventRights} from "@/pages/events/util.ts";
|
|||
*/
|
||||
const EventRow = ({event}: { event: EventModel }) => {
|
||||
|
||||
const {isPrivilegedUser, canEditEvent} = useEventRights(event)
|
||||
const {canEditEvent} = useEventRights(event)
|
||||
|
||||
const [opened, handlers] = useDisclosure(false)
|
||||
|
||||
|
@ -108,19 +107,7 @@ const EventRow = ({event}: { event: EventModel }) => {
|
|||
<IconUserStar/>
|
||||
</ThemeIcon>
|
||||
</Tooltip>
|
||||
) : isPrivilegedUser ?
|
||||
<Tooltip label={"Du kannst das Event einsehen"} position={"left"} color={"green"}
|
||||
withArrow>
|
||||
<ThemeIcon
|
||||
color={"green"}
|
||||
variant={"transparent"}
|
||||
aria-label={"you are a privileged user"}
|
||||
size={"xs"}
|
||||
mr={"sm"}
|
||||
>
|
||||
<IconList/>
|
||||
</ThemeIcon>
|
||||
</Tooltip> : (
|
||||
) : (
|
||||
<ThemeIcon
|
||||
variant={"transparent"}
|
||||
aria-label={"spacer"}
|
||||
|
@ -187,7 +174,6 @@ export const EventList = () => {
|
|||
return await pb.collection("events").getList(activePage, 10, {
|
||||
sort: sort,
|
||||
filter: [`hideFromPublic = false`, ...filter].join(" && "),
|
||||
expand: "privilegedLists.eventListSlots_via_eventList.eventListSlotEntries_via_eventListsSlot"
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
|
@ -15,11 +15,11 @@ export default function StatusEditor() {
|
|||
const eventQuery = useQuery({
|
||||
queryKey: ["event", eventId],
|
||||
queryFn: async () => (await pb.collection("events").getOne(eventId, {
|
||||
expand: "eventAdmins, privilegedLists, privilegedLists.eventListSlots_via_eventList.eventListSlotEntries_via_eventListsSlot"
|
||||
expand: "eventAdmins"
|
||||
}))
|
||||
})
|
||||
|
||||
const {isPrivilegedUser, canEditEvent} = useEventRights(eventQuery.data)
|
||||
const {canEditEvent} = useEventRights(eventQuery.data)
|
||||
|
||||
if (eventQuery.isLoading) {
|
||||
return <LoadingOverlay/>
|
||||
|
@ -29,7 +29,7 @@ export default function StatusEditor() {
|
|||
return <NotFound/>
|
||||
}
|
||||
|
||||
if (!(canEditEvent || isPrivilegedUser)) {
|
||||
if (!(canEditEvent)) {
|
||||
return <Navigate to={`/events/s/${eventId}`} replace/>
|
||||
}
|
||||
|
||||
|
|
|
@ -89,11 +89,11 @@ export default function EditEventRouter() {
|
|||
const eventQuery = useQuery({
|
||||
queryKey: ["event", eventId],
|
||||
queryFn: async () => (await pb.collection("events").getOne(eventId, {
|
||||
expand: "eventAdmins, privilegedLists, privilegedLists.eventListSlots_via_eventList.eventListSlotEntries_via_eventListsSlot"
|
||||
expand: "eventAdmins"
|
||||
}))
|
||||
})
|
||||
|
||||
const {isPrivilegedUser, canEditEvent} = useEventRights(eventQuery.data)
|
||||
const {canEditEvent} = useEventRights(eventQuery.data)
|
||||
|
||||
if (eventQuery.isLoading) {
|
||||
return <LoadingOverlay/>
|
||||
|
@ -103,7 +103,7 @@ export default function EditEventRouter() {
|
|||
return <NotFound/>
|
||||
}
|
||||
|
||||
if (!(canEditEvent || isPrivilegedUser)) {
|
||||
if (!(canEditEvent)) {
|
||||
return <Navigate to={`/events/s/${eventId}`} replace/>
|
||||
}
|
||||
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import {EventModel} from "@/models/EventTypes.ts";
|
||||
import classes from "../EditEventRouter.module.css";
|
||||
import {IconCalendar, IconHourglass, IconList, IconMap, IconSparkles} from "@tabler/icons-react";
|
||||
import {IconCalendar, IconHourglass, IconMap, IconSparkles} from "@tabler/icons-react";
|
||||
import {areDatesSame, humanDeltaFromNow, pprintDate} from "@/lib/datetime.ts";
|
||||
import UsersDisplay from "@/components/users/UsersDisplay.tsx";
|
||||
import {List, Text, ThemeIcon, Title, Tooltip} from "@mantine/core";
|
||||
import {Text, ThemeIcon, Title, Tooltip} from "@mantine/core";
|
||||
|
||||
/**
|
||||
* Displays the event data
|
||||
|
@ -86,21 +86,6 @@ export default function EventData({event, hideHeader}: { event: EventModel, hide
|
|||
</div>
|
||||
</Tooltip>
|
||||
}
|
||||
|
||||
{
|
||||
event.expand?.privilegedLists &&
|
||||
<Tooltip label={"Privilegierte Listen"} position={"top-start"}>
|
||||
<List size={"sm"} icon={<IconList size={10}/>}>
|
||||
{
|
||||
event.expand?.privilegedLists.map((l) => (
|
||||
<List.Item key={l.id} fw={500}>
|
||||
{l.name}
|
||||
</List.Item>
|
||||
))
|
||||
}
|
||||
</List>
|
||||
</Tooltip>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import {EventListModel, EventModel} from "@/models/EventTypes.ts";
|
||||
import {EventModel} from "@/models/EventTypes.ts";
|
||||
import {useForm} from "@mantine/form";
|
||||
import {UserModal} from "@/models/AuthTypes.ts";
|
||||
import {UserModel} from "@/models/AuthTypes.ts";
|
||||
import {PocketBaseErrorAlert, usePB} from "@/lib/pocketbase.tsx";
|
||||
import {Button, Group, Title} from "@mantine/core";
|
||||
import UserInput from "@/components/users/UserInput.tsx";
|
||||
|
@ -8,7 +8,6 @@ import {useMutation} from "@tanstack/react-query";
|
|||
import {queryClient} from "@/main.tsx";
|
||||
import {showSuccessNotification} from "@/components/util.tsx";
|
||||
import ShowHelp from "@/components/ShowHelp.tsx";
|
||||
import ListSelect from "@/pages/events/e/:eventId/EventLists/ListSelect.tsx";
|
||||
|
||||
/**
|
||||
* This component allows the user to edit the admins of the event and the event lists.
|
||||
|
@ -20,8 +19,7 @@ export default function EditEventMembers({event}: { event: EventModel }) {
|
|||
|
||||
const formValues = useForm({
|
||||
initialValues: {
|
||||
eventAdmins: event?.expand?.eventAdmins ?? [user] as UserModal[],
|
||||
privilegedLists: event?.expand?.privilegedLists ?? [] as EventListModel[]
|
||||
eventAdmins: event?.expand?.eventAdmins ?? [user] as UserModel[],
|
||||
},
|
||||
validate: {
|
||||
eventAdmins: (value) => value.length > 0 ? null : "Es muss mindestens ein Admin ausgewählt werden.",
|
||||
|
@ -32,7 +30,6 @@ export default function EditEventMembers({event}: { event: EventModel }) {
|
|||
mutationFn: async () => {
|
||||
return await pb.collection("events").update(event.id, {
|
||||
eventAdmins: [...formValues.values.eventAdmins.map((member) => member.id), user?.id],
|
||||
privilegedLists: formValues.values.privilegedLists.map((member) => member.id)
|
||||
})
|
||||
},
|
||||
onSuccess: () => {
|
||||
|
@ -45,20 +42,8 @@ export default function EditEventMembers({event}: { event: EventModel }) {
|
|||
<Title order={4} c={"blue"}>Event Admins</Title>
|
||||
|
||||
<ShowHelp>
|
||||
<Title order={6}>Event Admins</Title>
|
||||
Event Admin können alle Einstellungen des Events bearbeiten.
|
||||
Sie können auch alle Listen bearbeiten und verwalten.
|
||||
<br/>
|
||||
<br/>
|
||||
|
||||
<Title order={6}>Privilegierte Listen</Title>
|
||||
Alle Teilnehmenden in privilegierten Listen können alle Anmeldungen von allen Listen
|
||||
dieses Events sehen und den Status von allen Teilnehmenden bearbeiten.
|
||||
<br/>
|
||||
Du kannst eine privilegierte Liste z.B. für die Event-Orgs erstellen, so dass diese
|
||||
alle Anmeldungen sehen und bearbeiten können.
|
||||
<br/>
|
||||
<br/>
|
||||
</ShowHelp>
|
||||
|
||||
<PocketBaseErrorAlert error={editMutation.error}/>
|
||||
|
@ -72,14 +57,6 @@ export default function EditEventMembers({event}: { event: EventModel }) {
|
|||
setSelectedRecords={(records) => formValues.setFieldValue("eventAdmins", records)}
|
||||
/>
|
||||
|
||||
<ListSelect
|
||||
label={"Privilegierte Listen"}
|
||||
description={"Teilnehmende in privilegierten Listen können alle Event-Anmeldungen sehen und deren Status bearbeiten."}
|
||||
event={event}
|
||||
selectedRecords={formValues.values.privilegedLists}
|
||||
setSelectedRecords={(records) => formValues.setFieldValue("privilegedLists", records)}
|
||||
/>
|
||||
|
||||
<Group>
|
||||
<Button
|
||||
type={"submit"}
|
||||
|
|
|
@ -6,7 +6,6 @@ import {
|
|||
IconForms,
|
||||
IconLock,
|
||||
IconLockOpen,
|
||||
IconMessageCircle,
|
||||
IconSettings,
|
||||
IconUserCog
|
||||
} from "@tabler/icons-react";
|
||||
|
@ -58,14 +57,6 @@ export default function EventListRouter({event}: { event: EventModel }) {
|
|||
}
|
||||
]
|
||||
|
||||
if (list.enableChat) {
|
||||
nav.push({
|
||||
icon: <IconMessageCircle/>,
|
||||
to: `/chat/${list.id}`,
|
||||
title: "Chat"
|
||||
})
|
||||
}
|
||||
|
||||
if (canEditEvent) {
|
||||
nav.push(...[
|
||||
{
|
||||
|
|
|
@ -28,7 +28,7 @@ export default function ListEntryQuestionSettings({list, event}: { list: EventLi
|
|||
})
|
||||
},
|
||||
onSuccess: () => {
|
||||
showSuccessNotification("Fragen und Liste gespeichert")
|
||||
showSuccessNotification("Formular Fragen und Liste gespeichert")
|
||||
return queryClient.invalidateQueries({queryKey: ["event", event.id, "list", list.id]})
|
||||
}
|
||||
})
|
||||
|
@ -39,7 +39,7 @@ export default function ListEntryQuestionSettings({list, event}: { list: EventLi
|
|||
<PocketBaseErrorAlert error={editMutation.error}/>
|
||||
|
||||
{(!list.ignoreDefaultEntryQuestionScheme && event.defaultEntryQuestionSchema) && (
|
||||
<Alert color={"blue"} title={"Standard Fragen"} className={"stack"}>
|
||||
<Alert color={"blue"} title={"Standard Formular Fragen"} className={"stack"}>
|
||||
<Text c={"dimmed"} mb={"sm"} size={"xs"}>
|
||||
Folgende Fragen sind standardmäßig für dieses Event vorgesehen
|
||||
und werden automatisch angehängt:
|
||||
|
|
|
@ -21,7 +21,6 @@ export default function ListSettings({list, event}: { list: EventListModel, even
|
|||
open: list.open,
|
||||
allowOverlappingEntries: list.allowOverlappingEntries,
|
||||
onlyStuVeAccounts: list.onlyStuVeAccounts,
|
||||
enableChat: list.enableChat,
|
||||
favorite: list.favorite,
|
||||
description: list.description || "",
|
||||
}
|
||||
|
@ -106,19 +105,6 @@ export default function ListSettings({list, event}: { list: EventListModel, even
|
|||
{...formValues.getInputProps("allowOverlappingEntries", {type: "checkbox"})}
|
||||
/>
|
||||
|
||||
<CheckboxCard
|
||||
label={"Chat aktivieren"}
|
||||
description={
|
||||
"Du kannst eine neue Chatgruppe für diese Liste erstellen. " +
|
||||
"Alle Anmeldungen der Liste können diesen Chat nutzen. " +
|
||||
"Eventadmins können den Chat auch nutzen und alle Nachrichten bearbeiten sowie löschen. " +
|
||||
"Neue Anmeldungen sehen auch vergangene Nachrichten.<br/>" +
|
||||
"Der Chat wird automatisch gelöscht, wenn diese Liste gelöscht wird. Wenn du den Chat deaktivierst, " +
|
||||
"werden die Nachrichten nicht gelöscht und kann der Chat kann später wieder aktiviert werden."
|
||||
}
|
||||
{...formValues.getInputProps("enableChat", {type: "checkbox"})}
|
||||
/>
|
||||
|
||||
<TextEditor
|
||||
maxHeight={"500px"}
|
||||
fullToolbar
|
||||
|
|
|
@ -13,7 +13,9 @@ import {
|
|||
} from "@/pages/events/e/:eventId/EventLists/EventListComponents/UpdateEntryStatusModal.tsx";
|
||||
import {MoveEntryModal} from "@/pages/events/e/:eventId/EventLists/EventListComponents/MoveEntryModal.tsx";
|
||||
import EntryStatusSpoiler from "@/pages/events/e/:eventId/EventLists/EventListComponents/EntryStatusSpoiler.tsx";
|
||||
import {getListSchemas, useEventRights} from "@/pages/events/util.ts";
|
||||
import {getEventMailDefaultText, getListSchemas, useEventRights} from "@/pages/events/util.ts";
|
||||
import {UserModel} from "@/models/AuthTypes.ts";
|
||||
import {EmailModal} from "@/components/EmailModal";
|
||||
|
||||
|
||||
export default function EditSlotEntryMenu({entry, refetch, event}: {
|
||||
|
@ -30,6 +32,9 @@ export default function EditSlotEntryMenu({entry, refetch, event}: {
|
|||
|
||||
const [showMoveEntryModal, showMoveEntryModalHandler] = useDisclosure(false)
|
||||
|
||||
const [showEmailModal, showEmailModalHandler] = useDisclosure(false)
|
||||
|
||||
|
||||
const deleteEntryMutation = useMutation({
|
||||
mutationFn: async () => {
|
||||
await pb.collection("eventListSlotEntries").delete(entry.id)
|
||||
|
@ -51,6 +56,15 @@ export default function EditSlotEntryMenu({entry, refetch, event}: {
|
|||
const noStatusField = statusSchema.fields.length === 0
|
||||
|
||||
return <>
|
||||
|
||||
<EmailModal
|
||||
opened={showEmailModal}
|
||||
onClose={showEmailModalHandler.close}
|
||||
disableUserInput={true}
|
||||
recipients={[entry.expand?.user] as UserModel[]}
|
||||
content={getEventMailDefaultText(event)}
|
||||
/>
|
||||
|
||||
<ConfirmModal/>
|
||||
|
||||
<UpdateEntryStatusModal
|
||||
|
@ -97,7 +111,7 @@ export default function EditSlotEntryMenu({entry, refetch, event}: {
|
|||
<Menu.Dropdown>
|
||||
<Menu.Item
|
||||
leftSection={<IconSend size={16}/>}
|
||||
disabled
|
||||
onClick={showEmailModalHandler.toggle}
|
||||
>
|
||||
Person benachrichtigen
|
||||
</Menu.Item>
|
||||
|
|
|
@ -27,7 +27,7 @@ export default function EditDefaultEntryQuestionSchema({event}: { event: EventMo
|
|||
})
|
||||
},
|
||||
onSuccess: () => {
|
||||
showSuccessNotification("Fragen gespeichert")
|
||||
showSuccessNotification("Formular Fragen gespeichert")
|
||||
return queryClient.invalidateQueries({queryKey: ["event", event.id]})
|
||||
}
|
||||
})
|
||||
|
|
|
@ -1,10 +1,8 @@
|
|||
import {EventListModel, EventListSlotEntriesWithUserModel, EventModel} from "@/models/EventTypes.ts";
|
||||
import {ListResult} from "pocketbase";
|
||||
import {InfiniteData, UseInfiniteQueryResult, useMutation} from "@tanstack/react-query";
|
||||
import {Button, Checkbox, Fieldset, Group, Modal, Progress, Select, Text} from "@mantine/core";
|
||||
import {useMutation} from "@tanstack/react-query";
|
||||
import {Button, Checkbox, Fieldset, Group, Modal, Select, Text} from "@mantine/core";
|
||||
import ShowHelp from "@/components/ShowHelp.tsx";
|
||||
import {useForm} from "@mantine/form";
|
||||
import {useEffect} from "react";
|
||||
import {FormSchema} from "@/components/formUtil/formBuilder/types.ts";
|
||||
import {IconDownload} from "@tabler/icons-react";
|
||||
import {useShowDebug} from "@/components/ShowDebug.tsx";
|
||||
|
@ -22,12 +20,12 @@ type FormValues = {
|
|||
charset: typeof CSV_CHARSETS[number]
|
||||
}
|
||||
|
||||
export default function DownloadDataModal({opened, onClose, lists, event, query}: {
|
||||
export default function DownloadDataModal({opened, onClose, lists, event, entries}: {
|
||||
opened: boolean
|
||||
onClose: () => void
|
||||
event: EventModel
|
||||
lists: EventListModel[]
|
||||
query: UseInfiniteQueryResult<InfiniteData<ListResult<EventListSlotEntriesWithUserModel>, unknown>, Error>
|
||||
entries: EventListSlotEntriesWithUserModel[]
|
||||
}) {
|
||||
|
||||
const formValues = useForm<FormValues>({
|
||||
|
@ -41,13 +39,6 @@ export default function DownloadDataModal({opened, onClose, lists, event, query}
|
|||
|
||||
const {showDebug} = useShowDebug()
|
||||
|
||||
// Fetch all pages
|
||||
useEffect(() => {
|
||||
if (opened && query.hasNextPage && !query.isFetchingNextPage) {
|
||||
query.fetchNextPage()
|
||||
}
|
||||
}, [opened, query])
|
||||
|
||||
const RenderFieldCheckboxes = ({schema, formKey}: {
|
||||
schema: FormSchema | null,
|
||||
formKey: 'questionSchemaFields' | 'statusSchemaFields'
|
||||
|
@ -66,17 +57,8 @@ export default function DownloadDataModal({opened, onClose, lists, event, query}
|
|||
))
|
||||
)
|
||||
|
||||
const totalPages = query.data?.pages?.[0].totalPages || 0
|
||||
const currentPage = query.data?.pages?.length || 0
|
||||
|
||||
const progress = (currentPage / totalPages * 100) || 0
|
||||
const dataIsFetching = query.isFetching || query.isPending
|
||||
|
||||
const mutation = useMutation({
|
||||
mutationFn: async () => {
|
||||
// all loaded entries
|
||||
const entries = query.data?.pages.flatMap(p => p.items) ?? []
|
||||
|
||||
const selectedFields = {
|
||||
questionSchemaFields: formValues.values.questionSchemaFields.filter(onlyUnique),
|
||||
statusSchemaFields: formValues.values.statusSchemaFields.filter(onlyUnique)
|
||||
|
@ -154,25 +136,15 @@ export default function DownloadDataModal({opened, onClose, lists, event, query}
|
|||
>
|
||||
<div className={"stack"}>
|
||||
<ShowHelp>
|
||||
In der CSV sind automatisch die Felder <em>Person</em>, <em>Anmeldezeitpunkt</em>,
|
||||
<em>Anmelde-Liste</em> und <em>Zeitslot</em> enthalten. Diese
|
||||
Felder können nicht abgewählt werden.
|
||||
In der CSV sind automatisch die Felder
|
||||
<em>Person</em>, <em>Anmeldezeitpunkt</em>,
|
||||
<em>Anmelde-Liste</em> und <em>Zeitslot</em>
|
||||
enthalten.
|
||||
<br/>
|
||||
Um Listen spezifische Daten herunterzuladen, wähle im vorherigen Schritt eine oder mehrere Listen aus.
|
||||
Um Listen spezifische Formularfelder herunterzuladen,
|
||||
wähle im vorherigen Schritt eine oder mehrere Listen aus.
|
||||
</ShowHelp>
|
||||
|
||||
{dataIsFetching && <>
|
||||
<Text c={"dimmed"} size={"xs"}>
|
||||
Daten werden gesammelt ...
|
||||
</Text>
|
||||
|
||||
<Progress.Root size="xl">
|
||||
<Progress.Section value={progress} animated>
|
||||
<Progress.Label>{progress.toFixed(0)}%</Progress.Label>
|
||||
</Progress.Section>
|
||||
</Progress.Root>
|
||||
</>}
|
||||
|
||||
<Fieldset legend={event.name}>
|
||||
<div className={"stack"}>
|
||||
<Text c={"dimmed"} size={"xs"}>
|
||||
|
@ -227,8 +199,6 @@ export default function DownloadDataModal({opened, onClose, lists, event, query}
|
|||
</Button>
|
||||
<Button
|
||||
onClick={() => mutation.mutate()}
|
||||
disabled={dataIsFetching}
|
||||
loading={dataIsFetching}
|
||||
leftSection={<IconDownload/>}
|
||||
>
|
||||
CSV herunterladen
|
||||
|
|
|
@ -1,165 +0,0 @@
|
|||
import {EventListModel, EventListSlotEntriesWithUserModel, EventModel} from "@/models/EventTypes.ts";
|
||||
import {ListResult} from "pocketbase";
|
||||
import {InfiniteData, UseInfiniteQueryResult, useMutation} from "@tanstack/react-query";
|
||||
import {Button, Group, Modal, Progress, Text, Textarea, TextInput, Title} from "@mantine/core";
|
||||
import {IconSend} from "@tabler/icons-react";
|
||||
import {hasLength, useForm} from "@mantine/form";
|
||||
import ShowHelp from "@/components/ShowHelp.tsx";
|
||||
import TextEditor from "@/components/input/Editor";
|
||||
import ListSelect from "@/pages/events/e/:eventId/EventLists/ListSelect.tsx";
|
||||
import {showSuccessNotification} from "@/components/util.tsx";
|
||||
import {usePB} from "@/lib/pocketbase.tsx";
|
||||
import {useEffect} from "react";
|
||||
|
||||
|
||||
export default function MessageEntriesModal({opened, event, onClose, query}: {
|
||||
opened: boolean
|
||||
onClose: () => void,
|
||||
event: EventModel,
|
||||
query: UseInfiniteQueryResult<InfiniteData<ListResult<EventListSlotEntriesWithUserModel>, unknown>, Error>
|
||||
}) {
|
||||
const {pb, user} = usePB()
|
||||
|
||||
const totalPages = query.data?.pages?.[0].totalPages || 0
|
||||
const currentPage = query.data?.pages?.length || 0
|
||||
|
||||
const progress = (currentPage / totalPages * 100) || 0
|
||||
const dataIsFetching = query.isFetching || query.isPending
|
||||
const entries = query.data?.pages.flatMap(p => p.items) ?? []
|
||||
|
||||
const formValues = useForm({
|
||||
initialValues: {
|
||||
subject: "",
|
||||
content: "",
|
||||
comment: "",
|
||||
selectedLists: [] as EventListModel[],
|
||||
},
|
||||
validate: {
|
||||
subject: hasLength({max: 255}, "Der Betreff ist zu lang (max. 255 Zeichen)"),
|
||||
content: hasLength({min: 10, max: 10000}, "Die Nachricht muss zwischen 10 und 10000 Zeichen lang sein"),
|
||||
}
|
||||
})
|
||||
|
||||
// Fetch all pages
|
||||
useEffect(() => {
|
||||
if (opened && query.hasNextPage && !query.isFetchingNextPage) {
|
||||
query.fetchNextPage()
|
||||
}
|
||||
}, [opened, query])
|
||||
|
||||
const mutation = useMutation({
|
||||
mutationFn: async () => {
|
||||
await pb.collection("messages").create({
|
||||
subject: formValues.values.subject ?? undefined,
|
||||
content: formValues.values.content,
|
||||
recipients: entries.map(e => e.user),
|
||||
isAnnouncement: true,
|
||||
sender: user?.id,
|
||||
meta: {
|
||||
event: event.id
|
||||
}
|
||||
})
|
||||
|
||||
for (const l of formValues.values.selectedLists) {
|
||||
await pb.collection("messages").create({
|
||||
content: formValues.values.content,
|
||||
comment: formValues.values.comment ?? undefined,
|
||||
sender: user?.id,
|
||||
eventList: l.id,
|
||||
})
|
||||
}
|
||||
},
|
||||
onSuccess: () => {
|
||||
showSuccessNotification("Nachrichten wurden versendet")
|
||||
formValues.reset()
|
||||
onClose()
|
||||
}
|
||||
})
|
||||
|
||||
return <Modal
|
||||
opened={opened}
|
||||
onClose={onClose}
|
||||
title="Personen benachrichtigen"
|
||||
size="xl"
|
||||
>
|
||||
<form
|
||||
className={"stack"}
|
||||
onSubmit={formValues.onSubmit(() => {
|
||||
mutation.mutate()
|
||||
})}
|
||||
>
|
||||
{dataIsFetching && <>
|
||||
<Text c={"dimmed"} size={"xs"}>
|
||||
Daten werden gesammelt ...
|
||||
</Text>
|
||||
|
||||
<Progress.Root size="xl">
|
||||
<Progress.Section value={progress} animated>
|
||||
<Progress.Label>{progress.toFixed(0)}%</Progress.Label>
|
||||
</Progress.Section>
|
||||
</Progress.Root>
|
||||
</>}
|
||||
|
||||
<ShowHelp>
|
||||
Mit dieser Funktion kannst du eine Ankündigung an alle Personen senden,
|
||||
die du in der vorherigen Suche gefunden hast.
|
||||
<br/>
|
||||
Die Ankündigung enthält als zusätzliche Information den Namen des Events und
|
||||
alle Anmeldungen der jeweiligen Person.
|
||||
<br/>
|
||||
Wenn du eine Liste (z.B. Orga-Team) darüber informieren möchtest, dass du
|
||||
die Ankündigung verschickt hast, kannst du diese unter BCC auswählen. Über
|
||||
den Kommentar kannst du dieser Liste zusätzliche Informationen mitteilen, z.B.
|
||||
welchem Personenkreis die Nachricht gesendet wurde.
|
||||
</ShowHelp>
|
||||
|
||||
<ListSelect
|
||||
event={event}
|
||||
filter={`enableChat=true`}
|
||||
selectedRecords={formValues.values.selectedLists}
|
||||
setSelectedRecords={(ls) => formValues.setFieldValue("selectedLists", ls)}
|
||||
placeholder={"BCC"}
|
||||
/>
|
||||
|
||||
{
|
||||
formValues.values.selectedLists.length > 0 && <>
|
||||
<Textarea
|
||||
placeholder={"Kommentar an die BCC-Listen"}
|
||||
{...formValues.getInputProps("comment")}
|
||||
/>
|
||||
|
||||
<Title order={3}>
|
||||
Nachricht
|
||||
</Title>
|
||||
</>
|
||||
}
|
||||
|
||||
<TextInput
|
||||
placeholder={"Betreff"}
|
||||
{...formValues.getInputProps("subject")}
|
||||
/>
|
||||
|
||||
<TextEditor
|
||||
placeholder={"Nachricht"}
|
||||
value={formValues.values.content}
|
||||
fullToolbar
|
||||
onChange={content => formValues.setFieldValue("content", content)}
|
||||
error={formValues.errors.content}
|
||||
/>
|
||||
|
||||
<Group>
|
||||
<Button onClick={onClose} variant={"light"} color={"orange"}>
|
||||
Abbrechen
|
||||
</Button>
|
||||
<Button
|
||||
type={"submit"}
|
||||
disabled={dataIsFetching}
|
||||
loading={dataIsFetching}
|
||||
leftSection={<IconSend/>}
|
||||
>
|
||||
{entries.length ?? 0} Personen benachrichtigen
|
||||
</Button>
|
||||
</Group>
|
||||
</form>
|
||||
</Modal>
|
||||
}
|
|
@ -18,7 +18,7 @@ import {
|
|||
IconFilter,
|
||||
IconFilterEdit,
|
||||
IconFilterOff,
|
||||
IconSend,
|
||||
IconMail,
|
||||
IconUserSearch
|
||||
} from "@tabler/icons-react";
|
||||
import {useInfiniteQuery} from "@tanstack/react-query";
|
||||
|
@ -32,20 +32,22 @@ import {FieldEntriesFilter} from "@/components/formUtil/FromInput/types.ts";
|
|||
import {assembleFilter} from "@/components/formUtil/FormFilter/util.ts";
|
||||
import {DateTimePicker} from "@mantine/dates";
|
||||
import EventEntries from "@/pages/events/e/:eventId/EventLists/Search/EventEntries.tsx";
|
||||
import {useEventRights} from "@/pages/events/util.ts";
|
||||
import DownloadDataModal from "@/pages/events/e/:eventId/EventLists/Search/DownloadDataModal.tsx";
|
||||
import MessageEntriesModal from "@/pages/events/e/:eventId/EventLists/Search/MessageEntriesModal.tsx";
|
||||
import {useMemo} from "react";
|
||||
import {useMemo, useState} from "react";
|
||||
import {EmailModal} from "@/components/EmailModal";
|
||||
import {UserModel} from "@/models/AuthTypes.ts";
|
||||
import LoadInfinitQueryModal from "@/components/LoadInfinitQueryModal.tsx";
|
||||
import {getEventMailDefaultText} from "@/pages/events/util.ts";
|
||||
|
||||
export default function ListSearch({event}: { event: EventModel }) {
|
||||
|
||||
const {pb} = usePB()
|
||||
|
||||
const {canEditEvent} = useEventRights(event)
|
||||
|
||||
const [showFilter, showFilterHandler] = useDisclosure(true)
|
||||
const [showDownloadModal, showDownloadModalHandler] = useDisclosure(false)
|
||||
const [showMessageModal, showMessageModalHandler] = useDisclosure(false)
|
||||
const [sendEmailModal, sendEmailModalHandler] = useDisclosure(false)
|
||||
|
||||
const [action, setAction] = useState<"send" | "download" | null>(null)
|
||||
|
||||
const formValues = useForm({
|
||||
initialValues: {
|
||||
|
@ -183,7 +185,7 @@ export default function ListSearch({event}: { event: EventModel }) {
|
|||
|
||||
<Group>
|
||||
<FormFilter
|
||||
label={"Fragen"}
|
||||
label={"Formular"}
|
||||
schema={questionSchema}
|
||||
defaultValue={formValues.values.questionFilter}
|
||||
onChange={(qf) => formValues.setFieldValue("questionFilter", qf)}
|
||||
|
@ -225,18 +227,18 @@ export default function ListSearch({event}: { event: EventModel }) {
|
|||
|
||||
<Button
|
||||
size={"xs"}
|
||||
leftSection={<IconSend size={16}/>}
|
||||
disabled={!canEditEvent}
|
||||
onClick={showMessageModalHandler.toggle}
|
||||
leftSection={<IconMail size={16}/>}
|
||||
onClick={() => setAction("send")}
|
||||
disabled={entries.length === 0}
|
||||
>
|
||||
{entriesCount} Personen benachrichtigen
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
size={"xs"}
|
||||
disabled={!canEditEvent}
|
||||
leftSection={<IconCsv size={16}/>}
|
||||
onClick={showDownloadModalHandler.toggle}
|
||||
onClick={() => setAction("download")}
|
||||
disabled={entries.length === 0}
|
||||
>
|
||||
Daten exportieren
|
||||
</Button>
|
||||
|
@ -258,23 +260,40 @@ export default function ListSearch({event}: { event: EventModel }) {
|
|||
|
||||
<PocketBaseErrorAlert error={query.error}/>
|
||||
|
||||
<EventEntries event={event} entries={entries} refetch={() => query.refetch()}/>
|
||||
<LoadInfinitQueryModal
|
||||
start={action !== null}
|
||||
query={query}
|
||||
onSuccess={() => {
|
||||
switch (action) {
|
||||
case "download":
|
||||
showDownloadModalHandler.open()
|
||||
break
|
||||
case "send":
|
||||
sendEmailModalHandler.open()
|
||||
break
|
||||
}
|
||||
setAction(null)
|
||||
}}
|
||||
/>
|
||||
|
||||
<DownloadDataModal
|
||||
event={event}
|
||||
opened={showDownloadModal}
|
||||
onClose={showDownloadModalHandler.toggle}
|
||||
query={query}
|
||||
entries={entries}
|
||||
lists={formValues.values.selectedLists}
|
||||
/>
|
||||
|
||||
<MessageEntriesModal
|
||||
opened={showMessageModal}
|
||||
onClose={showMessageModalHandler.toggle}
|
||||
query={query}
|
||||
event={event}
|
||||
<EmailModal
|
||||
opened={sendEmailModal}
|
||||
onClose={sendEmailModalHandler.close}
|
||||
disableUserInput={true}
|
||||
recipients={entries.map(e => e.expand?.user).filter(u => u !== undefined && u !== null) as UserModel[]}
|
||||
content={getEventMailDefaultText(event)}
|
||||
/>
|
||||
|
||||
<EventEntries event={event} entries={entries} refetch={() => query.refetch()}/>
|
||||
|
||||
{query.hasNextPage && (
|
||||
<Center p={"xs"}>
|
||||
<Button
|
||||
|
|
|
@ -35,7 +35,7 @@ export default function SharedEvent() {
|
|||
enabled: !!user
|
||||
})
|
||||
|
||||
const {isPrivilegedUser, canEditEvent} = useEventRights(eventQuery.data)
|
||||
const {canEditEvent} = useEventRights(eventQuery.data)
|
||||
|
||||
if (eventQuery.isLoading) {
|
||||
return <Center h={"100%"}><Loader/></Center>
|
||||
|
@ -108,19 +108,6 @@ export default function SharedEvent() {
|
|||
</Alert>
|
||||
</div>}
|
||||
|
||||
{isPrivilegedUser && <div className={"section-transparent"}>
|
||||
<Alert color={"green"} title={"Du kannst dieses Event und alle Teilnehmenden ansehen"}>
|
||||
<Button
|
||||
component={"a"}
|
||||
href={`/events/${eventId}`}
|
||||
variant={"light"}
|
||||
leftSection={<IconEye/>}
|
||||
>
|
||||
Event ansehen
|
||||
</Button>
|
||||
</Alert>
|
||||
</div>}
|
||||
|
||||
{canEditEvent && <div className={"section-transparent"}>
|
||||
<Alert color={"green"} title={"Du kannst dieses Event bearbeiten"}>
|
||||
<Button
|
||||
|
|
|
@ -1,13 +1,12 @@
|
|||
import {
|
||||
EventListSlotEntriesWithUserModel,
|
||||
EventListSlotEntryModel,
|
||||
EventListSlotModel,
|
||||
EventListSlotsWithEntriesCountModel,
|
||||
EventModel
|
||||
} from "@/models/EventTypes.ts";
|
||||
import {usePB} from "@/lib/pocketbase.tsx";
|
||||
import {useSettings} from "@/lib/settings.ts";
|
||||
import {LdapGroupModel} from "@/models/AuthTypes.ts";
|
||||
import {APP_URL} from "../../../config.ts";
|
||||
|
||||
|
||||
export const useEventRights = (event?: EventModel) => {
|
||||
|
@ -22,15 +21,8 @@ export const useEventRights = (event?: EventModel) => {
|
|||
)
|
||||
const isEventAdmin = !!(user && event && event.eventAdmins.includes(user.id))
|
||||
|
||||
// for this to work the query of the event must include this backrelation expand:
|
||||
// 'privilegedLists.eventListSlots_via_eventList.eventListSlotEntries_via_eventListsSlot'
|
||||
const privilegedUsers = event?.expand?.privilegedLists?.flatMap(pl =>
|
||||
pl?.expand?.eventListSlots_via_eventList.flatMap((els: EventListSlotModel) =>
|
||||
els?.expand?.eventListSlotEntries_via_eventListsSlot.flatMap((e: EventListSlotEntryModel) => e.user))) ?? []
|
||||
|
||||
return {
|
||||
canEditEvent: isEventAdmin || isStex,
|
||||
isPrivilegedUser: user && privilegedUsers.includes(user?.id),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -57,3 +49,21 @@ export const getListSchemas = (slot: EventListSlotEntriesWithUserModel | EventLi
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a default text for a mail to send to a user that has signed up for an event.
|
||||
* @param event The event the user signed up for
|
||||
*/
|
||||
export const getEventMailDefaultText = (event: EventModel) => {
|
||||
return (
|
||||
`<p>Hallo 👋, </p>` +
|
||||
`<p>du hast dich für das Event <b><a href="${APP_URL + `/events/s/${event.id}`}">${event.name}</a></b> angemeldet.</p>` +
|
||||
`<p>Wir haben noch ein paar wichtige️ Informationen für dich: </p>` +
|
||||
`<p></p>` +
|
||||
`<p>...</p>` +
|
||||
`<p></p>` +
|
||||
`<p>Du kannst jederzeit alle deine Event-Anmeldungen <a href="${APP_URL + `/events/entries`}">hier</a> einsehen.</p>` +
|
||||
`<p>Viele Grüße</p>` +
|
||||
`<p>Dein ${event.name} Team 🎉</p>`
|
||||
)
|
||||
}
|
|
@ -1,10 +1,8 @@
|
|||
import {EventCalendar} from "@/pages/events/EventOverview/EventCalendar.tsx";
|
||||
import {Alert, Button, Group, ThemeIcon, Title} from "@mantine/core";
|
||||
import {IconConfetti, IconHandLoveYou, IconMessageCircle, IconSpeakerphone, IconUser} from "@tabler/icons-react";
|
||||
import {IconConfetti, IconHandLoveYou, IconUser} from "@tabler/icons-react";
|
||||
import {usePB} from "@/lib/pocketbase.tsx";
|
||||
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";
|
||||
|
||||
|
@ -55,21 +53,6 @@ export default function HomePage() {
|
|||
</>}
|
||||
</div>
|
||||
|
||||
{user && <>
|
||||
<div className={"section stack"}>
|
||||
<Title order={2}>Ankündigungen</Title>
|
||||
|
||||
<NavButtons buttons={[
|
||||
{title: "Ankündigungen", icon: <IconSpeakerphone/>, to: "/chat/announcements"},
|
||||
{title: "Chat", icon: <IconMessageCircle/>, to: "/chat"},
|
||||
]}/>
|
||||
|
||||
<div className={classes.announcementsContainer}>
|
||||
<Announcements/>
|
||||
</div>
|
||||
</div>
|
||||
</>}
|
||||
|
||||
<div className={"section stack"}>
|
||||
<Title order={2}>StuVe Events</Title>
|
||||
|
||||
|
|
|
@ -1,5 +1,16 @@
|
|||
import ShowDebug, {useShowDebug} from "@/components/ShowDebug.tsx";
|
||||
import {ActionIcon, Alert, Divider, Group, LoadingOverlay, Pagination, Text, TextInput, Title} from "@mantine/core";
|
||||
import {
|
||||
ActionIcon,
|
||||
Alert,
|
||||
Button,
|
||||
Divider,
|
||||
Group,
|
||||
LoadingOverlay,
|
||||
Pagination,
|
||||
Text,
|
||||
TextInput,
|
||||
Title
|
||||
} from "@mantine/core";
|
||||
import {useForm} from "@mantine/form";
|
||||
import {useQuery} from "@tanstack/react-query";
|
||||
import {PocketBaseErrorAlert, usePB} from "@/lib/pocketbase.tsx";
|
||||
|
@ -7,12 +18,19 @@ import {CodeHighlight} from "@mantine/code-highlight";
|
|||
import {IconRefresh} from "@tabler/icons-react";
|
||||
import {RecordListOptions} from "pocketbase";
|
||||
import {useState} from "react";
|
||||
import {useDisclosure} from "@mantine/hooks";
|
||||
import {EmailModal} from "@/components/EmailModal";
|
||||
import {UserModel} from "@/models/AuthTypes.ts";
|
||||
import UserInput from "@/components/users/UserInput.tsx";
|
||||
|
||||
export default function DebugPage() {
|
||||
|
||||
const {showDebug} = useShowDebug()
|
||||
|
||||
const [users, setUsers] = useState<UserModel[]>([])
|
||||
|
||||
const {pb} = usePB()
|
||||
const [opened, handler] = useDisclosure()
|
||||
|
||||
const [page, setPage] = useState(1)
|
||||
|
||||
|
@ -52,6 +70,24 @@ export default function DebugPage() {
|
|||
const result = formValues.values.select ? debugQuery.data?.items.map(i => i[formValues.values.select]) : debugQuery.data?.items
|
||||
|
||||
return <>
|
||||
<UserInput
|
||||
required
|
||||
placeholder={"Empfängerinnen"}
|
||||
variant={"filled"}
|
||||
selectedRecords={users}
|
||||
setSelectedRecords={setUsers}
|
||||
error={formValues.errors.recipients}
|
||||
/>
|
||||
|
||||
<EmailModal
|
||||
opened={opened}
|
||||
onClose={handler.close}
|
||||
recipients={users}
|
||||
description={"Hier kannst du eine Email an einen oder mehrere Empfänger senden."}
|
||||
/>
|
||||
|
||||
<Button onClick={handler.toggle}>Open Email Modal</Button>
|
||||
|
||||
<div className={"section"}>
|
||||
<Title c={"orange"} order={1}>Debug</Title>
|
||||
</div>
|
||||
|
@ -64,21 +100,25 @@ export default function DebugPage() {
|
|||
Über das Select Feld kannst du die Ausgabe auf ein bestimmtes Feld reduzieren, anstatt die gesamten
|
||||
Records zu sehen.
|
||||
</ShowDebug>
|
||||
|
||||
<TextInput
|
||||
label={"Collection Name"}
|
||||
placeholder={"collectionName"}
|
||||
{...formValues.getInputProps("collectionName")}
|
||||
/>
|
||||
|
||||
<TextInput
|
||||
label={"Filter"}
|
||||
placeholder={"filter"}
|
||||
{...formValues.getInputProps("filter")}
|
||||
/>
|
||||
|
||||
<TextInput
|
||||
label={"Sort"}
|
||||
placeholder={"sort"}
|
||||
{...formValues.getInputProps("sort")}
|
||||
/>
|
||||
|
||||
<TextInput
|
||||
label={"Expand"}
|
||||
placeholder={"expand"}
|
||||
|
|
396
yarn.lock
396
yarn.lock
|
@ -209,115 +209,120 @@
|
|||
"@babel/helper-validator-identifier" "^7.24.5"
|
||||
to-fast-properties "^2.0.0"
|
||||
|
||||
"@esbuild/android-arm64@0.18.20":
|
||||
version "0.18.20"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz#984b4f9c8d0377443cc2dfcef266d02244593622"
|
||||
integrity sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==
|
||||
"@esbuild/aix-ppc64@0.19.12":
|
||||
version "0.19.12"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.19.12.tgz#d1bc06aedb6936b3b6d313bf809a5a40387d2b7f"
|
||||
integrity sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA==
|
||||
|
||||
"@esbuild/android-arm@0.18.20":
|
||||
version "0.18.20"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.18.20.tgz#fedb265bc3a589c84cc11f810804f234947c3682"
|
||||
integrity sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==
|
||||
"@esbuild/android-arm64@0.19.12":
|
||||
version "0.19.12"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.19.12.tgz#7ad65a36cfdb7e0d429c353e00f680d737c2aed4"
|
||||
integrity sha512-P0UVNGIienjZv3f5zq0DP3Nt2IE/3plFzuaS96vihvD0Hd6H/q4WXUGpCxD/E8YrSXfNyRPbpTq+T8ZQioSuPA==
|
||||
|
||||
"@esbuild/android-x64@0.18.20":
|
||||
version "0.18.20"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.18.20.tgz#35cf419c4cfc8babe8893d296cd990e9e9f756f2"
|
||||
integrity sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==
|
||||
"@esbuild/android-arm@0.19.12":
|
||||
version "0.19.12"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.19.12.tgz#b0c26536f37776162ca8bde25e42040c203f2824"
|
||||
integrity sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w==
|
||||
|
||||
"@esbuild/darwin-arm64@0.18.20":
|
||||
version "0.18.20"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz#08172cbeccf95fbc383399a7f39cfbddaeb0d7c1"
|
||||
integrity sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==
|
||||
"@esbuild/android-x64@0.19.12":
|
||||
version "0.19.12"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.19.12.tgz#cb13e2211282012194d89bf3bfe7721273473b3d"
|
||||
integrity sha512-3k7ZoUW6Q6YqhdhIaq/WZ7HwBpnFBlW905Fa4s4qWJyiNOgT1dOqDiVAQFwBH7gBRZr17gLrlFCRzF6jFh7Kew==
|
||||
|
||||
"@esbuild/darwin-x64@0.18.20":
|
||||
version "0.18.20"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz#d70d5790d8bf475556b67d0f8b7c5bdff053d85d"
|
||||
integrity sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==
|
||||
"@esbuild/darwin-arm64@0.19.12":
|
||||
version "0.19.12"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.19.12.tgz#cbee41e988020d4b516e9d9e44dd29200996275e"
|
||||
integrity sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g==
|
||||
|
||||
"@esbuild/freebsd-arm64@0.18.20":
|
||||
version "0.18.20"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz#98755cd12707f93f210e2494d6a4b51b96977f54"
|
||||
integrity sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==
|
||||
"@esbuild/darwin-x64@0.19.12":
|
||||
version "0.19.12"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.19.12.tgz#e37d9633246d52aecf491ee916ece709f9d5f4cd"
|
||||
integrity sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A==
|
||||
|
||||
"@esbuild/freebsd-x64@0.18.20":
|
||||
version "0.18.20"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz#c1eb2bff03915f87c29cece4c1a7fa1f423b066e"
|
||||
integrity sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==
|
||||
"@esbuild/freebsd-arm64@0.19.12":
|
||||
version "0.19.12"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.12.tgz#1ee4d8b682ed363b08af74d1ea2b2b4dbba76487"
|
||||
integrity sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA==
|
||||
|
||||
"@esbuild/linux-arm64@0.18.20":
|
||||
version "0.18.20"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz#bad4238bd8f4fc25b5a021280c770ab5fc3a02a0"
|
||||
integrity sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==
|
||||
"@esbuild/freebsd-x64@0.19.12":
|
||||
version "0.19.12"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.19.12.tgz#37a693553d42ff77cd7126764b535fb6cc28a11c"
|
||||
integrity sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg==
|
||||
|
||||
"@esbuild/linux-arm@0.18.20":
|
||||
version "0.18.20"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz#3e617c61f33508a27150ee417543c8ab5acc73b0"
|
||||
integrity sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==
|
||||
"@esbuild/linux-arm64@0.19.12":
|
||||
version "0.19.12"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.19.12.tgz#be9b145985ec6c57470e0e051d887b09dddb2d4b"
|
||||
integrity sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA==
|
||||
|
||||
"@esbuild/linux-ia32@0.18.20":
|
||||
version "0.18.20"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz#699391cccba9aee6019b7f9892eb99219f1570a7"
|
||||
integrity sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==
|
||||
"@esbuild/linux-arm@0.19.12":
|
||||
version "0.19.12"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.19.12.tgz#207ecd982a8db95f7b5279207d0ff2331acf5eef"
|
||||
integrity sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w==
|
||||
|
||||
"@esbuild/linux-loong64@0.18.20":
|
||||
version "0.18.20"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz#e6fccb7aac178dd2ffb9860465ac89d7f23b977d"
|
||||
integrity sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==
|
||||
"@esbuild/linux-ia32@0.19.12":
|
||||
version "0.19.12"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.19.12.tgz#d0d86b5ca1562523dc284a6723293a52d5860601"
|
||||
integrity sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA==
|
||||
|
||||
"@esbuild/linux-mips64el@0.18.20":
|
||||
version "0.18.20"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz#eeff3a937de9c2310de30622a957ad1bd9183231"
|
||||
integrity sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==
|
||||
"@esbuild/linux-loong64@0.19.12":
|
||||
version "0.19.12"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.19.12.tgz#9a37f87fec4b8408e682b528391fa22afd952299"
|
||||
integrity sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA==
|
||||
|
||||
"@esbuild/linux-ppc64@0.18.20":
|
||||
version "0.18.20"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz#2f7156bde20b01527993e6881435ad79ba9599fb"
|
||||
integrity sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==
|
||||
"@esbuild/linux-mips64el@0.19.12":
|
||||
version "0.19.12"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.19.12.tgz#4ddebd4e6eeba20b509d8e74c8e30d8ace0b89ec"
|
||||
integrity sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w==
|
||||
|
||||
"@esbuild/linux-riscv64@0.18.20":
|
||||
version "0.18.20"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz#6628389f210123d8b4743045af8caa7d4ddfc7a6"
|
||||
integrity sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==
|
||||
"@esbuild/linux-ppc64@0.19.12":
|
||||
version "0.19.12"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.19.12.tgz#adb67dadb73656849f63cd522f5ecb351dd8dee8"
|
||||
integrity sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg==
|
||||
|
||||
"@esbuild/linux-s390x@0.18.20":
|
||||
version "0.18.20"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz#255e81fb289b101026131858ab99fba63dcf0071"
|
||||
integrity sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==
|
||||
"@esbuild/linux-riscv64@0.19.12":
|
||||
version "0.19.12"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.19.12.tgz#11bc0698bf0a2abf8727f1c7ace2112612c15adf"
|
||||
integrity sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg==
|
||||
|
||||
"@esbuild/linux-x64@0.18.20":
|
||||
version "0.18.20"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz#c7690b3417af318a9b6f96df3031a8865176d338"
|
||||
integrity sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==
|
||||
"@esbuild/linux-s390x@0.19.12":
|
||||
version "0.19.12"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.19.12.tgz#e86fb8ffba7c5c92ba91fc3b27ed5a70196c3cc8"
|
||||
integrity sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg==
|
||||
|
||||
"@esbuild/netbsd-x64@0.18.20":
|
||||
version "0.18.20"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz#30e8cd8a3dded63975e2df2438ca109601ebe0d1"
|
||||
integrity sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==
|
||||
"@esbuild/linux-x64@0.19.12":
|
||||
version "0.19.12"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.19.12.tgz#5f37cfdc705aea687dfe5dfbec086a05acfe9c78"
|
||||
integrity sha512-B71g1QpxfwBvNrfyJdVDexenDIt1CiDN1TIXLbhOw0KhJzE78KIFGX6OJ9MrtC0oOqMWf+0xop4qEU8JrJTwCg==
|
||||
|
||||
"@esbuild/openbsd-x64@0.18.20":
|
||||
version "0.18.20"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz#7812af31b205055874c8082ea9cf9ab0da6217ae"
|
||||
integrity sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==
|
||||
"@esbuild/netbsd-x64@0.19.12":
|
||||
version "0.19.12"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.19.12.tgz#29da566a75324e0d0dd7e47519ba2f7ef168657b"
|
||||
integrity sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA==
|
||||
|
||||
"@esbuild/sunos-x64@0.18.20":
|
||||
version "0.18.20"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz#d5c275c3b4e73c9b0ecd38d1ca62c020f887ab9d"
|
||||
integrity sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==
|
||||
"@esbuild/openbsd-x64@0.19.12":
|
||||
version "0.19.12"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.19.12.tgz#306c0acbdb5a99c95be98bdd1d47c916e7dc3ff0"
|
||||
integrity sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw==
|
||||
|
||||
"@esbuild/win32-arm64@0.18.20":
|
||||
version "0.18.20"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz#73bc7f5a9f8a77805f357fab97f290d0e4820ac9"
|
||||
integrity sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==
|
||||
"@esbuild/sunos-x64@0.19.12":
|
||||
version "0.19.12"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.19.12.tgz#0933eaab9af8b9b2c930236f62aae3fc593faf30"
|
||||
integrity sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA==
|
||||
|
||||
"@esbuild/win32-ia32@0.18.20":
|
||||
version "0.18.20"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz#ec93cbf0ef1085cc12e71e0d661d20569ff42102"
|
||||
integrity sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==
|
||||
"@esbuild/win32-arm64@0.19.12":
|
||||
version "0.19.12"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.19.12.tgz#773bdbaa1971b36db2f6560088639ccd1e6773ae"
|
||||
integrity sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A==
|
||||
|
||||
"@esbuild/win32-x64@0.18.20":
|
||||
version "0.18.20"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz#786c5f41f043b07afb1af37683d7c33668858f6d"
|
||||
integrity sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==
|
||||
"@esbuild/win32-ia32@0.19.12":
|
||||
version "0.19.12"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.19.12.tgz#000516cad06354cc84a73f0943a4aa690ef6fd67"
|
||||
integrity sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ==
|
||||
|
||||
"@esbuild/win32-x64@0.19.12":
|
||||
version "0.19.12"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.19.12.tgz#c57c8afbb4054a3ab8317591a0b7320360b444ae"
|
||||
integrity sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA==
|
||||
|
||||
"@eslint-community/eslint-utils@^4.2.0", "@eslint-community/eslint-utils@^4.4.0":
|
||||
version "4.4.0"
|
||||
|
@ -576,6 +581,96 @@
|
|||
estree-walker "^2.0.2"
|
||||
picomatch "^2.3.1"
|
||||
|
||||
"@rollup/rollup-android-arm-eabi@4.24.3":
|
||||
version "4.24.3"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.24.3.tgz#49a2a9808074f2683667992aa94b288e0b54fc82"
|
||||
integrity sha512-ufb2CH2KfBWPJok95frEZZ82LtDl0A6QKTa8MoM+cWwDZvVGl5/jNb79pIhRvAalUu+7LD91VYR0nwRD799HkQ==
|
||||
|
||||
"@rollup/rollup-android-arm64@4.24.3":
|
||||
version "4.24.3"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.24.3.tgz#197e3bc01c228d3c23591e0fcedca91f8f398ec1"
|
||||
integrity sha512-iAHpft/eQk9vkWIV5t22V77d90CRofgR2006UiCjHcHJFVI1E0oBkQIAbz+pLtthFw3hWEmVB4ilxGyBf48i2Q==
|
||||
|
||||
"@rollup/rollup-darwin-arm64@4.24.3":
|
||||
version "4.24.3"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.24.3.tgz#16772c0309d0dc3cca716580cdac7a1c560ddf46"
|
||||
integrity sha512-QPW2YmkWLlvqmOa2OwrfqLJqkHm7kJCIMq9kOz40Zo9Ipi40kf9ONG5Sz76zszrmIZZ4hgRIkez69YnTHgEz1w==
|
||||
|
||||
"@rollup/rollup-darwin-x64@4.24.3":
|
||||
version "4.24.3"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.24.3.tgz#4e98120a1c4cda7d4043ccce72347cee53784140"
|
||||
integrity sha512-KO0pN5x3+uZm1ZXeIfDqwcvnQ9UEGN8JX5ufhmgH5Lz4ujjZMAnxQygZAVGemFWn+ZZC0FQopruV4lqmGMshow==
|
||||
|
||||
"@rollup/rollup-freebsd-arm64@4.24.3":
|
||||
version "4.24.3"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.24.3.tgz#27145e414986e216e0d9b9a8d488028f33c39566"
|
||||
integrity sha512-CsC+ZdIiZCZbBI+aRlWpYJMSWvVssPuWqrDy/zi9YfnatKKSLFCe6fjna1grHuo/nVaHG+kiglpRhyBQYRTK4A==
|
||||
|
||||
"@rollup/rollup-freebsd-x64@4.24.3":
|
||||
version "4.24.3"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.24.3.tgz#67e75fd87a903090f038b212273c492e5ca6b32f"
|
||||
integrity sha512-F0nqiLThcfKvRQhZEzMIXOQG4EeX61im61VYL1jo4eBxv4aZRmpin6crnBJQ/nWnCsjH5F6J3W6Stdm0mBNqBg==
|
||||
|
||||
"@rollup/rollup-linux-arm-gnueabihf@4.24.3":
|
||||
version "4.24.3"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.24.3.tgz#bb45ebadbb9496298ab5461373bde357e8f33e88"
|
||||
integrity sha512-KRSFHyE/RdxQ1CSeOIBVIAxStFC/hnBgVcaiCkQaVC+EYDtTe4X7z5tBkFyRoBgUGtB6Xg6t9t2kulnX6wJc6A==
|
||||
|
||||
"@rollup/rollup-linux-arm-musleabihf@4.24.3":
|
||||
version "4.24.3"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.24.3.tgz#384276c23feb0a4d6ffa603a9a760decce8b4118"
|
||||
integrity sha512-h6Q8MT+e05zP5BxEKz0vi0DhthLdrNEnspdLzkoFqGwnmOzakEHSlXfVyA4HJ322QtFy7biUAVFPvIDEDQa6rw==
|
||||
|
||||
"@rollup/rollup-linux-arm64-gnu@4.24.3":
|
||||
version "4.24.3"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.24.3.tgz#89e5a4570ddd9eca908324a6de60bd64f904e3f0"
|
||||
integrity sha512-fKElSyXhXIJ9pqiYRqisfirIo2Z5pTTve5K438URf08fsypXrEkVmShkSfM8GJ1aUyvjakT+fn2W7Czlpd/0FQ==
|
||||
|
||||
"@rollup/rollup-linux-arm64-musl@4.24.3":
|
||||
version "4.24.3"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.24.3.tgz#9ffd7cd6c6c6670d8c039056d6a49ad9f1f66949"
|
||||
integrity sha512-YlddZSUk8G0px9/+V9PVilVDC6ydMz7WquxozToozSnfFK6wa6ne1ATUjUvjin09jp34p84milxlY5ikueoenw==
|
||||
|
||||
"@rollup/rollup-linux-powerpc64le-gnu@4.24.3":
|
||||
version "4.24.3"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.24.3.tgz#4d32ce982e2d25e3b8116336ad5ce6e270b5a024"
|
||||
integrity sha512-yNaWw+GAO8JjVx3s3cMeG5Esz1cKVzz8PkTJSfYzE5u7A+NvGmbVFEHP+BikTIyYWuz0+DX9kaA3pH9Sqxp69g==
|
||||
|
||||
"@rollup/rollup-linux-riscv64-gnu@4.24.3":
|
||||
version "4.24.3"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.24.3.tgz#f43d4e0572397e3d3acd82d77d79ce021dea3310"
|
||||
integrity sha512-lWKNQfsbpv14ZCtM/HkjCTm4oWTKTfxPmr7iPfp3AHSqyoTz5AgLemYkWLwOBWc+XxBbrU9SCokZP0WlBZM9lA==
|
||||
|
||||
"@rollup/rollup-linux-s390x-gnu@4.24.3":
|
||||
version "4.24.3"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.24.3.tgz#264f8a4c206173945bdab2a676d638b7945106a9"
|
||||
integrity sha512-HoojGXTC2CgCcq0Woc/dn12wQUlkNyfH0I1ABK4Ni9YXyFQa86Fkt2Q0nqgLfbhkyfQ6003i3qQk9pLh/SpAYw==
|
||||
|
||||
"@rollup/rollup-linux-x64-gnu@4.24.3":
|
||||
version "4.24.3"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.24.3.tgz#e86172a407b2edd41540ec2ae636e497fadccff6"
|
||||
integrity sha512-mnEOh4iE4USSccBOtcrjF5nj+5/zm6NcNhbSEfR3Ot0pxBwvEn5QVUXcuOwwPkapDtGZ6pT02xLoPaNv06w7KQ==
|
||||
|
||||
"@rollup/rollup-linux-x64-musl@4.24.3":
|
||||
version "4.24.3"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.24.3.tgz#8ae9bf78986d1b16ccbc89ab6f2dfa96807d3178"
|
||||
integrity sha512-rMTzawBPimBQkG9NKpNHvquIUTQPzrnPxPbCY1Xt+mFkW7pshvyIS5kYgcf74goxXOQk0CP3EoOC1zcEezKXhw==
|
||||
|
||||
"@rollup/rollup-win32-arm64-msvc@4.24.3":
|
||||
version "4.24.3"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.24.3.tgz#11d6a59f651a3c2a9e5eaab0a99367b77a29c319"
|
||||
integrity sha512-2lg1CE305xNvnH3SyiKwPVsTVLCg4TmNCF1z7PSHX2uZY2VbUpdkgAllVoISD7JO7zu+YynpWNSKAtOrX3AiuA==
|
||||
|
||||
"@rollup/rollup-win32-ia32-msvc@4.24.3":
|
||||
version "4.24.3"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.24.3.tgz#7ff146e53dc6e388b60329b7ec3335501d2b0f98"
|
||||
integrity sha512-9SjYp1sPyxJsPWuhOCX6F4jUMXGbVVd5obVpoVEi8ClZqo52ViZewA6eFz85y8ezuOA+uJMP5A5zo6Oz4S5rVQ==
|
||||
|
||||
"@rollup/rollup-win32-x64-msvc@4.24.3":
|
||||
version "4.24.3"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.24.3.tgz#7687335781efe6bee14d6ed8eff9746a9f24c9cd"
|
||||
integrity sha512-HGZgRFFYrMrP3TJlq58nR1xy8zHKId25vhmm5S9jETEfDf6xybPxsavFTJaufe2zgOGYJBskGlj49CwtEuFhWQ==
|
||||
|
||||
"@svgr/babel-plugin-add-jsx-attribute@8.0.0":
|
||||
version "8.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-add-jsx-attribute/-/babel-plugin-add-jsx-attribute-8.0.0.tgz#4001f5d5dd87fa13303e36ee106e3ff3a7eb8b22"
|
||||
|
@ -1011,6 +1106,11 @@
|
|||
resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.5.tgz#a6ce3e556e00fd9895dd872dd172ad0d4bd687f4"
|
||||
integrity sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==
|
||||
|
||||
"@types/estree@1.0.6":
|
||||
version "1.0.6"
|
||||
resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.6.tgz#628effeeae2064a1b4e79f78e81d87b7e5fc7b50"
|
||||
integrity sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==
|
||||
|
||||
"@types/file-saver@^2.0.7":
|
||||
version "2.0.7"
|
||||
resolved "https://registry.yarnpkg.com/@types/file-saver/-/file-saver-2.0.7.tgz#8dbb2f24bdc7486c54aa854eb414940bbd056f7d"
|
||||
|
@ -1664,33 +1764,34 @@ error-ex@^1.3.1:
|
|||
dependencies:
|
||||
is-arrayish "^0.2.1"
|
||||
|
||||
esbuild@^0.18.10:
|
||||
version "0.18.20"
|
||||
resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.18.20.tgz#4709f5a34801b43b799ab7d6d82f7284a9b7a7a6"
|
||||
integrity sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==
|
||||
esbuild@^0.19.3:
|
||||
version "0.19.12"
|
||||
resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.19.12.tgz#dc82ee5dc79e82f5a5c3b4323a2a641827db3e04"
|
||||
integrity sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg==
|
||||
optionalDependencies:
|
||||
"@esbuild/android-arm" "0.18.20"
|
||||
"@esbuild/android-arm64" "0.18.20"
|
||||
"@esbuild/android-x64" "0.18.20"
|
||||
"@esbuild/darwin-arm64" "0.18.20"
|
||||
"@esbuild/darwin-x64" "0.18.20"
|
||||
"@esbuild/freebsd-arm64" "0.18.20"
|
||||
"@esbuild/freebsd-x64" "0.18.20"
|
||||
"@esbuild/linux-arm" "0.18.20"
|
||||
"@esbuild/linux-arm64" "0.18.20"
|
||||
"@esbuild/linux-ia32" "0.18.20"
|
||||
"@esbuild/linux-loong64" "0.18.20"
|
||||
"@esbuild/linux-mips64el" "0.18.20"
|
||||
"@esbuild/linux-ppc64" "0.18.20"
|
||||
"@esbuild/linux-riscv64" "0.18.20"
|
||||
"@esbuild/linux-s390x" "0.18.20"
|
||||
"@esbuild/linux-x64" "0.18.20"
|
||||
"@esbuild/netbsd-x64" "0.18.20"
|
||||
"@esbuild/openbsd-x64" "0.18.20"
|
||||
"@esbuild/sunos-x64" "0.18.20"
|
||||
"@esbuild/win32-arm64" "0.18.20"
|
||||
"@esbuild/win32-ia32" "0.18.20"
|
||||
"@esbuild/win32-x64" "0.18.20"
|
||||
"@esbuild/aix-ppc64" "0.19.12"
|
||||
"@esbuild/android-arm" "0.19.12"
|
||||
"@esbuild/android-arm64" "0.19.12"
|
||||
"@esbuild/android-x64" "0.19.12"
|
||||
"@esbuild/darwin-arm64" "0.19.12"
|
||||
"@esbuild/darwin-x64" "0.19.12"
|
||||
"@esbuild/freebsd-arm64" "0.19.12"
|
||||
"@esbuild/freebsd-x64" "0.19.12"
|
||||
"@esbuild/linux-arm" "0.19.12"
|
||||
"@esbuild/linux-arm64" "0.19.12"
|
||||
"@esbuild/linux-ia32" "0.19.12"
|
||||
"@esbuild/linux-loong64" "0.19.12"
|
||||
"@esbuild/linux-mips64el" "0.19.12"
|
||||
"@esbuild/linux-ppc64" "0.19.12"
|
||||
"@esbuild/linux-riscv64" "0.19.12"
|
||||
"@esbuild/linux-s390x" "0.19.12"
|
||||
"@esbuild/linux-x64" "0.19.12"
|
||||
"@esbuild/netbsd-x64" "0.19.12"
|
||||
"@esbuild/openbsd-x64" "0.19.12"
|
||||
"@esbuild/sunos-x64" "0.19.12"
|
||||
"@esbuild/win32-arm64" "0.19.12"
|
||||
"@esbuild/win32-ia32" "0.19.12"
|
||||
"@esbuild/win32-x64" "0.19.12"
|
||||
|
||||
escalade@^3.1.2:
|
||||
version "3.1.2"
|
||||
|
@ -1913,7 +2014,7 @@ fs.realpath@^1.0.0:
|
|||
resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
|
||||
integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==
|
||||
|
||||
fsevents@~2.3.2:
|
||||
fsevents@~2.3.2, fsevents@~2.3.3:
|
||||
version "2.3.3"
|
||||
resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6"
|
||||
integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==
|
||||
|
@ -2991,6 +3092,11 @@ picocolors@^1.0.0:
|
|||
resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c"
|
||||
integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==
|
||||
|
||||
picocolors@^1.1.0:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b"
|
||||
integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==
|
||||
|
||||
picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.3.1:
|
||||
version "2.3.1"
|
||||
resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42"
|
||||
|
@ -3055,7 +3161,7 @@ postcss@^8.3.11:
|
|||
picocolors "^1.0.0"
|
||||
source-map-js "^1.2.0"
|
||||
|
||||
postcss@^8.4.27, postcss@^8.4.31:
|
||||
postcss@^8.4.31:
|
||||
version "8.4.31"
|
||||
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.31.tgz#92b451050a9f914da6755af352bdc0192508656d"
|
||||
integrity sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==
|
||||
|
@ -3064,6 +3170,15 @@ postcss@^8.4.27, postcss@^8.4.31:
|
|||
picocolors "^1.0.0"
|
||||
source-map-js "^1.0.2"
|
||||
|
||||
postcss@^8.4.33:
|
||||
version "8.4.47"
|
||||
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.47.tgz#5bf6c9a010f3e724c503bf03ef7947dcb0fea365"
|
||||
integrity sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==
|
||||
dependencies:
|
||||
nanoid "^3.3.7"
|
||||
picocolors "^1.1.0"
|
||||
source-map-js "^1.2.1"
|
||||
|
||||
prelude-ls@^1.2.1:
|
||||
version "1.2.1"
|
||||
resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396"
|
||||
|
@ -3525,11 +3640,31 @@ rimraf@^3.0.2:
|
|||
dependencies:
|
||||
glob "^7.1.3"
|
||||
|
||||
rollup@^3.27.1:
|
||||
version "3.29.4"
|
||||
resolved "https://registry.yarnpkg.com/rollup/-/rollup-3.29.4.tgz#4d70c0f9834146df8705bfb69a9a19c9e1109981"
|
||||
integrity sha512-oWzmBZwvYrU0iJHtDmhsm662rC15FRXmcjCk1xD771dFDx5jJ02ufAQQTn0etB2emNk4J9EZg/yWKpsn9BWGRw==
|
||||
rollup@^4.2.0:
|
||||
version "4.24.3"
|
||||
resolved "https://registry.yarnpkg.com/rollup/-/rollup-4.24.3.tgz#8b259063740af60b0030315f88665ba2041789b8"
|
||||
integrity sha512-HBW896xR5HGmoksbi3JBDtmVzWiPAYqp7wip50hjQ67JbDz61nyoMPdqu1DvVW9asYb2M65Z20ZHsyJCMqMyDg==
|
||||
dependencies:
|
||||
"@types/estree" "1.0.6"
|
||||
optionalDependencies:
|
||||
"@rollup/rollup-android-arm-eabi" "4.24.3"
|
||||
"@rollup/rollup-android-arm64" "4.24.3"
|
||||
"@rollup/rollup-darwin-arm64" "4.24.3"
|
||||
"@rollup/rollup-darwin-x64" "4.24.3"
|
||||
"@rollup/rollup-freebsd-arm64" "4.24.3"
|
||||
"@rollup/rollup-freebsd-x64" "4.24.3"
|
||||
"@rollup/rollup-linux-arm-gnueabihf" "4.24.3"
|
||||
"@rollup/rollup-linux-arm-musleabihf" "4.24.3"
|
||||
"@rollup/rollup-linux-arm64-gnu" "4.24.3"
|
||||
"@rollup/rollup-linux-arm64-musl" "4.24.3"
|
||||
"@rollup/rollup-linux-powerpc64le-gnu" "4.24.3"
|
||||
"@rollup/rollup-linux-riscv64-gnu" "4.24.3"
|
||||
"@rollup/rollup-linux-s390x-gnu" "4.24.3"
|
||||
"@rollup/rollup-linux-x64-gnu" "4.24.3"
|
||||
"@rollup/rollup-linux-x64-musl" "4.24.3"
|
||||
"@rollup/rollup-win32-arm64-msvc" "4.24.3"
|
||||
"@rollup/rollup-win32-ia32-msvc" "4.24.3"
|
||||
"@rollup/rollup-win32-x64-msvc" "4.24.3"
|
||||
fsevents "~2.3.2"
|
||||
|
||||
rope-sequence@^1.3.0:
|
||||
|
@ -3624,6 +3759,11 @@ source-map-js@^1.0.2:
|
|||
resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c"
|
||||
integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==
|
||||
|
||||
source-map-js@^1.2.1:
|
||||
version "1.2.1"
|
||||
resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.1.tgz#1ce5650fddd87abc099eda37dcff024c2667ae46"
|
||||
integrity sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==
|
||||
|
||||
space-separated-tokens@^2.0.0:
|
||||
version "2.0.2"
|
||||
resolved "https://registry.yarnpkg.com/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz#1ecd9d2350a3844572c3f4a312bceb018348859f"
|
||||
|
@ -3966,16 +4106,16 @@ vite-plugin-svgr@^4.2.0:
|
|||
"@svgr/core" "^8.1.0"
|
||||
"@svgr/plugin-jsx" "^8.1.0"
|
||||
|
||||
vite@^4.4.5:
|
||||
version "4.5.0"
|
||||
resolved "https://registry.yarnpkg.com/vite/-/vite-4.5.0.tgz#ec406295b4167ac3bc23e26f9c8ff559287cff26"
|
||||
integrity sha512-ulr8rNLA6rkyFAlVWw2q5YJ91v098AFQ2R0PRFwPzREXOUJQPtFUG0t+/ZikhaOCDqFoDhN6/v8Sq0o4araFAw==
|
||||
vite@5.1.0-beta.2:
|
||||
version "5.1.0-beta.2"
|
||||
resolved "https://registry.yarnpkg.com/vite/-/vite-5.1.0-beta.2.tgz#ca2bf7504952f8e0384ed5d2ed2e13c0ab4fbdd8"
|
||||
integrity sha512-FpzQ6WBc2x7F71QmLEP6nCaFy6IlhkfrzYuLEd4Ax8mGju+BncnggAe3e4j6wLnh8FA7GkCxJu1ds2ZY0+Ws4A==
|
||||
dependencies:
|
||||
esbuild "^0.18.10"
|
||||
postcss "^8.4.27"
|
||||
rollup "^3.27.1"
|
||||
esbuild "^0.19.3"
|
||||
postcss "^8.4.33"
|
||||
rollup "^4.2.0"
|
||||
optionalDependencies:
|
||||
fsevents "~2.3.2"
|
||||
fsevents "~2.3.3"
|
||||
|
||||
w3c-keyname@^2.2.0:
|
||||
version "2.2.8"
|
||||
|
|
Loading…
Reference in New Issue