Merge branch 'master' of https://git.sentientgeeks.us/Amit/mui-demo-project
This commit is contained in:
commit
b8523bea88
12
package.json
12
package.json
@ -12,23 +12,33 @@
|
||||
"@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": "^5.16.6",
|
||||
"@mui/material-nextjs": "^5.16.6",
|
||||
"@reduxjs/toolkit": "^2.2.7",
|
||||
"@tanstack/react-table": "^8.20.1",
|
||||
"axios": "^1.7.3",
|
||||
"moment": "^2.30.1",
|
||||
"next": "14.2.5",
|
||||
"react": "^18",
|
||||
"react-dom": "^18",
|
||||
"sass": "^1.77.8"
|
||||
"react-hook-form": "^7.52.2",
|
||||
"react-redux": "^9.1.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,46 @@ 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";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { RootState } from "@/services/store";
|
||||
import { setAuthTokens, setUserDetails } from "@/services/store/authSlice";
|
||||
|
||||
const loginSchema = z.object({
|
||||
username: z.string(),
|
||||
password: z.string(),
|
||||
});
|
||||
|
||||
type LoginFormValues = z.infer<typeof loginSchema>;
|
||||
|
||||
export default function LoginPage() {
|
||||
const router = useRouter()
|
||||
const { register, handleSubmit, formState: { errors }, control } = useForm<LoginFormValues>({
|
||||
resolver: zodResolver(loginSchema),
|
||||
});
|
||||
const [error, setError] = useState('');
|
||||
const dispatch = useDispatch();
|
||||
const onSubmit = async (data: LoginFormValues) => {
|
||||
try {
|
||||
const response = await loginApi(data);
|
||||
localStorage.setItem('token', response.token);
|
||||
localStorage.setItem('refreshToken', response.refreshToken);
|
||||
dispatch(setAuthTokens({ token: response.token, refreshToken: response.refreshToken }));
|
||||
dispatch(setUserDetails(response));
|
||||
|
||||
router.push('/home');
|
||||
} catch (err) {
|
||||
setError('Invalid credentials');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<main className={styles.loginPage}>
|
||||
<Grid container sx={{ height: "100%" }}>
|
||||
@ -40,7 +79,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 +90,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"}
|
||||
|
@ -1,10 +1,14 @@
|
||||
"use client"
|
||||
import DashboardWrapper from "@/components/wrapper/dashboardWrapper";
|
||||
import AuthGuard from "@/hoc/authGuard/authGuard";
|
||||
|
||||
interface RootLayoutProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export default function RootLayout(props: RootLayoutProps): JSX.Element {
|
||||
function RootLayout(props: RootLayoutProps): JSX.Element {
|
||||
const { children } = props;
|
||||
return <DashboardWrapper>{children}</DashboardWrapper>;
|
||||
}
|
||||
|
||||
export default AuthGuard(RootLayout);
|
@ -1,7 +1,10 @@
|
||||
"use client"
|
||||
import { AppRouterCacheProvider } from "@mui/material-nextjs/v13-appRouter";
|
||||
import { ThemeProvider } from "@mui/material/styles";
|
||||
import theme from "../theme";
|
||||
import "./globals.scss";
|
||||
import { Provider } from 'react-redux';
|
||||
import store from "@/services/store";
|
||||
|
||||
interface RootLayoutProps {
|
||||
children: React.ReactNode;
|
||||
@ -12,9 +15,11 @@ export default function RootLayout(props: RootLayoutProps): JSX.Element {
|
||||
return (
|
||||
<html lang="en">
|
||||
<body>
|
||||
<AppRouterCacheProvider>
|
||||
<ThemeProvider theme={theme}>{children}</ThemeProvider>
|
||||
</AppRouterCacheProvider>
|
||||
<Provider store={store}>
|
||||
<AppRouterCacheProvider>
|
||||
<ThemeProvider theme={theme}>{children}</ThemeProvider>
|
||||
</AppRouterCacheProvider>
|
||||
</Provider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
|
@ -1,5 +1,4 @@
|
||||
"use client"; // This ensures the file is a client component
|
||||
|
||||
import * as React from "react";
|
||||
import AppBar, { AppBarProps } from "@mui/material/AppBar";
|
||||
import Box from "@mui/material/Box";
|
||||
@ -12,7 +11,6 @@ import ListItem from "@mui/material/ListItem";
|
||||
import ListItemButton from "@mui/material/ListItemButton";
|
||||
import ListItemIcon from "@mui/material/ListItemIcon";
|
||||
import ListItemText from "@mui/material/ListItemText";
|
||||
import MailIcon from "@mui/icons-material/Mail";
|
||||
import MenuIcon from "@mui/icons-material/Menu";
|
||||
import Toolbar from "@mui/material/Toolbar";
|
||||
import Typography from "@mui/material/Typography";
|
||||
@ -22,6 +20,7 @@ import styles from "./DashboardWrapper.module.scss";
|
||||
import styled from "@emotion/styled";
|
||||
import { BUILDING, GEAR } from "@/utilities/svgConstant";
|
||||
import LogoutIcon from "@mui/icons-material/Logout";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
const drawerWidth = 260;
|
||||
|
||||
@ -75,7 +74,7 @@ export default function DashboardWrapper(props: Props) {
|
||||
const { children: Children } = props;
|
||||
const [mobileOpen, setMobileOpen] = useState(false);
|
||||
const [isClosing, setIsClosing] = useState(false);
|
||||
|
||||
const router = useRouter()
|
||||
const handleDrawerClose = () => {
|
||||
setIsClosing(true);
|
||||
setMobileOpen(false);
|
||||
@ -125,6 +124,12 @@ export default function DashboardWrapper(props: Props) {
|
||||
const container =
|
||||
window !== undefined ? () => window().document.body : undefined;
|
||||
|
||||
const logoutHandler = () => {
|
||||
localStorage.removeItem('token');
|
||||
localStorage.removeItem('refreshToken');
|
||||
router.push('/log-in');
|
||||
}
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{ display: "flex" }}
|
||||
@ -176,15 +181,8 @@ export default function DashboardWrapper(props: Props) {
|
||||
alt="Remy Sharp"
|
||||
src="/static/images/avatar/1.jpg"
|
||||
/>
|
||||
{/* <GEAR
|
||||
sx={{
|
||||
color: "var(--primary_Active_text)",
|
||||
fontSize: "1.8rem",
|
||||
cursor: "pointer",
|
||||
}}
|
||||
/> */}
|
||||
<IconButton aria-label="log out" size="medium" color="error">
|
||||
<LogoutIcon fontSize="small" />
|
||||
<LogoutIcon fontSize="small" onClick={logoutHandler} />
|
||||
</IconButton>
|
||||
</Box>
|
||||
</Box>
|
||||
|
31
src/hoc/authGuard/authGuard.tsx
Normal file
31
src/hoc/authGuard/authGuard.tsx
Normal file
@ -0,0 +1,31 @@
|
||||
"use client"
|
||||
// hoc/withAuth.tsx
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
const AuthGuard = (WrappedComponent: any) => {
|
||||
const AuthGuardComponent = (props: any) => {
|
||||
const router = useRouter();
|
||||
|
||||
useEffect(() => {
|
||||
const token = localStorage.getItem('token');
|
||||
if (!token) {
|
||||
router.push('/log-in');
|
||||
}
|
||||
}, [router]);
|
||||
|
||||
return <WrappedComponent {...props} />;
|
||||
};
|
||||
|
||||
// Set display name for the component
|
||||
AuthGuardComponent.displayName = `AuthGuard(${getDisplayName(WrappedComponent)})`;
|
||||
|
||||
return AuthGuardComponent;
|
||||
};
|
||||
|
||||
export default AuthGuard;
|
||||
|
||||
// Helper function to get the display name of a component
|
||||
function getDisplayName(WrappedComponent: any) {
|
||||
return WrappedComponent.displayName || WrappedComponent.name || 'Component';
|
||||
}
|
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';
|
||||
|
||||
export 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;
|
57
src/services/store/authSlice.ts
Normal file
57
src/services/store/authSlice.ts
Normal file
@ -0,0 +1,57 @@
|
||||
// store/slices/authSlice.ts
|
||||
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
|
||||
import { LoginResponse } from '../api/loginApi';
|
||||
|
||||
interface AuthState {
|
||||
token: string | null;
|
||||
refreshToken: string | null;
|
||||
id: number;
|
||||
username: string;
|
||||
email: string;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
gender: string;
|
||||
image: string;
|
||||
}
|
||||
|
||||
const initialState: AuthState = {
|
||||
token: null,
|
||||
refreshToken: null,
|
||||
id: 0,
|
||||
username: '',
|
||||
email: '',
|
||||
firstName: '',
|
||||
lastName: '',
|
||||
gender: '',
|
||||
image: '',
|
||||
};
|
||||
|
||||
const authSlice = createSlice({
|
||||
name: 'auth',
|
||||
initialState,
|
||||
reducers: {
|
||||
setAuthTokens: (
|
||||
state,
|
||||
action: PayloadAction<{ token: string; refreshToken: string }>
|
||||
) => {
|
||||
state.token = action.payload.token;
|
||||
state.refreshToken = action.payload.refreshToken;
|
||||
localStorage.setItem('token', action.payload.token);
|
||||
localStorage.setItem('refreshToken', action.payload.refreshToken);
|
||||
},
|
||||
clearAuthTokens: (state) => {
|
||||
state.token = null;
|
||||
state.refreshToken = null;
|
||||
localStorage.removeItem('token');
|
||||
localStorage.removeItem('refreshToken');
|
||||
},
|
||||
setUserDetails: (state, action: PayloadAction<LoginResponse>) => {
|
||||
state = action.payload;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const { setAuthTokens, clearAuthTokens, setUserDetails } =
|
||||
authSlice.actions;
|
||||
|
||||
export default authSlice.reducer;
|
15
src/services/store/index.tsx
Normal file
15
src/services/store/index.tsx
Normal file
@ -0,0 +1,15 @@
|
||||
|
||||
// store/index.ts
|
||||
import { configureStore } from '@reduxjs/toolkit';
|
||||
import authReducer from './authSlice';
|
||||
|
||||
const store = configureStore({
|
||||
reducer: {
|
||||
auth: authReducer,
|
||||
},
|
||||
});
|
||||
|
||||
export type RootState = ReturnType<typeof store.getState>;
|
||||
export type AppDispatch = typeof store.dispatch;
|
||||
|
||||
export default store;
|
58
src/ui/CustomTextField.tsx
Normal file
58
src/ui/CustomTextField.tsx
Normal file
@ -0,0 +1,58 @@
|
||||
// components/CustomTextField.tsx
|
||||
import React from 'react';
|
||||
import TextField from '@mui/material/TextField';
|
||||
import { Controller } from 'react-hook-form';
|
||||
import { styled } from "@mui/material/styles";
|
||||
|
||||
interface CustomTextFieldProps {
|
||||
name: string;
|
||||
control: any;
|
||||
label: string;
|
||||
type?: string;
|
||||
error?: boolean;
|
||||
helperText?: string;
|
||||
}
|
||||
|
||||
// Define your styled component
|
||||
const CssTextField = styled(TextField)({
|
||||
width: "100%",
|
||||
"& label.Mui-focused": {
|
||||
color: "var(--primary)",
|
||||
},
|
||||
"& .MuiInputBase-root": {
|
||||
borderRadius: 8,
|
||||
},
|
||||
"& .MuiOutlinedInput-root": {
|
||||
"& fieldset": {
|
||||
borderColor: "var(--input-border-default)",
|
||||
},
|
||||
"&:hover fieldset": {
|
||||
borderColor: "var(--input-border-hover)",
|
||||
},
|
||||
"&.Mui-focused fieldset": {
|
||||
borderColor: "var(--primary)",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const CustomTextField: React.FC<CustomTextFieldProps> = ({ name, control, label, type = 'text', error, helperText }) => {
|
||||
return (
|
||||
<Controller
|
||||
name={name}
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<CssTextField
|
||||
{...field}
|
||||
label={label}
|
||||
type={type}
|
||||
error={error}
|
||||
helperText={helperText}
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default CustomTextField;
|
@ -47,6 +47,7 @@ export default function CustomizedInputsStyled({
|
||||
type={type}
|
||||
id="custom-css-outlined-input"
|
||||
InputProps={{ endAdornment: endAdornmentBoolean && <VisibilityOff /> }}
|
||||
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
@ -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