Initial commit: AutoJobs MVP - AI job application platform
This commit is contained in:
@@ -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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user