diff --git a/config.ts b/config.ts index 4091085..be50288 100644 --- a/config.ts +++ b/config.ts @@ -9,5 +9,5 @@ export const PB_STORAGE_KEY = "stuve-it-login-record" // general export const APP_NAME = "StuVe IT" -export const APP_VERSION = "0.5.0 (beta)" +export const APP_VERSION = "0.7.0 (beta)" export const APP_URL = "https://it.stuve.uni-ulm.de" \ No newline at end of file diff --git a/package.json b/package.json index 42a5c51..1f575e7 100644 --- a/package.json +++ b/package.json @@ -27,11 +27,13 @@ "@tiptap/extension-collaboration": "^2.3.0", "@tiptap/extension-collaboration-cursor": "^2.3.0", "@tiptap/extension-link": "^2.3.0", + "@tiptap/extension-mention": "^2.4.0", "@tiptap/extension-placeholder": "^2.3.0", "@tiptap/extension-underline": "^2.3.0", "@tiptap/pm": "^2.3.0", "@tiptap/react": "^2.3.0", "@tiptap/starter-kit": "^2.3.0", + "@tiptap/suggestion": "^2.4.0", "@types/react-big-calendar": "^1.8.9", "clsx": "^2.1.1", "dayjs": "^1.11.10", @@ -43,11 +45,14 @@ "react-big-calendar": "^1.11.3", "react-dom": "^18.2.0", "react-dropzone": "^14.2.3", + "react-infinite-scroll-component": "^6.1.0", + "react-intersection-observer": "^9.10.2", "react-markdown": "^9.0.1", "react-router-dom": "^6.20.0", "recoil": "^0.7.7", "remark-gfm": "^4.0.0", "sanitize-html": "^2.13.0", + "tippy.js": "^6.3.7", "tiptap": "^1.32.2", "zod": "^3.22.4" }, diff --git a/src/Router.tsx b/src/Router.tsx index 0c1edb7..99ec223 100644 --- a/src/Router.tsx +++ b/src/Router.tsx @@ -5,6 +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"; const router = createBrowserRouter([ { @@ -23,6 +25,14 @@ const router = createBrowserRouter([ path: "events/*", element: , }, + { + path: "chat/*", + element: , + }, + { + path: "debug", + element: + }, { path: "util", children: [ diff --git a/src/components/input/ColorSchemeSwitch.tsx b/src/components/input/ColorSchemeSwitch.tsx new file mode 100644 index 0000000..1cf3d93 --- /dev/null +++ b/src/components/input/ColorSchemeSwitch.tsx @@ -0,0 +1,31 @@ +import {rem, Switch, useMantineColorScheme, useMantineTheme} from "@mantine/core"; +import {IconMoonStars, IconSun} from "@tabler/icons-react"; + +export default function ColorSchemeSwitch() { + const {colorScheme, toggleColorScheme} = useMantineColorScheme() + const theme = useMantineTheme() + + const sunIcon = ( + + ) + + const moonIcon = ( + + ) + + return +} \ No newline at end of file diff --git a/src/components/input/Editor/index.module.css b/src/components/input/Editor/index.module.css index f649171..b117239 100644 --- a/src/components/input/Editor/index.module.css +++ b/src/components/input/Editor/index.module.css @@ -21,7 +21,6 @@ overflow: auto; } - .toolbar { display: flex; flex-direction: row; diff --git a/src/components/input/Editor/index.tsx b/src/components/input/Editor/index.tsx index 58d202b..5276fcd 100644 --- a/src/components/input/Editor/index.tsx +++ b/src/components/input/Editor/index.tsx @@ -1,4 +1,4 @@ -import {BubbleMenu, Editor, useEditor} from "@tiptap/react"; +import {BubbleMenu, Editor, Extension, useEditor} from "@tiptap/react"; import {StarterKit} from "@tiptap/starter-kit"; import {Underline} from "@tiptap/extension-underline"; import Placeholder from '@tiptap/extension-placeholder'; @@ -14,6 +14,9 @@ const Bubble = ({editor}: { editor: Editor }) => ( + + + ) @@ -29,6 +32,7 @@ const Toolbar = ({fullToolbar}: { fullToolbar: boolean, editor: Editor }) => ( + @@ -88,6 +92,7 @@ export default function TextEditor({ hideToolbar, noBorder, disabled, + modEnter, ...props }: { value: string; @@ -98,6 +103,7 @@ export default function TextEditor({ hideToolbar?: boolean; noBorder?: boolean; disabled?: boolean; + modEnter?: () => void; } & Omit) { const editor = useEditor({ @@ -105,7 +111,17 @@ export default function TextEditor({ StarterKit, Underline, Link, - Placeholder.configure({placeholder: placeholder}) + Placeholder.configure({placeholder: placeholder}), + Extension.create({ + addKeyboardShortcuts() { + return { + 'Mod-Enter': () => { + modEnter?.() + return modEnter !== undefined + }, + } + }, + }) ], editable: !disabled, content: value, diff --git a/src/components/layout/footer/index.module.css b/src/components/layout/footer/index.module.css index 4bedc5f..58b416e 100644 --- a/src/components/layout/footer/index.module.css +++ b/src/components/layout/footer/index.module.css @@ -3,6 +3,38 @@ display: flex; flex-direction: column; gap: var(--gap); + z-index: 1000; + + @media (max-width: $mantine-breakpoint-sm) { + padding: calc(var(--padding)/2) var(--mantine-spacing-xl); + } + + /*noinspection CssInvalidFunction*/ + //box-shadow: 0 0 5px 10px light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-9));; +} + +.bottomContainer { + display: flex; + flex-direction: row; + gap: var(--gap); + color: var(--mantine-color-dimmed); + + & > :last-child { + text-decoration: underline; + } +} + +.bottomText { + font-size: var(--mantine-font-size-sm); +} + +.underline { + cursor: pointer; + text-decoration: underline; + + &:hover { + text-decoration: none; + } } .logo { @@ -10,12 +42,17 @@ flex-direction: row; gap: var(--gap); justify-content: center; + align-items: center; + + @media (min-width: $mantine-breakpoint-sm) { + display: none !important; + } } .title { - color: var(--mantine-color-text); - font-size: var(--mantine-font-size-lg); - font-weight: 600; + color: var(--mantine-color-dimmed); + font-size: var(--mantine-font-size-xs); + font-weight: 500; } @@ -60,13 +97,3 @@ } } -.rights { - color: var(--mantine-color-dimmed); - font-size: var(--mantine-font-size-sm); - - & > span { - [data-apiishealthy="true"] { - color: var(--mantine-color-red-filled); - } - } -} \ No newline at end of file diff --git a/src/components/layout/footer/index.tsx b/src/components/layout/footer/index.tsx index 69f17d7..5b0e768 100644 --- a/src/components/layout/footer/index.tsx +++ b/src/components/layout/footer/index.tsx @@ -1,72 +1,81 @@ import classes from "./index.module.css"; import {APP_NAME, APP_VERSION} from "../../../../config.ts"; -import {Anchor, Divider, Image} from "@mantine/core"; -import {Link} from "react-router-dom"; +import {Menu, UnstyledButton, useMantineColorScheme} from "@mantine/core"; +import {NavLink} from "react-router-dom"; import {usePB} from "@/lib/pocketbase.tsx"; +import {IconSectionSign} from "@tabler/icons-react"; + +import StuVeLogo from "~/stuve-logo.svg?react"; export default function Footer() { const {apiIsHealthy} = usePB() + const {colorScheme, toggleColorScheme} = useMantineColorScheme() + const currentYear = new Date().getFullYear() return ( -
-
-
- {"StuVe - -
{APP_NAME}
-
- -
-
Über
- -

Version {APP_VERSION}

- -

Entwickelt vom StuVe Computer Referat

- - Source Code - - Issues -
- -
-
Made with
- - Mantine - - React Query - - PocketBase - - ... - and much more -
- - {/* only these links will show on mobile */} -
-
Rechtliches
- - AGB der StuVe - - Datenschutzerklärung - - Impressum +
+
+ +
+ © {currentYear} {APP_NAME} - {APP_VERSION}
- -

© 2024 {APP_NAME}. All rights reserved. - {" "} - - {apiIsHealthy ? "Das Backend ist erreichbar." : "Das Backend ist nicht erreichbar!"} - -

- - -
+
+
{APP_VERSION}
+ • +
© {currentYear} {APP_NAME}
+ • +
+ {apiIsHealthy ? "Backend erreichbar" : "Backend nicht erreichbar"} +
+ • + + {colorScheme === "dark" ? "Heller Modus" : "Dunkler Modus"} + + • + + Source Code + + • + + +
+ Rechtliches +
+
+ + } + component={NavLink} + to={"/legal/imprint"} + > + Impressum + + } + component={NavLink} + to={"/legal/privacy-policy"} + > + Datenschutzerklärung + + } + component={NavLink} + to={"/legal/terms-and-conditions"} + > + AGB + + +
+
+ ) } \ No newline at end of file diff --git a/src/components/layout/index.module.css b/src/components/layout/index.module.css index 4f0faf6..a1bd993 100644 --- a/src/components/layout/index.module.css +++ b/src/components/layout/index.module.css @@ -10,16 +10,18 @@ } .body { - overflow: auto; + overflow: hidden; height: 100%; width: 100%; display: flex; flex-direction: column; justify-content: space-between; - padding: 0 var(--gap); } .content { + position: relative; + overflow: scroll; + padding: 0 var(--gap); padding-top: var(--gap); & > * { diff --git a/src/components/layout/index.tsx b/src/components/layout/index.tsx index 6720ca3..97cdc7f 100644 --- a/src/components/layout/index.tsx +++ b/src/components/layout/index.tsx @@ -3,12 +3,13 @@ import {Outlet} from "react-router-dom"; import classes from "./index.module.css"; import Footer from "./footer"; -import LoginModal from "@/components/auth/modals/LoginModal.tsx"; -import UserMenuModal from "@/components/auth/modals/UserMenuModal.tsx"; -import RegisterModal from "@/components/auth/modals/RegisterModal.tsx"; -import EmailTokenVerification from "@/components/auth/modals/EmailTokenVerification.tsx"; -import ForgotPasswordModal from "@/components/auth/modals/ForgotPasswordModal.tsx"; -import ChangeEmailModal from "@/components/auth/modals/ChangeEmailModal.tsx"; +import LoginModal from "@/components/users/modals/LoginModal.tsx"; +import UserMenuModal from "@/components/users/modals/UserMenuModal.tsx"; +import RegisterModal from "@/components/users/modals/RegisterModal.tsx"; +import EmailTokenVerification from "@/components/users/modals/EmailTokenVerification.tsx"; +import ForgotPasswordModal from "@/components/users/modals/ForgotPasswordModal.tsx"; +import ChangeEmailModal from "@/components/users/modals/ChangeEmailModal.tsx"; +import ShowMessagesModal from "@/components/users/modals/ShowMessagesModal"; export default function Layout({hideNav}: { hideNav?: boolean }) { return
@@ -20,9 +21,10 @@ export default function Layout({hideNav}: { hideNav?: boolean }) { + -
-
+
+
diff --git a/src/components/layout/nav/MenuItems.tsx b/src/components/layout/nav/MenuItems.tsx index 5f3a86e..c50ffd2 100644 --- a/src/components/layout/nav/MenuItems.tsx +++ b/src/components/layout/nav/MenuItems.tsx @@ -1,7 +1,17 @@ import {Menu} from "@mantine/core"; import {NavLink} from "react-router-dom"; import {Fragment} from "react"; -import {IconConfetti, IconHome, IconList, IconQrcode} from "@tabler/icons-react"; +import { + IconBug, + IconConfetti, + IconHome, + IconList, + IconMessageCircle, + IconQrcode, + IconSectionSign, + IconSpeakerphone +} from "@tabler/icons-react"; +import {useShowDebug} from "@/components/ShowDebug.tsx"; const NavItems = [ @@ -14,6 +24,18 @@ const NavItems = [ description: "Home", link: "/" }, + { + title: "Nachrichten", + icon: IconMessageCircle, + description: "Nachrichten", + link: "/chat" + }, + { + title: "Ankündigungen", + icon: IconSpeakerphone, + description: "Ankündigungen", + link: "/chat/announcements" + } ] }, { @@ -31,7 +53,7 @@ const NavItems = [ icon: IconList, description: "Deine Anmeldungen bei Events.", link: "/events/entries" - }, + } ] }, { @@ -45,11 +67,61 @@ const NavItems = [ } ] }, + + { + section: "Rechtliches", + items: [ + { + title: "Impressum", + icon: IconSectionSign, + description: "Impressum", + link: "/legal/imprint" + }, + { + title: "Datenschutzerklärung", + icon: IconSectionSign, + description: "Datenschutzerklärung", + link: "/legal/privacy-policy" + }, + { + title: "AGB", + icon: IconSectionSign, + description: "AGB", + link: "/legal/terms-and-conditions" + } + ] + } +] + +const DebugMenuItems = [ + { + section: "Debug", + items: [ + { + title: "Debug Seite", + icon: IconBug, + description: "Debug", + link: "/debug", + color: "orange" + } + ] + } ] export default function MenuItems() { + + const {showDebug} = useShowDebug() + + let nav + + if (showDebug) { + nav = [...NavItems, ...DebugMenuItems] + } else { + nav = NavItems + } + return <> - {NavItems.map((section, index) => ( + {nav.map((section, index) => ( {section.section} @@ -63,6 +135,7 @@ export default function MenuItems() { component={NavLink} to={item.link} aria-label={item.description} + color={'color' in item ? item.color as string : undefined} > {item.title} diff --git a/src/components/layout/nav/index.module.css b/src/components/layout/nav/index.module.css index e1968e6..13c0236 100644 --- a/src/components/layout/nav/index.module.css +++ b/src/components/layout/nav/index.module.css @@ -22,6 +22,16 @@ .title { font-size: var(--mantine-h2-font-size); font-weight: bold; + + transition: font-size 0.3s ease-in-out; + + @media (max-width: 768px) { + font-size: var(--mantine-h3-font-size); + } + + @media (max-width: 576px) { + font-size: var(--mantine-h4-font-size); + } } .actionIcons { diff --git a/src/components/layout/nav/index.tsx b/src/components/layout/nav/index.tsx index 5a453cc..f2706a1 100644 --- a/src/components/layout/nav/index.tsx +++ b/src/components/layout/nav/index.tsx @@ -1,16 +1,15 @@ import {usePB} from "@/lib/pocketbase.tsx"; import classes from "./index.module.css"; -import {ActionIcon, Image, Menu, ThemeIcon, useMantineColorScheme} from "@mantine/core"; -import {IconChevronDown, IconLogin, IconMoon, IconSun, IconUserStar} from "@tabler/icons-react"; +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/auth/modals/hooks.ts"; +import {useLogin, useUserMenu} from "@/components/users/modals/hooks.ts"; +import ChatNavIcon from "@/pages/chat/components/ChatNavIcon.tsx"; export default function NavBar() { const {user} = usePB() - const {colorScheme, toggleColorScheme} = useMantineColorScheme() - const {handler: userMenuHandler} = useUserMenu() const {handler: loginHandler} = useLogin() @@ -46,27 +45,18 @@ export default function NavBar() {
- - {colorScheme === "dark" ? - - : - - } - - {user ? - - - + <> + + + + + : ( - }> + }> { users.map((u) => ( diff --git a/src/components/auth/modals/ChangeEmailModal.tsx b/src/components/users/modals/ChangeEmailModal.tsx similarity index 92% rename from src/components/auth/modals/ChangeEmailModal.tsx rename to src/components/users/modals/ChangeEmailModal.tsx index 14f0a99..4081aac 100644 --- a/src/components/auth/modals/ChangeEmailModal.tsx +++ b/src/components/users/modals/ChangeEmailModal.tsx @@ -1,4 +1,4 @@ -import {useChangeEmail} from "@/components/auth/modals/hooks.ts"; +import {useChangeEmail} from "@/components/users/modals/hooks.ts"; import {isEmail, useForm} from "@mantine/form"; import {Button, Center, Group, Modal, PasswordInput, TextInput} from "@mantine/core"; import {PocketBaseErrorAlert, usePB} from "@/lib/pocketbase.tsx"; @@ -8,7 +8,8 @@ import {showSuccessNotification} from "@/components/util.tsx"; import {useSearchParams} from "react-router-dom"; import EmailSVG from "@/illustrations/email.svg?react" -import PromptLoginModal from "@/components/auth/modals/PromptLoginModal.tsx"; +import PromptLoginModal from "@/components/users/modals/PromptLoginModal.tsx"; +import ShowHelp from "@/components/ShowHelp.tsx"; export const CHANGE_EMAIL_TOKEN_KEY = "changeEmailToken" @@ -48,6 +49,10 @@ const RequestEmailChangeModal = ({open, onClose}: { + + Nur Gast Accounts können ihre E-Mail ändern. + + + + Nur Gast Accounts können ihre E-Mail ändern. + + + + Nur Gast Accounts können ihr Passwort zurücksetzen. + Wenn du dein StuVe IT Account Passwort vergessen hast, wende dich bitte an das C-Ref. + + + + Nur Gast Accounts können ihr Passwort zurücksetzen. + Wenn du dein StuVe IT Account Passwort vergessen hast, wende dich bitte an das C-Ref. + + - + { + formValues.values.authMethod === "guest" && + + } +
+ +
+ + +} \ No newline at end of file diff --git a/src/components/auth/modals/UserMenuModal.tsx b/src/components/users/modals/UserMenuModal.tsx similarity index 77% rename from src/components/auth/modals/UserMenuModal.tsx rename to src/components/users/modals/UserMenuModal.tsx index 9548a81..cbe5750 100644 --- a/src/components/auth/modals/UserMenuModal.tsx +++ b/src/components/users/modals/UserMenuModal.tsx @@ -1,8 +1,8 @@ -import {useChangeEmail, useForgotPassword, useUserMenu} from "@/components/auth/modals/hooks.ts"; +import {useChangeEmail, useForgotPassword, useUserMenu} from "@/components/users/modals/hooks.ts"; import {usePB} from "@/lib/pocketbase.tsx"; import {useShowHelp} from "@/components/ShowHelp.tsx"; import ShowDebug, {useShowDebug} from "@/components/ShowDebug.tsx"; -import {ActionIcon, Code, Divider, Group, Modal, Switch, Text, ThemeIcon, Title, Tooltip} from "@mantine/core"; +import {ActionIcon, Code, Divider, Group, Modal, Switch, Text, ThemeIcon, Title, Tooltip,} from "@mantine/core"; import classes from "@/components/layout/nav/index.module.css"; import { IconAt, @@ -11,9 +11,11 @@ import { IconMailCog, IconPassword, IconServer, - IconServerOff, IconUser + IconServerOff, + IconUser } from "@tabler/icons-react"; -import LdapGroupsDisplay from "@/components/auth/LdapGroupsDisplay.tsx"; +import LdapGroupsDisplay from "@/components/users/LdapGroupsDisplay.tsx"; +import ColorSchemeSwitch from "@/components/input/ColorSchemeSwitch.tsx"; export default function UserMenuModal() { const {value, handler} = useUserMenu() @@ -55,9 +57,9 @@ export default function UserMenuModal() { - - {user?.REALM === "LDAP" ? "StuVe IT Account" : "Gast Account"} - + + {user?.REALM === "LDAP" ? "StuVe IT Account" : "Gast Account"} +
@@ -68,7 +70,6 @@ export default function UserMenuModal() { - {user?.email} @@ -107,7 +108,6 @@ export default function UserMenuModal() { > - ) : ( + + + - - { - handler.close() - changeEmailHandler.open() - }} - > - - - - - { - handler.close() - passwordResetHandler.open() - }} - > - - - + {user?.REALM === "GUEST" && <> + + { + handler.close() + changeEmailHandler.open() + }} + > + + + + + + { + handler.close() + passwordResetHandler.open() + }} + > + + + + } { const value = searchParams.get(key) === "true" - const open = () => { + const open = () => { setSearchParams(prev => { const newParams = new URLSearchParams(prev); newParams.set(key, "true") return newParams - }, { replace: true }) + }, {replace: true}) } const close = () => { @@ -18,7 +18,7 @@ export const useSearchParamToggle = (key: string) => { const newParams = new URLSearchParams(prev); newParams.delete(key); return newParams; - }, { replace: true }) + }, {replace: true}) } return { @@ -31,6 +31,36 @@ export const useSearchParamToggle = (key: string) => { } } +export const useSearchParamsValue = (key: string, initialValue: string | null) => { + const [searchParams, setSearchParams] = useSearchParams() + + const value = searchParams.get(key) ?? initialValue + + const setValue = (value: string) => { + setSearchParams(prev => { + const newParams = new URLSearchParams(prev) + newParams.set(key, value) + return newParams + }, {replace: true}) + } + + const deleteValue = () => { + setSearchParams(prev => { + const newParams = new URLSearchParams(prev) + newParams.delete(key); + return newParams; + }, {replace: true}) + } + + return { + value: value, + handler: { + setValue, + delete: deleteValue, + } + } +} + export const useLogin = () => useSearchParamToggle("login") export const useRegister = () => useSearchParamToggle("register") @@ -39,4 +69,6 @@ export const useUserMenu = () => useSearchParamToggle("userMenu") export const useForgotPassword = () => useSearchParamToggle("forgotPassword") -export const useChangeEmail = () => useSearchParamToggle("changeEmail") \ No newline at end of file +export const useChangeEmail = () => useSearchParamToggle("changeEmail") + +export const useShowMessages = () => useSearchParamToggle("showMessages") \ No newline at end of file diff --git a/src/components/auth/modals/util.tsx b/src/components/users/modals/util.tsx similarity index 100% rename from src/components/auth/modals/util.tsx rename to src/components/users/modals/util.tsx diff --git a/src/models/EventTypes.ts b/src/models/EventTypes.ts index 37a0bc3..ea690a9 100644 --- a/src/models/EventTypes.ts +++ b/src/models/EventTypes.ts @@ -9,7 +9,6 @@ export type EventModel = { startDate: string; endDate: string; eventAdmins: string[]; - eventListAdmins: string[]; img: string | null; // png, jpg, gif location: string | null; isStuveEvent: boolean; @@ -18,9 +17,10 @@ export type EventModel = { eventLinks: EventLink[]; defaultEntryQuestionSchema: FormSchema | null; defaultEntryStatusSchema: FormSchema | null; + privilegedLists: string[]; expand?: { eventAdmins: UserModal[] | null; - eventListAdmins: UserModal[] | null; + privilegedLists: EventListModel[] | null; } } & RecordModel diff --git a/src/models/MessageTypes.ts b/src/models/MessageTypes.ts new file mode 100644 index 0000000..d28af7a --- /dev/null +++ b/src/models/MessageTypes.ts @@ -0,0 +1,33 @@ +import {RecordModel} from "pocketbase"; +import {UserModal} from "@/models/AuthTypes.ts"; + +export type MessagesModel = { + sender: string + recipients: string[] + thread: string | null + + subject: string | null + content: string + + repliedTo: string | null + + isAnnouncement: boolean | null + + expand: { + sender: UserModal + recipients: UserModal[] + thread: MessageThreadsModel | null + repliedTo: MessagesModel | null + } +} & RecordModel + +export type MessageThreadsModel = { + name: string + participants: string[] + img: string | null + systemThread: boolean | null + + expand: { + participants: UserModal[] + } +} & RecordModel diff --git a/src/models/index.ts b/src/models/index.ts index 311dedf..f6342e4 100644 --- a/src/models/index.ts +++ b/src/models/index.ts @@ -8,6 +8,7 @@ import { EventListSlotsWithEntriesCountModel, EventModel } from "./EventTypes.ts"; +import {MessagesModel, MessageThreadsModel} from "@/models/MessageTypes.ts"; export type SettingsModel = { key: ['privacyPolicy', 'agb', 'stexGroup', 'stuveEventQuestions'] @@ -26,6 +27,10 @@ export type LegalSettingsModal = { export interface TypedPocketBase extends PocketBase { collection(idOrName: string): RecordService + collection(idOrName: 'messages'): RecordService + + collection(idOrName: 'messageThreads'): RecordService + collection(idOrName: 'settings'): RecordService collection(idOrName: 'legalSettings'): RecordService diff --git a/src/pages/chat/ChatRouter.module.css b/src/pages/chat/ChatRouter.module.css new file mode 100644 index 0000000..c04713f --- /dev/null +++ b/src/pages/chat/ChatRouter.module.css @@ -0,0 +1,28 @@ +.container { + max-height: 100%; + position: relative; + overflow: hidden; + margin: 0 auto; + max-width: var(--max-content-width); +} + +.grid { + flex-grow: 1; + overflow-y: hidden; + display: flex; + flex-direction: row; + gap: var(--gap); + + @media (min-width: 768px) { + & > :first-child { + max-width: 500px; + width: 30%; + min-width: 300px; + } + } + + & > :last-child { + flex-grow: 1; + overflow: hidden; + } +} \ No newline at end of file diff --git a/src/pages/chat/ChatRouter.tsx b/src/pages/chat/ChatRouter.tsx new file mode 100644 index 0000000..d9a2ba4 --- /dev/null +++ b/src/pages/chat/ChatRouter.tsx @@ -0,0 +1,57 @@ +import {Anchor, Breadcrumbs, Center} from "@mantine/core"; +import {Link, Outlet, Route, Routes} from "react-router-dom"; +import classes from "./ChatRouter.module.css"; + +import ConversationSvg from "@/illustrations/conversation.svg?react"; +import {useMediaQuery} from "@mantine/hooks"; +import MessageThreadsList from "@/pages/chat/MessageThreadsList.tsx"; +import MessageThreadView from "@/pages/chat/MessageThreadView.tsx"; +import {usePB} from "@/lib/pocketbase.tsx"; +import {useLogin} from "@/components/users/modals/hooks.ts"; +import Announcements from "@/pages/chat/components/Announcements.tsx"; + +const ChatIndex = () => { + return
+ +
+} + +export default function ChatRouter() { + + const isMobile = useMediaQuery("(max-width: 768px)") + const {user} = usePB() + + const {handler} = useLogin() + + if (!user) { + handler.toggle() + return null + } + + return
+
+ {[ + {title: "Home", to: "/"}, + {title: "Nachrichten", to: "/chat"}, + ].map(({title, to}) => ( + + {title} + + ))} +
+ +
+ + + + }/> + + + + {!isMobile && }/>} + }/> + }/> + +
+
+} \ No newline at end of file diff --git a/src/pages/chat/MessageThreadView.module.css b/src/pages/chat/MessageThreadView.module.css new file mode 100644 index 0000000..6b89a86 --- /dev/null +++ b/src/pages/chat/MessageThreadView.module.css @@ -0,0 +1,5 @@ +.backIcon { + @media (min-width: 768px) { + display: none; + } +} \ No newline at end of file diff --git a/src/pages/chat/MessageThreadView.tsx b/src/pages/chat/MessageThreadView.tsx new file mode 100644 index 0000000..b1ddb76 --- /dev/null +++ b/src/pages/chat/MessageThreadView.tsx @@ -0,0 +1,134 @@ +import {Link, useNavigate, useParams} from "react-router-dom"; +import {useMutation, useQuery} from "@tanstack/react-query"; +import {PocketBaseErrorAlert, usePB} from "@/lib/pocketbase.tsx"; +import {ActionIcon, Alert, Button, Center, Collapse, Group, Loader, Text} from "@mantine/core"; +import PBAvatar from "@/components/PBAvatar.tsx"; +import {IconChevronLeft, IconEdit, IconInfoCircle, IconTrash} from "@tabler/icons-react"; +import {useDisclosure} from "@mantine/hooks"; +import UsersDisplay from "@/components/users/UsersDisplay.tsx"; +import UpsertThreadForm from "@/pages/chat/components/UpsertThreadForm.tsx"; +import Messages from "@/pages/chat/components/Messages.tsx"; +import {useConfirmModal} from "@/components/ConfirmModal.tsx"; +import classes from './MessageThreadView.module.css'; + +export default function MessageThreadView() { + const {threadId} = useParams() as { threadId: string } + + const {pb} = usePB() + + const [showEditThread, showEditThreadHandler] = useDisclosure(false) + + const [showThreadInfo, showThreadInfoHandler] = useDisclosure(false) + + const query = useQuery({ + queryKey: ["messageThreads", threadId], + queryFn: async () => ( + await pb.collection("messageThreads").getOne(threadId, { + expand: "participants" + }) + ) + }) + + const navigate = useNavigate() + + const deleteThreadMutation = useMutation({ + mutationFn: async () => { + await pb.collection("messageThreads").delete(threadId) + }, + onSuccess: () => { + navigate("/chat") + } + }) + + const {toggleConfirmModal, ConfirmModal} = useConfirmModal({ + title: "Thread löschen", + description: "Bist du sicher, dass du diesen Thread löschen möchtest?", + onConfirm: () => deleteThreadMutation.mutate() + + }) + + if (query.isError) return ( + + ) + + if (query.isLoading || !query.data) return ( +
+ +
+ ) + + const thread = query.data + + return
+ + + + + + + + + + {thread.name} + + + + + + + +
+ { + thread.expand.participants && ( + + ) + } + {!showEditThread && + + + + + + + + } + + { + showEditThreadHandler.toggle() + query.refetch() + }} onCancel={showEditThreadHandler.toggle}/> + +
+
+
+ + +
+} \ No newline at end of file diff --git a/src/pages/chat/MessageThreadsList.module.css b/src/pages/chat/MessageThreadsList.module.css new file mode 100644 index 0000000..8731db3 --- /dev/null +++ b/src/pages/chat/MessageThreadsList.module.css @@ -0,0 +1,49 @@ +.container { + width: 100%; + transition: width 0.3s ease +} + +.announcementLink { + display: flex; + flex-direction: row; + align-items: center; + padding: calc(var(--padding) / 2); + gap: var(--gap); + border: var(--border); + border-radius: var(--border-radius); + background-color: var(--mantine-color-body); +} + +.threadText { + color: var(--mantine-color-dimmed); + + width: calc(100% - 40px); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + + &[data-active="true"] { + color: var(--mantine-primary-color-5); + font-weight: 600; + } +} + +.threadLink { + display: flex; + flex-direction: row; + align-items: center; + padding: calc(var(--padding) / 2); + gap: var(--gap); +} + +.threadsContainer { + //flex-grow: 1; + overflow: auto; + border: var(--border); + border-radius: var(--border-radius); + background-color: var(--mantine-color-body); + + & > :not(:last-child) { + border-bottom: var(--border); + } +} \ No newline at end of file diff --git a/src/pages/chat/MessageThreadsList.tsx b/src/pages/chat/MessageThreadsList.tsx new file mode 100644 index 0000000..25e18fe --- /dev/null +++ b/src/pages/chat/MessageThreadsList.tsx @@ -0,0 +1,140 @@ +import {MessageThreadsModel} from "@/models/MessageTypes.ts"; +import { + ActionIcon, + Alert, + Button, + Center, + Collapse, + Divider, + Loader, + Stack, + Text, + TextInput, + ThemeIcon, + UnstyledButton +} from "@mantine/core"; +import {NavLink} from "react-router-dom"; +import classes from "@/pages/chat/MessageThreadsList.module.css"; +import PBAvatar from "@/components/PBAvatar.tsx"; +import {IconArrowDown, IconMinus, IconNeedleThread, IconPlus, IconSpeakerphone} from "@tabler/icons-react"; +import {usePB} from "@/lib/pocketbase.tsx"; +import {useDebouncedState, useDisclosure} from "@mantine/hooks"; +import {useInfiniteQuery} from "@tanstack/react-query"; +import UpsertThreadForm from "@/pages/chat/components/UpsertThreadForm.tsx"; + +const MessageThreadLink = ({thread}: { thread: MessageThreadsModel }) => { + return + { + ({isActive}) => <> + + + +
+ {thread.name} +
+ + } +
+} + +const AnnouncementsLink = () => { + return <> + + { + ({isActive}) => <> + + + + + +
+ Ankündigungen +
+ + } +
+ +} + +export default function MessageThreadsList() { + const {user, pb} = usePB() + + const [showCreateThread, showCreateThreadHandler] = useDisclosure(false) + + const [threadSearchQuery, setThreadSearchQuery] = useDebouncedState("", 200) + + const query = useInfiniteQuery({ + queryKey: ["threads", threadSearchQuery], + queryFn: async ({pageParam}) => ( + await pb.collection("messageThreads").getList(pageParam, 100, { + filter: `participants ?~ '${user?.id}' && name ~ '${threadSearchQuery}' && systemThread != true`, + }) + ), + getNextPageParam: (lastPage) => + lastPage.page >= lastPage.totalPages ? undefined : lastPage.page + 1, + initialPageParam: 1, + enabled: !!user, + }) + + const threads = query.data?.pages.flatMap(t => t.items) || [] + + return
+ + + + + } + rightSection={query.isPending ? : ( + showCreateThreadHandler.toggle()} + color={"green"} + variant={"transparent"} + > + {showCreateThread ? : } + + )} + defaultValue={threadSearchQuery} + onChange={(e) => setThreadSearchQuery(e.currentTarget.value)} + placeholder={"Nach Threads suchen..."} + /> + + + + { + query.refetch() + showCreateThreadHandler.close() + }} onCancel={showCreateThreadHandler.close}/> + + + + {threads.length === 0 ? + + + + + + {threadSearchQuery ? "Keine Threads gefunden" : "Keine Threads"} + + : ( +
+ {threads.map(thread => )} + + {query.hasNextPage && ( +
+ +
+ )} +
+ )} +
+} \ No newline at end of file diff --git a/src/pages/chat/components/Announcement.module.css b/src/pages/chat/components/Announcement.module.css new file mode 100644 index 0000000..993b4e7 --- /dev/null +++ b/src/pages/chat/components/Announcement.module.css @@ -0,0 +1,27 @@ +.announcement { + display: flex; + position: relative; + flex-direction: column; + + box-shadow: var(--shadow); + + background-color: var(--mantine-color-body); + + padding: var(--padding); + + border: var(--border); + border-color: var(--mantine-primary-color-5); + border-radius: var(--mantine-radius-lg); + + margin: var(--gap); + 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); +} \ No newline at end of file diff --git a/src/pages/chat/components/Announcement.tsx b/src/pages/chat/components/Announcement.tsx new file mode 100644 index 0000000..cfb7f5f --- /dev/null +++ b/src/pages/chat/components/Announcement.tsx @@ -0,0 +1,27 @@ +import InnerHtml from "@/components/InnerHtml"; +import classes from './Announcement.module.css' +import {Group, ThemeIcon} from "@mantine/core"; +import {IconSpeakerphone} from "@tabler/icons-react"; + +export default function Announcement({subject, content}: { + subject: string | null, + content: string, +}) { + return
+ + + {subject &&
+ {subject} +
} + + + + +
+ + +
+} \ No newline at end of file diff --git a/src/pages/chat/components/Announcements.module.css b/src/pages/chat/components/Announcements.module.css new file mode 100644 index 0000000..3db7f98 --- /dev/null +++ b/src/pages/chat/components/Announcements.module.css @@ -0,0 +1,6 @@ +.announcements { + flex-grow: 1; + overflow-y: auto; + display: flex; + flex-direction: column-reverse; +} \ No newline at end of file diff --git a/src/pages/chat/components/Announcements.tsx b/src/pages/chat/components/Announcements.tsx new file mode 100644 index 0000000..da5bb03 --- /dev/null +++ b/src/pages/chat/components/Announcements.tsx @@ -0,0 +1,71 @@ +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 {IconMessageCircleUp} 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, 100, { + filter: `isAnnouncement=true`, + sort: "-created" + }) + ), + 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 ( + + ) + + if (query.isLoading || !query.data) return ( +
+ +
+ ) + + return
+
+ {announcements.map((announcement) => ( + + ))} + + {query.hasNextPage ? ( +
+ +
+ ) :
+ + { + announcements.length > 0 ? + "Keine weiteren Ankündigungen" + : "Noch keine Ankündigungen" + } + +
} +
+
+} \ No newline at end of file diff --git a/src/pages/chat/components/ChatNavIcon.tsx b/src/pages/chat/components/ChatNavIcon.tsx new file mode 100644 index 0000000..8a93bed --- /dev/null +++ b/src/pages/chat/components/ChatNavIcon.tsx @@ -0,0 +1,58 @@ +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 {useState} from "react"; +import {useTimeout} from "@mantine/hooks"; +import {MessagesModel} from "@/models/MessageTypes.ts"; + +export default function ChatNavIcon() { + + const [newMessage, setNewMessage] = useState(null); + const {start} = useTimeout(() => setNewMessage(null), 5000); + + const {user, pb, useSubscription} = usePB() + + const match = useMatch("/chat/:threadId") + + useSubscription({ + idOrName: "messages", + topic: "*", + callback: (event) => { + if (event.action == "create" && event.record.thread) { + pb.collection("messageThreads").getOne(event.record.thread).then(thread => { + if (thread.systemThread) { + start() + setNewMessage("announcements") + } else if ( + match?.params.threadId !== event.record.thread // check if thread is not already open + && + event.record.sender !== user?.id // check if sender is not the user + ) { + start() + setNewMessage(event.record.thread) + } + }) + } + } + }) + + if (!user) { + return null + } + + return <> + + + + + + +} \ No newline at end of file diff --git a/src/pages/chat/components/Messages.module.css b/src/pages/chat/components/Messages.module.css new file mode 100644 index 0000000..e1a27ce --- /dev/null +++ b/src/pages/chat/components/Messages.module.css @@ -0,0 +1,55 @@ +.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; + + box-shadow: var(--shadow); + + 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; +} \ No newline at end of file diff --git a/src/pages/chat/components/Messages.tsx b/src/pages/chat/components/Messages.tsx new file mode 100644 index 0000000..fb4a2af --- /dev/null +++ b/src/pages/chat/components/Messages.tsx @@ -0,0 +1,150 @@ +import {MessagesModel, MessageThreadsModel} from "@/models/MessageTypes.ts"; +import TextEditor from "@/components/input/Editor"; +import {useForm} from "@mantine/form"; +import {ActionIcon, Button, Center, 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 {pprintDateTime} from "@/lib/datetime.ts"; + +export default function Messages({thread}: { + thread: MessageThreadsModel +}) { + const {user, pb, useSubscription} = usePB() + + const query = useInfiniteQuery({ + queryKey: ["messages", thread], + queryFn: async ({pageParam}) => ( + await pb.collection("messages").getList(pageParam, 100, { + filter: `thread='${thread?.id}'`, + sort: "-created", + expand: "sender" + }) + ), + getNextPageParam: (lastPage) => + lastPage.page >= lastPage.totalPages ? undefined : lastPage.page + 1, + initialPageParam: 1, + enabled: !!user, + }) + + useSubscription({ + idOrName: "messages", + topic: "*", + callback: (event) => { + if (event.action == "create" && event.record.thread == thread.id) { + query.refetch() + console.log("test") + } + } + }) + + const mutation = useMutation({ + mutationFn: async () => { + await pb.collection("messages").create({ + ...formValues.values, + sender: user!.id, + thread: thread.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
+ + +
+ {messages.map((message) => ( +
+ +
+ {getUserName(message.expand?.sender)} +
+ + + +
+ {pprintDateTime(message.created)} +
+
+ ))} + + {query.hasNextPage ? ( +
+ +
+ ) :
+ + { + messages.length > 0 ? + "Keine weiteren Nachrichten" + : "Noch keine Nachrichten" + } + +
} +
+ + + +
mutation.mutate())}> + + formValues.setFieldValue("content", value)} + modEnter={() => { + if (!formValues.validate().hasErrors) { + mutation.mutate() + } + }} + /> +
+ + + +
+
+
+
+} \ No newline at end of file diff --git a/src/pages/chat/components/UpsertThreadForm.tsx b/src/pages/chat/components/UpsertThreadForm.tsx new file mode 100644 index 0000000..beb77b3 --- /dev/null +++ b/src/pages/chat/components/UpsertThreadForm.tsx @@ -0,0 +1,76 @@ +import {MessageThreadsModel} from "@/models/MessageTypes.ts"; +import {usePB} from "@/lib/pocketbase.tsx"; +import {useForm} from "@mantine/form"; +import {useMutation} from "@tanstack/react-query"; +import {ActionIcon, Box, Button, Group, TextInput} from "@mantine/core"; +import UserInput from "@/components/users/UserInput.tsx"; +import {IconX} from "@tabler/icons-react"; + +export default function UpsertThreadForm({thread, onSuccess, onCancel}: { + thread?: MessageThreadsModel, + onSuccess: () => void, + onCancel: () => void +}) { + const {user, pb} = usePB() + const formValues = useForm({ + initialValues: { + name: thread?.name || "", + description: thread?.description || "", + participants: thread?.expand.participants || [user!], + }, + validate: { + name: (value) => { + if (value.length < 1) { + return "Bitte gib einen Namen ein" + } + }, + } + }) + const mutation = useMutation({ + mutationFn: async () => { + const values = { + name: formValues.values.name, + description: formValues.values.description, + participants: formValues.values.participants.map(p => p.id), + } + + if (thread) { + await pb.collection("messageThreads").update(thread.id, values) + } else { + await pb.collection("messageThreads").create(values) + } + }, + onSuccess: () => { + onSuccess() + } + }) + return
mutation.mutate())}> + + + + formValues.setFieldValue("participants", value)} + /> + + + + + + + + + +} \ No newline at end of file diff --git a/src/pages/events/EventNavigate.tsx b/src/pages/events/EventNavigate.tsx index 42429db..44958df 100644 --- a/src/pages/events/EventNavigate.tsx +++ b/src/pages/events/EventNavigate.tsx @@ -19,7 +19,7 @@ export default function EventNavigate() { const eventQuery = useQuery({ queryKey: ["event", eventId], queryFn: async () => (await pb.collection("events").getOne(eventId, { - expand: "eventAdmins, eventListAdmins" + expand: "eventAdmins, privilegedLists" })) }) diff --git a/src/pages/events/EventOverview/CreateEvent.tsx b/src/pages/events/EventOverview/CreateEvent.tsx index 507c2f0..0880ca1 100644 --- a/src/pages/events/EventOverview/CreateEvent.tsx +++ b/src/pages/events/EventOverview/CreateEvent.tsx @@ -6,7 +6,7 @@ import {useMutation} from "@tanstack/react-query"; import dayjs from "dayjs"; import {UserModal} from "@/models/AuthTypes.ts"; import {PocketBaseErrorAlert, usePB} from "@/lib/pocketbase.tsx"; -import UserInput from "@/components/auth/UserInput.tsx"; +import UserInput from "@/components/users/UserInput.tsx"; import {EventModel} from "@/models/EventTypes.ts"; import ShowHelp from "@/components/ShowHelp.tsx"; import {useSettings} from "@/lib/settings.ts"; diff --git a/src/pages/events/EventOverview/EventList.tsx b/src/pages/events/EventOverview/EventList.tsx index 4e9f0cc..1833c64 100644 --- a/src/pages/events/EventOverview/EventList.tsx +++ b/src/pages/events/EventOverview/EventList.tsx @@ -50,8 +50,8 @@ const EventRow = ({event}: { event: EventModel }) => { const delta = humanDeltaFromNow(event.startDate, event.endDate) return <> -
-
+
+
diff --git a/src/pages/events/EventOverview/index.module.css b/src/pages/events/EventOverview/index.module.css index 216ee80..6807262 100644 --- a/src/pages/events/EventOverview/index.module.css +++ b/src/pages/events/EventOverview/index.module.css @@ -8,7 +8,7 @@ } } -.eventRow { +.eventContainer { display: flex; flex-direction: column; gap: var(--gap); @@ -30,13 +30,17 @@ } } -.row { +.eventInfo { display: flex; flex-direction: row; flex-wrap: nowrap; gap: var(--gap); align-items: center; + + @media (max-width: 768px) { + flex-direction: column; + } } .descriptionContainer { diff --git a/src/pages/events/e/:eventId/EditEventRouter.tsx b/src/pages/events/e/:eventId/EditEventRouter.tsx index 33a5798..0503108 100644 --- a/src/pages/events/e/:eventId/EditEventRouter.tsx +++ b/src/pages/events/e/:eventId/EditEventRouter.tsx @@ -86,7 +86,7 @@ export default function EditEventRouter() { const eventQuery = useQuery({ queryKey: ["event", eventId], queryFn: async () => (await pb.collection("events").getOne(eventId, { - expand: "eventAdmins, eventListAdmins" + expand: "eventAdmins, privilegedLists" })) }) diff --git a/src/pages/events/e/:eventId/EventComponents/EventData.tsx b/src/pages/events/e/:eventId/EventComponents/EventData.tsx index e2cb36a..e2c903d 100644 --- a/src/pages/events/e/:eventId/EventComponents/EventData.tsx +++ b/src/pages/events/e/:eventId/EventComponents/EventData.tsx @@ -1,9 +1,9 @@ import {EventModel} from "@/models/EventTypes.ts"; import classes from "../EditEventRouter.module.css"; -import {IconAdjustments, IconCalendar, IconHourglass, IconList, IconMap, IconSparkles} from "@tabler/icons-react"; +import {IconCalendar, IconHourglass, IconList, IconMap, IconSparkles} from "@tabler/icons-react"; import {areDatesSame, humanDeltaFromNow, pprintDate} from "@/lib/datetime.ts"; -import UsersDisplay from "@/components/auth/UsersDisplay.tsx"; -import {Text, ThemeIcon, Title} from "@mantine/core"; +import UsersDisplay from "@/components/users/UsersDisplay.tsx"; +import {List, Text, ThemeIcon, Title, Tooltip} from "@mantine/core"; /** * Displays the event data @@ -22,7 +22,6 @@ export default function EventData({event, hideHeader}: { event: EventModel, hide return (
- {!hideHeader && Event Übersicht}
@@ -81,24 +80,26 @@ export default function EventData({event, hideHeader}: { event: EventModel, hide { event.expand?.eventAdmins && -
-
- - Event Admins + +
+
- -
+ } { - event.expand?.eventListAdmins && -
-
- - Listen Admins -
- -
+ event.expand?.privilegedLists && + + }> + { + event.expand?.privilegedLists.map((l) => ( + + {l.name} + + )) + } + + }
diff --git a/src/pages/events/e/:eventId/EventComponents/EventSettings/EditEventMembers.tsx b/src/pages/events/e/:eventId/EventComponents/EventSettings/EditEventMembers.tsx index a5e2b8e..843e37f 100644 --- a/src/pages/events/e/:eventId/EventComponents/EventSettings/EditEventMembers.tsx +++ b/src/pages/events/e/:eventId/EventComponents/EventSettings/EditEventMembers.tsx @@ -1,13 +1,14 @@ -import {EventModel} from "@/models/EventTypes.ts"; +import {EventListModel, EventModel} from "@/models/EventTypes.ts"; import {useForm} from "@mantine/form"; import {UserModal} from "@/models/AuthTypes.ts"; import {PocketBaseErrorAlert, usePB} from "@/lib/pocketbase.tsx"; import {Button, Group, Title} from "@mantine/core"; -import UserInput from "@/components/auth/UserInput.tsx"; +import UserInput from "@/components/users/UserInput.tsx"; 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,14 +21,10 @@ export default function EditEventMembers({event}: { event: EventModel }) { const formValues = useForm({ initialValues: { eventAdmins: event?.expand?.eventAdmins ?? [user] as UserModal[], - eventListAdmins: event?.expand?.eventListAdmins ?? [] as UserModal[], + privilegedLists: event?.expand?.privilegedLists ?? [] as EventListModel[] }, validate: { eventAdmins: (value) => value.length > 0 ? null : "Es muss mindestens ein Admin ausgewählt werden.", - eventListAdmins: (value, values) => - value.filter((admin) => values.eventAdmins.map((admin) => admin.id).includes(admin.id)).length === 0 - ? null - : "Ein Event Admin kann nicht gleichzeitig Listen Admin sein." } }) @@ -35,17 +32,16 @@ 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], - eventListAdmins: formValues.values.eventListAdmins.map((member) => member.id) + privilegedLists: formValues.values.privilegedLists.map((member) => member.id) }) }, onSuccess: () => { - showSuccessNotification("Event und Listen Admins gespeichert") + showSuccessNotification("Event Teilnehmende gespeichert") return queryClient.invalidateQueries({queryKey: ["event", event.id]}) } }) return
editMutation.mutate())}> - Event Admins @@ -55,17 +51,18 @@ export default function EditEventMembers({event}: { event: EventModel }) {

- Listen Admins - Listen Admin können Listen bearbeiten und verwalten. - Sie können keine Einstellungen des Events bearbeiten. + Privilegierte Listen + Alle Teilnehmenden in privilegierten Listen können alle Anmeldungen von allen Listen + dieses Events sehen und den Status von allen Teilnehmenden bearbeiten. +
+ Du kannst eine privilegierte Liste z.B. für die Event-Orgs erstellen, so dass diese + alle Anmeldungen sehen und bearbeiten können.

- Eine Person kann nicht gleichzeitig Event Admin und Listen Admin sein.
- formValues.setFieldValue("eventAdmins", records)} /> - formValues.setFieldValue("eventListAdmins", records)} + formValues.setFieldValue("privilegedLists", records)} /> diff --git a/src/pages/events/e/:eventId/EventLists/:listId/EventListSlotsTable/EventListSlotEntryRow.tsx b/src/pages/events/e/:eventId/EventLists/:listId/EventListSlotsTable/EventListSlotEntryRow.tsx index fda0995..68106bc 100644 --- a/src/pages/events/e/:eventId/EventLists/:listId/EventListSlotsTable/EventListSlotEntryRow.tsx +++ b/src/pages/events/e/:eventId/EventLists/:listId/EventListSlotsTable/EventListSlotEntryRow.tsx @@ -11,7 +11,7 @@ import {IconCheckupList, IconForms, IconUserMinus, IconUserPlus} from "@tabler/i import {renderEntries} from "@/components/formUtil/formTable"; import RenderCell from "@/components/formUtil/formTable/RenderCell.tsx"; import EditSlotEntryMenu from "@/pages/events/e/:eventId/EventLists/components/EditSlotEntryMenu.tsx"; -import {RenderUserName} from "@/components/auth/modals/util.tsx"; +import {RenderUserName} from "@/components/users/modals/util.tsx"; export const EventListSlotEntryDetails = ({entry}: { diff --git a/src/pages/events/e/:eventId/EventLists/EventListSearch/EventListSearchResult.tsx b/src/pages/events/e/:eventId/EventLists/EventListSearch/EventListSearchResult.tsx index 7840193..3058215 100644 --- a/src/pages/events/e/:eventId/EventLists/EventListSearch/EventListSearchResult.tsx +++ b/src/pages/events/e/:eventId/EventLists/EventListSearch/EventListSearchResult.tsx @@ -10,7 +10,7 @@ import { import {useDisclosure} from "@mantine/hooks"; import {RenderDateRange} from "@/pages/events/e/:eventId/EventLists/components/RenderDateRange.tsx"; import EditSlotEntryMenu from "@/pages/events/e/:eventId/EventLists/components/EditSlotEntryMenu.tsx"; -import {RenderUserName} from "@/components/auth/modals/util.tsx"; +import {RenderUserName} from "@/components/users/modals/util.tsx"; export default function EventListSearchResult({entry, refetch}: { entry: EventListSlotEntriesWithUserModel, diff --git a/src/pages/events/e/:eventId/EventLists/EventListSearch/index.tsx b/src/pages/events/e/:eventId/EventLists/EventListSearch/index.tsx index 049595f..8f3bbe1 100644 --- a/src/pages/events/e/:eventId/EventLists/EventListSearch/index.tsx +++ b/src/pages/events/e/:eventId/EventLists/EventListSearch/index.tsx @@ -25,7 +25,7 @@ export default function ListSearch({event}: { event: EventModel }) { const {pb} = usePB() - const [showFilter, showFilterHandler] = useDisclosure(false) + const [showFilter, showFilterHandler] = useDisclosure(true) const [searchQueryString, setSearchQueryString] = useState('') const [selectedLists, setSelectedLists] = useState([]) @@ -73,11 +73,12 @@ export default function ListSearch({event}: { event: EventModel }) { )} data={ - searchQuery + (searchQuery .data ?.items - .map(e => e.id) - .filter(onlyUnique) ?? [] + .map(e => e.expand?.user.username) + .filter(u => u !== undefined) + .filter(onlyUnique) ?? []) as string[] } value={searchQueryString} onChange={setSearchQueryString} /> @@ -102,17 +103,18 @@ export default function ListSearch({event}: { event: EventModel }) { - {/* - todo: more filter -
- - Filter - - Listenauswahl - Formularfelder Auswahl - Statusfelder Auswahl -
- */} + { + /* + todo: more filter +
+ + Filter + + Formularfelder Auswahl + Statusfelder Auswahl +
+ */ + } { searchQuery.data?.items.map((entry, index) => ( diff --git a/src/pages/events/e/:eventId/EventLists/EventListsRouter.tsx b/src/pages/events/e/:eventId/EventLists/EventListsRouter.tsx index 5e4be58..a414639 100644 --- a/src/pages/events/e/:eventId/EventLists/EventListsRouter.tsx +++ b/src/pages/events/e/:eventId/EventLists/EventListsRouter.tsx @@ -57,6 +57,7 @@ const nav = [ * @param target - the trigger element for the dropdown */ export const EventListsMenu = ({event, target}: { event: EventModel, target: ReactNode }) => { + return <> void, diff --git a/src/pages/events/e/:eventId/EventLists/components/EventListSlotProgress.tsx b/src/pages/events/e/:eventId/EventLists/components/EventListSlotProgress.tsx index ce763e3..aed6560 100644 --- a/src/pages/events/e/:eventId/EventLists/components/EventListSlotProgress.tsx +++ b/src/pages/events/e/:eventId/EventLists/components/EventListSlotProgress.tsx @@ -26,7 +26,7 @@ export default function EventListSlotProgress({slot}: { slot: EventListSlotsWith } // if the slot is full - if (slot.maxEntries < slot.entriesCount) { + if (slot.maxEntries <= slot.entriesCount) { return ( diff --git a/src/pages/events/e/:eventId/EventLists/components/MoveEventListSlotEntryModal.tsx b/src/pages/events/e/:eventId/EventLists/components/MoveEventListSlotEntryModal.tsx index 4f523ac..ba91b00 100644 --- a/src/pages/events/e/:eventId/EventLists/components/MoveEventListSlotEntryModal.tsx +++ b/src/pages/events/e/:eventId/EventLists/components/MoveEventListSlotEntryModal.tsx @@ -5,7 +5,7 @@ import {showSuccessNotification} from "@/components/util.tsx"; import {Alert, Button, Group, Modal, Select} from "@mantine/core"; import {useEffect, useState} from "react"; import {pprintDateTime} from "@/lib/datetime.ts"; -import {getUserName, RenderUserName} from "@/components/auth/modals/util.tsx"; +import {getUserName, RenderUserName} from "@/components/users/modals/util.tsx"; export const MoveEventListSlotEntryModal = ({opened, close, refetch, entry}: { opened: boolean, diff --git a/src/pages/events/e/:eventId/EventLists/components/UpdateEventListSlotEntryFormModal.tsx b/src/pages/events/e/:eventId/EventLists/components/UpdateEventListSlotEntryFormModal.tsx index b556024..7685630 100644 --- a/src/pages/events/e/:eventId/EventLists/components/UpdateEventListSlotEntryFormModal.tsx +++ b/src/pages/events/e/:eventId/EventLists/components/UpdateEventListSlotEntryFormModal.tsx @@ -5,7 +5,7 @@ import {FieldEntries} from "@/components/formUtil/FromInput/types.ts"; import {showSuccessNotification} from "@/components/util.tsx"; import {Modal} from "@mantine/core"; import FormInput from "@/components/formUtil/FromInput"; -import {getUserName, RenderUserName} from "@/components/auth/modals/util.tsx"; +import {getUserName, RenderUserName} from "@/components/users/modals/util.tsx"; export const UpdateEventListSlotEntryFormModal = ({opened, close, refetch, entry}: { opened: boolean, diff --git a/src/pages/events/e/:eventId/EventLists/components/UpdateEventListSlotEntryStatusModal.tsx b/src/pages/events/e/:eventId/EventLists/components/UpdateEventListSlotEntryStatusModal.tsx index 3729660..13dc653 100644 --- a/src/pages/events/e/:eventId/EventLists/components/UpdateEventListSlotEntryStatusModal.tsx +++ b/src/pages/events/e/:eventId/EventLists/components/UpdateEventListSlotEntryStatusModal.tsx @@ -7,7 +7,7 @@ import {Alert, Modal} from "@mantine/core"; import InnerHtml from "@/components/InnerHtml"; import {RenderDateRange} from "./RenderDateRange.tsx"; import FormInput from "@/components/formUtil/FromInput"; -import {getUserName, RenderUserName} from "@/components/auth/modals/util.tsx"; +import {getUserName, RenderUserName} from "@/components/users/modals/util.tsx"; export const UpdateEventListSlotEntryStatusModal = ({opened, close, refetch, entry}: { opened: boolean, diff --git a/src/pages/events/entries/UserEntries.tsx b/src/pages/events/entries/UserEntries.tsx index a503794..389d92f 100644 --- a/src/pages/events/entries/UserEntries.tsx +++ b/src/pages/events/entries/UserEntries.tsx @@ -1,4 +1,4 @@ -import PromptLoginModal from "@/components/auth/modals/PromptLoginModal.tsx"; +import PromptLoginModal from "@/components/users/modals/PromptLoginModal.tsx"; import {Link, useNavigate} from "react-router-dom"; import {useState} from "react"; import {useDebouncedValue, useToggle} from "@mantine/hooks"; diff --git a/src/pages/events/entries/UserEntryRow.module.css b/src/pages/events/entries/UserEntryRow.module.css index 577a018..234d123 100644 --- a/src/pages/events/entries/UserEntryRow.module.css +++ b/src/pages/events/entries/UserEntryRow.module.css @@ -5,6 +5,28 @@ padding: var(--mantine-spacing-xs); } +.entryInfo { + display: flex; + justify-content: start; + align-items: center; + gap: var(--gap); + + + @media (min-width: 768px) { + & > :nth-child(1) { + width: 20%; + } + + & > :nth-child(2) { + width: 20%; + } + } + + @media (max-width: 768px) { + flex-direction: column; + } +} + .row { display: flex; justify-content: start; @@ -14,14 +36,7 @@ font-size: var(--mantine-font-size-sm); & > :nth-child(2) { - width: 20%; - } - - & > :nth-child(3) { - width: 20%; - } - - & > :nth-child(4) { flex: 1; } + } \ No newline at end of file diff --git a/src/pages/events/entries/UserEntryRow.tsx b/src/pages/events/entries/UserEntryRow.tsx index ea05d49..45af8ec 100644 --- a/src/pages/events/entries/UserEntryRow.tsx +++ b/src/pages/events/entries/UserEntryRow.tsx @@ -17,7 +17,7 @@ import {usePB} from "@/lib/pocketbase.tsx"; import { UpdateEventListSlotEntryFormModal } from "@/pages/events/e/:eventId/EventLists/components/UpdateEventListSlotEntryFormModal.tsx"; -import {getUserName} from "@/components/auth/modals/util.tsx"; +import {getUserName} from "@/components/users/modals/util.tsx"; export default function UserEntryRow({entry, refetch}: { entry: EventListSlotEntriesWithUserModel, @@ -65,29 +65,30 @@ export default function UserEntryRow({entry, refetch}: { +
+ + + + }> + {entry.eventName} + - - - - }> - {entry.eventName} - + + + + }> + {entry.listName} + - - - - }> - {entry.listName} - - - - - - {delta.message} - - + + + + {delta.message} + + +
:nth-child(2) { - width: 40%; + flex-grow: 1; } - & > :nth-child(3) { - flex: 1; +} + +.slotInfo { + display: flex; + flex-direction: row; + gap: var(--gap); + + @media (min-width: 768px) { + & > :nth-child(1) { + width: 50%; + } + + & > :nth-child(2) { + flex: 1; + } + } + + + @media (max-width: 768px) { + flex-direction: column; + align-items: center; + + & > :nth-child(2) { + width: 100%; + } } } \ No newline at end of file diff --git a/src/pages/events/s/EventListSlotView.tsx b/src/pages/events/s/EventListSlotView.tsx index 85493ab..49c0213 100644 --- a/src/pages/events/s/EventListSlotView.tsx +++ b/src/pages/events/s/EventListSlotView.tsx @@ -60,9 +60,13 @@ export default function EventListSlotView({slot, list, refetch}: {
+
+ + +
diff --git a/src/pages/events/s/EventView.tsx b/src/pages/events/s/EventView.tsx index a0fccb8..a351e8c 100644 --- a/src/pages/events/s/EventView.tsx +++ b/src/pages/events/s/EventView.tsx @@ -5,7 +5,7 @@ import NotFound from "../../not-found/index.page.tsx"; import {Accordion, Alert, Anchor, Breadcrumbs, Button, Center, Group, Loader, Title} from "@mantine/core"; import PBAvatar from "@/components/PBAvatar.tsx"; import InnerHtml from "@/components/InnerHtml"; -import {IconExternalLink, IconLogin, IconPencil, IconSectionSign} from "@tabler/icons-react"; +import {IconArchive, IconExternalLink, IconEye, IconLogin, IconPencil, IconSectionSign} from "@tabler/icons-react"; import EventData from "@/pages/events/e/:eventId/EventComponents/EventData.tsx"; import EventListView from "@/pages/events/s/EventListView.tsx"; import {useEventRights} from "@/pages/events/util.ts"; @@ -13,7 +13,7 @@ import {useEventRights} from "@/pages/events/util.ts"; export default function SharedEvent() { - const {pb,user} = usePB() + const {pb, user} = usePB() const {eventId} = useParams() as { eventId: string } @@ -40,6 +40,8 @@ export default function SharedEvent() { const event = eventQuery.data + const eventIsArchived = event.eventAdmins.length === 0 + return <>
- {!user &&
- } color={"orange"}> - Um dich in eine Liste einzutragen, musst du dich anmelden - -
} -
{[ {title: "Home", to: "/"}, @@ -69,7 +65,32 @@ export default function SharedEvent() { ))}
- {(canEditEvent || canEditEventList) &&
+ {!user &&
+ } color={"orange"}> + Um dich in eine Liste einzutragen, musst du dich anmelden + +
} + + {eventIsArchived &&
+ }> + Dieses Event ist archiviert und wird nicht mehr verwaltet + +
} + + {canEditEventList &&
+ + + +
} + + {canEditEvent &&
+ )} + + )) + } + +} export default function HomePage() { const {user} = usePB() - const nav = [ - { - title: "Events", - icon: , - to: `/events` - }, - { - title: "QR Generator", - icon: , - to: `/util/qr` - } - ] - return <>
@@ -41,30 +54,29 @@ export default function HomePage() { }
- - { - nav.map(({title, icon, to}) => ( - - {({isActive}) => ( - - )} - - )) - } - + {user && <> +
+ Ankündigungen -
+ , to: "/chat/announcements"}, + {title: "Chat", icon: , to: "/chat"}, + ]}/> + +
+ +
+
+ } + +
StuVe Events + , to: "/events/entries"}, + {title: "Events", icon: , to: "/events"}, + ]}/> +
diff --git a/src/pages/test/DebugPage.tsx b/src/pages/test/DebugPage.tsx new file mode 100644 index 0000000..0aeeaa4 --- /dev/null +++ b/src/pages/test/DebugPage.tsx @@ -0,0 +1,102 @@ +import {useShowDebug} from "@/components/ShowDebug.tsx"; +import {ActionIcon, Alert, Group, LoadingOverlay, Text, TextInput, Title} from "@mantine/core"; +import {useForm} from "@mantine/form"; +import {useQuery} from "@tanstack/react-query"; +import {PocketBaseErrorAlert, usePB} from "@/lib/pocketbase.tsx"; +import {CodeHighlight} from "@mantine/code-highlight"; +import {IconRefresh} from "@tabler/icons-react"; + +export default function DebugPage() { + + + const {showDebug} = useShowDebug() + + const {pb} = usePB() + + const formValues = useForm({ + initialValues: { + collectionName: "", + filter: "", + sort: "", + expand: "", + } + }) + + const debugQuery = useQuery({ + queryKey: ["debug", formValues.values.collectionName, formValues.values.filter, formValues.values.sort, formValues.values.expand], + queryFn: async () => { + return await pb.collection(formValues.values.collectionName).getList(1, 10, { + filter: formValues.values.filter, + sort: formValues.values.sort, + expand: formValues.values.expand + }) + }, + enabled: formValues.values.collectionName !== "" + }) + + if (!showDebug) return ( +
+ + Der Debug Modus ist deaktiviert. Bitte aktiviere ihn in den Einstellungen um diese Seite zu sehen. + +
) + + return <> +
+ Debug +
+ +
+ + + + +
+ +
+ + + + {debugQuery.data &&
+ + + {debugQuery.data.totalItems} Ergebniss(e) + + + + {debugQuery.data.page}/{debugQuery.data.totalPages} Seiten + + + debugQuery.refetch()} + color={"orange"} + variant={"transparent"} + size={"sm"} + aria-label={"Refetch"} + disabled={debugQuery.isLoading} + > + + + + + +
} +
+ +} \ No newline at end of file diff --git a/src/style/global.css b/src/style/global.css index 957fc41..c6eadab 100644 --- a/src/style/global.css +++ b/src/style/global.css @@ -17,6 +17,10 @@ stroke-width: 1.5; } +.scrollable { + overflow-y: auto; +} + .scrollbar { scrollbar-color: var(--mantine-color-blue-5) var(--mantine-color-body); scrollbar-width: thin; @@ -46,6 +50,10 @@ } } +.overflow-hidden { + overflow: hidden; +} + .no-scrollbar { /* Hide scrollbar for WebKit (Safari, Chrome) */ diff --git a/tsconfig.json b/tsconfig.json index 1c47655..f6a6370 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -26,6 +26,9 @@ "paths": { "@/*": [ "src/*" + ], + "~/*": [ + "public/*" ] }, "types": [ diff --git a/vite.config.ts b/vite.config.ts index d6ed935..d54cb4c 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -8,8 +8,8 @@ export default defineConfig({ plugins: [react(), svgr()], resolve: { alias: { - // Add aliases @ for src directory "@": path.resolve(__dirname, "src"), - } + "~": path.resolve(__dirname, "public"), + }, } }) \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 3a54afd..f01d26d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -880,6 +880,11 @@ resolved "https://registry.yarnpkg.com/@tiptap/extension-list-item/-/extension-list-item-2.3.0.tgz#7f0af465c1af160648ace586100ff3f8e612e9bf" integrity sha512-mHU+IuRa56OT6YCtxf5Z7OSUrbWdKhGCEX7RTrteDVs5oMB6W3oF9j88M5qQmZ1WDcxvQhAOoXctnMt6eX9zcA== +"@tiptap/extension-mention@^2.4.0": + version "2.4.0" + resolved "https://registry.yarnpkg.com/@tiptap/extension-mention/-/extension-mention-2.4.0.tgz#35f13d71e207280cafe5b00e76f17b4c372fbe8b" + integrity sha512-7BqCNfqF1Mv9IrtdlHADwXMFo968UNmthf/TepVXC7EX2Ke6/Y4vvxmpYVNZc55FdswFwpVyZ2VeXBj3AC2JcA== + "@tiptap/extension-ordered-list@^2.3.0": version "2.3.0" resolved "https://registry.yarnpkg.com/@tiptap/extension-ordered-list/-/extension-ordered-list-2.3.0.tgz#75f7f668201a4cd3ec507c78d2229ec670e3e707" @@ -967,6 +972,11 @@ "@tiptap/extension-strike" "^2.3.0" "@tiptap/extension-text" "^2.3.0" +"@tiptap/suggestion@^2.4.0": + version "2.4.0" + resolved "https://registry.yarnpkg.com/@tiptap/suggestion/-/suggestion-2.4.0.tgz#1926cde5f197d116baf7794f55bd971245540e5c" + integrity sha512-6dCkjbL8vIzcLWtS6RCBx0jlYPKf2Beuyq5nNLrDDZZuyJow5qJAY0eGu6Xomp9z0WDK/BYOxT4hHNoGMDkoAg== + "@types/date-arithmetic@*": version "4.1.4" resolved "https://registry.yarnpkg.com/@types/date-arithmetic/-/date-arithmetic-4.1.4.tgz#bdb441f61a916f11af1874a8c2cf787f77ffcb94" @@ -3230,6 +3240,18 @@ react-dropzone@^14.2.3: file-selector "^0.6.0" prop-types "^15.8.1" +react-infinite-scroll-component@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/react-infinite-scroll-component/-/react-infinite-scroll-component-6.1.0.tgz#7e511e7aa0f728ac3e51f64a38a6079ac522407f" + integrity sha512-SQu5nCqy8DxQWpnUVLx7V7b7LcA37aM7tvoWjTLZp1dk6EJibM5/4EJKzOnl07/BsM1Y40sKLuqjCwwH/xV0TQ== + dependencies: + throttle-debounce "^2.1.0" + +react-intersection-observer@^9.10.2: + version "9.10.2" + resolved "https://registry.yarnpkg.com/react-intersection-observer/-/react-intersection-observer-9.10.2.tgz#d5b14f80c9a6bed525becc228db7dccac5d0ec1c" + integrity sha512-j2hGADK2hCbAlfaq6L3tVLb4iqngoN7B1fT16MwJ4J16YW/vWLcmAIinLsw0lgpZeMi4UDUWtHC9QDde0/P1yQ== + react-is@^16.13.1, react-is@^16.7.0: version "16.13.1" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" @@ -3608,6 +3630,11 @@ text-table@^0.2.0: resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" integrity sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw== +throttle-debounce@^2.1.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/throttle-debounce/-/throttle-debounce-2.3.0.tgz#fd31865e66502071e411817e241465b3e9c372e2" + integrity sha512-H7oLPV0P7+jgvrk+6mwwwBDmxTaxnu9HMXmloNLXwnNO0ZxZ31Orah2n8lU1eMPvsaowP2CX+USCgyovXfdOFQ== + tiny-invariant@^1.0.6: version "1.3.3" resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.3.3.tgz#46680b7a873a0d5d10005995eb90a70d74d60127"