diff --git a/src/main/frontend/package-lock.json b/src/main/frontend/package-lock.json index 2be3119..3848510 100644 --- a/src/main/frontend/package-lock.json +++ b/src/main/frontend/package-lock.json @@ -8,6 +8,7 @@ "name": "frontend", "version": "0.0.0", "dependencies": { + "@hookform/resolvers": "^3.9.0", "@radix-ui/react-icons": "^1.3.0", "@radix-ui/react-label": "^2.1.0", "@radix-ui/react-slot": "^1.1.0", @@ -16,9 +17,11 @@ "lucide-react": "^0.453.0", "react": "^18.3.1", "react-dom": "^18.3.1", + "react-hook-form": "^7.53.1", "react-router-dom": "^6.27.0", "tailwind-merge": "^2.5.4", - "tailwindcss-animate": "^1.0.7" + "tailwindcss-animate": "^1.0.7", + "zod": "^3.23.8" }, "devDependencies": { "@eslint/js": "^9.11.1", @@ -575,6 +578,14 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@hookform/resolvers": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-3.9.0.tgz", + "integrity": "sha512-bU0Gr4EepJ/EQsH/IwEzYLsT/PEj5C0ynLQ4m+GSHS+xKH4TfSelhluTgOaoc4kA5s7eCsQbM4wvZLzELmWzUg==", + "peerDependencies": { + "react-hook-form": "^7.0.0" + } + }, "node_modules/@humanfs/core": { "version": "0.19.0", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.0.tgz", @@ -765,7 +776,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.0.tgz", "integrity": "sha512-peLblDlFw/ngk3UWq0VnYaOLy6agTZZ+MUO/WhVfm14vJGML+xH4FAl2XQGLqdefjNb7ApRg6Yn7U42ZhmYXdw==", - "license": "MIT", "dependencies": { "@radix-ui/react-primitive": "2.0.0" }, @@ -811,7 +821,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.0.tgz", "integrity": "sha512-FUCf5XMfmW4dtYl69pdS4DbxKy8nj4M7SafBgPllysxmdachynNflAdp/gCsnYWNDnge6tI9onzMp5ARYc1KNw==", - "license": "MIT", "dependencies": { "@radix-ui/react-compose-refs": "1.1.0" }, @@ -3306,6 +3315,21 @@ "react": "^18.3.1" } }, + "node_modules/react-hook-form": { + "version": "7.53.1", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.53.1.tgz", + "integrity": "sha512-6aiQeBda4zjcuaugWvim9WsGqisoUk+etmFEsSUMm451/Ic8L/UAb7sRtMj3V+Hdzm6mMjU1VhiSzYUZeBm0Vg==", + "engines": { + "node": ">=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/react-hook-form" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17 || ^18 || ^19" + } + }, "node_modules/react-router": { "version": "6.27.0", "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.27.0.tgz", @@ -4092,6 +4116,14 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/zod": { + "version": "3.23.8", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz", + "integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } } } } diff --git a/src/main/frontend/package.json b/src/main/frontend/package.json index a1813f5..633d7e2 100644 --- a/src/main/frontend/package.json +++ b/src/main/frontend/package.json @@ -10,6 +10,7 @@ "preview": "vite preview" }, "dependencies": { + "@hookform/resolvers": "^3.9.0", "@radix-ui/react-icons": "^1.3.0", "@radix-ui/react-label": "^2.1.0", "@radix-ui/react-slot": "^1.1.0", @@ -18,9 +19,11 @@ "lucide-react": "^0.453.0", "react": "^18.3.1", "react-dom": "^18.3.1", + "react-hook-form": "^7.53.1", "react-router-dom": "^6.27.0", "tailwind-merge": "^2.5.4", - "tailwindcss-animate": "^1.0.7" + "tailwindcss-animate": "^1.0.7", + "zod": "^3.23.8" }, "devDependencies": { "@eslint/js": "^9.11.1", diff --git a/src/main/frontend/src/components/Auth.tsx b/src/main/frontend/src/components/Auth.tsx new file mode 100644 index 0000000..664637a --- /dev/null +++ b/src/main/frontend/src/components/Auth.tsx @@ -0,0 +1,36 @@ +import {createContext, useContext, useState} from "react"; + +const AuthContext = createContext(undefined); + +function AuthProvider({ children}: Props) { + const [user, setUser] = useState(() => { + const user = sessionStorage.getItem("user"); + return user ? JSON.parse(user) : undefined; + }); + + const signIn = (data: Login) => { + setUser(data); + sessionStorage.setItem("user", JSON.stringify(data)); + }; + + const signOut = () => { + setUser(undefined); + sessionStorage.removeItem("user"); + }; + + return ( + + {children} + + ); +} + +const useAuth = () => { + const context = useContext(AuthContext); + if (context === undefined) { + throw new Error("useAuth must be used within an AuthProvider"); + } + return context; +} + +export { AuthProvider, useAuth }; \ No newline at end of file diff --git a/src/main/frontend/src/components/ProtectedRoute.tsx b/src/main/frontend/src/components/ProtectedRoute.tsx new file mode 100644 index 0000000..a6c2b2c --- /dev/null +++ b/src/main/frontend/src/components/ProtectedRoute.tsx @@ -0,0 +1,13 @@ +import {Navigate, Outlet} from "react-router-dom"; +import {useAuth} from "@/components/Auth.tsx"; + +function ProtectedRoute() { + const {user} = useAuth(); + console.log(`User2: ${user}`); + if (user === undefined) { + return ; + } + return ; +} + +export default ProtectedRoute; \ No newline at end of file diff --git a/src/main/frontend/src/components/ui/button.tsx b/src/main/frontend/src/components/ui/button.tsx index 0270f64..65d4fcd 100644 --- a/src/main/frontend/src/components/ui/button.tsx +++ b/src/main/frontend/src/components/ui/button.tsx @@ -5,7 +5,7 @@ import { cva, type VariantProps } from "class-variance-authority" import { cn } from "@/lib/utils" const buttonVariants = cva( - "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50", + "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0", { variants: { variant: { diff --git a/src/main/frontend/src/components/ui/form.tsx b/src/main/frontend/src/components/ui/form.tsx new file mode 100644 index 0000000..f6afdaf --- /dev/null +++ b/src/main/frontend/src/components/ui/form.tsx @@ -0,0 +1,176 @@ +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" +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 ( +