fix(app): added legal pages and made formUtil description HTML
Build and Push Docker image / build-and-push (push) Successful in 1m43s Details

This commit is contained in:
Valentin Kolb 2024-05-13 18:41:56 +02:00
parent 5d2eba55b0
commit 1afa7596d0
27 changed files with 324 additions and 215 deletions

View File

@ -4,8 +4,7 @@ import NotFound from "./pages/not-found/index.page.tsx";
import Layout from "@/components/layout"; 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 PrivacyPolicy from "./pages/privacy-policy.page.tsx"; import LegalPage from "@/pages/LegalPage.tsx";
import TermsAndConditions from "./pages/terms-and-conditions.page.tsx";
const router = createBrowserRouter([ const router = createBrowserRouter([
{ {
@ -17,16 +16,8 @@ const router = createBrowserRouter([
element: <HomePage/> element: <HomePage/>
}, },
{ {
path: "privacy-policy", path: "legal/:page",
element: <PrivacyPolicy/> element: <LegalPage/>
},
{
path: "imprint",
element: <PrivacyPolicy/>
},
{
path: "terms-and-conditions",
element: <TermsAndConditions/>
}, },
{ {
path: "events/*", path: "events/*",

View File

@ -36,7 +36,7 @@ export default function LoginModal() {
username: "", username: "",
password: "", password: "",
authMethod: "ldap" as "ldap" | "guest", authMethod: "ldap" as "ldap" | "guest",
privacy: false terms: false
} }
}) })
@ -94,23 +94,23 @@ export default function LoginModal() {
required required
label={ label={
<Text> <Text>
Ich akzeptiere die <Anchor Ich habe die <Anchor
component={Link} component={Link}
target={"_blank"} target={"_blank"}
to={"/privacy-policy"} to={"/legal/terms-and-conditions"}
> >
Datenschutzerklärung AGB der StuVe
</Anchor>. </Anchor> gelesen und nehme sie zur Kenntnis
</Text> </Text>
} }
{...formValues.getInputProps("privacy", {type: "checkbox"})} {...formValues.getInputProps("terms", {type: "checkbox"})}
/> />
<PocketBaseErrorAlert error={loginMutation.error}/> <PocketBaseErrorAlert error={loginMutation.error}/>
<Button <Button
loading={loginMutation.isPending} loading={loginMutation.isPending}
disabled={formValues.values.username === "" || formValues.values.password === "" || !formValues.values.privacy} disabled={formValues.values.username === "" || formValues.values.password === "" || !formValues.values.terms}
type={"submit"} type={"submit"}
> >
Einloggen Einloggen

View File

@ -1,11 +1,12 @@
import {useRegister} from "@/components/auth/modals/hooks.ts"; import {useRegister} from "@/components/auth/modals/hooks.ts";
import {isEmail, useForm} from "@mantine/form"; import {isEmail, useForm} from "@mantine/form";
import {Alert, Button, Collapse, Group, Modal, PasswordInput, 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";
import {IconAt, IconKey, IconUser, IconUserPlus, IconX} from "@tabler/icons-react"; import {IconAt, IconKey, IconUser, IconUserPlus, IconX} from "@tabler/icons-react";
import PasswordStrengthMeter, {getPasswordStrength} from "@/components/input/PasswordStrengthMeter.tsx"; import PasswordStrengthMeter, {getPasswordStrength} from "@/components/input/PasswordStrengthMeter.tsx";
import {useMutation} from "@tanstack/react-query"; import {useMutation} from "@tanstack/react-query";
import {showSuccessNotification} from "@/components/util.tsx"; import {showSuccessNotification} from "@/components/util.tsx";
import {Link} from "react-router-dom";
export default function RegisterModal() { export default function RegisterModal() {
const {value, handler} = useRegister() const {value, handler} = useRegister()
@ -20,12 +21,16 @@ export default function RegisterModal() {
passwordConfirm: "", passwordConfirm: "",
sn: "", sn: "",
givenName: "", givenName: "",
terms: false,
privacy: false
}, },
validate: { validate: {
email: isEmail("Ungültige E-Mail Adresse"), email: isEmail("Ungültige E-Mail Adresse"),
username: (val) => val.length < 3 ? "Der Anmeldename muss mindestens 3 Zeichen lang sein" : null, username: (val) => val.length < 3 ? "Der Anmeldename muss mindestens 3 Zeichen lang sein" : null,
password: (val) => getPasswordStrength(val) !== 100 ? "Das Passwort ist zu schwach" : null, password: (val) => getPasswordStrength(val) !== 100 ? "Das Passwort ist zu schwach" : null,
passwordConfirm: (val, values) => val !== values.password ? "Die Passwörter stimmen nicht überein" : null 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
} }
}) })
@ -106,6 +111,38 @@ export default function RegisterModal() {
</Collapse> </Collapse>
</Collapse> </Collapse>
<Checkbox
required
label={
<Text>
Ich habe die <Anchor
component={Link}
target={"_blank"}
to={"/legal/privacy-policy"}
>
Datenschutzerklärung
</Anchor> gelesen und akzeptiere sie.
</Text>
}
{...formValues.getInputProps("privacy", {type: "checkbox"})}
/>
<Checkbox
required
label={
<Text>
Ich habe die <Anchor
component={Link}
target={"_blank"}
to={"/legal/terms-and-conditions"}
>
AGB der StuVe
</Anchor> gelesen und nehme sie zur Kenntnis
</Text>
}
{...formValues.getInputProps("terms", {type: "checkbox"})}
/>
<Group> <Group>
<Button <Button
leftSection={<IconX/>} leftSection={<IconX/>}

View File

@ -0,0 +1,164 @@
import {DatePickerInput, DatePickerInputProps, DateTimePicker, DateTimePickerProps} from "@mantine/dates";
import {
Input,
MultiSelect,
MultiSelectProps,
NumberInput,
NumberInputProps,
Select,
SelectProps,
Spoiler,
Stack,
Text,
Textarea,
TextareaProps,
TextInput,
TextInputProps
} from "@mantine/core";
import {IconAt} from "@tabler/icons-react";
import {
CheckboxFieldSchema,
DateFieldSchema,
DateRangeFieldSchema,
EmailFieldSchema,
FormSchemaField,
NumberFieldSchema,
SelectFieldSchema,
TextFieldSchema
} from "../formBuilder/types.ts";
import {CheckboxCard, CheckboxCardProps} from "@/components/input/CheckboxCard";
import InnerHtml from "@/components/InnerHtml";
import {ReactNode} from "react";
export const FormTextField = ({field, ...props}: { field: TextFieldSchema } & TextInputProps) => {
if (field.multiline) {
console.error("for multiline text fields use FormTextareaField instead of FormTextField")
return null
}
return <Wrapper field={field}>
<TextInput
required={field.required}
placeholder={field.placeholder ?? "Text eingeben"}
{...props}
/>
</Wrapper>
}
export const FormTextareaField = ({field, ...props}: { field: TextFieldSchema } & TextareaProps) => {
if (!field.multiline) {
console.error("for single line text fields use FormTextField instead of FormTextareaField")
return null
}
return <Wrapper field={field}>
<Textarea
resize={"vertical"}
required={field.required}
placeholder={field.placeholder ?? "Text eingeben"}
{...props}
/>
</Wrapper>
}
export const EmailField = ({field, ...props}: { field: EmailFieldSchema } & TextInputProps) => {
return <Wrapper field={field}>
<TextInput
leftSection={<IconAt/>}
required={field.required}
placeholder={field.placeholder ?? "Email eingeben"}
type={"email"}
{...props}
/>
</Wrapper>
}
export const NumberField = ({field, ...props}: { field: NumberFieldSchema } & NumberInputProps) => {
return <Wrapper field={field}>
<NumberInput
required={field.required}
placeholder={field.placeholder ?? "Zahl eingeben"}
{...props}
/>
</Wrapper>
}
export const DateField = ({field, ...props}: { field: DateFieldSchema } & DateTimePickerProps) => {
return <Wrapper field={field}>
<DateTimePicker
clearable
required={field.required}
placeholder={field.placeholder ?? "Datum auswählen"}
{...props}
/>
</Wrapper>
}
export const DateRangeField = ({field, ...props}: { field: DateRangeFieldSchema } & DatePickerInputProps<"range">) => {
return <Wrapper field={field}>
{
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
<DatePickerInput
type="range"
clearable
required={field.required}
placeholder={field.placeholder ?? "Zeitraum auswählen"}
{...props}
/>
}
</Wrapper>
}
export const SelectField = ({field, ...props}: {
field: SelectFieldSchema
} & SelectProps & MultiSelectProps) => {
const Component = field.multiple ? MultiSelect : Select
return <>
<Wrapper field={field}>
<Component
clearable
required={field.required}
placeholder={field.placeholder ?? "Auswählen"}
{...props}
data={field.options}
/>
</Wrapper>
</>
}
const Wrapper = ({field, children}: {
field: FormSchemaField,
children: ReactNode
}) => {
return <Stack gap={5}>
<Input.Label required>{field.label}</Input.Label>
{field.description && <>
<Input.Description>
<Spoiler
maxHeight={40}
showLabel={<Text span size={"xs"}>Mehr anzeigen</Text>}
hideLabel={<Text span size={"xs"}>Weniger anzeigen</Text>}
>
<InnerHtml html={field.description}/>
</Spoiler>
</Input.Description>
</>}
{children}
</Stack>
}
export const CheckboxField = ({field, ...props}: { field: CheckboxFieldSchema } & CheckboxCardProps) => {
return <CheckboxCard
label={field.label}
description={field.description}
required={field.required}
{...props}
/>
}

View File

@ -1,19 +1,18 @@
import {FormSchema} from "../formBuilder/types.ts"; import {FormSchema} from "../formBuilder/types.ts";
import {Button, Group,} from "@mantine/core"; import {Button, Group,} from "@mantine/core";
import {useForm} from "@mantine/form"; import {useForm} from "@mantine/form";
import {FieldEntries} from "./types.ts";
import ShowDebug from "../../ShowDebug.tsx";
import {FieldEntries} from "@/components/formUtil/FromInput/types.ts";
import {createValidationFromSchema} from "@/components/formUtil/FromInput/validation.ts";
import { import {
CheckboxField, CheckboxField,
DateField, DateField, DateRangeField,
DateRangeField,
EmailField, EmailField,
FormTextareaField, FormTextareaField,
FormTextField, FormTextField,
NumberField, NumberField, SelectField
SelectField } from "@/components/formUtil/FromInput/formFieldComponents.tsx";
} from "./formFieldComponents.tsx";
import {createValidationFromSchema} from "./validation.ts";
import ShowDebug from "../../ShowDebug.tsx";
/** /**
* This function creates default values based on a data type (e.g. "" for text, false for checkbox) * This function creates default values based on a data type (e.g. "" for text, false for checkbox)

View File

@ -6,8 +6,8 @@ import {
NumberFieldSchema, NumberFieldSchema,
TextFieldSchema TextFieldSchema
} from "../formBuilder/types.ts" } from "../formBuilder/types.ts"
import {FieldEntry} from "./types.ts";
import {pprintDateTime} from "@/lib/datetime.ts"; import {pprintDateTime} from "@/lib/datetime.ts";
import {FieldEntry} from "@/components/formUtil/FromInput/types.ts";
const textValidator = (field: TextFieldSchema, value: string) => { const textValidator = (field: TextFieldSchema, value: string) => {

View File

@ -9,6 +9,7 @@ import {
Fieldset, Fieldset,
Group, Group,
NumberInput, NumberInput,
Spoiler,
Stack, Stack,
Text, Text,
TextInput, TextInput,
@ -33,6 +34,8 @@ import {useDisclosure} from "@mantine/hooks";
import classes from "./editField.module.css" import classes from "./editField.module.css"
import {Draggable} from "@hello-pangea/dnd"; import {Draggable} from "@hello-pangea/dnd";
import {humanReadableField} from "../util.ts"; import {humanReadableField} from "../util.ts";
import TextEditor from "@/components/input/Editor";
import InnerHtml from "@/components/InnerHtml";
type EditFieldProps = { type EditFieldProps = {
index: number; index: number;
@ -132,8 +135,8 @@ const SelectFieldSettings = ({field, formValues, index}: Omit<EditFieldProps, "f
placeholder={"Option"} placeholder={"Option"}
rightSection={ rightSection={
<ActionIcon <ActionIcon
disabled={field.meta?.required}
color={"red"} color={"red"}
variant={"transparent"} variant={"transparent"}
aria-label={"delete option"} aria-label={"delete option"}
@ -153,6 +156,7 @@ const SelectFieldSettings = ({field, formValues, index}: Omit<EditFieldProps, "f
<Group> <Group>
<Button <Button
disabled={field.meta?.required}
variant={"light"} size="xs" variant={"light"} size="xs"
leftSection={<IconPlus/>} leftSection={<IconPlus/>}
onClick={() => { onClick={() => {
@ -297,12 +301,24 @@ export default function EditField({field, formValues, index}: EditFieldProps) {
</Badge> </Badge>
</Tooltip> </Tooltip>
<Group> {
<TextInput field.meta?.required ? <>
label={"Beschreibung"} <Fieldset legend={"Beschreibung"}>
{...formValues.getInputProps(`fields.${index}.description`)} <Spoiler hideLabel={"Beschreibung ausblenden"}
showLabel={"Beschreibung anzeigen"}>
<InnerHtml html={field.description ?? ""}/>
</Spoiler>
</Fieldset>
</> : <TextEditor
placeholder={"Beschreibung ..."}
value={formValues.values.fields[index].description ?? ""}
onChange={(value) => {
formValues.setFieldValue(`fields.${index}.description`, value)
}}
/> />
}
<Group>
<TextInput <TextInput
label={"Platzhalter"} label={"Platzhalter"}
{...formValues.getInputProps(`fields.${index}.placeholder`)} {...formValues.getInputProps(`fields.${index}.placeholder`)}

View File

@ -6,7 +6,7 @@ import EditField from "./editField.tsx";
import {IconDatabaseOff, IconEye, IconSquarePlus2} from "@tabler/icons-react"; import {IconDatabaseOff, IconEye, IconSquarePlus2} from "@tabler/icons-react";
import FormTypeIcon from "../FormTypeIcon.tsx"; import FormTypeIcon from "../FormTypeIcon.tsx";
import {useDisclosure} from "@mantine/hooks"; import {useDisclosure} from "@mantine/hooks";
import FormInput from "../fromInput"; import FormInput from "../FromInput";
import {DragDropContext, Droppable} from "@hello-pangea/dnd"; import {DragDropContext, Droppable} from "@hello-pangea/dnd";
import {useEffect} from "react"; import {useEffect} from "react";

View File

@ -1,4 +1,4 @@
import {FieldEntry} from "../fromInput/types.ts"; import {FieldEntry} from "@/components/formUtil/FromInput/types.ts";
import {ReactNode} from "react"; import {ReactNode} from "react";
import {Anchor, Badge, Code, Group, List, Spoiler, ThemeIcon, Tooltip} from "@mantine/core"; import {Anchor, Badge, Code, Group, List, Spoiler, ThemeIcon, Tooltip} from "@mantine/core";
import { import {

View File

@ -1,4 +1,4 @@
import {FieldEntries, FieldEntry} from "../fromInput/types.ts" import {FieldEntries, FieldEntry} from "@/components/formUtil/FromInput/types.ts"
import {objectMap} from "@/lib/util.ts"; import {objectMap} from "@/lib/util.ts";
import {FormSchemaField} from "../formBuilder/types.ts"; import {FormSchemaField} from "../formBuilder/types.ts";

View File

@ -1,5 +1,5 @@
import {FieldDataType} from "../formBuilder/types.ts"; import {FieldDataType} from "../formBuilder/types.ts";
import {FieldEntryValue} from "../fromInput/types.ts"; import {FieldEntryValue} from "@/components/formUtil/FromInput/types.ts";
import dayjs from "dayjs"; import dayjs from "dayjs";
/** /**

View File

@ -1,118 +0,0 @@
import {DatePickerInput, DatePickerInputProps, DateTimePicker, DateTimePickerProps} from "@mantine/dates";
import {
MultiSelect,
MultiSelectProps,
NumberInput,
NumberInputProps,
Select,
SelectProps,
Textarea,
TextareaProps,
TextInput,
TextInputProps
} from "@mantine/core";
import {IconAt} from "@tabler/icons-react";
import {
CheckboxFieldSchema,
DateFieldSchema,
DateRangeFieldSchema,
EmailFieldSchema,
FormSchemaField,
NumberFieldSchema,
SelectFieldSchema,
TextFieldSchema
} from "../formBuilder/types.ts";
import {CheckboxCard, CheckboxCardProps} from "@/components/input/CheckboxCard";
/**
* This function unpacks a field into a set of props that can be used in the input components
* @param field
*/
const unpackField = (field: FormSchemaField) => {
return {
label: field.label || undefined,
placeholder: field.placeholder || undefined,
description: field.description || undefined,
required: field.required || false,
}
}
export const FormTextField = ({field, ...props}: { field: TextFieldSchema } & TextInputProps) => {
if (field.multiline) {
console.error("for multiline text fields use FormTextareaField instead of FormTextField")
return null
}
return <TextInput
{...unpackField(field)}
{...props}
/>
}
export const FormTextareaField = ({field, ...props}: { field: TextFieldSchema } & TextareaProps) => {
if (!field.multiline) {
console.error("for single line text fields use FormTextField instead of FormTextareaField")
return null
}
return <Textarea
resize={"vertical"}
{...unpackField(field)}
{...props}/>
}
export const EmailField = ({field, ...props}: { field: EmailFieldSchema } & TextInputProps) => {
return <TextInput
leftSection={<IconAt/>}
{...unpackField(field)}
type={"email"}
{...props}
/>
}
export const NumberField = ({field, ...props}: { field: NumberFieldSchema } & NumberInputProps) => {
return <NumberInput
{...unpackField(field)}
{...props}
/>
}
export const DateField = ({field, ...props}: { field: DateFieldSchema } & DateTimePickerProps) => {
return <DateTimePicker
clearable
{...unpackField(field)}
{...props}
/>
}
export const DateRangeField = ({field, ...props}: { field: DateRangeFieldSchema } & DatePickerInputProps<"range">) => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
return <DatePickerInput
type="range"
clearable
{...unpackField(field)}
{...props}
/>
}
export const SelectField = ({field, ...props}: { field: SelectFieldSchema } & SelectProps & MultiSelectProps) => {
const Component = field.multiple ? MultiSelect : Select
return <Component
clearable
{...unpackField(field)}
{...props}
data={field.options}
/>
}
export const CheckboxField = ({field, ...props}: { field: CheckboxFieldSchema } & CheckboxCardProps) => {
return <CheckboxCard
{...unpackField(field)}
{...props}
/>
/*
return <Checkbox
{...unpackField(field)}
{...props}
/>
*/
}

View File

@ -9,6 +9,11 @@
padding: var(--mantine-spacing-md); padding: var(--mantine-spacing-md);
/*noinspection CssInvalidFunction*/ /*noinspection CssInvalidFunction*/
background-color: light-dark(var(--mantine-color-white), var(--mantine-color-dark-8)); background-color: light-dark(var(--mantine-color-white), var(--mantine-color-dark-8));
&[aria-error="true"] {
border-color: var(--mantine-color-error);
}
} }
.textContainer { .textContainer {

View File

@ -1,12 +1,13 @@
import {Checkbox, CheckboxProps, Input, InputWrapperProps} from '@mantine/core'; import {Checkbox, CheckboxProps, Input, InputWrapperProps} from '@mantine/core';
import classes from './index.module.css'; import classes from './index.module.css';
import InnerHtml from "@/components/InnerHtml";
export type CheckboxCardProps = InputWrapperProps & CheckboxProps export type CheckboxCardProps = InputWrapperProps & CheckboxProps
export function CheckboxCard(props: CheckboxCardProps) { export function CheckboxCard(props: CheckboxCardProps) {
return ( return (
<div className={classes.container}> <div className={classes.container} aria-error={!!props.error}>
<Checkbox <Checkbox
checked={props.checked} checked={props.checked}
onChange={props.onChange} onChange={props.onChange}
@ -19,7 +20,8 @@ export function CheckboxCard(props: CheckboxCardProps) {
<div className={classes.textContainer}> <div className={classes.textContainer}>
<Input.Label required={props.required}>{props.label}</Input.Label> <Input.Label required={props.required}>{props.label}</Input.Label>
{props.description && <Input.Description>{props.description}</Input.Description>} {props.description &&
<Input.Description><InnerHtml html={props.description.toString() ?? ""}/></Input.Description>}
{props.error && <Input.Error>{props.error}</Input.Error>} {props.error && <Input.Error>{props.error}</Input.Error>}
</div> </div>
</div> </div>

View File

@ -2,7 +2,7 @@ import {BubbleMenu, Editor, 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';
import {RichTextEditor, RichTextEditorContent} from "@mantine/tiptap"; import {Link, RichTextEditor, RichTextEditorContent} from "@mantine/tiptap";
import classes from './index.module.css'; import classes from './index.module.css';
import {Box, Input, InputWrapperProps, Loader} from "@mantine/core"; import {Box, Input, InputWrapperProps, Loader} from "@mantine/core";
import {useEffect} from "react"; import {useEffect} from "react";
@ -14,7 +14,6 @@ const Bubble = ({editor}: { editor: Editor }) => (
<RichTextEditor.ControlsGroup> <RichTextEditor.ControlsGroup>
<RichTextEditor.Bold/> <RichTextEditor.Bold/>
<RichTextEditor.Italic/> <RichTextEditor.Italic/>
<RichTextEditor.Link/>
</RichTextEditor.ControlsGroup> </RichTextEditor.ControlsGroup>
</BubbleMenu> </BubbleMenu>
) )
@ -45,6 +44,11 @@ const Toolbar = ({fullToolbar}: { fullToolbar: boolean, editor: Editor }) => (
<RichTextEditor.BulletList/> <RichTextEditor.BulletList/>
<RichTextEditor.OrderedList/> <RichTextEditor.OrderedList/>
</RichTextEditor.ControlsGroup> </RichTextEditor.ControlsGroup>
<RichTextEditor.ControlsGroup>
<RichTextEditor.Link/>
<RichTextEditor.Unlink/>
</RichTextEditor.ControlsGroup>
</> </>
: :
<> <>
@ -54,6 +58,10 @@ const Toolbar = ({fullToolbar}: { fullToolbar: boolean, editor: Editor }) => (
<RichTextEditor.Underline/> <RichTextEditor.Underline/>
<RichTextEditor.Code/> <RichTextEditor.Code/>
</RichTextEditor.ControlsGroup> </RichTextEditor.ControlsGroup>
<RichTextEditor.ControlsGroup>
<RichTextEditor.Link/>
<RichTextEditor.Unlink/>
</RichTextEditor.ControlsGroup>
</> </>
} }
@ -79,6 +87,7 @@ export default function TextEditor({
maxHeight, maxHeight,
hideToolbar, hideToolbar,
noBorder, noBorder,
disabled,
...props ...props
}: { }: {
value: string; value: string;
@ -88,14 +97,17 @@ export default function TextEditor({
maxHeight?: number | string; maxHeight?: number | string;
hideToolbar?: boolean; hideToolbar?: boolean;
noBorder?: boolean; noBorder?: boolean;
disabled?: boolean;
} & Omit<InputWrapperProps, "onChange">) { } & Omit<InputWrapperProps, "onChange">) {
const editor = useEditor({ const editor = useEditor({
extensions: [ extensions: [
StarterKit, StarterKit,
Underline, Underline,
Placeholder.configure({placeholder}) Link,
Placeholder.configure({placeholder: placeholder})
], ],
editable: !disabled,
content: value, content: value,
onUpdate: ({editor}) => { onUpdate: ({editor}) => {
const cleanHtml = sanitizeHtml(editor.getHTML()) const cleanHtml = sanitizeHtml(editor.getHTML())

View File

@ -50,14 +50,12 @@ export default function Footer() {
<div className={classes.links}> <div className={classes.links}>
<h5 className={classes.hideMobile}>Rechtliches</h5> <h5 className={classes.hideMobile}>Rechtliches</h5>
<Link to={"/terms-and-conditions"}>AGB der StuVe</Link> <Link to={"/legal/terms-and-conditions"}>AGB der StuVe</Link>
<Link to={"/privacy-policy"}>Datenschutzerklärung</Link> <Link to={"/legal/privacy-policy"}>Datenschutzerklärung</Link>
<Link to={"/imprint"}>Impressum</Link>
<Link to={"/legal/imprint"}>Impressum</Link>
</div> </div>
</div> </div>
<Divider/> <Divider/>

View File

@ -9,6 +9,7 @@ type Setting = {
} }
type Settings = { type Settings = {
imprint: Setting
privacyPolicy: Setting privacyPolicy: Setting
agb: Setting agb: Setting
stexGroupId: Setting stexGroupId: Setting

View File

@ -1,6 +1,6 @@
import {GuestUserModel, LdapUserModel} from "./AuthTypes.ts"; import {GuestUserModel, LdapUserModel} from "./AuthTypes.ts";
import {RecordModel} from "pocketbase"; import {RecordModel} from "pocketbase";
import {FieldEntries} from "@/components/formUtil/fromInput/types.ts"; import {FieldEntries} from "@/components/formUtil/FromInput/types.ts";
import {FormSchema} from "@/components/formUtil/formBuilder/types.ts"; import {FormSchema} from "@/components/formUtil/formBuilder/types.ts";
export type EventModel = { export type EventModel = {

View File

@ -16,11 +16,20 @@ export type SettingsModel = {
public?: boolean public?: boolean
} & RecordModel } & RecordModel
export type LegalSettingsModal = {
name: string,
updated: string,
legalText: string
} & RecordModel
export interface TypedPocketBase extends PocketBase { export interface TypedPocketBase extends PocketBase {
collection(idOrName: string): RecordService<RecordModel> collection(idOrName: string): RecordService<RecordModel>
collection(idOrName: 'settings'): RecordService<SettingsModel> collection(idOrName: 'settings'): RecordService<SettingsModel>
collection(idOrName: 'legalSettings'): RecordService<LegalSettingsModal>
collection(idOrName: 'users'): RecordService<UserModal> collection(idOrName: 'users'): RecordService<UserModal>
collection(idOrName: 'guest_users'): RecordService<GuestUserModel> collection(idOrName: 'guest_users'): RecordService<GuestUserModel>

33
src/pages/LegalPage.tsx Normal file
View File

@ -0,0 +1,33 @@
import {LoadingOverlay} from "@mantine/core";
import InnerHtml from "@/components/InnerHtml";
import {pprintDate} from "@/lib/datetime.ts";
import {useParams} from "react-router-dom";
import {useQuery} from "@tanstack/react-query";
import {usePB} from "@/lib/pocketbase.tsx";
import NotFound from "@/pages/not-found/index.page.tsx";
export default function LegalPage() {
const {page} = useParams() as { page: string }
const {pb} = usePB()
const query = useQuery({
queryKey: ["legalSettings", page],
queryFn: async () => await pb.collection("legalSettings").getFirstListItem(`name='${page}'`),
enabled: !!page
})
if (query.isPending) return <LoadingOverlay visible={true}/>
if (query.isError || !query.data) return <NotFound/>
return <>
<div className={"section"}>
Zuletzt Bearbeitet: {pprintDate(query.data.updated)}
</div>
<div className={"section"}>
<InnerHtml html={query.data.legalText}/>
</div>
</>
}

View File

@ -34,7 +34,7 @@ export default function EventAGB({event}: { event: EventModel }) {
<Button <Button
mt={"sm"} mt={"sm"}
variant={"light"} variant={"light"}
component={Link} to={"/terms-and-conditions"} target={"_blank"} component={Link} to={"/legal/terms-and-conditions"} target={"_blank"}
> >
AGB der StuVe AGB der StuVe
</Button> </Button>

View File

@ -1,10 +1,10 @@
import {EventListSlotEntriesWithUserModel} from "@/models/EventTypes.ts"; import {EventListSlotEntriesWithUserModel} from "@/models/EventTypes.ts";
import {PocketBaseErrorAlert, usePB} from "@/lib/pocketbase.tsx"; import {PocketBaseErrorAlert, usePB} from "@/lib/pocketbase.tsx";
import {useMutation} from "@tanstack/react-query"; import {useMutation} from "@tanstack/react-query";
import {FieldEntries} from "@/components/formUtil/fromInput/types.ts"; 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";
export const UpdateEventListSlotEntryFormModal = ({opened, close, refetch, entry}: { export const UpdateEventListSlotEntryFormModal = ({opened, close, refetch, entry}: {
opened: boolean, opened: boolean,

View File

@ -1,12 +1,12 @@
import {EventListSlotEntriesWithUserModel} from "@/models/EventTypes.ts"; import {EventListSlotEntriesWithUserModel} from "@/models/EventTypes.ts";
import {PocketBaseErrorAlert, usePB} from "@/lib/pocketbase.tsx"; import {PocketBaseErrorAlert, usePB} from "@/lib/pocketbase.tsx";
import {useMutation} from "@tanstack/react-query"; import {useMutation} from "@tanstack/react-query";
import {FieldEntries} from "@/components/formUtil/fromInput/types.ts"; import {FieldEntries} from "@/components/formUtil/FromInput/types.ts";
import {showSuccessNotification} from "@/components/util.tsx"; import {showSuccessNotification} from "@/components/util.tsx";
import {Alert, Modal} from "@mantine/core"; import {Alert, Modal} from "@mantine/core";
import FormInput from "@/components/formUtil/fromInput";
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";
export const UpdateEventListSlotEntryStatusModal = ({opened, close, refetch, entry}: { export const UpdateEventListSlotEntryStatusModal = ({opened, close, refetch, entry}: {
opened: boolean, opened: boolean,

View File

@ -7,10 +7,10 @@ import {Alert, Collapse, ThemeIcon, Tooltip, UnstyledButton} from "@mantine/core
import {IconChevronDown, IconChevronRight, IconInfoCircle} from "@tabler/icons-react"; import {IconChevronDown, IconChevronRight, IconInfoCircle} from "@tabler/icons-react";
import {PocketBaseErrorAlert, usePB} from "@/lib/pocketbase.tsx"; import {PocketBaseErrorAlert, usePB} from "@/lib/pocketbase.tsx";
import {useMutation} from "@tanstack/react-query"; import {useMutation} from "@tanstack/react-query";
import {FieldEntries} from "@/components/formUtil/fromInput/types.ts"; import {FieldEntries} from "@/components/formUtil/FromInput/types.ts";
import {showSuccessNotification} from "@/components/util.tsx"; import {showSuccessNotification} from "@/components/util.tsx";
import InnerHtml from "@/components/InnerHtml"; import InnerHtml from "@/components/InnerHtml";
import FormInput from "@/components/formUtil/fromInput"; import FormInput from "@/components/formUtil/FromInput";
import {useUser} from "@/lib/user.ts"; import {useUser} from "@/lib/user.ts";
export default function EventListSlotView({slot, refetch}: { export default function EventListSlotView({slot, refetch}: {

View File

@ -1,20 +0,0 @@
import {useSettings} from "@/lib/settings.ts";
import {LoadingOverlay} from "@mantine/core";
import InnerHtml from "@/components/InnerHtml";
import {pprintDate} from "@/lib/datetime.ts";
export default function PrivacyPolicy() {
const settings = useSettings()
if (!settings) return <LoadingOverlay visible={true}/>
return <>
<div className={"section"}>
Zuletzt Bearbeitet: {pprintDate(settings.privacyPolicy.updated)}
</div>
<div className={"section"}>
<InnerHtml html={settings.privacyPolicy.value}/>
</div>
</>
}

View File

@ -1,20 +0,0 @@
import {useSettings} from "@/lib/settings.ts";
import {LoadingOverlay} from "@mantine/core";
import InnerHtml from "@/components/InnerHtml";
import {pprintDate} from "@/lib/datetime.ts";
export default function TermsAndConditions() {
const settings = useSettings()
if (!settings) return <LoadingOverlay visible={true}/>
return <>
<div className={"section"}>
Zuletzt Bearbeitet: {pprintDate(settings.agb.updated)}
</div>
<div className={"section"}>
<InnerHtml html={settings.agb.value}/>
</div>
</>
}