From c45cf6df471a5c16152e0dd3d458cc2b618a3eb4 Mon Sep 17 00:00:00 2001 From: Fingertips Date: Fri, 13 Sep 2024 02:39:18 +0800 Subject: [PATCH] Create signup page Consume sign up auth api --- client/package-lock.json | 40 +++++++- client/package.json | 4 +- client/src/App.tsx | 8 +- client/src/components/button.tsx | 25 +++++ client/src/components/input.tsx | 98 +++++++++++++++++++ client/src/constants/collections.ts | 46 +++++++++ client/src/constants/keys.ts | 1 + client/src/constants/regex.ts | 14 +++ client/src/index.css | 4 +- client/src/lib/DTO/sign-up.dto.ts | 5 + client/src/lib/providers/query-provider.tsx | 15 +++ client/src/lib/providers/toast-provider.tsx | 20 ++++ client/src/lib/services/auth-service.ts | 17 ++++ client/src/lib/utils/validations.ts | 36 +++++++ client/src/main.tsx | 12 ++- .../root/_components}/header/index.tsx | 0 .../root/_components}/header/logo.tsx | 0 .../root/_components}/header/toggle-mode.tsx | 0 client/src/pages/root/page.tsx | 14 +++ client/src/pages/sign-up/page.tsx | 81 +++++++++++++++ client/src/routes/root/page.tsx | 9 -- client/tailwind.config.js | 4 +- 22 files changed, 431 insertions(+), 22 deletions(-) create mode 100644 client/src/components/button.tsx create mode 100644 client/src/components/input.tsx create mode 100644 client/src/constants/collections.ts create mode 100644 client/src/constants/keys.ts create mode 100644 client/src/constants/regex.ts create mode 100644 client/src/lib/DTO/sign-up.dto.ts create mode 100644 client/src/lib/providers/query-provider.tsx create mode 100644 client/src/lib/providers/toast-provider.tsx create mode 100644 client/src/lib/services/auth-service.ts create mode 100644 client/src/lib/utils/validations.ts rename client/src/{components => pages/root/_components}/header/index.tsx (100%) rename client/src/{components => pages/root/_components}/header/logo.tsx (100%) rename client/src/{components => pages/root/_components}/header/toggle-mode.tsx (100%) create mode 100644 client/src/pages/root/page.tsx create mode 100644 client/src/pages/sign-up/page.tsx delete mode 100644 client/src/routes/root/page.tsx diff --git a/client/package-lock.json b/client/package-lock.json index d6845f4..b66816c 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -9,10 +9,12 @@ "version": "1.0.0", "license": "MIT", "dependencies": { + "@tanstack/react-query": "^5.56.1", "lucide-react": "^0.440.0", "react": "^18.3.1", "react-dom": "^18.3.1", - "react-router-dom": "^6.26.2" + "react-router-dom": "^6.26.2", + "sonner": "^1.5.0" }, "devDependencies": { "@eslint/js": "^9.9.0", @@ -1196,6 +1198,32 @@ "@swc/counter": "^0.1.3" } }, + "node_modules/@tanstack/query-core": { + "version": "5.56.1", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.56.1.tgz", + "integrity": "sha512-hH9LvpGCr3yKbHUgi2b+vOwx2pj4dMFnGY7Cjvrm5mOqynWPr015h/GCLiSi5M5JaXXnB8VnLOSLh5KzjBh5fA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/react-query": { + "version": "5.56.1", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.56.1.tgz", + "integrity": "sha512-jlRCrE1qXEVtJ0i/bubQpqoEuQQSfRo9O26tWciKmNMwg2oXp96+D7A/0sU52sjSTrcKIQE4o7dSlbOOTYKTIg==", + "license": "MIT", + "dependencies": { + "@tanstack/query-core": "5.56.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^18 || ^19" + } + }, "node_modules/@types/estree": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", @@ -3468,6 +3496,16 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/sonner": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/sonner/-/sonner-1.5.0.tgz", + "integrity": "sha512-FBjhG/gnnbN6FY0jaNnqZOMmB73R+5IiyYAw8yBj7L54ER7HB3fOSE5OFiQiE2iXWxeXKvg6fIP4LtVppHEdJA==", + "license": "MIT", + "peerDependencies": { + "react": "^18.0.0", + "react-dom": "^18.0.0" + } + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", diff --git a/client/package.json b/client/package.json index c85f4ef..0f0da5c 100644 --- a/client/package.json +++ b/client/package.json @@ -15,10 +15,12 @@ "preview": "vite preview" }, "dependencies": { + "@tanstack/react-query": "^5.56.1", "lucide-react": "^0.440.0", "react": "^18.3.1", "react-dom": "^18.3.1", - "react-router-dom": "^6.26.2" + "react-router-dom": "^6.26.2", + "sonner": "^1.5.0" }, "devDependencies": { "@eslint/js": "^9.9.0", diff --git a/client/src/App.tsx b/client/src/App.tsx index 3b78955..b5f544b 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -1,15 +1,15 @@ import { Route, Routes } from "react-router-dom"; import { AppRoutes } from "@/constants/routes"; -import { Header } from "@/components/header"; -import RootPage from "@/routes/root/page"; +import SignUpPage from "@/pages/sign-up/page"; +import RootPage from "@/pages/root/page"; function App() { return ( -
-
+
} /> + } />
); diff --git a/client/src/components/button.tsx b/client/src/components/button.tsx new file mode 100644 index 0000000..304c4e4 --- /dev/null +++ b/client/src/components/button.tsx @@ -0,0 +1,25 @@ +import { Loader2 } from "lucide-react"; + +interface ButtonProps { + label: string; + onClick?: () => void; + type: "submit" | "reset" | "button" | undefined; + disabled?: boolean; + loading?: boolean; +} + +const Button = ({ label, onClick, type, disabled, loading }: ButtonProps) => { + return ( + + ); +}; + +export { Button }; diff --git a/client/src/components/input.tsx b/client/src/components/input.tsx new file mode 100644 index 0000000..b74a098 --- /dev/null +++ b/client/src/components/input.tsx @@ -0,0 +1,98 @@ +import { + ForwardRefExoticComponent, + RefAttributes, + useEffect, + useState, +} from "react"; +import { Eye, EyeOff, Info, LucideProps } from "lucide-react"; + +interface InputProps { + name?: string; + label: string; + placeholder: string; + type: React.HTMLInputTypeAttribute | undefined; + autoComplete: React.HTMLInputAutoCompleteAttribute | undefined; + suffixIcon: ForwardRefExoticComponent< + Omit & RefAttributes + >; + disabled?: boolean; + validation: (value: string) => boolean; + required?: boolean; +} + +const Input = ({ + name, + label, + placeholder, + type, + autoComplete, + suffixIcon: SuffixIcon, + disabled, + validation, + required, +}: InputProps) => { + const [value, setValue] = useState(""); + const [obscure, setObscure] = useState(true); + const [valid, setValid] = useState(false); + + const isPasswod = type === "password"; + const hasInput = value.length > 0; + + useEffect(() => { + setValid(validation(value)); + }, [validation, value]); + + return ( +
+
+ + +
+
+ setValue(e.target.value)} + type={isPasswod ? (obscure ? "password" : "text") : type} + placeholder={placeholder} + autoComplete={autoComplete} + id={name?.toLowerCase() ?? label.toLowerCase()} + name={name?.toLowerCase() ?? label.toLowerCase()} + disabled={disabled} + required={required} + className={`w-full lg:w-[400px] bg-background p-2.5 rounded-lg outline-none border ring-1 focus:ring-2 transition-all placeholder-foreground/50 + px-11 disabled:bg-opacity-25 disabled:border-primary/25 disabled:text-foreground/50 disabled:pointer-events-none + ${ + hasInput + ? valid + ? "border-green-400 focus:border-transparent ring-green-400 focus:ring-green-500" + : "border-red-400 focus:border-transparent ring-red-400 focus:ring-red-500" + : "border-primary/50 focus:border-transparent ring-primary/50 focus:ring-primary" + }`} + /> +
+ +
+ {isPasswod && ( + + )} +
+
+ ); +}; + +export { Input }; diff --git a/client/src/constants/collections.ts b/client/src/constants/collections.ts new file mode 100644 index 0000000..d13f1b6 --- /dev/null +++ b/client/src/constants/collections.ts @@ -0,0 +1,46 @@ +import { Lock, Mail, User } from "lucide-react"; + +import { + ValidateConfirmPassword, + ValidateEmail, + ValidatePassword, + ValidateUsername, +} from "@/lib/utils/validations"; + +export const SIGNUPINPUTS = [ + { + name: "username", + label: "Username", + placeholder: "e.g. john doe", + type: "text", + autoComplete: "username", + suffixIcon: User, + validation: ValidateUsername, + }, + { + name: "email", + label: "Email Address", + placeholder: "e.g. john@domain.com", + type: "email", + autoComplete: "email", + suffixIcon: Mail, + validation: ValidateEmail, + }, + { + name: "password", + label: "Password", + placeholder: "e.g. m#P52s@ap$V", + type: "password", + autoComplete: "new-password", + suffixIcon: Lock, + validation: ValidatePassword, + }, + { + label: "Confirm Password", + placeholder: "e.g. m#P52s@ap$V", + type: "password", + autoComplete: "new-password", + suffixIcon: Lock, + validation: ValidateConfirmPassword, + }, +]; diff --git a/client/src/constants/keys.ts b/client/src/constants/keys.ts new file mode 100644 index 0000000..f422536 --- /dev/null +++ b/client/src/constants/keys.ts @@ -0,0 +1 @@ +export const SIGNUPKEY = "sign-up"; diff --git a/client/src/constants/regex.ts b/client/src/constants/regex.ts new file mode 100644 index 0000000..4fb1db9 --- /dev/null +++ b/client/src/constants/regex.ts @@ -0,0 +1,14 @@ +const EmailRegex = /^[\w.-]+@[a-zA-Z\d.-]+\.[a-zA-Z]{2,}$/; + +const UpperCaseRegex = /[A-Z]/; +const LowerCaseRegex = /[a-z]/; +const NumberRegex = /\d/; +const SpecialRegex = /[!@#$%^&*()_+{}[\]:;<>,.?~\\/-]/; + +export { + EmailRegex, + UpperCaseRegex, + LowerCaseRegex, + NumberRegex, + SpecialRegex, +}; diff --git a/client/src/index.css b/client/src/index.css index f8c48cf..f12b33b 100644 --- a/client/src/index.css +++ b/client/src/index.css @@ -24,12 +24,12 @@ } * { - @apply m-0 p-0 scroll-smooth box-border; + @apply scroll-smooth box-border; } html, body { - @apply bg-background text-foreground font-poppins; + @apply m-0 p-0 bg-background text-foreground font-poppins; } ::-webkit-scrollbar { diff --git a/client/src/lib/DTO/sign-up.dto.ts b/client/src/lib/DTO/sign-up.dto.ts new file mode 100644 index 0000000..0adf82f --- /dev/null +++ b/client/src/lib/DTO/sign-up.dto.ts @@ -0,0 +1,5 @@ +export type SignUpDTO = { + username: string; + email: string; + password: string; +}; diff --git a/client/src/lib/providers/query-provider.tsx b/client/src/lib/providers/query-provider.tsx new file mode 100644 index 0000000..fe2474b --- /dev/null +++ b/client/src/lib/providers/query-provider.tsx @@ -0,0 +1,15 @@ +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; + +interface QueryProviderProps { + children: React.ReactNode; +} + +const queryClient = new QueryClient(); + +const QueryProvider = ({ children }: QueryProviderProps) => { + return ( + {children} + ); +}; + +export default QueryProvider; diff --git a/client/src/lib/providers/toast-provider.tsx b/client/src/lib/providers/toast-provider.tsx new file mode 100644 index 0000000..2763a5d --- /dev/null +++ b/client/src/lib/providers/toast-provider.tsx @@ -0,0 +1,20 @@ +import { Toaster } from "sonner"; + +import { useTheme } from "@/lib/hooks/use-theme"; + +interface ToastProviderProps { + children: React.ReactNode; +} + +const ToastProvider = ({ children }: ToastProviderProps) => { + const { theme } = useTheme(); + + return ( + <> + + {children} + + ); +}; + +export default ToastProvider; diff --git a/client/src/lib/services/auth-service.ts b/client/src/lib/services/auth-service.ts new file mode 100644 index 0000000..8701df5 --- /dev/null +++ b/client/src/lib/services/auth-service.ts @@ -0,0 +1,17 @@ +import { SignUpDTO } from "@/lib/DTO/sign-up.dto"; +import { AppRoutes } from "@/constants/routes"; + +const baseURL = + import.meta.env.VITE_ENV === "development" + ? `${import.meta.env.VITE_BASE_URL}/api/auth` + : "/api/auth"; + +export const AuthService = { + signUp: async (signUp: SignUpDTO) => { + return await fetch(`${baseURL}${AppRoutes.SignUp}`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(signUp), + }); + }, +}; diff --git a/client/src/lib/utils/validations.ts b/client/src/lib/utils/validations.ts new file mode 100644 index 0000000..d0fff85 --- /dev/null +++ b/client/src/lib/utils/validations.ts @@ -0,0 +1,36 @@ +import { + EmailRegex, + LowerCaseRegex, + NumberRegex, + SpecialRegex, + UpperCaseRegex, +} from "@/constants/regex"; + +export const ValidateUsername = (username: string) => + username.length > 3 && username.length <= 20; + +export const ValidateEmail = (email: string) => EmailRegex.test(email); + +export const ValidatePassword = (password: string) => { + const criteria = [ + password.length >= 6, + UpperCaseRegex.test(password), + LowerCaseRegex.test(password), + NumberRegex.test(password), + SpecialRegex.test(password), + ]; + + return criteria.every((criterion) => criterion); +}; + +export const ValidateConfirmPassword = ({ + pass1, + pass2, +}: { + pass1: string; + pass2: string; +}) => { + if (pass1 !== pass2) return false; + + return true; +}; diff --git a/client/src/main.tsx b/client/src/main.tsx index 23b7314..57a3552 100644 --- a/client/src/main.tsx +++ b/client/src/main.tsx @@ -3,6 +3,8 @@ import { createRoot } from "react-dom/client"; import { StrictMode } from "react"; import { ThemeProvider } from "@/lib/providers/theme-provider.tsx"; +import QueryProvider from "@/lib/providers/query-provider.tsx"; +import ToastProvider from "@/lib/providers/toast-provider.tsx"; import App from "./App.tsx"; import "./index.css"; @@ -10,9 +12,13 @@ import "./index.css"; createRoot(document.getElementById("root")!).render( - - - + + + + + + + ); diff --git a/client/src/components/header/index.tsx b/client/src/pages/root/_components/header/index.tsx similarity index 100% rename from client/src/components/header/index.tsx rename to client/src/pages/root/_components/header/index.tsx diff --git a/client/src/components/header/logo.tsx b/client/src/pages/root/_components/header/logo.tsx similarity index 100% rename from client/src/components/header/logo.tsx rename to client/src/pages/root/_components/header/logo.tsx diff --git a/client/src/components/header/toggle-mode.tsx b/client/src/pages/root/_components/header/toggle-mode.tsx similarity index 100% rename from client/src/components/header/toggle-mode.tsx rename to client/src/pages/root/_components/header/toggle-mode.tsx diff --git a/client/src/pages/root/page.tsx b/client/src/pages/root/page.tsx new file mode 100644 index 0000000..f734d65 --- /dev/null +++ b/client/src/pages/root/page.tsx @@ -0,0 +1,14 @@ +import { Header } from "./_components/header"; + +const RootPage = () => { + return ( + <> +
+
+

Root Page

+
+ + ); +}; + +export default RootPage; diff --git a/client/src/pages/sign-up/page.tsx b/client/src/pages/sign-up/page.tsx new file mode 100644 index 0000000..17eb3b8 --- /dev/null +++ b/client/src/pages/sign-up/page.tsx @@ -0,0 +1,81 @@ +import { useMutation } from "@tanstack/react-query"; +import { useNavigate } from "react-router-dom"; +import { FormEvent, useState } from "react"; +import { toast } from "sonner"; + +import { AuthService } from "@/lib/services/auth-service"; +import { SIGNUPINPUTS } from "@/constants/collections"; +import { SignUpDTO } from "@/lib/DTO/sign-up.dto"; +import { AppRoutes } from "@/constants/routes"; +import { Button } from "@/components/button"; +import { SIGNUPKEY } from "@/constants/keys"; +import { Input } from "@/components/input"; + +const SignUpPage = () => { + const [confirmPassword, setConfirmPassword] = useState(""); + const navigate = useNavigate(); + + const { mutate, isPending } = useMutation({ + mutationKey: [SIGNUPKEY], + mutationFn: AuthService.signUp, + onSuccess: () => { + toast.success("Registered successfully"); + navigate(AppRoutes.Root); + }, + onError: ({ message }) => toast.error(message || "Unable to register"), + }); + + const onSubmit = (e: FormEvent) => { + e.preventDefault(); + + const formData = new FormData(e.currentTarget); + + const signUpData = Object.fromEntries(formData.entries()) as SignUpDTO; + + mutate(signUpData); + }; + + return ( +
+
+

+ Create Account +

+ + {SIGNUPINPUTS.map((s) => ( + { + if (s.name === "password") { + setConfirmPassword(value); + } + if (s.name === undefined) { + return s.validation({ + pass1: value, + pass2: confirmPassword, + }); + } + + return s.validation(value); + }} + /> + ))} + +
+ ); +}; + +export default SignUpPage; diff --git a/client/src/routes/root/page.tsx b/client/src/routes/root/page.tsx deleted file mode 100644 index 9d88029..0000000 --- a/client/src/routes/root/page.tsx +++ /dev/null @@ -1,9 +0,0 @@ -const RootPage = () => { - return ( -
-

Root Page

-
- ); -}; - -export default RootPage; diff --git a/client/tailwind.config.js b/client/tailwind.config.js index 35e4da2..30fe5d1 100644 --- a/client/tailwind.config.js +++ b/client/tailwind.config.js @@ -27,8 +27,8 @@ export default { "0 0px 50px rgb(var(--secondary))", ], "accent-glow": [ - "0 0px 25px rgb(var(--accent))", - "0 0px 50px rgb(var(--accent))", + "0 0px 15px rgb(var(--accent))", + "0 0px 60px rgb(var(--accent))", ], }, },