feat: complete UI tasks, backend optimizations, coupon admin, user kundli
This commit is contained in:
155
src/app/admin/coupons/page.tsx
Normal file
155
src/app/admin/coupons/page.tsx
Normal file
@@ -0,0 +1,155 @@
|
||||
"use client"
|
||||
|
||||
import { useState, useEffect } from "react"
|
||||
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Label } from "@/components/ui/label"
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Loader2, Plus, Trash2, Edit } from "lucide-react"
|
||||
|
||||
export default function CouponsAdmin() {
|
||||
const [coupons, setCoupons] = useState<any[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [submitting, setSubmitting] = useState(false)
|
||||
const [newCoupon, setNewCoupon] = useState({
|
||||
code: "",
|
||||
discount_percent: 10,
|
||||
max_discount_amount: 100,
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
fetchCoupons()
|
||||
}, [])
|
||||
|
||||
const fetchCoupons = async () => {
|
||||
try {
|
||||
const res = await fetch('/api/admin/coupons')
|
||||
const data = await res.json()
|
||||
setCoupons(data)
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleCreate = async () => {
|
||||
if (!newCoupon.code) return
|
||||
setSubmitting(true)
|
||||
try {
|
||||
await fetch('/api/admin/coupons', {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(newCoupon)
|
||||
})
|
||||
setNewCoupon({ code: "", discount_percent: 10, max_discount_amount: 100 })
|
||||
await fetchCoupons()
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
} finally {
|
||||
setSubmitting(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleDelete = async (id: string) => {
|
||||
if (!confirm("Delete this coupon?")) return
|
||||
try {
|
||||
await fetch(`/api/admin/coupons/${id}`, { method: "DELETE" })
|
||||
await fetchCoupons()
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="p-8 max-w-6xl mx-auto space-y-8 animate-in fade-in duration-500">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold tracking-tight">Coupon Management</h1>
|
||||
<p className="text-muted-foreground mt-1 font-medium">Create and manage discount codes for subscriptions.</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
|
||||
<Card className="col-span-1 border-border shadow-sm">
|
||||
<CardHeader>
|
||||
<CardTitle>Create Coupon</CardTitle>
|
||||
<CardDescription>Add a new promo code</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label>Code</Label>
|
||||
<Input
|
||||
placeholder="e.g. DIWALI20"
|
||||
value={newCoupon.code}
|
||||
onChange={e => setNewCoupon({...newCoupon, code: e.target.value.toUpperCase()})}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>Discount %</Label>
|
||||
<Input
|
||||
type="number"
|
||||
value={newCoupon.discount_percent}
|
||||
onChange={e => setNewCoupon({...newCoupon, discount_percent: Number(e.target.value)})}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>Max Discount (₹)</Label>
|
||||
<Input
|
||||
type="number"
|
||||
value={newCoupon.max_discount_amount}
|
||||
onChange={e => setNewCoupon({...newCoupon, max_discount_amount: Number(e.target.value)})}
|
||||
/>
|
||||
</div>
|
||||
<Button className="w-full" onClick={handleCreate} disabled={submitting || !newCoupon.code}>
|
||||
{submitting ? <Loader2 className="animate-spin" /> : <><Plus size={16} className="mr-2"/> Create Coupon</>}
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="col-span-1 md:col-span-2 border-border shadow-sm">
|
||||
<CardHeader>
|
||||
<CardTitle>Active Coupons</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{loading ? (
|
||||
<div className="flex justify-center p-8"><Loader2 className="animate-spin text-muted-foreground" /></div>
|
||||
) : (
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Code</TableHead>
|
||||
<TableHead>Discount</TableHead>
|
||||
<TableHead>Max Off</TableHead>
|
||||
<TableHead>Status</TableHead>
|
||||
<TableHead className="text-right">Actions</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{coupons.length === 0 && (
|
||||
<TableRow><TableCell colSpan={5} className="text-center text-muted-foreground">No coupons found.</TableCell></TableRow>
|
||||
)}
|
||||
{coupons.map(c => (
|
||||
<TableRow key={c.id}>
|
||||
<TableCell className="font-bold">{c.code}</TableCell>
|
||||
<TableCell>{c.discount_percent}%</TableCell>
|
||||
<TableCell>₹{c.max_discount_amount}</TableCell>
|
||||
<TableCell>
|
||||
{c.is_active ? <Badge className="bg-green-500/20 text-green-500 border-green-500/30">Active</Badge> : <Badge variant="outline">Inactive</Badge>}
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
<Button variant="ghost" size="sm" onClick={() => handleDelete(c.id)}>
|
||||
<Trash2 size={16} className="text-red-500" />
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client"
|
||||
|
||||
import { ReactNode, useEffect } from "react"
|
||||
import { LayoutDashboard, Users, Database, Terminal, Settings, FileText } from "lucide-react"
|
||||
import { LayoutDashboard, Users, Database, Terminal, Settings, FileText, Tag } from "lucide-react"
|
||||
import Link from "next/link"
|
||||
import { usePathname } from "next/navigation"
|
||||
import { Logo } from "@/components/Logo"
|
||||
@@ -27,6 +27,7 @@ export default function AdminLayout({ children }: { children: ReactNode }) {
|
||||
<NavItem href="/admin/questions" icon={<Database size={18} />} label="Question Bank" active={pathname.startsWith("/admin/questions")} />
|
||||
<NavItem href="/admin/agents" icon={<Terminal size={18} />} label="AI Agents" active={pathname.startsWith("/admin/agents")} />
|
||||
<NavItem href="/admin/students" icon={<Users size={18} />} label="Students" active={pathname.startsWith("/admin/students")} />
|
||||
<NavItem href="/admin/coupons" icon={<Tag size={18} />} label="Coupons" active={pathname.startsWith("/admin/coupons")} />
|
||||
</nav>
|
||||
<div className="p-4 border-t border-border">
|
||||
<NavItem href="/admin/settings" icon={<Settings size={18} />} label="System Settings" active={pathname.startsWith("/admin/settings")} />
|
||||
|
||||
202
src/app/admin/students/[studentId]/page.tsx
Normal file
202
src/app/admin/students/[studentId]/page.tsx
Normal file
@@ -0,0 +1,202 @@
|
||||
"use client"
|
||||
|
||||
import { useEffect, useState } from "react"
|
||||
import { useParams, useRouter } from "next/navigation"
|
||||
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { ArrowLeft, Loader2, User, Mail, Target, Award, Calendar, Activity, BookOpen } from "lucide-react"
|
||||
|
||||
export default function StudentKundliPage() {
|
||||
const params = useParams()
|
||||
const router = useRouter()
|
||||
const studentId = params.studentId as string
|
||||
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [userData, setUserData] = useState<any>(null)
|
||||
|
||||
useEffect(() => {
|
||||
// Simulated fetch of student's complete "Kundli"
|
||||
setTimeout(() => {
|
||||
setUserData({
|
||||
id: studentId,
|
||||
name: "Rahul Sharma",
|
||||
email: "rahul@example.com",
|
||||
target: "NEET",
|
||||
tier: "AI Pro",
|
||||
joined: "Oct 12, 2025",
|
||||
stats: {
|
||||
total_tests: 42,
|
||||
average_score: "78%",
|
||||
streak: 45,
|
||||
total_questions_solved: 3450,
|
||||
ai_coach_interactions: 124
|
||||
},
|
||||
recent_tests: [
|
||||
{ id: 1, name: "Full Syllabus NEET Mock #4", score: "620/720", date: "2 days ago", percentile: "98.5%" },
|
||||
{ id: 2, name: "Physics: Kinematics DPP", score: "45/50", date: "4 days ago", percentile: "95.2%" },
|
||||
{ id: 3, name: "Biology: Human Reproduction", score: "88/100", date: "1 week ago", percentile: "99.1%" }
|
||||
],
|
||||
payments: [
|
||||
{ id: "PAY_1", amount: "₹1599", date: "Oct 12, 2025", plan: "AI Pro (Yearly)", status: "Success" }
|
||||
],
|
||||
weak_topics: ["Rotational Dynamics", "Organic Chemistry", "Genetics"]
|
||||
})
|
||||
setLoading(false)
|
||||
}, 800)
|
||||
}, [studentId])
|
||||
|
||||
if (loading) {
|
||||
return <div className="h-full flex items-center justify-center"><Loader2 className="animate-spin text-muted-foreground w-8 h-8" /></div>
|
||||
}
|
||||
|
||||
if (!userData) return <div>User not found</div>
|
||||
|
||||
return (
|
||||
<div className="p-8 space-y-6 max-w-6xl mx-auto animate-in fade-in duration-500">
|
||||
<Button variant="ghost" onClick={() => router.back()} className="text-muted-foreground hover:text-foreground mb-4">
|
||||
<ArrowLeft size={16} className="mr-2" /> Back to Students
|
||||
</Button>
|
||||
|
||||
<div className="flex flex-col md:flex-row gap-6">
|
||||
{/* User Identity Card */}
|
||||
<Card className="flex-1 border-border shadow-sm">
|
||||
<CardHeader className="pb-4 border-b border-border">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="w-16 h-16 rounded-full bg-secondary flex items-center justify-center text-2xl font-bold">
|
||||
{userData.name.charAt(0)}
|
||||
</div>
|
||||
<div>
|
||||
<CardTitle className="text-2xl">{userData.name}</CardTitle>
|
||||
<CardDescription className="flex items-center gap-2 mt-1">
|
||||
<Mail size={14} /> {userData.email}
|
||||
</CardDescription>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="pt-6 grid grid-cols-2 gap-y-4 gap-x-8">
|
||||
<div>
|
||||
<div className="text-sm text-muted-foreground flex items-center gap-2 mb-1"><Target size={14}/> Target Exam</div>
|
||||
<div className="font-medium">{userData.target}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-sm text-muted-foreground flex items-center gap-2 mb-1"><Award size={14}/> Plan Tier</div>
|
||||
<Badge className="bg-[#00F0FF]/10 text-[#00F0FF] border-[#00F0FF]/20">{userData.tier}</Badge>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-sm text-muted-foreground flex items-center gap-2 mb-1"><Calendar size={14}/> Joined</div>
|
||||
<div className="font-medium">{userData.joined}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-sm text-muted-foreground flex items-center gap-2 mb-1"><User size={14}/> User ID</div>
|
||||
<div className="font-mono text-xs">{userData.id}</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Stats Summary */}
|
||||
<div className="grid grid-cols-2 gap-4 flex-1">
|
||||
<Card className="bg-secondary/20 border-border">
|
||||
<CardContent className="p-4 flex flex-col items-center justify-center h-full text-center">
|
||||
<div className="text-3xl font-black text-[#00F0FF]">{userData.stats.total_tests}</div>
|
||||
<div className="text-sm text-muted-foreground mt-1">Tests Attempted</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="bg-secondary/20 border-border">
|
||||
<CardContent className="p-4 flex flex-col items-center justify-center h-full text-center">
|
||||
<div className="text-3xl font-black text-orange-500">{userData.stats.streak}</div>
|
||||
<div className="text-sm text-muted-foreground mt-1">Day Streak</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="bg-secondary/20 border-border">
|
||||
<CardContent className="p-4 flex flex-col items-center justify-center h-full text-center">
|
||||
<div className="text-3xl font-black">{userData.stats.average_score}</div>
|
||||
<div className="text-sm text-muted-foreground mt-1">Avg. Accuracy</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="bg-secondary/20 border-border">
|
||||
<CardContent className="p-4 flex flex-col items-center justify-center h-full text-center">
|
||||
<div className="text-3xl font-black">{userData.stats.total_questions_solved}</div>
|
||||
<div className="text-sm text-muted-foreground mt-1">Questions Solved</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Recent Activity & Weak Topics */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
<Card className="lg:col-span-2 border-border shadow-sm">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2"><Activity size={18} className="text-[#00F0FF]"/> Recent Tests</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
{userData.recent_tests.map((test: any) => (
|
||||
<div key={test.id} className="flex items-center justify-between p-3 rounded-lg border border-border bg-secondary/10">
|
||||
<div>
|
||||
<div className="font-bold">{test.name}</div>
|
||||
<div className="text-xs text-muted-foreground mt-1">{test.date}</div>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<div className="font-mono font-bold text-[#00F0FF]">{test.score}</div>
|
||||
<div className="text-xs text-muted-foreground mt-1">{test.percentile} %ile</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="border-border shadow-sm">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2"><BookOpen size={18} className="text-red-500"/> AI Identified Weaknesses</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ul className="space-y-3">
|
||||
{userData.weak_topics.map((topic: string, i: number) => (
|
||||
<li key={i} className="flex items-center gap-3">
|
||||
<div className="w-2 h-2 rounded-full bg-red-500" />
|
||||
<span className="font-medium text-sm">{topic}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Payment History */}
|
||||
<Card className="border-border shadow-sm">
|
||||
<CardHeader>
|
||||
<CardTitle>Payment History</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="border border-border rounded-lg overflow-hidden">
|
||||
<table className="w-full text-left text-sm">
|
||||
<thead className="bg-secondary/30">
|
||||
<tr>
|
||||
<th className="px-4 py-3 font-semibold">Transaction ID</th>
|
||||
<th className="px-4 py-3 font-semibold">Plan</th>
|
||||
<th className="px-4 py-3 font-semibold">Date</th>
|
||||
<th className="px-4 py-3 font-semibold">Amount</th>
|
||||
<th className="px-4 py-3 font-semibold">Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-border">
|
||||
{userData.payments.map((p: any) => (
|
||||
<tr key={p.id}>
|
||||
<td className="px-4 py-3 font-mono text-xs">{p.id}</td>
|
||||
<td className="px-4 py-3 font-medium">{p.plan}</td>
|
||||
<td className="px-4 py-3 text-muted-foreground">{p.date}</td>
|
||||
<td className="px-4 py-3 font-bold">{p.amount}</td>
|
||||
<td className="px-4 py-3"><Badge className="bg-green-500/20 text-green-500 border-green-500/30">{p.status}</Badge></td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -3,8 +3,10 @@
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Search, Download, MoreVertical } from "lucide-react"
|
||||
import { useRouter } from "next/navigation"
|
||||
|
||||
export default function StudentsPage() {
|
||||
const router = useRouter()
|
||||
const students = [
|
||||
{ id: "STU-001", name: "Rahul Sharma", email: "rahul@example.com", tier: "AI Pro", target: "NEET", joined: "Oct 12, 2025", tests: 42, streak: 45, lastLogin: "Today, 08:30 AM" },
|
||||
{ id: "STU-002", name: "Priya Patel", email: "priya@example.com", tier: "Basic", target: "JEE Main", joined: "Nov 05, 2025", tests: 14, streak: 2, lastLogin: "Yesterday, 14:15 PM" },
|
||||
@@ -50,7 +52,11 @@ export default function StudentsPage() {
|
||||
</thead>
|
||||
<tbody className="divide-y divide-border">
|
||||
{students.map((s) => (
|
||||
<tr key={s.id} className="hover:bg-secondary/20 transition-colors">
|
||||
<tr
|
||||
key={s.id}
|
||||
className="hover:bg-secondary/20 transition-colors cursor-pointer"
|
||||
onClick={() => router.push(`/admin/students/${s.id}`)}
|
||||
>
|
||||
<td className="px-6 py-4">
|
||||
<div className="font-bold">{s.name}</div>
|
||||
<div className="text-muted-foreground text-xs">{s.email}</div>
|
||||
|
||||
Reference in New Issue
Block a user