/* eslint-disable react-refresh/only-export-components */
import React, { createContext, useContext, useState, useEffect } from 'react'
import { authService } from '@/services/authService'
import PropTypes from 'prop-types'
import { useMutation } from '@tanstack/react-query'
const AuthContext = createContext(undefined)
export const AuthProvider = ({ children }) => {
const [user, setUser] = useState(authService.getUser())
const [isLoading, setIsLoading] = useState(true)
// On first load, restore user if present
useEffect(() => {
const storedUser = authService.getUser()
setUser(storedUser)
setIsLoading(false)
}, [])
const loginMutation = useMutation({
mutationFn: async (credentials) => {
const userData = await authService.login(credentials)
setUser(userData)
return userData
},
})
const logoutMutation = useMutation({
mutationFn: async () => {
await authService.logout()
setUser(null)
},
})
const signupMutation = useMutation({
mutationFn: async (credentials) => {
const userData = await authService.signup(credentials)
setUser(userData)
return userData
},
})
const changePasswordMutation = useMutation({
mutationFn: async (data) => {
return await authService.changePassword(data)
},
})
return (
<AuthContext.Provider
value={{
user: user,
isAuthenticated: !!user,
isLoading,
login: loginMutation.mutateAsync,
logout: logoutMutation.mutateAsync,
signup: signupMutation.mutateAsync,
changePassword: changePasswordMutation.mutateAsync,
setUser,
}}
>
{children}
</AuthContext.Provider>
)
}
AuthProvider.propTypes = {
children: PropTypes.node.isRequired,
}
export const useAuth = () => {
const context = useContext(AuthContext)
if (context === undefined) {
throw new Error('useAuth must be used within an AuthProvider')
}
return context
}
------------------- AUTH SERVICE-------------------------
import api from './api'
import Cookies from 'js-cookie'
const USER_KEY = 'user' // localStorage key for storing user data
export const authService = {
// Login with email & password
async login(credentials) {
try {
const response = await api.post('/auth/login', credentials, {
withCredentials: true, // important: to include cookies
})
// console.log('Data from backend', response.data.user)
this.setUser(response.data.user)
return response.data.user
} catch (error) {
const message = error.response?.data?.message || 'Login failed!'
console.error('Login error:', error)
throw new Error(message)
}
},
// Signup
async signup(data) {
try {
const response = await api.post('/auth/signup', data, {
withCredentials: true,
})
this.setUser(response.data.user)
return response.data.user
} catch (error) {
const message = error.response?.data?.message || 'Signup failed!'
console.error('Signup error:', message)
throw new Error(message)
}
},
// Logout
async logout() {
try {
await api.post('/auth/logout', {}, { withCredentials: true })
this.clearUser()
} catch (error) {
console.error('Logout error:', error.message)
}
},
// Forgot Password
async forgotPassword(email) {
try {
const res = await api.post('/auth/forgot-password', { email })
return res.data
} catch (error) {
const message = error.response?.data?.message || 'Request failed'
throw new Error(message)
}
},
// Reset Password
async resetPassword(token, data) {
try {
const res = await api.post(`/auth/reset-password/${token}`, data)
return res.data
} catch (error) {
const message = error.response?.data?.message || 'Reset failed'
throw new Error(message)
}
},
// Change Password (authenticated)
async changePassword(data) {
try {
const res = await api.put('/auth/change-password', data, {
withCredentials: true,
})
return res.data
} catch (error) {
const message = error.response?.data?.message || 'Password change failed'
throw new Error(message)
}
},
// Set user in localStorage
setUser(userData) {
localStorage.setItem(USER_KEY, JSON.stringify(userData))
},
getUser() {
const userStr = localStorage.getItem(USER_KEY)
try {
return userStr ? JSON.parse(userStr) : null
} catch (err) {
console.error('Error parsing user data', err)
return null
}
},
clearUser() {
localStorage.removeItem(USER_KEY)
},
isAuthenticated() {
return !!Cookies.get('token') // JWT cookie present?
},
}
------------------------ API.JS---------------------------
import axios from 'axios'
import Cookies from 'js-cookie'
// const API_URL = import.meta.env.VITE_API_URL || 'http://localhost:4000/api'
const API_URL = import.meta.env.VITE_API_URL || 'https://customerapi.mendt.in/api'
if (!import.meta.env.VITE_API_URL) {
console.warn('VITE_API_URL is not set, using default API URL.')
}
const api = axios.create({
baseURL: API_URL,
withCredentials: true,
// headers: {
// "Content-Type": "application/json",
// },
})
// Request interceptor for API calls
api.interceptors.request.use(
(config) => {
const token = Cookies.get('token')
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
return config
},
(error) => {
return Promise.reject(error)
}
)
// Response interceptor for API calls
api.interceptors.response.use(
(response) => {
return response
},
async (error) => {
const originalRequest = error.config
// Handle token expiration and refresh logic here if needed
if (error.response?.status === 401 && !originalRequest._retry) {
originalRequest._retry = true
// A dd logic to refresh the token or redirect to login
}
return Promise.reject(error)
}
)
export default api
---------------------- jsconfig.json-----------------------
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
}
}
-------------------------QueryProvider.jsx---------------------
import React from "react";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
import PropTypes from "prop-types";
const queryClient = new QueryClient({
defaultOptions: {
queries: {
refetchOnWindowFocus: false,
retry: 1,
staleTime: 1000 * 60 * 5, // 5 minutes
},
},
});
export const QueryProvider = ({ children }) => {
return (
<QueryClientProvider client={queryClient}>
{children}
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
);
};
QueryProvider.propTypes = {
children: PropTypes.node.isRequired,
};
--------------------------SIGNUP--------------------------------
import React from 'react';
import styled from 'styled-components';
import { motion } from 'framer-motion';
import { useNavigate } from 'react-router-dom';
import { useAuth } from '@/contexts/AuthContext';
import { useForm } from 'react-hook-form';
import toast from 'react-hot-toast';
import { InputGroup } from './Dashboard/CreateAccountStyles';
import { FaSpinner } from 'react-icons/fa';
// Design tokens
const colors = {
primary: '#667eea',
primaryDark: '#5a67d8',
secondary: '#764ba2',
success: '#48bb78',
error: '#ff6b6b',
warning: '#f6ad55',
text: '#1a202c',
textSecondary: '#4a5568',
textMuted: '#a0aec0',
background: 'rgba(255, 255, 255, 0.95)',
border: 'rgba(102, 126, 234, 0.2)',
borderHover: '#667eea',
glass: 'rgba(255, 255, 255, 0.95)',
glassLight: 'rgba(255, 255, 255, 0.8)',
shadow: 'rgba(0, 0, 0, 0.1)',
shadowHover: 'rgba(102, 126, 234, 0.1)'
};
const gradients = {
primary: `linear-gradient(135deg, ${colors.primary}, ${colors.secondary})`,
success: `linear-gradient(135deg, ${colors.success}, #38b2ac)`,
error: `linear-gradient(135deg, ${colors.error}, #ee5a52)`,
warning: `linear-gradient(135deg, ${colors.warning}, #ff8c00)`
};
const spacing = {
xs: '4px',
sm: '8px',
md: '12px',
lg: '16px',
xl: '20px',
xxl: '24px',
xxxl: '32px'
};
// Styled Components
const Container = styled.div`
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: ${spacing.xl};
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
position: relative;
&::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background:
radial-gradient(circle at 20% 80%, rgba(120, 119, 198, 0.3) 0%, transparent 50%),
radial-gradient(circle at 80% 20%, rgba(255, 255, 255, 0.1) 0%, transparent 50%),
radial-gradient(circle at 40% 40%, rgba(120, 119, 198, 0.2) 0%, transparent 50%);
}
`;
const FormWrapper = styled(motion.div)`
background: ${colors.glass};
backdrop-filter: blur(20px);
border-radius: 20px;
padding: ${spacing.xxxl};
box-shadow: 0 8px 32px ${colors.shadow};
border: 1px solid rgba(255, 255, 255, 0.2);
position: relative;
overflow: hidden;
width: 100%;
max-width: 450px;
z-index: 1;
&::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 4px;
background: ${gradients.primary};
}
@media (max-width: 768px) {
padding: ${spacing.xxl};
max-width: 100%;
}
`;
const Title = styled.h1`
font-size: 32px;
font-weight: 800;
color: ${colors.text};
margin-bottom: ${spacing.sm};
background: ${gradients.primary};
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
text-align: center;
line-height: 1.2;
@media (max-width: 768px) {
font-size: 28px;
}
`;
const Subtitle = styled.p`
font-size: 16px;
color: ${colors.textSecondary};
text-align: center;
margin-bottom: ${spacing.xxxl};
font-weight: 500;
line-height: 1.4;
`;
const Form = styled.form`
display: flex;
flex-direction: column;
gap: ${spacing.lg};
`;
const StyledInputGroup = styled.div`
display: flex;
flex-direction: column;
gap: ${spacing.xs};
`;
const Input = styled.input`
width: 100%;
padding: ${spacing.lg} ${spacing.xl};
border: 2px solid ${props => props.errors ? colors.error : colors.border};
border-radius: 12px;
font-size: 16px;
font-weight: 500;
color: ${colors.text};
background: ${colors.glassLight};
backdrop-filter: blur(10px);
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
box-sizing: border-box;
&:focus {
outline: none;
border-color: ${props => props.errors ? colors.error : colors.borderHover};
box-shadow: 0 0 0 4px ${props => props.errors ? 'rgba(255, 107, 107, 0.1)' : colors.shadowHover};
background: ${colors.background};
}
&::placeholder {
color: ${colors.textMuted};
}
&:-webkit-autofill {
-webkit-box-shadow: 0 0 0 30px ${colors.background} inset;
-webkit-text-fill-color: ${colors.text};
}
`;
const ErrorMessage = styled.p`
color: ${colors.error};
font-size: 14px;
margin: 0;
font-weight: 500;
margin-top: ${spacing.xs};
`;
const Button = styled(motion.button)`
width: 100%;
padding: ${spacing.lg} ${spacing.xl};
border-radius: 12px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
border: none;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
background: ${gradients.primary};
color: white;
box-shadow: 0 4px 16px rgba(102, 126, 234, 0.4);
display: flex;
align-items: center;
justify-content: center;
gap: ${spacing.sm};
margin-top: ${spacing.md};
&:hover:not(:disabled) {
transform: translateY(-2px);
box-shadow: 0 8px 25px rgba(102, 126, 234, 0.5);
}
&:active {
transform: translateY(0);
}
&:disabled {
opacity: 0.6;
cursor: not-allowed;
}
svg {
animation: spin 1s linear infinite;
}
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
`;
const LoginText = styled.p`
text-align: center;
margin-top: ${spacing.xxxl};
color: ${colors.textSecondary};
font-size: 14px;
font-weight: 500;
`;
const LoginLink = styled.a`
color: ${colors.primary};
text-decoration: none;
font-weight: 600;
margin-left: ${spacing.xs};
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
&:hover {
color: ${colors.primaryDark};
text-decoration: underline;
}
`;
function Signup() {
const navigate = useNavigate();
const { signup } = useAuth();
const { register, handleSubmit, formState: { errors, isSubmitting } } = useForm();
const onSubmit = async (data) => {
try {
const userData = await signup({
name: data.name,
email: data.email,
password: data.password,
confirmPassword: data.confirmPassword
});
toast.success("Signed up successfully");
setTimeout(() => {
navigate("/dashboard/profile");
}, 1000);
} catch (error) {
console.error("Signup error", error);
toast.error(error.response?.data?.message || error.message || "Signup failed. Please try again!");
}
}
return (
<Container>
<FormWrapper
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, ease: 'easeOut' }}
aria-label="Signup form"
>
<Title>Banking Application</Title>
<Subtitle>Create Your Account</Subtitle>
<Form onSubmit={handleSubmit(onSubmit)}>
<StyledInputGroup>
<Input
id="name"
type="text"
placeholder="Full Name"
errors={errors.name}
{...register('name')}
/>
{errors.name && (
<ErrorMessage>
{errors.name.message}
</ErrorMessage>
)}
</StyledInputGroup>
<StyledInputGroup>
<Input
id="email"
type="text"
placeholder="Email"
errors={errors.email}
{...register('email')}
/>
{errors.email && (
<ErrorMessage>
{errors.email.message}
</ErrorMessage>
)}
</StyledInputGroup>
<StyledInputGroup>
<Input
type="password"
id="password"
placeholder="Password"
errors={errors.password}
{...register('password')}
/>
{errors.password && (
<ErrorMessage>
{errors.password.message}
</ErrorMessage>
)}
</StyledInputGroup>
<StyledInputGroup>
<Input
type="password"
id="confirmPassword"
placeholder="Confirm Password"
errors={errors.confirmPassword}
{...register('confirmPassword')}
/>
{errors.confirmPassword && (
<ErrorMessage>
{errors.confirmPassword.message}
</ErrorMessage>
)}
</StyledInputGroup>
<Button
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
type="submit"
disabled={isSubmitting}
>
{isSubmitting ? <FaSpinner /> : "Sign Up"}
</Button>
</Form>
<LoginText>
Already have an account?
<LoginLink href="/login"> Log in</LoginLink>
</LoginText>
</FormWrapper>
</Container>
);
}
export default Signup;
Top comments (0)