Separate private and agency subscription plans, add LinkedIn OAuth integration
This commit is contained in:
+168
-32
@@ -17,21 +17,23 @@ DB_PATH = os.getenv("DB_PATH", "/var/www/autojobs/autojobs.db")
|
||||
router = APIRouter(prefix="/autojobs/api")
|
||||
|
||||
# --- Plans & Pricing ---
|
||||
PLANS = {
|
||||
"free": {"name": "Free", "applications": 5, "ai_customize": 5, "price": 0, "type": "jobseeker"},
|
||||
"starter": {"name": "Starter", "applications": 50, "ai_customize": 50, "price": 9, "type": "jobseeker"},
|
||||
"pro": {"name": "Pro", "applications": 100, "ai_customize": 100, "price": 39, "type": "jobseeker"},
|
||||
"ultra": {"name": "Ultra", "applications": 200, "ai_customize": 200, "price": 69, "type": "jobseeker"},
|
||||
"business": {"name": "Business", "applications": 500, "ai_customize": 500, "price": 119, "type": "jobseeker"},
|
||||
"unlimited": {"name": "Unlimited", "applications": 99999, "ai_customize": 99999, "price": 319, "type": "jobseeker"},
|
||||
# Private User Plans (job seekers)
|
||||
PRIVATE_PLANS = {
|
||||
"free": {"name": "Free", "applications": 5, "ai_customize": 5, "price": 0},
|
||||
"starter": {"name": "Starter", "applications": 50, "ai_customize": 50, "price": 9},
|
||||
"pro": {"name": "Pro", "applications": 100, "ai_customize": 100, "price": 39},
|
||||
"ultra": {"name": "Ultra", "applications": 200, "ai_customize": 200, "price": 69},
|
||||
}
|
||||
|
||||
# Alias for backwards compatibility
|
||||
PLANS = PRIVATE_PLANS
|
||||
|
||||
# Agency Plans (recruiting agencies - manage multiple clients)
|
||||
AGENCY_PLANS = {
|
||||
"agency_starter": {"name": "Agency Starter", "submissions": 1000, "clients": 10, "price": 555, "type": "agency"},
|
||||
"agency_growth": {"name": "Agency Growth", "submissions": 3000, "clients": 50, "price": 999, "type": "agency"},
|
||||
"agency_scale": {"name": "Agency Scale", "submissions": 5000, "clients": 150, "price": 1499, "type": "agency"},
|
||||
"agency_enterprise": {"name": "Agency Enterprise", "submissions": 10000, "clients": 500, "price": 2222, "type": "agency"},
|
||||
"agency_starter": {"name": "Agency Starter", "submissions": 1000, "clients": 10, "price": 555},
|
||||
"agency_growth": {"name": "Agency Growth", "submissions": 3000, "clients": 50, "price": 999},
|
||||
"agency_scale": {"name": "Agency Scale", "submissions": 5000, "clients": 150, "price": 1499},
|
||||
"agency_enterprise": {"name": "Agency Enterprise", "submissions": 10000, "clients": 500, "price": 2222},
|
||||
}
|
||||
|
||||
# --- Database ---
|
||||
@@ -45,9 +47,13 @@ def init_db():
|
||||
id TEXT PRIMARY KEY,
|
||||
email TEXT UNIQUE NOT NULL,
|
||||
name TEXT,
|
||||
user_type TEXT DEFAULT 'private',
|
||||
plan TEXT DEFAULT 'free',
|
||||
api_keys TEXT DEFAULT '{}',
|
||||
profile TEXT DEFAULT '{}',
|
||||
linkedin_access_token TEXT DEFAULT '',
|
||||
linkedin_refresh_token TEXT DEFAULT '',
|
||||
linkedin_profile_id TEXT DEFAULT '',
|
||||
monthly_count INTEGER DEFAULT 0,
|
||||
ai_customize_count INTEGER DEFAULT 0,
|
||||
billing_cycle_start TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
@@ -131,10 +137,11 @@ def get_user_row(user_id: str):
|
||||
return dict(user) if user else None
|
||||
|
||||
def check_plan_limit(user: dict, action: str, count: int = 1) -> dict:
|
||||
"""Check if user has reached their plan limit. Returns {"allowed": bool, "remaining": int}"""
|
||||
"""Check if user has reached their plan limit"""
|
||||
plan_name = user.get("plan", "free")
|
||||
user_type = user.get("user_type", "private")
|
||||
|
||||
# Check if agency plan
|
||||
# Agency plans
|
||||
if plan_name in AGENCY_PLANS:
|
||||
plan = AGENCY_PLANS[plan_name]
|
||||
if action == "submission":
|
||||
@@ -143,10 +150,10 @@ def check_plan_limit(user: dict, action: str, count: int = 1) -> dict:
|
||||
else:
|
||||
return {"allowed": True, "remaining": 99999}
|
||||
remaining = max(0, limit - used)
|
||||
return {"allowed": remaining >= count, "remaining": remaining, "limit": limit, "used": used, "type": "agency"}
|
||||
return {"allowed": remaining >= count, "remaining": remaining, "limit": limit, "used": used, "type": "agency", "user_type": "agency"}
|
||||
|
||||
# Jobseeker plan
|
||||
plan = PLANS.get(plan_name, PLANS["free"])
|
||||
# Private user plans
|
||||
plan = PRIVATE_PLANS.get(plan_name, PRIVATE_PLANS["free"])
|
||||
if action == "application":
|
||||
used = user.get("monthly_count", 0)
|
||||
limit = plan["applications"]
|
||||
@@ -157,7 +164,7 @@ def check_plan_limit(user: dict, action: str, count: int = 1) -> dict:
|
||||
return {"allowed": True, "remaining": 99999}
|
||||
|
||||
remaining = max(0, limit - used)
|
||||
return {"allowed": remaining >= count, "remaining": remaining, "limit": limit, "used": used, "type": "jobseeker"}
|
||||
return {"allowed": remaining >= count, "remaining": remaining, "limit": limit, "used": used, "type": "jobseeker", "user_type": "private"}
|
||||
|
||||
def increment_usage(user_id: str, action: str):
|
||||
conn = sqlite3.connect(DB_PATH)
|
||||
@@ -231,17 +238,133 @@ def health():
|
||||
|
||||
# --- User Routes ---
|
||||
|
||||
|
||||
# --- LinkedIn OAuth ---
|
||||
@router.get("/auth/linkedin")
|
||||
def linkedin_auth(redirect_uri: str = "https://hostpioneers.com/autojobs/dashboard"):
|
||||
"""Start LinkedIn OAuth flow"""
|
||||
client_id = os.getenv("LINKEDIN_CLIENT_ID", "")
|
||||
if not client_id:
|
||||
raise HTTPException(status_code=500, detail="LinkedIn OAuth not configured")
|
||||
|
||||
scope = "openid profile email w_member_social"
|
||||
state = str(uuid.uuid4())
|
||||
|
||||
auth_url = (
|
||||
f"https://www.linkedin.com/oauth/v2/authorization"
|
||||
f"?response_type=code"
|
||||
f"&client_id={client_id}"
|
||||
f"&redirect_uri={redirect_uri}"
|
||||
f"&scope={scope}"
|
||||
f"&state={state}"
|
||||
)
|
||||
return {"auth_url": auth_url}
|
||||
|
||||
@router.post("/auth/linkedin/callback")
|
||||
def linkedin_callback(code: str, user=Depends(get_current_user)):
|
||||
"""Exchange LinkedIn code for access token and store it"""
|
||||
client_id = os.getenv("LINKEDIN_CLIENT_ID", "")
|
||||
client_secret = os.getenv("LINKEDIN_CLIENT_SECRET", "")
|
||||
redirect_uri = "https://hostpioneers.com/autojobs/dashboard"
|
||||
|
||||
import requests
|
||||
token_url = "https://www.linkedin.com/oauth/v2/accessToken"
|
||||
data = {
|
||||
"grant_type": "authorization_code",
|
||||
"code": code,
|
||||
"redirect_uri": redirect_uri,
|
||||
"client_id": client_id,
|
||||
"client_secret": client_secret,
|
||||
}
|
||||
|
||||
try:
|
||||
resp = requests.post(token_url, data=data, timeout=10)
|
||||
if resp.status_code == 200:
|
||||
token_data = resp.json()
|
||||
access_token = token_data.get("access_token", "")
|
||||
|
||||
conn = sqlite3.connect(DB_PATH)
|
||||
c = conn.cursor()
|
||||
c.execute("""
|
||||
UPDATE users SET
|
||||
linkedin_access_token = ?,
|
||||
linkedin_refresh_token = ?
|
||||
WHERE id = ?
|
||||
""", (access_token, token_data.get("refresh_token", ""), user["id"]))
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
return {"success": True, "message": "LinkedIn connected"}
|
||||
else:
|
||||
raise HTTPException(status_code=400, detail="Failed to get LinkedIn token")
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@router.get("/linkedin/resumes")
|
||||
def get_linkedin_resumes(user=Depends(get_current_user)):
|
||||
"""Get user's LinkedIn saved resumes"""
|
||||
user_row = get_user_row(user["id"])
|
||||
if not user_row or not user_row.get("linkedin_access_token"):
|
||||
raise HTTPException(status_code=400, detail="LinkedIn not connected")
|
||||
|
||||
import requests
|
||||
headers = {"Authorization": f"Bearer {user_row['linkedin_access_token']}"}
|
||||
|
||||
# Get LinkedIn profile to get resume info
|
||||
try:
|
||||
# LinkedIn Lite Profile
|
||||
profile_resp = requests.get(
|
||||
"https://api.linkedin.com/v2/me",
|
||||
headers=headers,
|
||||
timeout=10
|
||||
)
|
||||
|
||||
# For MVP - return placeholder (LinkedIn API requires special approval for resume access)
|
||||
# In production you'd use LinkedIn's resume API
|
||||
return {
|
||||
"resumes": [
|
||||
{"id": "linkedin_default", "name": "LinkedIn Saved Resume", "source": "linkedin"}
|
||||
],
|
||||
"message": "Connect your LinkedIn to import your saved resumes"
|
||||
}
|
||||
except:
|
||||
return {
|
||||
"resumes": [],
|
||||
"message": "Could not access LinkedIn. Please re-connect."
|
||||
}
|
||||
|
||||
@router.post("/linkedin/disconnect")
|
||||
def linkedin_disconnect(user=Depends(get_current_user)):
|
||||
"""Disconnect LinkedIn account"""
|
||||
conn = sqlite3.connect(DB_PATH)
|
||||
c = conn.cursor()
|
||||
c.execute("""
|
||||
UPDATE users SET
|
||||
linkedin_access_token = '',
|
||||
linkedin_refresh_token = '',
|
||||
linkedin_profile_id = ''
|
||||
WHERE id = ?
|
||||
""", (user["id"],))
|
||||
conn.commit()
|
||||
conn.close()
|
||||
return {"success": True}
|
||||
|
||||
@router.post("/users")
|
||||
def create_user(email: str, name: str = ""):
|
||||
"""Create new user with free plan"""
|
||||
def create_user(data: dict):
|
||||
"""Create new user (private or agency)"""
|
||||
user_id = str(uuid.uuid4())
|
||||
email = data.get("email", "")
|
||||
name = data.get("name", "")
|
||||
user_type = data.get("user_type", "private")
|
||||
plan = data.get("plan", "free")
|
||||
|
||||
conn = sqlite3.connect(DB_PATH)
|
||||
c = conn.cursor()
|
||||
try:
|
||||
c.execute("""
|
||||
INSERT INTO users (id, email, name, plan, monthly_count, ai_customize_count)
|
||||
VALUES (?, ?, ?, 'free', 0, 0)
|
||||
""", (user_id, email, name))
|
||||
INSERT INTO users (id, email, name, user_type, plan, monthly_count, ai_customize_count)
|
||||
VALUES (?, ?, ?, ?, ?, 0, 0)
|
||||
""", (user_id, email, name, user_type, plan))
|
||||
conn.commit()
|
||||
except sqlite3.IntegrityError:
|
||||
conn.close()
|
||||
@@ -250,17 +373,24 @@ def create_user(email: str, name: str = ""):
|
||||
return {"user_id": user_id, "email": email, "plan": "free"}
|
||||
|
||||
@router.post("/users/login")
|
||||
def login(email: str):
|
||||
def login(data: dict):
|
||||
"""Login by email — returns user_id as token (MVP auth)"""
|
||||
email = data.get("email", "")
|
||||
conn = sqlite3.connect(DB_PATH)
|
||||
conn.row_factory = sqlite3.Row
|
||||
c = conn.cursor()
|
||||
c.execute("SELECT id, email, name, plan FROM users WHERE email = ?", (email,))
|
||||
c.execute("SELECT id, email, name, user_type, plan FROM users WHERE email = ?", (email,))
|
||||
row = c.fetchone()
|
||||
conn.close()
|
||||
if not row:
|
||||
raise HTTPException(status_code=404, detail="User not found")
|
||||
return {"user_id": row["id"], "email": row["email"], "name": row["name"], "plan": row["plan"]}
|
||||
return {
|
||||
"user_id": row["id"],
|
||||
"email": row["email"],
|
||||
"name": row["name"],
|
||||
"user_type": row["user_type"],
|
||||
"plan": row["plan"]
|
||||
}
|
||||
|
||||
@router.get("/users/me")
|
||||
def get_me(user=Depends(get_current_user)):
|
||||
@@ -268,23 +398,29 @@ def get_me(user=Depends(get_current_user)):
|
||||
user_row = get_user_row(user["id"])
|
||||
if not user_row:
|
||||
raise HTTPException(status_code=404, detail="User not found")
|
||||
plan = PLANS.get(user_row["plan"], PLANS["free"])
|
||||
|
||||
user_type = user_row.get("user_type", "private")
|
||||
if user_type == "agency":
|
||||
plan = AGENCY_PLANS.get(user_row["plan"], AGENCY_PLANS["agency_starter"])
|
||||
else:
|
||||
plan = PRIVATE_PLANS.get(user_row["plan"], PRIVATE_PLANS["free"])
|
||||
|
||||
return {
|
||||
"id": user_row["id"],
|
||||
"email": user_row["email"],
|
||||
"name": user_row["name"],
|
||||
"user_type": user_type,
|
||||
"plan": user_row["plan"],
|
||||
"plan_details": {
|
||||
"name": plan["name"],
|
||||
"applications_limit": plan["applications"],
|
||||
"ai_customize_limit": plan["ai_customize"],
|
||||
"price": plan["price"]
|
||||
},
|
||||
"linkedin_connected": bool(user_row.get("linkedin_access_token")),
|
||||
"usage": {
|
||||
"applications_this_month": user_row["monthly_count"],
|
||||
"ai_customizations_this_month": user_row["ai_customize_count"],
|
||||
"applications_remaining": max(0, plan["applications"] - user_row["monthly_count"]),
|
||||
"ai_customize_remaining": max(0, plan["ai_customize"] - user_row["ai_customize_count"])
|
||||
"applications_remaining": max(0, plan.get("applications", plan.get("submissions", 99999)) - user_row["monthly_count"]),
|
||||
"ai_customize_remaining": max(0, plan.get("ai_customize", 99999) - user_row["ai_customize_count"])
|
||||
},
|
||||
"billing_cycle_start": user_row["billing_cycle_start"],
|
||||
"created_at": user_row["created_at"]
|
||||
@@ -349,9 +485,9 @@ def get_profile(user=Depends(get_current_user)):
|
||||
|
||||
@router.get("/plans")
|
||||
def list_plans():
|
||||
"""List all available plans (jobseeker + agency)"""
|
||||
"""List all available plans separated by user type"""
|
||||
return {
|
||||
"jobseeker_plans": PLANS,
|
||||
"private_plans": PRIVATE_PLANS,
|
||||
"agency_plans": AGENCY_PLANS
|
||||
}
|
||||
|
||||
|
||||
+60
-72
@@ -1,14 +1,12 @@
|
||||
import Link from "next/link"
|
||||
|
||||
const jobseekerPlans = [
|
||||
const privatePlans = [
|
||||
{
|
||||
name: "Free",
|
||||
price: "$0",
|
||||
period: "forever",
|
||||
apps: "5",
|
||||
badge: "",
|
||||
highlight: false,
|
||||
features: ["5 AI job applications/month", "5 AI resume customizations/month", "Application tracker", "Add your own API keys"],
|
||||
features: ["5 job applications/month", "5 AI resume customizations", "Basic application tracker"],
|
||||
cta: "Get Started",
|
||||
ctaStyle: "bg-white/10 hover:bg-white/20"
|
||||
},
|
||||
@@ -17,10 +15,8 @@ const jobseekerPlans = [
|
||||
price: "$9",
|
||||
period: "/mo",
|
||||
apps: "50",
|
||||
badge: "",
|
||||
highlight: false,
|
||||
features: ["50 AI job applications/month", "50 AI resume customizations/month", "Application tracker", "Add your own API keys"],
|
||||
cta: "Start Applying",
|
||||
features: ["50 job applications/month", "50 AI resume customizations", "Add your own API keys", "Email support"],
|
||||
cta: "Start Now",
|
||||
ctaStyle: "bg-blue-500 hover:bg-blue-600"
|
||||
},
|
||||
{
|
||||
@@ -28,9 +24,9 @@ const jobseekerPlans = [
|
||||
price: "$39",
|
||||
period: "/mo",
|
||||
apps: "100",
|
||||
badge: "Popular",
|
||||
badge: "Most Popular",
|
||||
highlight: true,
|
||||
features: ["100 AI job applications/month", "100 AI resume customizations/month", "Application tracker", "Add your own API keys", "Email notifications"],
|
||||
features: ["100 job applications/month", "100 AI resume customizations", "Add your own API keys", "LinkedIn resume import", "Priority support"],
|
||||
cta: "Go Pro",
|
||||
ctaStyle: "bg-blue-500 hover:bg-blue-600"
|
||||
},
|
||||
@@ -39,9 +35,7 @@ const jobseekerPlans = [
|
||||
price: "$69",
|
||||
period: "/mo",
|
||||
apps: "200",
|
||||
badge: "",
|
||||
highlight: false,
|
||||
features: ["200 AI job applications/month", "200 AI resume customizations/month", "Priority processing", "Email + SMS notifications"],
|
||||
features: ["200 job applications/month", "200 AI resume customizations", "Add your own API keys", "LinkedIn resume import", "SMS notifications"],
|
||||
cta: "Go Ultra",
|
||||
ctaStyle: "bg-slate-600 hover:bg-slate-500"
|
||||
},
|
||||
@@ -56,7 +50,7 @@ const agencyPlans = [
|
||||
clients: "10",
|
||||
badge: "",
|
||||
highlight: false,
|
||||
features: ["1,000 job submissions/month", "Up to 10 client profiles", "AI resume tailoring", "White-label dashboard", "Priority support"],
|
||||
features: ["1,000 submissions/month", "10 client profiles", "White-label dashboard", "API access"],
|
||||
cta: "Start Agency",
|
||||
ctaStyle: "bg-purple-600 hover:bg-purple-500"
|
||||
},
|
||||
@@ -68,8 +62,8 @@ const agencyPlans = [
|
||||
clients: "50",
|
||||
badge: "",
|
||||
highlight: true,
|
||||
features: ["3,000 job submissions/month", "Up to 50 client profiles", "AI resume tailoring", "White-label dashboard", "Priority support"],
|
||||
cta: "Grow Agency",
|
||||
features: ["3,000 submissions/month", "50 client profiles", "White-label dashboard", "Priority support"],
|
||||
cta: "Grow",
|
||||
ctaStyle: "bg-purple-600 hover:bg-purple-500"
|
||||
},
|
||||
{
|
||||
@@ -80,8 +74,8 @@ const agencyPlans = [
|
||||
clients: "150",
|
||||
badge: "",
|
||||
highlight: false,
|
||||
features: ["5,000 job submissions/month", "Up to 150 client profiles", "AI resume tailoring", "White-label dashboard", "Dedicated account manager"],
|
||||
cta: "Scale Up",
|
||||
features: ["5,000 submissions/month", "150 client profiles", "White-label dashboard", "Dedicated account manager"],
|
||||
cta: "Scale",
|
||||
ctaStyle: "bg-slate-600 hover:bg-slate-500"
|
||||
},
|
||||
{
|
||||
@@ -92,8 +86,8 @@ const agencyPlans = [
|
||||
clients: "500",
|
||||
badge: "",
|
||||
highlight: false,
|
||||
features: ["10,000 job submissions/month", "Up to 500 client profiles", "AI resume tailoring", "White-label dashboard", "Dedicated account manager", "Custom integrations"],
|
||||
cta: "Get Enterprise",
|
||||
features: ["10,000 submissions/month", "500 client profiles", "White-label dashboard", "Dedicated account manager", "Custom integrations"],
|
||||
cta: "Enterprise",
|
||||
ctaStyle: "bg-slate-600 hover:bg-slate-500"
|
||||
},
|
||||
]
|
||||
@@ -106,8 +100,10 @@ export default function LandingPage() {
|
||||
<div className="text-2xl font-bold text-white">
|
||||
Auto<span className="text-blue-400">Jobs</span>
|
||||
</div>
|
||||
<div className="flex gap-6">
|
||||
<Link href="/autojobs/login" className="text-slate-300 hover:text-white transition">Login</Link>
|
||||
<div className="flex gap-4">
|
||||
<Link href="/autojobs/login" className="text-slate-300 hover:text-white transition flex items-center gap-2">
|
||||
Login
|
||||
</Link>
|
||||
<Link href="/autojobs/signup" className="px-5 py-2 bg-blue-500 hover:bg-blue-600 text-white rounded-lg font-medium transition">
|
||||
Get Started
|
||||
</Link>
|
||||
@@ -115,20 +111,18 @@ export default function LandingPage() {
|
||||
</nav>
|
||||
|
||||
{/* Hero */}
|
||||
<section className="py-24 px-6 text-center">
|
||||
<section className="py-20 px-6 text-center">
|
||||
<div className="max-w-4xl mx-auto">
|
||||
<div className="inline-block px-4 py-1.5 rounded-full bg-blue-500/20 border border-blue-500/30 text-blue-300 text-sm mb-6">
|
||||
Your Personal AI Job Agent
|
||||
AI-Powered Job Application Automation
|
||||
</div>
|
||||
<h1 className="text-5xl md:text-6xl font-bold text-white mb-6 leading-tight">
|
||||
Stop Applying to Jobs.
|
||||
Stop Manually Applying.
|
||||
<br />
|
||||
<span className="text-blue-400">Let AI Do It For You.</span>
|
||||
<span className="text-blue-400">Let AI Handle It.</span>
|
||||
</h1>
|
||||
<p className="text-xl text-slate-300 mb-10 max-w-2xl mx-auto leading-relaxed">
|
||||
Upload your resume once. Set your keywords. Our AI finds every matching job,
|
||||
rewrites your resume + cover letter for each one, and applies automatically —
|
||||
while you sleep.
|
||||
Upload your resume once. Set your preferences. AI finds matching jobs, rewrites your resume for each one, and applies — while you sleep.
|
||||
</p>
|
||||
<div className="flex flex-col sm:flex-row gap-4 justify-center">
|
||||
<Link
|
||||
@@ -137,50 +131,59 @@ export default function LandingPage() {
|
||||
>
|
||||
Start Free — 5 Applications
|
||||
</Link>
|
||||
<Link
|
||||
href="/autojobs/login"
|
||||
className="px-10 py-4 bg-white/10 hover:bg-white/20 text-white rounded-xl font-semibold text-lg border border-white/20 transition flex items-center justify-center gap-2"
|
||||
>
|
||||
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M19 3a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h14m-.5 15.5v-5.3a3.26 3.26 0 0 0-3.26-3.26c-.85 0-1.84.52-2.32 1.3v-1.11h-2.79v8.37h2.79v-4.93c0-.77.62-1.4 1.39-1.4a1.4 1.4 0 0 1 1.4 1.4v4.93h2.79M6.88 8.56a1.68 1.68 0 0 0 1.68-1.68c0-.93-.75-1.69-1.68-1.69a1.69 1.69 0 0 0-1.69 1.69c0 .93.76 1.68 1.69 1.68m1.39 9.94v-8.37H5.5v8.37h2.77z"/>
|
||||
</svg>
|
||||
Login with LinkedIn
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* How It Works */}
|
||||
<section className="py-20 px-6 bg-slate-800/40">
|
||||
<section className="py-16 px-6 bg-slate-800/40">
|
||||
<div className="max-w-5xl mx-auto">
|
||||
<h2 className="text-3xl font-bold text-white text-center mb-4">How AutoJobs Works</h2>
|
||||
<h2 className="text-3xl font-bold text-white text-center mb-12">How It Works</h2>
|
||||
<div className="grid md:grid-cols-3 gap-8">
|
||||
{[
|
||||
{
|
||||
step: "01",
|
||||
title: "Create Your Profile",
|
||||
desc: "Upload your resume. Tell us what jobs you want — keywords, location, salary range."
|
||||
desc: "Upload your resume. Connect your LinkedIn. Tell us what jobs you want — keywords, location, salary range."
|
||||
},
|
||||
{
|
||||
step: "02",
|
||||
title: "AI Finds & Customizes",
|
||||
desc: "We search Jooble, JSearch, and more. AI rewrites your resume and writes cover letters."
|
||||
desc: "We search across multiple job boards. For each match, AI rewrites your resume and writes a personalized cover letter."
|
||||
},
|
||||
{
|
||||
step: "03",
|
||||
title: "Apply & Track",
|
||||
desc: "Apply with one click. Track every application status — from applied to offer."
|
||||
desc: "Apply with one click or let AI apply automatically. Track every application status in your dashboard."
|
||||
}
|
||||
].map((item) => (
|
||||
<div key={item.step} className="relative bg-slate-700/50 rounded-2xl p-8 border border-slate-600 hover:border-blue-500/50 transition">
|
||||
<div key={item.step} className="bg-slate-700/50 rounded-2xl p-8 border border-slate-600">
|
||||
<div className="text-5xl font-bold text-blue-500/20 mb-4">{item.step}</div>
|
||||
<h3 className="text-xl font-semibold text-white mb-3">{item.title}</h3>
|
||||
<p className="text-slate-300 leading-relaxed">{item.desc}</p>
|
||||
<p className="text-slate-300">{item.desc}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Jobseeker Pricing */}
|
||||
{/* Private Plans */}
|
||||
<section className="py-20 px-6">
|
||||
<div className="max-w-6xl mx-auto text-center">
|
||||
<h2 className="text-3xl font-bold text-white mb-4">For Job Seekers</h2>
|
||||
<p className="text-slate-400 mb-12">Pick the plan that fits your job search.</p>
|
||||
<h2 className="text-3xl font-bold text-white mb-2">For Job Seekers</h2>
|
||||
<p className="text-slate-400 mb-12">Choose your monthly application limit</p>
|
||||
|
||||
<div className="grid md:grid-cols-2 lg:grid-cols-4 gap-6 max-w-5xl mx-auto">
|
||||
{jobseekerPlans.map((plan) => (
|
||||
{privatePlans.map((plan) => (
|
||||
<div
|
||||
key={plan.name}
|
||||
className={`rounded-2xl p-6 border text-left relative ${
|
||||
@@ -191,9 +194,7 @@ export default function LandingPage() {
|
||||
>
|
||||
{plan.badge && (
|
||||
<div className="absolute -top-3 left-1/2 -translate-x-1/2">
|
||||
<span className="px-3 py-1 bg-blue-500 text-white text-xs font-bold rounded-full">
|
||||
{plan.badge}
|
||||
</span>
|
||||
<span className="px-3 py-1 bg-blue-500 text-white text-xs font-bold rounded-full">{plan.badge}</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="text-sm text-slate-400 font-medium mb-1">{plan.name}</div>
|
||||
@@ -201,7 +202,7 @@ export default function LandingPage() {
|
||||
<span className="text-3xl font-bold text-white">{plan.price}</span>
|
||||
<span className="text-slate-400 text-sm">{plan.period}</span>
|
||||
</div>
|
||||
<div className="text-xs text-blue-400 mb-4">{plan.apps} apps + AI customizations/mo</div>
|
||||
<div className="text-xs text-blue-400 mb-4">{plan.apps} apps/month</div>
|
||||
<ul className="space-y-2 mb-6">
|
||||
{plan.features.map((f) => (
|
||||
<li key={f} className="text-slate-300 text-sm flex items-start gap-2">
|
||||
@@ -210,7 +211,7 @@ export default function LandingPage() {
|
||||
))}
|
||||
</ul>
|
||||
<Link
|
||||
href="/autojobs/signup"
|
||||
href="/autojobs/signup?type=private"
|
||||
className={`block text-center px-4 py-2.5 rounded-lg font-medium text-white transition ${plan.ctaStyle}`}
|
||||
>
|
||||
{plan.cta}
|
||||
@@ -218,17 +219,24 @@ export default function LandingPage() {
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="mt-8 text-slate-500 text-sm flex items-center justify-center gap-2">
|
||||
<svg className="w-4 h-4" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M19 3a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h14m-.5 15.5v-5.3a3.26 3.26 0 0 0-3.26-3.26c-.85 0-1.84.52-2.32 1.3v-1.11h-2.79v8.37h2.79v-4.93c0-.77.62-1.4 1.39-1.4a1.4 1.4 0 0 1 1.4 1.4v4.93h2.79M6.88 8.56a1.68 1.68 0 0 0 1.68-1.68c0-.93-.75-1.69-1.68-1.69a1.69 1.69 0 0 0-1.69 1.69c0 .93.76 1.68 1.69 1.68m1.39 9.94v-8.37H5.5v8.37h2.77z"/>
|
||||
</svg>
|
||||
Login with LinkedIn — import your saved resumes automatically
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Agency Pricing */}
|
||||
{/* Agency Plans */}
|
||||
<section className="py-20 px-6 bg-slate-800/40">
|
||||
<div className="max-w-6xl mx-auto text-center">
|
||||
<div className="inline-block px-4 py-1.5 rounded-full bg-purple-500/20 border border-purple-500/30 text-purple-300 text-sm mb-4">
|
||||
For Recruiting Agencies
|
||||
</div>
|
||||
<h2 className="text-3xl font-bold text-white mb-4">Agency Plans — Manage Multiple Clients</h2>
|
||||
<p className="text-slate-400 mb-12">Run job applications for your entire client roster. Each plan includes client profile management and submission limits.</p>
|
||||
<h2 className="text-3xl font-bold text-white mb-4">Agency Plans</h2>
|
||||
<p className="text-slate-400 mb-12">Manage multiple clients. Each plan has a hard submission cap — no unlimited.</p>
|
||||
|
||||
<div className="grid md:grid-cols-2 lg:grid-cols-4 gap-6 max-w-5xl mx-auto">
|
||||
{agencyPlans.map((plan) => (
|
||||
@@ -236,18 +244,16 @@ export default function LandingPage() {
|
||||
key={plan.name}
|
||||
className={`rounded-2xl p-6 border text-left relative ${
|
||||
plan.highlight
|
||||
? 'bg-gradient-to-br from-purple-600/30 to-pink-600/30 border-purple-500/50 shadow-lg shadow-purple-500/10'
|
||||
? 'bg-gradient-to-br from-purple-600/30 to-pink-600/30 border-purple-500/50'
|
||||
: 'bg-slate-700/50 border-slate-600'
|
||||
}`}
|
||||
>
|
||||
{plan.badge && (
|
||||
<div className="absolute -top-3 left-1/2 -translate-x-1/2">
|
||||
<span className="px-3 py-1 bg-purple-500 text-white text-xs font-bold rounded-full">
|
||||
{plan.badge}
|
||||
</span>
|
||||
<span className="px-3 py-1 bg-purple-500 text-white text-xs font-bold rounded-full">{plan.badge}</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="text-sm text-purple-400 font-medium mb-1">{plan.name}</div>
|
||||
<div className="text-sm text-purple-400 font-medium mb-1">Agency {plan.name}</div>
|
||||
<div className="flex items-baseline gap-1 mb-1">
|
||||
<span className="text-3xl font-bold text-white">{plan.price}</span>
|
||||
<span className="text-slate-400 text-sm">{plan.period}</span>
|
||||
@@ -271,25 +277,7 @@ export default function LandingPage() {
|
||||
</div>
|
||||
|
||||
<div className="mt-8 text-slate-500 text-sm">
|
||||
<p>No unlimited agency plan — all submissions are capped to prevent abuse.</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Stats */}
|
||||
<section className="py-16 px-6">
|
||||
<div className="max-w-4xl mx-auto grid grid-cols-3 gap-8 text-center">
|
||||
<div>
|
||||
<div className="text-3xl font-bold text-white mb-1"><$0.01</div>
|
||||
<div className="text-slate-400 text-sm">Cost per application</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-3xl font-bold text-white mb-1">15-25s</div>
|
||||
<div className="text-slate-400 text-sm">AI time per job customization</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-3xl font-bold text-white mb-1">500+</div>
|
||||
<div className="text-slate-400 text-sm">Jobs found per search</div>
|
||||
All plans have hard caps. No unlimited access for agencies.
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
Reference in New Issue
Block a user