From 03848a501d8bdbf110cee50314e4e512c6294ae7 Mon Sep 17 00:00:00 2001 From: Michael Harlow <94586121+michaelharlow@users.noreply.github.com> Date: Wed, 23 Oct 2024 18:33:19 -0500 Subject: [PATCH 1/8] frontend: basic LoginService by searching db for matching user --- .../example/controller/LoginController.java | 21 +++++++++++++++++++ .../com/avengers/example/domain/Login.java | 12 +++++++++++ .../example/repository/AccountRepository.java | 1 + .../example/service/AccountService.java | 6 ++++++ .../example/service/LoginService.java | 20 ++++++++++++++++++ 5 files changed, 60 insertions(+) create mode 100644 src/main/java/com/avengers/example/controller/LoginController.java create mode 100644 src/main/java/com/avengers/example/domain/Login.java create mode 100644 src/main/java/com/avengers/example/service/LoginService.java diff --git a/src/main/java/com/avengers/example/controller/LoginController.java b/src/main/java/com/avengers/example/controller/LoginController.java new file mode 100644 index 0000000..366e80e --- /dev/null +++ b/src/main/java/com/avengers/example/controller/LoginController.java @@ -0,0 +1,21 @@ +package com.avengers.example.controller; + +import com.avengers.example.domain.Login; +import com.avengers.example.service.LoginService; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class LoginController { + private final LoginService loginService; + + public LoginController(LoginService loginService) { + this.loginService = loginService; + } + + @PostMapping("/login") + public boolean login(@RequestBody Login login) { + return loginService.isLoginValid(login.username(), login.password()); + } +} diff --git a/src/main/java/com/avengers/example/domain/Login.java b/src/main/java/com/avengers/example/domain/Login.java new file mode 100644 index 0000000..605f22f --- /dev/null +++ b/src/main/java/com/avengers/example/domain/Login.java @@ -0,0 +1,12 @@ +package com.avengers.example.domain; + +import java.util.Objects; + +public record Login(String username, String password) +{ + public Login + { + Objects.requireNonNull(username, "Username cannot be null."); + Objects.requireNonNull(password, "Password cannot be null."); + } +} diff --git a/src/main/java/com/avengers/example/repository/AccountRepository.java b/src/main/java/com/avengers/example/repository/AccountRepository.java index efcf511..400ca04 100644 --- a/src/main/java/com/avengers/example/repository/AccountRepository.java +++ b/src/main/java/com/avengers/example/repository/AccountRepository.java @@ -7,4 +7,5 @@ @Repository public interface AccountRepository extends JpaRepository { + Account findByUsernameAndPassword(String username, String password); } diff --git a/src/main/java/com/avengers/example/service/AccountService.java b/src/main/java/com/avengers/example/service/AccountService.java index b316c17..1f79e25 100644 --- a/src/main/java/com/avengers/example/service/AccountService.java +++ b/src/main/java/com/avengers/example/service/AccountService.java @@ -4,6 +4,7 @@ import com.avengers.example.repository.AccountRepository; import jakarta.transaction.Transactional; import lombok.AllArgsConstructor; +import org.springframework.data.domain.Example; import org.springframework.stereotype.Service; import java.util.List; @@ -24,4 +25,9 @@ public List findAll() { return accountRepository.findAll(); } + + public Account findByUsernameAndPassword(String username, String password) + { + return accountRepository.findByUsernameAndPassword(username, password); + } } diff --git a/src/main/java/com/avengers/example/service/LoginService.java b/src/main/java/com/avengers/example/service/LoginService.java new file mode 100644 index 0000000..a00eff2 --- /dev/null +++ b/src/main/java/com/avengers/example/service/LoginService.java @@ -0,0 +1,20 @@ +package com.avengers.example.service; + +import org.springframework.stereotype.Service; + +import java.util.Objects; + +@Service +public class LoginService { + + private final AccountService accountService; + + public LoginService(AccountService accountService) { + this.accountService = accountService; + } + + public boolean isLoginValid(String username, String password) { + return !Objects.isNull(accountService.findByUsernameAndPassword(username, password)); + } + +} From f6c3cc6c5d164932e86bc707d703c32190433d83 Mon Sep 17 00:00:00 2001 From: Michael Harlow <94586121+michaelharlow@users.noreply.github.com> Date: Wed, 23 Oct 2024 18:33:56 -0500 Subject: [PATCH 2/8] frontend: unused import --- src/main/java/com/avengers/example/service/AccountService.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/java/com/avengers/example/service/AccountService.java b/src/main/java/com/avengers/example/service/AccountService.java index 1f79e25..6217aa2 100644 --- a/src/main/java/com/avengers/example/service/AccountService.java +++ b/src/main/java/com/avengers/example/service/AccountService.java @@ -4,7 +4,6 @@ import com.avengers.example.repository.AccountRepository; import jakarta.transaction.Transactional; import lombok.AllArgsConstructor; -import org.springframework.data.domain.Example; import org.springframework.stereotype.Service; import java.util.List; From 1225fc8f7824914ba0df7c8928b36eec1b744945 Mon Sep 17 00:00:00 2001 From: Michael Harlow <94586121+michaelharlow@users.noreply.github.com> Date: Thu, 24 Oct 2024 10:40:26 -0500 Subject: [PATCH 3/8] login: component style update --- src/main/frontend/src/components/ui/button.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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: { From a106d9abc2334370bdb1de02be30f645548cd31c Mon Sep 17 00:00:00 2001 From: Michael Harlow <94586121+michaelharlow@users.noreply.github.com> Date: Thu, 24 Oct 2024 21:16:37 -0500 Subject: [PATCH 4/8] login: form wrapper component --- src/main/frontend/package-lock.json | 38 +++- src/main/frontend/package.json | 5 +- src/main/frontend/src/components/ui/form.tsx | 176 +++++++++++++++++++ 3 files changed, 215 insertions(+), 4 deletions(-) create mode 100644 src/main/frontend/src/components/ui/form.tsx 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/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 ( +