Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| daa9694bc7 |
@@ -203,7 +203,7 @@ function Shell({ jobPageSize, setJobPageSize, jobColumns, setJobColumns, themeMo
|
||||
}, []);
|
||||
|
||||
if (requireAuth === null || !authResolved) return <Box sx={{ p: 4 }}><Typography variant="h6">Loading...</Typography></Box>;
|
||||
if (requireAuth && !me) return <Navigate to="/login" replace state={{ from: path }} />;
|
||||
if (requireAuth && !me) return <Navigate to="/" replace state={{ from: path }} />;
|
||||
|
||||
const pageTitle = titleFor(path, t);
|
||||
const breadcrumbs = breadcrumbsFor(path, t);
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
import React from "react";
|
||||
import "@testing-library/jest-dom";
|
||||
import { render, screen, waitFor } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { MemoryRouter, Route, Routes, useLocation } from "react-router-dom";
|
||||
import { api } from "./api";
|
||||
import LandingPage from "./pages/LandingPage";
|
||||
|
||||
jest.mock("./api", () => ({
|
||||
api: {
|
||||
get: jest.fn(),
|
||||
post: jest.fn(),
|
||||
interceptors: { request: { use: jest.fn() }, response: { use: jest.fn() } },
|
||||
},
|
||||
}));
|
||||
|
||||
const mockedApi = api as jest.Mocked<typeof api>;
|
||||
|
||||
function LoginStub() {
|
||||
const location = useLocation() as { state?: { from?: string } };
|
||||
return <div>login page, from={location.state?.from ?? "none"}</div>;
|
||||
}
|
||||
|
||||
// Regression check for the auth-guard fix: a protected route bounces an
|
||||
// unauthenticated visitor to "/" with `state.from` set to the page they
|
||||
// wanted. The home page must forward that state to /login so sign-in
|
||||
// returns them to the originally-requested page instead of dropping them
|
||||
// on /jobs.
|
||||
describe("LandingPage forwards deep-link intent to /login", () => {
|
||||
it("preserves location.state.from through the Sign in CTA", async () => {
|
||||
mockedApi.get.mockRejectedValueOnce(new Error("401")); // /auth/me: not signed in
|
||||
|
||||
render(
|
||||
<MemoryRouter initialEntries={[{ pathname: "/", state: { from: "/jobs/42" } }]}>
|
||||
<Routes>
|
||||
<Route path="/" element={<LandingPage />} />
|
||||
<Route path="/login" element={<LoginStub />} />
|
||||
</Routes>
|
||||
</MemoryRouter>,
|
||||
);
|
||||
|
||||
await waitFor(() => expect(mockedApi.get).toHaveBeenCalledWith("/auth/me"));
|
||||
|
||||
const signInButtons = await screen.findAllByText("Sign in", { selector: "button" });
|
||||
await userEvent.click(signInButtons[0]);
|
||||
|
||||
expect(await screen.findByText("login page, from=/jobs/42")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -2,7 +2,7 @@ import React, { useEffect, useState } from "react";
|
||||
|
||||
import { Box, Button, Container, Stack, Typography } from "@mui/material";
|
||||
import { alpha } from "@mui/material/styles";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useLocation, useNavigate } from "react-router-dom";
|
||||
|
||||
import DashboardIcon from "@mui/icons-material/SpaceDashboardOutlined";
|
||||
import AlarmIcon from "@mui/icons-material/NotificationsActiveOutlined";
|
||||
@@ -42,6 +42,7 @@ const PRICING: { name: string; price: string; cadence: string; highlight: boolea
|
||||
|
||||
export default function LandingPage() {
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation() as { state?: { from?: string } };
|
||||
const [checking, setChecking] = useState(true);
|
||||
|
||||
// If the visitor already has a session, send them straight into the app.
|
||||
@@ -54,6 +55,11 @@ export default function LandingPage() {
|
||||
return () => { active = false; };
|
||||
}, [navigate]);
|
||||
|
||||
// A protected route redirects unauthenticated visitors here with the page they
|
||||
// wanted in location state; forward it to /login so sign-in returns them there
|
||||
// instead of dropping them on /jobs.
|
||||
const goToLogin = () => navigate("/login", { state: location.state });
|
||||
|
||||
if (checking) {
|
||||
return (
|
||||
<Box sx={{ minHeight: "100vh", display: "grid", placeItems: "center", bgcolor: BRAND_DARK }}>
|
||||
@@ -78,7 +84,7 @@ export default function LandingPage() {
|
||||
<Box sx={{ width: 30, height: 30, borderRadius: "8px", background: "linear-gradient(135deg,#6366f1,#22d3ee)", display: "grid", placeItems: "center", color: BRAND_DARK, fontWeight: 900 }}>✓</Box>
|
||||
<Typography sx={{ color: "#fff", fontWeight: 800, fontSize: 20 }}>JobTrack</Typography>
|
||||
</Stack>
|
||||
<Button variant="contained" onClick={() => navigate("/login")} sx={{ background: "linear-gradient(90deg,#6366f1,#22d3ee)", color: "#0b1020", fontWeight: 700 }}>
|
||||
<Button variant="contained" onClick={goToLogin} sx={{ background: "linear-gradient(90deg,#6366f1,#22d3ee)", color: "#0b1020", fontWeight: 700 }}>
|
||||
Sign in
|
||||
</Button>
|
||||
</Stack>
|
||||
@@ -100,7 +106,7 @@ export default function LandingPage() {
|
||||
right job — all in one focused workspace. Assistive, never autonomous: you approve every draft.
|
||||
</Typography>
|
||||
<Stack direction={{ xs: "column", sm: "row" }} spacing={2}>
|
||||
<Button size="large" variant="contained" onClick={() => navigate("/login")} sx={{ background: "linear-gradient(90deg,#6366f1,#22d3ee)", color: "#0b1020", fontWeight: 800, px: 4, py: 1.25 }}>
|
||||
<Button size="large" variant="contained" onClick={goToLogin} sx={{ background: "linear-gradient(90deg,#6366f1,#22d3ee)", color: "#0b1020", fontWeight: 800, px: 4, py: 1.25 }}>
|
||||
Get started
|
||||
</Button>
|
||||
<Button size="large" variant="outlined" href="#features" sx={{ color: "#e2e8f0", borderColor: alpha("#ffffff", 0.25), px: 3, py: 1.25 }}>
|
||||
@@ -217,7 +223,7 @@ export default function LandingPage() {
|
||||
<Button
|
||||
fullWidth
|
||||
variant={tier.highlight ? "contained" : "outlined"}
|
||||
onClick={() => navigate("/login")}
|
||||
onClick={goToLogin}
|
||||
sx={tier.highlight ? { background: "linear-gradient(90deg,#6366f1,#22d3ee)", color: "#0b1020", fontWeight: 800 } : { fontWeight: 700 }}
|
||||
>
|
||||
{tier.cta}
|
||||
@@ -237,7 +243,7 @@ export default function LandingPage() {
|
||||
<Typography sx={{ fontWeight: 800, fontSize: { xs: 24, md: 30 }, mb: 1 }}>Ready to organize your search?</Typography>
|
||||
<Typography sx={{ color: "#94a3b8", fontSize: 17 }}>Sign in to start tracking applications, tailoring CVs, and following up with intent.</Typography>
|
||||
</Box>
|
||||
<Button size="large" variant="contained" onClick={() => navigate("/login")} sx={{ background: "linear-gradient(90deg,#6366f1,#22d3ee)", color: "#0b1020", fontWeight: 800, px: 4, py: 1.25, whiteSpace: "nowrap" }}>
|
||||
<Button size="large" variant="contained" onClick={goToLogin} sx={{ background: "linear-gradient(90deg,#6366f1,#22d3ee)", color: "#0b1020", fontWeight: 800, px: 4, py: 1.25, whiteSpace: "nowrap" }}>
|
||||
Sign in →
|
||||
</Button>
|
||||
</Box>
|
||||
@@ -248,7 +254,7 @@ export default function LandingPage() {
|
||||
<Container maxWidth="lg">
|
||||
<Stack direction={{ xs: "column", sm: "row" }} justifyContent="space-between" alignItems="center" spacing={1}>
|
||||
<Typography sx={{ color: "text.secondary", fontSize: 14 }}>© {new Date().getFullYear()} JobTrack — a focused workspace for the modern job search.</Typography>
|
||||
<Button variant="text" onClick={() => navigate("/login")} sx={{ fontWeight: 700 }}>Sign in</Button>
|
||||
<Button variant="text" onClick={goToLogin} sx={{ fontWeight: 700 }}>Sign in</Button>
|
||||
</Stack>
|
||||
</Container>
|
||||
</Box>
|
||||
|
||||
Reference in New Issue
Block a user