Initial commit: AutoJobs MVP - AI job application platform

This commit is contained in:
2026-04-13 18:39:26 +02:00
commit 8d1845c874
16 changed files with 3483 additions and 0 deletions
+276
View File
@@ -0,0 +1,276 @@
"use client"
import { useState, useEffect } from "react"
import Link from "next/link"
interface Stats {
total_users: number
total_applications: number
applied: number
interviews: number
conversion_rate: number
api_usage: { source: string; searches: number; total_jobs: number }[]
daily_applications: { date: string; count: number }[]
top_companies: { company: string; count: number }[]
}
interface User {
id: string
email: string
name: string
created_at: string
applications: number
interviews: number
}
interface Application {
id: string
email: string
name: string
job_title: string
company: string
location: string
status: string
applied_at: string
}
export default function AdminPage() {
const [stats, setStats] = useState<Stats | null>(null)
const [users, setUsers] = useState<User[]>([])
const [applications, setApplications] = useState<Application[]>([])
const [tab, setTab] = useState<"overview" | "users" | "applications">("overview")
const [loading, setLoading] = useState(true)
useEffect(() => {
loadData()
}, [])
const loadData = async () => {
setLoading(true)
try {
const [statsRes, usersRes, appsRes] = await Promise.all([
fetch("/api/admin/stats"),
fetch("/api/admin/users"),
fetch("/api/admin/applications")
])
const statsData = await statsRes.json()
const usersData = await usersRes.json()
const appsData = await appsRes.json()
setStats(statsData)
setUsers(usersData)
setApplications(appsData)
} catch (err) {
console.error("Failed to load admin data:", err)
}
setLoading(false)
}
const statusColor: Record<string, string> = {
pending: "bg-yellow-500/20 text-yellow-400",
applied: "bg-blue-500/20 text-blue-400",
interview: "bg-green-500/20 text-green-400",
rejected: "bg-red-500/20 text-red-400",
offer: "bg-purple-500/20 text-purple-400"
}
if (loading) {
return (
<div className="min-h-screen bg-slate-900 text-white flex items-center justify-center">
<div className="text-slate-400">Loading admin data...</div>
</div>
)
}
return (
<div className="min-h-screen bg-slate-900 text-white">
{/* Header */}
<header className="bg-slate-800 border-b border-slate-700 px-6 py-4">
<div className="max-w-7xl mx-auto flex justify-between items-center">
<div>
<Link href="/autojobs" className="text-xl font-bold text-blue-400">AutoJobs</Link>
<span className="ml-3 text-slate-400 text-sm">/ Admin</span>
</div>
<Link href="/autojobs/dashboard" className="text-slate-400 hover:text-white text-sm">
Back to Dashboard
</Link>
</div>
</header>
<div className="max-w-7xl mx-auto px-6 py-8">
<h1 className="text-3xl font-bold mb-8">Admin Dashboard</h1>
{/* Tabs */}
<div className="flex gap-2 mb-8">
{["overview", "users", "applications"].map(t => (
<button
key={t}
onClick={() => setTab(t as any)}
className={`px-5 py-2 rounded-lg font-medium capitalize transition ${
tab === t ? "bg-blue-500 text-white" : "bg-slate-800 text-slate-400 hover:bg-slate-700"
}`}
>
{t}
</button>
))}
</div>
{/* Overview Tab */}
{tab === "overview" && stats && (
<div>
{/* KPI Cards */}
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-8">
{[
{ label: "Total Users", value: stats.total_users, icon: "👥" },
{ label: "Applications", value: stats.total_applications, icon: "📋" },
{ label: "Interviews", value: stats.interviews, icon: "🎯" },
{ label: "Conversion Rate", value: `${stats.conversion_rate}%`, icon: "📈" },
].map(kpi => (
<div key={kpi.label} className="bg-slate-800 rounded-xl p-5 border border-slate-700">
<div className="text-2xl mb-2">{kpi.icon}</div>
<div className="text-3xl font-bold">{kpi.value}</div>
<div className="text-slate-400 text-sm">{kpi.label}</div>
</div>
))}
</div>
{/* Charts placeholder + tables */}
<div className="grid md:grid-cols-2 gap-6">
{/* Top Companies */}
<div className="bg-slate-800 rounded-xl p-6 border border-slate-700">
<h3 className="font-semibold mb-4">Top Companies Applied To</h3>
{stats.top_companies.length > 0 ? (
<div className="space-y-2">
{stats.top_companies.map((c, i) => (
<div key={i} className="flex justify-between items-center p-3 bg-slate-700/50 rounded-lg">
<span className="text-white">{c.company}</span>
<span className="text-blue-400 font-semibold">{c.count}</span>
</div>
))}
</div>
) : (
<p className="text-slate-400 text-sm">No data yet</p>
)}
</div>
{/* API Usage */}
<div className="bg-slate-800 rounded-xl p-6 border border-slate-700">
<h3 className="font-semibold mb-4">API Usage</h3>
{stats.api_usage.length > 0 ? (
<div className="space-y-2">
{stats.api_usage.map((u, i) => (
<div key={i} className="flex justify-between items-center p-3 bg-slate-700/50 rounded-lg">
<span className="text-white">{u.source}</span>
<div className="text-right">
<span className="text-blue-400 font-semibold">{u.searches}</span>
<span className="text-slate-400 text-xs block">{u.total_jobs} jobs found</span>
</div>
</div>
))}
</div>
) : (
<p className="text-slate-400 text-sm">No searches yet</p>
)}
</div>
{/* Daily Applications */}
<div className="bg-slate-800 rounded-xl p-6 border border-slate-700 md:col-span-2">
<h3 className="font-semibold mb-4">Daily Applications (Last 30 Days)</h3>
{stats.daily_applications.length > 0 ? (
<div className="flex items-end gap-1 h-32">
{stats.daily_applications.slice(-14).map((d, i) => {
const max = Math.max(...stats.daily_applications.map(x => x.count))
const height = max > 0 ? (d.count / max) * 100 : 0
return (
<div key={i} className="flex-1 flex flex-col items-center gap-1">
<div className="w-full bg-blue-500 rounded-t" style={{ height: `${Math.max(height, 4)}%` }} />
<span className="text-xs text-slate-500">{d.date?.slice(5)}</span>
</div>
)
})}
</div>
) : (
<p className="text-slate-400 text-sm">No data yet</p>
)}
</div>
</div>
</div>
)}
{/* Users Tab */}
{tab === "users" && (
<div>
<div className="bg-slate-800 rounded-xl border border-slate-700 overflow-hidden">
<table className="w-full">
<thead className="bg-slate-700/50">
<tr>
<th className="text-left px-4 py-3 text-slate-400 text-sm font-medium">User</th>
<th className="text-left px-4 py-3 text-slate-400 text-sm font-medium">Email</th>
<th className="text-left px-4 py-3 text-slate-400 text-sm font-medium">Joined</th>
<th className="text-center px-4 py-3 text-slate-400 text-sm font-medium">Applications</th>
<th className="text-center px-4 py-3 text-slate-400 text-sm font-medium">Interviews</th>
</tr>
</thead>
<tbody>
{users.map((u) => (
<tr key={u.id} className="border-t border-slate-700 hover:bg-slate-700/30">
<td className="px-4 py-3 text-white font-medium">{u.name || "—"}</td>
<td className="px-4 py-3 text-slate-400">{u.email}</td>
<td className="px-4 py-3 text-slate-400 text-sm">{new Date(u.created_at).toLocaleDateString()}</td>
<td className="px-4 py-3 text-center text-blue-400 font-semibold">{u.applications}</td>
<td className="px-4 py-3 text-center text-green-400 font-semibold">{u.interviews}</td>
</tr>
))}
{users.length === 0 && (
<tr>
<td colSpan={5} className="px-4 py-8 text-center text-slate-400">No users yet</td>
</tr>
)}
</tbody>
</table>
</div>
</div>
)}
{/* Applications Tab */}
{tab === "applications" && (
<div>
<div className="bg-slate-800 rounded-xl border border-slate-700 overflow-hidden">
<table className="w-full">
<thead className="bg-slate-700/50">
<tr>
<th className="text-left px-4 py-3 text-slate-400 text-sm font-medium">User</th>
<th className="text-left px-4 py-3 text-slate-400 text-sm font-medium">Job</th>
<th className="text-left px-4 py-3 text-slate-400 text-sm font-medium">Company</th>
<th className="text-left px-4 py-3 text-slate-400 text-sm font-medium">Location</th>
<th className="text-left px-4 py-3 text-slate-400 text-sm font-medium">Applied</th>
<th className="text-center px-4 py-3 text-slate-400 text-sm font-medium">Status</th>
</tr>
</thead>
<tbody>
{applications.map((a) => (
<tr key={a.id} className="border-t border-slate-700 hover:bg-slate-700/30">
<td className="px-4 py-3 text-white text-sm">{a.name || a.email}</td>
<td className="px-4 py-3 text-white text-sm">{a.job_title}</td>
<td className="px-4 py-3 text-blue-400 text-sm">{a.company}</td>
<td className="px-4 py-3 text-slate-400 text-sm">{a.location}</td>
<td className="px-4 py-3 text-slate-400 text-sm">{new Date(a.applied_at).toLocaleDateString()}</td>
<td className="px-4 py-3 text-center">
<span className={`px-2 py-1 rounded-full text-xs font-medium ${statusColor[a.status] || "bg-slate-700"}`}>
{a.status}
</span>
</td>
</tr>
))}
{applications.length === 0 && (
<tr>
<td colSpan={6} className="px-4 py-8 text-center text-slate-400">No applications yet</td>
</tr>
)}
</tbody>
</table>
</div>
</div>
)}
</div>
</div>
)
}