no separate login/register page, use modal throughout

This commit is contained in:
jackiettran
2025-10-10 15:26:07 -04:00
parent 0a9b875a9d
commit 462dbf6b7a
12 changed files with 125 additions and 296 deletions

View File

@@ -1,11 +1,10 @@
import React from 'react'; import React from 'react';
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom'; 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 Navbar from './components/Navbar';
import Footer from './components/Footer'; import Footer from './components/Footer';
import AuthModal from './components/AuthModal';
import Home from './pages/Home'; import Home from './pages/Home';
import Login from './pages/Login';
import Register from './pages/Register';
import GoogleCallback from './pages/GoogleCallback'; import GoogleCallback from './pages/GoogleCallback';
import VerifyEmail from './pages/VerifyEmail'; import VerifyEmail from './pages/VerifyEmail';
import ItemList from './pages/ItemList'; import ItemList from './pages/ItemList';
@@ -27,17 +26,17 @@ import EarningsDashboard from './pages/EarningsDashboard';
import PrivateRoute from './components/PrivateRoute'; import PrivateRoute from './components/PrivateRoute';
import './App.css'; import './App.css';
function App() { const AppContent: React.FC = () => {
const { showAuthModal, authModalMode, closeAuthModal } = useAuth();
return ( return (
<AuthProvider> <>
<Router> <Router>
<div className="d-flex flex-column min-vh-100"> <div className="d-flex flex-column min-vh-100">
<Navbar /> <Navbar />
<main className="flex-grow-1"> <main className="flex-grow-1">
<Routes> <Routes>
<Route path="/" element={<Home />} /> <Route path="/" element={<Home />} />
<Route path="/login" element={<Login />} />
<Route path="/register" element={<Register />} />
<Route path="/auth/google/callback" element={<GoogleCallback />} /> <Route path="/auth/google/callback" element={<GoogleCallback />} />
<Route path="/verify-email" element={<VerifyEmail />} /> <Route path="/verify-email" element={<VerifyEmail />} />
<Route path="/items" element={<ItemList />} /> <Route path="/items" element={<ItemList />} />
@@ -138,6 +137,20 @@ function App() {
<Footer /> <Footer />
</div> </div>
</Router> </Router>
<AuthModal
show={showAuthModal}
onHide={closeAuthModal}
initialMode={authModalMode}
/>
</>
);
};
function App() {
return (
<AuthProvider>
<AppContent />
</AuthProvider> </AuthProvider>
); );
} }

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

View File

@@ -1,4 +1,4 @@
import React, { useState, useEffect, useRef, useCallback } from "react"; import React, { useState, useEffect } from "react";
import { useAuth } from "../contexts/AuthContext"; import { useAuth } from "../contexts/AuthContext";
import PasswordStrengthMeter from "./PasswordStrengthMeter"; import PasswordStrengthMeter from "./PasswordStrengthMeter";
import PasswordInput from "./PasswordInput"; import PasswordInput from "./PasswordInput";

View File

@@ -1,15 +1,10 @@
import React, { useState } from "react"; import React, { useState } from "react";
import { Link, useNavigate } from "react-router-dom"; import { Link, useNavigate } from "react-router-dom";
import { useAuth } from "../contexts/AuthContext"; import { useAuth } from "../contexts/AuthContext";
import AuthModal from "./AuthModal";
const Navbar: React.FC = () => { const Navbar: React.FC = () => {
const { user, logout } = useAuth(); const { user, logout, openAuthModal } = useAuth();
const navigate = useNavigate(); const navigate = useNavigate();
const [showAuthModal, setShowAuthModal] = useState(false);
const [authModalMode, setAuthModalMode] = useState<"login" | "signup">(
"login"
);
const [searchFilters, setSearchFilters] = useState({ const [searchFilters, setSearchFilters] = useState({
search: "", search: "",
location: "", location: "",
@@ -20,11 +15,6 @@ const Navbar: React.FC = () => {
navigate("/"); navigate("/");
}; };
const openAuthModal = (mode: "login" | "signup") => {
setAuthModalMode(mode);
setShowAuthModal(true);
};
const handleSearch = (e: React.FormEvent) => { const handleSearch = (e: React.FormEvent) => {
e.preventDefault(); e.preventDefault();
const params = new URLSearchParams(); const params = new URLSearchParams();
@@ -204,12 +194,6 @@ const Navbar: React.FC = () => {
</div> </div>
</div> </div>
</nav> </nav>
<AuthModal
show={showAuthModal}
onHide={() => setShowAuthModal(false)}
initialMode={authModalMode}
/>
</> </>
); );
}; };

View File

@@ -1,18 +1,25 @@
import React from 'react'; import React, { useEffect } from "react";
import { Navigate, useLocation } from 'react-router-dom'; import { useAuth } from "../contexts/AuthContext";
import { useAuth } from '../contexts/AuthContext';
interface PrivateRouteProps { interface PrivateRouteProps {
children: React.ReactNode; children: React.ReactNode;
} }
const PrivateRoute: React.FC<PrivateRouteProps> = ({ children }) => { const PrivateRoute: React.FC<PrivateRouteProps> = ({ children }) => {
const { user, loading } = useAuth(); const { user, loading, openAuthModal } = useAuth();
const location = useLocation();
useEffect(() => {
if (!loading && !user) {
openAuthModal("login");
}
}, [loading, user, openAuthModal]);
if (loading) { if (loading) {
return ( 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"> <div className="spinner-border text-primary" role="status">
<span className="visually-hidden">Loading...</span> <span className="visually-hidden">Loading...</span>
</div> </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;

View File

@@ -9,7 +9,6 @@ import React, {
import { User } from "../types"; import { User } from "../types";
import { import {
authAPI, authAPI,
userAPI,
fetchCSRFToken, fetchCSRFToken,
resetCSRFToken, resetCSRFToken,
} from "../services/api"; } from "../services/api";
@@ -23,6 +22,10 @@ interface AuthContextType {
logout: () => Promise<void>; logout: () => Promise<void>;
updateUser: (user: User) => void; updateUser: (user: User) => void;
checkAuth: () => Promise<void>; checkAuth: () => Promise<void>;
showAuthModal: boolean;
authModalMode: "login" | "signup";
openAuthModal: (mode: "login" | "signup") => void;
closeAuthModal: () => void;
} }
const AuthContext = createContext<AuthContextType | undefined>(undefined); const AuthContext = createContext<AuthContextType | undefined>(undefined);
@@ -42,6 +45,8 @@ interface AuthProviderProps {
export const AuthProvider: React.FC<AuthProviderProps> = ({ children }) => { export const AuthProvider: React.FC<AuthProviderProps> = ({ children }) => {
const [user, setUser] = useState<User | null>(null); const [user, setUser] = useState<User | null>(null);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [showAuthModal, setShowAuthModal] = useState(false);
const [authModalMode, setAuthModalMode] = useState<"login" | "signup">("login");
const isAuthenticating = useRef(false); const isAuthenticating = useRef(false);
const checkAuth = async () => { const checkAuth = async () => {
@@ -132,6 +137,15 @@ export const AuthProvider: React.FC<AuthProviderProps> = ({ children }) => {
setUser(user); setUser(user);
}; };
const openAuthModal = (mode: "login" | "signup") => {
setAuthModalMode(mode);
setShowAuthModal(true);
};
const closeAuthModal = () => {
setShowAuthModal(false);
};
return ( return (
<AuthContext.Provider <AuthContext.Provider
value={{ value={{
@@ -143,6 +157,10 @@ export const AuthProvider: React.FC<AuthProviderProps> = ({ children }) => {
logout, logout,
updateUser, updateUser,
checkAuth, checkAuth,
showAuthModal,
authModalMode,
openAuthModal,
closeAuthModal,
}} }}
> >
{children} {children}

View File

@@ -4,6 +4,7 @@ import { useAuth } from '../contexts/AuthContext';
import { itemAPI } from '../services/api'; import { itemAPI } from '../services/api';
import { Item } from '../types'; import { Item } from '../types';
import ItemCard from '../components/ItemCard'; import ItemCard from '../components/ItemCard';
import AuthButton from '../components/AuthButton';
const Home: React.FC = () => { const Home: React.FC = () => {
const { user } = useAuth(); const { user } = useAuth();
@@ -48,9 +49,9 @@ const Home: React.FC = () => {
List Your Items List Your Items
</Link> </Link>
) : ( ) : (
<Link to="/register" className="btn btn-outline-light"> <AuthButton mode="signup" className="btn btn-outline-light">
Start Earning Start Earning
</Link> </AuthButton>
)} )}
</div> </div>
</div> </div>
@@ -292,9 +293,9 @@ const Home: React.FC = () => {
List an Item List an Item
</Link> </Link>
) : ( ) : (
<Link to="/register" className="btn btn-outline-light btn-lg"> <AuthButton mode="signup" className="btn btn-outline-light btn-lg">
Sign Up Free Sign Up Free
</Link> </AuthButton>
)} )}
</div> </div>
</div> </div>

View File

@@ -4,6 +4,7 @@ import { useAuth } from '../contexts/AuthContext';
import { itemRequestAPI } from '../services/api'; import { itemRequestAPI } from '../services/api';
import { ItemRequest, ItemRequestResponse } from '../types'; import { ItemRequest, ItemRequestResponse } from '../types';
import RequestResponseModal from '../components/RequestResponseModal'; import RequestResponseModal from '../components/RequestResponseModal';
import AuthButton from '../components/AuthButton';
const ItemRequestDetail: React.FC = () => { const ItemRequestDetail: React.FC = () => {
const { id } = useParams<{ id: string }>(); const { id } = useParams<{ id: string }>();
@@ -330,9 +331,9 @@ const ItemRequestDetail: React.FC = () => {
</div> </div>
) : !user ? ( ) : !user ? (
<div className="text-center"> <div className="text-center">
<Link to="/login" className="btn btn-outline-primary"> <AuthButton mode="login" className="btn btn-outline-primary">
Log in to Respond Log in to Respond
</Link> </AuthButton>
</div> </div>
) : null} ) : null}

View File

@@ -4,6 +4,7 @@ import { useAuth } from '../contexts/AuthContext';
import { itemRequestAPI } from '../services/api'; import { itemRequestAPI } from '../services/api';
import { ItemRequest } from '../types'; import { ItemRequest } from '../types';
import ItemRequestCard from '../components/ItemRequestCard'; import ItemRequestCard from '../components/ItemRequestCard';
import AuthButton from '../components/AuthButton';
const ItemRequests: React.FC = () => { const ItemRequests: React.FC = () => {
const { user } = useAuth(); const { user } = useAuth();
@@ -200,7 +201,7 @@ const ItemRequests: React.FC = () => {
<div className="mt-4"> <div className="mt-4">
<div className="alert alert-info" role="alert"> <div className="alert alert-info" role="alert">
<i className="bi bi-info-circle me-2"></i> <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>
</div> </div>
)} )}

View File

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

View File

@@ -1,13 +1,12 @@
import React, { useState, useEffect } from 'react'; 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 { useAuth } from '../contexts/AuthContext';
import { itemRequestAPI } from '../services/api'; import { itemRequestAPI } from '../services/api';
import { ItemRequest, ItemRequestResponse } from '../types'; import { ItemRequest, ItemRequestResponse } from '../types';
import ConfirmationModal from '../components/ConfirmationModal'; import ConfirmationModal from '../components/ConfirmationModal';
const MyRequests: React.FC = () => { const MyRequests: React.FC = () => {
const { user } = useAuth(); const { user, openAuthModal } = useAuth();
const navigate = useNavigate();
const [requests, setRequests] = useState<ItemRequest[]>([]); const [requests, setRequests] = useState<ItemRequest[]>([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
@@ -20,9 +19,9 @@ const MyRequests: React.FC = () => {
if (user) { if (user) {
fetchMyRequests(); fetchMyRequests();
} else { } else {
navigate('/login'); openAuthModal('login');
} }
}, [user, navigate]); }, [user, openAuthModal]);
const fetchMyRequests = async () => { const fetchMyRequests = async () => {
try { try {

View File

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