login changes
This commit is contained in:
parent
3088141451
commit
fbbbfb8f67
10
package.json
10
package.json
@ -12,19 +12,27 @@
|
||||
"@emotion/cache": "^11.13.1",
|
||||
"@emotion/react": "^11.13.0",
|
||||
"@emotion/styled": "^11.13.0",
|
||||
"@hookform/resolvers": "^3.9.0",
|
||||
"@mui/icons-material": "^5.16.6",
|
||||
"@mui/material-nextjs": "^5.16.6",
|
||||
"next": "14.2.5",
|
||||
"react": "^18",
|
||||
"react-dom": "^18",
|
||||
"sass": "^1.77.8"
|
||||
"react-hook-form": "^7.52.2",
|
||||
"sass": "^1.77.8",
|
||||
"zod": "^3.23.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@testing-library/jest-dom": "^6.4.8",
|
||||
"@testing-library/react": "^16.0.0",
|
||||
"@testing-library/user-event": "^14.5.2",
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^18",
|
||||
"@types/react-dom": "^18",
|
||||
"axios-mock-adapter": "^2.0.0",
|
||||
"eslint": "^8",
|
||||
"eslint-config-next": "14.2.5",
|
||||
"jest": "^29.7.0",
|
||||
"typescript": "^5"
|
||||
}
|
||||
}
|
||||
|
@ -1,3 +1,4 @@
|
||||
"use client"
|
||||
import React from "react";
|
||||
import styles from "./loginPage.module.scss";
|
||||
import Box from "@mui/material/Box";
|
||||
@ -7,8 +8,42 @@ import { UTILITY_CONSTANT } from "@/utilities/utilityConstant";
|
||||
import Link from "next/link";
|
||||
import CustomizedInputsStyled from "@/ui/CustomizedInputsStyled";
|
||||
import CustomizedButtons from "@/ui/customizedButtons";
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { z } from 'zod';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { useState } from 'react';
|
||||
import { loginApi } from "@/services/api/loginApi";
|
||||
import { FormControl } from '@mui/material';
|
||||
import CustomTextField from "@/ui/CustomTextField";
|
||||
|
||||
const loginSchema = z.object({
|
||||
username: z.string(),
|
||||
password: z.string(),
|
||||
});
|
||||
|
||||
type LoginFormValues = z.infer<typeof loginSchema>;
|
||||
|
||||
export default function LoginPage() {
|
||||
|
||||
const { register, handleSubmit, formState: { errors }, control } = useForm<LoginFormValues>({
|
||||
resolver: zodResolver(loginSchema),
|
||||
});
|
||||
console.log("errors", errors)
|
||||
const [error, setError] = useState('');
|
||||
|
||||
const onSubmit = async (data: LoginFormValues) => {
|
||||
try {
|
||||
const response = await loginApi(data);
|
||||
localStorage.setItem('token', response.token);
|
||||
localStorage.setItem('refreshToken', response.refreshToken);
|
||||
console.log("🚀 ~ response @@@ @@@ @@@ @@@ @@@@:", response)
|
||||
|
||||
// router.push('/dashboard');
|
||||
} catch (err) {
|
||||
setError('Invalid credentials');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<main className={styles.loginPage}>
|
||||
<Grid container sx={{ height: "100%" }}>
|
||||
@ -40,7 +75,7 @@ export default function LoginPage() {
|
||||
paddingBlock={5}
|
||||
>
|
||||
<img src={UTILITY_CONSTANT.IMAGES.AUTH.HEADER_LOG} alt="logo" />
|
||||
<Box width={"100%"}>
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<Typography variant="h4">Welcome to Convexsol</Typography>
|
||||
<Typography variant="h4">Sign in to your account</Typography>
|
||||
<Box
|
||||
@ -51,17 +86,26 @@ export default function LoginPage() {
|
||||
flexDirection={"column"}
|
||||
gap={3}
|
||||
>
|
||||
<CustomizedInputsStyled label="User Name" type="text" />
|
||||
<CustomizedInputsStyled
|
||||
<CustomTextField
|
||||
name="username"
|
||||
control={control}
|
||||
label="Username"
|
||||
error={!!errors.username}
|
||||
helperText={errors.username?.message}
|
||||
/>
|
||||
<CustomTextField
|
||||
name="password"
|
||||
control={control}
|
||||
label="Password"
|
||||
type="password"
|
||||
endAdornmentBoolean
|
||||
error={!!errors.password}
|
||||
helperText={errors.password?.message}
|
||||
/>
|
||||
</Box>
|
||||
<Box width={"100%"} marginBlockStart={"20px"}>
|
||||
<CustomizedButtons label={"Sign In"} />
|
||||
<CustomizedButtons btnType="submit" label={"Sign In"} />
|
||||
</Box>
|
||||
</Box>
|
||||
</form>
|
||||
<Box
|
||||
width={"100%"}
|
||||
display={"flex"}
|
||||
|
24
src/services/api/loginApi.ts
Normal file
24
src/services/api/loginApi.ts
Normal file
@ -0,0 +1,24 @@
|
||||
// api.ts
|
||||
import axiosInstance from '../axios/axiosInstance';
|
||||
|
||||
interface LoginResponse {
|
||||
id: number;
|
||||
username: string;
|
||||
email: string;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
gender: string;
|
||||
image: string;
|
||||
token: string;
|
||||
refreshToken: string;
|
||||
}
|
||||
|
||||
interface LoginData {
|
||||
username: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
export const loginApi = async (data: LoginData): Promise<LoginResponse> => {
|
||||
const response = await axiosInstance.post<LoginResponse>('/auth/login', data);
|
||||
return response.data;
|
||||
};
|
88
src/services/axios/axiosInstance.ts
Normal file
88
src/services/axios/axiosInstance.ts
Normal file
@ -0,0 +1,88 @@
|
||||
// axiosInstance.ts
|
||||
import axios from 'axios';
|
||||
|
||||
const axiosInstance = axios.create({
|
||||
baseURL: process.env.NEXT_PUBLIC_API_URL,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
let isRefreshing = false;
|
||||
let failedQueue: any[] = [];
|
||||
|
||||
const processQueue = (error: any, token: string | null = null) => {
|
||||
failedQueue.forEach((prom) => {
|
||||
if (token) {
|
||||
prom.resolve(token);
|
||||
} else {
|
||||
prom.reject(error);
|
||||
}
|
||||
});
|
||||
|
||||
failedQueue = [];
|
||||
};
|
||||
|
||||
axiosInstance.interceptors.request.use(
|
||||
(config) => {
|
||||
const token = localStorage.getItem('token');
|
||||
if (token) {
|
||||
config.headers['Authorization'] = `Bearer ${token}`;
|
||||
}
|
||||
return config;
|
||||
},
|
||||
(error) => Promise.reject(error)
|
||||
);
|
||||
|
||||
axiosInstance.interceptors.response.use(
|
||||
(response) => response,
|
||||
async (error) => {
|
||||
const originalRequest = error.config;
|
||||
if (error.response.status === 401 && !originalRequest._retry) {
|
||||
if (isRefreshing) {
|
||||
return new Promise(function (resolve, reject) {
|
||||
failedQueue.push({ resolve, reject });
|
||||
})
|
||||
.then((token) => {
|
||||
originalRequest.headers['Authorization'] = 'Bearer ' + token;
|
||||
return axiosInstance(originalRequest);
|
||||
})
|
||||
.catch((err) => Promise.reject(err));
|
||||
}
|
||||
|
||||
originalRequest._retry = true;
|
||||
isRefreshing = true;
|
||||
|
||||
const refreshToken = localStorage.getItem('refreshToken');
|
||||
if (!refreshToken) {
|
||||
window.location.href = '/log-in';
|
||||
return Promise.reject(error);
|
||||
}
|
||||
|
||||
try {
|
||||
const { data } = await axios.post(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/auth/refresh`,
|
||||
{ token: refreshToken }
|
||||
);
|
||||
localStorage.setItem('token', data.token);
|
||||
localStorage.setItem('refreshToken', data.refreshToken);
|
||||
axiosInstance.defaults.headers['Authorization'] =
|
||||
'Bearer ' + data.token;
|
||||
processQueue(null, data.token);
|
||||
return axiosInstance(originalRequest);
|
||||
} catch (err) {
|
||||
processQueue(err, null);
|
||||
localStorage.removeItem('token');
|
||||
localStorage.removeItem('refreshToken');
|
||||
window.location.href = '/login';
|
||||
return Promise.reject(err);
|
||||
} finally {
|
||||
isRefreshing = false;
|
||||
}
|
||||
}
|
||||
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
export default axiosInstance;
|
35
src/ui/CustomTextField.tsx
Normal file
35
src/ui/CustomTextField.tsx
Normal file
@ -0,0 +1,35 @@
|
||||
// components/CustomTextField.tsx
|
||||
import React from 'react';
|
||||
import TextField from '@mui/material/TextField';
|
||||
import { Controller } from 'react-hook-form';
|
||||
|
||||
interface CustomTextFieldProps {
|
||||
name: string;
|
||||
control: any;
|
||||
label: string;
|
||||
type?: string;
|
||||
error?: boolean;
|
||||
helperText?: string;
|
||||
}
|
||||
|
||||
const CustomTextField: React.FC<CustomTextFieldProps> = ({ name, control, label, type = 'text', error, helperText }) => {
|
||||
return (
|
||||
<Controller
|
||||
name={name}
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<TextField
|
||||
{...field}
|
||||
label={label}
|
||||
type={type}
|
||||
error={error}
|
||||
helperText={helperText}
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default CustomTextField;
|
@ -5,6 +5,7 @@ import { styled } from "@mui/material/styles";
|
||||
import TextField from "@mui/material/TextField";
|
||||
import Visibility from "@mui/icons-material/Visibility";
|
||||
import VisibilityOff from "@mui/icons-material/VisibilityOff";
|
||||
import { Controller } from 'react-hook-form';
|
||||
|
||||
// Define your styled component
|
||||
const CssTextField = styled(TextField)({
|
||||
@ -32,6 +33,10 @@ type InputType = {
|
||||
label: string;
|
||||
type: string;
|
||||
endAdornmentBoolean?: React.ReactNode;
|
||||
name: string;
|
||||
control: any;
|
||||
error?: boolean;
|
||||
helperText?: string;
|
||||
};
|
||||
|
||||
// Client component
|
||||
@ -39,15 +44,28 @@ export default function CustomizedInputsStyled({
|
||||
label,
|
||||
type,
|
||||
endAdornmentBoolean,
|
||||
name,
|
||||
control,
|
||||
error,
|
||||
helperText,
|
||||
}: InputType) {
|
||||
return (
|
||||
<>
|
||||
<CssTextField
|
||||
label={label}
|
||||
type={type}
|
||||
id="custom-css-outlined-input"
|
||||
InputProps={{ endAdornment: endAdornmentBoolean && <VisibilityOff /> }}
|
||||
/>
|
||||
</>
|
||||
<Controller
|
||||
name={name}
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<CssTextField
|
||||
label={label}
|
||||
InputProps={{ endAdornment: endAdornmentBoolean && <VisibilityOff /> }}
|
||||
id="custom-css-outlined-input"
|
||||
{...field}
|
||||
type={type}
|
||||
error={error}
|
||||
helperText={helperText}
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -16,12 +16,14 @@ const ColorButton = styled(Button)<ButtonProps>(() => ({
|
||||
type InputType = {
|
||||
label: string;
|
||||
statIcon?: React.ReactNode;
|
||||
onPress?: () => void;
|
||||
btnType?: "submit" | "button";
|
||||
};
|
||||
|
||||
export default function CustomizedButtons({ label, statIcon }: InputType) {
|
||||
export default function CustomizedButtons({ label, statIcon, onPress, btnType = "button" }: InputType) {
|
||||
return (
|
||||
<>
|
||||
<ColorButton variant="contained">
|
||||
<ColorButton type={btnType} onClick={onPress} variant="contained">
|
||||
{statIcon && statIcon}
|
||||
{label}
|
||||
</ColorButton>
|
||||
|
Loading…
x
Reference in New Issue
Block a user