feat(plans): publish honest Free and Pro
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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)} />
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user