From a2c9a9f2be26cc8d02b5c29fc5af2334ceb44bc3 Mon Sep 17 00:00:00 2001 From: Benjamin Frost Date: Mon, 28 Oct 2024 14:27:13 +0100 Subject: [PATCH 1/9] feat: add registration page --- src/pages/[lang]/anmeldung.astro | 47 ++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 src/pages/[lang]/anmeldung.astro diff --git a/src/pages/[lang]/anmeldung.astro b/src/pages/[lang]/anmeldung.astro new file mode 100644 index 0000000..cb9fb51 --- /dev/null +++ b/src/pages/[lang]/anmeldung.astro @@ -0,0 +1,47 @@ +--- +import { InformationCircleIcon } from "@heroicons/react/20/solid"; +import NoTranslate from "~/components/NoTranslate.astro"; +import { localeParams } from "~/i18n"; +import Layout from "~/layouts/Layout.astro"; + +export const getStaticPaths = localeParams; +--- + + + +
+
+

+ Anmeldung zur Deutschen Meisterschaft +

+

+ Der Online-Wettkampf und die Deutschen Meisterschaften werden von allen + drei Skills gemeinsam durchgeführt. Die Aufteilung des Nationalteams auf + Skill 08, Skill 09 und Skill 17 erfolgt erst im + Februar 2025 im Rahmen eines Wettkampfes, also vor dem + internationalen Albert-Einstein-Cup.

Um teilzunehmen solltest + du mindestens eine Programmiersprache beherrschen, Android-Apps + programmieren können, grundlegende Kenntnisse in UML und Datenbanken + besitzen und englischsprachige Aufgabenstellungen verstehen können. +

+
+
+
+
+
+

+ Bitte beachte, dass du im Wettkampfjahr 2025 nicht älter als + 22 Jahre werden darfst, d. h. Jahrgänge 01.01.2004 und + aufwärts. +

+
+
+
+ +
+
+
From 91323d20bb5fb12f99addea20a9afbf4c0191acc Mon Sep 17 00:00:00 2001 From: Benjamin Frost Date: Mon, 28 Oct 2024 14:55:51 +0100 Subject: [PATCH 2/9] feat: add registration form --- package.json | 5 +- src/components/forms/component/alert.tsx | 23 ++ src/components/forms/component/button.tsx | 44 ++++ src/components/forms/component/input.tsx | 33 +++ src/components/forms/component/select.tsx | 36 +++ src/components/forms/registration-form.tsx | 269 +++++++++++++++++++++ src/components/forms/utils.ts | 11 + src/pages/[lang]/anmeldung.astro | 5 +- yarn.lock | 10 + 9 files changed, 433 insertions(+), 3 deletions(-) create mode 100644 src/components/forms/component/alert.tsx create mode 100644 src/components/forms/component/button.tsx create mode 100644 src/components/forms/component/input.tsx create mode 100644 src/components/forms/component/select.tsx create mode 100644 src/components/forms/registration-form.tsx create mode 100644 src/components/forms/utils.ts diff --git a/package.json b/package.json index 39b7c99..5c095fd 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "@fontsource/poppins": "^5.1.0", "@headlessui/react": "^2.1.10", "@heroicons/react": "^2.1.5", + "@hookform/resolvers": "^3.9.0", "@iconify-json/mdi": "^1.2.1", "@tailwindcss/typography": "^0.5.15", "@types/react": "^18.3.11", @@ -28,7 +29,9 @@ "prettier-plugin-tailwindcss": "^0.6.8", "react": "^18.3.1", "react-dom": "^18.3.1", + "react-hook-form": "^7.53.1", "tailwindcss": "^3.4.13", - "typescript": "^5.6.2" + "typescript": "^5.6.2", + "zod": "^3.23.8" } } diff --git a/src/components/forms/component/alert.tsx b/src/components/forms/component/alert.tsx new file mode 100644 index 0000000..7bea057 --- /dev/null +++ b/src/components/forms/component/alert.tsx @@ -0,0 +1,23 @@ +import { clsx } from "clsx"; + +interface Props { + type?: "error" | "success"; + children?: React.ReactNode; +} + +export function Alert({ type, children }: Props) { + const alertClasses = clsx("rounded-md p-4", { + "bg-red-50": type === "error", + "bg-green-50": type === "success", + }); + const textClasses = clsx("text-sm font-medium", { + "text-red-800": type === "error", + "text-green-800": type === "success", + }); + + return ( +
+

{children}

+
+ ); +} diff --git a/src/components/forms/component/button.tsx b/src/components/forms/component/button.tsx new file mode 100644 index 0000000..6d5f535 --- /dev/null +++ b/src/components/forms/component/button.tsx @@ -0,0 +1,44 @@ +import { clsx } from "clsx"; +import type { ComponentPropsWithRef } from "react"; + +interface Props extends ComponentPropsWithRef<"button"> { + isLoading?: boolean; + children?: React.ReactNode; +} + +export const Button = ({ isLoading, children, ...rest }: Props) => { + const buttonClasses = clsx( + "inline-flex justify-center rounded-md border border-transparent bg-wsg-orange-500 py-2 px-4 text-sm font-medium text-white shadow-sm", + { + "cursor-not-allowed opacity-60": isLoading, + }, + ); + + return ( + + ); +}; diff --git a/src/components/forms/component/input.tsx b/src/components/forms/component/input.tsx new file mode 100644 index 0000000..0a060f1 --- /dev/null +++ b/src/components/forms/component/input.tsx @@ -0,0 +1,33 @@ +import { clsx } from "clsx"; +import { type ComponentPropsWithoutRef, forwardRef } from "react"; + +interface Props extends ComponentPropsWithoutRef<"input"> { + id: string; + label: string; + error?: string; +} + +export const Input = forwardRef( + ({ id, label, error, ...rest }, ref) => { + const inputClasses = clsx( + "block w-full rounded-lg focus:outline-none shadow-sm sm:text-sm border py-2 px-3", + { + "border-red-300 focus:border-red-500 focus:ring-red-500": error, + "border-gray-300 focus:border-wsg-orange-500 focus:ring-wsg-orange-500": + !error, + }, + ); + + return ( +
+ +
+ +
+ {error &&

{error}

} +
+ ); + }, +); diff --git a/src/components/forms/component/select.tsx b/src/components/forms/component/select.tsx new file mode 100644 index 0000000..bf685cb --- /dev/null +++ b/src/components/forms/component/select.tsx @@ -0,0 +1,36 @@ +import { clsx } from "clsx"; +import { type ComponentPropsWithoutRef, forwardRef } from "react"; + +interface Props extends ComponentPropsWithoutRef<"select"> { + id: string; + label: string; + error?: string; + children?: React.ReactNode; +} + +export const Select = forwardRef( + ({ id, label, error, children, ...rest }, ref) => { + const inputClasses = clsx( + "block w-full rounded-lg focus:outline-none shadow-sm sm:text-sm border py-2 px-3", + { + "border-red-300 focus:border-red-500 focus:ring-red-500": error, + "border-gray-300 focus:border-wsg-orange-500 focus:ring-wsg-orange-500": + !error, + }, + ); + + return ( +
+ +
+ +
+ {error &&

{error}

} +
+ ); + }, +); diff --git a/src/components/forms/registration-form.tsx b/src/components/forms/registration-form.tsx new file mode 100644 index 0000000..1a89339 --- /dev/null +++ b/src/components/forms/registration-form.tsx @@ -0,0 +1,269 @@ +import { zodResolver } from "@hookform/resolvers/zod"; +import { useState } from "react"; +import { useForm } from "react-hook-form"; +import { z } from "zod"; +import { Alert } from "./component/alert"; +import { Button } from "./component/button"; +import { Input } from "./component/input"; +import { Select } from "./component/select"; +import { fixOptional } from "./utils"; + +const institutionSchema = z.object({ + name: z.string().min(1).max(255), + city: z.string().min(1).max(255), +}); + +export const participantSchema = z.object({ + firstName: z.string().min(1).max(255), + lastName: z.string().min(1).max(255), + birthday: z + .string() + .regex(/^\d{4}-\d{2}-\d{2}$/, "Invalid date (YYYY-MM-DD)"), + email: z.string().email(), + state: z.enum([ + "BADEN_WUERTTEMBERG", + "BAVARIA", + "BERLIN", + "BRANDENBURG", + "BREMEN", + "HAMBURG", + "HESSE", + "MECKLENBURG_WESTERN_POMERANIA", + "LOWER_SAXONY", + "NORTH_RHINE_WESTPHALIA", + "RHINELAND_PALATINATE", + "SAARLAND", + "SAXONY", + "SAXONY_ANHALT", + "SCHLESWIG_HOLSTEIN", + "THURINGIA", + ]), + city: z.string().min(1).max(255), + phone: z.preprocess(fixOptional, z.string().min(1).max(255).optional()), + occupation: z.enum(["APPRENTICE", "PUPIL", "STUDENT", "EMPLOYEE", "OTHER"]), + company: z.preprocess(fixOptional, institutionSchema.optional()), + educationalInsitution: z.preprocess( + fixOptional, + institutionSchema.optional(), + ), +}); + +type Participant = z.infer; + +interface Message { + type: "error" | "success"; + text: string; +} + +export default function RegistrationForm() { + const [message, setMessage] = useState(null); + + const { + register, + handleSubmit, + reset, + formState: { isSubmitting, errors }, + } = useForm({ resolver: zodResolver(participantSchema) }); + + const onSubmit = handleSubmit(async (data) => { + const response = await fetch( + "https://registration-api.blz-it.de/participants", + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(data), + }, + ); + + if (response.ok) { + setMessage({ type: "success", text: "Erfolgreich angemeldet!" }); + reset(); + } else { + const json = await response.json(); + setMessage({ + type: "error", + text: + json.message ?? + "Es ist ein unbekannter Fehler aufgetreten! Bitte versuche es erneut.", + }); + } + }); + + return ( +
+
+
+
+

+ Persönliche Informationen +

+

+ Die folgenden Angaben sind für die Anmeldung verpflichtend. +

+
+
+
+ +
+
+ +
+ +
+ +
+
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+
+
+ +
+
+

+ Zusätzliche Persönliche Informationen +

+

+ Die folgenden Angaben sind vollkommen optional. +

+
+
+
+ +
+
+
+ +
+
+

+ Deine Firma +

+

+ Falls du in einer Firma arbeitest, kannst du diese hier angeben. +

+
+
+
+ +
+
+ +
+
+
+
+ +
+ {message && ( +
+ {message.text} +
+ )} + +
+ +
+
+
+ ); +} diff --git a/src/components/forms/utils.ts b/src/components/forms/utils.ts new file mode 100644 index 0000000..905aea8 --- /dev/null +++ b/src/components/forms/utils.ts @@ -0,0 +1,11 @@ +export const fixOptional = (value: unknown) => { + if (value === "") return undefined; + if (typeof value === "object" && value !== null) { + // Check if every child value (even nested) is empty + const isEmpty = Object.values(value).every( + (v) => fixOptional(v) === undefined, + ); + if (isEmpty) return undefined; + } + return value; +}; diff --git a/src/pages/[lang]/anmeldung.astro b/src/pages/[lang]/anmeldung.astro index cb9fb51..15c17b4 100644 --- a/src/pages/[lang]/anmeldung.astro +++ b/src/pages/[lang]/anmeldung.astro @@ -1,5 +1,6 @@ --- import { InformationCircleIcon } from "@heroicons/react/20/solid"; +import RegistrationForm from "~/components/forms/registration-form"; import NoTranslate from "~/components/NoTranslate.astro"; import { localeParams } from "~/i18n"; import Layout from "~/layouts/Layout.astro"; @@ -24,7 +25,7 @@ export const getStaticPaths = localeParams; programmieren können, grundlegende Kenntnisse in UML und Datenbanken besitzen und englischsprachige Aufgabenstellungen verstehen können.

-
+
- +
diff --git a/yarn.lock b/yarn.lock index ed5983a..6fa5f0c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -574,6 +574,11 @@ resolved "https://registry.yarnpkg.com/@heroicons/react/-/react-2.1.5.tgz#1e13f34976cc542deae92353c01c8b3d7942e9ba" integrity sha512-FuzFN+BsHa+7OxbvAERtgBTNeZpUjgM/MIizfVkSCL2/edriN0Hx/DWRCR//aPYwO5QX/YlgLGXk+E3PcfZwjA== +"@hookform/resolvers@^3.9.0": + version "3.9.0" + resolved "https://registry.yarnpkg.com/@hookform/resolvers/-/resolvers-3.9.0.tgz#cf540ac21c6c0cd24a40cf53d8e6d64391fb753d" + integrity sha512-bU0Gr4EepJ/EQsH/IwEzYLsT/PEj5C0ynLQ4m+GSHS+xKH4TfSelhluTgOaoc4kA5s7eCsQbM4wvZLzELmWzUg== + "@iconify-json/mdi@^1.2.1": version "1.2.1" resolved "https://registry.yarnpkg.com/@iconify-json/mdi/-/mdi-1.2.1.tgz#029deff92cedf38430a9ed2ee811a8818f1ded43" @@ -3582,6 +3587,11 @@ react-dom@^18.3.1: loose-envify "^1.1.0" scheduler "^0.23.2" +react-hook-form@^7.53.1: + version "7.53.1" + resolved "https://registry.yarnpkg.com/react-hook-form/-/react-hook-form-7.53.1.tgz#3f2cd1ed2b3af99416a4ac674da2d526625add67" + integrity sha512-6aiQeBda4zjcuaugWvim9WsGqisoUk+etmFEsSUMm451/Ic8L/UAb7sRtMj3V+Hdzm6mMjU1VhiSzYUZeBm0Vg== + react-refresh@^0.14.2: version "0.14.2" resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.14.2.tgz#3833da01ce32da470f1f936b9d477da5c7028bf9" From 341122685b99593b52aa94498660dcb1cd88481b Mon Sep 17 00:00:00 2001 From: Benjamin Frost Date: Mon, 28 Oct 2024 15:01:16 +0100 Subject: [PATCH 3/9] fix: registration links --- src/components/skill/SkillInformation.astro | 17 +++++++++++++---- src/components/skill/SkillRoadmap.astro | 6 +++--- src/layouts/SkillPage.astro | 2 +- src/pages/en/index.astro | 2 +- src/pages/index.astro | 2 +- 5 files changed, 19 insertions(+), 10 deletions(-) diff --git a/src/components/skill/SkillInformation.astro b/src/components/skill/SkillInformation.astro index 3bccb7b..3cd1969 100644 --- a/src/components/skill/SkillInformation.astro +++ b/src/components/skill/SkillInformation.astro @@ -1,5 +1,7 @@ --- +import { getRelativeLocaleUrl } from "astro:i18n"; import { getLangFromUrl, useTranslations } from "~/i18n"; +import Link from "../Link.astro"; import Headline from "../typography/Headline.astro"; interface Props { @@ -31,10 +33,17 @@ const t = useTranslations(lang); }

- +

+ { + t({ + de: "Du bist interessiert?", + en: "You are interested?", + }) + } + + {t({ de: "Jetzt bewerben!", en: "Apply now!" })} + +