feat(chat): fixed issue for mobile and added chat
Build and Push Docker image / build-and-push (push) Successful in 4m30s
Details
Build and Push Docker image / build-and-push (push) Successful in 4m30s
Details
This commit is contained in:
parent
03bd913d45
commit
4f53566b61
|
@ -9,5 +9,5 @@ export const PB_STORAGE_KEY = "stuve-it-login-record"
|
||||||
|
|
||||||
// general
|
// general
|
||||||
export const APP_NAME = "StuVe IT"
|
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"
|
export const APP_URL = "https://it.stuve.uni-ulm.de"
|
|
@ -27,11 +27,13 @@
|
||||||
"@tiptap/extension-collaboration": "^2.3.0",
|
"@tiptap/extension-collaboration": "^2.3.0",
|
||||||
"@tiptap/extension-collaboration-cursor": "^2.3.0",
|
"@tiptap/extension-collaboration-cursor": "^2.3.0",
|
||||||
"@tiptap/extension-link": "^2.3.0",
|
"@tiptap/extension-link": "^2.3.0",
|
||||||
|
"@tiptap/extension-mention": "^2.4.0",
|
||||||
"@tiptap/extension-placeholder": "^2.3.0",
|
"@tiptap/extension-placeholder": "^2.3.0",
|
||||||
"@tiptap/extension-underline": "^2.3.0",
|
"@tiptap/extension-underline": "^2.3.0",
|
||||||
"@tiptap/pm": "^2.3.0",
|
"@tiptap/pm": "^2.3.0",
|
||||||
"@tiptap/react": "^2.3.0",
|
"@tiptap/react": "^2.3.0",
|
||||||
"@tiptap/starter-kit": "^2.3.0",
|
"@tiptap/starter-kit": "^2.3.0",
|
||||||
|
"@tiptap/suggestion": "^2.4.0",
|
||||||
"@types/react-big-calendar": "^1.8.9",
|
"@types/react-big-calendar": "^1.8.9",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"dayjs": "^1.11.10",
|
"dayjs": "^1.11.10",
|
||||||
|
@ -43,11 +45,14 @@
|
||||||
"react-big-calendar": "^1.11.3",
|
"react-big-calendar": "^1.11.3",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-dropzone": "^14.2.3",
|
"react-dropzone": "^14.2.3",
|
||||||
|
"react-infinite-scroll-component": "^6.1.0",
|
||||||
|
"react-intersection-observer": "^9.10.2",
|
||||||
"react-markdown": "^9.0.1",
|
"react-markdown": "^9.0.1",
|
||||||
"react-router-dom": "^6.20.0",
|
"react-router-dom": "^6.20.0",
|
||||||
"recoil": "^0.7.7",
|
"recoil": "^0.7.7",
|
||||||
"remark-gfm": "^4.0.0",
|
"remark-gfm": "^4.0.0",
|
||||||
"sanitize-html": "^2.13.0",
|
"sanitize-html": "^2.13.0",
|
||||||
|
"tippy.js": "^6.3.7",
|
||||||
"tiptap": "^1.32.2",
|
"tiptap": "^1.32.2",
|
||||||
"zod": "^3.22.4"
|
"zod": "^3.22.4"
|
||||||
},
|
},
|
||||||
|
|
|
@ -5,6 +5,8 @@ import Layout from "@/components/layout";
|
||||||
import QRCodeGenerator from "./pages/util/qr/index.page.tsx";
|
import QRCodeGenerator from "./pages/util/qr/index.page.tsx";
|
||||||
import EventsRouter from "./pages/events/EventsRouter.tsx";
|
import EventsRouter from "./pages/events/EventsRouter.tsx";
|
||||||
import LegalPage from "@/pages/LegalPage.tsx";
|
import LegalPage from "@/pages/LegalPage.tsx";
|
||||||
|
import ChatRouter from "@/pages/chat/ChatRouter.tsx";
|
||||||
|
import DebugPage from "@/pages/test/DebugPage.tsx";
|
||||||
|
|
||||||
const router = createBrowserRouter([
|
const router = createBrowserRouter([
|
||||||
{
|
{
|
||||||
|
@ -23,6 +25,14 @@ const router = createBrowserRouter([
|
||||||
path: "events/*",
|
path: "events/*",
|
||||||
element: <EventsRouter/>,
|
element: <EventsRouter/>,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "chat/*",
|
||||||
|
element: <ChatRouter/>,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "debug",
|
||||||
|
element: <DebugPage/>
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: "util",
|
path: "util",
|
||||||
children: [
|
children: [
|
||||||
|
|
|
@ -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 = (
|
||||||
|
<IconSun
|
||||||
|
style={{width: rem(16), height: rem(16)}}
|
||||||
|
stroke={2.5}
|
||||||
|
color={theme.colors.yellow[4]}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
|
||||||
|
const moonIcon = (
|
||||||
|
<IconMoonStars
|
||||||
|
style={{width: rem(16), height: rem(16)}}
|
||||||
|
stroke={2.5}
|
||||||
|
color={theme.colors.blue[6]}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
|
||||||
|
return <Switch
|
||||||
|
color="dark.4"
|
||||||
|
checked={colorScheme === "dark"}
|
||||||
|
onChange={toggleColorScheme}
|
||||||
|
onLabel={sunIcon} offLabel={moonIcon}
|
||||||
|
label={"Farbschema umschalten"}
|
||||||
|
/>
|
||||||
|
}
|
|
@ -21,7 +21,6 @@
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.toolbar {
|
.toolbar {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
|
|
@ -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 {StarterKit} from "@tiptap/starter-kit";
|
||||||
import {Underline} from "@tiptap/extension-underline";
|
import {Underline} from "@tiptap/extension-underline";
|
||||||
import Placeholder from '@tiptap/extension-placeholder';
|
import Placeholder from '@tiptap/extension-placeholder';
|
||||||
|
@ -14,6 +14,9 @@ const Bubble = ({editor}: { editor: Editor }) => (
|
||||||
<RichTextEditor.ControlsGroup>
|
<RichTextEditor.ControlsGroup>
|
||||||
<RichTextEditor.Bold/>
|
<RichTextEditor.Bold/>
|
||||||
<RichTextEditor.Italic/>
|
<RichTextEditor.Italic/>
|
||||||
|
<RichTextEditor.BulletList/>
|
||||||
|
<RichTextEditor.Link/>
|
||||||
|
<RichTextEditor.ClearFormatting/>
|
||||||
</RichTextEditor.ControlsGroup>
|
</RichTextEditor.ControlsGroup>
|
||||||
</BubbleMenu>
|
</BubbleMenu>
|
||||||
)
|
)
|
||||||
|
@ -29,6 +32,7 @@ const Toolbar = ({fullToolbar}: { fullToolbar: boolean, editor: Editor }) => (
|
||||||
<RichTextEditor.Underline/>
|
<RichTextEditor.Underline/>
|
||||||
<RichTextEditor.Code/>
|
<RichTextEditor.Code/>
|
||||||
<RichTextEditor.Strikethrough/>
|
<RichTextEditor.Strikethrough/>
|
||||||
|
<RichTextEditor.ClearFormatting/>
|
||||||
</RichTextEditor.ControlsGroup>
|
</RichTextEditor.ControlsGroup>
|
||||||
|
|
||||||
<RichTextEditor.ControlsGroup>
|
<RichTextEditor.ControlsGroup>
|
||||||
|
@ -88,6 +92,7 @@ export default function TextEditor({
|
||||||
hideToolbar,
|
hideToolbar,
|
||||||
noBorder,
|
noBorder,
|
||||||
disabled,
|
disabled,
|
||||||
|
modEnter,
|
||||||
...props
|
...props
|
||||||
}: {
|
}: {
|
||||||
value: string;
|
value: string;
|
||||||
|
@ -98,6 +103,7 @@ export default function TextEditor({
|
||||||
hideToolbar?: boolean;
|
hideToolbar?: boolean;
|
||||||
noBorder?: boolean;
|
noBorder?: boolean;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
|
modEnter?: () => void;
|
||||||
} & Omit<InputWrapperProps, "onChange">) {
|
} & Omit<InputWrapperProps, "onChange">) {
|
||||||
|
|
||||||
const editor = useEditor({
|
const editor = useEditor({
|
||||||
|
@ -105,7 +111,17 @@ export default function TextEditor({
|
||||||
StarterKit,
|
StarterKit,
|
||||||
Underline,
|
Underline,
|
||||||
Link,
|
Link,
|
||||||
Placeholder.configure({placeholder: placeholder})
|
Placeholder.configure({placeholder: placeholder}),
|
||||||
|
Extension.create({
|
||||||
|
addKeyboardShortcuts() {
|
||||||
|
return {
|
||||||
|
'Mod-Enter': () => {
|
||||||
|
modEnter?.()
|
||||||
|
return modEnter !== undefined
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
],
|
],
|
||||||
editable: !disabled,
|
editable: !disabled,
|
||||||
content: value,
|
content: value,
|
||||||
|
|
|
@ -3,6 +3,38 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: var(--gap);
|
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 {
|
.logo {
|
||||||
|
@ -10,12 +42,17 @@
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
gap: var(--gap);
|
gap: var(--gap);
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
@media (min-width: $mantine-breakpoint-sm) {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.title {
|
.title {
|
||||||
color: var(--mantine-color-text);
|
color: var(--mantine-color-dimmed);
|
||||||
font-size: var(--mantine-font-size-lg);
|
font-size: var(--mantine-font-size-xs);
|
||||||
font-weight: 600;
|
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,72 +1,81 @@
|
||||||
import classes from "./index.module.css";
|
import classes from "./index.module.css";
|
||||||
import {APP_NAME, APP_VERSION} from "../../../../config.ts";
|
import {APP_NAME, APP_VERSION} from "../../../../config.ts";
|
||||||
import {Anchor, Divider, Image} from "@mantine/core";
|
import {Menu, UnstyledButton, useMantineColorScheme} from "@mantine/core";
|
||||||
import {Link} from "react-router-dom";
|
import {NavLink} from "react-router-dom";
|
||||||
import {usePB} from "@/lib/pocketbase.tsx";
|
import {usePB} from "@/lib/pocketbase.tsx";
|
||||||
|
import {IconSectionSign} from "@tabler/icons-react";
|
||||||
|
|
||||||
|
import StuVeLogo from "~/stuve-logo.svg?react";
|
||||||
|
|
||||||
export default function Footer() {
|
export default function Footer() {
|
||||||
const {apiIsHealthy} = usePB()
|
const {apiIsHealthy} = usePB()
|
||||||
|
const {colorScheme, toggleColorScheme} = useMantineColorScheme()
|
||||||
|
const currentYear = new Date().getFullYear()
|
||||||
return (
|
return (
|
||||||
<div className={classes.footer}>
|
<footer className={`${classes.footer}`}>
|
||||||
<div className={classes.inner}>
|
<div className={classes.logo}>
|
||||||
<div className={classes.logo}>
|
<StuVeLogo
|
||||||
<Image
|
height={15}
|
||||||
h={15}
|
width={15}
|
||||||
w={15}
|
/>
|
||||||
src={"/stuve-logo.svg"}
|
<div className={classes.title}>
|
||||||
alt={"StuVe IT Logo"}
|
© {currentYear} {APP_NAME} - {APP_VERSION}
|
||||||
/>
|
|
||||||
|
|
||||||
<div className={classes.title}>{APP_NAME}</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={`${classes.links} ${classes.hideMobile}`}>
|
|
||||||
<h5>Über</h5>
|
|
||||||
|
|
||||||
<p>Version {APP_VERSION}</p>
|
|
||||||
|
|
||||||
<p>Entwickelt vom StuVe Computer Referat</p>
|
|
||||||
|
|
||||||
<Anchor href={"https://git.stuve.uni-ulm.de/stuve-it/stuve-it-frontend"}>Source Code</Anchor>
|
|
||||||
|
|
||||||
<Anchor href={"https://git.stuve.uni-ulm.de/stuve-it/stuve-it-frontend/issues"}>Issues</Anchor>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={`${classes.links} ${classes.hideMobile}`}>
|
|
||||||
<h5>Made with</h5>
|
|
||||||
|
|
||||||
<Anchor href={"https://mantine.dev/"}>Mantine</Anchor>
|
|
||||||
|
|
||||||
<Anchor href={"https://tanstack.com/"}>React Query</Anchor>
|
|
||||||
|
|
||||||
<Anchor href={"https://pocketbase.dev/"}>PocketBase</Anchor>
|
|
||||||
|
|
||||||
<Anchor
|
|
||||||
href={"https://git.stuve.uni-ulm.de/stuve-it/stuve-it-frontend/src/branch/main/package.json"}>...
|
|
||||||
and much more</Anchor>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* only these links will show on mobile */}
|
|
||||||
<div className={classes.links}>
|
|
||||||
<h5 className={classes.hideMobile}>Rechtliches</h5>
|
|
||||||
|
|
||||||
<Link to={"/legal/terms-and-conditions"}>AGB der StuVe</Link>
|
|
||||||
|
|
||||||
<Link to={"/legal/privacy-policy"}>Datenschutzerklärung</Link>
|
|
||||||
|
|
||||||
<Link to={"/legal/imprint"}>Impressum</Link>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Divider/>
|
<div className={`${classes.bottomContainer} ${classes.hideMobile}`}>
|
||||||
<p className={classes.rights}>© 2024 {APP_NAME}. All rights reserved.
|
<div className={classes.bottomText}>{APP_VERSION}</div>
|
||||||
{" "}
|
•
|
||||||
<span data-apiishealthy={apiIsHealthy}>
|
<div className={classes.bottomText}>© {currentYear} {APP_NAME}</div>
|
||||||
{apiIsHealthy ? "Das Backend ist erreichbar." : "Das Backend ist nicht erreichbar!"}
|
•
|
||||||
</span>
|
<div className={classes.bottomText}>
|
||||||
</p>
|
{apiIsHealthy ? "Backend erreichbar" : "Backend nicht erreichbar"}
|
||||||
|
</div>
|
||||||
|
•
|
||||||
</div>
|
<UnstyledButton className={`${classes.bottomText} ${classes.underline}`} component={"span"}
|
||||||
|
onClick={toggleColorScheme}>
|
||||||
|
{colorScheme === "dark" ? "Heller Modus" : "Dunkler Modus"}
|
||||||
|
</UnstyledButton>
|
||||||
|
•
|
||||||
|
<UnstyledButton
|
||||||
|
component={"a"}
|
||||||
|
className={`${classes.bottomText} ${classes.underline}`}
|
||||||
|
href={"https://git.stuve.uni-ulm.de/stuve-it/stuve-it-frontend"}
|
||||||
|
>
|
||||||
|
Source Code
|
||||||
|
</UnstyledButton>
|
||||||
|
•
|
||||||
|
<Menu trigger={"click-hover"}>
|
||||||
|
<Menu.Target>
|
||||||
|
<div className={`${classes.bottomText} ${classes.underline}`}>
|
||||||
|
Rechtliches
|
||||||
|
</div>
|
||||||
|
</Menu.Target>
|
||||||
|
<Menu.Dropdown>
|
||||||
|
<Menu.Item
|
||||||
|
leftSection={<IconSectionSign size={16}/>}
|
||||||
|
component={NavLink}
|
||||||
|
to={"/legal/imprint"}
|
||||||
|
>
|
||||||
|
Impressum
|
||||||
|
</Menu.Item>
|
||||||
|
<Menu.Item
|
||||||
|
leftSection={<IconSectionSign size={16}/>}
|
||||||
|
component={NavLink}
|
||||||
|
to={"/legal/privacy-policy"}
|
||||||
|
>
|
||||||
|
Datenschutzerklärung
|
||||||
|
</Menu.Item>
|
||||||
|
<Menu.Item
|
||||||
|
leftSection={<IconSectionSign size={16}/>}
|
||||||
|
component={NavLink}
|
||||||
|
to={"/legal/terms-and-conditions"}
|
||||||
|
>
|
||||||
|
AGB
|
||||||
|
</Menu.Item>
|
||||||
|
</Menu.Dropdown>
|
||||||
|
</Menu>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
)
|
)
|
||||||
}
|
}
|
|
@ -10,16 +10,18 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.body {
|
.body {
|
||||||
overflow: auto;
|
overflow: hidden;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
padding: 0 var(--gap);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.content {
|
.content {
|
||||||
|
position: relative;
|
||||||
|
overflow: scroll;
|
||||||
|
padding: 0 var(--gap);
|
||||||
padding-top: var(--gap);
|
padding-top: var(--gap);
|
||||||
|
|
||||||
& > * {
|
& > * {
|
||||||
|
|
|
@ -3,12 +3,13 @@ import {Outlet} from "react-router-dom";
|
||||||
|
|
||||||
import classes from "./index.module.css";
|
import classes from "./index.module.css";
|
||||||
import Footer from "./footer";
|
import Footer from "./footer";
|
||||||
import LoginModal from "@/components/auth/modals/LoginModal.tsx";
|
import LoginModal from "@/components/users/modals/LoginModal.tsx";
|
||||||
import UserMenuModal from "@/components/auth/modals/UserMenuModal.tsx";
|
import UserMenuModal from "@/components/users/modals/UserMenuModal.tsx";
|
||||||
import RegisterModal from "@/components/auth/modals/RegisterModal.tsx";
|
import RegisterModal from "@/components/users/modals/RegisterModal.tsx";
|
||||||
import EmailTokenVerification from "@/components/auth/modals/EmailTokenVerification.tsx";
|
import EmailTokenVerification from "@/components/users/modals/EmailTokenVerification.tsx";
|
||||||
import ForgotPasswordModal from "@/components/auth/modals/ForgotPasswordModal.tsx";
|
import ForgotPasswordModal from "@/components/users/modals/ForgotPasswordModal.tsx";
|
||||||
import ChangeEmailModal from "@/components/auth/modals/ChangeEmailModal.tsx";
|
import ChangeEmailModal from "@/components/users/modals/ChangeEmailModal.tsx";
|
||||||
|
import ShowMessagesModal from "@/components/users/modals/ShowMessagesModal";
|
||||||
|
|
||||||
export default function Layout({hideNav}: { hideNav?: boolean }) {
|
export default function Layout({hideNav}: { hideNav?: boolean }) {
|
||||||
return <div className={classes.container}>
|
return <div className={classes.container}>
|
||||||
|
@ -20,9 +21,10 @@ export default function Layout({hideNav}: { hideNav?: boolean }) {
|
||||||
<EmailTokenVerification/>
|
<EmailTokenVerification/>
|
||||||
<ForgotPasswordModal/>
|
<ForgotPasswordModal/>
|
||||||
<ChangeEmailModal/>
|
<ChangeEmailModal/>
|
||||||
|
<ShowMessagesModal/>
|
||||||
|
|
||||||
<div className={`${classes.body} no-scrollbar`}>
|
<div className={`${classes.body}`}>
|
||||||
<div className={`${classes.content}`}>
|
<div className={`${classes.content} no-scrollbar`}>
|
||||||
<Outlet/>
|
<Outlet/>
|
||||||
</div>
|
</div>
|
||||||
<Footer/>
|
<Footer/>
|
||||||
|
|
|
@ -1,7 +1,17 @@
|
||||||
import {Menu} from "@mantine/core";
|
import {Menu} from "@mantine/core";
|
||||||
import {NavLink} from "react-router-dom";
|
import {NavLink} from "react-router-dom";
|
||||||
import {Fragment} from "react";
|
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 = [
|
const NavItems = [
|
||||||
|
@ -14,6 +24,18 @@ const NavItems = [
|
||||||
description: "Home",
|
description: "Home",
|
||||||
link: "/"
|
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,
|
icon: IconList,
|
||||||
description: "Deine Anmeldungen bei Events.",
|
description: "Deine Anmeldungen bei Events.",
|
||||||
link: "/events/entries"
|
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() {
|
export default function MenuItems() {
|
||||||
|
|
||||||
|
const {showDebug} = useShowDebug()
|
||||||
|
|
||||||
|
let nav
|
||||||
|
|
||||||
|
if (showDebug) {
|
||||||
|
nav = [...NavItems, ...DebugMenuItems]
|
||||||
|
} else {
|
||||||
|
nav = NavItems
|
||||||
|
}
|
||||||
|
|
||||||
return <>
|
return <>
|
||||||
{NavItems.map((section, index) => (
|
{nav.map((section, index) => (
|
||||||
<Fragment key={index + section.section}>
|
<Fragment key={index + section.section}>
|
||||||
<Menu.Label>
|
<Menu.Label>
|
||||||
{section.section}
|
{section.section}
|
||||||
|
@ -63,6 +135,7 @@ export default function MenuItems() {
|
||||||
component={NavLink}
|
component={NavLink}
|
||||||
to={item.link}
|
to={item.link}
|
||||||
aria-label={item.description}
|
aria-label={item.description}
|
||||||
|
color={'color' in item ? item.color as string : undefined}
|
||||||
>
|
>
|
||||||
{item.title}
|
{item.title}
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
|
|
|
@ -22,6 +22,16 @@
|
||||||
.title {
|
.title {
|
||||||
font-size: var(--mantine-h2-font-size);
|
font-size: var(--mantine-h2-font-size);
|
||||||
font-weight: bold;
|
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 {
|
.actionIcons {
|
||||||
|
|
|
@ -1,16 +1,15 @@
|
||||||
import {usePB} from "@/lib/pocketbase.tsx";
|
import {usePB} from "@/lib/pocketbase.tsx";
|
||||||
import classes from "./index.module.css";
|
import classes from "./index.module.css";
|
||||||
import {ActionIcon, Image, Menu, ThemeIcon, useMantineColorScheme} from "@mantine/core";
|
import {ActionIcon, Image, Menu, ThemeIcon} from "@mantine/core";
|
||||||
import {IconChevronDown, IconLogin, IconMoon, IconSun, IconUserStar} from "@tabler/icons-react";
|
import {IconChevronDown, IconLogin, IconUserStar} from "@tabler/icons-react";
|
||||||
import MenuItems from "./MenuItems.tsx";
|
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() {
|
export default function NavBar() {
|
||||||
|
|
||||||
const {user} = usePB()
|
const {user} = usePB()
|
||||||
|
|
||||||
const {colorScheme, toggleColorScheme} = useMantineColorScheme()
|
|
||||||
|
|
||||||
const {handler: userMenuHandler} = useUserMenu()
|
const {handler: userMenuHandler} = useUserMenu()
|
||||||
const {handler: loginHandler} = useLogin()
|
const {handler: loginHandler} = useLogin()
|
||||||
|
|
||||||
|
@ -46,27 +45,18 @@ export default function NavBar() {
|
||||||
</Menu>
|
</Menu>
|
||||||
|
|
||||||
<div className={classes.actionIcons}>
|
<div className={classes.actionIcons}>
|
||||||
<ActionIcon
|
|
||||||
variant={"transparent"}
|
|
||||||
color={"gray"}
|
|
||||||
onClick={toggleColorScheme}
|
|
||||||
>
|
|
||||||
{colorScheme === "dark" ?
|
|
||||||
<IconSun/>
|
|
||||||
:
|
|
||||||
<IconMoon/>
|
|
||||||
}
|
|
||||||
</ActionIcon>
|
|
||||||
|
|
||||||
{user ?
|
{user ?
|
||||||
<ActionIcon
|
<>
|
||||||
variant={"transparent"}
|
<ChatNavIcon/>
|
||||||
color={"gray"}
|
<ActionIcon
|
||||||
aria-label={"User Menu"}
|
variant={"transparent"}
|
||||||
onClick={userMenuHandler.open}
|
color={"gray"}
|
||||||
>
|
aria-label={"User Menu"}
|
||||||
<IconUserStar/>
|
onClick={userMenuHandler.open}
|
||||||
</ActionIcon>
|
>
|
||||||
|
<IconUserStar/>
|
||||||
|
</ActionIcon>
|
||||||
|
</>
|
||||||
: (
|
: (
|
||||||
<ActionIcon
|
<ActionIcon
|
||||||
variant={"transparent"}
|
variant={"transparent"}
|
||||||
|
|
|
@ -8,7 +8,7 @@ export default function UsersDisplay({users}: { users: UserModal[] }) {
|
||||||
const {user} = usePB()
|
const {user} = usePB()
|
||||||
|
|
||||||
return <>
|
return <>
|
||||||
<List size={"sm"} icon={<IconUser size={16}/>}>
|
<List size={"sm"} icon={<IconUser size={10}/>}>
|
||||||
{
|
{
|
||||||
users.map((u) => (
|
users.map((u) => (
|
||||||
<List.Item key={u.id} fw={500} c={user && user.id == u.id ? "green" : ""}>
|
<List.Item key={u.id} fw={500} c={user && user.id == u.id ? "green" : ""}>
|
|
@ -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 {isEmail, useForm} from "@mantine/form";
|
||||||
import {Button, Center, Group, Modal, PasswordInput, TextInput} from "@mantine/core";
|
import {Button, Center, Group, Modal, PasswordInput, TextInput} from "@mantine/core";
|
||||||
import {PocketBaseErrorAlert, usePB} from "@/lib/pocketbase.tsx";
|
import {PocketBaseErrorAlert, usePB} from "@/lib/pocketbase.tsx";
|
||||||
|
@ -8,7 +8,8 @@ import {showSuccessNotification} from "@/components/util.tsx";
|
||||||
import {useSearchParams} from "react-router-dom";
|
import {useSearchParams} from "react-router-dom";
|
||||||
|
|
||||||
import EmailSVG from "@/illustrations/email.svg?react"
|
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"
|
export const CHANGE_EMAIL_TOKEN_KEY = "changeEmailToken"
|
||||||
|
|
||||||
|
@ -48,6 +49,10 @@ const RequestEmailChangeModal = ({open, onClose}: {
|
||||||
<EmailSVG height={"200px"} width={"200px"}/>
|
<EmailSVG height={"200px"} width={"200px"}/>
|
||||||
</Center>
|
</Center>
|
||||||
|
|
||||||
|
<ShowHelp>
|
||||||
|
Nur Gast Accounts können ihre E-Mail ändern.
|
||||||
|
</ShowHelp>
|
||||||
|
|
||||||
<PocketBaseErrorAlert error={mutation.error}/>
|
<PocketBaseErrorAlert error={mutation.error}/>
|
||||||
|
|
||||||
<TextInput
|
<TextInput
|
||||||
|
@ -117,6 +122,10 @@ const ConfirmEmailChangeModal = ({open, onClose, token}: {
|
||||||
<EmailSVG height={"200px"} width={"200px"}/>
|
<EmailSVG height={"200px"} width={"200px"}/>
|
||||||
</Center>
|
</Center>
|
||||||
|
|
||||||
|
<ShowHelp>
|
||||||
|
Nur Gast Accounts können ihre E-Mail ändern.
|
||||||
|
</ShowHelp>
|
||||||
|
|
||||||
<PocketBaseErrorAlert error={mutation.error}/>
|
<PocketBaseErrorAlert error={mutation.error}/>
|
||||||
|
|
||||||
<PasswordInput
|
<PasswordInput
|
|
@ -5,7 +5,7 @@ import {usePB} from "@/lib/pocketbase.tsx";
|
||||||
import {showErrorNotification, showSuccessNotification} from "@/components/util.tsx";
|
import {showErrorNotification, showSuccessNotification} from "@/components/util.tsx";
|
||||||
import {Alert, Button, Group, Modal, TextInput, Title} from "@mantine/core";
|
import {Alert, Button, Group, Modal, TextInput, Title} from "@mantine/core";
|
||||||
import {IconAt} from "@tabler/icons-react";
|
import {IconAt} from "@tabler/icons-react";
|
||||||
import {useLogin} from "@/components/auth/modals/hooks.ts";
|
import {useLogin} from "@/components/users/modals/hooks.ts";
|
||||||
|
|
||||||
export const EMAIL_TOKEN_KEY = "emailVerificationToken"
|
export const EMAIL_TOKEN_KEY = "emailVerificationToken"
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import {useForgotPassword, useLogin} from "@/components/auth/modals/hooks.ts";
|
import {useForgotPassword, useLogin} from "@/components/users/modals/hooks.ts";
|
||||||
import {isEmail, useForm} from "@mantine/form";
|
import {isEmail, useForm} from "@mantine/form";
|
||||||
import {Button, Center, Collapse, Group, Modal, PasswordInput, TextInput} from "@mantine/core";
|
import {Button, Center, Collapse, Group, Modal, PasswordInput, TextInput} from "@mantine/core";
|
||||||
import {PocketBaseErrorAlert, usePB} from "@/lib/pocketbase.tsx";
|
import {PocketBaseErrorAlert, usePB} from "@/lib/pocketbase.tsx";
|
||||||
|
@ -9,6 +9,7 @@ import {useSearchParams} from "react-router-dom";
|
||||||
|
|
||||||
import PasswordSVG from "@/illustrations/boy-with-key.svg?react";
|
import PasswordSVG from "@/illustrations/boy-with-key.svg?react";
|
||||||
import PasswordStrengthMeter, {getPasswordStrength} from "@/components/input/PasswordStrengthMeter.tsx";
|
import PasswordStrengthMeter, {getPasswordStrength} from "@/components/input/PasswordStrengthMeter.tsx";
|
||||||
|
import ShowHelp from "@/components/ShowHelp.tsx";
|
||||||
|
|
||||||
export const PWD_RESET_TOKEN_KEY = "passwordResetToken"
|
export const PWD_RESET_TOKEN_KEY = "passwordResetToken"
|
||||||
|
|
||||||
|
@ -47,6 +48,11 @@ const RequestResetPasswordModal = ({open, onClose}: {
|
||||||
<PasswordSVG height={"200px"} width={"200px"}/>
|
<PasswordSVG height={"200px"} width={"200px"}/>
|
||||||
</Center>
|
</Center>
|
||||||
|
|
||||||
|
<ShowHelp>
|
||||||
|
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.
|
||||||
|
</ShowHelp>
|
||||||
|
|
||||||
<PocketBaseErrorAlert error={requestResetPasswordMutation.error}/>
|
<PocketBaseErrorAlert error={requestResetPasswordMutation.error}/>
|
||||||
|
|
||||||
<TextInput
|
<TextInput
|
||||||
|
@ -126,6 +132,11 @@ const ResetPasswordModal = ({open, onClose, token}: {
|
||||||
<PasswordSVG height={"200px"} width={"200px"}/>
|
<PasswordSVG height={"200px"} width={"200px"}/>
|
||||||
</Center>
|
</Center>
|
||||||
|
|
||||||
|
<ShowHelp>
|
||||||
|
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.
|
||||||
|
</ShowHelp>
|
||||||
|
|
||||||
<PocketBaseErrorAlert error={requestResetPasswordMutation.error}/>
|
<PocketBaseErrorAlert error={requestResetPasswordMutation.error}/>
|
||||||
|
|
||||||
<PasswordInput
|
<PasswordInput
|
|
@ -17,7 +17,7 @@ import {
|
||||||
Title
|
Title
|
||||||
} from "@mantine/core";
|
} from "@mantine/core";
|
||||||
import LoginSVG from "@/illustrations/boy-with-key.svg?react"
|
import LoginSVG from "@/illustrations/boy-with-key.svg?react"
|
||||||
import {useForgotPassword, useLogin, useRegister} from "@/components/auth/modals/hooks.ts";
|
import {useForgotPassword, useLogin, useRegister} from "@/components/users/modals/hooks.ts";
|
||||||
import {showSuccessNotification} from "@/components/util.tsx";
|
import {showSuccessNotification} from "@/components/util.tsx";
|
||||||
|
|
||||||
|
|
||||||
|
@ -119,16 +119,19 @@ export default function LoginModal() {
|
||||||
<Divider label={"oder"}/>
|
<Divider label={"oder"}/>
|
||||||
|
|
||||||
<Group justify={"space-evenly"}>
|
<Group justify={"space-evenly"}>
|
||||||
<Button
|
{
|
||||||
size={"compact-xs"}
|
formValues.values.authMethod === "guest" &&
|
||||||
variant={"transparent"}
|
<Button
|
||||||
onClick={() => {
|
size={"compact-xs"}
|
||||||
handler.close()
|
variant={"transparent"}
|
||||||
forgorPasswordHandler.open()
|
onClick={() => {
|
||||||
}}
|
handler.close()
|
||||||
>
|
forgorPasswordHandler.open()
|
||||||
Passwort vergessen?
|
}}
|
||||||
</Button>
|
>
|
||||||
|
Passwort vergessen?
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
size={"compact-xs"}
|
size={"compact-xs"}
|
|
@ -1,6 +1,6 @@
|
||||||
import {useDisclosure} from "@mantine/hooks";
|
import {useDisclosure} from "@mantine/hooks";
|
||||||
import {Button, Group, Modal} from "@mantine/core";
|
import {Button, Group, Modal} from "@mantine/core";
|
||||||
import {useLogin} from "@/components/auth/modals/hooks.ts";
|
import {useLogin} from "@/components/users/modals/hooks.ts";
|
||||||
import {IconLogin, IconX} from "@tabler/icons-react";
|
import {IconLogin, IconX} from "@tabler/icons-react";
|
||||||
|
|
||||||
export default function PromptLoginModal({onAbort, description}: {
|
export default function PromptLoginModal({onAbort, description}: {
|
||||||
|
@ -9,6 +9,7 @@ export default function PromptLoginModal({onAbort, description}: {
|
||||||
}) {
|
}) {
|
||||||
const [open, openHandler] = useDisclosure(true)
|
const [open, openHandler] = useDisclosure(true)
|
||||||
const {handler: loginHandler} = useLogin()
|
const {handler: loginHandler} = useLogin()
|
||||||
|
|
||||||
return <Modal opened={open} onClose={openHandler.close} title={"Zugang beschränkt"}>
|
return <Modal opened={open} onClose={openHandler.close} title={"Zugang beschränkt"}>
|
||||||
<div className={"stack"}>
|
<div className={"stack"}>
|
||||||
|
|
||||||
|
@ -18,7 +19,6 @@ export default function PromptLoginModal({onAbort, description}: {
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
<Group>
|
<Group>
|
||||||
<Button
|
<Button
|
||||||
variant={"light"}
|
variant={"light"}
|
|
@ -1,4 +1,4 @@
|
||||||
import {useRegister} from "@/components/auth/modals/hooks.ts";
|
import {useRegister} from "@/components/users/modals/hooks.ts";
|
||||||
import {isEmail, useForm} from "@mantine/form";
|
import {isEmail, useForm} from "@mantine/form";
|
||||||
import {Alert, Anchor, Button, Checkbox, Collapse, Group, Modal, PasswordInput, Text, TextInput} from "@mantine/core";
|
import {Alert, Anchor, Button, Checkbox, Collapse, Group, Modal, PasswordInput, Text, TextInput} from "@mantine/core";
|
||||||
import {PocketBaseErrorAlert, usePB} from "@/lib/pocketbase.tsx";
|
import {PocketBaseErrorAlert, usePB} from "@/lib/pocketbase.tsx";
|
|
@ -0,0 +1,26 @@
|
||||||
|
import {MessagesModel} from "@/models/MessageTypes.ts";
|
||||||
|
import {Notification} from '@mantine/core';
|
||||||
|
import InnerHtml from "@/components/InnerHtml";
|
||||||
|
import {getUserName} from "@/components/users/modals/util.tsx";
|
||||||
|
|
||||||
|
export default function Message({message}: { message: MessagesModel }) {
|
||||||
|
|
||||||
|
|
||||||
|
const senderName = getUserName(message.expand?.sender)
|
||||||
|
|
||||||
|
let title
|
||||||
|
|
||||||
|
if (message.subject) {
|
||||||
|
title = `${senderName} - ${message.subject}`
|
||||||
|
} else {
|
||||||
|
title = senderName
|
||||||
|
}
|
||||||
|
|
||||||
|
return <>
|
||||||
|
<Notification title={
|
||||||
|
title
|
||||||
|
} withCloseButton={false} radius="lg">
|
||||||
|
<InnerHtml html={message.content ?? ""}/>
|
||||||
|
</Notification>
|
||||||
|
</>
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
.messagesContainer {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column-reverse;
|
||||||
|
gap: var(--gap);
|
||||||
|
}
|
|
@ -0,0 +1,109 @@
|
||||||
|
import {PocketBaseErrorAlert, usePB} from "@/lib/pocketbase.tsx";
|
||||||
|
import {Button, Modal, Stack, Text, ThemeIcon} from "@mantine/core";
|
||||||
|
import {useShowMessages} from "@/components/users/modals/hooks.ts";
|
||||||
|
import PromptLoginModal from "@/components/users/modals/PromptLoginModal.tsx";
|
||||||
|
import {useInfiniteQuery} from "@tanstack/react-query";
|
||||||
|
import {IconMessageCircleOff, IconMessageCircleUp} from "@tabler/icons-react";
|
||||||
|
import Message from "@/components/users/modals/ShowMessagesModal/Message.tsx";
|
||||||
|
import InfiniteScroll from "react-infinite-scroll-component";
|
||||||
|
|
||||||
|
|
||||||
|
export default function ShowMessagesModal() {
|
||||||
|
|
||||||
|
const {value, handler} = useShowMessages()
|
||||||
|
const {user, pb} = usePB()
|
||||||
|
|
||||||
|
const PER_PAGE = 1 // todo
|
||||||
|
|
||||||
|
const {
|
||||||
|
fetchNextPage,
|
||||||
|
hasNextPage,
|
||||||
|
isFetchingNextPage,
|
||||||
|
...query
|
||||||
|
} = useInfiniteQuery({
|
||||||
|
queryKey: ["messages", user?.id, "infinite"],
|
||||||
|
queryFn: async ({pageParam}) => (
|
||||||
|
await pb.collection("messages").getList(pageParam, PER_PAGE, {
|
||||||
|
filter: `recipients?~'${user?.id}'&&thread=null`,
|
||||||
|
sort: "-created",
|
||||||
|
expand: "sender"
|
||||||
|
})
|
||||||
|
),
|
||||||
|
initialPageParam: 1,
|
||||||
|
getNextPageParam: (lastPage) =>
|
||||||
|
lastPage.page >= lastPage.totalPages ? undefined : lastPage.page + 1,
|
||||||
|
enabled: !!user
|
||||||
|
})
|
||||||
|
|
||||||
|
if (value && !user) {
|
||||||
|
return <PromptLoginModal
|
||||||
|
onAbort={handler.close}
|
||||||
|
description={"Du musst eingeloggt sein um deine Nachrichten zu sehen."}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
|
||||||
|
return <>
|
||||||
|
<Modal
|
||||||
|
styles={{
|
||||||
|
content: {minHeight: '100%'},
|
||||||
|
}}
|
||||||
|
opened={value}
|
||||||
|
onClose={handler.close}
|
||||||
|
title={"Nachrichten"}
|
||||||
|
size={"lg"}
|
||||||
|
>
|
||||||
|
<div className={"stack"}>
|
||||||
|
<PocketBaseErrorAlert error={query.error}/>
|
||||||
|
|
||||||
|
{
|
||||||
|
query.data?.pages[0].totalItems === 0 && <Stack align={"center"}>
|
||||||
|
<ThemeIcon size={"xl"} variant={"transparent"} color={"gray"}>
|
||||||
|
<IconMessageCircleOff/>
|
||||||
|
</ThemeIcon>
|
||||||
|
<Text c={"dimmed"} size={"xs"}>
|
||||||
|
Keine Nachrichten
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
}
|
||||||
|
|
||||||
|
<InfiniteScroll
|
||||||
|
dataLength={query.data?.pages.length ?? 0}
|
||||||
|
next={fetchNextPage}
|
||||||
|
hasMore={hasNextPage}
|
||||||
|
loader={<h4>Loading...</h4>}
|
||||||
|
endMessage={
|
||||||
|
<p style={{textAlign: 'center'}}>
|
||||||
|
<b>Yay! You have seen it all</b>
|
||||||
|
</p>
|
||||||
|
}
|
||||||
|
refreshFunction={query.refetch}
|
||||||
|
pullDownToRefresh
|
||||||
|
pullDownToRefreshThreshold={50}
|
||||||
|
pullDownToRefreshContent={
|
||||||
|
<h3 style={{textAlign: 'center'}}>↓ Pull down to refresh</h3>
|
||||||
|
}
|
||||||
|
releaseToRefreshContent={
|
||||||
|
<h3 style={{textAlign: 'center'}}>↑ Release to refresh</h3>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
{
|
||||||
|
query.data?.pages.map(page => page?.items.map((message) => (
|
||||||
|
<Message message={message} key={message.id}/>
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
|
||||||
|
<Button
|
||||||
|
leftSection={<IconMessageCircleUp/>}
|
||||||
|
disabled={!hasNextPage}
|
||||||
|
onClick={() => fetchNextPage()}
|
||||||
|
loading={isFetchingNextPage}
|
||||||
|
>
|
||||||
|
Mehr Nachrichten laden
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</InfiniteScroll>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
</>
|
||||||
|
}
|
|
@ -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 {usePB} from "@/lib/pocketbase.tsx";
|
||||||
import {useShowHelp} from "@/components/ShowHelp.tsx";
|
import {useShowHelp} from "@/components/ShowHelp.tsx";
|
||||||
import ShowDebug, {useShowDebug} from "@/components/ShowDebug.tsx";
|
import ShowDebug, {useShowDebug} from "@/components/ShowDebug.tsx";
|
||||||
import {ActionIcon, Code, Divider, Group, Modal, Switch, Text, ThemeIcon, Title, Tooltip} from "@mantine/core";
|
import {ActionIcon, Code, Divider, Group, Modal, Switch, Text, ThemeIcon, Title, Tooltip,} from "@mantine/core";
|
||||||
import classes from "@/components/layout/nav/index.module.css";
|
import classes from "@/components/layout/nav/index.module.css";
|
||||||
import {
|
import {
|
||||||
IconAt,
|
IconAt,
|
||||||
|
@ -11,9 +11,11 @@ import {
|
||||||
IconMailCog,
|
IconMailCog,
|
||||||
IconPassword,
|
IconPassword,
|
||||||
IconServer,
|
IconServer,
|
||||||
IconServerOff, IconUser
|
IconServerOff,
|
||||||
|
IconUser
|
||||||
} from "@tabler/icons-react";
|
} 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() {
|
export default function UserMenuModal() {
|
||||||
const {value, handler} = useUserMenu()
|
const {value, handler} = useUserMenu()
|
||||||
|
@ -55,9 +57,9 @@ export default function UserMenuModal() {
|
||||||
</ThemeIcon>
|
</ThemeIcon>
|
||||||
|
|
||||||
|
|
||||||
<Text>
|
<Text>
|
||||||
{user?.REALM === "LDAP" ? "StuVe IT Account" : "Gast Account"}
|
{user?.REALM === "LDAP" ? "StuVe IT Account" : "Gast Account"}
|
||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={classes.row}>
|
<div className={classes.row}>
|
||||||
|
@ -68,7 +70,6 @@ export default function UserMenuModal() {
|
||||||
<IconAt/>
|
<IconAt/>
|
||||||
</ThemeIcon>
|
</ThemeIcon>
|
||||||
|
|
||||||
|
|
||||||
<Tooltip label={`Dein Email ist ${user?.emailVisibility ? "sichtbar" : "nicht sichtbar"}`}>
|
<Tooltip label={`Dein Email ist ${user?.emailVisibility ? "sichtbar" : "nicht sichtbar"}`}>
|
||||||
<Text>
|
<Text>
|
||||||
{user?.email}
|
{user?.email}
|
||||||
|
@ -107,7 +108,6 @@ export default function UserMenuModal() {
|
||||||
>
|
>
|
||||||
<IconServer/>
|
<IconServer/>
|
||||||
</ThemeIcon>
|
</ThemeIcon>
|
||||||
|
|
||||||
) : (
|
) : (
|
||||||
<ThemeIcon
|
<ThemeIcon
|
||||||
variant={"transparent"}
|
variant={"transparent"}
|
||||||
|
@ -134,6 +134,9 @@ export default function UserMenuModal() {
|
||||||
Die folgenden Einstellungen werden lokal auf deinem Gerät gespeichert und nicht an den Server
|
Die folgenden Einstellungen werden lokal auf deinem Gerät gespeichert und nicht an den Server
|
||||||
übertragen.
|
übertragen.
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
|
<ColorSchemeSwitch/>
|
||||||
|
|
||||||
<Switch
|
<Switch
|
||||||
checked={showHelp}
|
checked={showHelp}
|
||||||
onChange={toggleShowHelp}
|
onChange={toggleShowHelp}
|
||||||
|
@ -156,31 +159,34 @@ export default function UserMenuModal() {
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Group justify={"center"}>
|
<Group justify={"center"}>
|
||||||
<Tooltip label={"Email ändern"}>
|
|
||||||
<ActionIcon
|
|
||||||
variant={"transparent"}
|
|
||||||
aria-label={"change email"}
|
|
||||||
onClick={() => {
|
|
||||||
handler.close()
|
|
||||||
changeEmailHandler.open()
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<IconMailCog/>
|
|
||||||
</ActionIcon>
|
|
||||||
</Tooltip>
|
|
||||||
|
|
||||||
<Tooltip label={"Passwort zurücksetzen"}>
|
{user?.REALM === "GUEST" && <>
|
||||||
<ActionIcon
|
<Tooltip label={"Email ändern"}>
|
||||||
variant={"transparent"}
|
<ActionIcon
|
||||||
aria-label={"change password"}
|
variant={"transparent"}
|
||||||
onClick={() => {
|
aria-label={"change email"}
|
||||||
handler.close()
|
onClick={() => {
|
||||||
passwordResetHandler.open()
|
handler.close()
|
||||||
}}
|
changeEmailHandler.open()
|
||||||
>
|
}}
|
||||||
<IconPassword/>
|
>
|
||||||
</ActionIcon>
|
<IconMailCog/>
|
||||||
</Tooltip>
|
</ActionIcon>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
|
<Tooltip label={"Passwort zurücksetzen"}>
|
||||||
|
<ActionIcon
|
||||||
|
variant={"transparent"}
|
||||||
|
aria-label={"change password"}
|
||||||
|
onClick={() => {
|
||||||
|
handler.close()
|
||||||
|
passwordResetHandler.open()
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<IconPassword/>
|
||||||
|
</ActionIcon>
|
||||||
|
</Tooltip>
|
||||||
|
</>}
|
||||||
|
|
||||||
<Tooltip label={"Ausloggen"}>
|
<Tooltip label={"Ausloggen"}>
|
||||||
<ActionIcon
|
<ActionIcon
|
|
@ -5,12 +5,12 @@ export const useSearchParamToggle = (key: string) => {
|
||||||
|
|
||||||
const value = searchParams.get(key) === "true"
|
const value = searchParams.get(key) === "true"
|
||||||
|
|
||||||
const open = () => {
|
const open = () => {
|
||||||
setSearchParams(prev => {
|
setSearchParams(prev => {
|
||||||
const newParams = new URLSearchParams(prev);
|
const newParams = new URLSearchParams(prev);
|
||||||
newParams.set(key, "true")
|
newParams.set(key, "true")
|
||||||
return newParams
|
return newParams
|
||||||
}, { replace: true })
|
}, {replace: true})
|
||||||
}
|
}
|
||||||
|
|
||||||
const close = () => {
|
const close = () => {
|
||||||
|
@ -18,7 +18,7 @@ export const useSearchParamToggle = (key: string) => {
|
||||||
const newParams = new URLSearchParams(prev);
|
const newParams = new URLSearchParams(prev);
|
||||||
newParams.delete(key);
|
newParams.delete(key);
|
||||||
return newParams;
|
return newParams;
|
||||||
}, { replace: true })
|
}, {replace: true})
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
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 useLogin = () => useSearchParamToggle("login")
|
||||||
|
|
||||||
export const useRegister = () => useSearchParamToggle("register")
|
export const useRegister = () => useSearchParamToggle("register")
|
||||||
|
@ -40,3 +70,5 @@ export const useUserMenu = () => useSearchParamToggle("userMenu")
|
||||||
export const useForgotPassword = () => useSearchParamToggle("forgotPassword")
|
export const useForgotPassword = () => useSearchParamToggle("forgotPassword")
|
||||||
|
|
||||||
export const useChangeEmail = () => useSearchParamToggle("changeEmail")
|
export const useChangeEmail = () => useSearchParamToggle("changeEmail")
|
||||||
|
|
||||||
|
export const useShowMessages = () => useSearchParamToggle("showMessages")
|
|
@ -9,7 +9,6 @@ export type EventModel = {
|
||||||
startDate: string;
|
startDate: string;
|
||||||
endDate: string;
|
endDate: string;
|
||||||
eventAdmins: string[];
|
eventAdmins: string[];
|
||||||
eventListAdmins: string[];
|
|
||||||
img: string | null; // png, jpg, gif
|
img: string | null; // png, jpg, gif
|
||||||
location: string | null;
|
location: string | null;
|
||||||
isStuveEvent: boolean;
|
isStuveEvent: boolean;
|
||||||
|
@ -18,9 +17,10 @@ export type EventModel = {
|
||||||
eventLinks: EventLink[];
|
eventLinks: EventLink[];
|
||||||
defaultEntryQuestionSchema: FormSchema | null;
|
defaultEntryQuestionSchema: FormSchema | null;
|
||||||
defaultEntryStatusSchema: FormSchema | null;
|
defaultEntryStatusSchema: FormSchema | null;
|
||||||
|
privilegedLists: string[];
|
||||||
expand?: {
|
expand?: {
|
||||||
eventAdmins: UserModal[] | null;
|
eventAdmins: UserModal[] | null;
|
||||||
eventListAdmins: UserModal[] | null;
|
privilegedLists: EventListModel[] | null;
|
||||||
}
|
}
|
||||||
} & RecordModel
|
} & RecordModel
|
||||||
|
|
||||||
|
|
|
@ -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
|
|
@ -8,6 +8,7 @@ import {
|
||||||
EventListSlotsWithEntriesCountModel,
|
EventListSlotsWithEntriesCountModel,
|
||||||
EventModel
|
EventModel
|
||||||
} from "./EventTypes.ts";
|
} from "./EventTypes.ts";
|
||||||
|
import {MessagesModel, MessageThreadsModel} from "@/models/MessageTypes.ts";
|
||||||
|
|
||||||
export type SettingsModel = {
|
export type SettingsModel = {
|
||||||
key: ['privacyPolicy', 'agb', 'stexGroup', 'stuveEventQuestions']
|
key: ['privacyPolicy', 'agb', 'stexGroup', 'stuveEventQuestions']
|
||||||
|
@ -26,6 +27,10 @@ export type LegalSettingsModal = {
|
||||||
export interface TypedPocketBase extends PocketBase {
|
export interface TypedPocketBase extends PocketBase {
|
||||||
collection(idOrName: string): RecordService<RecordModel>
|
collection(idOrName: string): RecordService<RecordModel>
|
||||||
|
|
||||||
|
collection(idOrName: 'messages'): RecordService<MessagesModel>
|
||||||
|
|
||||||
|
collection(idOrName: 'messageThreads'): RecordService<MessageThreadsModel>
|
||||||
|
|
||||||
collection(idOrName: 'settings'): RecordService<SettingsModel>
|
collection(idOrName: 'settings'): RecordService<SettingsModel>
|
||||||
|
|
||||||
collection(idOrName: 'legalSettings'): RecordService<LegalSettingsModal>
|
collection(idOrName: 'legalSettings'): RecordService<LegalSettingsModal>
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 <Center>
|
||||||
|
<ConversationSvg width={"100%"} height={"100%"}/>
|
||||||
|
</Center>
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ChatRouter() {
|
||||||
|
|
||||||
|
const isMobile = useMediaQuery("(max-width: 768px)")
|
||||||
|
const {user} = usePB()
|
||||||
|
|
||||||
|
const {handler} = useLogin()
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
handler.toggle()
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return <div className={`${classes.container} stack`}>
|
||||||
|
<div className={"section-transparent"}>
|
||||||
|
<Breadcrumbs>{[
|
||||||
|
{title: "Home", to: "/"},
|
||||||
|
{title: "Nachrichten", to: "/chat"},
|
||||||
|
].map(({title, to}) => (
|
||||||
|
<Anchor component={Link} to={to} key={title}>
|
||||||
|
{title}
|
||||||
|
</Anchor>
|
||||||
|
))}</Breadcrumbs>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={classes.grid}>
|
||||||
|
<Outlet/>
|
||||||
|
|
||||||
|
<Routes>
|
||||||
|
<Route path={isMobile ? "/" : "*"} element={<MessageThreadsList/>}/>
|
||||||
|
</Routes>
|
||||||
|
|
||||||
|
<Routes>
|
||||||
|
{!isMobile && <Route index element={<ChatIndex/>}/>}
|
||||||
|
<Route path={":threadId"} element={<MessageThreadView/>}/>
|
||||||
|
<Route path={"announcements"} element={<Announcements/>}/>
|
||||||
|
</Routes>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
.backIcon {
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 (
|
||||||
|
<PocketBaseErrorAlert error={query.error}/>
|
||||||
|
)
|
||||||
|
|
||||||
|
if (query.isLoading || !query.data) return (
|
||||||
|
<Center>
|
||||||
|
<Loader/>
|
||||||
|
</Center>
|
||||||
|
)
|
||||||
|
|
||||||
|
const thread = query.data
|
||||||
|
|
||||||
|
return <div className={"stack"}>
|
||||||
|
|
||||||
|
<ConfirmModal/>
|
||||||
|
|
||||||
|
<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={thread} name={thread.name} img={thread.img} size={"lg"}/>
|
||||||
|
<Text size={"lg"} fw={600}>{thread.name}</Text>
|
||||||
|
<ActionIcon
|
||||||
|
variant={"transparent"} color={"blue"}
|
||||||
|
radius={"xl"} ms={"auto"}
|
||||||
|
aria-label={"Thread Info"}
|
||||||
|
onClick={showThreadInfoHandler.toggle}
|
||||||
|
>
|
||||||
|
<IconInfoCircle/>
|
||||||
|
</ActionIcon>
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
<Collapse in={showThreadInfo}>
|
||||||
|
<Alert color={"blue"} title={"Thread Info"}>
|
||||||
|
<div className={"stack"}>
|
||||||
|
{
|
||||||
|
thread.expand.participants && (
|
||||||
|
<UsersDisplay users={thread.expand.participants}/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
{!showEditThread && <Group>
|
||||||
|
|
||||||
|
<ActionIcon
|
||||||
|
variant={"transparent"} color={"red"}
|
||||||
|
radius={"xl"}
|
||||||
|
aria-label={"Delete Thread"}
|
||||||
|
onClick={toggleConfirmModal}
|
||||||
|
>
|
||||||
|
<IconTrash/>
|
||||||
|
</ActionIcon>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant={"light"}
|
||||||
|
color={"blue"}
|
||||||
|
size={"xs"}
|
||||||
|
radius={"xl"}
|
||||||
|
aria-label={"Edit Thread"}
|
||||||
|
onClick={showEditThreadHandler.toggle}
|
||||||
|
leftSection={<IconEdit/>}
|
||||||
|
>
|
||||||
|
Bearbeiten
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
}
|
||||||
|
<Collapse in={showEditThread}>
|
||||||
|
<UpsertThreadForm thread={thread} onSuccess={() => {
|
||||||
|
showEditThreadHandler.toggle()
|
||||||
|
query.refetch()
|
||||||
|
}} onCancel={showEditThreadHandler.toggle}/>
|
||||||
|
</Collapse>
|
||||||
|
</div>
|
||||||
|
</Alert>
|
||||||
|
</Collapse>
|
||||||
|
|
||||||
|
<Messages thread={thread}/>
|
||||||
|
</div>
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 <UnstyledButton component={NavLink} to={`/chat/${thread.id}`} className={classes.threadLink}>
|
||||||
|
{
|
||||||
|
({isActive}) => <>
|
||||||
|
|
||||||
|
<PBAvatar model={thread} name={thread.name} img={thread.img} size={"sm"}/>
|
||||||
|
|
||||||
|
<div className={classes.threadText} data-active={isActive}>
|
||||||
|
{thread.name}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
</UnstyledButton>
|
||||||
|
}
|
||||||
|
|
||||||
|
const AnnouncementsLink = () => {
|
||||||
|
return <>
|
||||||
|
<UnstyledButton component={NavLink} to={`/chat/announcements`} className={classes.announcementLink}>
|
||||||
|
{
|
||||||
|
({isActive}) => <>
|
||||||
|
|
||||||
|
<ThemeIcon variant="light" radius={"xl"} size="md" color="green">
|
||||||
|
<IconSpeakerphone/>
|
||||||
|
</ThemeIcon>
|
||||||
|
|
||||||
|
<div className={`${classes.threadText} `} data-active={isActive}>
|
||||||
|
Ankündigungen
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
</UnstyledButton>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
|
||||||
|
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 <div className={`stack ${classes.container}`}>
|
||||||
|
<AnnouncementsLink/>
|
||||||
|
|
||||||
|
<Divider label={"Threads"}/>
|
||||||
|
|
||||||
|
<TextInput
|
||||||
|
leftSection={<IconNeedleThread/>}
|
||||||
|
rightSection={query.isPending ? <Loader size={"xs"}/> : (
|
||||||
|
<ActionIcon
|
||||||
|
onClick={() => showCreateThreadHandler.toggle()}
|
||||||
|
color={"green"}
|
||||||
|
variant={"transparent"}
|
||||||
|
>
|
||||||
|
{showCreateThread ? <IconMinus/> : <IconPlus/>}
|
||||||
|
</ActionIcon>
|
||||||
|
)}
|
||||||
|
defaultValue={threadSearchQuery}
|
||||||
|
onChange={(e) => setThreadSearchQuery(e.currentTarget.value)}
|
||||||
|
placeholder={"Nach Threads suchen..."}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Collapse in={showCreateThread}>
|
||||||
|
<Alert className={"stack"} color={"green"} title={"Neuen Thread erstellen"}>
|
||||||
|
<UpsertThreadForm onSuccess={() => {
|
||||||
|
query.refetch()
|
||||||
|
showCreateThreadHandler.close()
|
||||||
|
}} onCancel={showCreateThreadHandler.close}/>
|
||||||
|
</Alert>
|
||||||
|
</Collapse>
|
||||||
|
|
||||||
|
{threads.length === 0 ? <Stack gap={"xs"} align={"center"}>
|
||||||
|
<ThemeIcon variant="transparent" size="xs" color="gray">
|
||||||
|
<IconNeedleThread/>
|
||||||
|
</ThemeIcon>
|
||||||
|
|
||||||
|
<Text size={"xs"} c={"dimmed"}>
|
||||||
|
{threadSearchQuery ? "Keine Threads gefunden" : "Keine Threads"}
|
||||||
|
</Text>
|
||||||
|
</Stack> : (
|
||||||
|
<div className={`${classes.threadsContainer} no-scrollbar`}>
|
||||||
|
{threads.map(thread => <MessageThreadLink key={thread.id} thread={thread}/>)}
|
||||||
|
|
||||||
|
{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>
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
|
@ -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 <div className={classes.announcement}>
|
||||||
|
|
||||||
|
<Group justify={"space-between"} wrap={"nowrap"} align={"top"} mb={"md"}>
|
||||||
|
{subject && <div className={classes.subject}>
|
||||||
|
{subject}
|
||||||
|
</div>}
|
||||||
|
|
||||||
|
<ThemeIcon
|
||||||
|
className={classes.icon}
|
||||||
|
variant={"transparent"} size={"sm"}
|
||||||
|
>
|
||||||
|
<IconSpeakerphone/>
|
||||||
|
</ThemeIcon>
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
<InnerHtml html={content}/>
|
||||||
|
</div>
|
||||||
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
.announcements {
|
||||||
|
flex-grow: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column-reverse;
|
||||||
|
}
|
|
@ -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 (
|
||||||
|
<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}
|
||||||
|
subject={announcement.subject}
|
||||||
|
content={announcement.content}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{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"}>
|
||||||
|
{
|
||||||
|
announcements.length > 0 ?
|
||||||
|
"Keine weiteren Ankündigungen"
|
||||||
|
: "Noch keine Ankündigungen"
|
||||||
|
}
|
||||||
|
</Text>
|
||||||
|
</div>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
|
@ -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<string | null>(null);
|
||||||
|
const {start} = useTimeout(() => setNewMessage(null), 5000);
|
||||||
|
|
||||||
|
const {user, pb, useSubscription} = usePB()
|
||||||
|
|
||||||
|
const match = useMatch("/chat/:threadId")
|
||||||
|
|
||||||
|
useSubscription<MessagesModel>({
|
||||||
|
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 <>
|
||||||
|
<Indicator inline processing disabled={newMessage === null} classNames={{
|
||||||
|
root: "stack"
|
||||||
|
}}>
|
||||||
|
<ActionIcon
|
||||||
|
component={Link}
|
||||||
|
to={`/chat${newMessage ? `/${newMessage}` : ""}`}
|
||||||
|
variant={"transparent"}
|
||||||
|
color={"gray"}
|
||||||
|
>
|
||||||
|
<IconMessageCircle/>
|
||||||
|
</ActionIcon>
|
||||||
|
</Indicator>
|
||||||
|
</>
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
|
@ -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<MessagesModel>({
|
||||||
|
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 <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}/>
|
||||||
|
|
||||||
|
<div className={classes.messageSender}>
|
||||||
|
{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>
|
||||||
|
}
|
|
@ -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 <form onSubmit={formValues.onSubmit(() => mutation.mutate())}>
|
||||||
|
<TextInput
|
||||||
|
mb={"sm"}
|
||||||
|
placeholder={"Name"}
|
||||||
|
{...formValues.getInputProps("name")}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Box mb={"sm"}>
|
||||||
|
<UserInput
|
||||||
|
placeholder={"Teilnehmer"}
|
||||||
|
selectedRecords={formValues.values.participants}
|
||||||
|
setSelectedRecords={(value) => formValues.setFieldValue("participants", value)}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Group>
|
||||||
|
<ActionIcon variant={"transparent"} size={"sm"} aria-label={"abort"} onClick={onCancel}>
|
||||||
|
<IconX/>
|
||||||
|
</ActionIcon>
|
||||||
|
<Button
|
||||||
|
size={"xs"}
|
||||||
|
variant={"light"}
|
||||||
|
type={"submit"}
|
||||||
|
loading={mutation.isPending}
|
||||||
|
>
|
||||||
|
Speichern
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
</form>
|
||||||
|
}
|
|
@ -19,7 +19,7 @@ export default function EventNavigate() {
|
||||||
const eventQuery = useQuery({
|
const eventQuery = useQuery({
|
||||||
queryKey: ["event", eventId],
|
queryKey: ["event", eventId],
|
||||||
queryFn: async () => (await pb.collection("events").getOne(eventId, {
|
queryFn: async () => (await pb.collection("events").getOne(eventId, {
|
||||||
expand: "eventAdmins, eventListAdmins"
|
expand: "eventAdmins, privilegedLists"
|
||||||
}))
|
}))
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -6,7 +6,7 @@ import {useMutation} from "@tanstack/react-query";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import {UserModal} from "@/models/AuthTypes.ts";
|
import {UserModal} from "@/models/AuthTypes.ts";
|
||||||
import {PocketBaseErrorAlert, usePB} from "@/lib/pocketbase.tsx";
|
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 {EventModel} from "@/models/EventTypes.ts";
|
||||||
import ShowHelp from "@/components/ShowHelp.tsx";
|
import ShowHelp from "@/components/ShowHelp.tsx";
|
||||||
import {useSettings} from "@/lib/settings.ts";
|
import {useSettings} from "@/lib/settings.ts";
|
||||||
|
|
|
@ -50,8 +50,8 @@ const EventRow = ({event}: { event: EventModel }) => {
|
||||||
const delta = humanDeltaFromNow(event.startDate, event.endDate)
|
const delta = humanDeltaFromNow(event.startDate, event.endDate)
|
||||||
|
|
||||||
return <>
|
return <>
|
||||||
<div className={classes.eventRow}>
|
<div className={classes.eventContainer}>
|
||||||
<div className={classes.row}>
|
<div className={classes.eventInfo}>
|
||||||
<div>
|
<div>
|
||||||
<PBAvatar model={event} name={event.name} img={event.img}/>
|
<PBAvatar model={event} name={event.name} img={event.img}/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.eventRow {
|
.eventContainer {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: var(--gap);
|
gap: var(--gap);
|
||||||
|
@ -30,13 +30,17 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.row {
|
.eventInfo {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
flex-wrap: nowrap;
|
flex-wrap: nowrap;
|
||||||
gap: var(--gap);
|
gap: var(--gap);
|
||||||
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.descriptionContainer {
|
.descriptionContainer {
|
||||||
|
|
|
@ -86,7 +86,7 @@ export default function EditEventRouter() {
|
||||||
const eventQuery = useQuery({
|
const eventQuery = useQuery({
|
||||||
queryKey: ["event", eventId],
|
queryKey: ["event", eventId],
|
||||||
queryFn: async () => (await pb.collection("events").getOne(eventId, {
|
queryFn: async () => (await pb.collection("events").getOne(eventId, {
|
||||||
expand: "eventAdmins, eventListAdmins"
|
expand: "eventAdmins, privilegedLists"
|
||||||
}))
|
}))
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
import {EventModel} from "@/models/EventTypes.ts";
|
import {EventModel} from "@/models/EventTypes.ts";
|
||||||
import classes from "../EditEventRouter.module.css";
|
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 {areDatesSame, humanDeltaFromNow, pprintDate} from "@/lib/datetime.ts";
|
||||||
import UsersDisplay from "@/components/auth/UsersDisplay.tsx";
|
import UsersDisplay from "@/components/users/UsersDisplay.tsx";
|
||||||
import {Text, ThemeIcon, Title} from "@mantine/core";
|
import {List, Text, ThemeIcon, Title, Tooltip} from "@mantine/core";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Displays the event data
|
* Displays the event data
|
||||||
|
@ -22,7 +22,6 @@ export default function EventData({event, hideHeader}: { event: EventModel, hide
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`section`}>
|
<div className={`section`}>
|
||||||
|
|
||||||
{!hideHeader && <Title order={2}>Event Übersicht</Title>}
|
{!hideHeader && <Title order={2}>Event Übersicht</Title>}
|
||||||
|
|
||||||
<div className={classes.data}>
|
<div className={classes.data}>
|
||||||
|
@ -81,24 +80,26 @@ export default function EventData({event, hideHeader}: { event: EventModel, hide
|
||||||
|
|
||||||
{
|
{
|
||||||
event.expand?.eventAdmins &&
|
event.expand?.eventAdmins &&
|
||||||
<div className={classes.stack}>
|
<Tooltip label={"Event Admins"} position={"top-start"}>
|
||||||
<div className={"group"}>
|
<div>
|
||||||
<IconAdjustments size={16}/>
|
<UsersDisplay users={event.expand.eventAdmins}/>
|
||||||
Event Admins
|
|
||||||
</div>
|
</div>
|
||||||
<UsersDisplay users={event.expand.eventAdmins}/>
|
</Tooltip>
|
||||||
</div>
|
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
event.expand?.eventListAdmins &&
|
event.expand?.privilegedLists &&
|
||||||
<div className={classes.stack}>
|
<Tooltip label={"Privilegierte Listen"} position={"top-start"}>
|
||||||
<div className={"group"}>
|
<List size={"sm"} icon={<IconList size={10}/>}>
|
||||||
<IconList size={16}/>
|
{
|
||||||
Listen Admins
|
event.expand?.privilegedLists.map((l) => (
|
||||||
</div>
|
<List.Item key={l.id} fw={500}>
|
||||||
<UsersDisplay users={event.expand.eventListAdmins}/>
|
{l.name}
|
||||||
</div>
|
</List.Item>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</List>
|
||||||
|
</Tooltip>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,13 +1,14 @@
|
||||||
import {EventModel} from "@/models/EventTypes.ts";
|
import {EventListModel, EventModel} from "@/models/EventTypes.ts";
|
||||||
import {useForm} from "@mantine/form";
|
import {useForm} from "@mantine/form";
|
||||||
import {UserModal} from "@/models/AuthTypes.ts";
|
import {UserModal} from "@/models/AuthTypes.ts";
|
||||||
import {PocketBaseErrorAlert, usePB} from "@/lib/pocketbase.tsx";
|
import {PocketBaseErrorAlert, usePB} from "@/lib/pocketbase.tsx";
|
||||||
import {Button, Group, Title} from "@mantine/core";
|
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 {useMutation} from "@tanstack/react-query";
|
||||||
import {queryClient} from "@/main.tsx";
|
import {queryClient} from "@/main.tsx";
|
||||||
import {showSuccessNotification} from "@/components/util.tsx";
|
import {showSuccessNotification} from "@/components/util.tsx";
|
||||||
import ShowHelp from "@/components/ShowHelp.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.
|
* 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({
|
const formValues = useForm({
|
||||||
initialValues: {
|
initialValues: {
|
||||||
eventAdmins: event?.expand?.eventAdmins ?? [user] as UserModal[],
|
eventAdmins: event?.expand?.eventAdmins ?? [user] as UserModal[],
|
||||||
eventListAdmins: event?.expand?.eventListAdmins ?? [] as UserModal[],
|
privilegedLists: event?.expand?.privilegedLists ?? [] as EventListModel[]
|
||||||
},
|
},
|
||||||
validate: {
|
validate: {
|
||||||
eventAdmins: (value) => value.length > 0 ? null : "Es muss mindestens ein Admin ausgewählt werden.",
|
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 () => {
|
mutationFn: async () => {
|
||||||
return await pb.collection("events").update(event.id, {
|
return await pb.collection("events").update(event.id, {
|
||||||
eventAdmins: [...formValues.values.eventAdmins.map((member) => member.id), user?.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: () => {
|
onSuccess: () => {
|
||||||
showSuccessNotification("Event und Listen Admins gespeichert")
|
showSuccessNotification("Event Teilnehmende gespeichert")
|
||||||
return queryClient.invalidateQueries({queryKey: ["event", event.id]})
|
return queryClient.invalidateQueries({queryKey: ["event", event.id]})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
return <form className="stack" onSubmit={formValues.onSubmit(() => editMutation.mutate())}>
|
return <form className="stack" onSubmit={formValues.onSubmit(() => editMutation.mutate())}>
|
||||||
|
|
||||||
<Title order={4} c={"blue"}>Event Admins</Title>
|
<Title order={4} c={"blue"}>Event Admins</Title>
|
||||||
|
|
||||||
<ShowHelp>
|
<ShowHelp>
|
||||||
|
@ -55,17 +51,18 @@ export default function EditEventMembers({event}: { event: EventModel }) {
|
||||||
<br/>
|
<br/>
|
||||||
<br/>
|
<br/>
|
||||||
|
|
||||||
<Title order={6}>Listen Admins</Title>
|
<Title order={6}>Privilegierte Listen</Title>
|
||||||
Listen Admin können Listen bearbeiten und verwalten.
|
Alle Teilnehmenden in privilegierten Listen können alle Anmeldungen von allen Listen
|
||||||
Sie können <b>keine</b> Einstellungen des Events bearbeiten.
|
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/>
|
||||||
<br/>
|
<br/>
|
||||||
Eine Person kann nicht gleichzeitig Event Admin und Listen Admin sein.
|
|
||||||
</ShowHelp>
|
</ShowHelp>
|
||||||
|
|
||||||
<PocketBaseErrorAlert error={editMutation.error}/>
|
<PocketBaseErrorAlert error={editMutation.error}/>
|
||||||
|
|
||||||
|
|
||||||
<UserInput
|
<UserInput
|
||||||
required
|
required
|
||||||
label={"Event Admins"}
|
label={"Event Admins"}
|
||||||
|
@ -75,12 +72,12 @@ export default function EditEventMembers({event}: { event: EventModel }) {
|
||||||
setSelectedRecords={(records) => formValues.setFieldValue("eventAdmins", records)}
|
setSelectedRecords={(records) => formValues.setFieldValue("eventAdmins", records)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<UserInput
|
<ListSelect
|
||||||
label={"Listen Admins"}
|
label={"Privilegierte Listen"}
|
||||||
description={"Die Listen Admins können Listen bearbeiten und verwalten."}
|
description={"Teilnehmende in privilegierten Listen können alle Event-Anmeldungen sehen und deren Status bearbeiten."}
|
||||||
error={formValues.errors.eventListAdmins}
|
event={event}
|
||||||
selectedRecords={formValues.values.eventListAdmins}
|
selectedRecords={formValues.values.privilegedLists}
|
||||||
setSelectedRecords={(records) => formValues.setFieldValue("eventListAdmins", records)}
|
setSelectedRecords={(records) => formValues.setFieldValue("privilegedLists", records)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Group>
|
<Group>
|
||||||
|
|
|
@ -11,7 +11,7 @@ import {IconCheckupList, IconForms, IconUserMinus, IconUserPlus} from "@tabler/i
|
||||||
import {renderEntries} from "@/components/formUtil/formTable";
|
import {renderEntries} from "@/components/formUtil/formTable";
|
||||||
import RenderCell from "@/components/formUtil/formTable/RenderCell.tsx";
|
import RenderCell from "@/components/formUtil/formTable/RenderCell.tsx";
|
||||||
import EditSlotEntryMenu from "@/pages/events/e/:eventId/EventLists/components/EditSlotEntryMenu.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}: {
|
export const EventListSlotEntryDetails = ({entry}: {
|
||||||
|
|
|
@ -10,7 +10,7 @@ import {
|
||||||
import {useDisclosure} from "@mantine/hooks";
|
import {useDisclosure} from "@mantine/hooks";
|
||||||
import {RenderDateRange} from "@/pages/events/e/:eventId/EventLists/components/RenderDateRange.tsx";
|
import {RenderDateRange} from "@/pages/events/e/:eventId/EventLists/components/RenderDateRange.tsx";
|
||||||
import EditSlotEntryMenu from "@/pages/events/e/:eventId/EventLists/components/EditSlotEntryMenu.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}: {
|
export default function EventListSearchResult({entry, refetch}: {
|
||||||
entry: EventListSlotEntriesWithUserModel,
|
entry: EventListSlotEntriesWithUserModel,
|
||||||
|
|
|
@ -25,7 +25,7 @@ export default function ListSearch({event}: { event: EventModel }) {
|
||||||
|
|
||||||
const {pb} = usePB()
|
const {pb} = usePB()
|
||||||
|
|
||||||
const [showFilter, showFilterHandler] = useDisclosure(false)
|
const [showFilter, showFilterHandler] = useDisclosure(true)
|
||||||
|
|
||||||
const [searchQueryString, setSearchQueryString] = useState('')
|
const [searchQueryString, setSearchQueryString] = useState('')
|
||||||
const [selectedLists, setSelectedLists] = useState<EventListModel[]>([])
|
const [selectedLists, setSelectedLists] = useState<EventListModel[]>([])
|
||||||
|
@ -73,11 +73,12 @@ export default function ListSearch({event}: { event: EventModel }) {
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
data={
|
data={
|
||||||
searchQuery
|
(searchQuery
|
||||||
.data
|
.data
|
||||||
?.items
|
?.items
|
||||||
.map(e => e.id)
|
.map(e => e.expand?.user.username)
|
||||||
.filter(onlyUnique) ?? []
|
.filter(u => u !== undefined)
|
||||||
|
.filter(onlyUnique) ?? []) as string[]
|
||||||
}
|
}
|
||||||
value={searchQueryString} onChange={setSearchQueryString}
|
value={searchQueryString} onChange={setSearchQueryString}
|
||||||
/>
|
/>
|
||||||
|
@ -102,17 +103,18 @@ export default function ListSearch({event}: { event: EventModel }) {
|
||||||
</Group>
|
</Group>
|
||||||
|
|
||||||
|
|
||||||
{/*
|
{
|
||||||
todo: more filter
|
/*
|
||||||
<div className={"section stack"}>
|
todo: more filter
|
||||||
<Title order={4} c={"blue"}>
|
<div className={"section stack"}>
|
||||||
Filter
|
<Title order={4} c={"blue"}>
|
||||||
</Title>
|
Filter
|
||||||
<Text> Listenauswahl </Text>
|
</Title>
|
||||||
<Text> Formularfelder Auswahl </Text>
|
<Text> Formularfelder Auswahl </Text>
|
||||||
<Text> Statusfelder Auswahl </Text>
|
<Text> Statusfelder Auswahl </Text>
|
||||||
</div>
|
</div>
|
||||||
*/}
|
*/
|
||||||
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
searchQuery.data?.items.map((entry, index) => (
|
searchQuery.data?.items.map((entry, index) => (
|
||||||
|
|
|
@ -57,6 +57,7 @@ const nav = [
|
||||||
* @param target - the trigger element for the dropdown
|
* @param target - the trigger element for the dropdown
|
||||||
*/
|
*/
|
||||||
export const EventListsMenu = ({event, target}: { event: EventModel, target: ReactNode }) => {
|
export const EventListsMenu = ({event, target}: { event: EventModel, target: ReactNode }) => {
|
||||||
|
|
||||||
return <>
|
return <>
|
||||||
<Menu
|
<Menu
|
||||||
withArrow shadow="md" width={200} trigger="click-hover" loop={false}
|
withArrow shadow="md" width={200} trigger="click-hover" loop={false}
|
||||||
|
|
|
@ -15,7 +15,7 @@ import {
|
||||||
} from "@/pages/events/e/:eventId/EventLists/components/UpdateEventListSlotEntryFormModal.tsx";
|
} from "@/pages/events/e/:eventId/EventLists/components/UpdateEventListSlotEntryFormModal.tsx";
|
||||||
import {ActionIcon, Button, Menu} from "@mantine/core";
|
import {ActionIcon, Button, Menu} from "@mantine/core";
|
||||||
import {IconArrowsMove, IconCheckupList, IconForms, IconSettings, IconTrash} from "@tabler/icons-react";
|
import {IconArrowsMove, IconCheckupList, IconForms, IconSettings, IconTrash} from "@tabler/icons-react";
|
||||||
import {getUserName} from "@/components/auth/modals/util.tsx";
|
import {getUserName} from "@/components/users/modals/util.tsx";
|
||||||
|
|
||||||
export default function EditSlotEntryMenu({entry, refetch}: {
|
export default function EditSlotEntryMenu({entry, refetch}: {
|
||||||
refetch: () => void,
|
refetch: () => void,
|
||||||
|
|
|
@ -26,7 +26,7 @@ export default function EventListSlotProgress({slot}: { slot: EventListSlotsWith
|
||||||
}
|
}
|
||||||
|
|
||||||
// if the slot is full
|
// if the slot is full
|
||||||
if (slot.maxEntries < slot.entriesCount) {
|
if (slot.maxEntries <= slot.entriesCount) {
|
||||||
return (
|
return (
|
||||||
<Group align={"center"} justify={"center"}>
|
<Group align={"center"} justify={"center"}>
|
||||||
<ThemeIcon size={"sm"} variant={"transparent"} color={"orange"}>
|
<ThemeIcon size={"sm"} variant={"transparent"} color={"orange"}>
|
||||||
|
|
|
@ -5,7 +5,7 @@ import {showSuccessNotification} from "@/components/util.tsx";
|
||||||
import {Alert, Button, Group, Modal, Select} from "@mantine/core";
|
import {Alert, Button, Group, Modal, Select} from "@mantine/core";
|
||||||
import {useEffect, useState} from "react";
|
import {useEffect, useState} from "react";
|
||||||
import {pprintDateTime} from "@/lib/datetime.ts";
|
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}: {
|
export const MoveEventListSlotEntryModal = ({opened, close, refetch, entry}: {
|
||||||
opened: boolean,
|
opened: boolean,
|
||||||
|
|
|
@ -5,7 +5,7 @@ import {FieldEntries} from "@/components/formUtil/FromInput/types.ts";
|
||||||
import {showSuccessNotification} from "@/components/util.tsx";
|
import {showSuccessNotification} from "@/components/util.tsx";
|
||||||
import {Modal} from "@mantine/core";
|
import {Modal} from "@mantine/core";
|
||||||
import FormInput from "@/components/formUtil/FromInput";
|
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}: {
|
export const UpdateEventListSlotEntryFormModal = ({opened, close, refetch, entry}: {
|
||||||
opened: boolean,
|
opened: boolean,
|
||||||
|
|
|
@ -7,7 +7,7 @@ import {Alert, Modal} from "@mantine/core";
|
||||||
import InnerHtml from "@/components/InnerHtml";
|
import InnerHtml from "@/components/InnerHtml";
|
||||||
import {RenderDateRange} from "./RenderDateRange.tsx";
|
import {RenderDateRange} from "./RenderDateRange.tsx";
|
||||||
import FormInput from "@/components/formUtil/FromInput";
|
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}: {
|
export const UpdateEventListSlotEntryStatusModal = ({opened, close, refetch, entry}: {
|
||||||
opened: boolean,
|
opened: boolean,
|
||||||
|
|
|
@ -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 {Link, useNavigate} from "react-router-dom";
|
||||||
import {useState} from "react";
|
import {useState} from "react";
|
||||||
import {useDebouncedValue, useToggle} from "@mantine/hooks";
|
import {useDebouncedValue, useToggle} from "@mantine/hooks";
|
||||||
|
|
|
@ -5,6 +5,28 @@
|
||||||
padding: var(--mantine-spacing-xs);
|
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 {
|
.row {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: start;
|
justify-content: start;
|
||||||
|
@ -14,14 +36,7 @@
|
||||||
font-size: var(--mantine-font-size-sm);
|
font-size: var(--mantine-font-size-sm);
|
||||||
|
|
||||||
& > :nth-child(2) {
|
& > :nth-child(2) {
|
||||||
width: 20%;
|
|
||||||
}
|
|
||||||
|
|
||||||
& > :nth-child(3) {
|
|
||||||
width: 20%;
|
|
||||||
}
|
|
||||||
|
|
||||||
& > :nth-child(4) {
|
|
||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
|
@ -17,7 +17,7 @@ import {usePB} from "@/lib/pocketbase.tsx";
|
||||||
import {
|
import {
|
||||||
UpdateEventListSlotEntryFormModal
|
UpdateEventListSlotEntryFormModal
|
||||||
} from "@/pages/events/e/:eventId/EventLists/components/UpdateEventListSlotEntryFormModal.tsx";
|
} 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}: {
|
export default function UserEntryRow({entry, refetch}: {
|
||||||
entry: EventListSlotEntriesWithUserModel,
|
entry: EventListSlotEntriesWithUserModel,
|
||||||
|
@ -65,29 +65,30 @@ export default function UserEntryRow({entry, refetch}: {
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
|
<div className={classes.entryInfo}>
|
||||||
|
<TextWithIcon icon={
|
||||||
|
<ThemeIcon variant={"transparent"} size={"xs"} color={"gray"}>
|
||||||
|
<IconConfetti/>
|
||||||
|
</ThemeIcon>
|
||||||
|
}>
|
||||||
|
{entry.eventName}
|
||||||
|
</TextWithIcon>
|
||||||
|
|
||||||
<TextWithIcon icon={
|
<TextWithIcon icon={
|
||||||
<ThemeIcon variant={"transparent"} size={"xs"} color={"gray"}>
|
<ThemeIcon variant={"transparent"} size={"xs"} color={"gray"}>
|
||||||
<IconConfetti/>
|
<IconList/>
|
||||||
</ThemeIcon>
|
</ThemeIcon>
|
||||||
}>
|
}>
|
||||||
{entry.eventName}
|
{entry.listName}
|
||||||
</TextWithIcon>
|
</TextWithIcon>
|
||||||
|
|
||||||
<TextWithIcon icon={
|
<Group gap={"xs"} justify={"center"}>
|
||||||
<ThemeIcon variant={"transparent"} size={"xs"} color={"gray"}>
|
<RenderDateRange start={new Date(entry.slotStartDate)} end={new Date(entry.slotEndDate)}/>
|
||||||
<IconList/>
|
<Text size={"sm"} c={"dimmed"}>
|
||||||
</ThemeIcon>
|
{delta.message}
|
||||||
}>
|
</Text>
|
||||||
{entry.listName}
|
</Group>
|
||||||
</TextWithIcon>
|
</div>
|
||||||
|
|
||||||
<Group gap={"xs"}>
|
|
||||||
<RenderDateRange start={new Date(entry.slotStartDate)} end={new Date(entry.slotEndDate)}/>
|
|
||||||
<Text size={"sm"} c={"dimmed"}>
|
|
||||||
{delta.message}
|
|
||||||
</Text>
|
|
||||||
</Group>
|
|
||||||
|
|
||||||
<Group gap={"xs"}>
|
<Group gap={"xs"}>
|
||||||
<Tooltip
|
<Tooltip
|
||||||
|
|
|
@ -8,10 +8,33 @@
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
& > :nth-child(2) {
|
& > :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%;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -60,9 +60,13 @@ export default function EventListSlotView({slot, list, refetch}: {
|
||||||
</ThemeIcon>
|
</ThemeIcon>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
|
<div className={classes.slotInfo}>
|
||||||
|
|
||||||
<RenderDateRange start={new Date(slot.startDate)} end={new Date(slot.endDate)}/>
|
<RenderDateRange start={new Date(slot.startDate)} end={new Date(slot.endDate)}/>
|
||||||
|
|
||||||
<EventListSlotProgress slot={slot}/>
|
<EventListSlotProgress slot={slot}/>
|
||||||
|
|
||||||
|
</div>
|
||||||
</UnstyledButton>
|
</UnstyledButton>
|
||||||
|
|
||||||
<Collapse in={expanded} className={"stack"}>
|
<Collapse in={expanded} className={"stack"}>
|
||||||
|
|
|
@ -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 {Accordion, Alert, Anchor, Breadcrumbs, Button, Center, Group, Loader, Title} from "@mantine/core";
|
||||||
import PBAvatar from "@/components/PBAvatar.tsx";
|
import PBAvatar from "@/components/PBAvatar.tsx";
|
||||||
import InnerHtml from "@/components/InnerHtml";
|
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 EventData from "@/pages/events/e/:eventId/EventComponents/EventData.tsx";
|
||||||
import EventListView from "@/pages/events/s/EventListView.tsx";
|
import EventListView from "@/pages/events/s/EventListView.tsx";
|
||||||
import {useEventRights} from "@/pages/events/util.ts";
|
import {useEventRights} from "@/pages/events/util.ts";
|
||||||
|
@ -13,7 +13,7 @@ import {useEventRights} from "@/pages/events/util.ts";
|
||||||
|
|
||||||
export default function SharedEvent() {
|
export default function SharedEvent() {
|
||||||
|
|
||||||
const {pb,user} = usePB()
|
const {pb, user} = usePB()
|
||||||
|
|
||||||
|
|
||||||
const {eventId} = useParams() as { eventId: string }
|
const {eventId} = useParams() as { eventId: string }
|
||||||
|
@ -40,6 +40,8 @@ export default function SharedEvent() {
|
||||||
|
|
||||||
const event = eventQuery.data
|
const event = eventQuery.data
|
||||||
|
|
||||||
|
const eventIsArchived = event.eventAdmins.length === 0
|
||||||
|
|
||||||
return <>
|
return <>
|
||||||
<div className={"section-transparent stack center"}>
|
<div className={"section-transparent stack center"}>
|
||||||
<PBAvatar
|
<PBAvatar
|
||||||
|
@ -51,12 +53,6 @@ export default function SharedEvent() {
|
||||||
</Title>
|
</Title>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{!user && <div className={"section-transparent"}>
|
|
||||||
<Alert icon={<IconLogin/>} color={"orange"}>
|
|
||||||
Um dich in eine Liste einzutragen, musst du dich anmelden
|
|
||||||
</Alert>
|
|
||||||
</div>}
|
|
||||||
|
|
||||||
<div className={"section-transparent"}>
|
<div className={"section-transparent"}>
|
||||||
<Breadcrumbs>{[
|
<Breadcrumbs>{[
|
||||||
{title: "Home", to: "/"},
|
{title: "Home", to: "/"},
|
||||||
|
@ -69,7 +65,32 @@ export default function SharedEvent() {
|
||||||
))}</Breadcrumbs>
|
))}</Breadcrumbs>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{(canEditEvent || canEditEventList) && <div className={"section-transparent"}>
|
{!user && <div className={"section-transparent"}>
|
||||||
|
<Alert icon={<IconLogin/>} color={"orange"}>
|
||||||
|
Um dich in eine Liste einzutragen, musst du dich anmelden
|
||||||
|
</Alert>
|
||||||
|
</div>}
|
||||||
|
|
||||||
|
{eventIsArchived && <div className={"section-transparent"}>
|
||||||
|
<Alert color={"orange"} icon={<IconArchive/>}>
|
||||||
|
Dieses Event ist archiviert und wird nicht mehr verwaltet
|
||||||
|
</Alert>
|
||||||
|
</div>}
|
||||||
|
|
||||||
|
{canEditEventList && <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"}>
|
<Alert color={"green"} title={"Du kannst dieses Event bearbeiten"}>
|
||||||
<Button
|
<Button
|
||||||
component={"a"}
|
component={"a"}
|
||||||
|
|
|
@ -15,10 +15,9 @@ export const useEventRights = (event?: EventModel) => {
|
||||||
user.expand?.memberOf?.map((g: LdapGroupModel) => g.cn).includes(settings.stexGroupId.value)
|
user.expand?.memberOf?.map((g: LdapGroupModel) => g.cn).includes(settings.stexGroupId.value)
|
||||||
)
|
)
|
||||||
const isEventAdmin = !!(user && event && event.eventAdmins.includes(user.id))
|
const isEventAdmin = !!(user && event && event.eventAdmins.includes(user.id))
|
||||||
const isEventListAdmin = !!(user && event && event.eventListAdmins.includes(user.id))
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
canEditEvent: isEventAdmin || isStex,
|
canEditEvent: isEventAdmin || isStex,
|
||||||
canEditEventList: isEventListAdmin || isEventAdmin || isStex
|
canEditEventList: false
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -0,0 +1,8 @@
|
||||||
|
.announcementsContainer {
|
||||||
|
max-height: 60vh;
|
||||||
|
display: flex;
|
||||||
|
& > * {
|
||||||
|
max-height: 100%;
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,27 +1,40 @@
|
||||||
import {EventCalendar} from "@/pages/events/EventOverview/EventCalendar.tsx";
|
import {EventCalendar} from "@/pages/events/EventOverview/EventCalendar.tsx";
|
||||||
import {Alert, Button, Group, ThemeIcon, Title} from "@mantine/core";
|
import {Alert, Button, Group, ThemeIcon, Title} from "@mantine/core";
|
||||||
import {IconConfetti, IconHandLoveYou, IconQrcode} from "@tabler/icons-react";
|
import {IconConfetti, IconHandLoveYou, IconMessageCircle, IconSpeakerphone, IconUser} from "@tabler/icons-react";
|
||||||
import {usePB} from "@/lib/pocketbase.tsx";
|
import {usePB} from "@/lib/pocketbase.tsx";
|
||||||
import {NavLink} from "react-router-dom";
|
import {NavLink} from "react-router-dom";
|
||||||
|
import Announcements from "@/pages/chat/components/Announcements.tsx";
|
||||||
|
import classes from './index.module.css'
|
||||||
|
import {ReactNode} from "react";
|
||||||
|
|
||||||
|
const NavButtons = ({buttons}: {
|
||||||
|
buttons: { title: string, icon: ReactNode, to: string }[]
|
||||||
|
}) => {
|
||||||
|
return <Group className={"section-transparent"} gap={"xs"}>
|
||||||
|
{
|
||||||
|
buttons.map(({title, icon, to}) => (
|
||||||
|
<NavLink to={to} replace key={to}>
|
||||||
|
{({isActive}) => (
|
||||||
|
<Button
|
||||||
|
component={"span"}
|
||||||
|
color={isActive ? "green" : "blue"}
|
||||||
|
variant={"subtle"}
|
||||||
|
size={"sm"}
|
||||||
|
leftSection={icon}
|
||||||
|
>
|
||||||
|
{title}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</NavLink>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</Group>
|
||||||
|
}
|
||||||
|
|
||||||
export default function HomePage() {
|
export default function HomePage() {
|
||||||
|
|
||||||
const {user} = usePB()
|
const {user} = usePB()
|
||||||
|
|
||||||
const nav = [
|
|
||||||
{
|
|
||||||
title: "Events",
|
|
||||||
icon: <IconConfetti/>,
|
|
||||||
to: `/events`
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "QR Generator",
|
|
||||||
icon: <IconQrcode/>,
|
|
||||||
to: `/util/qr`
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
return <>
|
return <>
|
||||||
<div className={"section"}>
|
<div className={"section"}>
|
||||||
|
|
||||||
|
@ -41,30 +54,29 @@ export default function HomePage() {
|
||||||
</>}
|
</>}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Group className={"section-transparent"}>
|
{user && <>
|
||||||
{
|
<div className={"section stack"}>
|
||||||
nav.map(({title, icon, to}) => (
|
<Title order={2}>Ankündigungen</Title>
|
||||||
<NavLink to={to} replace key={to}>
|
|
||||||
{({isActive}) => (
|
|
||||||
<Button
|
|
||||||
radius={"xl"}
|
|
||||||
component={"span"}
|
|
||||||
color={isActive ? "green" : "blue"}
|
|
||||||
variant={"outline"}
|
|
||||||
size={"md"}
|
|
||||||
leftSection={icon}
|
|
||||||
>
|
|
||||||
{title}
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</NavLink>
|
|
||||||
))
|
|
||||||
}
|
|
||||||
</Group>
|
|
||||||
|
|
||||||
<div className={"section"}>
|
<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>
|
<Title order={2}>StuVe Events</Title>
|
||||||
|
|
||||||
|
<NavButtons buttons={[
|
||||||
|
{title: "Deine Anmeldungen", icon: <IconUser/>, to: "/events/entries"},
|
||||||
|
{title: "Events", icon: <IconConfetti/>, to: "/events"},
|
||||||
|
]}/>
|
||||||
|
|
||||||
<EventCalendar/>
|
<EventCalendar/>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
|
|
@ -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 (
|
||||||
|
<div className={"section-transparent"}>
|
||||||
|
<Alert color={"orange"}>
|
||||||
|
Der Debug Modus ist deaktiviert. Bitte aktiviere ihn in den Einstellungen um diese Seite zu sehen.
|
||||||
|
</Alert>
|
||||||
|
</div>)
|
||||||
|
|
||||||
|
return <>
|
||||||
|
<div className={"section"}>
|
||||||
|
<Title c={"orange"} order={1}>Debug</Title>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={"section stack"}>
|
||||||
|
<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"}
|
||||||
|
{...formValues.getInputProps("expand")}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={"section stack"} style={{position: "relative"}}>
|
||||||
|
<LoadingOverlay visible={debugQuery.isLoading}/>
|
||||||
|
<PocketBaseErrorAlert error={debugQuery.error}/>
|
||||||
|
|
||||||
|
{debugQuery.data && <div className={"stack"}>
|
||||||
|
<Group justify={"space-between"}>
|
||||||
|
<Text c={"dimmed"}>
|
||||||
|
{debugQuery.data.totalItems} Ergebniss(e)
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<Text c={"dimmed"}>
|
||||||
|
{debugQuery.data.page}/{debugQuery.data.totalPages} Seiten
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<ActionIcon
|
||||||
|
onClick={() => debugQuery.refetch()}
|
||||||
|
color={"orange"}
|
||||||
|
variant={"transparent"}
|
||||||
|
size={"sm"}
|
||||||
|
aria-label={"Refetch"}
|
||||||
|
disabled={debugQuery.isLoading}
|
||||||
|
>
|
||||||
|
<IconRefresh/>
|
||||||
|
</ActionIcon>
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
<CodeHighlight code={JSON.stringify(debugQuery.data.items, null, 2)} language="json"/>
|
||||||
|
</div>}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
}
|
|
@ -17,6 +17,10 @@
|
||||||
stroke-width: 1.5;
|
stroke-width: 1.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.scrollable {
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
.scrollbar {
|
.scrollbar {
|
||||||
scrollbar-color: var(--mantine-color-blue-5) var(--mantine-color-body);
|
scrollbar-color: var(--mantine-color-blue-5) var(--mantine-color-body);
|
||||||
scrollbar-width: thin;
|
scrollbar-width: thin;
|
||||||
|
@ -46,6 +50,10 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.overflow-hidden {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
.no-scrollbar {
|
.no-scrollbar {
|
||||||
/* Hide scrollbar for WebKit (Safari, Chrome) */
|
/* Hide scrollbar for WebKit (Safari, Chrome) */
|
||||||
|
|
||||||
|
|
|
@ -26,6 +26,9 @@
|
||||||
"paths": {
|
"paths": {
|
||||||
"@/*": [
|
"@/*": [
|
||||||
"src/*"
|
"src/*"
|
||||||
|
],
|
||||||
|
"~/*": [
|
||||||
|
"public/*"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"types": [
|
"types": [
|
||||||
|
|
|
@ -8,8 +8,8 @@ export default defineConfig({
|
||||||
plugins: [react(), svgr()],
|
plugins: [react(), svgr()],
|
||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
alias: {
|
||||||
// Add aliases @ for src directory
|
|
||||||
"@": path.resolve(__dirname, "src"),
|
"@": path.resolve(__dirname, "src"),
|
||||||
}
|
"~": path.resolve(__dirname, "public"),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
})
|
})
|
27
yarn.lock
27
yarn.lock
|
@ -880,6 +880,11 @@
|
||||||
resolved "https://registry.yarnpkg.com/@tiptap/extension-list-item/-/extension-list-item-2.3.0.tgz#7f0af465c1af160648ace586100ff3f8e612e9bf"
|
resolved "https://registry.yarnpkg.com/@tiptap/extension-list-item/-/extension-list-item-2.3.0.tgz#7f0af465c1af160648ace586100ff3f8e612e9bf"
|
||||||
integrity sha512-mHU+IuRa56OT6YCtxf5Z7OSUrbWdKhGCEX7RTrteDVs5oMB6W3oF9j88M5qQmZ1WDcxvQhAOoXctnMt6eX9zcA==
|
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":
|
"@tiptap/extension-ordered-list@^2.3.0":
|
||||||
version "2.3.0"
|
version "2.3.0"
|
||||||
resolved "https://registry.yarnpkg.com/@tiptap/extension-ordered-list/-/extension-ordered-list-2.3.0.tgz#75f7f668201a4cd3ec507c78d2229ec670e3e707"
|
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-strike" "^2.3.0"
|
||||||
"@tiptap/extension-text" "^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@*":
|
"@types/date-arithmetic@*":
|
||||||
version "4.1.4"
|
version "4.1.4"
|
||||||
resolved "https://registry.yarnpkg.com/@types/date-arithmetic/-/date-arithmetic-4.1.4.tgz#bdb441f61a916f11af1874a8c2cf787f77ffcb94"
|
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"
|
file-selector "^0.6.0"
|
||||||
prop-types "^15.8.1"
|
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:
|
react-is@^16.13.1, react-is@^16.7.0:
|
||||||
version "16.13.1"
|
version "16.13.1"
|
||||||
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
|
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"
|
resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4"
|
||||||
integrity sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==
|
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:
|
tiny-invariant@^1.0.6:
|
||||||
version "1.3.3"
|
version "1.3.3"
|
||||||
resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.3.3.tgz#46680b7a873a0d5d10005995eb90a70d74d60127"
|
resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.3.3.tgz#46680b7a873a0d5d10005995eb90a70d74d60127"
|
||||||
|
|
Loading…
Reference in New Issue