diff --git a/.env b/.env
index 48de936..05aaff5 100644
--- a/.env
+++ b/.env
@@ -1,5 +1,5 @@
PUBLIC_URL="http://localhost:8080"
-API_KEY="alongsecurestring"
+API_KEY=""
POSTGRES_PASSWORD="password123"
POSTGRES_USER="postgres"
diff --git a/frontend/app/error.tsx b/frontend/app/error.tsx
new file mode 100644
index 0000000..bab8132
--- /dev/null
+++ b/frontend/app/error.tsx
@@ -0,0 +1,17 @@
+"use client";
+
+import { buttonVariants } from "@/components/ui/button";
+import Image from "next/image";
+import Link from "next/link";
+
+export default function Error() {
+ return (
+
+
+
Something went wrong!
+
+ Go Back
+
+
+ );
+}
diff --git a/frontend/app/registration/page.tsx b/frontend/app/registration/page.tsx
new file mode 100644
index 0000000..90c63d1
--- /dev/null
+++ b/frontend/app/registration/page.tsx
@@ -0,0 +1,301 @@
+"use client";
+
+import { useRouter } from "next/navigation";
+import {
+ QuestionType,
+ RegistrationFormDocument,
+ RegistrationFormQuery,
+ RegistrationFormQueryVariables,
+} from "@/lib/gql/generated/graphql";
+import { client } from "@/lib/graphClient";
+import { Button } from "@/components/ui/button";
+import {
+ Card,
+ CardContent,
+ CardFooter,
+ CardHeader,
+ CardTitle,
+} from "@/components/ui/card";
+import { useEffect, useState } from "react";
+import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
+import { Slider } from "@/components/ui/slider";
+import { Label } from "@/components/ui/label";
+import { Checkbox } from "@/components/ui/checkbox";
+import { Progress } from "@/components/ui/progress";
+import { z } from "zod";
+import { useForm } from "react-hook-form";
+import { zodResolver } from "@hookform/resolvers/zod";
+import {
+ Form,
+ FormControl,
+ FormField,
+ FormItem,
+ FormMessage,
+} from "@/components/ui/form";
+
+type Props = {
+ searchParams: {
+ e: string;
+ };
+};
+
+const SingleChoiceFormSchema = (required: boolean) =>
+ z.object({
+ singleChoice: required
+ ? z.number({
+ required_error: "Bitte wähle eine Option",
+ })
+ : z.number().optional(),
+ });
+
+const MultipleChoiceFormSchema = (required: boolean) =>
+ z.object({
+ multipleChoice: required
+ ? z.array(z.number()).refine((value) => value.some((item) => item), {
+ message: "Bitte triff eine Auswahl",
+ })
+ : z.array(z.number()).optional(),
+ });
+
+const Home = ({ searchParams }: Props) => {
+ const [regForm, setForm] = useState(
+ null
+ );
+ const [progressValue, setProgressValue] = useState(0);
+ const [sliderValue, setSliderValue] = useState(0);
+ const [index, setIndex] = useState(0);
+ const [loading, setLoading] = useState(true);
+ const router = useRouter();
+
+ useEffect(() => {
+ const fetchData = async () => {
+ const eventID = searchParams.e;
+
+ const vars: RegistrationFormQueryVariables = {
+ eventID: parseInt(eventID),
+ };
+
+ await new Promise((resolve) => setTimeout(resolve, 250));
+
+ const data = await client.request(
+ RegistrationFormDocument,
+ vars
+ );
+
+ if (!data.forms.length) {
+ router.push("/");
+ return;
+ }
+
+ setForm(data.forms[0]);
+ setLoading(false);
+ };
+
+ fetchData();
+ }, [searchParams.e, router]);
+
+ useEffect(() => {
+ if (regForm) {
+ setProgressValue((100 / (regForm.questions.length - 1)) * index);
+ }
+ }, [index, regForm]);
+
+ const mcForm = useForm>>({
+ resolver: zodResolver(MultipleChoiceFormSchema(regForm?.questions[index].required!)),
+ defaultValues: {
+ multipleChoice: [],
+ },
+ });
+
+ const scForm = useForm>>({
+ resolver: zodResolver(SingleChoiceFormSchema(regForm?.questions[index].required!)),
+ defaultValues: {
+ singleChoice: undefined,
+ },
+ });
+
+ function handleQuit() {
+ router.push("/");
+ }
+
+ const FooterButtons = () => (
+
+
+
+
+ );
+
+ function onSubmit() {
+ if (regForm?.questions.length !== index + 1) {
+ setIndex((prevIndex) => prevIndex + 1);
+ }
+ }
+
+ function onScaleSubmit() {
+ onSubmit();
+ setSliderValue(0);
+ }
+
+ function onMCSubmit(data: z.infer>) {
+ onSubmit();
+ }
+
+ function onSCSubmit(data: z.infer>) {
+ onSubmit();
+ }
+
+ if (loading) {
+ return Loading...
;
+ }
+
+ return (
+
+
+
+
{regForm?.title}
+
+ {regForm?.description}
+
+
+
+
+
+ {regForm?.questions[index].title}
+
+ {regForm?.questions[index].type === QuestionType.MultipleChoice && (
+
+
+ )}
+
+ {regForm?.questions[index].type === QuestionType.SingleChoice && (
+
+
+ )}
+
+ {regForm?.questions[index].type === QuestionType.Scale && (
+
+ )}
+
+
+
+ );
+};
+
+export default Home;
diff --git a/frontend/components/ui/card.tsx b/frontend/components/ui/card.tsx
new file mode 100644
index 0000000..b77cd83
--- /dev/null
+++ b/frontend/components/ui/card.tsx
@@ -0,0 +1,79 @@
+import * as React from "react"
+
+import { cn } from "@/lib/utils/tailwindUtils"
+
+const Card = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+Card.displayName = "Card"
+
+const CardHeader = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+CardHeader.displayName = "CardHeader"
+
+const CardTitle = React.forwardRef<
+ HTMLParagraphElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+CardTitle.displayName = "CardTitle"
+
+const CardDescription = React.forwardRef<
+ HTMLParagraphElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+CardDescription.displayName = "CardDescription"
+
+const CardContent = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+CardContent.displayName = "CardContent"
+
+const CardFooter = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+CardFooter.displayName = "CardFooter"
+
+export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
diff --git a/frontend/components/ui/form.tsx b/frontend/components/ui/form.tsx
new file mode 100644
index 0000000..c68c11a
--- /dev/null
+++ b/frontend/components/ui/form.tsx
@@ -0,0 +1,178 @@
+"use client"
+
+import * as React from "react"
+import * as LabelPrimitive from "@radix-ui/react-label"
+import { Slot } from "@radix-ui/react-slot"
+import {
+ Controller,
+ ControllerProps,
+ FieldPath,
+ FieldValues,
+ FormProvider,
+ useFormContext,
+} from "react-hook-form"
+
+import { cn } from "@/lib/utils/tailwindUtils"
+import { Label } from "@/components/ui/label"
+
+const Form = FormProvider
+
+type FormFieldContextValue<
+ TFieldValues extends FieldValues = FieldValues,
+ TName extends FieldPath = FieldPath
+> = {
+ name: TName
+}
+
+const FormFieldContext = React.createContext(
+ {} as FormFieldContextValue
+)
+
+const FormField = <
+ TFieldValues extends FieldValues = FieldValues,
+ TName extends FieldPath = FieldPath
+>({
+ ...props
+}: ControllerProps) => {
+ return (
+
+
+
+ )
+}
+
+const useFormField = () => {
+ const fieldContext = React.useContext(FormFieldContext)
+ const itemContext = React.useContext(FormItemContext)
+ const { getFieldState, formState } = useFormContext()
+
+ const fieldState = getFieldState(fieldContext.name, formState)
+
+ if (!fieldContext) {
+ throw new Error("useFormField should be used within ")
+ }
+
+ const { id } = itemContext
+
+ return {
+ id,
+ name: fieldContext.name,
+ formItemId: `${id}-form-item`,
+ formDescriptionId: `${id}-form-item-description`,
+ formMessageId: `${id}-form-item-message`,
+ ...fieldState,
+ }
+}
+
+type FormItemContextValue = {
+ id: string
+}
+
+const FormItemContext = React.createContext(
+ {} as FormItemContextValue
+)
+
+const FormItem = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => {
+ const id = React.useId()
+
+ return (
+
+
+
+ )
+})
+FormItem.displayName = "FormItem"
+
+const FormLabel = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => {
+ const { error, formItemId } = useFormField()
+
+ return (
+
+ )
+})
+FormLabel.displayName = "FormLabel"
+
+const FormControl = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ ...props }, ref) => {
+ const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
+
+ return (
+
+ )
+})
+FormControl.displayName = "FormControl"
+
+const FormDescription = React.forwardRef<
+ HTMLParagraphElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => {
+ const { formDescriptionId } = useFormField()
+
+ return (
+
+ )
+})
+FormDescription.displayName = "FormDescription"
+
+const FormMessage = React.forwardRef<
+ HTMLParagraphElement,
+ React.HTMLAttributes
+>(({ className, children, ...props }, ref) => {
+ const { error, formMessageId } = useFormField()
+ const body = error ? String(error?.message) : children
+
+ if (!body) {
+ return null
+ }
+
+ return (
+
+ {body}
+
+ )
+})
+FormMessage.displayName = "FormMessage"
+
+export {
+ useFormField,
+ Form,
+ FormItem,
+ FormLabel,
+ FormControl,
+ FormDescription,
+ FormMessage,
+ FormField,
+}
diff --git a/frontend/components/ui/label.tsx b/frontend/components/ui/label.tsx
new file mode 100644
index 0000000..7c02ced
--- /dev/null
+++ b/frontend/components/ui/label.tsx
@@ -0,0 +1,26 @@
+"use client"
+
+import * as React from "react"
+import * as LabelPrimitive from "@radix-ui/react-label"
+import { cva, type VariantProps } from "class-variance-authority"
+
+import { cn } from "@/lib/utils/tailwindUtils"
+
+const labelVariants = cva(
+ "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
+)
+
+const Label = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef &
+ VariantProps
+>(({ className, ...props }, ref) => (
+
+))
+Label.displayName = LabelPrimitive.Root.displayName
+
+export { Label }
diff --git a/frontend/components/ui/progress.tsx b/frontend/components/ui/progress.tsx
new file mode 100644
index 0000000..7b67521
--- /dev/null
+++ b/frontend/components/ui/progress.tsx
@@ -0,0 +1,28 @@
+"use client"
+
+import * as React from "react"
+import * as ProgressPrimitive from "@radix-ui/react-progress"
+
+import { cn } from "@/lib/utils/tailwindUtils"
+
+const Progress = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, value, ...props }, ref) => (
+
+
+
+))
+Progress.displayName = ProgressPrimitive.Root.displayName
+
+export { Progress }
diff --git a/frontend/components/ui/radio-group.tsx b/frontend/components/ui/radio-group.tsx
new file mode 100644
index 0000000..b904d96
--- /dev/null
+++ b/frontend/components/ui/radio-group.tsx
@@ -0,0 +1,44 @@
+"use client"
+
+import * as React from "react"
+import * as RadioGroupPrimitive from "@radix-ui/react-radio-group"
+import { Circle } from "lucide-react"
+
+import { cn } from "@/lib/utils/tailwindUtils"
+
+const RadioGroup = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => {
+ return (
+
+ )
+})
+RadioGroup.displayName = RadioGroupPrimitive.Root.displayName
+
+const RadioGroupItem = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => {
+ return (
+
+
+
+
+
+ )
+})
+RadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName
+
+export { RadioGroup, RadioGroupItem }
diff --git a/frontend/components/ui/select.tsx b/frontend/components/ui/select.tsx
new file mode 100644
index 0000000..b37495d
--- /dev/null
+++ b/frontend/components/ui/select.tsx
@@ -0,0 +1,160 @@
+"use client"
+
+import * as React from "react"
+import * as SelectPrimitive from "@radix-ui/react-select"
+import { Check, ChevronDown, ChevronUp } from "lucide-react"
+
+import { cn } from "@/lib/utils/tailwindUtils"
+
+const Select = SelectPrimitive.Root
+
+const SelectGroup = SelectPrimitive.Group
+
+const SelectValue = SelectPrimitive.Value
+
+const SelectTrigger = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, ...props }, ref) => (
+ span]:line-clamp-1",
+ className
+ )}
+ {...props}
+ >
+ {children}
+
+
+
+
+))
+SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
+
+const SelectScrollUpButton = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+
+
+))
+SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName
+
+const SelectScrollDownButton = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+
+
+))
+SelectScrollDownButton.displayName =
+ SelectPrimitive.ScrollDownButton.displayName
+
+const SelectContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, position = "popper", ...props }, ref) => (
+
+
+
+
+ {children}
+
+
+
+
+))
+SelectContent.displayName = SelectPrimitive.Content.displayName
+
+const SelectLabel = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+SelectLabel.displayName = SelectPrimitive.Label.displayName
+
+const SelectItem = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, ...props }, ref) => (
+
+
+
+
+
+
+
+ {children}
+
+))
+SelectItem.displayName = SelectPrimitive.Item.displayName
+
+const SelectSeparator = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+SelectSeparator.displayName = SelectPrimitive.Separator.displayName
+
+export {
+ Select,
+ SelectGroup,
+ SelectValue,
+ SelectTrigger,
+ SelectContent,
+ SelectLabel,
+ SelectItem,
+ SelectSeparator,
+ SelectScrollUpButton,
+ SelectScrollDownButton,
+}
diff --git a/frontend/components/ui/slider.tsx b/frontend/components/ui/slider.tsx
new file mode 100644
index 0000000..991d7c2
--- /dev/null
+++ b/frontend/components/ui/slider.tsx
@@ -0,0 +1,28 @@
+"use client"
+
+import * as React from "react"
+import * as SliderPrimitive from "@radix-ui/react-slider"
+
+import { cn } from "@/lib/utils/tailwindUtils"
+
+const Slider = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+
+
+
+
+
+))
+Slider.displayName = SliderPrimitive.Root.displayName
+
+export { Slider }
diff --git a/frontend/lib/gql/generated/gql.ts b/frontend/lib/gql/generated/gql.ts
index 436d939..163e602 100644
--- a/frontend/lib/gql/generated/gql.ts
+++ b/frontend/lib/gql/generated/gql.ts
@@ -15,6 +15,7 @@ import { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/
const documents = {
"mutation addTutor($firstName: String!, $lastName: String!, $email: String!, $eventsAvailable: [Int!]!) {\n addTutor(\n tutor: {fn: $firstName, sn: $lastName, mail: $email}\n availability: {userMail: $email, eventID: $eventsAvailable}\n ) {\n fn\n }\n}": types.AddTutorDocument,
"query tutorFormEvents {\n events(needsTutors: true, onlyFuture: true) {\n ID\n title\n from\n to\n topic {\n name\n color\n }\n type {\n name\n color\n }\n }\n}": types.TutorFormEventsDocument,
+ "query registrationForm($eventID: Int!) {\n forms(id: [$eventID]) {\n title\n description\n questions {\n title\n type\n required\n answers {\n ID\n title\n points\n }\n }\n }\n}": types.RegistrationFormDocument,
};
/**
@@ -39,6 +40,10 @@ export function graphql(source: "mutation addTutor($firstName: String!, $lastNam
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "query tutorFormEvents {\n events(needsTutors: true, onlyFuture: true) {\n ID\n title\n from\n to\n topic {\n name\n color\n }\n type {\n name\n color\n }\n }\n}"): (typeof documents)["query tutorFormEvents {\n events(needsTutors: true, onlyFuture: true) {\n ID\n title\n from\n to\n topic {\n name\n color\n }\n type {\n name\n color\n }\n }\n}"];
+/**
+ * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
+ */
+export function graphql(source: "query registrationForm($eventID: Int!) {\n forms(id: [$eventID]) {\n title\n description\n questions {\n title\n type\n required\n answers {\n ID\n title\n points\n }\n }\n }\n}"): (typeof documents)["query registrationForm($eventID: Int!) {\n forms(id: [$eventID]) {\n title\n description\n questions {\n title\n type\n required\n answers {\n ID\n title\n points\n }\n }\n }\n}"];
export function graphql(source: string) {
return (documents as any)[source] ?? {};
diff --git a/frontend/lib/gql/generated/graphql.ts b/frontend/lib/gql/generated/graphql.ts
index c9924b9..d0cee9b 100644
--- a/frontend/lib/gql/generated/graphql.ts
+++ b/frontend/lib/gql/generated/graphql.ts
@@ -18,6 +18,13 @@ export type Scalars = {
timestamptz: { input: any; output: any; }
};
+export type Answer = {
+ __typename?: 'Answer';
+ ID: Scalars['Int']['output'];
+ points: Scalars['Int']['output'];
+ title: Scalars['String']['output'];
+};
+
export type Building = {
__typename?: 'Building';
ID: Scalars['Int']['output'];
@@ -60,6 +67,14 @@ export type EventTutorRoomPair = {
tutors?: Maybe>;
};
+export type Form = {
+ __typename?: 'Form';
+ description?: Maybe;
+ eventID: Scalars['Int']['output'];
+ questions: Array;
+ title: Scalars['String']['output'];
+};
+
export type Label = {
__typename?: 'Label';
color?: Maybe;
@@ -76,6 +91,7 @@ export type Mutation = {
addBuilding: Building;
addEvent: Event;
addEventAssignmentForTutor: Event;
+ addForm: Form;
addLabel: Label;
addRoom: Room;
addRoomAvailabilityForEvent: Room;
@@ -87,6 +103,7 @@ export type Mutation = {
deleteBuilding: Scalars['Int']['output'];
deleteEvent: Scalars['Int']['output'];
deleteEventAssignmentForTutor: Event;
+ deleteForm: Scalars['Int']['output'];
deleteLabel: Scalars['Int']['output'];
deleteRoom: Scalars['Int']['output'];
deleteRoomAvailabilityForEvent: Room;
@@ -96,6 +113,7 @@ export type Mutation = {
deleteUser: Scalars['Int']['output'];
updateBuilding: Building;
updateEvent: Event;
+ updateForm: Form;
updateLabel: Label;
updateRoom: Room;
updateSetting: Setting;
@@ -118,6 +136,11 @@ export type MutationAddEventAssignmentForTutorArgs = {
};
+export type MutationAddFormArgs = {
+ form: NewForm;
+};
+
+
export type MutationAddLabelArgs = {
label: NewLabel;
};
@@ -174,6 +197,11 @@ export type MutationDeleteEventAssignmentForTutorArgs = {
};
+export type MutationDeleteFormArgs = {
+ id: Array;
+};
+
+
export type MutationDeleteLabelArgs = {
name: Array;
};
@@ -222,6 +250,12 @@ export type MutationUpdateEventArgs = {
};
+export type MutationUpdateFormArgs = {
+ form: NewForm;
+ id: Scalars['Int']['input'];
+};
+
+
export type MutationUpdateLabelArgs = {
label: NewLabel;
};
@@ -241,6 +275,11 @@ export type MutationUpdateUserArgs = {
user: NewUser;
};
+export type NewAnswer = {
+ points: Scalars['Int']['input'];
+ title: Scalars['String']['input'];
+};
+
export type NewBuilding = {
city: Scalars['String']['input'];
name?: InputMaybe;
@@ -261,12 +300,25 @@ export type NewEvent = {
umbrellaID?: InputMaybe;
};
+export type NewForm = {
+ description?: InputMaybe;
+ questions: Array;
+ title: Scalars['String']['input'];
+};
+
export type NewLabel = {
color?: InputMaybe;
kind: LabelKind;
name: Scalars['String']['input'];
};
+export type NewQuestion = {
+ answers: Array;
+ required?: InputMaybe;
+ title: Scalars['String']['input'];
+ type: QuestionType;
+};
+
export type NewRoom = {
buildingID: Scalars['Int']['input'];
capacity?: InputMaybe;
@@ -297,6 +349,7 @@ export type Query = {
__typename?: 'Query';
buildings: Array;
events: Array;
+ forms: Array