Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 | 2x 2x 2x 2x 7x 7x 7x 7x 7x 7x 7x 7x 7x 7x 5x 2x 2x 2x 2x 2x 2x 2x 1x 1x 2x 2x 2x 2x 7x 7x 7x 7x 7x 3x 7x 2x 5x 5x 5x 5x 5x 2x 2x | import React, { Suspense, useState, useEffect, useRef } from "react";
import { Outlet, Navigate, useNavigate } from "react-router-dom";
import { useAuth0 } from "@auth0/auth0-react";
import { useSnackbar } from "notistack";
import { useDispatch } from "react-redux";
import SimpleLoading from "@/utils/components/SimpleLoading";
import NavBar from "@/app/landing-page/navbar/NavBar.jsx";
import Footer from "@/app/landing-page/footer/Footer.jsx";
import HeroSection from "@/app/landing-page/sections/hero/HeroSection.jsx";
import FeaturesSection from "@/app/landing-page/sections/features/FeaturesSection.jsx";
import StatsSection from "@/app/landing-page/sections/stats/StatsSection.jsx";
import BenefitsSection from "@/app/landing-page/sections/benefits/BenefitsSection.jsx";
import { ROLES_CLAIM } from "@/authentication/auth0";
import {
AUTH_PORTAL_REJECTED_MSG_KEY,
clearAuthPortalIntent,
getAuthPortalIntent,
getRolesForPortalCheck,
rolesMatchStrictPortal,
} from "@/authentication/authPortalIntent";
import PortalMismatchLogout from "@/authentication/PortalMismatchLogout";
import { useAuthSession } from "@/authentication/AuthSessionProvider";
import { logoutSession, getStoredRole } from "@/authentication/session";
import { _userLogout } from "@/store/slices/auth/UserAuth";
import { getApiBaseUrlForRole, getPostLoginRedirectPath } from "@/utils/helpers/apiBaseUrl";
import Box from "@mui/material/Box";
import Typography from "@mui/material/Typography";
import Button from "@mui/material/Button";
const AboutPage = React.lazy(() => import("@/app/landing-page/pages/about/AboutPage.jsx"));
const ContactPage = React.lazy(() => import("@/app/landing-page/pages/contact/ContactPage.jsx"));
const BACKEND_STATUS = {
IDLE: "idle",
CHECKING: "checking",
AVAILABLE: "available",
UNAVAILABLE: "unavailable",
};
// Layout component with navbar and footer
const LandingPageLayout = () => {
const { isAuthenticated, isLoading, user, logout: auth0Logout } = useAuth0();
const { isAuthenticated: hasServerSession, loading: sessionLoading, refresh: refreshSession } = useAuthSession();
const navigate = useNavigate();
const dispatch = useDispatch();
const { enqueueSnackbar } = useSnackbar();
const [backendStatus, setBackendStatus] = useState(BACKEND_STATUS.IDLE);
const [stoppingSession, setStoppingSession] = useState(false);
const stoppingRef = useRef(false);
const role = isAuthenticated && user ? (user[ROLES_CLAIM] || [])[0] || localStorage.getItem("role") : null;
// EI-3435: Reaching the PUBLIC landing page must STOP any live session.
// "View as student" is an in-place toggle on the teacher URL, so the browser
// BACK arrow walks router history out to "/" while the httpOnly session
// cookie is still valid. Auth here comes from the BFF probe (useAuthSession),
// NOT the vestigial Auth0 SDK (useAuth0 is never authenticated under BFF
// login). Without stopping the session: (1) it stays alive so the next person
// on the machine can reopen the previous teacher's account, and (2) the Login
// button bounces back into the portal (LoginPage's already-authenticated
// guard) instead of rendering the login form. This mirrors the canonical
// logout (SideDrawer/AppLayout): revoke the session server-side, re-probe,
// wipe the cached user profile from Redux (so the prior teacher's data
// doesn't linger in memory on a shared machine), and clear the Auth0 SDK.
useEffect(() => {
// `stoppingRef` is the in-flight guard (a ref, so it never re-triggers the
// effect and can't leave a window where a re-render re-enters before the
// async logout finishes). `stoppingSession` state only drives the loading UI.
if (sessionLoading || !hasServerSession || stoppingRef.current) return;
let mounted = true;
stoppingRef.current = true;
setStoppingSession(true);
(async () => {
try {
await logoutSession(); // revoke refresh token + clear cookies/role
await refreshSession(); // re-probe so guards see logged-out
dispatch(_userLogout()); // wipe cached user profile from Redux (shared-machine PII)
auth0Logout({ openUrl: false }); // clear vestigial Auth0 SDK state
} catch {
/* best-effort, like the rest of the logout flow — swallow so an
unawaited probe error never becomes an unhandled rejection */
} finally {
// Always release the guard + spinner, even if a probe rejects, so
// the landing page can never get stuck on the loader.
stoppingRef.current = false;
Eif (mounted) setStoppingSession(false);
}
})();
return () => {
mounted = false;
};
}, [sessionLoading, hasServerSession, refreshSession, dispatch, auth0Logout]);
useEffect(() => {
Iif (isLoading) return;
const rejectedMsg = sessionStorage.getItem(AUTH_PORTAL_REJECTED_MSG_KEY);
Iif (rejectedMsg && !isAuthenticated) {
sessionStorage.removeItem(AUTH_PORTAL_REJECTED_MSG_KEY);
enqueueSnackbar(rejectedMsg, { variant: "error", autoHideDuration: 8000 });
}
}, [isLoading, isAuthenticated, enqueueSnackbar]);
useEffect(() => {
Eif (!role) return;
setBackendStatus(BACKEND_STATUS.CHECKING);
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 5000);
fetch(`${getApiBaseUrlForRole(role)}/`, { signal: controller.signal, mode: "no-cors" })
.then(() => {
clearTimeout(timeoutId);
setBackendStatus(BACKEND_STATUS.AVAILABLE);
})
.catch(() => {
clearTimeout(timeoutId);
setBackendStatus(BACKEND_STATUS.UNAVAILABLE);
});
return () => {
clearTimeout(timeoutId);
controller.abort();
};
}, [role]);
// Block render while stopping a live session (EI-3435). Also wait for the BFF
// probe when a role hint suggests the visitor may be authenticated — so a
// back-navigated teacher lands on the logged-out page without a flash. Visitors
// with no role hint (never-logged-in here) see the marketing page immediately;
// a returning visitor whose session ended without a clean logout keeps a stale
// role hint, so they briefly see the spinner for one /auth/me probe — acceptable.
if (isLoading || stoppingSession || (sessionLoading && getStoredRole())) {
return <SimpleLoading showLoading={true} />;
}
Iif (isAuthenticated && user) {
const intent = getAuthPortalIntent();
if (intent) {
const roles = getRolesForPortalCheck(user, true);
if (roles.length === 0) {
return <SimpleLoading showLoading={true} />;
}
if (!rolesMatchStrictPortal(intent, roles)) {
return <PortalMismatchLogout />;
}
}
}
Iif (role && backendStatus === BACKEND_STATUS.CHECKING) {
return <SimpleLoading showLoading={true} />;
}
Iif (role && backendStatus === BACKEND_STATUS.UNAVAILABLE) {
return (
<Box
display="flex"
flexDirection="column"
justifyContent="center"
alignItems="center"
minHeight="100vh"
gap={2}
>
<Typography variant="h5" fontWeight={600}>
Service Unavailable
</Typography>
<Typography variant="body1" color="text.secondary" textAlign="center" maxWidth={480}>
The server is currently unreachable. Please try again later or contact support if the issue
persists.
</Typography>
<Button variant="contained" onClick={() => navigate("/login")}>
Back to Login
</Button>
</Box>
);
}
Iif (isAuthenticated && user && role && backendStatus === BACKEND_STATUS.AVAILABLE) {
clearAuthPortalIntent();
return <Navigate to={getPostLoginRedirectPath(role)} replace />;
}
return (
<React.Fragment>
<NavBar />
<Suspense fallback={<SimpleLoading showLoading={true} />}>
<Outlet />
</Suspense>
<Footer />
</React.Fragment>
);
};
// Home page component with all sections
const HomePage = () => {
return (
<React.Fragment>
<HeroSection />
<FeaturesSection />
<StatsSection />
<BenefitsSection />
</React.Fragment>
);
};
export const LandingPageRoutes = {
path: "/",
element: <LandingPageLayout />,
children: [
{
path: "",
element: <HomePage />,
},
{
path: "about",
element: <AboutPage />,
},
{
path: "contact",
element: <ContactPage />,
},
],
};
|