no separate login/register page, use modal throughout
This commit is contained in:
@@ -1,11 +1,10 @@
|
||||
import React from 'react';
|
||||
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
|
||||
import { AuthProvider } from './contexts/AuthContext';
|
||||
import { AuthProvider, useAuth } from './contexts/AuthContext';
|
||||
import Navbar from './components/Navbar';
|
||||
import Footer from './components/Footer';
|
||||
import AuthModal from './components/AuthModal';
|
||||
import Home from './pages/Home';
|
||||
import Login from './pages/Login';
|
||||
import Register from './pages/Register';
|
||||
import GoogleCallback from './pages/GoogleCallback';
|
||||
import VerifyEmail from './pages/VerifyEmail';
|
||||
import ItemList from './pages/ItemList';
|
||||
@@ -27,17 +26,17 @@ import EarningsDashboard from './pages/EarningsDashboard';
|
||||
import PrivateRoute from './components/PrivateRoute';
|
||||
import './App.css';
|
||||
|
||||
function App() {
|
||||
const AppContent: React.FC = () => {
|
||||
const { showAuthModal, authModalMode, closeAuthModal } = useAuth();
|
||||
|
||||
return (
|
||||
<AuthProvider>
|
||||
<>
|
||||
<Router>
|
||||
<div className="d-flex flex-column min-vh-100">
|
||||
<Navbar />
|
||||
<main className="flex-grow-1">
|
||||
<Routes>
|
||||
<Route path="/" element={<Home />} />
|
||||
<Route path="/login" element={<Login />} />
|
||||
<Route path="/register" element={<Register />} />
|
||||
<Route path="/auth/google/callback" element={<GoogleCallback />} />
|
||||
<Route path="/verify-email" element={<VerifyEmail />} />
|
||||
<Route path="/items" element={<ItemList />} />
|
||||
@@ -138,6 +137,20 @@ function App() {
|
||||
<Footer />
|
||||
</div>
|
||||
</Router>
|
||||
|
||||
<AuthModal
|
||||
show={showAuthModal}
|
||||
onHide={closeAuthModal}
|
||||
initialMode={authModalMode}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<AuthProvider>
|
||||
<AppContent />
|
||||
</AuthProvider>
|
||||
);
|
||||
}
|
||||
|
||||
41
frontend/src/components/AuthButton.tsx
Normal file
41
frontend/src/components/AuthButton.tsx
Normal file
@@ -0,0 +1,41 @@
|
||||
import React from 'react';
|
||||
import { useAuth } from '../contexts/AuthContext';
|
||||
|
||||
interface AuthButtonProps {
|
||||
mode: 'login' | 'signup';
|
||||
className?: string;
|
||||
children: React.ReactNode;
|
||||
asLink?: boolean;
|
||||
}
|
||||
|
||||
const AuthButton: React.FC<AuthButtonProps> = ({ mode, className = '', children, asLink = false }) => {
|
||||
const { openAuthModal } = useAuth();
|
||||
|
||||
const handleClick = (e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
openAuthModal(mode);
|
||||
};
|
||||
|
||||
if (asLink) {
|
||||
return (
|
||||
<a
|
||||
href="#"
|
||||
onClick={handleClick}
|
||||
className={className}
|
||||
>
|
||||
{children}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={handleClick}
|
||||
className={className}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
export default AuthButton;
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useState, useEffect, useRef, useCallback } from "react";
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { useAuth } from "../contexts/AuthContext";
|
||||
import PasswordStrengthMeter from "./PasswordStrengthMeter";
|
||||
import PasswordInput from "./PasswordInput";
|
||||
|
||||
@@ -1,15 +1,10 @@
|
||||
import React, { useState } from "react";
|
||||
import { Link, useNavigate } from "react-router-dom";
|
||||
import { useAuth } from "../contexts/AuthContext";
|
||||
import AuthModal from "./AuthModal";
|
||||
|
||||
const Navbar: React.FC = () => {
|
||||
const { user, logout } = useAuth();
|
||||
const { user, logout, openAuthModal } = useAuth();
|
||||
const navigate = useNavigate();
|
||||
const [showAuthModal, setShowAuthModal] = useState(false);
|
||||
const [authModalMode, setAuthModalMode] = useState<"login" | "signup">(
|
||||
"login"
|
||||
);
|
||||
const [searchFilters, setSearchFilters] = useState({
|
||||
search: "",
|
||||
location: "",
|
||||
@@ -20,11 +15,6 @@ const Navbar: React.FC = () => {
|
||||
navigate("/");
|
||||
};
|
||||
|
||||
const openAuthModal = (mode: "login" | "signup") => {
|
||||
setAuthModalMode(mode);
|
||||
setShowAuthModal(true);
|
||||
};
|
||||
|
||||
const handleSearch = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
const params = new URLSearchParams();
|
||||
@@ -204,12 +194,6 @@ const Navbar: React.FC = () => {
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<AuthModal
|
||||
show={showAuthModal}
|
||||
onHide={() => setShowAuthModal(false)}
|
||||
initialMode={authModalMode}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,18 +1,25 @@
|
||||
import React from 'react';
|
||||
import { Navigate, useLocation } from 'react-router-dom';
|
||||
import { useAuth } from '../contexts/AuthContext';
|
||||
import React, { useEffect } from "react";
|
||||
import { useAuth } from "../contexts/AuthContext";
|
||||
|
||||
interface PrivateRouteProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
const PrivateRoute: React.FC<PrivateRouteProps> = ({ children }) => {
|
||||
const { user, loading } = useAuth();
|
||||
const location = useLocation();
|
||||
const { user, loading, openAuthModal } = useAuth();
|
||||
|
||||
useEffect(() => {
|
||||
if (!loading && !user) {
|
||||
openAuthModal("login");
|
||||
}
|
||||
}, [loading, user, openAuthModal]);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="d-flex justify-content-center align-items-center" style={{ minHeight: '80vh' }}>
|
||||
<div
|
||||
className="d-flex justify-content-center align-items-center"
|
||||
style={{ minHeight: "80vh" }}
|
||||
>
|
||||
<div className="spinner-border text-primary" role="status">
|
||||
<span className="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
@@ -20,7 +27,21 @@ const PrivateRoute: React.FC<PrivateRouteProps> = ({ children }) => {
|
||||
);
|
||||
}
|
||||
|
||||
return user ? <>{children}</> : <Navigate to="/login" state={{ from: location }} replace />;
|
||||
if (!user) {
|
||||
return (
|
||||
<div
|
||||
className="d-flex justify-content-center align-items-center"
|
||||
style={{ minHeight: "80vh" }}
|
||||
>
|
||||
<div className="text-center">
|
||||
<i className="bi bi-lock display-1 text-muted mb-3"></i>
|
||||
<h3>Please log in or sign up to access this page.</h3>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return <>{children}</>;
|
||||
};
|
||||
|
||||
export default PrivateRoute;
|
||||
export default PrivateRoute;
|
||||
|
||||
@@ -9,7 +9,6 @@ import React, {
|
||||
import { User } from "../types";
|
||||
import {
|
||||
authAPI,
|
||||
userAPI,
|
||||
fetchCSRFToken,
|
||||
resetCSRFToken,
|
||||
} from "../services/api";
|
||||
@@ -23,6 +22,10 @@ interface AuthContextType {
|
||||
logout: () => Promise<void>;
|
||||
updateUser: (user: User) => void;
|
||||
checkAuth: () => Promise<void>;
|
||||
showAuthModal: boolean;
|
||||
authModalMode: "login" | "signup";
|
||||
openAuthModal: (mode: "login" | "signup") => void;
|
||||
closeAuthModal: () => void;
|
||||
}
|
||||
|
||||
const AuthContext = createContext<AuthContextType | undefined>(undefined);
|
||||
@@ -42,6 +45,8 @@ interface AuthProviderProps {
|
||||
export const AuthProvider: React.FC<AuthProviderProps> = ({ children }) => {
|
||||
const [user, setUser] = useState<User | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [showAuthModal, setShowAuthModal] = useState(false);
|
||||
const [authModalMode, setAuthModalMode] = useState<"login" | "signup">("login");
|
||||
const isAuthenticating = useRef(false);
|
||||
|
||||
const checkAuth = async () => {
|
||||
@@ -132,6 +137,15 @@ export const AuthProvider: React.FC<AuthProviderProps> = ({ children }) => {
|
||||
setUser(user);
|
||||
};
|
||||
|
||||
const openAuthModal = (mode: "login" | "signup") => {
|
||||
setAuthModalMode(mode);
|
||||
setShowAuthModal(true);
|
||||
};
|
||||
|
||||
const closeAuthModal = () => {
|
||||
setShowAuthModal(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<AuthContext.Provider
|
||||
value={{
|
||||
@@ -143,6 +157,10 @@ export const AuthProvider: React.FC<AuthProviderProps> = ({ children }) => {
|
||||
logout,
|
||||
updateUser,
|
||||
checkAuth,
|
||||
showAuthModal,
|
||||
authModalMode,
|
||||
openAuthModal,
|
||||
closeAuthModal,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useAuth } from '../contexts/AuthContext';
|
||||
import { itemAPI } from '../services/api';
|
||||
import { Item } from '../types';
|
||||
import ItemCard from '../components/ItemCard';
|
||||
import AuthButton from '../components/AuthButton';
|
||||
|
||||
const Home: React.FC = () => {
|
||||
const { user } = useAuth();
|
||||
@@ -48,9 +49,9 @@ const Home: React.FC = () => {
|
||||
List Your Items
|
||||
</Link>
|
||||
) : (
|
||||
<Link to="/register" className="btn btn-outline-light">
|
||||
<AuthButton mode="signup" className="btn btn-outline-light">
|
||||
Start Earning
|
||||
</Link>
|
||||
</AuthButton>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
@@ -292,9 +293,9 @@ const Home: React.FC = () => {
|
||||
List an Item
|
||||
</Link>
|
||||
) : (
|
||||
<Link to="/register" className="btn btn-outline-light btn-lg">
|
||||
<AuthButton mode="signup" className="btn btn-outline-light btn-lg">
|
||||
Sign Up Free
|
||||
</Link>
|
||||
</AuthButton>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useAuth } from '../contexts/AuthContext';
|
||||
import { itemRequestAPI } from '../services/api';
|
||||
import { ItemRequest, ItemRequestResponse } from '../types';
|
||||
import RequestResponseModal from '../components/RequestResponseModal';
|
||||
import AuthButton from '../components/AuthButton';
|
||||
|
||||
const ItemRequestDetail: React.FC = () => {
|
||||
const { id } = useParams<{ id: string }>();
|
||||
@@ -330,9 +331,9 @@ const ItemRequestDetail: React.FC = () => {
|
||||
</div>
|
||||
) : !user ? (
|
||||
<div className="text-center">
|
||||
<Link to="/login" className="btn btn-outline-primary">
|
||||
<AuthButton mode="login" className="btn btn-outline-primary">
|
||||
Log in to Respond
|
||||
</Link>
|
||||
</AuthButton>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useAuth } from '../contexts/AuthContext';
|
||||
import { itemRequestAPI } from '../services/api';
|
||||
import { ItemRequest } from '../types';
|
||||
import ItemRequestCard from '../components/ItemRequestCard';
|
||||
import AuthButton from '../components/AuthButton';
|
||||
|
||||
const ItemRequests: React.FC = () => {
|
||||
const { user } = useAuth();
|
||||
@@ -200,7 +201,7 @@ const ItemRequests: React.FC = () => {
|
||||
<div className="mt-4">
|
||||
<div className="alert alert-info" role="alert">
|
||||
<i className="bi bi-info-circle me-2"></i>
|
||||
<Link to="/login" className="alert-link">Log in</Link> to create your own item requests or respond to existing ones.
|
||||
<AuthButton mode="login" className="alert-link" asLink>Log in</AuthButton> to create your own item requests or respond to existing ones.
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -1,89 +0,0 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Link, useNavigate, useLocation } from 'react-router-dom';
|
||||
import { useAuth } from '../contexts/AuthContext';
|
||||
import PasswordInput from '../components/PasswordInput';
|
||||
|
||||
const Login: React.FC = () => {
|
||||
const [email, setEmail] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [error, setError] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const { login } = useAuth();
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
|
||||
const from = location.state?.from?.pathname || '/';
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setError('');
|
||||
setLoading(true);
|
||||
|
||||
try {
|
||||
await login(email, password);
|
||||
navigate(from, { replace: true });
|
||||
} catch (err: any) {
|
||||
setError(err.response?.data?.error || 'Failed to login');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="container mt-5">
|
||||
<div className="row justify-content-center">
|
||||
<div className="col-md-6 col-lg-5">
|
||||
<div className="card shadow">
|
||||
<div className="card-body p-4">
|
||||
<h2 className="text-center mb-4">Login</h2>
|
||||
{error && (
|
||||
<div className="alert alert-danger" role="alert">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="mb-3">
|
||||
<label htmlFor="email" className="form-label">
|
||||
Email
|
||||
</label>
|
||||
<input
|
||||
type="email"
|
||||
className="form-control"
|
||||
id="email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<PasswordInput
|
||||
id="password"
|
||||
label="Password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
required
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
className="btn btn-primary w-100"
|
||||
disabled={loading}
|
||||
>
|
||||
{loading ? 'Logging in...' : 'Login'}
|
||||
</button>
|
||||
</form>
|
||||
<div className="text-center mt-3">
|
||||
<p className="mb-0">
|
||||
Don't have an account?{' '}
|
||||
<Link to="/register" state={{ from: location.state?.from }} className="text-decoration-none">
|
||||
Sign up
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Login;
|
||||
@@ -1,13 +1,12 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Link, useNavigate } from 'react-router-dom';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { useAuth } from '../contexts/AuthContext';
|
||||
import { itemRequestAPI } from '../services/api';
|
||||
import { ItemRequest, ItemRequestResponse } from '../types';
|
||||
import ConfirmationModal from '../components/ConfirmationModal';
|
||||
|
||||
const MyRequests: React.FC = () => {
|
||||
const { user } = useAuth();
|
||||
const navigate = useNavigate();
|
||||
const { user, openAuthModal } = useAuth();
|
||||
const [requests, setRequests] = useState<ItemRequest[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
@@ -20,9 +19,9 @@ const MyRequests: React.FC = () => {
|
||||
if (user) {
|
||||
fetchMyRequests();
|
||||
} else {
|
||||
navigate('/login');
|
||||
openAuthModal('login');
|
||||
}
|
||||
}, [user, navigate]);
|
||||
}, [user, openAuthModal]);
|
||||
|
||||
const fetchMyRequests = async () => {
|
||||
try {
|
||||
|
||||
@@ -1,161 +0,0 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Link, useNavigate, useLocation } from 'react-router-dom';
|
||||
import { useAuth } from '../contexts/AuthContext';
|
||||
import PasswordInput from '../components/PasswordInput';
|
||||
|
||||
const Register: React.FC = () => {
|
||||
const [formData, setFormData] = useState({
|
||||
username: '',
|
||||
email: '',
|
||||
password: '',
|
||||
firstName: '',
|
||||
lastName: '',
|
||||
phone: ''
|
||||
});
|
||||
const [error, setError] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const { register } = useAuth();
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
|
||||
const from = location.state?.from?.pathname || '/';
|
||||
|
||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setFormData({
|
||||
...formData,
|
||||
[e.target.name]: e.target.value
|
||||
});
|
||||
};
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setError('');
|
||||
setLoading(true);
|
||||
|
||||
try {
|
||||
await register(formData);
|
||||
navigate(from, { replace: true });
|
||||
} catch (err: any) {
|
||||
setError(err.response?.data?.error || 'Failed to create account');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="container mt-5">
|
||||
<div className="row justify-content-center">
|
||||
<div className="col-md-6 col-lg-5">
|
||||
<div className="card shadow">
|
||||
<div className="card-body p-4">
|
||||
<h2 className="text-center mb-4">Create Account</h2>
|
||||
{error && (
|
||||
<div className="alert alert-danger" role="alert">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="row">
|
||||
<div className="col-md-6 mb-3">
|
||||
<label htmlFor="firstName" className="form-label">
|
||||
First Name
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
className="form-control"
|
||||
id="firstName"
|
||||
name="firstName"
|
||||
value={formData.firstName}
|
||||
onChange={handleChange}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="col-md-6 mb-3">
|
||||
<label htmlFor="lastName" className="form-label">
|
||||
Last Name
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
className="form-control"
|
||||
id="lastName"
|
||||
name="lastName"
|
||||
value={formData.lastName}
|
||||
onChange={handleChange}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mb-3">
|
||||
<label htmlFor="username" className="form-label">
|
||||
Username
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
className="form-control"
|
||||
id="username"
|
||||
name="username"
|
||||
value={formData.username}
|
||||
onChange={handleChange}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-3">
|
||||
<label htmlFor="email" className="form-label">
|
||||
Email
|
||||
</label>
|
||||
<input
|
||||
type="email"
|
||||
className="form-control"
|
||||
id="email"
|
||||
name="email"
|
||||
value={formData.email}
|
||||
onChange={handleChange}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-3">
|
||||
<label htmlFor="phone" className="form-label">
|
||||
Phone (optional)
|
||||
</label>
|
||||
<input
|
||||
type="tel"
|
||||
className="form-control"
|
||||
id="phone"
|
||||
name="phone"
|
||||
value={formData.phone}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
</div>
|
||||
<PasswordInput
|
||||
id="password"
|
||||
name="password"
|
||||
label="Password"
|
||||
value={formData.password}
|
||||
onChange={handleChange}
|
||||
required
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
className="btn btn-primary w-100"
|
||||
disabled={loading}
|
||||
>
|
||||
{loading ? 'Creating Account...' : 'Sign Up'}
|
||||
</button>
|
||||
</form>
|
||||
<div className="text-center mt-3">
|
||||
<p className="mb-0">
|
||||
Already have an account?{' '}
|
||||
<Link to="/login" className="text-decoration-none">
|
||||
Login
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Register;
|
||||
Reference in New Issue
Block a user