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 { 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>
);
}

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 PasswordStrengthMeter from "./PasswordStrengthMeter";
import PasswordInput from "./PasswordInput";

View File

@@ -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}
/>
</>
);
};

View File

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

View File

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

View File

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

View File

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

View File

@@ -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>
)}

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

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;