Fix AI Coach typo, fix bulk register UI, and update exam tiers
This commit is contained in:
@@ -11,7 +11,7 @@ export default function SettingsPage() {
|
||||
allow_new_registrations: true,
|
||||
enable_free_trial: true,
|
||||
trial_duration_days: 5,
|
||||
trial_tier: "Al_Coach"
|
||||
trial_tier: "AI Coach"
|
||||
});
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
@@ -117,7 +117,7 @@ export default function SettingsPage() {
|
||||
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)]">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -13,11 +13,11 @@ import {
|
||||
} from "lucide-react"
|
||||
|
||||
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",
|
||||
"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) {
|
||||
if (!iso) return "—"
|
||||
@@ -164,7 +164,7 @@ export default function StudentKundliPage() {
|
||||
>
|
||||
<option value="Free">Free</option>
|
||||
<option value="Pro">Pro</option>
|
||||
<option value="Al_Coach">AI Coach</option>
|
||||
<option value="AI Coach">AI Coach</option>
|
||||
</select>
|
||||
) : (
|
||||
<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)}
|
||||
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="JEE_Main">JEE Main</option>
|
||||
<option value="JEE_Advanced">JEE Advanced</option>
|
||||
<option value="JEE_NEET">JEE + NEET</option>
|
||||
</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>
|
||||
|
||||
|
||||
@@ -9,13 +9,13 @@ 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",
|
||||
"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",
|
||||
"Free": "bg-secondary text-foreground border-border",
|
||||
}
|
||||
|
||||
const TIER_LABEL: Record<string, string> = {
|
||||
"Al_Coach": "AI Coach",
|
||||
"AI Coach": "AI Coach",
|
||||
"Pro": "Pro",
|
||||
"Free": "Free",
|
||||
}
|
||||
@@ -27,12 +27,14 @@ export default function StudentsPage() {
|
||||
const [search, setSearch] = useState("")
|
||||
const [selectedIds, setSelectedIds] = useState<string[]>([])
|
||||
const [deleting, setDeleting] = useState(false)
|
||||
const [isUploading, setIsUploading] = useState(false)
|
||||
|
||||
const fileInputRef = useRef<HTMLInputElement>(null)
|
||||
|
||||
const handleBulkUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0]
|
||||
if (!file) return
|
||||
setIsUploading(true)
|
||||
setLoading(true)
|
||||
try {
|
||||
const formData = new FormData()
|
||||
@@ -68,6 +70,7 @@ export default function StudentsPage() {
|
||||
} catch (err: any) {
|
||||
alert("Error: " + err.message)
|
||||
} finally {
|
||||
setIsUploading(false)
|
||||
setLoading(false)
|
||||
if (fileInputRef.current) fileInputRef.current.value = ""
|
||||
}
|
||||
@@ -141,9 +144,10 @@ export default function StudentsPage() {
|
||||
<Trash2 size={16} className="mr-2" /> Delete ({selectedIds.length})
|
||||
</Button>
|
||||
)}
|
||||
<input type="file" accept=".csv" className="hidden" ref={fileInputRef} onChange={handleBulkUpload} />
|
||||
<Button variant="outline" className="border-border hover:bg-secondary hover:text-white" onClick={() => fileInputRef.current?.click()}>
|
||||
<Upload size={16} className="mr-2" /> Bulk Register CSV
|
||||
<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" disabled={isUploading} onClick={() => fileInputRef.current?.click()}>
|
||||
{isUploading ? <RefreshCw size={16} className="mr-2 animate-spin" /> : <Upload size={16} className="mr-2" />}
|
||||
{isUploading ? "Uploading..." : "Bulk Register CSV"}
|
||||
</Button>
|
||||
<Button variant="outline" className="border-border hover:bg-secondary hover:text-white" onClick={downloadTemplate}>
|
||||
<Download size={16} className="mr-2" /> Download Template
|
||||
@@ -236,7 +240,9 @@ export default function StudentsPage() {
|
||||
{TIER_LABEL[s.tier] || s.tier}
|
||||
</Badge>
|
||||
</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">
|
||||
<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])
|
||||
|
||||
const amount = tier === "Al_Coach" ? 11999 : 3499
|
||||
const amount = tier === "AI Coach" ? 11999 : 3499
|
||||
|
||||
const handlePayment = async () => {
|
||||
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">
|
||||
<p className="text-sm font-bold leading-none truncate">{user ? cleanDisplayName(user.name).split(" ")[0] : "Loading..."}</p>
|
||||
<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>
|
||||
|
||||
@@ -48,7 +48,7 @@ export default function DashboardPage() {
|
||||
const rank = isNew ? "#--" : `#${userStats?.city_rank || 1} / ${userStats?.city_total || 50}`
|
||||
|
||||
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 date = new Intl.DateTimeFormat('en-IN', {
|
||||
dateStyle: 'medium',
|
||||
|
||||
@@ -33,9 +33,9 @@ const AI_COACH_FEATURES = [
|
||||
|
||||
export default function OnboardingPage() {
|
||||
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);
|
||||
// Save chosen plan preference to localStorage so dashboard can read it
|
||||
if (typeof window !== "undefined") {
|
||||
@@ -165,7 +165,7 @@ export default function OnboardingPage() {
|
||||
background:
|
||||
"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 */}
|
||||
<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
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
if (!selecting) handleSelect("Al_Coach");
|
||||
if (!selecting) handleSelect("AI Coach");
|
||||
}}
|
||||
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"
|
||||
>
|
||||
{selecting === "Al_Coach" ? (
|
||||
{selecting === "AI Coach" ? (
|
||||
<>
|
||||
<span className="size-4 border-2 border-black/30 border-t-black rounded-full animate-spin" />
|
||||
Starting Trial…
|
||||
|
||||
@@ -87,7 +87,7 @@ export default function UpgradePage() {
|
||||
</div>
|
||||
<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"
|
||||
onClick={() => handleUpgrade("Al_Coach")}
|
||||
onClick={() => handleUpgrade("AI Coach")}
|
||||
disabled={userLoading}
|
||||
>
|
||||
{userLoading ? <Loader2 className="animate-spin" /> : "Get AI Coach"}
|
||||
|
||||
@@ -15,7 +15,7 @@ export function ProgressShare() {
|
||||
const accuracy = "85"
|
||||
const name = user?.name || "Student"
|
||||
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 handleDownload = async () => {
|
||||
@@ -97,7 +97,7 @@ export function ProgressShare() {
|
||||
<div className="mt-auto flex justify-between items-end w-full pt-4">
|
||||
<div className="text-left">
|
||||
<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>
|
||||
<div className="text-right">
|
||||
|
||||
@@ -14,7 +14,7 @@ export interface UserProfile {
|
||||
school: string | null;
|
||||
city: string;
|
||||
state: string;
|
||||
tier: "Free" | "Pro" | "Al_Coach";
|
||||
tier: "Free" | "Pro" | "AI Coach";
|
||||
streak_count: number;
|
||||
longest_streak: number;
|
||||
active_session_token?: string;
|
||||
@@ -48,14 +48,14 @@ export function computeTrialInfo(user: UserProfile | null, globalSettings: any):
|
||||
trialDayNumber: number; // 1-based current day in trial
|
||||
trialDaysRemaining: number; // days left (0 if expired)
|
||||
trialTotalDays: number;
|
||||
effectiveTier: "Free" | "Pro" | "Al_Coach";
|
||||
effectiveTier: "Free" | "Pro" | "AI Coach";
|
||||
} {
|
||||
const defaultResult = {
|
||||
isTrialActive: false,
|
||||
trialDayNumber: 0,
|
||||
trialDaysRemaining: 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;
|
||||
@@ -106,7 +106,7 @@ export function computeTrialInfo(user: UserProfile | null, globalSettings: any):
|
||||
trialDaysRemaining,
|
||||
trialTotalDays: trialDays,
|
||||
effectiveTier: isTrialActive
|
||||
? ((globalSettings.trial_tier as "Pro" | "Al_Coach") ?? "Al_Coach")
|
||||
? ((globalSettings.trial_tier as "Pro" | "AI Coach") ?? "AI Coach")
|
||||
: "Free",
|
||||
};
|
||||
}
|
||||
@@ -311,14 +311,14 @@ export function useActiveUser() {
|
||||
case "weekly-test-series":
|
||||
case "full-length-mock-tests":
|
||||
case "previous-year-papers":
|
||||
case "detailed-performance-analytics":
|
||||
case "chapter-wise-progress-tracking":
|
||||
case "doubt-resolution-community":
|
||||
case "parent-dashboard":
|
||||
case "offline-mode":
|
||||
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 "personalized-study-plan":
|
||||
case "ai-tutor-doubt-resolution":
|
||||
@@ -327,7 +327,7 @@ export function useActiveUser() {
|
||||
case "ai-tutor-chat":
|
||||
case "college-predictor":
|
||||
case "exam-day-strategy":
|
||||
return tier === "Al_Coach";
|
||||
return tier === "AI Coach";
|
||||
|
||||
default:
|
||||
// Free tier features: DPS (1/day), basic dashboard, basic streak tracking, leaderboard
|
||||
|
||||
Reference in New Issue
Block a user