Migrated to react router v7

This commit is contained in:
jackiettran
2026-01-19 22:50:53 -05:00
parent 1923ffc251
commit 28554acc2d
35 changed files with 180 additions and 284 deletions

View File

@@ -1,13 +1,10 @@
import React, { useState, useEffect, useCallback } from 'react';
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
import React, { useState, useEffect } from 'react';
import { createBrowserRouter, RouterProvider } from 'react-router';
import { AuthProvider, useAuth } from './contexts/AuthContext';
import { SocketProvider } from './contexts/SocketContext';
import Navbar from './components/Navbar';
import Footer from './components/Footer';
import AuthModal from './components/AuthModal';
import RootLayout from './components/RootLayout';
import ProtectedLayout from './components/ProtectedLayout';
import AlphaGate from './components/AlphaGate';
import FeedbackButton from './components/FeedbackButton';
import { TwoFactorVerifyModal } from './components/TwoFactor';
import Home from './pages/Home';
import GoogleCallback from './pages/GoogleCallback';
import VerifyEmail from './pages/VerifyEmail';
@@ -30,50 +27,56 @@ import EarningsDashboard from './pages/EarningsDashboard';
import CompletePayment from './pages/CompletePayment';
import FAQ from './pages/FAQ';
import NotFound from './pages/NotFound';
import PrivateRoute from './components/PrivateRoute';
import axios from 'axios';
import './App.css';
const API_URL = import.meta.env.VITE_API_URL || 'http://localhost:5001';
const router = createBrowserRouter([
{
element: <RootLayout />,
children: [
// Public routes
{ path: '/', element: <Home /> },
{ path: '/auth/google/callback', element: <GoogleCallback /> },
{ path: '/verify-email', element: <VerifyEmail /> },
{ path: '/reset-password', element: <ResetPassword /> },
{ path: '/items', element: <ItemList /> },
{ path: '/items/:id', element: <ItemDetail /> },
{ path: '/users/:id', element: <PublicProfile /> },
{ path: '/forum', element: <ForumPosts /> },
{ path: '/forum/:id', element: <ForumPostDetail /> },
{ path: '/faq', element: <FAQ /> },
// Protected routes group
{
element: <ProtectedLayout />,
children: [
{ path: '/items/:id/edit', element: <EditItem /> },
{ path: '/items/:id/rent', element: <RentItem /> },
{ path: '/create-item', element: <CreateItem /> },
{ path: '/renting', element: <Renting /> },
{ path: '/complete-payment/:rentalId', element: <CompletePayment /> },
{ path: '/owning', element: <Owning /> },
{ path: '/profile', element: <Profile /> },
{ path: '/messages', element: <Messages /> },
{ path: '/forum/create', element: <CreateForumPost /> },
{ path: '/forum/:id/edit', element: <CreateForumPost /> },
{ path: '/my-posts', element: <MyPosts /> },
{ path: '/earnings', element: <EarningsDashboard /> },
],
},
// Catch-all route
{ path: '*', element: <NotFound /> },
],
},
]);
const AppContent: React.FC = () => {
const { showAuthModal, authModalMode, closeAuthModal, user } = useAuth();
const [hasAlphaAccess, setHasAlphaAccess] = useState<boolean | null>(null);
const [checkingAccess, setCheckingAccess] = useState(true);
// Step-up authentication state
const [showStepUpModal, setShowStepUpModal] = useState(false);
const [stepUpAction, setStepUpAction] = useState<string | undefined>();
const [stepUpMethods, setStepUpMethods] = useState<("totp" | "email" | "recovery")[]>([]);
// Listen for step-up authentication required events
useEffect(() => {
const handleStepUpRequired = (event: CustomEvent) => {
const { action, methods } = event.detail;
setStepUpAction(action);
setStepUpMethods(methods || ["totp", "email", "recovery"]);
setShowStepUpModal(true);
};
window.addEventListener("stepUpRequired", handleStepUpRequired as EventListener);
return () => {
window.removeEventListener("stepUpRequired", handleStepUpRequired as EventListener);
};
}, []);
const handleStepUpSuccess = useCallback(() => {
setShowStepUpModal(false);
setStepUpAction(undefined);
// Dispatch event so pending actions can auto-retry
window.dispatchEvent(new CustomEvent("stepUpSuccess"));
}, []);
const handleStepUpClose = useCallback(() => {
setShowStepUpModal(false);
setStepUpAction(undefined);
}, []);
useEffect(() => {
const checkAlphaAccess = async () => {
// Bypass alpha access check if feature is disabled
@@ -115,145 +118,7 @@ const AppContent: React.FC = () => {
return <AlphaGate />;
}
return (
<>
<Router>
<div className="d-flex flex-column min-vh-100">
<Navbar />
<main className="flex-grow-1">
<Routes>
<Route path="/" element={<Home />} />
<Route path="/auth/google/callback" element={<GoogleCallback />} />
<Route path="/verify-email" element={<VerifyEmail />} />
<Route path="/reset-password" element={<ResetPassword />} />
<Route path="/items" element={<ItemList />} />
<Route path="/items/:id" element={<ItemDetail />} />
<Route path="/users/:id" element={<PublicProfile />} />
<Route
path="/items/:id/edit"
element={
<PrivateRoute>
<EditItem />
</PrivateRoute>
}
/>
<Route
path="/items/:id/rent"
element={
<PrivateRoute>
<RentItem />
</PrivateRoute>
}
/>
<Route
path="/create-item"
element={
<PrivateRoute>
<CreateItem />
</PrivateRoute>
}
/>
<Route
path="/renting"
element={
<PrivateRoute>
<Renting />
</PrivateRoute>
}
/>
<Route
path="/complete-payment/:rentalId"
element={
<PrivateRoute>
<CompletePayment />
</PrivateRoute>
}
/>
<Route
path="/owning"
element={
<PrivateRoute>
<Owning />
</PrivateRoute>
}
/>
<Route
path="/profile"
element={
<PrivateRoute>
<Profile />
</PrivateRoute>
}
/>
<Route
path="/messages"
element={
<PrivateRoute>
<Messages />
</PrivateRoute>
}
/>
<Route path="/forum" element={<ForumPosts />} />
<Route path="/forum/:id" element={<ForumPostDetail />} />
<Route
path="/forum/create"
element={
<PrivateRoute>
<CreateForumPost />
</PrivateRoute>
}
/>
<Route
path="/forum/:id/edit"
element={
<PrivateRoute>
<CreateForumPost />
</PrivateRoute>
}
/>
<Route
path="/my-posts"
element={
<PrivateRoute>
<MyPosts />
</PrivateRoute>
}
/>
<Route
path="/earnings"
element={
<PrivateRoute>
<EarningsDashboard />
</PrivateRoute>
}
/>
<Route path="/faq" element={<FAQ />} />
<Route path="*" element={<NotFound />} />
</Routes>
</main>
<Footer />
</div>
</Router>
<AuthModal
show={showAuthModal}
onHide={closeAuthModal}
initialMode={authModalMode}
/>
{/* Show feedback button for authenticated users */}
{user && <FeedbackButton />}
{/* Global Step-Up Authentication Modal */}
<TwoFactorVerifyModal
show={showStepUpModal}
onHide={handleStepUpClose}
onSuccess={handleStepUpSuccess}
action={stepUpAction}
methods={stepUpMethods}
/>
</>
);
return <RouterProvider router={router} />;
};
const AppWithSocket: React.FC = () => {
@@ -274,4 +139,4 @@ function App() {
);
}
export default App;
export default App;

View File

@@ -7,7 +7,7 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import { BrowserRouter } from 'react-router-dom';
import { BrowserRouter } from 'react-router';
import { vi, type MockedFunction } from 'vitest';
import ItemCard from '../../components/ItemCard';
import { Item } from '../../types';

View File

@@ -7,7 +7,7 @@
import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react';
import { BrowserRouter } from 'react-router-dom';
import { BrowserRouter } from 'react-router';
import { vi, type Mock } from 'vitest';
import Navbar from '../../components/Navbar';
import { rentalAPI, messageAPI } from '../../services/api';
@@ -45,8 +45,8 @@ vi.mock('../../contexts/AuthContext', () => ({
// Mock useNavigate
const mockNavigate = vi.fn();
vi.mock('react-router-dom', async () => {
const actual = await vi.importActual('react-router-dom');
vi.mock('react-router', async () => {
const actual = await vi.importActual('react-router');
return {
...actual,
useNavigate: () => mockNavigate,

View File

@@ -1,5 +1,5 @@
import React from 'react';
import { Link } from 'react-router-dom';
import { Link } from 'react-router';
const Footer: React.FC = () => {
return (

View File

@@ -1,5 +1,5 @@
import React from "react";
import { Link } from "react-router-dom";
import { Link } from "react-router";
import { ForumPost } from "../types";
import CategoryBadge from "./CategoryBadge";
import PostStatusBadge from "./PostStatusBadge";

View File

@@ -1,5 +1,5 @@
import React from 'react';
import { Link } from 'react-router-dom';
import { Link } from 'react-router';
import { Item } from '../types';
import { getImageUrl } from '../services/uploadService';

View File

@@ -1,5 +1,5 @@
import React, { useState, useEffect } from "react";
import { useNavigate } from "react-router-dom";
import { useNavigate } from "react-router";
import { Rental } from "../types";
import { itemAPI } from "../services/api";
import Avatar from "./Avatar";

View File

@@ -1,5 +1,5 @@
import React, { useState, useEffect, useRef, useCallback } from "react";
import { Link, useNavigate, useLocation } from "react-router-dom";
import { Link, useNavigate, useLocation } from "react-router";
import { useAuth } from "../contexts/AuthContext";
import { useSocket } from "../contexts/SocketContext";
import { rentalAPI, messageAPI } from "../services/api";

View File

@@ -1,5 +1,5 @@
import React from "react";
import { Link } from "react-router-dom";
import { Link } from "react-router";
interface PricingFormProps {
pricePerHour: number | string;

View File

@@ -1,11 +1,8 @@
import React, { useEffect } from "react";
import { Outlet } from "react-router";
import { useAuth } from "../contexts/AuthContext";
interface PrivateRouteProps {
children: React.ReactNode;
}
const PrivateRoute: React.FC<PrivateRouteProps> = ({ children }) => {
const ProtectedLayout: React.FC = () => {
const { user, loading, openAuthModal } = useAuth();
useEffect(() => {
@@ -41,7 +38,7 @@ const PrivateRoute: React.FC<PrivateRouteProps> = ({ children }) => {
);
}
return <>{children}</>;
return <Outlet />;
};
export default PrivateRoute;
export default ProtectedLayout;

View File

@@ -0,0 +1,77 @@
import React, { useState, useEffect, useCallback } from "react";
import { Outlet } from "react-router";
import { useAuth } from "../contexts/AuthContext";
import Navbar from "./Navbar";
import Footer from "./Footer";
import AuthModal from "./AuthModal";
import FeedbackButton from "./FeedbackButton";
import { TwoFactorVerifyModal } from "./TwoFactor";
const RootLayout: React.FC = () => {
const { showAuthModal, authModalMode, closeAuthModal, user } = useAuth();
// Step-up authentication state
const [showStepUpModal, setShowStepUpModal] = useState(false);
const [stepUpAction, setStepUpAction] = useState<string | undefined>();
const [stepUpMethods, setStepUpMethods] = useState<("totp" | "email" | "recovery")[]>([]);
// Listen for step-up authentication required events
useEffect(() => {
const handleStepUpRequired = (event: CustomEvent) => {
const { action, methods } = event.detail;
setStepUpAction(action);
setStepUpMethods(methods || ["totp", "email", "recovery"]);
setShowStepUpModal(true);
};
window.addEventListener("stepUpRequired", handleStepUpRequired as EventListener);
return () => {
window.removeEventListener("stepUpRequired", handleStepUpRequired as EventListener);
};
}, []);
const handleStepUpSuccess = useCallback(() => {
setShowStepUpModal(false);
setStepUpAction(undefined);
// Dispatch event so pending actions can auto-retry
window.dispatchEvent(new CustomEvent("stepUpSuccess"));
}, []);
const handleStepUpClose = useCallback(() => {
setShowStepUpModal(false);
setStepUpAction(undefined);
}, []);
return (
<>
<div className="d-flex flex-column min-vh-100">
<Navbar />
<main className="flex-grow-1">
<Outlet />
</main>
<Footer />
</div>
<AuthModal
show={showAuthModal}
onHide={closeAuthModal}
initialMode={authModalMode}
/>
{/* Show feedback button for authenticated users */}
{user && <FeedbackButton />}
{/* Global Step-Up Authentication Modal */}
<TwoFactorVerifyModal
show={showStepUpModal}
onHide={handleStepUpClose}
onSuccess={handleStepUpSuccess}
action={stepUpAction}
methods={stepUpMethods}
/>
</>
);
};
export default RootLayout;

View File

@@ -1,5 +1,5 @@
import React, { useEffect, useState, useCallback, useRef } from "react";
import { useParams, useNavigate, Link } from "react-router-dom";
import { useParams, useNavigate, Link } from "react-router";
import { loadStripe } from "@stripe/stripe-js";
import { rentalAPI } from "../services/api";
import { useAuth } from "../contexts/AuthContext";

View File

@@ -1,5 +1,5 @@
import React, { useState, useEffect } from "react";
import { useNavigate, Link, useParams } from "react-router-dom";
import { useNavigate, Link, useParams } from "react-router";
import { useAuth } from "../contexts/AuthContext";
import { forumAPI, addressAPI } from "../services/api";
import { uploadImagesWithVariants, getImageUrl } from "../services/uploadService";

View File

@@ -1,5 +1,5 @@
import React, { useState, useEffect, useRef } from "react";
import { useNavigate } from "react-router-dom";
import { useNavigate } from "react-router";
import { useAuth } from "../contexts/AuthContext";
import api, { addressAPI, userAPI, itemAPI } from "../services/api";
import { uploadImagesWithVariants } from "../services/uploadService";

View File

@@ -1,5 +1,5 @@
import React, { useState, useEffect } from "react";
import { Link } from "react-router-dom";
import { Link } from "react-router";
import { rentalAPI, userAPI, stripeAPI } from "../services/api";
import { Rental, User } from "../types";
import StripeConnectOnboarding from "../components/StripeConnectOnboarding";

View File

@@ -1,5 +1,5 @@
import React, { useState, useEffect, useRef } from "react";
import { useParams, useNavigate } from "react-router-dom";
import { useParams, useNavigate } from "react-router";
import { Item, Rental, Address } from "../types";
import { useAuth } from "../contexts/AuthContext";
import { itemAPI, rentalAPI, addressAPI, userAPI } from "../services/api";

View File

@@ -1,5 +1,5 @@
import React, { useState } from "react";
import { Link } from "react-router-dom";
import { Link } from "react-router";
interface FAQItem {
question: string;

View File

@@ -1,5 +1,5 @@
import React, { useState, useEffect } from 'react';
import { useParams, useNavigate, Link, useSearchParams } from 'react-router-dom';
import { useParams, useNavigate, Link, useSearchParams } from 'react-router';
import { useAuth } from '../contexts/AuthContext';
import { forumAPI } from '../services/api';
import { uploadImagesWithVariants, getImageUrl } from '../services/uploadService';

View File

@@ -1,5 +1,5 @@
import React, { useState, useEffect } from 'react';
import { Link, useSearchParams } from 'react-router-dom';
import { Link, useSearchParams } from 'react-router';
import { useAuth } from '../contexts/AuthContext';
import { forumAPI } from '../services/api';
import { ForumPost } from '../types';

View File

@@ -1,5 +1,5 @@
import React, { useEffect, useState, useRef } from 'react';
import { useNavigate, useSearchParams } from 'react-router-dom';
import { useNavigate, useSearchParams } from 'react-router';
import { useAuth } from '../contexts/AuthContext';
import { fetchCSRFToken } from '../services/api';

View File

@@ -1,5 +1,5 @@
import React, { useState, useEffect } from "react";
import { Link } from "react-router-dom";
import { Link } from "react-router";
import { useAuth } from "../contexts/AuthContext";
import { itemAPI } from "../services/api";
import { Item } from "../types";

View File

@@ -1,5 +1,5 @@
import React, { useState, useEffect } from "react";
import { useParams, useNavigate } from "react-router-dom";
import { useParams, useNavigate } from "react-router";
import DatePicker from "react-datepicker";
import "react-datepicker/dist/react-datepicker.css";
import { Item, Rental } from "../types";

View File

@@ -1,5 +1,5 @@
import React, { useState, useEffect, useRef } from "react";
import { useSearchParams, useNavigate } from "react-router-dom";
import { useSearchParams, useNavigate } from "react-router";
import { Item } from "../types";
import { itemAPI } from "../services/api";
import ItemCard from "../components/ItemCard";

View File

@@ -1,5 +1,5 @@
import React, { useState, useEffect } from 'react';
import { Link } from 'react-router-dom';
import { Link } from 'react-router';
import { useAuth } from '../contexts/AuthContext';
import { forumAPI } from '../services/api';
import { ForumPost } from '../types';

View File

@@ -1,5 +1,5 @@
import React from "react";
import { Link } from "react-router-dom";
import { Link } from "react-router";
const NotFound: React.FC = () => {
return (

View File

@@ -1,5 +1,5 @@
import React, { useState, useEffect } from "react";
import { Link, useNavigate } from "react-router-dom";
import { Link, useNavigate } from "react-router";
import { useAuth } from "../contexts/AuthContext";
import api from "../services/api";
import { Item, Rental, ConditionCheck } from "../types";

View File

@@ -1,5 +1,5 @@
import React, { useState, useEffect, useCallback, useMemo } from "react";
import { useNavigate } from "react-router-dom";
import { useNavigate } from "react-router";
import { useAuth } from "../contexts/AuthContext";
import { userAPI, itemAPI, rentalAPI, addressAPI, conditionCheckAPI } from "../services/api";
import { User, Item, Rental, Address, ConditionCheck } from "../types";

View File

@@ -1,5 +1,5 @@
import React, { useState, useEffect } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import { useParams, useNavigate } from 'react-router';
import { User, Item } from '../types';
import { userAPI, itemAPI } from '../services/api';
import { getImageUrl } from '../services/uploadService';

View File

@@ -1,5 +1,5 @@
import React, { useState, useEffect } from "react";
import { useParams, useNavigate, useSearchParams } from "react-router-dom";
import { useParams, useNavigate, useSearchParams } from "react-router";
import { Item } from "../types";
import { useAuth } from "../contexts/AuthContext";
import { itemAPI, rentalAPI } from "../services/api";

View File

@@ -1,5 +1,5 @@
import React, { useState, useEffect } from "react";
import { Link, useNavigate } from "react-router-dom";
import { Link, useNavigate } from "react-router";
import { useAuth } from "../contexts/AuthContext";
import { rentalAPI, conditionCheckAPI } from "../services/api";
import { getImageUrl } from "../services/uploadService";

View File

@@ -1,5 +1,5 @@
import React, { useEffect, useState, useRef } from 'react';
import { useNavigate, useSearchParams, Link } from 'react-router-dom';
import { useNavigate, useSearchParams, Link } from 'react-router';
import { useAuth } from '../contexts/AuthContext';
import { authAPI } from '../services/api';
import PasswordInput from '../components/PasswordInput';

View File

@@ -1,5 +1,5 @@
import React, { useEffect, useState, useRef } from "react";
import { useNavigate, useSearchParams, Link } from "react-router-dom";
import { useNavigate, useSearchParams, Link } from "react-router";
import { useAuth } from "../contexts/AuthContext";
import { authAPI } from "../services/api";

View File

@@ -160,7 +160,7 @@ api.interceptors.response.use(
isRefreshing = false;
processQueue(refreshError as AxiosError);
// Refresh failed - let React Router handle redirects via PrivateRoute
// Refresh failed - let React Router handle redirects via ProtectedLayout
return Promise.reject(refreshError);
}
}