feat(eventSearch): export to csv is now available
Build and Push Docker image / build-and-push (push) Successful in 2m5s
Details
Build and Push Docker image / build-and-push (push) Successful in 2m5s
Details
This commit is contained in:
parent
4f401f4eda
commit
54057be1f6
|
@ -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.8.5 (beta)"
|
export const APP_VERSION = "0.8.9 (beta)"
|
||||||
export const APP_URL = "https://it.stuve.uni-ulm.de"
|
export const APP_URL = "https://it.stuve.uni-ulm.de"
|
|
@ -0,0 +1,232 @@
|
||||||
|
import {EventListModel, EventListSlotEntriesWithUserModel, EventModel} from "@/models/EventTypes.ts";
|
||||||
|
import {ListResult} from "pocketbase";
|
||||||
|
import {InfiniteData, UseInfiniteQueryResult} from "@tanstack/react-query";
|
||||||
|
import {Button, Checkbox, Fieldset, Group, Modal, Progress, Select, Text} from "@mantine/core";
|
||||||
|
import ShowHelp from "@/components/ShowHelp.tsx";
|
||||||
|
import {useForm} from "@mantine/form";
|
||||||
|
import {useEffect} from "react";
|
||||||
|
import {FormSchema} from "@/components/formUtil/formBuilder/types.ts";
|
||||||
|
import {IconDownload} from "@tabler/icons-react";
|
||||||
|
|
||||||
|
const CHARSETS = [
|
||||||
|
"utf-8", "ISO-8859-1", "windows-1252", "utf-16be", "utf-16le", "us-ascii"
|
||||||
|
] as const
|
||||||
|
|
||||||
|
type FormValues = {
|
||||||
|
questionSchemaFields: string[]
|
||||||
|
statusSchemaFields: string[],
|
||||||
|
separator: "," | ";" | "\t",
|
||||||
|
charset: typeof CHARSETS[number]
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function DownloadDataModal({opened, onClose, lists, event, query}: {
|
||||||
|
opened: boolean
|
||||||
|
onClose: () => void
|
||||||
|
event: EventModel
|
||||||
|
lists: EventListModel[]
|
||||||
|
query: UseInfiniteQueryResult<InfiniteData<ListResult<EventListSlotEntriesWithUserModel>, unknown>, Error>
|
||||||
|
}) {
|
||||||
|
|
||||||
|
const formValues = useForm<FormValues>({
|
||||||
|
initialValues: {
|
||||||
|
questionSchemaFields: [],
|
||||||
|
statusSchemaFields: [],
|
||||||
|
separator: ",",
|
||||||
|
charset: "utf-8"
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Fetch all pages
|
||||||
|
useEffect(() => {
|
||||||
|
if (opened && query.hasNextPage && !query.isFetchingNextPage) {
|
||||||
|
query.fetchNextPage()
|
||||||
|
}
|
||||||
|
}, [opened, query])
|
||||||
|
|
||||||
|
const RenderFieldCheckboxes = ({schema, formKey}: {
|
||||||
|
schema: FormSchema | null,
|
||||||
|
formKey: 'questionSchemaFields' | 'statusSchemaFields'
|
||||||
|
}) => (
|
||||||
|
schema?.fields.map(field => (
|
||||||
|
<Checkbox
|
||||||
|
key={field.id}
|
||||||
|
label={field.label}
|
||||||
|
checked={formValues.values[formKey]?.includes(field.id)}
|
||||||
|
onChange={() => {
|
||||||
|
formValues.setFieldValue(formKey, formValues.values[formKey]?.includes(field.id) ?
|
||||||
|
formValues.values[formKey]?.filter(f => f !== field.id) :
|
||||||
|
[...formValues.values[formKey], field.id])
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
)
|
||||||
|
|
||||||
|
const totalPages = query.data?.pages?.[0].totalPages || 0
|
||||||
|
const currentPage = query.data?.pages?.length || 0
|
||||||
|
|
||||||
|
const progress = currentPage / totalPages * 100
|
||||||
|
const dataIsFetching = query.isFetching || query.isPending
|
||||||
|
|
||||||
|
const createCSV = () => {
|
||||||
|
// all loaded entries
|
||||||
|
const entries = query.data?.pages.flatMap(p => p.items) ?? []
|
||||||
|
|
||||||
|
const selectedFields = {
|
||||||
|
questionSchemaFields: formValues.values.questionSchemaFields,
|
||||||
|
statusSchemaFields: formValues.values.statusSchemaFields
|
||||||
|
}
|
||||||
|
|
||||||
|
// which question and status fields to include in the CSV
|
||||||
|
const questionSchema = {
|
||||||
|
fields: [
|
||||||
|
...event.defaultEntryQuestionSchema?.fields.filter(f => selectedFields.questionSchemaFields.includes(f.id)) ?? [],
|
||||||
|
...lists.flatMap(l => l.entryQuestionSchema?.fields.filter(f => selectedFields.questionSchemaFields.includes(f.id)) ?? [])
|
||||||
|
]
|
||||||
|
}
|
||||||
|
const statusSchema = {
|
||||||
|
fields: [
|
||||||
|
...event.defaultEntryStatusSchema?.fields.filter(f => selectedFields.statusSchemaFields.includes(f.id)) ?? [],
|
||||||
|
...lists.flatMap(l => l.entryStatusSchema?.fields.filter(f => selectedFields.statusSchemaFields.includes(f.id)) ?? [])
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Escapes the csv separator if it is present in the string
|
||||||
|
*
|
||||||
|
* trims the string in any case
|
||||||
|
* @param str the string to escape
|
||||||
|
*/
|
||||||
|
const escapeSeparator = (str: string) => {
|
||||||
|
if (str.includes(formValues.values.separator)) {
|
||||||
|
return `"${str.trim()}"`
|
||||||
|
}
|
||||||
|
return str.trim()
|
||||||
|
}
|
||||||
|
|
||||||
|
// assemble the header (first question fields, then status fields)
|
||||||
|
const header = [
|
||||||
|
"Person", "Person ID", "Anmeldezeitpunkt", "Anmelde-ID", "Anmelde-Liste",
|
||||||
|
...questionSchema.fields.map(f => f.label).map(escapeSeparator),
|
||||||
|
...statusSchema.fields.map(f => f.label).map(escapeSeparator)
|
||||||
|
]
|
||||||
|
|
||||||
|
// assemble the data
|
||||||
|
const data = entries.map(e => {
|
||||||
|
const questionData = e.entryQuestionData || {}
|
||||||
|
const statusData = e.entryStatusData || {}
|
||||||
|
return [
|
||||||
|
e.expand?.user.username ?? "N/A", e.user, e.created, e.id, e.listName,
|
||||||
|
...questionSchema.fields.map(f => questionData[f.id]?.value?.toString() ?? "N/A").map(escapeSeparator),
|
||||||
|
...statusSchema.fields.map(f => statusData[f.id]?.value?.toString() ?? "N/A").map(escapeSeparator)
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
// assemble CSV
|
||||||
|
const csv = [
|
||||||
|
header.join(formValues.values.separator),
|
||||||
|
...data.map(row => row.join(formValues.values.separator))
|
||||||
|
].join("\r\n")
|
||||||
|
|
||||||
|
const csvName = `${event.name.replace(/[^a-z0-9]/gi, '_').toLowerCase()}_export.csv`
|
||||||
|
|
||||||
|
// download
|
||||||
|
const blob = new Blob([csv], {type: "text/csv;charset=" + formValues.values.charset})
|
||||||
|
const url = URL.createObjectURL(blob)
|
||||||
|
const a = document.createElement("a")
|
||||||
|
a.href = url
|
||||||
|
a.download = csvName
|
||||||
|
a.style.display = "hidden"
|
||||||
|
a.click()
|
||||||
|
URL.revokeObjectURL(url)
|
||||||
|
onClose()
|
||||||
|
}
|
||||||
|
|
||||||
|
return <Modal
|
||||||
|
opened={opened}
|
||||||
|
onClose={onClose}
|
||||||
|
title="Daten herunterladen"
|
||||||
|
size="xl"
|
||||||
|
>
|
||||||
|
<div className={"stack"}>
|
||||||
|
<ShowHelp>
|
||||||
|
In der CSV sind automatisch die Felder <em>Person</em>, <em>Person
|
||||||
|
ID</em>, <em>Anmeldezeitpunkt</em>, <em>Anmelde-ID</em> und <em>Anmelde-Liste</em> enthalten. Diese
|
||||||
|
Felder können nicht abgewählt werden.
|
||||||
|
<br/>
|
||||||
|
Um Listen spezifische Daten herunterzuladen, wähle im vorherigen Schritt eine oder mehrere Listen aus.
|
||||||
|
</ShowHelp>
|
||||||
|
|
||||||
|
{dataIsFetching && <>
|
||||||
|
<Text c={"dimmed"} size={"xs"}>
|
||||||
|
Daten werden gesammelt ...
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<Progress.Root size="xl">
|
||||||
|
<Progress.Section value={progress} animated>
|
||||||
|
<Progress.Label>{progress.toFixed(0)}%</Progress.Label>
|
||||||
|
</Progress.Section>
|
||||||
|
</Progress.Root>
|
||||||
|
</>}
|
||||||
|
|
||||||
|
<Fieldset legend={event.name}>
|
||||||
|
<div className={"stack"}>
|
||||||
|
<Text c={"dimmed"} size={"xs"}>
|
||||||
|
Formularfelder
|
||||||
|
</Text>
|
||||||
|
<RenderFieldCheckboxes formKey={"questionSchemaFields"} schema={event.defaultEntryQuestionSchema}/>
|
||||||
|
<Text c={"dimmed"} size={"xs"}>
|
||||||
|
Statusfelder
|
||||||
|
</Text>
|
||||||
|
<RenderFieldCheckboxes formKey={"statusSchemaFields"} schema={event.defaultEntryStatusSchema}/>
|
||||||
|
</div>
|
||||||
|
</Fieldset>
|
||||||
|
|
||||||
|
{
|
||||||
|
lists.map(list => <Fieldset key={list.id} legend={list.name}>
|
||||||
|
<div className={"stack"}>
|
||||||
|
<Text c={"dimmed"} size={"xs"}>
|
||||||
|
Formularfelder
|
||||||
|
</Text>
|
||||||
|
<RenderFieldCheckboxes formKey={"questionSchemaFields"} schema={list.entryQuestionSchema}/>
|
||||||
|
<Text c={"dimmed"} size={"xs"}>
|
||||||
|
Statusfelder
|
||||||
|
</Text>
|
||||||
|
<RenderFieldCheckboxes formKey={"statusSchemaFields"} schema={list.entryStatusSchema}/>
|
||||||
|
</div>
|
||||||
|
</Fieldset>)
|
||||||
|
}
|
||||||
|
|
||||||
|
<Select
|
||||||
|
label={"CSV-Trennzeichen"}
|
||||||
|
placeholder={"Wähle ein Trennzeichen"}
|
||||||
|
data={[
|
||||||
|
{value: ",", label: "Komma"},
|
||||||
|
{value: ";", label: "Semikolon"},
|
||||||
|
{value: "\t", label: "Tabulator"}
|
||||||
|
]}
|
||||||
|
{...formValues.getInputProps("separator")}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Select
|
||||||
|
label={"Zeichensatz"}
|
||||||
|
placeholder={"Wähle einen Zeichensatz"}
|
||||||
|
data={CHARSETS.map(c => ({value: c, label: c.toUpperCase()}))}
|
||||||
|
{...formValues.getInputProps("charset")}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Group>
|
||||||
|
<Button onClick={onClose} variant={"light"} color={"orange"}>
|
||||||
|
Abbrechen
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={createCSV}
|
||||||
|
disabled={dataIsFetching}
|
||||||
|
loading={dataIsFetching}
|
||||||
|
leftSection={<IconDownload/>}
|
||||||
|
>
|
||||||
|
CSV herunterladen
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
}
|
|
@ -32,12 +32,17 @@ import {FieldEntriesFilter} from "@/components/formUtil/FromInput/types.ts";
|
||||||
import {assembleFilter} from "@/components/formUtil/FormFilter/util.ts";
|
import {assembleFilter} from "@/components/formUtil/FormFilter/util.ts";
|
||||||
import {DateTimePicker} from "@mantine/dates";
|
import {DateTimePicker} from "@mantine/dates";
|
||||||
import EventEntries from "@/pages/events/e/:eventId/EventLists/Search/EventEntries.tsx";
|
import EventEntries from "@/pages/events/e/:eventId/EventLists/Search/EventEntries.tsx";
|
||||||
|
import {useEventRights} from "@/pages/events/util.ts";
|
||||||
|
import DownloadDataModal from "@/pages/events/e/:eventId/EventLists/Search/DownloadDataModal.tsx";
|
||||||
|
|
||||||
export default function ListSearch({event}: { event: EventModel }) {
|
export default function ListSearch({event}: { event: EventModel }) {
|
||||||
|
|
||||||
const {pb} = usePB()
|
const {pb} = usePB()
|
||||||
|
|
||||||
|
const {canEditEvent} = useEventRights(event)
|
||||||
|
|
||||||
const [showFilter, showFilterHandler] = useDisclosure(true)
|
const [showFilter, showFilterHandler] = useDisclosure(true)
|
||||||
|
const [showDownloadModal, showDownloadModalHandler] = useDisclosure(false)
|
||||||
|
|
||||||
const formValues = useForm({
|
const formValues = useForm({
|
||||||
initialValues: {
|
initialValues: {
|
||||||
|
@ -197,7 +202,12 @@ export default function ListSearch({event}: { event: EventModel }) {
|
||||||
{entriesCount} Personen benachrichtigen
|
{entriesCount} Personen benachrichtigen
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Button size={"xs"} leftSection={<IconCsv size={16}/>} disabled>
|
<Button
|
||||||
|
size={"xs"}
|
||||||
|
disabled={!canEditEvent}
|
||||||
|
leftSection={<IconCsv size={16}/>}
|
||||||
|
onClick={showDownloadModalHandler.toggle}
|
||||||
|
>
|
||||||
Daten exportieren
|
Daten exportieren
|
||||||
</Button>
|
</Button>
|
||||||
</Group>
|
</Group>
|
||||||
|
@ -220,6 +230,14 @@ export default function ListSearch({event}: { event: EventModel }) {
|
||||||
|
|
||||||
<EventEntries event={event} entries={entries} refetch={() => query.refetch()}/>
|
<EventEntries event={event} entries={entries} refetch={() => query.refetch()}/>
|
||||||
|
|
||||||
|
<DownloadDataModal
|
||||||
|
event={event}
|
||||||
|
opened={showDownloadModal}
|
||||||
|
onClose={showDownloadModalHandler.toggle}
|
||||||
|
query={query}
|
||||||
|
lists={formValues.values.selectedLists}
|
||||||
|
/>
|
||||||
|
|
||||||
{query.hasNextPage && (
|
{query.hasNextPage && (
|
||||||
<Center p={"xs"}>
|
<Center p={"xs"}>
|
||||||
<Button
|
<Button
|
||||||
|
|
|
@ -13,6 +13,7 @@ import {useNavigate} from "react-router-dom";
|
||||||
import {getListSchemas} from "@/pages/events/util.ts";
|
import {getListSchemas} from "@/pages/events/util.ts";
|
||||||
import {RenderDateRange} from "@/pages/events/e/:eventId/EventLists/EventListComponents/RenderDateRange.tsx";
|
import {RenderDateRange} from "@/pages/events/e/:eventId/EventLists/EventListComponents/RenderDateRange.tsx";
|
||||||
import SlotProgress from "@/pages/events/e/:eventId/EventLists/EventListComponents/SlotProgress.tsx";
|
import SlotProgress from "@/pages/events/e/:eventId/EventLists/EventListComponents/SlotProgress.tsx";
|
||||||
|
import EventLoginWarning from "@/pages/events/s/EventLoginWarning.tsx";
|
||||||
|
|
||||||
export default function EventListSlotView({slot, list, refetch}: {
|
export default function EventListSlotView({slot, list, refetch}: {
|
||||||
list: EventListModel,
|
list: EventListModel,
|
||||||
|
@ -75,26 +76,26 @@ export default function EventListSlotView({slot, list, refetch}: {
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
|
!user ? <EventLoginWarning/> :
|
||||||
list.onlyStuVeAccounts && user?.REALM !== "LDAP" ? <>
|
list.onlyStuVeAccounts && user?.REALM !== "LDAP" ? <>
|
||||||
<Alert color={"red"}>
|
<Alert color={"red"}>
|
||||||
Für diese Liste sind nur StuVe-Accounts zugelassen
|
Für diese Liste sind nur StuVe-Accounts zugelassen
|
||||||
</Alert>
|
|
||||||
</> : slotIsInPast ? <>
|
|
||||||
<Alert color={"red"}>
|
|
||||||
Dieser Zeitslot ist bereits vorbei
|
|
||||||
</Alert>
|
|
||||||
</> : slotIsFull ? <>
|
|
||||||
<Alert color={"red"} title={"Zeitslot voll"}>
|
|
||||||
Dieser Zeitslot ist bereits voll
|
|
||||||
</Alert>
|
</Alert>
|
||||||
</>
|
</> : slotIsInPast ? <>
|
||||||
:
|
<Alert color={"red"}>
|
||||||
<FormInput
|
Dieser Zeitslot ist bereits vorbei
|
||||||
disabled={!user || slotIsFull}
|
</Alert>
|
||||||
schema={questionSchema}
|
</> : slotIsFull ? <>
|
||||||
onSubmit={createEntryMutation.mutateAsync}
|
<Alert color={"red"} title={"Zeitslot voll"}>
|
||||||
/>
|
Dieser Zeitslot ist bereits voll
|
||||||
|
</Alert>
|
||||||
|
</>
|
||||||
|
:
|
||||||
|
<FormInput
|
||||||
|
disabled={!user || slotIsFull}
|
||||||
|
schema={questionSchema}
|
||||||
|
onSubmit={createEntryMutation.mutateAsync}
|
||||||
|
/>
|
||||||
}
|
}
|
||||||
</Collapse>
|
</Collapse>
|
||||||
</>
|
</>
|
||||||
|
|
|
@ -0,0 +1,40 @@
|
||||||
|
import {usePB} from "@/lib/pocketbase.tsx";
|
||||||
|
import {ActionIcon, Alert, Text} from "@mantine/core";
|
||||||
|
import {IconLogin} from "@tabler/icons-react";
|
||||||
|
import {useLogin} from "@/components/users/modals/hooks.ts";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display a warning if the user is not logged in
|
||||||
|
* @constructor
|
||||||
|
*/
|
||||||
|
export default function EventLoginWarning() {
|
||||||
|
const {user} = usePB()
|
||||||
|
const {handler: loginHandler} = useLogin()
|
||||||
|
|
||||||
|
return <>
|
||||||
|
{!user && <div className={"section-transparent"}>
|
||||||
|
<Alert icon={
|
||||||
|
<ActionIcon
|
||||||
|
color={"green"}
|
||||||
|
variant={"transparent"}
|
||||||
|
onClick={loginHandler.open}
|
||||||
|
>
|
||||||
|
<IconLogin/>
|
||||||
|
</ActionIcon>
|
||||||
|
} color={"orange"}>
|
||||||
|
Um dich in eine Liste für dieses Event einzutragen, musst du dich
|
||||||
|
{" "}
|
||||||
|
<Text
|
||||||
|
onClick={loginHandler.open}
|
||||||
|
size={"sm"}
|
||||||
|
fw={700}
|
||||||
|
span
|
||||||
|
td={"underline"}
|
||||||
|
style={{cursor: "pointer"}}
|
||||||
|
>
|
||||||
|
anmelden oder einen Gastaccount erstellen
|
||||||
|
</Text>
|
||||||
|
</Alert>
|
||||||
|
</div>}
|
||||||
|
</>
|
||||||
|
}
|
|
@ -2,31 +2,19 @@ import {Link, useParams, useSearchParams} from "react-router-dom";
|
||||||
import {usePB} from "@/lib/pocketbase.tsx";
|
import {usePB} from "@/lib/pocketbase.tsx";
|
||||||
import {useQuery} from "@tanstack/react-query";
|
import {useQuery} from "@tanstack/react-query";
|
||||||
import NotFound from "../../not-found/index.page.tsx";
|
import NotFound from "../../not-found/index.page.tsx";
|
||||||
import {
|
import {Accordion, Alert, Anchor, Breadcrumbs, Button, Center, Group, Loader, Title} from "@mantine/core";
|
||||||
Accordion,
|
|
||||||
ActionIcon,
|
|
||||||
Alert,
|
|
||||||
Anchor,
|
|
||||||
Breadcrumbs,
|
|
||||||
Button,
|
|
||||||
Center,
|
|
||||||
Group,
|
|
||||||
Loader,
|
|
||||||
Text,
|
|
||||||
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 {IconArchive, IconExternalLink, IconEye, IconLogin, IconPencil, IconSectionSign} from "@tabler/icons-react";
|
import {IconArchive, IconExternalLink, IconEye, 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";
|
||||||
import {useLogin} from "@/components/users/modals/hooks.ts";
|
import EventLoginWarning from "@/pages/events/s/EventLoginWarning.tsx";
|
||||||
|
|
||||||
|
|
||||||
export default function SharedEvent() {
|
export default function SharedEvent() {
|
||||||
|
|
||||||
const {pb, user} = usePB()
|
const {pb} = usePB()
|
||||||
|
|
||||||
|
|
||||||
const {eventId} = useParams() as { eventId: string }
|
const {eventId} = useParams() as { eventId: string }
|
||||||
|
@ -43,9 +31,6 @@ export default function SharedEvent() {
|
||||||
|
|
||||||
const {isPrivilegedUser, canEditEvent} = useEventRights(eventQuery.data)
|
const {isPrivilegedUser, canEditEvent} = useEventRights(eventQuery.data)
|
||||||
|
|
||||||
const {handler: loginHandler} = useLogin()
|
|
||||||
|
|
||||||
|
|
||||||
if (eventQuery.isLoading) {
|
if (eventQuery.isLoading) {
|
||||||
return <Center h={"100%"}><Loader/></Center>
|
return <Center h={"100%"}><Loader/></Center>
|
||||||
}
|
}
|
||||||
|
@ -81,30 +66,7 @@ export default function SharedEvent() {
|
||||||
))}</Breadcrumbs>
|
))}</Breadcrumbs>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{!user && <div className={"section-transparent"}>
|
<EventLoginWarning/>
|
||||||
<Alert icon={
|
|
||||||
<ActionIcon
|
|
||||||
color={"green"}
|
|
||||||
variant={"transparent"}
|
|
||||||
onClick={loginHandler.open}
|
|
||||||
>
|
|
||||||
<IconLogin/>
|
|
||||||
</ActionIcon>
|
|
||||||
} color={"orange"}>
|
|
||||||
Um dich in eine Liste für dieses Event einzutragen, musst du dich
|
|
||||||
{" "}
|
|
||||||
<Text
|
|
||||||
onClick={loginHandler.open}
|
|
||||||
size={"sm"}
|
|
||||||
fw={700}
|
|
||||||
span
|
|
||||||
td={"underline"}
|
|
||||||
style={{cursor: "pointer"}}
|
|
||||||
>
|
|
||||||
anmelden oder einen Gastaccount erstellen
|
|
||||||
</Text>
|
|
||||||
</Alert>
|
|
||||||
</div>}
|
|
||||||
|
|
||||||
{eventIsArchived && <div className={"section-transparent"}>
|
{eventIsArchived && <div className={"section-transparent"}>
|
||||||
<Alert color={"orange"} icon={<IconArchive/>}>
|
<Alert color={"orange"} icon={<IconArchive/>}>
|
||||||
|
|
Loading…
Reference in New Issue