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 ( +
+ Error +

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].answers.map((answer) => ( + ( + +
+ + { + return checked + ? field.onChange([ + ...field.value || [], + answer.ID, + ]) + : field.onChange( + field.value?.filter( + (value) => value !== answer.ID + ) + ); + }} + /> + + +
+
+ )} + /> + ))} + +
+ )} + /> +
+
+ + + +
+ + )} + + {regForm?.questions[index].type === QuestionType.SingleChoice && ( +
+ + +
+ ( + + + field.onChange(parseInt(value, 10)) + } + > + {regForm.questions[index].answers.map((answer) => ( +
+ + +
+ ))} +
+ +
+ )} + /> +
+
+ + + +
+ + )} + + {regForm?.questions[index].type === QuestionType.Scale && ( +
+ +
+
+ + {regForm.questions[index].answers[1].title} + + + {regForm.questions[index].answers[0].title} + +
+ setSliderValue(value[0])} + min={regForm.questions[index].answers[1].points} + max={regForm.questions[index].answers[0].points} + step={1} + /> +
+
+ + + +
+ )} +
+
+
+ ); +}; + +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 ( +