feat: Add Bulk Registration, Change Password, and update Auth UI
This commit is contained in:
@@ -12,7 +12,7 @@ export default function AboutPage() {
|
||||
<p className="text-xl text-muted-foreground max-w-4xl leading-relaxed mb-8 mx-auto text-left md:text-center">
|
||||
PragatiVeer is India's first AI-powered daily practice platform for JEE and NEET aspirants — a product of Gyannetra Pvt Ltd, the first company to bring the power of artificial intelligence into Indian exam preparation. We combine daily practice sheets, exam-realistic test series, and an AI coach that personalises every student's learning journey, making high-quality preparation accessible and affordable for every Indian student.
|
||||
</p>
|
||||
<Link href="/signup">
|
||||
<Link href="/login">
|
||||
<Button size="lg" className="rounded-full font-bold px-8 shadow-xl">Join the Revolution</Button>
|
||||
</Link>
|
||||
</main>
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
"use client"
|
||||
"use client"
|
||||
|
||||
import { fetchWithAuth } from "@/lib/apiClient";
|
||||
import { useEffect, useState } from "react"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Search, Download, Flame, RefreshCw } from "lucide-react"
|
||||
import { Search, Download, Flame, RefreshCw, Upload } from "lucide-react"
|
||||
import { useRouter } from "next/navigation"
|
||||
import { useRef } from "react"
|
||||
|
||||
const TIER_STYLE: Record<string, string> = {
|
||||
"Al_Coach": "bg-[var(--cyan-ai-10)] text-[var(--cyan-ai)] border-[var(--cyan-ai-20)] font-bold",
|
||||
@@ -25,6 +26,61 @@ export default function StudentsPage() {
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [search, setSearch] = useState("")
|
||||
|
||||
const fileInputRef = useRef<HTMLInputElement>(null)
|
||||
|
||||
const handleBulkUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0]
|
||||
if (!file) return
|
||||
setLoading(true)
|
||||
try {
|
||||
const formData = new FormData()
|
||||
formData.append("file", file)
|
||||
const rawRes = await fetch(process.env.NEXT_PUBLIC_API_URL ? `${process.env.NEXT_PUBLIC_API_URL}/api/admin/students/bulk` : "http://localhost:8000/api/admin/students/bulk", {
|
||||
method: "POST",
|
||||
body: formData,
|
||||
headers: {
|
||||
"Authorization": `Bearer ${localStorage.getItem("active_session_token")}`
|
||||
}
|
||||
})
|
||||
|
||||
if (!rawRes.ok) throw new Error("Upload failed")
|
||||
|
||||
const results = await rawRes.json()
|
||||
alert(`Successfully registered ${results.length} students!`)
|
||||
|
||||
// Download CSV with passwords
|
||||
let csvContent = "data:text/csv;charset=utf-8,name,email,password\\n"
|
||||
results.forEach((row: any) => {
|
||||
csvContent += `${row.name},${row.email},${row.password}\\n`
|
||||
})
|
||||
const encodedUri = encodeURI(csvContent)
|
||||
const link = document.createElement("a")
|
||||
link.setAttribute("href", encodedUri)
|
||||
link.setAttribute("download", "bulk_registered_passwords.csv")
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
|
||||
// Refresh students
|
||||
const fresh = await fetchWithAuth("/api/admin/students").then(r => r.json())
|
||||
setStudents(fresh)
|
||||
} catch (err: any) {
|
||||
alert("Error: " + err.message)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
if (fileInputRef.current) fileInputRef.current.value = ""
|
||||
}
|
||||
}
|
||||
|
||||
const downloadTemplate = () => {
|
||||
const template = "data:text/csv;charset=utf-8,name,email,phone,target_exam,tier\\nJohn Doe,john@example.com,9876543210,JEE_Main,Free\\nJane Smith,jane@example.com,9876543211,NEET,Pro"
|
||||
const encodedUri = encodeURI(template)
|
||||
const link = document.createElement("a")
|
||||
link.setAttribute("href", encodedUri)
|
||||
link.setAttribute("download", "bulk_register_template.csv")
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
fetchWithAuth("/api/admin/students")
|
||||
.then(r => r.json())
|
||||
@@ -53,9 +109,18 @@ export default function StudentsPage() {
|
||||
{loading ? "Loading..." : `${students.length} students enrolled`}
|
||||
</p>
|
||||
</div>
|
||||
<Button variant="outline" className="border-border hover:bg-secondary">
|
||||
<Download size={16} className="mr-2" /> Export CSV
|
||||
</Button>
|
||||
<div className="flex gap-2">
|
||||
<input type="file" accept=".csv" className="hidden" ref={fileInputRef} onChange={handleBulkUpload} />
|
||||
<Button variant="outline" className="border-border hover:bg-secondary text-[var(--cyan-ai)]" onClick={() => fileInputRef.current?.click()}>
|
||||
<Upload size={16} className="mr-2" /> Bulk Register CSV
|
||||
</Button>
|
||||
<Button variant="outline" className="border-border hover:bg-secondary" onClick={downloadTemplate}>
|
||||
<Download size={16} className="mr-2" /> Download Template
|
||||
</Button>
|
||||
<Button variant="outline" className="border-border hover:bg-secondary">
|
||||
<Download size={16} className="mr-2" /> Export CSV
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-secondary/10 border border-border rounded-xl overflow-hidden backdrop-blur-sm">
|
||||
|
||||
@@ -69,6 +69,44 @@ export default function SettingsPage() {
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const [currentPassword, setCurrentPassword] = useState("");
|
||||
const [newPassword, setNewPassword] = useState("");
|
||||
const [confirmPassword, setConfirmPassword] = useState("");
|
||||
const [passwordSaving, setPasswordSaving] = useState(false);
|
||||
|
||||
const handleChangePassword = async () => {
|
||||
if (newPassword !== confirmPassword) {
|
||||
alert("New passwords do not match.");
|
||||
return;
|
||||
}
|
||||
setPasswordSaving(true);
|
||||
try {
|
||||
const res = await fetchWithAuth("/api/auth/change-password", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
current_password: currentPassword,
|
||||
new_password: newPassword,
|
||||
}),
|
||||
});
|
||||
const data = await res.json();
|
||||
if (res.ok) {
|
||||
alert(data.message || "Password updated successfully!");
|
||||
setCurrentPassword("");
|
||||
setNewPassword("");
|
||||
setConfirmPassword("");
|
||||
} else {
|
||||
alert("Failed to change password: " + (data.detail || "Unknown error"));
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
alert("Network error. Could not change password.");
|
||||
} finally {
|
||||
setPasswordSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!userId) return;
|
||||
setSaving(true);
|
||||
|
||||
@@ -138,7 +138,7 @@ export default function LoginPage() {
|
||||
</Card>
|
||||
|
||||
<p className="text-center text-sm text-muted-foreground font-medium">
|
||||
Don't have an account? <Link href="/signup" className="font-bold text-foreground hover:text-primary transition-colors">Sign up</Link>
|
||||
For registration contact us: support@pragativeer.com
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -83,7 +83,7 @@ export default function MarketingPage() {
|
||||
<div className="flex items-center gap-4">
|
||||
<ThemeToggle />
|
||||
<Link href="/login" className="text-sm font-bold hover:text-primary transition-colors hidden sm:block">Log in</Link>
|
||||
<Link href="/signup">
|
||||
<Link href="/login">
|
||||
<Button className="rounded-full font-bold px-6 shadow-[0_0_15px_-5px_var(--color-primary)] hover:shadow-[0_0_25px_-5px_var(--color-primary)] transition-all">Get Started</Button>
|
||||
</Link>
|
||||
</div>
|
||||
@@ -128,7 +128,7 @@ export default function MarketingPage() {
|
||||
transition={{ duration: 0.8, delay: 0.4 }}
|
||||
className="flex flex-col sm:flex-row items-center justify-center gap-4 pt-8"
|
||||
>
|
||||
<Link href="/signup">
|
||||
<Link href="/login">
|
||||
<Button size="lg" className="group rounded-full h-14 px-8 text-base font-bold shadow-[0_0_40px_-10px_var(--color-primary)] hover:shadow-[0_0_60px_-10px_var(--color-primary)] transition-all w-full sm:w-auto relative overflow-hidden">
|
||||
<span className="relative z-10 flex items-center">Start Free Trial <ArrowRight className="ml-2 group-hover:translate-x-1 transition-transform" size={18} /></span>
|
||||
<div className="absolute inset-0 -translate-x-full animate-shine bg-gradient-to-r from-transparent via-white/20 to-transparent z-0"></div>
|
||||
@@ -437,8 +437,8 @@ export default function MarketingPage() {
|
||||
<li className="flex items-center gap-4"><CheckCircle2 size={24} className="text-white" /> Basic Performance Analytics</li>
|
||||
<li className="flex items-center gap-4"><CheckCircle2 size={24} className="text-white" /> Community Access</li>
|
||||
</ul>
|
||||
<Link href="/signup">
|
||||
<Button variant="outline" size="lg" className="w-full rounded-2xl h-12 border-white text-white font-bold text-lg hover:bg-secondary hover:text-white">Get Started Free</Button>
|
||||
<Link href="/login">
|
||||
<Button variant="outline" size="lg" className="w-full rounded-2xl h-12 border-white text-white font-bold text-lg hover:bg-secondary hover:text-white">Login</Button>
|
||||
</Link>
|
||||
</Card>
|
||||
</motion.div>
|
||||
@@ -461,7 +461,7 @@ export default function MarketingPage() {
|
||||
<li className="flex items-center gap-4"><CheckCircle2 size={24} className="text-yellow-500" /> Detailed Performance Analytics</li>
|
||||
<li className="flex items-center gap-4"><CheckCircle2 size={24} className="text-yellow-500" /> Parent Dashboard Access</li>
|
||||
</ul>
|
||||
<Link href="/signup">
|
||||
<Link href="/login">
|
||||
<Button size="lg" className="w-full rounded-2xl h-12 bg-yellow-500 text-black font-bold text-lg hover:bg-yellow-400">Get Pro</Button>
|
||||
</Link>
|
||||
</Card>
|
||||
@@ -485,7 +485,7 @@ export default function MarketingPage() {
|
||||
<li className="flex items-center gap-4"><CheckCircle2 size={24} className="text-[var(--cyan-ai)]" /> 24/7 Streaming AI Tutor Chat</li>
|
||||
<li className="flex items-center gap-4"><CheckCircle2 size={24} className="text-[var(--cyan-ai)]" /> Predictive Rank & College Predictor</li>
|
||||
</ul>
|
||||
<Link href="/signup">
|
||||
<Link href="/login">
|
||||
<Button size="lg" className="w-full rounded-2xl h-12 font-bold text-lg bg-[var(--cyan-ai)] text-black hover:bg-[var(--cyan-ai-80)] transition-all">Get AI Coach</Button>
|
||||
</Link>
|
||||
</Card>
|
||||
|
||||
@@ -26,7 +26,7 @@ export function PublicLayout({ children }: { children: React.ReactNode }) {
|
||||
<div className="flex items-center gap-4">
|
||||
<ThemeToggle />
|
||||
<Link href="/login" className="text-sm font-bold hover:text-primary transition-colors hidden sm:block">Log in</Link>
|
||||
<Link href="/signup">
|
||||
<Link href="/login">
|
||||
<Button className="rounded-full font-bold px-6 shadow-[0_0_15px_-5px_var(--color-primary)] hover:shadow-[0_0_25px_-5px_var(--color-primary)] transition-all">Get Started</Button>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user