Skip to content

Commit

Permalink
Create signup page
Browse files Browse the repository at this point in the history
Consume sign up auth api
  • Loading branch information
Fingertips18 committed Sep 12, 2024
1 parent 145008d commit c45cf6d
Show file tree
Hide file tree
Showing 22 changed files with 431 additions and 22 deletions.
40 changes: 39 additions & 1 deletion client/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
8 changes: 4 additions & 4 deletions client/src/App.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<main className="h-dvh">
<Header />
<main className="h-dvh overflow-x-hidden">
<Routes>
<Route path={AppRoutes.Root} element={<RootPage />} />
<Route path={AppRoutes.SignUp} element={<SignUpPage />} />
</Routes>
</main>
);
Expand Down
25 changes: 25 additions & 0 deletions client/src/components/button.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<button
type={type}
onClick={onClick}
disabled={disabled || loading}
className="w-full h-10 p-6 rounded-lg bg-accent transition-all hover:drop-shadow-accent-glow border-2 border-transparent hover:border-white/50
flex-center font-bold text-lg active:scale-90 disabled:bg-accent/50 disabled:text-foreground/50 disabled:pointer-events-none"
>
{loading ? <Loader2 className="animate-spin duration-200" /> : label}
</button>
);
};

export { Button };
98 changes: 98 additions & 0 deletions client/src/components/input.tsx
Original file line number Diff line number Diff line change
@@ -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<LucideProps, "ref"> & RefAttributes<SVGSVGElement>
>;
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 (
<div className="space-y-1.5 flex flex-col">
<div className="flex-between px-1.5">
<label
className="text-sm font-semibold uppercase"
htmlFor={name?.toLowerCase() ?? label.toLowerCase()}
>
{label}
</label>
<Info size={16} className="text-primary" />
</div>
<div className="relative flex-center">
<input
value={value}
onChange={(e) => 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"
}`}
/>
<div className="absolute inset-y-0 left-0 flex-center pl-3 pointer-events-none">
<SuffixIcon size={24} className="text-primary" />
</div>
{isPasswod && (
<button
type="button"
onClick={() => setObscure(!obscure)}
className="absolute inset-y-0 right-0 flex-center pr-3"
>
{obscure ? (
<EyeOff size={24} className="text-primary" />
) : (
<Eye size={24} className="text-primary" />
)}
</button>
)}
</div>
</div>
);
};

export { Input };
46 changes: 46 additions & 0 deletions client/src/constants/collections.ts
Original file line number Diff line number Diff line change
@@ -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. [email protected]",
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,
},
];
1 change: 1 addition & 0 deletions client/src/constants/keys.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const SIGNUPKEY = "sign-up";
14 changes: 14 additions & 0 deletions client/src/constants/regex.ts
Original file line number Diff line number Diff line change
@@ -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,
};
4 changes: 2 additions & 2 deletions client/src/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
5 changes: 5 additions & 0 deletions client/src/lib/DTO/sign-up.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export type SignUpDTO = {
username: string;
email: string;
password: string;
};
15 changes: 15 additions & 0 deletions client/src/lib/providers/query-provider.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
);
};

export default QueryProvider;
20 changes: 20 additions & 0 deletions client/src/lib/providers/toast-provider.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<>
<Toaster richColors position="top-center" theme={theme} />
{children}
</>
);
};

export default ToastProvider;
17 changes: 17 additions & 0 deletions client/src/lib/services/auth-service.ts
Original file line number Diff line number Diff line change
@@ -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),
});
},
};
36 changes: 36 additions & 0 deletions client/src/lib/utils/validations.ts
Original file line number Diff line number Diff line change
@@ -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;
};
Loading

0 comments on commit c45cf6d

Please sign in to comment.