diff --git a/README.md b/README.md index 17f6919..af9c1ac 100644 --- a/README.md +++ b/README.md @@ -17,10 +17,12 @@ Dieses Repository enthält den Quellcode für die StuVe IT Frontend Webseite. ## Backend -Als Backend wird ein selbst erweitertes Pocketbase verwendet. Dieses ist in diesem [Repo](https://gitlab.uni-ulm.de/stuve-it/it-tools/backend/) +Als Backend wird ein selbst erweitertes Pocketbase verwendet. Dieses ist in +diesem [Repo](https://gitlab.uni-ulm.de/stuve-it/it-tools/backend/) zu finden. ## Projekt Struktur + ``` ├── index.html # html Datei die den React Code in die Seite einbindet ├── tsconfig.json # Typescript Config Datei @@ -41,14 +43,31 @@ zu finden. ### Pocketbase API -Um die Kommunikation mit dem Backend zu erleichtern ist in der Datei "s@/lib/pocketbase.ts" ein Pocketbase Client implementiert. +Um die Kommunikation mit dem Backend zu erleichtern ist in der Datei "@/lib/pocketbase.ts" ein Pocketbase Client +implementiert. Dieser Client ist ein React Hook und kann in jeder React Komponente verwendet werden. ```typescript -import {usePB} from @/lib/pocketbase"; +import {usePB} from "@/lib/pocketbase" +import {useQuery} from "@tanstack/react-query"; -const {pb} = usePB(); +const {pb} = usePB() +const user = useUser() + +const query = useQuery({ + queryKey: ["collection", id], + queryFn: async () => { + return await pb.collection("collection").getOne(id) + }, + enabled: !!id && !!user +}) ``` #### Pocketbase Modell Types + +#### Todo + +- todo api rules so that only event admins can set status +- todo api rules so that only entries for future events can be created +- todo api rule so that entries for past events cant be edited or deleted diff --git a/src/components/formUtil/formBuilder/index.tsx b/src/components/formUtil/formBuilder/index.tsx index f575bce..94479bc 100644 --- a/src/components/formUtil/formBuilder/index.tsx +++ b/src/components/formUtil/formBuilder/index.tsx @@ -39,7 +39,6 @@ export default function FormBuilder({ const [showPreview, setShowPreview] = useDisclosure(false) const formValues = useForm({ - // mode: "uncontrolled", todo: fix this initialValues: defaultValue, validateInputOnChange: true, validate: { diff --git a/src/main.tsx b/src/main.tsx index 6f86dd8..7f0a660 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -29,7 +29,7 @@ const themeOverride = createTheme({ components: { Alert: Alert.extend({ defaultProps: { - radius: 'md', + radius: 'var(--border-radius)', } }) } diff --git a/src/models/EventTypes.ts b/src/models/EventTypes.ts index b7571b3..c1a3cd5 100644 --- a/src/models/EventTypes.ts +++ b/src/models/EventTypes.ts @@ -83,7 +83,8 @@ export type EventListSlotEntriesWithUserModel = slotDescription: string | null, eventList: string, listDescription: string | null, - event: string + event: string, + eventName: string, } & Pick & Pick \ No newline at end of file diff --git a/src/pages/events/EventsRouter.tsx b/src/pages/events/EventsRouter.tsx index 695468b..acbf25c 100644 --- a/src/pages/events/EventsRouter.tsx +++ b/src/pages/events/EventsRouter.tsx @@ -4,6 +4,7 @@ import EventOverview from "@/pages/events/EventOverview/index.page.tsx"; import EventView from "./s/EventView.tsx"; import EventNavigate from "@/pages/events/EventNavigate.tsx"; import EditEventRouter from "@/pages/events/e/:eventId/EditEventRouter.tsx"; +import UserEntries from "@/pages/events/entries/UserEntries.tsx"; export default function EventsRouter() { @@ -13,6 +14,7 @@ export default function EventsRouter() { }/> }/> }/> + }/> }/> diff --git a/src/pages/events/e/:eventId/EventLists/EventListSearch/index.tsx b/src/pages/events/e/:eventId/EventLists/EventListSearch/index.tsx index 70d821e..94ebbb3 100644 --- a/src/pages/events/e/:eventId/EventLists/EventListSearch/index.tsx +++ b/src/pages/events/e/:eventId/EventLists/EventListSearch/index.tsx @@ -102,7 +102,7 @@ export default function ListSearch({event}: { event: EventModel }) { {/* - todo: filter + todo: more filter
Filter diff --git a/src/pages/events/entries/UserEntries.tsx b/src/pages/events/entries/UserEntries.tsx new file mode 100644 index 0000000..c3a2af8 --- /dev/null +++ b/src/pages/events/entries/UserEntries.tsx @@ -0,0 +1,133 @@ +import {useUser} from "@/lib/user.ts"; +import PromptLoginModal from "@/components/auth/modals/PromptLoginModal.tsx"; +import {Link, useNavigate} from "react-router-dom"; +import {useState} from "react"; +import {useDebouncedValue, useToggle} from "@mantine/hooks"; +import {useQuery} from "@tanstack/react-query"; +import {PocketBaseErrorAlert, usePB} from "@/lib/pocketbase.tsx"; +import { + ActionIcon, + Anchor, + Breadcrumbs, + Center, + Group, + Loader, + LoadingOverlay, + Pagination, + Stack, + Text, + TextInput, + ThemeIcon, + Title, + Tooltip +} from "@mantine/core"; +import ShowHelp from "@/components/ShowHelp.tsx"; +import {IconConfetti, IconDatabaseOff, IconSortAscending, IconSortDescending} from "@tabler/icons-react"; +import UserEntryRow from "@/pages/events/entries/UserEntryRow.tsx"; + +export default function UserEntries() { + + const user = useUser() + const navigate = useNavigate() + + const {pb} = usePB() + + const [page, setPage] = useState(1) + + const [sortDirection, toggleSortDirection] = useToggle(['DESC', 'ASC']); + + const [eventSearch, setEventSearch] = useState("") + + const [debouncedSearch] = useDebouncedValue(eventSearch, 200) + + const entriesQuery = useQuery({ + queryKey: ["userEntries", user?.id, page, debouncedSearch, sortDirection], + queryFn: async () => ( + await pb.collection("eventListSlotEntriesWithUser").getList(page, 50, { + filter: `userId='${user?.id}'${debouncedSearch ? `&&event.name~'${debouncedSearch}'` : ""}`, + sort: sortDirection === "ASC" ? "-event.startDate" : "event.startDate" + }) + ), + enabled: !!user + }) + + if (!user) { + return <PromptLoginModal onAbort={() => navigate("/")}/> + } + + return <> + {entriesQuery.isLoading && <LoadingOverlay/>} + + <div className={"section-transparent"}> + <Breadcrumbs>{[ + {title: "Home", to: "/"}, + {title: "Events", to: "/events"}, + {title: "Meine Einträge", to: `/events/entries`}, + ].map(({title, to}) => ( + <Anchor component={Link} to={to} key={title}> + {title} + </Anchor> + ))}</Breadcrumbs> + </div> + <div className={"section"}> + <Title order={1} c={"blue"}>Meine Einträge +
+
+ + Auf dieser Seite findest du alle deine Einträge zu Events und kannst diese verwalten. + +
+ +
+ setEventSearch(ev.target.value)} + leftSection={} + rightSection={entriesQuery.isLoading ? : ( + + toggleSortDirection()} + variant={"transparent"} + aria-label={"toggle sort"} + > + {sortDirection === "ASC" ? : } + + + )} + placeholder={"Nach Events suchen ..."} + /> + + + + {eventSearch ? `Suche nach Einträgen für Event '${eventSearch}'` : "Alle deine Einträge"} + + + + {entriesQuery.data?.totalItems ?? 0} {eventSearch ? "Ergebnisse" : "Einträge"} + + + + + + {entriesQuery.data?.totalItems === 0 && + + + + + Keine Einträge vorhanden + + } + + {entriesQuery.data?.items.map(entry => ( + entriesQuery.refetch()} key={entry.id}/> + ))} + +
+ +
+ +
+ +} \ No newline at end of file diff --git a/src/pages/events/entries/UserEntryRow.module.css b/src/pages/events/entries/UserEntryRow.module.css new file mode 100644 index 0000000..577a018 --- /dev/null +++ b/src/pages/events/entries/UserEntryRow.module.css @@ -0,0 +1,27 @@ +.container { + background-color: var(--mantine-color-body); + border: var(--border); + border-radius: var(--border-radius); + padding: var(--mantine-spacing-xs); +} + +.row { + display: flex; + justify-content: start; + align-items: center; + gap: var(--gap); + + font-size: var(--mantine-font-size-sm); + + & > :nth-child(2) { + width: 20%; + } + + & > :nth-child(3) { + width: 20%; + } + + & > :nth-child(4) { + flex: 1; + } +} \ No newline at end of file diff --git a/src/pages/events/entries/UserEntryRow.tsx b/src/pages/events/entries/UserEntryRow.tsx new file mode 100644 index 0000000..f0c359a --- /dev/null +++ b/src/pages/events/entries/UserEntryRow.tsx @@ -0,0 +1,127 @@ +import {EventListSlotEntriesWithUserModel} from "@/models/EventTypes.ts"; +import {ActionIcon, Collapse, Group, Text, ThemeIcon, Tooltip} from "@mantine/core"; +import {IconChevronDown, IconChevronRight, IconConfetti, IconForms, IconList, IconTrash} from "@tabler/icons-react"; +import TextWithIcon from "@/components/layout/TextWithIcon"; +import { + EventListSlotEntryDetails +} from "@/pages/events/e/:eventId/EventLists/:listId/EventListSlotsTable/EventListSlotEntryRow.tsx"; + +import {useDisclosure} from "@mantine/hooks"; +import {RenderDateRange} from "@/pages/events/e/:eventId/EventLists/components/RenderDateRange.tsx"; +import classes from "./UserEntryRow.module.css" +import {humanDeltaFromNow} from "@/lib/datetime.ts"; +import {useMutation} from "@tanstack/react-query"; +import {showSuccessNotification} from "@/components/util.tsx"; +import {useConfirmModal} from "@/components/ConfirmModal.tsx"; +import {usePB} from "@/lib/pocketbase.tsx"; +import { + UpdateEventListSlotEntryFormModal +} from "@/pages/events/e/:eventId/EventLists/components/UpdateEventListSlotEntryFormModal.tsx"; + +export default function UserEntryRow({entry, refetch}: { + entry: EventListSlotEntriesWithUserModel, + refetch: () => void +}) { + const {pb} = usePB() + + const [expanded, expandedHandler] = useDisclosure(false) + + const [showEditFormModal, showEditFormModalHandler] = useDisclosure(false) + + const delta = humanDeltaFromNow(entry.slotStartDate, entry.slotEndDate) + + const deleteEntryMutation = useMutation({ + mutationFn: async () => { + await pb.collection("eventListSlotEntries").delete(entry.id) + }, + onSuccess: () => { + refetch() + showSuccessNotification("Eintrag gelöscht") + } + }) + + const {ConfirmModal, toggleConfirmModal} = useConfirmModal({ + title: 'Eintrag löschen', + description: `Möchtest du den Eintrag von ${entry.userName} wirklich löschen?`, + onConfirm: () => deleteEntryMutation.mutate() + }) + + return
+ + + + + +
+ + + {expanded ? : } + + + + + + + + }> + {entry.eventName} + + + + + + }> + {entry.listName} + + + + + + {delta.message} + + + + + + + + + + + + + + + + +
+ + + + +
+} \ No newline at end of file diff --git a/src/pages/events/s/EventListSlotView.tsx b/src/pages/events/s/EventListSlotView.tsx index c02cd7f..62a4ecf 100644 --- a/src/pages/events/s/EventListSlotView.tsx +++ b/src/pages/events/s/EventListSlotView.tsx @@ -22,6 +22,8 @@ export default function EventListSlotView({slot, refetch}: { const slotIsFull = slot.maxEntries === 0 || slot.maxEntries === null ? false : slot.entriesCount >= slot.maxEntries + const slotIsInPast = new Date(slot.endDate) < new Date() + const schema = { fields: [ ...slot.entryQuestionSchema?.fields ?? [], @@ -36,7 +38,7 @@ export default function EventListSlotView({slot, refetch}: { const createEntryMutation = useMutation({ mutationFn: async (data: FieldEntries) => { await pb.collection("eventListSlotEntries").create({ - entryQuestionData: data, // todo + entryQuestionData: data, eventListsSlot: slot.id, ldapUser: user?.realm === "STUVE" ? user.id : null, guestUser: user?.realm === "GUEST" ? user.id : null @@ -73,7 +75,11 @@ export default function EventListSlotView({slot, refetch}: { } { - slotIsFull ? <> + slotIsInPast ? <> + + Dieser Zeitslot ist bereits vorbei + + : slotIsFull ? <> Dieser Zeitslot ist bereits voll diff --git a/src/pages/events/s/EventView.tsx b/src/pages/events/s/EventView.tsx index 1d6bd79..e0d0c16 100644 --- a/src/pages/events/s/EventView.tsx +++ b/src/pages/events/s/EventView.tsx @@ -1,12 +1,11 @@ -import {useParams, useSearchParams} from "react-router-dom"; +import {Link, useParams, useSearchParams} from "react-router-dom"; import {usePB} from "@/lib/pocketbase.tsx"; import {useQuery} from "@tanstack/react-query"; import NotFound from "../../not-found/index.page.tsx"; -import {Accordion, Alert, 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 InnerHtml from "@/components/InnerHtml"; import {IconExternalLink, IconLogin, IconPencil, IconSectionSign} from "@tabler/icons-react"; -import {useSettings} from "@/lib/settings.ts"; import EventData from "@/pages/events/e/:eventId/EventComponents/EventData.tsx"; import EventListView from "@/pages/events/s/EventListView.tsx"; import {useEventRights} from "@/pages/events/util.ts"; @@ -24,7 +23,6 @@ export default function SharedEvent() { const [searchParams] = useSearchParams() const listIds = searchParams.get('lists')?.split(",") ?? [] - const settings = useSettings() const eventQuery = useQuery({ queryKey: ["event", eventId], @@ -61,6 +59,18 @@ export default function SharedEvent() { } +
+ {[ + {title: "Home", to: "/"}, + {title: "Events", to: "/events"}, + {title: event.name, to: `/events/${event.id}`}, + ].map(({title, to}) => ( + + {title} + + ))} +
+ {(canEditEvent || canEditEventList) &&
+ + + )} {event.additionalAgb && (