diff --git a/config.ts b/config.ts index 1d07d63..7048e55 100644 --- a/config.ts +++ b/config.ts @@ -9,5 +9,5 @@ export const PB_STORAGE_KEY = "stuve-it-login-record" // general export const APP_NAME = "StuVe IT" -export const APP_VERSION = "0.8.9 (beta)" +export const APP_VERSION = "0.8.11 (beta)" export const APP_URL = "https://it.stuve.uni-ulm.de" \ No newline at end of file diff --git a/src/components/users/modals/EmailTokenVerification.tsx b/src/components/users/modals/EmailTokenVerification.tsx index 3c5c6a1..8cae1ed 100644 --- a/src/components/users/modals/EmailTokenVerification.tsx +++ b/src/components/users/modals/EmailTokenVerification.tsx @@ -33,8 +33,7 @@ export default function EmailTokenVerification() { const verifyTokenMutation = useMutation({ mutationFn: async (token: string) => { - const res = await pb.collection("users").confirmVerification(token) - console.log({res}) + await pb.collection("users").confirmVerification(token) }, onSuccess: () => { showSuccessNotification("E-Mail erfolgreich verifiziert") @@ -51,7 +50,7 @@ export default function EmailTokenVerification() { useEffect(() => { const token = searchParams.get(EMAIL_TOKEN_KEY) - if (token !== null) { + if (token !== null && !user && !verifyTokenMutation.isPending) { verifyTokenMutation.mutate(token) } // eslint-disable-next-line react-hooks/exhaustive-deps diff --git a/src/components/users/modals/RegisterModal.tsx b/src/components/users/modals/RegisterModal.tsx index b559a92..8f4b544 100644 --- a/src/components/users/modals/RegisterModal.tsx +++ b/src/components/users/modals/RegisterModal.tsx @@ -8,6 +8,7 @@ import {useMutation} from "@tanstack/react-query"; import {showSuccessNotification} from "@/components/util.tsx"; import {Link} from "react-router-dom"; + export default function RegisterModal() { const {value, handler} = useRegister() @@ -31,7 +32,8 @@ export default function RegisterModal() { passwordConfirm: (val, values) => val !== values.password ? "Die Passwörter stimmen nicht überein" : null, terms: (val) => !val ? "Du musst die AGB akzeptieren" : null, privacy: (val) => !val ? "Du musst die Datenschutzerklärung akzeptieren" : null - } + }, + }) const registerMutation = useMutation({ @@ -79,10 +81,28 @@ export default function RegisterModal() { 0} className={"stack"}> + + + + + + } required {...formValues.getInputProps("username")} diff --git a/src/lib/pocketbase.tsx b/src/lib/pocketbase.tsx index 6987643..a4dc475 100644 --- a/src/lib/pocketbase.tsx +++ b/src/lib/pocketbase.tsx @@ -94,6 +94,7 @@ const PocketData = () => { pb.authStore.save(token, record) return record } catch (e) { + console.error("refresh token error", e) pb.authStore.clear() return null } @@ -111,12 +112,14 @@ const PocketData = () => { const record = await pb.collection(PB_USER_COLLECTION).getOne(pb.authStore.model?.id ?? "", { expand: "memberOf" }) - if (record.verified) { + if (record.verified === true) { pb.authStore.save(pb.authStore.token, record) return record - } else { + } else if (record.verified === false) { pb.authStore.clear() return null + } else { + return null } } catch (e) { if (e instanceof ClientResponseError && e.status === 401) { @@ -179,7 +182,7 @@ const PocketData = () => { ldapLogin, guestLogin, logout, - user: user as UserModal | null, + user: pb.authStore.isValid ? user as UserModal | null : null, pb, refreshUser: refreshUserQuery.refetch, useSubscription, diff --git a/src/pages/events/entries/UserEntries.tsx b/src/pages/events/entries/UserEntries.tsx index 389d92f..17007aa 100644 --- a/src/pages/events/entries/UserEntries.tsx +++ b/src/pages/events/entries/UserEntries.tsx @@ -2,17 +2,17 @@ import PromptLoginModal from "@/components/users/modals/PromptLoginModal.tsx"; import {Link, useNavigate} from "react-router-dom"; import {useState} from "react"; import {useDebouncedValue, useToggle} from "@mantine/hooks"; -import {useQuery} from "@tanstack/react-query"; +import {useInfiniteQuery} from "@tanstack/react-query"; import {PocketBaseErrorAlert, usePB} from "@/lib/pocketbase.tsx"; import { ActionIcon, Anchor, Breadcrumbs, + Button, Center, Group, Loader, LoadingOverlay, - Pagination, Stack, Text, TextInput, @@ -21,7 +21,7 @@ import { Tooltip } from "@mantine/core"; import ShowHelp from "@/components/ShowHelp.tsx"; -import {IconConfetti, IconDatabaseOff, IconSortAscending, IconSortDescending} from "@tabler/icons-react"; +import {IconArrowDown, IconConfetti, IconDatabaseOff, IconSortAscending, IconSortDescending} from "@tabler/icons-react"; import UserEntryRow from "@/pages/events/entries/UserEntryRow.tsx"; export default function UserEntries() { @@ -30,23 +30,24 @@ export default function UserEntries() { const {pb, user} = 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, { + const entriesQuery = useInfiniteQuery({ + queryKey: ["userEntries", user?.id, debouncedSearch, sortDirection], + queryFn: async ({pageParam}) => ( + await pb.collection("eventListSlotEntriesWithUser").getList(pageParam, 50, { filter: `user='${user?.id}'${debouncedSearch ? `&&event.name~'${debouncedSearch}'` : ""}`, sort: sortDirection === "ASC" ? "-event.startDate" : "event.startDate", expand: "user" }) ), + getNextPageParam: (lastPage) => + lastPage.page >= lastPage.totalPages ? undefined : lastPage.page + 1, + initialPageParam: 1, enabled: !!user }) @@ -54,6 +55,9 @@ export default function UserEntries() { return navigate("/")}/> } + const entries = entriesQuery.data?.pages.flatMap(page => page.items) ?? [] + const totalItems = entriesQuery.data?.pages[0].totalItems ?? 0 + return <> {entriesQuery.isLoading && } @@ -104,13 +108,13 @@ export default function UserEntries() { - {entriesQuery.data?.totalItems ?? 0} {eventSearch ? "Ergebnisse" : "Anmeldungen"} + {totalItems ?? 0} {eventSearch ? "Ergebnisse" : "Anmeldungen"} - {entriesQuery.data?.totalItems === 0 && + {totalItems === 0 && @@ -119,14 +123,25 @@ export default function UserEntries() { } - {entriesQuery.data?.items.map(entry => ( + {entries.map(entry => ( entriesQuery.refetch()} key={entry.id}/> ))} -
- -
- + { + entriesQuery.hasNextPage && +
+ +
+ } } \ No newline at end of file diff --git a/src/pages/events/entries/UserEntryRow.tsx b/src/pages/events/entries/UserEntryRow.tsx index abe6b5f..f583874 100644 --- a/src/pages/events/entries/UserEntryRow.tsx +++ b/src/pages/events/entries/UserEntryRow.tsx @@ -18,6 +18,7 @@ import {RenderDateRange} from "@/pages/events/e/:eventId/EventLists/EventListCom import { EntryQuestionAndStatusData } from "@/pages/events/e/:eventId/EventLists/EventListComponents/EntryQuestionAndStatusData.tsx"; +import {Link} from "react-router-dom"; export default function UserEntryRow({entry, refetch}: { @@ -80,7 +81,9 @@ export default function UserEntryRow({entry, refetch}: { }> - {entry.listName} + + {entry.listName} + diff --git a/src/pages/events/s/EventListSlotView.tsx b/src/pages/events/s/EventListSlotView.tsx index 048e670..98eed53 100644 --- a/src/pages/events/s/EventListSlotView.tsx +++ b/src/pages/events/s/EventListSlotView.tsx @@ -1,19 +1,20 @@ import {EventListModel, EventListSlotsWithEntriesCountModel} from "@/models/EventTypes.ts"; import classes from "./EventListSlotView.module.css"; import {useDisclosure} from "@mantine/hooks"; -import {Alert, Collapse, ThemeIcon, Tooltip, UnstyledButton} from "@mantine/core"; -import {IconChevronDown, IconChevronRight, IconInfoCircle} from "@tabler/icons-react"; +import {Alert, Button, Collapse, Group, List, ThemeIcon, Tooltip, UnstyledButton} from "@mantine/core"; +import {IconChevronDown, IconChevronRight, IconEye, IconInfoCircle} from "@tabler/icons-react"; import {PocketBaseErrorAlert, usePB} from "@/lib/pocketbase.tsx"; -import {useMutation} from "@tanstack/react-query"; +import {useMutation, useQuery} from "@tanstack/react-query"; import {FieldEntries} from "@/components/formUtil/FromInput/types.ts"; import {showSuccessNotification} from "@/components/util.tsx"; import InnerHtml from "@/components/InnerHtml"; import FormInput from "@/components/formUtil/FromInput"; -import {useNavigate} from "react-router-dom"; +import {Link, useNavigate} from "react-router-dom"; import {getListSchemas} from "@/pages/events/util.ts"; import {RenderDateRange} from "@/pages/events/e/:eventId/EventLists/EventListComponents/RenderDateRange.tsx"; import SlotProgress from "@/pages/events/e/:eventId/EventLists/EventListComponents/SlotProgress.tsx"; import EventLoginWarning from "@/pages/events/s/EventLoginWarning.tsx"; +import {pprintDateRange} from "@/lib/datetime.ts"; export default function EventListSlotView({slot, list, refetch}: { list: EventListModel, @@ -32,6 +33,24 @@ export default function EventListSlotView({slot, list, refetch}: { const navigate = useNavigate() + const slotOccupiedByUserQuery = useQuery({ + queryKey: ["eventListSlotEntries", {eventListsSlot: slot.id, user: user?.id}], + queryFn: async () => { + return await pb.collection("eventListSlotEntriesWithUser").getList(1, 50, { + filter: `event='${list.event}'&& + user='${user?.id}'&& + ( + slotStartDate>='${slot.startDate}'&&slotStartDate<='${slot.endDate}'|| + slotEndDate>='${slot.startDate}'&&slotEndDate<='${slot.endDate}'|| + slotStartDate<='${slot.startDate}'&&slotEndDate>='${slot.endDate}' + ) + `, + requestKey: `slotOccupiedByUserQuery-${slot.id}-${user?.id}` + }) + }, + enabled: !!user && !list.allowOverlappingEntries + }) + const createEntryMutation = useMutation({ mutationFn: async (data: FieldEntries) => { await pb.collection("eventListSlotEntries").create({ @@ -57,11 +76,9 @@ export default function EventListSlotView({slot, list, refetch}: {
- -
@@ -77,7 +94,35 @@ export default function EventListSlotView({slot, list, refetch}: { { !user ? : - list.onlyStuVeAccounts && user?.REALM !== "LDAP" ? <> + (slotOccupiedByUserQuery.data?.totalItems && !list.allowOverlappingEntries) ? <> + +
+ Du hast dich bei diesem Event bereits in einen überschneidenden Zeitslot eingetragen + und + diese Liste erlaubt keine überschneidenden Einträge. + + {slotOccupiedByUserQuery.data.items.map((entry) => ( + + {entry.listName} + {", Zeitslot "} + {pprintDateRange(entry.slotStartDate, entry.slotEndDate)} + + ))} + + + + +
+
+ : list.onlyStuVeAccounts && user?.REALM !== "LDAP" ? <> Für diese Liste sind nur StuVe-Accounts zugelassen @@ -89,8 +134,7 @@ export default function EventListSlotView({slot, list, refetch}: { Dieser Zeitslot ist bereits voll - - : + : } - { - !list.allowOverlappingEntries && - - Wenn du bei diesem Event bereits für einen anderen Zeitslot angemeldet bist, - kannst du in dieser Liste nur einen Zeitslot wählen, - der nicht mit deinen anderen Anmeldungen kollidiert. - - } - { list.onlyStuVeAccounts && @@ -78,7 +69,6 @@ export default function EventListView({event, listId}: { event: EventModel, list } - { (list.open && slots.length !== 0) && <> {slots.map(slot => ( diff --git a/src/pages/events/s/EventView.tsx b/src/pages/events/s/EventView.tsx index e359eeb..8428ba2 100644 --- a/src/pages/events/s/EventView.tsx +++ b/src/pages/events/s/EventView.tsx @@ -14,21 +14,27 @@ import EventLoginWarning from "@/pages/events/s/EventLoginWarning.tsx"; export default function SharedEvent() { - const {pb} = usePB() - + const {pb, user} = usePB() const {eventId} = useParams() as { eventId: string } const [searchParams] = useSearchParams() const listIds = searchParams.get('lists')?.split(",") ?? [] - const eventQuery = useQuery({ queryKey: ["event", eventId], queryFn: async () => (await pb.collection("events").getOne(eventId)), enabled: !!eventId }) + const userEntriesQuery = useQuery({ + queryKey: ["userEntries", user?.id ?? "anonym"], + queryFn: async () => (await pb.collection("eventListSlotEntriesWithUser").getList(1, 0, { + filter: `user='${user?.id ?? "anonym"}'&&event='${eventId}'`, + })), + enabled: !!user + }) + const {isPrivilegedUser, canEditEvent} = useEventRights(eventQuery.data) if (eventQuery.isLoading) { @@ -68,6 +74,34 @@ export default function SharedEvent() { + { + (userEntriesQuery.data?.totalItems ?? 0) > 0 && <> +
+ + Du hast dich bereits für dieses Event angemeldet. Du kannst deine Anmeldungen auf der + einsehen. + +
+ + } + {eventIsArchived &&
}> Dieses Event ist archiviert und wird nicht mehr verwaltet