Files
jobtrackingapp/job-tracker-ui/src/views/LandingPage.tsx
T
2026-08-31 12:09:32 +02:00

282 lines
16 KiB
TypeScript

import React, { useEffect, useState } from "react";
import { Box, Button, Container, Stack, Typography } from "@mui/material";
import { alpha } from "@mui/material/styles";
import { useLocation, useNavigate } from "react-router-dom";
import DashboardIcon from "@mui/icons-material/SpaceDashboardOutlined";
import AlarmIcon from "@mui/icons-material/NotificationsActiveOutlined";
import MatchIcon from "@mui/icons-material/FactCheckOutlined";
import MailIcon from "@mui/icons-material/MarkEmailReadOutlined";
import AttachIcon from "@mui/icons-material/DescriptionOutlined";
import InsightsIcon from "@mui/icons-material/InsightsOutlined";
import { api } from "../api";
import GradientButton from "../components/GradientButton";
import { PUBLIC_PLANS, PublicPlanId } from "../planCatalog";
import { useI18n } from "../i18n/I18nProvider";
import type { TranslationKey } from "../i18n/translations";
import { StackLanguageToggle } from "../layout/AppShell";
const BRAND_DARK = "#0b1020";
const BRAND_PANEL = "#111a33";
const FEATURES: { icon: React.ReactNode; title: TranslationKey; body: TranslationKey }[] = [
{ icon: <DashboardIcon />, title: "landingFeaturePipeline", body: "landingFeaturePipelineBody" },
{ icon: <AlarmIcon />, title: "landingFeatureFollowups", body: "landingFeatureFollowupsBody" },
{ icon: <MatchIcon />, title: "landingFeatureMatch", body: "landingFeatureMatchBody" },
{ icon: <MailIcon />, title: "landingFeatureEmail", body: "landingFeatureEmailBody" },
{ icon: <AttachIcon />, title: "landingFeatureDocuments", body: "landingFeatureDocumentsBody" },
{ icon: <InsightsIcon />, title: "landingFeatureInsights", body: "landingFeatureInsightsBody" },
];
const STEPS: { n: number; title: TranslationKey; body: TranslationKey }[] = [
{ n: 1, title: "landingStepImport", body: "landingStepImportBody" },
{ n: 2, title: "landingStepMatch", body: "landingStepMatchBody" },
{ n: 3, title: "landingStepTailor", body: "landingStepTailorBody" },
{ n: 4, title: "landingStepTrack", body: "landingStepTrackBody" },
{ n: 5, title: "landingStepFollowUp", body: "landingStepFollowUpBody" },
{ n: 6, title: "landingStepAnalyze", body: "landingStepAnalyzeBody" },
];
export default function LandingPage() {
const navigate = useNavigate();
const { language, setLanguage, t } = useI18n();
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.
useEffect(() => {
let active = true;
api
.get("/auth/me")
.then(() => { if (active) navigate("/dashboard", { replace: true }); })
.catch(() => { if (active) setChecking(false); });
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 });
const goToPlan = (plan: PublicPlanId) => {
if (plan === "free") {
navigate("/register");
return;
}
navigate("/login", { state: { from: "/settings" } });
};
if (checking) {
return (
<Box sx={{ minHeight: "100vh", display: "grid", placeItems: "center", bgcolor: BRAND_DARK }}>
<Typography sx={{ color: "#94a3b8" }}>{t("loading")}</Typography>
</Box>
);
}
const gradientText = {
background: "linear-gradient(90deg,#6366f1,#22d3ee)",
WebkitBackgroundClip: "text",
WebkitTextFillColor: "transparent",
} as const;
const plans = PUBLIC_PLANS.map((plan) => ({
...plan,
name: t(plan.id === "free" ? "landingPlanFree" : "landingPlanPro"),
summary: t(plan.id === "free" ? "landingPlanFreeSummary" : "landingPlanProSummary"),
features: plan.features.map((_, index) => t(`landingPlan${plan.id === "free" ? "Free" : "Pro"}Feature${index + 1}` as TranslationKey)),
cta: t(plan.id === "free" ? "landingPlanFreeCta" : "landingPlanProCta"),
}));
return (
<Box sx={{ bgcolor: "background.default" }}>
{/* Top bar */}
<Box sx={{ position: "sticky", top: 0, zIndex: 10, bgcolor: alpha(BRAND_DARK, 0.85), backdropFilter: "blur(8px)", borderBottom: `1px solid ${alpha("#ffffff", 0.08)}` }}>
<Container maxWidth="lg">
<Stack direction="row" alignItems="center" justifyContent="space-between" sx={{ height: 64 }}>
<Stack direction="row" alignItems="center" spacing={1.25}>
<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 }}>Jobbjakt</Typography>
</Stack>
<Stack direction="row" alignItems="center" spacing={1}>
<StackLanguageToggle language={language} setLanguage={setLanguage} onDark />
<GradientButton onClick={goToLogin} sx={{ color: "#0b1020", fontWeight: 700 }}>
{t("landingSignIn")}
</GradientButton>
</Stack>
</Stack>
</Container>
</Box>
{/* Hero */}
<Box sx={{ background: `radial-gradient(1200px 500px at 80% -10%, ${alpha("#6366f1", 0.35)}, transparent), linear-gradient(180deg, ${BRAND_DARK}, ${BRAND_PANEL})`, color: "#fff", py: { xs: 8, md: 12 } }}>
<Container maxWidth="lg">
<Box sx={{ maxWidth: 760 }}>
<Box sx={{ display: "inline-block", px: 1.5, py: 0.5, borderRadius: 999, bgcolor: alpha("#ffffff", 0.08), color: "#a5b4fc", fontSize: 13, fontWeight: 600, letterSpacing: 0.5, mb: 3 }}>
{t("landingEyebrow")}
</Box>
<Typography component="h1" sx={{ fontWeight: 800, fontSize: { xs: 40, md: 60 }, lineHeight: 1.05, mb: 2 }}>
{t("landingHeroBefore")} <Box component="span" sx={gradientText}>{t("landingHeroAccent")}</Box>.
</Typography>
<Typography sx={{ color: "#94a3b8", fontSize: { xs: 17, md: 20 }, mb: 4 }}>
{t("landingHeroBody")}
</Typography>
<Stack direction={{ xs: "column", sm: "row" }} spacing={2}>
<GradientButton size="large" onClick={goToLogin} sx={{ color: "#0b1020", fontWeight: 800, px: 4, py: 1.25 }}>
{t("landingGetStarted")}
</GradientButton>
<Button size="large" variant="outlined" href="#features" sx={{ color: "#e2e8f0", borderColor: alpha("#ffffff", 0.25), px: 3, py: 1.25 }}>
{t("landingSeeFeatures")}
</Button>
</Stack>
<Typography sx={{ color: "#64748b", fontSize: 14, mt: 3 }}>
React · TypeScript · ASP.NET Core · EF Core · FastAPI AI · Gmail
</Typography>
</Box>
</Container>
</Box>
{/* Product preview */}
<Container maxWidth="lg" sx={{ py: { xs: 6, md: 9 } }}>
<Box sx={{ textAlign: "center", mb: 5 }}>
<Typography sx={{ color: "primary.main", fontWeight: 700, letterSpacing: 1, fontSize: 13 }}>{t("landingPreviewEyebrow")}</Typography>
<Typography component="h2" sx={{ fontWeight: 800, fontSize: { xs: 26, md: 34 }, mt: 1 }}>{t("landingPreviewTitle")}</Typography>
</Box>
<Box sx={{ borderRadius: 3, overflow: "hidden", border: "1px solid", borderColor: "divider", boxShadow: 10, mb: 3, bgcolor: "background.paper" }}>
<Box component="img" src="/mockups/dashboard.svg" alt={t("landingDashboardAlt")} sx={{ width: "100%", display: "block" }} />
</Box>
<Box sx={{ display: "grid", gridTemplateColumns: { xs: "1fr", md: "1fr 1fr" }, gap: 3 }}>
<Box sx={{ borderRadius: 3, overflow: "hidden", border: "1px solid", borderColor: "divider", boxShadow: 6, bgcolor: "background.paper" }}>
<Box component="img" src="/mockups/pipeline.svg" alt={t("landingPipelineAlt")} sx={{ width: "100%", display: "block" }} />
</Box>
<Box sx={{ borderRadius: 3, overflow: "hidden", border: "1px solid", borderColor: "divider", boxShadow: 6, bgcolor: "background.paper" }}>
<Box component="img" src="/mockups/workspace.svg" alt={t("landingWorkspaceAlt")} sx={{ width: "100%", display: "block" }} />
</Box>
</Box>
<Typography sx={{ textAlign: "center", color: "text.secondary", fontSize: 13, mt: 2 }}>{t("landingPreviewCaption")}</Typography>
</Container>
{/* Features */}
<Container id="features" maxWidth="lg" sx={{ py: { xs: 7, md: 10 } }}>
<Box sx={{ textAlign: "center", mb: 6 }}>
<Typography sx={{ color: "primary.main", fontWeight: 700, letterSpacing: 1, fontSize: 13 }}>{t("landingFeaturesEyebrow")}</Typography>
<Typography component="h2" sx={{ fontWeight: 800, fontSize: { xs: 28, md: 38 }, mt: 1 }}>{t("landingFeaturesTitle")}</Typography>
<Typography sx={{ color: "text.secondary", fontSize: 18, mt: 1.5 }}>
{t("landingFeaturesBody")}
</Typography>
</Box>
<Box sx={{ display: "grid", gridTemplateColumns: { xs: "1fr", sm: "1fr 1fr", md: "1fr 1fr 1fr" }, gap: 3 }}>
{FEATURES.map((f) => (
<Box key={f.title} sx={{ p: 3, borderRadius: 3, border: "1px solid", borderColor: "divider", bgcolor: "background.paper", transition: "box-shadow .2s, transform .2s", "&:hover": { boxShadow: 6, transform: "translateY(-2px)" } }}>
<Box sx={{ width: 48, height: 48, borderRadius: 2.5, display: "grid", placeItems: "center", bgcolor: alpha("#6366f1", 0.12), color: "primary.main", mb: 2 }}>{f.icon}</Box>
<Typography sx={{ fontWeight: 700, fontSize: 19, mb: 0.75 }}>{t(f.title)}</Typography>
<Typography sx={{ color: "text.secondary", fontSize: 15 }}>{t(f.body)}</Typography>
</Box>
))}
</Box>
</Container>
{/* How it works */}
<Box sx={{ background: `linear-gradient(180deg, ${BRAND_DARK}, ${BRAND_PANEL})`, color: "#fff", py: { xs: 7, md: 10 } }}>
<Container maxWidth="lg">
<Box sx={{ textAlign: "center", mb: 6 }}>
<Typography sx={{ color: "#a5b4fc", fontWeight: 700, letterSpacing: 1, fontSize: 13 }}>{t("landingHowEyebrow")}</Typography>
<Typography component="h2" sx={{ fontWeight: 800, fontSize: { xs: 28, md: 38 }, mt: 1 }}>{t("landingHowTitle")}</Typography>
</Box>
<Box sx={{ display: "grid", gridTemplateColumns: { xs: "1fr", sm: "1fr 1fr", md: "1fr 1fr 1fr" }, gap: 3 }}>
{STEPS.map((s) => (
<Box key={s.n} sx={{ p: 3, borderRadius: 3, border: `1px solid ${alpha("#ffffff", 0.1)}`, bgcolor: alpha("#ffffff", 0.03) }}>
<Box sx={{ width: 40, height: 40, borderRadius: 999, display: "grid", placeItems: "center", background: "linear-gradient(135deg,#6366f1,#22d3ee)", color: "#0b1020", fontWeight: 900, mb: 1.5 }}>{s.n}</Box>
<Typography sx={{ fontWeight: 700, fontSize: 18, mb: 0.5 }}>{t(s.title)}</Typography>
<Typography sx={{ color: "#94a3b8", fontSize: 15 }}>{t(s.body)}</Typography>
</Box>
))}
</Box>
</Container>
</Box>
{/* Plans */}
<Container id="plans" maxWidth="lg" sx={{ py: { xs: 7, md: 10 } }}>
<Box sx={{ textAlign: "center", mb: 6 }}>
<Typography sx={{ color: "primary.main", fontWeight: 700, letterSpacing: 1, fontSize: 13 }}>{t("landingPlansEyebrow")}</Typography>
<Typography component="h2" sx={{ fontWeight: 800, fontSize: { xs: 28, md: 38 }, mt: 1 }}>{t("landingPlansTitle")}</Typography>
<Typography sx={{ color: "text.secondary", fontSize: 18, mt: 1.5 }}>
{t("landingPlansBody")}
</Typography>
</Box>
<Box sx={{ display: "grid", gridTemplateColumns: { xs: "1fr", md: "repeat(2, minmax(0, 1fr))" }, gap: 3, alignItems: "stretch", maxWidth: 900, mx: "auto" }}>
{plans.map((plan) => (
<Box
component="section"
aria-labelledby={`plan-${plan.id}`}
key={plan.id}
sx={{
p: 3.5,
borderRadius: 3,
position: "relative",
bgcolor: "background.paper",
border: "1px solid",
borderColor: plan.id === "pro" ? "primary.main" : "divider",
boxShadow: plan.id === "pro" ? 5 : 0,
display: "flex",
flexDirection: "column",
}}
>
{plan.id === "pro" && (
<Box sx={{ position: "absolute", top: -13, left: 24, px: 1.5, py: 0.5, borderRadius: 999, background: "linear-gradient(90deg,#6366f1,#22d3ee)", color: "#0b1020", fontSize: 12, fontWeight: 800 }}>
{t("landingAiAssistance")}
</Box>
)}
<Typography id={`plan-${plan.id}`} component="h3" sx={{ fontWeight: 800, fontSize: 24 }}>{plan.name}</Typography>
<Typography sx={{ color: "text.secondary", mt: 1 }}>{plan.summary}</Typography>
<Stack spacing={1.25} sx={{ my: 2.5, flex: 1 }}>
{plan.features.map((feature) => (
<Stack key={feature} direction="row" spacing={1.25} alignItems="flex-start">
<Box aria-hidden="true" sx={{ color: "success.main", fontWeight: 900, lineHeight: 1.4 }}>✓</Box>
<Typography sx={{ fontSize: 15, color: "text.secondary" }}>{feature}</Typography>
</Stack>
))}
</Stack>
{plan.id === "pro" ? (
<GradientButton fullWidth onClick={() => goToPlan(plan.id)} sx={{ color: "#0b1020", fontWeight: 800 }}>
{plan.cta}
</GradientButton>
) : (
<Button fullWidth variant="outlined" onClick={() => goToPlan(plan.id)} sx={{ fontWeight: 700 }}>
{plan.cta}
</Button>
)}
</Box>
))}
</Box>
<Typography sx={{ textAlign: "center", color: "text.secondary", fontSize: 13, mt: 3 }}>
{t("landingCommercialTerms")}
</Typography>
</Container>
{/* CTA */}
<Container maxWidth="lg" sx={{ py: { xs: 7, md: 10 } }}>
<Box sx={{ borderRadius: 4, p: { xs: 4, md: 6 }, background: "linear-gradient(120deg,#0f172a,#1e293b)", color: "#fff", display: "flex", flexDirection: { xs: "column", md: "row" }, alignItems: { md: "center" }, justifyContent: "space-between", gap: 3 }}>
<Box>
<Typography sx={{ fontWeight: 800, fontSize: { xs: 24, md: 30 }, mb: 1 }}>{t("landingReadyTitle")}</Typography>
<Typography sx={{ color: "#94a3b8", fontSize: 17 }}>{t("landingReadyBody")}</Typography>
</Box>
<GradientButton size="large" onClick={goToLogin} sx={{ color: "#0b1020", fontWeight: 800, px: 4, py: 1.25, whiteSpace: "nowrap" }}>
{t("landingSignInArrow")}
</GradientButton>
</Box>
</Container>
{/* Footer */}
<Box sx={{ borderTop: "1px solid", borderColor: "divider", py: 4 }}>
<Container maxWidth="lg">
<Stack direction={{ xs: "column", sm: "row" }} justifyContent="space-between" alignItems="center" spacing={1}>
<Typography sx={{ color: "text.secondary", fontSize: 14 }}>{t("landingFooter", { year: new Date().getFullYear() })}</Typography>
<Button variant="text" onClick={goToLogin} sx={{ fontWeight: 700 }}>{t("landingSignIn")}</Button>
</Stack>
</Container>
</Box>
</Box>
);
}