Fix AI Coach typo, fix bulk register UI, and update exam tiers
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
from fastapi import FastAPI, Depends, HTTPException, status, Request, UploadFile, File, Form, BackgroundTasks
|
from fastapi import FastAPI, Depends, HTTPException, status, Request, UploadFile, File, Form, BackgroundTasks
|
||||||
from fastapi.responses import StreamingResponse
|
from fastapi.responses import StreamingResponse
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
from sqlmodel import Session, select
|
from sqlmodel import Session, select, delete
|
||||||
from contextlib import asynccontextmanager
|
from contextlib import asynccontextmanager
|
||||||
from typing import List, Dict, Any, Optional
|
from typing import List, Dict, Any, Optional
|
||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
@@ -286,6 +286,21 @@ async def bulk_register_students(file: UploadFile = File(...), session: Session
|
|||||||
session.commit()
|
session.commit()
|
||||||
return results
|
return results
|
||||||
|
|
||||||
|
class DeleteUsersRequest(BaseModel):
|
||||||
|
user_ids: List[UUID]
|
||||||
|
|
||||||
|
@app.delete("/api/admin/students/bulk", tags=["Admin"])
|
||||||
|
def bulk_delete_students(payload: DeleteUsersRequest, session: Session = Depends(get_session)):
|
||||||
|
for uid in payload.user_ids:
|
||||||
|
user = session.get(models.User, uid)
|
||||||
|
if user:
|
||||||
|
# Delete associated records manually since foreign key cascade might not be configured in sqlmodel by default
|
||||||
|
session.exec(delete(models.Attempt).where(models.Attempt.user_id == uid))
|
||||||
|
session.exec(delete(models.Mistake).where(models.Mistake.user_id == uid))
|
||||||
|
session.delete(user)
|
||||||
|
session.commit()
|
||||||
|
return {"success": True}
|
||||||
|
|
||||||
@app.post("/api/auth/register", tags=["Authentication"])
|
@app.post("/api/auth/register", tags=["Authentication"])
|
||||||
def register(payload: RegisterPayload, session: Session = Depends(get_session)):
|
def register(payload: RegisterPayload, session: Session = Depends(get_session)):
|
||||||
import bcrypt
|
import bcrypt
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ class MediumEnum(str, Enum):
|
|||||||
class TierEnum(str, Enum):
|
class TierEnum(str, Enum):
|
||||||
FREE = "Free"
|
FREE = "Free"
|
||||||
PRO = "Pro"
|
PRO = "Pro"
|
||||||
AI_COACH = "Al_Coach"
|
AI_COACH = "AI Coach"
|
||||||
|
|
||||||
class QuestionTypeEnum(str, Enum):
|
class QuestionTypeEnum(str, Enum):
|
||||||
MCQ_SINGLE = "MCQ_Single"
|
MCQ_SINGLE = "MCQ_Single"
|
||||||
|
|||||||
31
demo_30_users.csv
Normal file
31
demo_30_users.csv
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
name,email,phone,target_exam,tier
|
||||||
|
Aarav Sharma,aarav.sharma@example.com,9876543201,JEE + NEET,Pro
|
||||||
|
Vivaan Gupta,vivaan.gupta@example.com,9876543202,NEET,Free
|
||||||
|
Aditya Patel,aditya.patel@example.com,9876543203,JEE,Pro
|
||||||
|
Vihaan Kumar,vihaan.kumar@example.com,9876543204,JEE + NEET,Pro
|
||||||
|
Arjun Singh,arjun.singh@example.com,9876543205,JEE,Free
|
||||||
|
Sai Reddy,sai.reddy@example.com,9876543206,JEE + NEET,AI Coach
|
||||||
|
Reyansh Das,reyansh.das@example.com,9876543207,NEET,Pro
|
||||||
|
Ayaan Joshi,ayaan.joshi@example.com,9876543208,JEE + NEET,Free
|
||||||
|
Krishna Iyer,krishna.iyer@example.com,9876543209,NEET,Pro
|
||||||
|
Ishaan Verma,ishaan.verma@example.com,9876543210,JEE,Free
|
||||||
|
Shaurya Rajput,shaurya.rajput@example.com,9876543211,JEE,AI Coach
|
||||||
|
Atharv Nair,atharv.nair@example.com,9876543212,NEET,Free
|
||||||
|
Ananya Menon,ananya.menon@example.com,9876543213,JEE + NEET,Pro
|
||||||
|
Diya Agarwal,diya.agarwal@example.com,9876543214,JEE + NEET,AI Coach
|
||||||
|
Pari Kapoor,pari.kapoor@example.com,9876543215,JEE + NEET,Free
|
||||||
|
Navya Jain,navya.jain@example.com,9876543216,JEE,Free
|
||||||
|
Kavya Desai,kavya.desai@example.com,9876543217,JEE + NEET,Free
|
||||||
|
Myra Choudhury,myra.choudhury@example.com,9876543218,JEE,Pro
|
||||||
|
Ira Sengupta,ira.sengupta@example.com,9876543219,JEE,Free
|
||||||
|
Saanvi Bhatt,saanvi.bhatt@example.com,9876543220,JEE + NEET,Pro
|
||||||
|
Rudra Tiwari,rudra.tiwari@example.com,9876543221,NEET,Pro
|
||||||
|
Kabir Mehra,kabir.mehra@example.com,9876543222,NEET,Free
|
||||||
|
Madhav Rao,madhav.rao@example.com,9876543223,JEE + NEET,AI Coach
|
||||||
|
Dhruv Menon,dhruv.menon@example.com,9876543224,JEE,Free
|
||||||
|
Aryan Dutta,aryan.dutta@example.com,9876543225,JEE + NEET,Free
|
||||||
|
Prisha Soni,prisha.soni@example.com,9876543226,JEE,Pro
|
||||||
|
Avni Khatri,avni.khatri@example.com,9876543227,JEE + NEET,Pro
|
||||||
|
Zara Sheikh,zara.sheikh@example.com,9876543228,JEE,Free
|
||||||
|
Riya Mishra,riya.mishra@example.com,9876543229,JEE,Free
|
||||||
|
Sneha Chawla,sneha.chawla@example.com,9876543230,JEE,Free
|
||||||
|
@@ -11,7 +11,7 @@ export default function SettingsPage() {
|
|||||||
allow_new_registrations: true,
|
allow_new_registrations: true,
|
||||||
enable_free_trial: true,
|
enable_free_trial: true,
|
||||||
trial_duration_days: 5,
|
trial_duration_days: 5,
|
||||||
trial_tier: "Al_Coach"
|
trial_tier: "AI Coach"
|
||||||
});
|
});
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
@@ -117,7 +117,7 @@ export default function SettingsPage() {
|
|||||||
onChange={(e) => setSettings(prev => ({ ...prev, trial_tier: e.target.value }))}
|
onChange={(e) => setSettings(prev => ({ ...prev, trial_tier: e.target.value }))}
|
||||||
className="w-full bg-background border border-border rounded-lg px-4 py-2 text-sm focus:outline-none focus:ring-1 focus:ring-[var(--cyan-ai)]">
|
className="w-full bg-background border border-border rounded-lg px-4 py-2 text-sm focus:outline-none focus:ring-1 focus:ring-[var(--cyan-ai)]">
|
||||||
<option value="Pro">Pro Tier</option>
|
<option value="Pro">Pro Tier</option>
|
||||||
<option value="Al_Coach">AI Coach (All Features)</option>
|
<option value="AI Coach">AI Coach (All Features)</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -13,11 +13,11 @@ import {
|
|||||||
} from "lucide-react"
|
} from "lucide-react"
|
||||||
|
|
||||||
const TIER_STYLE: Record<string, string> = {
|
const TIER_STYLE: Record<string, string> = {
|
||||||
"Al_Coach": "bg-[var(--cyan-ai-10)] text-[var(--cyan-ai)] border-[var(--cyan-ai-20)]",
|
"AI Coach": "bg-[var(--cyan-ai-10)] text-[var(--cyan-ai)] border-[var(--cyan-ai-20)]",
|
||||||
"Pro": "bg-yellow-500/10 text-yellow-500 border-yellow-500/20",
|
"Pro": "bg-yellow-500/10 text-yellow-500 border-yellow-500/20",
|
||||||
"Free": "bg-secondary text-foreground border-border",
|
"Free": "bg-secondary text-foreground border-border",
|
||||||
}
|
}
|
||||||
const TIER_LABEL: Record<string, string> = { "Al_Coach": "AI Coach", "Pro": "Pro", "Free": "Free" }
|
const TIER_LABEL: Record<string, string> = { "AI Coach": "AI Coach", "Pro": "Pro", "Free": "Free" }
|
||||||
|
|
||||||
function formatDate(iso?: string | null) {
|
function formatDate(iso?: string | null) {
|
||||||
if (!iso) return "—"
|
if (!iso) return "—"
|
||||||
@@ -164,7 +164,7 @@ export default function StudentKundliPage() {
|
|||||||
>
|
>
|
||||||
<option value="Free">Free</option>
|
<option value="Free">Free</option>
|
||||||
<option value="Pro">Pro</option>
|
<option value="Pro">Pro</option>
|
||||||
<option value="Al_Coach">AI Coach</option>
|
<option value="AI Coach">AI Coach</option>
|
||||||
</select>
|
</select>
|
||||||
) : (
|
) : (
|
||||||
<Badge className={TIER_STYLE[data.tier] || TIER_STYLE["Free"]} variant="outline">
|
<Badge className={TIER_STYLE[data.tier] || TIER_STYLE["Free"]} variant="outline">
|
||||||
@@ -178,13 +178,14 @@ export default function StudentKundliPage() {
|
|||||||
onChange={e => setEditExam(e.target.value)}
|
onChange={e => setEditExam(e.target.value)}
|
||||||
className="text-sm bg-secondary border border-border rounded-lg px-3 py-1.5 focus:outline-none focus:ring-1 focus:ring-primary"
|
className="text-sm bg-secondary border border-border rounded-lg px-3 py-1.5 focus:outline-none focus:ring-1 focus:ring-primary"
|
||||||
>
|
>
|
||||||
|
<option value="JEE_Main">JEE</option>
|
||||||
<option value="NEET">NEET</option>
|
<option value="NEET">NEET</option>
|
||||||
<option value="JEE_Main">JEE Main</option>
|
|
||||||
<option value="JEE_Advanced">JEE Advanced</option>
|
|
||||||
<option value="JEE_NEET">JEE + NEET</option>
|
<option value="JEE_NEET">JEE + NEET</option>
|
||||||
</select>
|
</select>
|
||||||
) : (
|
) : (
|
||||||
<Badge variant="outline" className="border-border">{data.target_exam.replace("_", " ")}</Badge>
|
<Badge variant="outline" className="border-border">
|
||||||
|
{data.target_exam === "JEE_Main" ? "JEE" : data.target_exam.replace("_", " ")}
|
||||||
|
</Badge>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -9,13 +9,13 @@ import { useRouter } from "next/navigation"
|
|||||||
import { useRef } from "react"
|
import { useRef } from "react"
|
||||||
|
|
||||||
const TIER_STYLE: Record<string, string> = {
|
const TIER_STYLE: Record<string, string> = {
|
||||||
"Al_Coach": "bg-[var(--cyan-ai-10)] text-[var(--cyan-ai)] border-[var(--cyan-ai-20)] font-bold",
|
"AI Coach": "bg-[var(--cyan-ai-10)] text-[var(--cyan-ai)] border-[var(--cyan-ai-20)] font-bold",
|
||||||
"Pro": "bg-yellow-500/10 text-yellow-500 border-yellow-500/20 font-bold",
|
"Pro": "bg-yellow-500/10 text-yellow-500 border-yellow-500/20 font-bold",
|
||||||
"Free": "bg-secondary text-foreground border-border",
|
"Free": "bg-secondary text-foreground border-border",
|
||||||
}
|
}
|
||||||
|
|
||||||
const TIER_LABEL: Record<string, string> = {
|
const TIER_LABEL: Record<string, string> = {
|
||||||
"Al_Coach": "AI Coach",
|
"AI Coach": "AI Coach",
|
||||||
"Pro": "Pro",
|
"Pro": "Pro",
|
||||||
"Free": "Free",
|
"Free": "Free",
|
||||||
}
|
}
|
||||||
@@ -27,12 +27,14 @@ export default function StudentsPage() {
|
|||||||
const [search, setSearch] = useState("")
|
const [search, setSearch] = useState("")
|
||||||
const [selectedIds, setSelectedIds] = useState<string[]>([])
|
const [selectedIds, setSelectedIds] = useState<string[]>([])
|
||||||
const [deleting, setDeleting] = useState(false)
|
const [deleting, setDeleting] = useState(false)
|
||||||
|
const [isUploading, setIsUploading] = useState(false)
|
||||||
|
|
||||||
const fileInputRef = useRef<HTMLInputElement>(null)
|
const fileInputRef = useRef<HTMLInputElement>(null)
|
||||||
|
|
||||||
const handleBulkUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
const handleBulkUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
const file = e.target.files?.[0]
|
const file = e.target.files?.[0]
|
||||||
if (!file) return
|
if (!file) return
|
||||||
|
setIsUploading(true)
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
try {
|
try {
|
||||||
const formData = new FormData()
|
const formData = new FormData()
|
||||||
@@ -68,6 +70,7 @@ export default function StudentsPage() {
|
|||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
alert("Error: " + err.message)
|
alert("Error: " + err.message)
|
||||||
} finally {
|
} finally {
|
||||||
|
setIsUploading(false)
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
if (fileInputRef.current) fileInputRef.current.value = ""
|
if (fileInputRef.current) fileInputRef.current.value = ""
|
||||||
}
|
}
|
||||||
@@ -141,9 +144,10 @@ export default function StudentsPage() {
|
|||||||
<Trash2 size={16} className="mr-2" /> Delete ({selectedIds.length})
|
<Trash2 size={16} className="mr-2" /> Delete ({selectedIds.length})
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
<input type="file" accept=".csv" className="hidden" ref={fileInputRef} onChange={handleBulkUpload} />
|
<input type="file" accept=".csv" className="hidden" ref={fileInputRef} onChange={handleBulkUpload} disabled={isUploading} />
|
||||||
<Button variant="outline" className="border-border hover:bg-secondary hover:text-white" onClick={() => fileInputRef.current?.click()}>
|
<Button variant="outline" className="border-border hover:bg-secondary hover:text-white" disabled={isUploading} onClick={() => fileInputRef.current?.click()}>
|
||||||
<Upload size={16} className="mr-2" /> Bulk Register CSV
|
{isUploading ? <RefreshCw size={16} className="mr-2 animate-spin" /> : <Upload size={16} className="mr-2" />}
|
||||||
|
{isUploading ? "Uploading..." : "Bulk Register CSV"}
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="outline" className="border-border hover:bg-secondary hover:text-white" onClick={downloadTemplate}>
|
<Button variant="outline" className="border-border hover:bg-secondary hover:text-white" onClick={downloadTemplate}>
|
||||||
<Download size={16} className="mr-2" /> Download Template
|
<Download size={16} className="mr-2" /> Download Template
|
||||||
@@ -236,7 +240,9 @@ export default function StudentsPage() {
|
|||||||
{TIER_LABEL[s.tier] || s.tier}
|
{TIER_LABEL[s.tier] || s.tier}
|
||||||
</Badge>
|
</Badge>
|
||||||
</td>
|
</td>
|
||||||
<td className="px-6 py-4 font-medium">{s.target_exam.replace("_", " ")}</td>
|
<td className="px-6 py-4 font-medium">
|
||||||
|
{s.target_exam === "JEE_Main" ? "JEE" : s.target_exam.replace("_", " ")}
|
||||||
|
</td>
|
||||||
<td className="px-6 py-4 font-mono">{s.total_tests}</td>
|
<td className="px-6 py-4 font-mono">{s.total_tests}</td>
|
||||||
<td className="px-6 py-4">
|
<td className="px-6 py-4">
|
||||||
<span className={`font-bold ${s.avg_accuracy >= 80 ? "text-green-500" : s.avg_accuracy >= 60 ? "text-yellow-500" : "text-muted-foreground"}`}>
|
<span className={`font-bold ${s.avg_accuracy >= 80 ? "text-green-500" : s.avg_accuracy >= 60 ? "text-yellow-500" : "text-muted-foreground"}`}>
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ function CheckoutContent() {
|
|||||||
}
|
}
|
||||||
}, [userId, userLoading, router, tier])
|
}, [userId, userLoading, router, tier])
|
||||||
|
|
||||||
const amount = tier === "Al_Coach" ? 11999 : 3499
|
const amount = tier === "AI Coach" ? 11999 : 3499
|
||||||
|
|
||||||
const handlePayment = async () => {
|
const handlePayment = async () => {
|
||||||
if (!address.trim() || !phone.trim() || phone.length < 10) {
|
if (!address.trim() || !phone.trim() || phone.length < 10) {
|
||||||
|
|||||||
@@ -350,7 +350,7 @@ export default function DashboardLayout({ children }: { children: ReactNode }) {
|
|||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<p className="text-sm font-bold leading-none truncate">{user ? cleanDisplayName(user.name).split(" ")[0] : "Loading..."}</p>
|
<p className="text-sm font-bold leading-none truncate">{user ? cleanDisplayName(user.name).split(" ")[0] : "Loading..."}</p>
|
||||||
<div className="mt-1">
|
<div className="mt-1">
|
||||||
<TierBadge tier={user ? (user.tier === "Al_Coach" ? "AI Coach" : user.tier === "Pro" ? "Pro" : "Free") : "Free"} />
|
<TierBadge tier={user ? (user.tier === "AI Coach" ? "AI Coach" : user.tier === "Pro" ? "Pro" : "Free") : "Free"} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ export default function DashboardPage() {
|
|||||||
const rank = isNew ? "#--" : `#${userStats?.city_rank || 1} / ${userStats?.city_total || 50}`
|
const rank = isNew ? "#--" : `#${userStats?.city_rank || 1} / ${userStats?.city_total || 50}`
|
||||||
|
|
||||||
const target = user?.target_exam === "NEET" ? "NEET" : "JEE"
|
const target = user?.target_exam === "NEET" ? "NEET" : "JEE"
|
||||||
const plan = user?.tier === "Al_Coach" ? "AI Coach" : user?.tier || "Free"
|
const plan = user?.tier === "AI Coach" ? "AI Coach" : user?.tier || "Free"
|
||||||
const name = user?.name || "Student"
|
const name = user?.name || "Student"
|
||||||
const date = new Intl.DateTimeFormat('en-IN', {
|
const date = new Intl.DateTimeFormat('en-IN', {
|
||||||
dateStyle: 'medium',
|
dateStyle: 'medium',
|
||||||
|
|||||||
@@ -33,9 +33,9 @@ const AI_COACH_FEATURES = [
|
|||||||
|
|
||||||
export default function OnboardingPage() {
|
export default function OnboardingPage() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [selecting, setSelecting] = useState<"Pro" | "Al_Coach" | null>(null);
|
const [selecting, setSelecting] = useState<"Pro" | "AI Coach" | null>(null);
|
||||||
|
|
||||||
const handleSelect = async (plan: "Pro" | "Al_Coach") => {
|
const handleSelect = async (plan: "Pro" | "AI Coach") => {
|
||||||
setSelecting(plan);
|
setSelecting(plan);
|
||||||
// Save chosen plan preference to localStorage so dashboard can read it
|
// Save chosen plan preference to localStorage so dashboard can read it
|
||||||
if (typeof window !== "undefined") {
|
if (typeof window !== "undefined") {
|
||||||
@@ -165,7 +165,7 @@ export default function OnboardingPage() {
|
|||||||
background:
|
background:
|
||||||
"linear-gradient(135deg, rgba(12,96,96,0.5) 0%, rgba(12,96,96,0.08) 100%)",
|
"linear-gradient(135deg, rgba(12,96,96,0.5) 0%, rgba(12,96,96,0.08) 100%)",
|
||||||
}}
|
}}
|
||||||
onClick={() => !selecting && handleSelect("Al_Coach")}
|
onClick={() => !selecting && handleSelect("AI Coach")}
|
||||||
>
|
>
|
||||||
{/* Most Popular Badge */}
|
{/* Most Popular Badge */}
|
||||||
<div className="absolute -top-4 left-1/2 -translate-x-1/2 z-10 inline-flex items-center gap-1.5 px-4 py-1.5 rounded-full bg-[var(--cyan-ai)] text-black text-xs font-black uppercase tracking-wider shadow-[0_0_20px_-3px_var(--cyan-ai)]">
|
<div className="absolute -top-4 left-1/2 -translate-x-1/2 z-10 inline-flex items-center gap-1.5 px-4 py-1.5 rounded-full bg-[var(--cyan-ai)] text-black text-xs font-black uppercase tracking-wider shadow-[0_0_20px_-3px_var(--cyan-ai)]">
|
||||||
@@ -201,12 +201,12 @@ export default function OnboardingPage() {
|
|||||||
<button
|
<button
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
if (!selecting) handleSelect("Al_Coach");
|
if (!selecting) handleSelect("AI Coach");
|
||||||
}}
|
}}
|
||||||
disabled={!!selecting}
|
disabled={!!selecting}
|
||||||
className="w-full h-12 rounded-xl bg-[var(--cyan-ai)] text-black font-bold text-base hover:bg-[var(--cyan-ai-80)] transition-all duration-300 shadow-[0_0_20px_-5px_rgba(12,96,96,0.5)] hover:shadow-[0_0_30px_-3px_rgba(12,96,96,0.8)] hover:-translate-y-0.5 disabled:opacity-70 disabled:cursor-not-allowed flex items-center justify-center gap-2"
|
className="w-full h-12 rounded-xl bg-[var(--cyan-ai)] text-black font-bold text-base hover:bg-[var(--cyan-ai-80)] transition-all duration-300 shadow-[0_0_20px_-5px_rgba(12,96,96,0.5)] hover:shadow-[0_0_30px_-3px_rgba(12,96,96,0.8)] hover:-translate-y-0.5 disabled:opacity-70 disabled:cursor-not-allowed flex items-center justify-center gap-2"
|
||||||
>
|
>
|
||||||
{selecting === "Al_Coach" ? (
|
{selecting === "AI Coach" ? (
|
||||||
<>
|
<>
|
||||||
<span className="size-4 border-2 border-black/30 border-t-black rounded-full animate-spin" />
|
<span className="size-4 border-2 border-black/30 border-t-black rounded-full animate-spin" />
|
||||||
Starting Trial…
|
Starting Trial…
|
||||||
|
|||||||
@@ -87,7 +87,7 @@ export default function UpgradePage() {
|
|||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
className="w-full font-bold text-lg rounded-2xl h-12 bg-[var(--cyan-ai)] text-black hover:bg-[var(--cyan-ai-80)] transition-all"
|
className="w-full font-bold text-lg rounded-2xl h-12 bg-[var(--cyan-ai)] text-black hover:bg-[var(--cyan-ai-80)] transition-all"
|
||||||
onClick={() => handleUpgrade("Al_Coach")}
|
onClick={() => handleUpgrade("AI Coach")}
|
||||||
disabled={userLoading}
|
disabled={userLoading}
|
||||||
>
|
>
|
||||||
{userLoading ? <Loader2 className="animate-spin" /> : "Get AI Coach"}
|
{userLoading ? <Loader2 className="animate-spin" /> : "Get AI Coach"}
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ export function ProgressShare() {
|
|||||||
const accuracy = "85"
|
const accuracy = "85"
|
||||||
const name = user?.name || "Student"
|
const name = user?.name || "Student"
|
||||||
const target = user?.target_exam === "NEET" ? "NEET" : "JEE"
|
const target = user?.target_exam === "NEET" ? "NEET" : "JEE"
|
||||||
const plan = user?.tier === "Al_Coach" ? "AI Coach" : user?.tier || "Free"
|
const plan = user?.tier === "AI Coach" ? "AI Coach" : user?.tier || "Free"
|
||||||
const imageUrl = `/api/og/progress?streak=${streak}&accuracy=${accuracy}&name=${encodeURIComponent(name)}&target=${target}&plan=${encodeURIComponent(plan)}`
|
const imageUrl = `/api/og/progress?streak=${streak}&accuracy=${accuracy}&name=${encodeURIComponent(name)}&target=${target}&plan=${encodeURIComponent(plan)}`
|
||||||
|
|
||||||
const handleDownload = async () => {
|
const handleDownload = async () => {
|
||||||
@@ -97,7 +97,7 @@ export function ProgressShare() {
|
|||||||
<div className="mt-auto flex justify-between items-end w-full pt-4">
|
<div className="mt-auto flex justify-between items-end w-full pt-4">
|
||||||
<div className="text-left">
|
<div className="text-left">
|
||||||
<div className="text-sm font-bold text-white">{name}</div>
|
<div className="text-sm font-bold text-white">{name}</div>
|
||||||
<div className="text-[10px] text-white/70 mt-0.5">{user?.tier === "Al_Coach" ? "AI Coach Plan" : `${user?.tier || "Free"} Plan`}</div>
|
<div className="text-[10px] text-white/70 mt-0.5">{user?.tier === "AI Coach" ? "AI Coach Plan" : `${user?.tier || "Free"} Plan`}</div>
|
||||||
<div className="text-[10px] text-[var(--cyan-ai)]">{user?.target_exam === "NEET" ? "NEET Aspirant" : "JEE Aspirant"}</div>
|
<div className="text-[10px] text-[var(--cyan-ai)]">{user?.target_exam === "NEET" ? "NEET Aspirant" : "JEE Aspirant"}</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-right">
|
<div className="text-right">
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ export interface UserProfile {
|
|||||||
school: string | null;
|
school: string | null;
|
||||||
city: string;
|
city: string;
|
||||||
state: string;
|
state: string;
|
||||||
tier: "Free" | "Pro" | "Al_Coach";
|
tier: "Free" | "Pro" | "AI Coach";
|
||||||
streak_count: number;
|
streak_count: number;
|
||||||
longest_streak: number;
|
longest_streak: number;
|
||||||
active_session_token?: string;
|
active_session_token?: string;
|
||||||
@@ -48,14 +48,14 @@ export function computeTrialInfo(user: UserProfile | null, globalSettings: any):
|
|||||||
trialDayNumber: number; // 1-based current day in trial
|
trialDayNumber: number; // 1-based current day in trial
|
||||||
trialDaysRemaining: number; // days left (0 if expired)
|
trialDaysRemaining: number; // days left (0 if expired)
|
||||||
trialTotalDays: number;
|
trialTotalDays: number;
|
||||||
effectiveTier: "Free" | "Pro" | "Al_Coach";
|
effectiveTier: "Free" | "Pro" | "AI Coach";
|
||||||
} {
|
} {
|
||||||
const defaultResult = {
|
const defaultResult = {
|
||||||
isTrialActive: false,
|
isTrialActive: false,
|
||||||
trialDayNumber: 0,
|
trialDayNumber: 0,
|
||||||
trialDaysRemaining: 0,
|
trialDaysRemaining: 0,
|
||||||
trialTotalDays: 0,
|
trialTotalDays: 0,
|
||||||
effectiveTier: (user?.tier ?? "Free") as "Free" | "Pro" | "Al_Coach",
|
effectiveTier: (user?.tier ?? "Free") as "Free" | "Pro" | "AI Coach",
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!user || !globalSettings) return defaultResult;
|
if (!user || !globalSettings) return defaultResult;
|
||||||
@@ -106,7 +106,7 @@ export function computeTrialInfo(user: UserProfile | null, globalSettings: any):
|
|||||||
trialDaysRemaining,
|
trialDaysRemaining,
|
||||||
trialTotalDays: trialDays,
|
trialTotalDays: trialDays,
|
||||||
effectiveTier: isTrialActive
|
effectiveTier: isTrialActive
|
||||||
? ((globalSettings.trial_tier as "Pro" | "Al_Coach") ?? "Al_Coach")
|
? ((globalSettings.trial_tier as "Pro" | "AI Coach") ?? "AI Coach")
|
||||||
: "Free",
|
: "Free",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -311,14 +311,14 @@ export function useActiveUser() {
|
|||||||
case "weekly-test-series":
|
case "weekly-test-series":
|
||||||
case "full-length-mock-tests":
|
case "full-length-mock-tests":
|
||||||
case "previous-year-papers":
|
case "previous-year-papers":
|
||||||
case "detailed-performance-analytics":
|
|
||||||
case "chapter-wise-progress-tracking":
|
case "chapter-wise-progress-tracking":
|
||||||
case "doubt-resolution-community":
|
case "doubt-resolution-community":
|
||||||
case "parent-dashboard":
|
case "parent-dashboard":
|
||||||
case "offline-mode":
|
case "offline-mode":
|
||||||
case "pdf-export":
|
case "pdf-export":
|
||||||
return tier === "Pro" || tier === "Al_Coach";
|
return tier === "Pro" || tier === "AI Coach";
|
||||||
|
|
||||||
|
case "detailed-performance-analytics":
|
||||||
case "ai-coach-custom-schedules":
|
case "ai-coach-custom-schedules":
|
||||||
case "personalized-study-plan":
|
case "personalized-study-plan":
|
||||||
case "ai-tutor-doubt-resolution":
|
case "ai-tutor-doubt-resolution":
|
||||||
@@ -327,7 +327,7 @@ export function useActiveUser() {
|
|||||||
case "ai-tutor-chat":
|
case "ai-tutor-chat":
|
||||||
case "college-predictor":
|
case "college-predictor":
|
||||||
case "exam-day-strategy":
|
case "exam-day-strategy":
|
||||||
return tier === "Al_Coach";
|
return tier === "AI Coach";
|
||||||
|
|
||||||
default:
|
default:
|
||||||
// Free tier features: DPS (1/day), basic dashboard, basic streak tracking, leaderboard
|
// Free tier features: DPS (1/day), basic dashboard, basic streak tracking, leaderboard
|
||||||
|
|||||||
Reference in New Issue
Block a user