Skip to content
83 changes: 80 additions & 3 deletions frontend/src/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import PrivacyModal from './components/PrivacyModal';
import AuthLinkPage from './components/AuthLinkPage';
import UserNav from './components/UserNav';
import ThemeToggle from './components/ThemeToggle';
import LandingPage from './components/landing/LandingPage';
import {
getResumes,
getApplications,
Expand All @@ -39,12 +40,31 @@ export default function App() {
const [authMode, setAuthMode] = useState('login');
const [currentPath, setCurrentPath] = useState(() => (typeof window !== 'undefined' ? window.location.pathname : '/'));

const navigateTo = (path) => {
if (typeof window !== 'undefined') {
window.history.pushState(null, '', path);
}
setCurrentPath(path);
if (typeof window !== 'undefined') {
window.scrollTo(0, 0);
}
};

useEffect(() => {
const onPopState = () => setCurrentPath(window.location.pathname);
window.addEventListener('popstate', onPopState);
return () => window.removeEventListener('popstate', onPopState);
}, []);

useEffect(() => {
if (getCurrentUser() && window.location.pathname === '/') {
if (typeof window !== 'undefined') {
window.history.replaceState(null, '', '/app');
}
setCurrentPath('/app');
}
}, []);


const [theme, setTheme] = useState(() => {
return localStorage.getItem('jobhelperguru_theme') || 'light';
Expand All @@ -53,8 +73,10 @@ export default function App() {
useEffect(() => {
if (theme === 'dark') {
document.documentElement.classList.add('dark');
document.body.classList.add('dark');
} else {
document.documentElement.classList.remove('dark');
document.body.classList.remove('dark');
}
localStorage.setItem('jobhelperguru_theme', theme);
}, [theme]);
Expand Down Expand Up @@ -235,9 +257,9 @@ export default function App() {
<>
<AuthLinkPage
path={currentPath}
onGoHome={() => setCurrentPath('/')}
onGoHome={() => navigateTo('/')}
onOpenAuth={(mode) => {
setCurrentPath('/');
navigateTo('/');
setAuthMode(mode || 'login');
setIsAuthOpen(true);
}}
Expand All @@ -255,14 +277,69 @@ export default function App() {
);
}

if (currentPath !== '/app') {
return (
<>
<LandingPage
currentUser={currentUser}
theme={theme}
onToggleTheme={toggleTheme}
onSignIn={() => {
setAuthMode('login');
setIsAuthOpen(true);
}}
onGetStarted={() => {
setAuthMode('register');
setIsAuthOpen(true);
}}
onGoToDashboard={() => navigateTo('/app')}
onExploreGuest={() => navigateTo('/app')}
onOpenPrivacy={() => setIsPrivacyOpen(true)}
/>
<AuthModal
isOpen={isAuthOpen}
onClose={() => setIsAuthOpen(false)}
initialMode={authMode}
onSuccess={async (user, isNewUser = false) => {
sessionRef.current += 1;
resetAccountState();
setCurrentUser(user);
await loadInitialData();
try {
const settings = await getSettings();
if (isNewUser || (!settings?.has_api_key && !settings?.use_offline_mode)) {
setIsOnboardingSettings(true);
setIsSettingsOpen(true);
}
} catch (e) {
if (isNewUser) {
setIsOnboardingSettings(true);
setIsSettingsOpen(true);
}
}
navigateTo('/app');
}}
/>
<PrivacyModal
isOpen={isPrivacyOpen}
onClose={() => setIsPrivacyOpen(false)}
/>
</>
);
}

return (

<div className="min-h-screen bg-[#f3f6f8] text-[#000000] flex flex-col font-sans selection:bg-[#0a66c2] selection:text-white">
{/* Top Corporate Navbar */}
<header className="sticky top-0 z-40 bg-white border-b border-[#e0e0e0]">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 h-16 flex items-center justify-between gap-4">
{/* Brand Logo */}
<div className="flex items-center gap-3 cursor-pointer" onClick={() => setActiveTab('analyzer')}>
<div
data-testid="brand-logo"
className="flex items-center gap-3 cursor-pointer"
onClick={() => navigateTo('/')}
>
<div className="w-9 h-9 rounded-md bg-[#0a66c2] flex items-center justify-center shadow-sm">
<Briefcase size={20} className="text-white" />
</div>
Expand Down
72 changes: 72 additions & 0 deletions frontend/src/components/landing/FaqSection.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { useState } from 'react';
import { ChevronDown, HelpCircle } from 'lucide-react';

export default function FaqSection() {
const [openIndex, setOpenIndex] = useState(null);

const faqs = [
{
q: 'Is JobHelperGuru free to use?',
a: 'Yes! JobHelperGuru includes a 100% free offline heuristic NLP mode that works immediately with zero API keys required. If you want ultra-fast cloud model inference, you can optionally bring your own API key for Groq, Nvidia NIM, OpenAI, or connect a local Ollama instance.',
},
{
q: 'What is the BulletCraft Framework?',
a: 'The BulletCraft Framework is an accomplishment engineering formula: Accomplished [X], measured by [Y], by doing [Z]. Instead of listing duties (e.g., "Worked on APIs"), it articulates measurable scale, impact, and tooling (e.g., "Engineered 14 resilient FastAPI microservices handling 2.4M daily requests, cutting p99 latency by 38% using Redis").',
},
{
q: 'Are my resumes and credentials secure?',
a: 'Absolutely. Your private API keys are encrypted at rest using AES-256 Fernet authenticated symmetric encryption. Resumes are isolated per user in private Cloudflare R2 object storage and retrieved strictly via time-limited presigned URLs. No third-party data selling or public indexing.',
},
{
q: 'Can I export my pipeline data?',
a: 'Yes. With one click, you can download a formatted, styled Excel (.xlsx) spreadsheet complete with status indicators, interview round notes, and application links for offline archiving and audit tracking.',
},
];

return (
<section id="faq" className="py-16 sm:py-24 max-w-4xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="text-center mb-12">
<span className="text-xs font-bold uppercase tracking-widest text-[#0a66c2] dark:text-[#70b5f9] bg-white dark:bg-[#1a1d24] px-3 py-1 rounded-full border border-[#e0e0e0] dark:border-[#2b313c]">
Frequently Asked Questions
</span>
<h2 className="text-2xl sm:text-3xl font-extrabold text-[#000000] dark:text-[#f3f6f8] mt-3 tracking-tight">
Got Questions? We Have Answers.
</h2>
</div>

<div className="space-y-4">
{faqs.map((faq, idx) => {
const isOpen = openIndex === idx;
return (
<div
key={idx}
className="card-corporate rounded-lg border border-[#e0e0e0] dark:border-[#2b313c] bg-white dark:bg-[#1a1d24] overflow-hidden transition-all shadow-none"
>
<button
onClick={() => setOpenIndex(isOpen ? null : idx)}
className="w-full px-6 py-4 text-left flex items-center justify-between gap-4 focus:outline-none"
aria-expanded={isOpen}
>
<span className="text-sm sm:text-base font-bold text-[#000000] dark:text-[#f3f6f8]">
{faq.q}
</span>
<ChevronDown
size={18}
className={`text-[#666666] dark:text-[#9aa1b2] transition-transform duration-200 shrink-0 ${
isOpen ? 'rotate-180 text-[#0a66c2] dark:text-[#70b5f9]' : ''
}`}
/>
</button>

{isOpen && (
<div className="px-6 pb-5 pt-1 text-xs sm:text-sm text-[#666666] dark:text-[#9aa1b2] leading-relaxed border-t border-[#e0e0e0] dark:border-[#2b313c]">
{faq.a}
</div>
)}
</div>
);
})}
</div>
</section>
);
}
81 changes: 81 additions & 0 deletions frontend/src/components/landing/HowItWorks.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { FileSearch, Sparkles, FileSpreadsheet, ArrowRight } from 'lucide-react';

export default function HowItWorks() {
const steps = [
{
number: '01',
icon: FileSearch,
title: 'Parse the True Requirements',
description:
'Drop in a job posting URL or raw description. JobHelperGuru automatically extracts standardized job titles, compensation packages, required vs. preferred tech stacks, and hidden ATS keyword indexes.',
},
{
number: '02',
icon: Sparkles,
title: 'Optimize Bullets & Pick the Best Resume',
description:
'Audit your resume library to select the highest-scoring resume. Use the BulletCraft Framework to transform weak bullet points into quantifiable achievements, and generate tailored outreach pitches in seconds.',
},
{
number: '03',
icon: FileSpreadsheet,
title: 'Track Deadlines & Export to Excel',
description:
'Seamlessly progress applications across Kanban stages (Wishlist, Applied, Interviewing, Offered). Get automatic follow-up reminders and export clean, styled Excel spreadsheets for offline recordkeeping.',
},
];

return (
<section id="how-it-works" className="py-16 sm:py-24 bg-[#f3f6f8] dark:bg-[#0f1115] border-y border-[#e0e0e0] dark:border-[#2b313c] transition-colors">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="text-center max-w-3xl mx-auto mb-14">
<span className="text-xs font-bold uppercase tracking-widest text-[#0a66c2] dark:text-[#70b5f9] bg-white dark:bg-[#1a1d24] px-3 py-1 rounded-full border border-[#e0e0e0] dark:border-[#2b313c]">
3-Step Workflow
</span>
<h2 className="text-2xl sm:text-3xl font-extrabold text-[#000000] dark:text-[#f3f6f8] mt-3 tracking-tight">
How JobHelperGuru Accelerates Your Hunt
</h2>
<p className="text-sm text-[#666666] dark:text-[#9aa1b2] mt-3 leading-relaxed">
From discovering a posting to negotiating an offer, streamline every phase of your job search in minutes.
</p>
</div>

<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
{steps.map((step, idx) => {
const Icon = step.icon;
return (
<div
key={idx}
className="card-corporate relative p-6 sm:p-7 rounded-lg bg-white dark:bg-[#1a1d24] border border-[#e0e0e0] dark:border-[#2b313c] flex flex-col justify-between"
>
<div>
<div className="flex items-center justify-between mb-5">
<span className="text-3xl font-black text-[#c1c6d4] dark:text-[#404856] font-mono">
{step.number}
</span>
<div className="w-10 h-10 rounded-md bg-[#f3f6f8] dark:bg-[#0f1115] border border-[#e0e0e0] dark:border-[#2b313c] text-[#0a66c2] dark:text-[#70b5f9] flex items-center justify-center">
<Icon size={18} />
</div>
</div>

<h3 className="text-base font-bold text-[#000000] dark:text-[#f3f6f8] mb-2">
{step.title}
</h3>
<p className="text-xs sm:text-sm text-[#666666] dark:text-[#9aa1b2] leading-relaxed">
{step.description}
</p>
</div>

{idx < steps.length - 1 && (
<div className="hidden lg:block absolute -right-4 top-1/2 -translate-y-1/2 text-[#c1c6d4] dark:text-[#404856] z-10">
<ArrowRight size={20} />
</div>
)}
</div>
);
})}
</div>
</div>
</section>
);
}
Loading
Loading