DEV Community

Nebula
Nebula

Posted on

frontend4

/* 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
}

Enter fullscreen mode Exit fullscreen mode

------------------- 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?
  },
}

Enter fullscreen mode Exit fullscreen mode

------------------------ 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

Enter fullscreen mode Exit fullscreen mode

---------------------- jsconfig.json-----------------------

{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@/*": ["./src/*"]
    }
  }
}

Enter fullscreen mode Exit fullscreen mode

-------------------------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,
};

Enter fullscreen mode Exit fullscreen mode

--------------------------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;
Enter fullscreen mode Exit fullscreen mode

Top comments (0)