Migrated to react router v7
This commit is contained in:
80
frontend/package-lock.json
generated
80
frontend/package-lock.json
generated
@@ -20,14 +20,13 @@
|
|||||||
"@types/node": "^20.0.0",
|
"@types/node": "^20.0.0",
|
||||||
"@types/react": "^19.1.8",
|
"@types/react": "^19.1.8",
|
||||||
"@types/react-dom": "^19.1.6",
|
"@types/react-dom": "^19.1.6",
|
||||||
"@types/react-router-dom": "^5.3.3",
|
|
||||||
"axios": "^1.10.0",
|
"axios": "^1.10.0",
|
||||||
"bootstrap": "^5.3.7",
|
"bootstrap": "^5.3.7",
|
||||||
"browser-image-compression": "^2.0.2",
|
"browser-image-compression": "^2.0.2",
|
||||||
"react": "^19.1.0",
|
"react": "^19.1.0",
|
||||||
"react-datepicker": "^9.1.0",
|
"react-datepicker": "^9.1.0",
|
||||||
"react-dom": "^19.1.0",
|
"react-dom": "^19.1.0",
|
||||||
"react-router-dom": "^6.30.1",
|
"react-router": "^7.12.0",
|
||||||
"socket.io-client": "^4.8.1",
|
"socket.io-client": "^4.8.1",
|
||||||
"stripe": "^18.4.0",
|
"stripe": "^18.4.0",
|
||||||
"typescript": "^4.9.5"
|
"typescript": "^4.9.5"
|
||||||
@@ -1271,15 +1270,6 @@
|
|||||||
"url": "https://opencollective.com/popperjs"
|
"url": "https://opencollective.com/popperjs"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@remix-run/router": {
|
|
||||||
"version": "1.23.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.2.tgz",
|
|
||||||
"integrity": "sha512-Ic6m2U/rMjTkhERIa/0ZtXJP17QUi2CbWE7cqx4J58M8aA3QTfW+2UlQ4psvTX9IO1RfNVhK3pcpdjej7L+t2w==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=14.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@rolldown/pluginutils": {
|
"node_modules/@rolldown/pluginutils": {
|
||||||
"version": "1.0.0-beta.27",
|
"version": "1.0.0-beta.27",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz",
|
||||||
@@ -1860,12 +1850,6 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@types/history": {
|
|
||||||
"version": "4.7.11",
|
|
||||||
"resolved": "https://registry.npmjs.org/@types/history/-/history-4.7.11.tgz",
|
|
||||||
"integrity": "sha512-qjDJRrmvBMiTx+jyLxvLfJU7UznFuokDv4f3WRuriHKERccVpFU+8XMQUAbDzoiJCsmexxRExQeMwwCdamSKDA==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/@types/node": {
|
"node_modules/@types/node": {
|
||||||
"version": "20.19.30",
|
"version": "20.19.30",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.30.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.30.tgz",
|
||||||
@@ -1893,27 +1877,6 @@
|
|||||||
"@types/react": "^19.2.0"
|
"@types/react": "^19.2.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@types/react-router": {
|
|
||||||
"version": "5.1.20",
|
|
||||||
"resolved": "https://registry.npmjs.org/@types/react-router/-/react-router-5.1.20.tgz",
|
|
||||||
"integrity": "sha512-jGjmu/ZqS7FjSH6owMcD5qpq19+1RS9DeVRqfl1FeBMxTDQAGwlMWOcs52NDoXaNKyG3d1cYQFMs9rCrb88o9Q==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@types/history": "^4.7.11",
|
|
||||||
"@types/react": "*"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@types/react-router-dom": {
|
|
||||||
"version": "5.3.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/@types/react-router-dom/-/react-router-dom-5.3.3.tgz",
|
|
||||||
"integrity": "sha512-kpqnYK4wcdm5UaWI3fLcELopqLrHgLqNsdpHauzlQktfkHL3npOSwtj1Uz9oKBAzs7lFtVkV8j83voAz2D8fhw==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@types/history": "^4.7.11",
|
|
||||||
"@types/react": "*",
|
|
||||||
"@types/react-router": "*"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@types/statuses": {
|
"node_modules/@types/statuses": {
|
||||||
"version": "2.0.6",
|
"version": "2.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/@types/statuses/-/statuses-2.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/@types/statuses/-/statuses-2.0.6.tgz",
|
||||||
@@ -2384,7 +2347,6 @@
|
|||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz",
|
||||||
"integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==",
|
"integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
@@ -3692,35 +3654,25 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/react-router": {
|
"node_modules/react-router": {
|
||||||
"version": "6.30.3",
|
"version": "7.12.0",
|
||||||
"resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.3.tgz",
|
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.12.0.tgz",
|
||||||
"integrity": "sha512-XRnlbKMTmktBkjCLE8/XcZFlnHvr2Ltdr1eJX4idL55/9BbORzyZEaIkBFDhFGCEWBBItsVrDxwx3gnisMitdw==",
|
"integrity": "sha512-kTPDYPFzDVGIIGNLS5VJykK0HfHLY5MF3b+xj0/tTyNYL1gF1qs7u67Z9jEhQk2sQ98SUaHxlG31g1JtF7IfVw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@remix-run/router": "1.23.2"
|
"cookie": "^1.0.1",
|
||||||
|
"set-cookie-parser": "^2.6.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=14.0.0"
|
"node": ">=20.0.0"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"react": ">=16.8"
|
"react": ">=18",
|
||||||
}
|
"react-dom": ">=18"
|
||||||
},
|
|
||||||
"node_modules/react-router-dom": {
|
|
||||||
"version": "6.30.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.3.tgz",
|
|
||||||
"integrity": "sha512-pxPcv1AczD4vso7G4Z3TKcvlxK7g7TNt3/FNGMhfqyntocvYKj+GCatfigGDjbLozC4baguJ0ReCigoDJXb0ag==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@remix-run/router": "1.23.2",
|
|
||||||
"react-router": "6.30.3"
|
|
||||||
},
|
},
|
||||||
"engines": {
|
"peerDependenciesMeta": {
|
||||||
"node": ">=14.0.0"
|
"react-dom": {
|
||||||
},
|
"optional": true
|
||||||
"peerDependencies": {
|
}
|
||||||
"react": ">=16.8",
|
|
||||||
"react-dom": ">=16.8"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/redent": {
|
"node_modules/redent": {
|
||||||
@@ -3837,6 +3789,12 @@
|
|||||||
"semver": "bin/semver.js"
|
"semver": "bin/semver.js"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/set-cookie-parser": {
|
||||||
|
"version": "2.7.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz",
|
||||||
|
"integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/side-channel": {
|
"node_modules/side-channel": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
|
||||||
|
|||||||
@@ -16,14 +16,13 @@
|
|||||||
"@types/node": "^20.0.0",
|
"@types/node": "^20.0.0",
|
||||||
"@types/react": "^19.1.8",
|
"@types/react": "^19.1.8",
|
||||||
"@types/react-dom": "^19.1.6",
|
"@types/react-dom": "^19.1.6",
|
||||||
"@types/react-router-dom": "^5.3.3",
|
|
||||||
"axios": "^1.10.0",
|
"axios": "^1.10.0",
|
||||||
"bootstrap": "^5.3.7",
|
"bootstrap": "^5.3.7",
|
||||||
"browser-image-compression": "^2.0.2",
|
"browser-image-compression": "^2.0.2",
|
||||||
"react": "^19.1.0",
|
"react": "^19.1.0",
|
||||||
"react-datepicker": "^9.1.0",
|
"react-datepicker": "^9.1.0",
|
||||||
"react-dom": "^19.1.0",
|
"react-dom": "^19.1.0",
|
||||||
"react-router-dom": "^6.30.1",
|
"react-router": "^7.12.0",
|
||||||
"socket.io-client": "^4.8.1",
|
"socket.io-client": "^4.8.1",
|
||||||
"stripe": "^18.4.0",
|
"stripe": "^18.4.0",
|
||||||
"typescript": "^4.9.5"
|
"typescript": "^4.9.5"
|
||||||
|
|||||||
@@ -1,13 +1,10 @@
|
|||||||
import React, { useState, useEffect, useCallback } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
|
import { createBrowserRouter, RouterProvider } from 'react-router';
|
||||||
import { AuthProvider, useAuth } from './contexts/AuthContext';
|
import { AuthProvider, useAuth } from './contexts/AuthContext';
|
||||||
import { SocketProvider } from './contexts/SocketContext';
|
import { SocketProvider } from './contexts/SocketContext';
|
||||||
import Navbar from './components/Navbar';
|
import RootLayout from './components/RootLayout';
|
||||||
import Footer from './components/Footer';
|
import ProtectedLayout from './components/ProtectedLayout';
|
||||||
import AuthModal from './components/AuthModal';
|
|
||||||
import AlphaGate from './components/AlphaGate';
|
import AlphaGate from './components/AlphaGate';
|
||||||
import FeedbackButton from './components/FeedbackButton';
|
|
||||||
import { TwoFactorVerifyModal } from './components/TwoFactor';
|
|
||||||
import Home from './pages/Home';
|
import Home from './pages/Home';
|
||||||
import GoogleCallback from './pages/GoogleCallback';
|
import GoogleCallback from './pages/GoogleCallback';
|
||||||
import VerifyEmail from './pages/VerifyEmail';
|
import VerifyEmail from './pages/VerifyEmail';
|
||||||
@@ -30,50 +27,56 @@ import EarningsDashboard from './pages/EarningsDashboard';
|
|||||||
import CompletePayment from './pages/CompletePayment';
|
import CompletePayment from './pages/CompletePayment';
|
||||||
import FAQ from './pages/FAQ';
|
import FAQ from './pages/FAQ';
|
||||||
import NotFound from './pages/NotFound';
|
import NotFound from './pages/NotFound';
|
||||||
import PrivateRoute from './components/PrivateRoute';
|
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import './App.css';
|
import './App.css';
|
||||||
|
|
||||||
const API_URL = import.meta.env.VITE_API_URL || 'http://localhost:5001';
|
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 AppContent: React.FC = () => {
|
||||||
const { showAuthModal, authModalMode, closeAuthModal, user } = useAuth();
|
|
||||||
const [hasAlphaAccess, setHasAlphaAccess] = useState<boolean | null>(null);
|
const [hasAlphaAccess, setHasAlphaAccess] = useState<boolean | null>(null);
|
||||||
const [checkingAccess, setCheckingAccess] = useState(true);
|
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(() => {
|
useEffect(() => {
|
||||||
const checkAlphaAccess = async () => {
|
const checkAlphaAccess = async () => {
|
||||||
// Bypass alpha access check if feature is disabled
|
// Bypass alpha access check if feature is disabled
|
||||||
@@ -115,145 +118,7 @@ const AppContent: React.FC = () => {
|
|||||||
return <AlphaGate />;
|
return <AlphaGate />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return <RouterProvider router={router} />;
|
||||||
<>
|
|
||||||
<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}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const AppWithSocket: React.FC = () => {
|
const AppWithSocket: React.FC = () => {
|
||||||
@@ -274,4 +139,4 @@ function App() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default App;
|
export default App;
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { render, screen } from '@testing-library/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 { vi, type MockedFunction } from 'vitest';
|
||||||
import ItemCard from '../../components/ItemCard';
|
import ItemCard from '../../components/ItemCard';
|
||||||
import { Item } from '../../types';
|
import { Item } from '../../types';
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { render, screen, fireEvent } from '@testing-library/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 { vi, type Mock } from 'vitest';
|
||||||
import Navbar from '../../components/Navbar';
|
import Navbar from '../../components/Navbar';
|
||||||
import { rentalAPI, messageAPI } from '../../services/api';
|
import { rentalAPI, messageAPI } from '../../services/api';
|
||||||
@@ -45,8 +45,8 @@ vi.mock('../../contexts/AuthContext', () => ({
|
|||||||
|
|
||||||
// Mock useNavigate
|
// Mock useNavigate
|
||||||
const mockNavigate = vi.fn();
|
const mockNavigate = vi.fn();
|
||||||
vi.mock('react-router-dom', async () => {
|
vi.mock('react-router', async () => {
|
||||||
const actual = await vi.importActual('react-router-dom');
|
const actual = await vi.importActual('react-router');
|
||||||
return {
|
return {
|
||||||
...actual,
|
...actual,
|
||||||
useNavigate: () => mockNavigate,
|
useNavigate: () => mockNavigate,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router';
|
||||||
|
|
||||||
const Footer: React.FC = () => {
|
const Footer: React.FC = () => {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router";
|
||||||
import { ForumPost } from "../types";
|
import { ForumPost } from "../types";
|
||||||
import CategoryBadge from "./CategoryBadge";
|
import CategoryBadge from "./CategoryBadge";
|
||||||
import PostStatusBadge from "./PostStatusBadge";
|
import PostStatusBadge from "./PostStatusBadge";
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router';
|
||||||
import { Item } from '../types';
|
import { Item } from '../types';
|
||||||
import { getImageUrl } from '../services/uploadService';
|
import { getImageUrl } from '../services/uploadService';
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { useState, useEffect } from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router";
|
||||||
import { Rental } from "../types";
|
import { Rental } from "../types";
|
||||||
import { itemAPI } from "../services/api";
|
import { itemAPI } from "../services/api";
|
||||||
import Avatar from "./Avatar";
|
import Avatar from "./Avatar";
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { useState, useEffect, useRef, useCallback } from "react";
|
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 { useAuth } from "../contexts/AuthContext";
|
||||||
import { useSocket } from "../contexts/SocketContext";
|
import { useSocket } from "../contexts/SocketContext";
|
||||||
import { rentalAPI, messageAPI } from "../services/api";
|
import { rentalAPI, messageAPI } from "../services/api";
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router";
|
||||||
|
|
||||||
interface PricingFormProps {
|
interface PricingFormProps {
|
||||||
pricePerHour: number | string;
|
pricePerHour: number | string;
|
||||||
|
|||||||
@@ -1,11 +1,8 @@
|
|||||||
import React, { useEffect } from "react";
|
import React, { useEffect } from "react";
|
||||||
|
import { Outlet } from "react-router";
|
||||||
import { useAuth } from "../contexts/AuthContext";
|
import { useAuth } from "../contexts/AuthContext";
|
||||||
|
|
||||||
interface PrivateRouteProps {
|
const ProtectedLayout: React.FC = () => {
|
||||||
children: React.ReactNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
const PrivateRoute: React.FC<PrivateRouteProps> = ({ children }) => {
|
|
||||||
const { user, loading, openAuthModal } = useAuth();
|
const { user, loading, openAuthModal } = useAuth();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -41,7 +38,7 @@ const PrivateRoute: React.FC<PrivateRouteProps> = ({ children }) => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return <>{children}</>;
|
return <Outlet />;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default PrivateRoute;
|
export default ProtectedLayout;
|
||||||
77
frontend/src/components/RootLayout.tsx
Normal file
77
frontend/src/components/RootLayout.tsx
Normal 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;
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { useEffect, useState, useCallback, useRef } from "react";
|
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 { loadStripe } from "@stripe/stripe-js";
|
||||||
import { rentalAPI } from "../services/api";
|
import { rentalAPI } from "../services/api";
|
||||||
import { useAuth } from "../contexts/AuthContext";
|
import { useAuth } from "../contexts/AuthContext";
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { useState, useEffect } from "react";
|
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 { useAuth } from "../contexts/AuthContext";
|
||||||
import { forumAPI, addressAPI } from "../services/api";
|
import { forumAPI, addressAPI } from "../services/api";
|
||||||
import { uploadImagesWithVariants, getImageUrl } from "../services/uploadService";
|
import { uploadImagesWithVariants, getImageUrl } from "../services/uploadService";
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { useState, useEffect, useRef } from "react";
|
import React, { useState, useEffect, useRef } from "react";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router";
|
||||||
import { useAuth } from "../contexts/AuthContext";
|
import { useAuth } from "../contexts/AuthContext";
|
||||||
import api, { addressAPI, userAPI, itemAPI } from "../services/api";
|
import api, { addressAPI, userAPI, itemAPI } from "../services/api";
|
||||||
import { uploadImagesWithVariants } from "../services/uploadService";
|
import { uploadImagesWithVariants } from "../services/uploadService";
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { useState, useEffect } from "react";
|
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 { rentalAPI, userAPI, stripeAPI } from "../services/api";
|
||||||
import { Rental, User } from "../types";
|
import { Rental, User } from "../types";
|
||||||
import StripeConnectOnboarding from "../components/StripeConnectOnboarding";
|
import StripeConnectOnboarding from "../components/StripeConnectOnboarding";
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { useState, useEffect, useRef } from "react";
|
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 { Item, Rental, Address } from "../types";
|
||||||
import { useAuth } from "../contexts/AuthContext";
|
import { useAuth } from "../contexts/AuthContext";
|
||||||
import { itemAPI, rentalAPI, addressAPI, userAPI } from "../services/api";
|
import { itemAPI, rentalAPI, addressAPI, userAPI } from "../services/api";
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router";
|
||||||
|
|
||||||
interface FAQItem {
|
interface FAQItem {
|
||||||
question: string;
|
question: string;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
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 { useAuth } from '../contexts/AuthContext';
|
||||||
import { forumAPI } from '../services/api';
|
import { forumAPI } from '../services/api';
|
||||||
import { uploadImagesWithVariants, getImageUrl } from '../services/uploadService';
|
import { uploadImagesWithVariants, getImageUrl } from '../services/uploadService';
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
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 { useAuth } from '../contexts/AuthContext';
|
||||||
import { forumAPI } from '../services/api';
|
import { forumAPI } from '../services/api';
|
||||||
import { ForumPost } from '../types';
|
import { ForumPost } from '../types';
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { useEffect, useState, useRef } from 'react';
|
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 { useAuth } from '../contexts/AuthContext';
|
||||||
import { fetchCSRFToken } from '../services/api';
|
import { fetchCSRFToken } from '../services/api';
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { useState, useEffect } from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router";
|
||||||
import { useAuth } from "../contexts/AuthContext";
|
import { useAuth } from "../contexts/AuthContext";
|
||||||
import { itemAPI } from "../services/api";
|
import { itemAPI } from "../services/api";
|
||||||
import { Item } from "../types";
|
import { Item } from "../types";
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { useState, useEffect } from "react";
|
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 DatePicker from "react-datepicker";
|
||||||
import "react-datepicker/dist/react-datepicker.css";
|
import "react-datepicker/dist/react-datepicker.css";
|
||||||
import { Item, Rental } from "../types";
|
import { Item, Rental } from "../types";
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { useState, useEffect, useRef } from "react";
|
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 { Item } from "../types";
|
||||||
import { itemAPI } from "../services/api";
|
import { itemAPI } from "../services/api";
|
||||||
import ItemCard from "../components/ItemCard";
|
import ItemCard from "../components/ItemCard";
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router';
|
||||||
import { useAuth } from '../contexts/AuthContext';
|
import { useAuth } from '../contexts/AuthContext';
|
||||||
import { forumAPI } from '../services/api';
|
import { forumAPI } from '../services/api';
|
||||||
import { ForumPost } from '../types';
|
import { ForumPost } from '../types';
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router";
|
||||||
|
|
||||||
const NotFound: React.FC = () => {
|
const NotFound: React.FC = () => {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { useState, useEffect } from "react";
|
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 { useAuth } from "../contexts/AuthContext";
|
||||||
import api from "../services/api";
|
import api from "../services/api";
|
||||||
import { Item, Rental, ConditionCheck } from "../types";
|
import { Item, Rental, ConditionCheck } from "../types";
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { useState, useEffect, useCallback, useMemo } from "react";
|
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 { useAuth } from "../contexts/AuthContext";
|
||||||
import { userAPI, itemAPI, rentalAPI, addressAPI, conditionCheckAPI } from "../services/api";
|
import { userAPI, itemAPI, rentalAPI, addressAPI, conditionCheckAPI } from "../services/api";
|
||||||
import { User, Item, Rental, Address, ConditionCheck } from "../types";
|
import { User, Item, Rental, Address, ConditionCheck } from "../types";
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
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 { User, Item } from '../types';
|
||||||
import { userAPI, itemAPI } from '../services/api';
|
import { userAPI, itemAPI } from '../services/api';
|
||||||
import { getImageUrl } from '../services/uploadService';
|
import { getImageUrl } from '../services/uploadService';
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { useState, useEffect } from "react";
|
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 { Item } from "../types";
|
||||||
import { useAuth } from "../contexts/AuthContext";
|
import { useAuth } from "../contexts/AuthContext";
|
||||||
import { itemAPI, rentalAPI } from "../services/api";
|
import { itemAPI, rentalAPI } from "../services/api";
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { useState, useEffect } from "react";
|
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 { useAuth } from "../contexts/AuthContext";
|
||||||
import { rentalAPI, conditionCheckAPI } from "../services/api";
|
import { rentalAPI, conditionCheckAPI } from "../services/api";
|
||||||
import { getImageUrl } from "../services/uploadService";
|
import { getImageUrl } from "../services/uploadService";
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { useEffect, useState, useRef } from 'react';
|
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 { useAuth } from '../contexts/AuthContext';
|
||||||
import { authAPI } from '../services/api';
|
import { authAPI } from '../services/api';
|
||||||
import PasswordInput from '../components/PasswordInput';
|
import PasswordInput from '../components/PasswordInput';
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { useEffect, useState, useRef } from "react";
|
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 { useAuth } from "../contexts/AuthContext";
|
||||||
import { authAPI } from "../services/api";
|
import { authAPI } from "../services/api";
|
||||||
|
|
||||||
|
|||||||
@@ -160,7 +160,7 @@ api.interceptors.response.use(
|
|||||||
isRefreshing = false;
|
isRefreshing = false;
|
||||||
processQueue(refreshError as AxiosError);
|
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);
|
return Promise.reject(refreshError);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user