feat(plans): publish honest Free and Pro
CI and Deploy / test (pull_request) Successful in 5m6s
CI and Deploy / deploy (pull_request) Has been skipped

This commit is contained in:
cesnimda
2026-08-15 18:04:18 +02:00
parent a7c25499bb
commit a25c31b93a
26 changed files with 403 additions and 93 deletions
@@ -29,6 +29,7 @@ import {
import { useToast } from "../toast";
import { useI18n } from "../i18n/I18nProvider";
import { useAccountPlan } from "../accountPlan";
import ProFeatureNotice from "../components/ProFeatureNotice";
import type { UserOperation } from "../types";
import {
emptyStructuredCv,
@@ -411,7 +412,13 @@ export default function CareerProfilePage() {
<Box id="career-cv-import" sx={{ gridColumn: "1 / -1", p: { xs: 1.5, sm: 2 }, borderRadius: 3, border: "1px solid", borderColor: "divider", backgroundColor: "background.default", display: careerOnly ? "block" : "none", scrollMarginTop: 96 }}>
{!canUseAi && <Alert severity="info" sx={{ mb: 2 }} action={<Button href="/settings" size="small">View Pro</Button>}>AI CV import, rebuilding, improvement, and reprocessing require Pro. Manual profile editing remains available.</Alert>}
{!canUseAi && (
<Box sx={{ mb: 2 }}>
<ProFeatureNotice featureKey="career-ai" title="Build your Career Profile faster with Pro.">
Import, rebuild and improve CV content with AI. Manual profile editing remains available on Free.
</ProFeatureNotice>
</Box>
)}
{profileDirty ? <Alert severity="warning" sx={{ mb: 2 }}>You have unsaved career edits. Save them before running actions that use the stored profile.</Alert> : null}
<Box sx={{ display: "flex", justifyContent: "space-between", gap: 2, flexWrap: "wrap", alignItems: "center", mb: 1.5 }}>
<Box>
+8 -1
View File
@@ -31,6 +31,7 @@ import {
cvBuilderApi, getCvPageCount, getCvPageMetrics, moveItem,
} from "../cvBuilder";
import { useAccountPlan } from "../accountPlan";
import ProFeatureNotice from "../components/ProFeatureNotice";
import { useDialogActions } from "../dialogs";
const FONTS = [
@@ -865,7 +866,13 @@ function AiToolsTab() {
return (
<Stack spacing={1.5}>
<Alert severity="info" sx={{ py: 0.5 }} action={!canUseAi ? <Button href="/settings" size="small">View Pro</Button> : undefined}>{canUseAi ? "AI suggestions never change your profile automatically. Copy what you like back into your CV." : "AI writing assistance requires Pro. Your CV content remains editable."}</Alert>
{canUseAi ? (
<Alert severity="info" sx={{ py: 0.5 }}>AI suggestions never change your profile automatically. Copy what you like back into your CV.</Alert>
) : (
<ProFeatureNotice featureKey="cv-writing-ai" title="Refine CV wording with Pro.">
Generate optional writing suggestions while keeping all CV content editable on Free.
</ProFeatureNotice>
)}
<TextField label="Text to improve" multiline minRows={4} fullWidth size="small" value={text} onChange={(e) => setText(e.target.value)}
placeholder="Paste a summary, a bullet, or a whole section…" />
<TextField label="Target role (optional)" size="small" fullWidth value={role} onChange={(e) => setRole(e.target.value)} />
+44 -41
View File
@@ -13,15 +13,16 @@ import InsightsIcon from "@mui/icons-material/InsightsOutlined";
import { api } from "../api";
import GradientButton from "../components/GradientButton";
import { PUBLIC_PLANS, PublicPlanId } from "../planCatalog";
const BRAND_DARK = "#0b1020";
const BRAND_PANEL = "#111a33";
const FEATURES: { icon: React.ReactNode; title: string; body: string }[] = [
{ icon: <DashboardIcon />, title: "Centralized pipeline", body: "Track every application across Applied, Waiting, Interview, Offer, Rejected and Ghosted — drag to update." },
{ icon: <AlarmIcon />, title: "Smart follow-ups", body: "Reminders surface what needs attention next, with a grounded draft ready to review and send." },
{ icon: <AlarmIcon />, title: "Smart follow-ups", body: "Reminders surface what needs attention next; Pro can help draft a response for you to review." },
{ icon: <MatchIcon />, title: "Honest CV match", body: "A deterministic keyword-coverage score with matched vs missing skills — not an opaque black box." },
{ icon: <MailIcon />, title: "Email correspondence", body: "Link Gmail threads to a job; new replies appear automatically without re-importing." },
{ icon: <MailIcon />, title: "Email correspondence", body: "Keep linked Gmail and Microsoft messages in the context of the right application." },
{ icon: <AttachIcon />, title: "Attachments & docs", body: "Keep resumes, cover letters and portfolios versioned per application, right where you need them." },
{ icon: <InsightsIcon />, title: "Dashboard & insights", body: "Response rates, funnel, time-in-stage and skill demand across your whole search." },
];
@@ -29,18 +30,12 @@ const FEATURES: { icon: React.ReactNode; title: string; body: string }[] = [
const STEPS: { n: number; title: string; body: string }[] = [
{ n: 1, title: "Import", body: "Paste a job URL or use the bookmarklet — we parse the role into structured fields." },
{ n: 2, title: "Match", body: "See how your CV covers the role: matched keywords and the gaps to close." },
{ n: 3, title: "Tailor", body: "AI drafts a tailored CV and cover letter — you review every word before it goes out." },
{ n: 3, title: "Tailor", body: "With Pro, AI can draft a tailored CV and cover letter — you review every word before it goes out." },
{ n: 4, title: "Track", body: "Move it through the pipeline; documents, notes and history stay attached." },
{ n: 5, title: "Follow up", body: "Linked email threads and reminders keep momentum with grounded replies." },
{ n: 6, title: "Analyze", body: "See what's working — response rate, funnel and time-in-stage — and focus your effort." },
];
const PRICING: { name: string; price: string; cadence: string; highlight: boolean; features: string[]; cta: string }[] = [
{ name: "Free", price: "£0", cadence: "forever", highlight: false, cta: "Get started", features: ["Unlimited job tracking & pipeline", "One-click capture (bookmarklet + PWA)", "Deterministic CV↔job match score", "3 AI CV tailors / month"] },
{ name: "Pro", price: "£9", cadence: "/ month · billed monthly or yearly", highlight: true, cta: "Start Pro", features: ["Everything in Free", "Unlimited AI CV & cover-letter tailoring", "CV versions + factuality guardrail", "Gmail correspondence CRM", "Analytics drill-downs"] },
{ name: "Bring your own key", price: "£3", cadence: "/ month + your AI key", highlight: false, cta: "Get started", features: ["Everything in Pro", "Use your own Gemini / Groq key", "Unlimited AI at provider cost", "Privacy-first & self-host friendly"] },
];
export default function LandingPage() {
const navigate = useNavigate();
const location = useLocation() as { state?: { from?: string } };
@@ -60,6 +55,13 @@ export default function LandingPage() {
// 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 (
@@ -83,7 +85,7 @@ export default function LandingPage() {
<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 }}>JobTrack</Typography>
<Typography sx={{ color: "#fff", fontWeight: 800, fontSize: 20 }}>Jobbjakt</Typography>
</Stack>
<GradientButton onClick={goToLogin} sx={{ color: "#0b1020", fontWeight: 700 }}>
Sign in
@@ -128,7 +130,7 @@ export default function LandingPage() {
<Typography component="h2" sx={{ fontWeight: 800, fontSize: { xs: 26, md: 34 }, mt: 1 }}>Your whole search, at a glance</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="JobTrack dashboard — KPIs, funnel, response trend and follow-ups" sx={{ width: "100%", display: "block" }} />
<Box component="img" src="/mockups/dashboard.svg" alt="Jobbjakt dashboard — KPIs, funnel, response trend and follow-ups" 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" }}>
@@ -180,61 +182,62 @@ export default function LandingPage() {
</Container>
</Box>
{/* Pricing */}
<Container id="pricing" maxWidth="lg" sx={{ py: { xs: 7, md: 10 } }}>
{/* 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 }}>PRICING</Typography>
<Typography component="h2" sx={{ fontWeight: 800, fontSize: { xs: 28, md: 38 }, mt: 1 }}>Honest, simple pricing</Typography>
<Typography sx={{ color: "primary.main", fontWeight: 700, letterSpacing: 1, fontSize: 13 }}>PLANS</Typography>
<Typography component="h2" sx={{ fontWeight: 800, fontSize: { xs: 28, md: 38 }, mt: 1 }}>Two clear plans</Typography>
<Typography sx={{ color: "text.secondary", fontSize: 18, mt: 1.5 }}>
Billed monthly or yearly never by the week. Cancel anytime.
Free covers the core job search. Pro adds AI assistance and Pro CV themes.
</Typography>
</Box>
<Box sx={{ display: "grid", gridTemplateColumns: { xs: "1fr", md: "repeat(3, 1fr)" }, gap: 3, alignItems: "start" }}>
{PRICING.map((tier) => (
<Box sx={{ display: "grid", gridTemplateColumns: { xs: "1fr", md: "repeat(2, minmax(0, 1fr))" }, gap: 3, alignItems: "stretch", maxWidth: 900, mx: "auto" }}>
{PUBLIC_PLANS.map((plan) => (
<Box
key={tier.name}
component="section"
aria-labelledby={`plan-${plan.id}`}
key={plan.id}
sx={{
p: 3.5,
borderRadius: 3,
position: "relative",
bgcolor: "background.paper",
border: "2px solid",
borderColor: tier.highlight ? "primary.main" : "divider",
boxShadow: tier.highlight ? 8 : 0,
border: "1px solid",
borderColor: plan.id === "pro" ? "primary.main" : "divider",
boxShadow: plan.id === "pro" ? 5 : 0,
display: "flex",
flexDirection: "column",
}}
>
{tier.highlight && (
{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 }}>
Most popular
AI ASSISTANCE
</Box>
)}
<Typography sx={{ fontWeight: 700, fontSize: 18 }}>{tier.name}</Typography>
<Stack direction="row" alignItems="baseline" spacing={0.75} sx={{ my: 1.5 }}>
<Typography sx={{ fontWeight: 900, fontSize: 40, lineHeight: 1 }}>{tier.price}</Typography>
<Typography sx={{ color: "text.secondary", fontSize: 14 }}>{tier.cadence}</Typography>
</Stack>
<Stack spacing={1.25} sx={{ my: 2.5 }}>
{tier.features.map((f) => (
<Stack key={f} direction="row" spacing={1.25} alignItems="flex-start">
<Box sx={{ color: "success.main", fontWeight: 900, lineHeight: 1.4 }}></Box>
<Typography sx={{ fontSize: 15, color: "text.secondary" }}>{f}</Typography>
<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>
{tier.highlight ? (
<GradientButton fullWidth onClick={goToLogin} sx={{ color: "#0b1020", fontWeight: 800 }}>
{tier.cta}
{plan.id === "pro" ? (
<GradientButton fullWidth onClick={() => goToPlan(plan.id)} sx={{ color: "#0b1020", fontWeight: 800 }}>
{plan.cta}
</GradientButton>
) : (
<Button fullWidth variant="outlined" onClick={goToLogin} sx={{ fontWeight: 700 }}>
{tier.cta}
<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 }}>
Prices indicative assistive, never autonomous: you always review and send. No auto-apply spam.
Commercial terms appear only in configured checkout. Your applications, documents and exports remain accessible if your plan changes.
</Typography>
</Container>
@@ -255,7 +258,7 @@ export default function LandingPage() {
<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 }}>© {new Date().getFullYear()} JobTrack a focused workspace for the modern job search.</Typography>
<Typography sx={{ color: "text.secondary", fontSize: 14 }}>© {new Date().getFullYear()} Jobbjakt a focused workspace for the modern job search.</Typography>
<Button variant="text" onClick={goToLogin} sx={{ fontWeight: 700 }}>Sign in</Button>
</Stack>
</Container>