diff --git a/index.html b/index.html index f8617e6..8a21289 100644 --- a/index.html +++ b/index.html @@ -6,6 +6,9 @@ StellarAid + + +
diff --git a/package-lock.json b/package-lock.json index d92eacd..bac82be 100644 --- a/package-lock.json +++ b/package-lock.json @@ -83,7 +83,6 @@ "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", @@ -1805,7 +1804,6 @@ "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -1853,7 +1851,6 @@ "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -2016,7 +2013,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -2183,8 +2179,7 @@ "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/debug": { "version": "4.4.3", @@ -2381,7 +2376,6 @@ "integrity": "sha512-VmQ+sifHUbI/IcSopBCF/HO3YiHQx/AVd3UVyYL6weuwW+HvON9VYn5l6Zl1WZzPWXPNZrSQpxwkkZ/VuvJZzg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -3562,7 +3556,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -3590,7 +3583,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -3658,7 +3650,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -3668,7 +3659,6 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz", "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==", "license": "MIT", - "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -3681,7 +3671,6 @@ "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.71.2.tgz", "integrity": "sha512-1CHvcDYzuRUNOflt4MOq3ZM46AronNJtQ1S7tnX6YN4y72qhgiUItpacZUAQ0TyWYci3yz1X+rXaSxiuEm86PA==", "license": "MIT", - "peer": true, "engines": { "node": ">=18.0.0" }, @@ -3724,7 +3713,6 @@ "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==", "license": "MIT", - "peer": true, "dependencies": { "@types/use-sync-external-store": "^0.0.6", "use-sync-external-store": "^1.4.0" @@ -3795,8 +3783,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/redux-persist": { "version": "6.0.0", @@ -3963,8 +3950,7 @@ "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.2.1.tgz", "integrity": "sha512-/tBrSQ36vCleJkAOsy9kbNTgaxvGbyOamC30PRePTQe/o1MFwEKHQk4Cn7BNGaPtjp+PuUrByJehM1hgxfq4sw==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/tapable": { "version": "2.3.0", @@ -4097,7 +4083,6 @@ "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", @@ -4231,7 +4216,6 @@ "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", "dev": true, "license": "MIT", - "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/src/App.jsx b/src/App.jsx index 111658f..a529792 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1,7 +1,40 @@ import AppRouter from './routes/AppRouter'; +import { Toaster } from 'react-hot-toast'; function App() { - return ; + return ( + <> + + + + ); } export default App; diff --git a/src/components/common/GlobalLoadingWrapper.jsx b/src/components/common/GlobalLoadingWrapper.jsx index 9c843a7..e4ca01e 100644 --- a/src/components/common/GlobalLoadingWrapper.jsx +++ b/src/components/common/GlobalLoadingWrapper.jsx @@ -1,4 +1,3 @@ -import { useNavigation } from 'react-router-dom'; import { useSelector } from 'react-redux'; import Spinner from './Spinner'; @@ -9,15 +8,12 @@ const selectIsAnyLoading = (state) => state.dashboard.loading; const GlobalLoadingWrapper = ({ children }) => { - const navigation = useNavigation(); const isReduxLoading = useSelector(selectIsAnyLoading); - const isLoading = navigation.state !== 'idle' || isReduxLoading; - return ( <> {children} - {isLoading && ( + {isReduxLoading && (
- -
diff --git a/src/features/auth/authThunks.ts b/src/features/auth/authThunks.js similarity index 66% rename from src/features/auth/authThunks.ts rename to src/features/auth/authThunks.js index 1f7682a..6e85d09 100644 --- a/src/features/auth/authThunks.ts +++ b/src/features/auth/authThunks.js @@ -2,28 +2,7 @@ import { createAsyncThunk } from '@reduxjs/toolkit'; import api from '../../services/api'; import { toastSuccess, toastError } from '../../utils/toast'; -// payload/type definitions -interface RegisterData { - // specify fields if known, otherwise use any - [key: string]: any; -} - -interface LoginCredentials { - email: string; - password: string; -} - -interface ResetPasswordPayload { - token: string; - password: string; -} - -// generic reject value type used across thunks -interface RejectValue { - message?: string; -} - -export const registerUser = createAsyncThunk( +export const registerUser = createAsyncThunk( 'auth/register', async (userData, { rejectWithValue }) => { try { @@ -31,97 +10,95 @@ export const registerUser = createAsyncThunk( +export const loginUser = createAsyncThunk( 'auth/login', async (credentials, { rejectWithValue }) => { try { const response = await api.post('/auth/login', credentials); toastSuccess('Logged in successfully'); return response.data; - } catch (err: any) { + } catch (err) { toastError(err); return rejectWithValue(err.response?.data || err.message); } } ); -export const logoutUser = createAsyncThunk( +export const logoutUser = createAsyncThunk( 'auth/logout', async (_, { rejectWithValue }) => { try { await api.post('/auth/logout'); toastSuccess('Logged out successfully'); return true; - } catch (err: any) { + } catch (err) { toastError(err); return rejectWithValue(err.response?.data || err.message); } } ); -export const verifyEmail = createAsyncThunk( +export const verifyEmail = createAsyncThunk( 'auth/verifyEmail', async (token, { rejectWithValue }) => { try { const response = await api.post('/auth/verify-email', { token }); toastSuccess('Email verified'); return response.data; - } catch (err: any) { + } catch (err) { toastError(err); return rejectWithValue(err.response?.data || err.message); } } ); -export const resendEmailVerification = createAsyncThunk( +export const resendEmailVerification = createAsyncThunk( 'auth/resendEmailVerification', async (email, { rejectWithValue }) => { try { const response = await api.post('/auth/resend-verification', { email }); toastSuccess('Verification email sent'); return response.data; - } catch (err: any) { + } catch (err) { toastError(err); return rejectWithValue(err.response?.data || err.message); } } ); -export const forgotPassword = createAsyncThunk( +export const forgotPassword = createAsyncThunk( 'auth/forgotPassword', async (email) => { try { const response = await api.post('/auth/forgot-password', { email }); toastSuccess('If this email is registered, a reset link has been sent'); return response.data; - } catch (err: any) { + } catch (err) { // Log for debugging but don't show error toast to user console.error('Forgot password background error:', err); - // For security (avoiding user enumeration), always return success state to the UI - // unless it's a critical application error we want the user to see. - // In this specific task, "regardless of whether the email exists" implies a uniform success UI. + toastSuccess('If this email is registered, a reset link has been sent'); return { status: 'success', message: 'Email processed' }; } } ); -export const resetPassword = createAsyncThunk( +export const resetPassword = createAsyncThunk( 'auth/resetPassword', async ({ token, password }, { rejectWithValue }) => { try { const response = await api.post('/auth/reset-password', { token, password }); toastSuccess('Password reset successfully'); return response.data; - } catch (err: any) { + } catch (err) { toastError(err); return rejectWithValue(err.response?.data || err.message); } diff --git a/src/main.jsx b/src/main.jsx index bf23027..358e101 100644 --- a/src/main.jsx +++ b/src/main.jsx @@ -11,6 +11,9 @@ import { PersistGate } from "redux-persist/integration/react"; // Validate environment variables on startup validateEnv(); +// Set global store reference for API service to avoid circular dependencies +window.__REDUX_STORE__ = store; + createRoot(document.getElementById("root")).render( diff --git a/src/pages/Login.tsx b/src/pages/Login.tsx deleted file mode 100644 index 09d02a2..0000000 --- a/src/pages/Login.tsx +++ /dev/null @@ -1,177 +0,0 @@ -import { useState } from 'react'; -import { useNavigate, Link } from 'react-router-dom'; -import { AiOutlineEye, AiOutlineEyeInvisible } from 'react-icons/ai'; -import { useDispatch } from 'react-redux'; -import { loginUser } from '../features/auth/authThunks'; -import { AppDispatch } from '../store'; -export default function LoginPage() { - const navigate = useNavigate(); - const dispatch = useDispatch(); - - const [email, setEmail] = useState(''); - const [password, setPassword] = useState(''); - const [showPassword, setShowPassword] = useState(false); - const [loading, setLoading] = useState(false); - const [error, setError] = useState(null); - - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault(); - setError(null); - setLoading(true); - - try { - const result = await dispatch(loginUser({ email, password })); - - if (result.type === loginUser.fulfilled.type) { - // Login successful, navigate to dashboard - navigate('/dashboard'); - } else if (result.type === loginUser.rejected.type) { - // Handle error from rejection - const errorPayload = result.payload as { message?: string } | undefined; - setError(errorPayload?.message || 'Login failed. Please try again.'); - } else { - setError('Login failed. Please try again.'); - } - } catch (err) { - setError('An error occurred. Please try again.'); - } finally { - setLoading(false); - } - }; - - return ( -
-
-
- {/* Logo and Header */} -
-
-
- S -
-
-

Welcome Back

-

Sign in to your account to continue

-
- - {/* Error Alert */} - {error && ( -
- {error} -
- )} - - {/* Login Form */} -
- {/* Email Field */} -
- - setEmail(e.target.value)} - required - className="w-full px-4 py-2.5 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent text-gray-900 placeholder-gray-400" - /> -
- - {/* Password Field */} -
-
- - - Forgot password? - -
-
- setPassword(e.target.value)} - required - className="w-full px-4 py-2.5 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent text-gray-900 placeholder-gray-400" - /> - -
-
- - {/* Remember Me */} -
- - -
- - {/* Submit Button */} - -
- - {/* Divider */} -
-
-
-
-
- or -
-
- - {/* Social Login Buttons */} -
- - -
- - {/* Sign Up Link */} -
- Don't have an account?{' '} - - Create one - -
-
-
-
- ); -} diff --git a/src/services/api.js b/src/services/api.js index b75ea73..2aa914e 100644 --- a/src/services/api.js +++ b/src/services/api.js @@ -1,6 +1,4 @@ import axios from 'axios'; -import { store } from '../store'; -import { clearCredentials } from '../features/auth/authSlice'; // Create configured Axios instance const api = axios.create({ @@ -14,8 +12,9 @@ const api = axios.create({ // Request interceptor to attach JWT token api.interceptors.request.use( (config) => { - const state = store.getState(); - const token = state.auth.token; + // Get token from localStorage directly to avoid circular dependency + const state = window.__REDUX_STORE__?.getState?.(); + const token = state?.auth?.token; if (token) { config.headers.Authorization = `Bearer ${token}`; @@ -36,7 +35,8 @@ api.interceptors.response.use( (error) => { // Handle 401 Unauthorized - logout user if (error.response?.status === 401) { - store.dispatch(clearCredentials()); + // Dispatch clearCredentials through global store reference + window.__REDUX_STORE__?.dispatch({ type: 'auth/clearCredentials' }); // You might want to redirect to login page here // window.location.href = '/login'; } diff --git a/src/store/index.ts b/src/store/index.js similarity index 87% rename from src/store/index.ts rename to src/store/index.js index f840083..fb58285 100644 --- a/src/store/index.ts +++ b/src/store/index.js @@ -25,6 +25,3 @@ export const store = configureStore({ // persistor is exported so PersistGate in main.jsx can use it export const persistor = persistStore(store); - -export type AppDispatch = typeof store.dispatch; -export type RootState = ReturnType; diff --git a/src/store/rootReducer.ts b/src/store/rootReducer.js similarity index 100% rename from src/store/rootReducer.ts rename to src/store/rootReducer.js