feat: complete pragativeer v2 frontend and backend, docker setup, and cbt engine

This commit is contained in:
2026-07-06 17:08:13 +05:30
parent ce01795aa9
commit 426b98ddab
66 changed files with 7949 additions and 363 deletions

226
src/app/upgrade/page.tsx Normal file
View File

@@ -0,0 +1,226 @@
"use client"
import { useState } from "react"
import { Card, CardContent, CardHeader, CardTitle, CardDescription, CardFooter } from "@/components/ui/card"
import { Button } from "@/components/ui/button"
import { Check, X, ShieldCheck, Loader2 } from "lucide-react"
const USER_ID = "b0e17378-864d-439e-8806-708dc48a891b"
const WEBHOOK_SECRET = "local_webhook_secret"
export default function UpgradePage() {
const [loadingTier, setLoadingTier] = useState<string | null>(null)
const [showModal, setShowModal] = useState(false)
const [orderData, setOrderData] = useState<any>(null)
const [processing, setProcessing] = useState(false)
const [success, setSuccess] = useState(false)
const handleUpgrade = async (tier: string) => {
setLoadingTier(tier)
try {
const res = await fetch(`http://localhost:8000/api/billing/create-order?user_id=${USER_ID}&plan_tier=${tier}`, {
method: "POST"
})
if (!res.ok) throw new Error("Order creation failed")
const order = await res.json()
setOrderData({ ...order, tier })
setShowModal(true)
} catch (err) {
console.error(err)
alert("Failed to create order")
} finally {
setLoadingTier(null)
}
}
const generateMockSignature = async (payloadStr: string) => {
const encoder = new TextEncoder();
const key = await crypto.subtle.importKey(
'raw',
encoder.encode(WEBHOOK_SECRET),
{ name: 'HMAC', hash: 'SHA-256' },
false,
['sign']
);
const signature = await crypto.subtle.sign('HMAC', key, encoder.encode(payloadStr));
return Array.from(new Uint8Array(signature)).map(b => b.toString(16).padStart(2, '0')).join('');
}
const simulatePayment = async () => {
setProcessing(true)
try {
const payloadObj = {
event: "payment.captured",
payload: {
payment: {
entity: {
id: `pay_mock_${Date.now()}`,
amount: orderData.amount,
notes: {
user_id: USER_ID,
tier_requested: orderData.tier
}
}
}
}
}
const payloadStr = JSON.stringify(payloadObj)
const signature = await generateMockSignature(payloadStr)
const res = await fetch(`http://localhost:8000/api/billing/webhook/razorpay`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-Razorpay-Signature": signature
},
body: payloadStr
})
if (!res.ok) throw new Error("Webhook failed")
setSuccess(true)
setTimeout(() => {
window.location.href = "/dashboard"
}, 2000)
} catch (err) {
console.error(err)
alert("Payment simulation failed")
setProcessing(false)
}
}
return (
<div className="p-8 max-w-7xl mx-auto animate-in fade-in duration-500 min-h-screen">
<div className="text-center max-w-2xl mx-auto mb-16">
<h1 className="text-4xl font-extrabold tracking-tight mb-4">Upgrade Your Preparation</h1>
<p className="text-xl text-muted-foreground">Unlock the full power of PragatiVeer to master JEE & NEET.</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-8 items-start">
{/* Free Tier */}
<Card className="border-border">
<CardHeader>
<CardTitle className="text-2xl">Free</CardTitle>
<CardDescription>Basic access for everyone.</CardDescription>
<div className="mt-4 font-black text-4xl">₹0<span className="text-xl text-muted-foreground font-normal">/mo</span></div>
</CardHeader>
<CardContent className="space-y-4 text-sm font-medium">
<div className="flex items-center gap-3"><Check className="text-green-500" size={18}/> Subject-wise diagnostics</div>
<div className="flex items-center gap-3"><Check className="text-green-500" size={18}/> Basic progress tracking</div>
<div className="flex items-center gap-3 text-muted-foreground opacity-50"><X size={18}/> Unlimited Tests</div>
<div className="flex items-center gap-3 text-muted-foreground opacity-50"><X size={18}/> AI Spaced Repetition</div>
<div className="flex items-center gap-3 text-muted-foreground opacity-50"><X size={18}/> Parent Analytics</div>
</CardContent>
<CardFooter>
<Button variant="outline" className="w-full" disabled>Current Plan</Button>
</CardFooter>
</Card>
{/* Pro Tier */}
<Card className="border-border border-primary shadow-lg shadow-primary/10 relative">
<div className="absolute top-0 left-1/2 -translate-x-1/2 -translate-y-1/2 bg-primary text-primary-foreground px-3 py-1 rounded-full text-xs font-bold tracking-wider uppercase">Most Popular</div>
<CardHeader>
<CardTitle className="text-2xl text-primary">Pro</CardTitle>
<CardDescription>For serious aspirants.</CardDescription>
<div className="mt-4 font-black text-4xl">₹299<span className="text-xl text-muted-foreground font-normal">/mo</span></div>
</CardHeader>
<CardContent className="space-y-4 text-sm font-medium">
<div className="flex items-center gap-3"><Check className="text-green-500" size={18}/> Everything in Free</div>
<div className="flex items-center gap-3"><Check className="text-green-500" size={18}/> Unlimited Sectional Tests</div>
<div className="flex items-center gap-3"><Check className="text-green-500" size={18}/> Performance Analytics</div>
<div className="flex items-center gap-3"><Check className="text-green-500" size={18}/> Parent Dashboard</div>
<div className="flex items-center gap-3 text-muted-foreground opacity-50"><X size={18}/> AI Mistake Heatmap</div>
<div className="flex items-center gap-3 text-muted-foreground opacity-50"><X size={18}/> Spaced Repetition (SM-2)</div>
</CardContent>
<CardFooter>
<Button
className="w-full"
onClick={() => handleUpgrade("Pro")}
disabled={!!loadingTier}
>
{loadingTier === "Pro" ? <Loader2 className="animate-spin" /> : "Upgrade to Pro"}
</Button>
</CardFooter>
</Card>
{/* AI Coach Tier */}
<Card className="border-border bg-gradient-to-b from-primary/10 to-background border-primary">
<CardHeader>
<CardTitle className="text-2xl flex items-center gap-2">AI Coach <ShieldCheck size={24} className="text-primary"/></CardTitle>
<CardDescription>Personalized mastery loop.</CardDescription>
<div className="mt-4 font-black text-4xl">₹599<span className="text-xl text-muted-foreground font-normal">/mo</span></div>
</CardHeader>
<CardContent className="space-y-4 text-sm font-medium">
<div className="flex items-center gap-3"><Check className="text-green-500" size={18}/> Everything in Pro</div>
<div className="flex items-center gap-3"><Check className="text-green-500" size={18}/> Daily AI Error Review Queue</div>
<div className="flex items-center gap-3"><Check className="text-green-500" size={18}/> SM-2 Spaced Repetition</div>
<div className="flex items-center gap-3"><Check className="text-green-500" size={18}/> Mistake Heatmap Tracking</div>
<div className="flex items-center gap-3"><Check className="text-green-500" size={18}/> AI Chat Tutor Support</div>
</CardContent>
<CardFooter>
<Button
className="w-full font-bold"
size="lg"
onClick={() => handleUpgrade("Al_Coach")}
disabled={!!loadingTier}
>
{loadingTier === "Al_Coach" ? <Loader2 className="animate-spin" /> : "Get AI Coach"}
</Button>
</CardFooter>
</Card>
</div>
{/* Mock Razorpay Modal */}
{showModal && (
<div className="fixed inset-0 bg-background/80 backdrop-blur-sm flex items-center justify-center z-50 p-4">
<Card className="max-w-md w-full shadow-2xl border-primary">
<CardHeader className="text-center pb-2">
<div className="mx-auto w-16 h-16 bg-blue-600 rounded-lg flex items-center justify-center mb-4">
<span className="text-white font-black text-2xl">RZP</span>
</div>
<CardTitle>Mock Checkout Window</CardTitle>
<CardDescription>Razorpay Test Sandbox Simulation</CardDescription>
</CardHeader>
<CardContent className="space-y-4 mt-4">
<div className="bg-secondary p-4 rounded-xl space-y-2 text-sm font-mono">
<div className="flex justify-between">
<span className="text-muted-foreground">Order ID:</span>
<span className="font-bold">{orderData?.id}</span>
</div>
<div className="flex justify-between">
<span className="text-muted-foreground">Amount:</span>
<span className="font-bold">₹{(orderData?.amount / 100).toFixed(2)}</span>
</div>
<div className="flex justify-between">
<span className="text-muted-foreground">Tier:</span>
<span className="font-bold">{orderData?.tier}</span>
</div>
</div>
{success ? (
<div className="p-4 bg-green-500/10 text-green-500 rounded-xl text-center font-bold animate-in zoom-in">
Payment Captured! Redirecting...
</div>
) : (
<p className="text-sm text-muted-foreground text-center">
Clicking confirm will securely sign and POST a payment.captured webhook to the backend.
</p>
)}
</CardContent>
<CardFooter className="flex gap-3">
<Button variant="outline" className="w-full" onClick={() => setShowModal(false)} disabled={processing || success}>
Cancel
</Button>
<Button className="w-full bg-blue-600 hover:bg-blue-700 text-white" onClick={simulatePayment} disabled={processing || success}>
{processing ? <Loader2 className="animate-spin" /> : "Confirm Payment"}
</Button>
</CardFooter>
</Card>
</div>
)}
</div>
)
}