Browse Source

login changes

master
Prakash Maity 6 months ago
parent
commit
fbbbfb8f67
7 changed files with 236 additions and 17 deletions
  1. +9
    -1
      package.json
  2. +50
    -6
      src/app/(auth)/log-in/page.tsx
  3. +24
    -0
      src/services/api/loginApi.ts
  4. +88
    -0
      src/services/axios/axiosInstance.ts
  5. +35
    -0
      src/ui/CustomTextField.tsx
  6. +26
    -8
      src/ui/CustomizedInputsStyled.tsx
  7. +4
    -2
      src/ui/customizedButtons.tsx

+ 9
- 1
package.json View File

@ -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"
}
}

+ 50
- 6
src/app/(auth)/log-in/page.tsx View File

@ -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
- 0
src/services/api/loginApi.ts View 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
- 0
src/services/axios/axiosInstance.ts View 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
- 0
src/ui/CustomTextField.tsx View 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;

+ 26
- 8
src/ui/CustomizedInputsStyled.tsx View File

@ -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
/>
)}
/>
);
}

+ 4
- 2
src/ui/customizedButtons.tsx View File

@ -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…
Cancel
Save