feat(chat): added new chat feature
Build and Push Docker image / build-and-push (push) Successful in 5m32s
Details
Build and Push Docker image / build-and-push (push) Successful in 5m32s
Details
you can now enable chat rooms for eventLists
This commit is contained in:
parent
7cef873acd
commit
390edb38bd
|
@ -9,5 +9,5 @@ export const PB_STORAGE_KEY = "stuve-it-login-record"
|
|||
|
||||
// general
|
||||
export const APP_NAME = "StuVe IT"
|
||||
export const APP_VERSION = "0.8.11 (beta)"
|
||||
export const APP_VERSION = "0.9.0 (beta)"
|
||||
export const APP_URL = "https://it.stuve.uni-ulm.de"
|
|
@ -9,7 +9,6 @@ import RegisterModal from "@/components/users/modals/RegisterModal.tsx";
|
|||
import EmailTokenVerification from "@/components/users/modals/EmailTokenVerification.tsx";
|
||||
import ForgotPasswordModal from "@/components/users/modals/ForgotPasswordModal.tsx";
|
||||
import ChangeEmailModal from "@/components/users/modals/ChangeEmailModal.tsx";
|
||||
import ShowMessagesModal from "@/components/users/modals/ShowMessagesModal";
|
||||
|
||||
export default function Layout({hideNav}: { hideNav?: boolean }) {
|
||||
return <div className={classes.container}>
|
||||
|
@ -21,7 +20,6 @@ export default function Layout({hideNav}: { hideNav?: boolean }) {
|
|||
<EmailTokenVerification/>
|
||||
<ForgotPasswordModal/>
|
||||
<ChangeEmailModal/>
|
||||
<ShowMessagesModal/>
|
||||
|
||||
<div className={`${classes.body}`}>
|
||||
<div className={`${classes.content} no-scrollbar`}>
|
||||
|
|
|
@ -1,26 +0,0 @@
|
|||
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>
|
||||
</>
|
||||
}
|
|
@ -1,5 +0,0 @@
|
|||
.messagesContainer {
|
||||
display: flex;
|
||||
flex-direction: column-reverse;
|
||||
gap: var(--gap);
|
||||
}
|
|
@ -1,109 +0,0 @@
|
|||
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>
|
||||
</>
|
||||
}
|
|
@ -36,6 +36,7 @@ export type EventListModel = {
|
|||
favourite: boolean | null;
|
||||
allowOverlappingEntries: boolean | null;
|
||||
onlyStuVeAccounts: boolean | null;
|
||||
enableChat: boolean | null;
|
||||
event: string
|
||||
entryQuestionSchema: FormSchema | null
|
||||
entryStatusSchema: FormSchema | null;
|
||||
|
|
|
@ -1,13 +1,15 @@
|
|||
import {RecordModel} from "pocketbase";
|
||||
import {UserModal} from "@/models/AuthTypes.ts";
|
||||
import {EventListModel} from "@/models/EventTypes.ts";
|
||||
|
||||
export type MessagesModel = {
|
||||
sender: string
|
||||
recipients: string[]
|
||||
thread: string | null
|
||||
recipient: string | null
|
||||
eventList: string | null
|
||||
|
||||
subject: string | null
|
||||
content: string
|
||||
comment: string | null
|
||||
|
||||
repliedTo: string | null
|
||||
|
||||
|
@ -15,19 +17,8 @@ export type MessagesModel = {
|
|||
|
||||
expand: {
|
||||
sender: UserModal
|
||||
recipients: UserModal[]
|
||||
thread: MessageThreadsModel | null
|
||||
recipient: UserModal
|
||||
eventList: EventListModel | null
|
||||
repliedTo: MessagesModel | null
|
||||
}
|
||||
} & RecordModel
|
||||
|
||||
export type MessageThreadsModel = {
|
||||
name: string
|
||||
participants: string[]
|
||||
img: string | null
|
||||
systemThread: boolean | null
|
||||
|
||||
expand: {
|
||||
participants: UserModal[]
|
||||
}
|
||||
} & RecordModel
|
||||
} & RecordModel
|
|
@ -8,7 +8,7 @@ import {
|
|||
EventListSlotsWithEntriesCountModel,
|
||||
EventModel
|
||||
} from "./EventTypes.ts";
|
||||
import {MessagesModel, MessageThreadsModel} from "@/models/MessageTypes.ts";
|
||||
import {MessagesModel} from "@/models/MessageTypes.ts";
|
||||
|
||||
export type SettingsModel = {
|
||||
key: ['privacyPolicy', 'agb', 'stexGroup', 'stuveEventQuestions']
|
||||
|
@ -29,8 +29,6 @@ export interface TypedPocketBase extends PocketBase {
|
|||
|
||||
collection(idOrName: 'messages'): RecordService<MessagesModel>
|
||||
|
||||
collection(idOrName: 'messageThreads'): RecordService<MessageThreadsModel>
|
||||
|
||||
collection(idOrName: 'settings'): RecordService<SettingsModel>
|
||||
|
||||
collection(idOrName: 'legalSettings'): RecordService<LegalSettingsModal>
|
||||
|
|
|
@ -4,8 +4,8 @@ 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 EventListMessagesList from "@/pages/chat/EventListMessagesList.tsx";
|
||||
import ListMessagesView from "@/pages/chat/ListMessagesView.tsx";
|
||||
import {usePB} from "@/lib/pocketbase.tsx";
|
||||
import {useLogin} from "@/components/users/modals/hooks.ts";
|
||||
import Announcements from "@/pages/chat/components/Announcements.tsx";
|
||||
|
@ -34,7 +34,7 @@ export default function ChatRouter() {
|
|||
{title: "Home", to: "/"},
|
||||
{title: "Nachrichten", to: "/chat"},
|
||||
].map(({title, to}) => (
|
||||
<Anchor component={Link} to={to} key={title}>
|
||||
<Anchor component={Link} to={to} key={title} className={"wrapWords"}>
|
||||
{title}
|
||||
</Anchor>
|
||||
))}</Breadcrumbs>
|
||||
|
@ -44,12 +44,12 @@ export default function ChatRouter() {
|
|||
<Outlet/>
|
||||
|
||||
<Routes>
|
||||
<Route path={isMobile ? "/" : "*"} element={<MessageThreadsList/>}/>
|
||||
<Route path={isMobile ? "/" : "*"} element={<EventListMessagesList/>}/>
|
||||
</Routes>
|
||||
|
||||
<Routes>
|
||||
{!isMobile && <Route index element={<ChatIndex/>}/>}
|
||||
<Route path={":threadId"} element={<MessageThreadView/>}/>
|
||||
<Route path={":listId"} element={<ListMessagesView/>}/>
|
||||
<Route path={"announcements"} element={<Announcements/>}/>
|
||||
</Routes>
|
||||
</div>
|
||||
|
|
|
@ -14,7 +14,7 @@
|
|||
background-color: var(--mantine-color-body);
|
||||
}
|
||||
|
||||
.threadText {
|
||||
.listText {
|
||||
color: var(--mantine-color-dimmed);
|
||||
|
||||
width: calc(100% - 40px);
|
||||
|
@ -28,7 +28,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
.threadLink {
|
||||
.listLink {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
|
@ -36,7 +36,7 @@
|
|||
gap: var(--gap);
|
||||
}
|
||||
|
||||
.threadsContainer {
|
||||
.listsContainer {
|
||||
//flex-grow: 1;
|
||||
overflow: auto;
|
||||
border: var(--border);
|
|
@ -0,0 +1,107 @@
|
|||
import {Button, Center, Divider, Loader, Stack, Text, TextInput, ThemeIcon, UnstyledButton} from "@mantine/core";
|
||||
import {NavLink} from "react-router-dom";
|
||||
import classes from "@/pages/chat/EventListMessagesList.module.css";
|
||||
import PBAvatar from "@/components/PBAvatar.tsx";
|
||||
import {IconArrowDown, IconListSearch, IconMoodPuzzled, IconSpeakerphone} from "@tabler/icons-react";
|
||||
import {usePB} from "@/lib/pocketbase.tsx";
|
||||
import {useDebouncedState} from "@mantine/hooks";
|
||||
import {useInfiniteQuery} from "@tanstack/react-query";
|
||||
import {EventListModel} from "@/models/EventTypes.ts";
|
||||
|
||||
const ListMessagesLink = ({eventList}: { eventList: EventListModel }) => {
|
||||
return <UnstyledButton component={NavLink} to={`/chat/${eventList.id}`} className={classes.listLink}>
|
||||
{
|
||||
({isActive}) => <>
|
||||
<PBAvatar model={eventList} name={eventList.name} img={eventList.img} size={"sm"}/>
|
||||
<div className={classes.listText} data-active={isActive}>
|
||||
{eventList.name} ({eventList.expand?.event.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.listText} `} data-active={isActive}>
|
||||
Ankündigungen
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
</UnstyledButton>
|
||||
</>
|
||||
}
|
||||
|
||||
export default function EventListMessagesList() {
|
||||
const {user, pb} = usePB()
|
||||
|
||||
const [eventListNameQuery, setEventListNameQuery] = useDebouncedState("", 200)
|
||||
|
||||
const query = useInfiniteQuery({
|
||||
queryKey: ["eventLists", eventListNameQuery],
|
||||
queryFn: async ({pageParam}) => (
|
||||
await pb.collection("eventLists").getList(pageParam, 100, {
|
||||
filter: `enableChat=true&&name~'${eventListNameQuery}'`,
|
||||
sort: "-messages_via_eventList.created,name",
|
||||
expand: "event"
|
||||
})
|
||||
),
|
||||
getNextPageParam: (lastPage) =>
|
||||
lastPage.page >= lastPage.totalPages ? undefined : lastPage.page + 1,
|
||||
initialPageParam: 1,
|
||||
enabled: !!user,
|
||||
})
|
||||
|
||||
const eventLists = query.data?.pages.flatMap(t => t.items) || []
|
||||
|
||||
return <div className={`stack ${classes.container}`}>
|
||||
<AnnouncementsLink/>
|
||||
|
||||
<Divider label={"Chats"}/>
|
||||
|
||||
<TextInput
|
||||
leftSection={<IconListSearch/>}
|
||||
rightSection={query.isPending ? <Loader size={"xs"}/> : undefined}
|
||||
defaultValue={eventListNameQuery}
|
||||
onChange={(e) => setEventListNameQuery(e.currentTarget.value)}
|
||||
placeholder={"Nach Listen suchen..."}
|
||||
/>
|
||||
|
||||
{eventLists.length === 0 ? <Stack gap={"xs"} align={"center"}>
|
||||
<ThemeIcon variant="transparent" size="xs" color="gray">
|
||||
<IconMoodPuzzled/>
|
||||
</ThemeIcon>
|
||||
|
||||
<Text size={"xs"} c={"dimmed"}>
|
||||
{eventListNameQuery ? "Keine Listen gefunden" : "Keine Listen"}
|
||||
</Text>
|
||||
</Stack> : (
|
||||
<div className={`${classes.listsContainer} no-scrollbar`}>
|
||||
{eventLists.map(list => <ListMessagesLink key={list.id} eventList={list}/>)}
|
||||
|
||||
{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,92 @@
|
|||
import {Link, useParams} from "react-router-dom";
|
||||
import {useQuery} from "@tanstack/react-query";
|
||||
import {PocketBaseErrorAlert, usePB} from "@/lib/pocketbase.tsx";
|
||||
import {ActionIcon, Button, Center, Collapse, Group, Loader, Text} from "@mantine/core";
|
||||
import PBAvatar from "@/components/PBAvatar.tsx";
|
||||
import {IconArrowRight, IconChevronLeft, IconInfoCircle, IconInfoCircleFilled} from "@tabler/icons-react";
|
||||
import {useDisclosure} from "@mantine/hooks";
|
||||
import Messages from "@/pages/chat/components/Messages.tsx";
|
||||
import classes from './ListMessagesView.module.css';
|
||||
import InnerHtml from "@/components/InnerHtml";
|
||||
import {useEventRights} from "@/pages/events/util.ts";
|
||||
|
||||
export default function ListMessagesView() {
|
||||
const {listId} = useParams() as { listId: string }
|
||||
|
||||
const {pb} = usePB()
|
||||
|
||||
const [showListInfo, showListInfoHandler] = useDisclosure(false)
|
||||
|
||||
const query = useQuery({
|
||||
queryKey: ["eventListMessageView", listId],
|
||||
queryFn: async () => (
|
||||
await pb.collection("eventLists").getOne(listId, {expand: "event"})
|
||||
)
|
||||
})
|
||||
|
||||
const {canEditEvent} = useEventRights(query.data?.expand?.event)
|
||||
|
||||
if (query.isError) return (
|
||||
<PocketBaseErrorAlert error={query.error}/>
|
||||
)
|
||||
|
||||
if (query.isLoading || !query.data) return (
|
||||
<Center>
|
||||
<Loader/>
|
||||
</Center>
|
||||
)
|
||||
|
||||
const eventList = query.data
|
||||
|
||||
return <div className={"stack"}>
|
||||
<Group wrap={"nowrap"}>
|
||||
<ActionIcon
|
||||
component={Link}
|
||||
to={"/chat"}
|
||||
variant={"transparent"} color={"blue"}
|
||||
radius={"xl"}
|
||||
aria-label={"Back to Threads"}
|
||||
className={classes.backIcon}
|
||||
>
|
||||
<IconChevronLeft/>
|
||||
</ActionIcon>
|
||||
|
||||
<PBAvatar model={eventList} name={eventList.name} img={eventList.img} size={"lg"}/>
|
||||
<div>
|
||||
<Text size={"lg"} fw={600}>{eventList.name}</Text>
|
||||
<Text size={"xs"} c={"dimmed"}>{eventList.expand?.event.name}</Text>
|
||||
</div>
|
||||
<ActionIcon
|
||||
variant={"transparent"} color={"blue"}
|
||||
radius={"xl"} ms={"auto"}
|
||||
aria-label={"Thread Info"}
|
||||
onClick={showListInfoHandler.toggle}
|
||||
>
|
||||
{showListInfo ? <IconInfoCircleFilled/> : <IconInfoCircle/>}
|
||||
</ActionIcon>
|
||||
</Group>
|
||||
|
||||
<Collapse in={showListInfo}>
|
||||
<div className={"stack"}>
|
||||
<InnerHtml html={eventList.description ?? "Keine Beschreibung vorhanden"}/>
|
||||
{
|
||||
(canEditEvent || eventList.expand?.event.privilegedLists.includes(eventList.id))
|
||||
&&
|
||||
<Group>
|
||||
<Button
|
||||
rightSection={<IconArrowRight/>}
|
||||
variant={"light"} color={"blue"}
|
||||
size={"xs"}
|
||||
component={Link}
|
||||
to={`/events/e/${eventList.event}/lists/overview/${eventList.id}`}
|
||||
>
|
||||
Zur Liste
|
||||
</Button>
|
||||
</Group>
|
||||
}
|
||||
</div>
|
||||
</Collapse>
|
||||
|
||||
<Messages eventList={eventList}/>
|
||||
</div>
|
||||
}
|
|
@ -1,134 +0,0 @@
|
|||
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>
|
||||
}
|
|
@ -1,140 +0,0 @@
|
|||
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>
|
||||
}
|
|
@ -8,7 +8,6 @@ export default function Announcement({subject, content}: {
|
|||
content: string,
|
||||
}) {
|
||||
return <div className={classes.announcement}>
|
||||
|
||||
<Group justify={"space-between"} wrap={"nowrap"} align={"top"} mb={"md"}>
|
||||
{subject && <div className={classes.subject}>
|
||||
{subject}
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
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";
|
||||
|
@ -13,7 +12,7 @@ export default function Announcements() {
|
|||
queryKey: ["announcements"],
|
||||
queryFn: async ({pageParam}) => (
|
||||
await pb.collection("messages").getList(pageParam, 100, {
|
||||
filter: `isAnnouncement=true`,
|
||||
filter: `isAnnouncement=true&&sender!='${user?.id}'`,
|
||||
sort: "-created"
|
||||
})
|
||||
),
|
||||
|
|
|
@ -11,28 +11,28 @@ export default function ChatNavIcon() {
|
|||
const [newMessage, setNewMessage] = useState<string | null>(null);
|
||||
const {start} = useTimeout(() => setNewMessage(null), 5000);
|
||||
|
||||
const {user, pb, useSubscription} = usePB()
|
||||
const {user, useSubscription} = usePB()
|
||||
|
||||
const match = useMatch("/chat/:threadId")
|
||||
const match = useMatch("/chat/:listId")
|
||||
|
||||
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 (event.action == "create") {
|
||||
if (event.record.isAnnouncement) {
|
||||
start()
|
||||
setNewMessage("announcements")
|
||||
} else if (
|
||||
event.record.eventList
|
||||
&&
|
||||
match?.params.listId !== event.record.eventList // check if chat page is not already open
|
||||
&&
|
||||
event.record.sender !== user?.id // check if sender is not the user
|
||||
) {
|
||||
start()
|
||||
setNewMessage(event.record.eventList)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
|
|
@ -1,26 +1,26 @@
|
|||
import {MessagesModel, MessageThreadsModel} from "@/models/MessageTypes.ts";
|
||||
import {MessagesModel} 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";
|
||||
import {EventListModel} from "@/models/EventTypes.ts";
|
||||
|
||||
export default function Messages({thread}: {
|
||||
thread: MessageThreadsModel
|
||||
export default function Messages({eventList}: {
|
||||
eventList: EventListModel
|
||||
}) {
|
||||
const {user, pb, useSubscription} = usePB()
|
||||
|
||||
const query = useInfiniteQuery({
|
||||
queryKey: ["messages", thread],
|
||||
queryKey: ["messages", eventList],
|
||||
queryFn: async ({pageParam}) => (
|
||||
await pb.collection("messages").getList(pageParam, 100, {
|
||||
filter: `thread='${thread?.id}'`,
|
||||
filter: `eventList='${eventList?.id}'`,
|
||||
sort: "-created",
|
||||
expand: "sender"
|
||||
})
|
||||
|
@ -35,9 +35,8 @@ export default function Messages({thread}: {
|
|||
idOrName: "messages",
|
||||
topic: "*",
|
||||
callback: (event) => {
|
||||
if (event.action == "create" && event.record.thread == thread.id) {
|
||||
if (event.action == "create" && event.record.eventList == eventList.id) {
|
||||
query.refetch()
|
||||
console.log("test")
|
||||
}
|
||||
}
|
||||
})
|
||||
|
@ -47,7 +46,7 @@ export default function Messages({thread}: {
|
|||
await pb.collection("messages").create({
|
||||
...formValues.values,
|
||||
sender: user!.id,
|
||||
thread: thread.id,
|
||||
eventList: eventList.id,
|
||||
})
|
||||
},
|
||||
onSuccess: () => {
|
||||
|
|
|
@ -1,76 +0,0 @@
|
|||
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 EventOverview() {
|
|||
<Breadcrumbs>{
|
||||
[{title: "Home", to: "/"}, {title: "Events", to: "/events"},
|
||||
].map(({title, to}) => (
|
||||
<Anchor component={Link} to={to} key={title}>{title}</Anchor>
|
||||
<Anchor component={Link} to={to} key={title} className={"wrapWords"}>{title}</Anchor>
|
||||
))
|
||||
}</Breadcrumbs>
|
||||
</div>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import QRCodeModal from "@/pages/events/StatusEditor/QrCodeModal.tsx";
|
||||
import EntrySearchModal from "@/pages/events/StatusEditor/EntrySearchModal.tsx";
|
||||
import {ActionIcon, Center, Group, Text} from "@mantine/core";
|
||||
import {ActionIcon, Group, Text, Title} from "@mantine/core";
|
||||
import {IconQrcode, IconUserSearch} from "@tabler/icons-react";
|
||||
import {useDisclosure} from "@mantine/hooks";
|
||||
import SearchSVG from "@/illustrations/search.svg?react"
|
||||
|
@ -14,11 +14,13 @@ export default function EntrySearch() {
|
|||
<QRCodeModal opened={showQrCodeModal} onClose={showQrCodeModalHandler.close}/>
|
||||
<EntrySearchModal opened={showEntrySearchModal} onClose={showEntrySearchModalHandler.close}/>
|
||||
|
||||
<Center>
|
||||
<div className={"stack center"}>
|
||||
<SearchSVG height={"300px"} width={"300px"}/>
|
||||
</Center>
|
||||
|
||||
<div className={" center"}>
|
||||
<Title c={"blue"} fw={900} mb={"xl"}>
|
||||
Status Editor
|
||||
</Title>
|
||||
|
||||
<Group justify={"center"}>
|
||||
<ActionIcon
|
||||
variant="light" size="xl" radius="xl"
|
||||
|
|
|
@ -43,7 +43,7 @@ export default function StatusEditor() {
|
|||
{title: event.name, to: `/events/${event.id}`},
|
||||
{title: "Status Editor", to: `/events/e/${event.id}/lists/status`},
|
||||
].map(({title, to}) => (
|
||||
<Anchor component={Link} to={to} key={title}>
|
||||
<Anchor component={Link} to={to} key={title} className={"wrapWords"}>
|
||||
{title}
|
||||
</Anchor>
|
||||
))}</Breadcrumbs>
|
||||
|
|
|
@ -118,7 +118,7 @@ export default function EditEventRouter() {
|
|||
{title: "Events", to: "/events"},
|
||||
{title: event.name, to: `/events/${event.id}`},
|
||||
].map(({title, to}) => (
|
||||
<Anchor component={Link} to={to} key={title}>
|
||||
<Anchor className={"wrapWords"} component={Link} to={to} key={title}>
|
||||
{title}
|
||||
</Anchor>
|
||||
))}</Breadcrumbs>
|
||||
|
|
|
@ -6,6 +6,7 @@ import {
|
|||
IconForms,
|
||||
IconLock,
|
||||
IconLockOpen,
|
||||
IconMessageCircle,
|
||||
IconSettings,
|
||||
IconUserCog
|
||||
} from "@tabler/icons-react";
|
||||
|
@ -57,6 +58,14 @@ export default function EventListRouter({event}: { event: EventModel }) {
|
|||
}
|
||||
]
|
||||
|
||||
if (list.enableChat) {
|
||||
nav.push({
|
||||
icon: <IconMessageCircle/>,
|
||||
to: `/chat/${list.id}`,
|
||||
title: "Chat"
|
||||
})
|
||||
}
|
||||
|
||||
if (canEditEvent) {
|
||||
nav.push(...[
|
||||
{
|
||||
|
@ -81,10 +90,10 @@ export default function EventListRouter({event}: { event: EventModel }) {
|
|||
<Breadcrumbs>
|
||||
<Link to={`/events/e/${event.id}/lists/overview`}>
|
||||
<Title order={2}>
|
||||
Event Listen
|
||||
Listen
|
||||
</Title>
|
||||
</Link>
|
||||
<Link to={`/events/e/${event.id}/lists/overview/${list.id}`}>
|
||||
<Link to={`/events/e/${event.id}/lists/overview/${list.id}`} className={"wrapWords"}>
|
||||
<Title order={2}>
|
||||
{list.name}
|
||||
</Title>
|
||||
|
|
|
@ -21,6 +21,7 @@ export default function ListSettings({list, event}: { list: EventListModel, even
|
|||
open: list.open,
|
||||
allowOverlappingEntries: list.allowOverlappingEntries,
|
||||
onlyStuVeAccounts: list.onlyStuVeAccounts,
|
||||
enableChat: list.enableChat,
|
||||
favorite: list.favorite,
|
||||
description: list.description || "",
|
||||
}
|
||||
|
@ -105,6 +106,19 @@ export default function ListSettings({list, event}: { list: EventListModel, even
|
|||
{...formValues.getInputProps("allowOverlappingEntries", {type: "checkbox"})}
|
||||
/>
|
||||
|
||||
<CheckboxCard
|
||||
label={"Chat aktivieren"}
|
||||
description={
|
||||
"Du kannst eine neue Chatgruppe für diese Liste erstellen. " +
|
||||
"Alle Anmeldungen der Liste können diesen Chat nutzen. " +
|
||||
"Eventadmins können den Chat auch nutzen und alle Nachrichten bearbeiten sowie löschen. " +
|
||||
"Neue Anmeldungen sehen auch vergangene Nachrichten.<br/>" +
|
||||
"Der Chat wird automatisch gelöscht, wenn diese Liste gelöscht wird. Wenn du den Chat deaktivierst, " +
|
||||
"werden die Nachrichten nicht gelöscht und kann der Chat kann später wieder aktiviert werden."
|
||||
}
|
||||
{...formValues.getInputProps("enableChat", {type: "checkbox"})}
|
||||
/>
|
||||
|
||||
<TextEditor
|
||||
maxHeight={"500px"}
|
||||
fullToolbar
|
||||
|
@ -113,7 +127,6 @@ export default function ListSettings({list, event}: { list: EventListModel, even
|
|||
/>
|
||||
|
||||
<Group>
|
||||
|
||||
<Button
|
||||
variant={"outline"}
|
||||
color={"red"}
|
||||
|
|
|
@ -5,7 +5,6 @@ import {
|
|||
ActionIcon,
|
||||
Alert,
|
||||
Autocomplete,
|
||||
Box,
|
||||
Button,
|
||||
Center,
|
||||
Collapse,
|
||||
|
@ -22,9 +21,9 @@ import {
|
|||
import {hasLength, useForm} from "@mantine/form";
|
||||
import TextEditor from "@/components/input/Editor";
|
||||
import {Link, useNavigate} from "react-router-dom";
|
||||
import {IconArrowRight, IconListSearch, IconLock, IconLockOpen, IconPlus} from "@tabler/icons-react";
|
||||
import {useRef, useState} from "react";
|
||||
import {useDebouncedState} from "@mantine/hooks";
|
||||
import {IconArrowRight, IconListSearch, IconLock, IconLockOpen, IconMinus, IconPlus} from "@tabler/icons-react";
|
||||
import {useState} from "react";
|
||||
import {useDebouncedState, useDisclosure} from "@mantine/hooks";
|
||||
import {onlyUnique} from "@/lib/util.ts";
|
||||
import {useEventRights} from "@/pages/events/util.ts";
|
||||
|
||||
|
@ -88,6 +87,8 @@ export default function EventListsOverview({event}: { event: EventModel }) {
|
|||
|
||||
const navigate = useNavigate()
|
||||
|
||||
const [createNewList, createNewListHandler] = useDisclosure(false)
|
||||
|
||||
const formValues = useForm({
|
||||
initialValues: {
|
||||
name: "",
|
||||
|
@ -123,48 +124,43 @@ export default function EventListsOverview({event}: { event: EventModel }) {
|
|||
}
|
||||
})
|
||||
|
||||
const newListNameRef = useRef<HTMLInputElement>(null)
|
||||
|
||||
const {canEditEvent} = useEventRights(event)
|
||||
|
||||
return (
|
||||
<div className={"stack"}>
|
||||
{canEditEvent &&
|
||||
<form
|
||||
className={"section stack"}
|
||||
onSubmit={formValues.onSubmit(() => createListMutation.mutate())}
|
||||
>
|
||||
<Title order={3} c={"blue"}>
|
||||
<Group>
|
||||
<ActionIcon
|
||||
mb={"5"} variant={"transparent"}
|
||||
onClick={() => newListNameRef.current?.focus()}
|
||||
>
|
||||
<IconPlus/>
|
||||
</ActionIcon>
|
||||
<Collapse in={createNewList}>
|
||||
<form
|
||||
className={"section stack"}
|
||||
onSubmit={formValues.onSubmit(() => createListMutation.mutate())}
|
||||
>
|
||||
<Title order={3} c={"blue"}>
|
||||
Neue Liste Erstellen
|
||||
</Group>
|
||||
</Title>
|
||||
</Title>
|
||||
|
||||
<PocketBaseErrorAlert error={createListMutation.error}/>
|
||||
<PocketBaseErrorAlert error={createListMutation.error}/>
|
||||
|
||||
<TextInput
|
||||
ref={newListNameRef}
|
||||
variant={"filled"}
|
||||
placeholder={"Name der neuen Liste ..."}
|
||||
{...formValues.getInputProps("name")}
|
||||
/>
|
||||
<TextInput
|
||||
placeholder={"Name der neuen Liste ..."}
|
||||
{...formValues.getInputProps("name")}
|
||||
/>
|
||||
|
||||
<Collapse in={!!formValues.values.name}>
|
||||
<Title order={4} c={"blue"}>Beschreibung</Title>
|
||||
|
||||
<TextEditor
|
||||
mt={"sm"}
|
||||
value={formValues.values.description}
|
||||
onChange={(value) => formValues.setFieldValue("description", value)}
|
||||
/>
|
||||
|
||||
<Box mt={"sm"}>
|
||||
<Group>
|
||||
<Button
|
||||
variant={"light"}
|
||||
color={"orange"}
|
||||
onClick={createNewListHandler.close}
|
||||
>
|
||||
Abbrechen
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
type={"submit"}
|
||||
loading={createListMutation.isPending}
|
||||
|
@ -172,15 +168,25 @@ export default function EventListsOverview({event}: { event: EventModel }) {
|
|||
>
|
||||
Liste Erstellen
|
||||
</Button>
|
||||
</Box>
|
||||
</Collapse>
|
||||
</form>
|
||||
</Group>
|
||||
</form>
|
||||
</Collapse>
|
||||
}
|
||||
|
||||
<Autocomplete
|
||||
placeholder={"Nach einer Liste suchen ..."}
|
||||
leftSection={<IconListSearch/>}
|
||||
rightSection={listsQuery.isLoading ? <Loader size={"xs"}/> : undefined}
|
||||
rightSection={listsQuery.isLoading ? <Loader size={"xs"}/> : (
|
||||
canEditEvent &&
|
||||
<Tooltip label={"Neue Liste erstellen"} withArrow>
|
||||
<ActionIcon
|
||||
variant={"transparent"}
|
||||
onClick={createNewListHandler.toggle}
|
||||
>
|
||||
{createNewList ? <IconMinus/> : <IconPlus/>}
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
)}
|
||||
data={
|
||||
listsQuery
|
||||
.data
|
||||
|
|
|
@ -67,7 +67,7 @@ export default function UserEntries() {
|
|||
{title: "Events", to: "/events"},
|
||||
{title: "Anmeldungen", to: `/events/entries`},
|
||||
].map(({title, to}) => (
|
||||
<Anchor component={Link} to={to} key={title}>
|
||||
<Anchor component={Link} to={to} key={title} className={"wrapWords"}>
|
||||
{title}
|
||||
</Anchor>
|
||||
))}</Breadcrumbs>
|
||||
|
|
|
@ -23,7 +23,7 @@ export default function EventListView({event, listId}: { event: EventModel, list
|
|||
|
||||
const listSlotsQuery = useQuery({
|
||||
queryKey: ["event", event.id, "list", listId, "slots", activePage],
|
||||
queryFn: async () => (await pb.collection("eventListSlotsWithEntriesCount").getList(activePage, 10, {
|
||||
queryFn: async () => (await pb.collection("eventListSlotsWithEntriesCount").getList(activePage, 20, {
|
||||
filter: `eventList='${listId}'`,
|
||||
sort: "startDate",
|
||||
enabled: !!listId,
|
||||
|
|
|
@ -66,7 +66,7 @@ export default function SharedEvent() {
|
|||
{title: "Events", to: "/events"},
|
||||
{title: event.name, to: `/events/${event.id}`},
|
||||
].map(({title, to}) => (
|
||||
<Anchor component={Link} to={to} key={title}>
|
||||
<Anchor component={Link} to={to} key={title} className={"wrapWords"}>
|
||||
{title}
|
||||
</Anchor>
|
||||
))}</Breadcrumbs>
|
||||
|
|
|
@ -67,12 +67,10 @@
|
|||
|
||||
a {
|
||||
text-decoration: unset; /* Removes default underline */
|
||||
color: revert;
|
||||
}
|
||||
|
||||
a:hover, a:active, a:visited {
|
||||
text-decoration: unset; /* Removes default underline */
|
||||
color: unset;
|
||||
}
|
||||
|
||||
.section-icon {
|
||||
|
@ -156,5 +154,5 @@ a:hover, a:active, a:visited {
|
|||
}
|
||||
|
||||
.monospace {
|
||||
font-family: var(--mantine-font-family-monospace);
|
||||
font-family: var(--mantine-font-family-monospace), monospace;
|
||||
}
|
Loading…
Reference in New Issue