fix: demo name, trial flow, signup profile fields, docker pgbouncer/frontend

- page.tsx: hero mock shows 'Neha Verma' + 'AI Coach Plan' (was 'Demo Student')
- backend/main.py: demo login maps to Neha Verma UUID (33333333-...)
- backend/main.py: register endpoint stores exam/class/medium/school + trial_end from admin settings
- backend/main.py: register checks allow_new_registrations setting
- backend/main.py: admin student list includes trial_end field
- useActiveUser.ts: rewrite trial logic using real created_at date
- useActiveUser.ts: add computeTrialInfo(), isDemoUser() helpers + expose trialInfo from hook
- useActiveUser.ts: hasAccessTo() now uses effectiveTier from trial logic
- dashboard/layout.tsx: hide demo switcher for real users (only show for known demo accounts)
- dashboard/layout.tsx: trial badge in top header (Day N/15 Trial · Xd left)
- dashboard/layout.tsx: trial day shown in sidebar user card and dropdown
- signup/page.tsx: wire step 2 selects (exam/class/medium/school) to state + send to backend
- signup/page.tsx: success screen shows 'Day 1 of your Free Trial has started!'
- userlist.txt: was empty directory causing pgbouncer mount failure; replaced with proper file
- pgbouncer.ini: added DB credentials in connection string; fixed admin_users
- Dockerfile: added ARG NEXT_PUBLIC_API_URL baked at build time (needed for standalone)
- docker-compose.yml: frontend build arg http://backend:8000; pgbouncer healthcheck; backend waits for pgbouncer
This commit is contained in:
2026-09-13 21:19:35 +05:30
parent d0472da976
commit a81e53c781
11 changed files with 261 additions and 77 deletions

View File

@@ -10,7 +10,7 @@ import { ThemeToggle } from "@/components/ThemeToggle"
import { TierBadge } from "@/components/TierBadge"
import { StreakCounter } from "@/components/StreakCounter"
import { AITutorChat } from "@/components/AI/AITutorChat"
import { useActiveUser, DEMO_USERS } from "@/hooks/useActiveUser"
import { useActiveUser, DEMO_USERS, isDemoUser } from "@/hooks/useActiveUser"
import { StreakOnboardingModal } from "@/components/Dashboard/StreakOnboardingModal"
import { Badge } from "@/components/ui/badge"
import MaintenanceScreen from "@/components/MaintenanceScreen"
@@ -20,7 +20,7 @@ export default function DashboardLayout({ children }: { children: ReactNode }) {
const [dropdownOpen, setDropdownOpen] = useState(false)
const dropdownRef = useRef<HTMLDivElement>(null)
const { userId, user, globalSettings, switchUser, logout, hasAccessTo, loading } = useActiveUser();
const { userId, user, globalSettings, switchUser, logout, hasAccessTo, loading, trialInfo } = useActiveUser();
const [maintenance, setMaintenance] = useState(false);
const [checkingMaintenance, setCheckingMaintenance] = useState(true);
@@ -253,17 +253,28 @@ export default function DashboardLayout({ children }: { children: ReactNode }) {
<div className="px-4 py-3 border-b border-border bg-secondary/30">
<p className="text-sm font-bold text-foreground truncate">{user.name}</p>
<p className="text-xs text-muted-foreground truncate">{user.email || user.phone}</p>
{/* Trial info in dropdown */}
{trialInfo.isTrialActive && (
<div className="mt-1.5 flex items-center gap-1.5">
<span className="inline-flex items-center gap-1 text-[10px] font-bold px-2 py-0.5 rounded-full bg-primary/10 text-primary border border-primary/20">
Day {trialInfo.trialDayNumber} of {trialInfo.trialTotalDays} Trial
</span>
<span className="text-[10px] text-muted-foreground">{trialInfo.trialDaysRemaining}d left</span>
</div>
)}
</div>
<div className="p-1.5">
<Link href="/dashboard/settings" className="flex items-center gap-2 px-3 py-2 text-sm text-foreground hover:bg-secondary rounded-lg transition-colors">
<User size={16} /> Edit Profile
</Link>
<button
onClick={() => switchUser(DEMO_USERS.AI_COACH.id)}
className="w-full text-left flex items-center gap-2 px-3 py-2 text-sm text-foreground hover:bg-secondary rounded-lg transition-colors"
>
Reset Demo
</button>
{isDemoUser(userId) && (
<button
onClick={() => switchUser(DEMO_USERS.NEHA.id)}
className="w-full text-left flex items-center gap-2 px-3 py-2 text-sm text-foreground hover:bg-secondary rounded-lg transition-colors"
>
Reset Demo
</button>
)}
<div className="h-px bg-border my-1" />
<button
onClick={logout}
@@ -285,7 +296,15 @@ export default function DashboardLayout({ children }: { children: ReactNode }) {
</div>
<div className="flex-1 min-w-0">
<p className="text-sm font-bold leading-none truncate">{user ? user.name.split(" ")[0] : "Loading..."}</p>
<div className="mt-1"><TierBadge tier={user ? (user.tier === "Al_Coach" ? "AI Coach" : user.tier) : "Free"} /></div>
<div className="mt-1">
{trialInfo.isTrialActive ? (
<span className="inline-flex items-center text-[10px] font-bold text-primary">
Day {trialInfo.trialDayNumber}/{trialInfo.trialTotalDays} Trial
</span>
) : (
<TierBadge tier={user ? (user.tier === "Al_Coach" ? "AI Coach" : user.tier) : "Free"} />
)}
</div>
</div>
</div>
<ChevronUp size={15} className={`text-muted-foreground transition-transform shrink-0 ${dropdownOpen ? "rotate-180" : ""}`} />
@@ -307,6 +326,13 @@ export default function DashboardLayout({ children }: { children: ReactNode }) {
{targetExamLabel} Target
</Badge>
)}
{/* Trial countdown badge — shown to trial users in top header */}
{trialInfo.isTrialActive && (
<Badge variant="outline" className="bg-primary/10 text-primary border-primary/20 font-bold px-2.5 py-0.5 hidden md:inline-flex gap-1">
<span className="size-1.5 rounded-full bg-primary animate-pulse"></span>
Day {trialInfo.trialDayNumber}/{trialInfo.trialTotalDays} Trial · {trialInfo.trialDaysRemaining}d left
</Badge>
)}
</div>
<div className="flex items-center gap-4">
@@ -317,20 +343,22 @@ export default function DashboardLayout({ children }: { children: ReactNode }) {
</Link>
)}
{/* Profile switcher dropdown for testing */}
<div className="flex items-center gap-2">
<span className="text-xs text-muted-foreground font-bold hidden lg:inline">Demo Switcher:</span>
<select
value={userId}
onChange={(e) => switchUser(e.target.value)}
className="bg-secondary border border-border text-foreground px-2.5 py-1.5 rounded-xl text-xs font-bold focus:ring-2 focus:ring-[#00F0FF] outline-none cursor-pointer hover:bg-secondary/80 transition-colors"
>
<option value={DEMO_USERS.NEHA.id}>Neha Verma (AI Coach)</option>
<option value={DEMO_USERS.AI_COACH.id}>Rahul (AI Coach)</option>
<option value={DEMO_USERS.PRO.id}>Priya (Pro Plan)</option>
{/* <option value={DEMO_USERS.FREE.id}>Amit (Free Plan)</option> */}
</select>
</div>
{/* Demo Switcher — only visible when logged in as a known demo account */}
{isDemoUser(userId) && (
<div className="flex items-center gap-2">
<span className="text-xs text-muted-foreground font-bold hidden lg:inline">Demo:</span>
<select
value={userId}
onChange={(e) => switchUser(e.target.value)}
className="bg-secondary border border-border text-foreground px-2.5 py-1.5 rounded-xl text-xs font-bold focus:ring-2 focus:ring-[#00F0FF] outline-none cursor-pointer hover:bg-secondary/80 transition-colors"
>
<option value={DEMO_USERS.NEHA.id}>Neha Verma (AI Coach)</option>
<option value={DEMO_USERS.AI_COACH.id}>Rahul (AI Coach)</option>
<option value={DEMO_USERS.PRO.id}>Priya (Pro Plan)</option>
{/* <option value={DEMO_USERS.FREE.id}>Amit (Free Plan)</option> */}
</select>
</div>
)}
<div className="flex items-center gap-2 border-l border-border pl-4">
{/* <LanguageToggle /> */}

View File

@@ -203,12 +203,12 @@ export default function MarketingPage() {
{/* Greeting & Header */}
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
<div>
<h3 className="text-xl font-bold tracking-tight">Welcome, Demo Student</h3>
<h3 className="text-xl font-bold tracking-tight">Welcome, Neha Verma</h3>
<p className="text-xs text-muted-foreground font-medium mt-1">Your adaptive NEET path is optimized. Next review in 2 hours.</p>
</div>
<div className="flex items-center gap-2 bg-secondary/30 border border-border/50 px-3 py-1.5 rounded-xl text-xs font-semibold">
<span className="size-2 rounded-full bg-emerald-500 shadow-[0_0_8px_#10b981]"></span>
Subscription: <span className="text-primary font-bold ml-1">AI Pro Plan</span>
Subscription: <span className="text-primary font-bold ml-1">AI Coach Plan</span>
</div>
</div>

View File

@@ -19,6 +19,10 @@ export default function SignupPage() {
const [lastName, setLastName] = useState("")
const [phone, setPhone] = useState("")
const [termsAccepted, setTermsAccepted] = useState(false)
const [targetExam, setTargetExam] = useState("JEE_Main")
const [studentClass, setStudentClass] = useState("12")
const [medium, setMedium] = useState("English")
const [schoolName, setSchoolName] = useState("")
// Validation temporarily relaxed: phone length check and OTP verification removed for now.
// Original: firstName.trim() !== "" && lastName.trim() !== "" && email.trim() !== "" && phone.length === 10 && termsAccepted
@@ -34,7 +38,15 @@ export default function SignupPage() {
const res = await fetch("/api/auth/register", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name: `${firstName} ${lastName}`, email, password })
body: JSON.stringify({
name: `${firstName} ${lastName}`,
email,
password,
target_exam: targetExam,
student_class: studentClass,
medium,
school: schoolName || undefined,
})
});
const data = await res.json();
@@ -97,13 +109,19 @@ export default function SignupPage() {
<Card className="border-border bg-secondary/10 backdrop-blur-xl shadow-[0_0_50px_-20px_var(--color-primary)] rounded-[2rem] overflow-hidden text-center py-8">
<CardContent className="space-y-6 pt-4 flex flex-col items-center">
<CheckCircle2 size={64} className="text-primary mb-4" />
<h2 className="text-3xl font-black tracking-tight text-foreground">Thanks for registering!</h2>
<p className="text-muted-foreground text-sm font-medium px-4 leading-relaxed">
Thank you for the registration. Our team will contact you shortly.
</p>
<h2 className="text-3xl font-black tracking-tight text-foreground">Welcome, {firstName}! 🎉</h2>
<div className="flex flex-col items-center gap-2">
<div className="inline-flex items-center gap-2 px-4 py-2 rounded-full bg-primary/10 border border-primary/20 text-primary font-bold text-sm">
<span className="size-2 rounded-full bg-primary animate-pulse"></span>
Day 1 of your Free Trial has started!
</div>
<p className="text-muted-foreground text-sm font-medium px-4 leading-relaxed">
You have full AI Coach access for your trial period. Let&apos;s build your streak starting today.
</p>
</div>
<Link href="/dashboard" className="w-full mt-4">
<Button className="w-full rounded-xl h-12 font-bold text-base bg-primary text-primary-foreground hover:bg-primary/90 transition-all shadow-[0_0_20px_-5px_var(--color-primary)]">
Go to Dashboard
Start Day 1 → Go to Dashboard
</Button>
</Link>
</CardContent>
@@ -208,16 +226,21 @@ export default function SignupPage() {
<div className="space-y-4 animate-in fade-in slide-in-from-right-8">
<div className="space-y-2">
<label className="text-sm font-bold text-foreground">Target Exam</label>
<select className="w-full bg-background border border-border rounded-xl px-4 py-3 text-sm focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent transition-all cursor-pointer">
<option value="jee_main" className="bg-background text-foreground font-medium">JEE Main</option>
<option value="jee_adv" className="bg-background text-foreground font-medium">JEE Advanced</option>
<option value="neet" className="bg-background text-foreground font-medium">NEET</option>
<option value="jee_neet" className="bg-background text-foreground font-medium">JEE + NEET</option>
<select
value={targetExam}
onChange={(e) => setTargetExam(e.target.value)}
className="w-full bg-background border border-border rounded-xl px-4 py-3 text-sm focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent transition-all cursor-pointer">
<option value="JEE_Main" className="bg-background text-foreground font-medium">JEE Main</option>
<option value="JEE_Advanced" className="bg-background text-foreground font-medium">JEE Advanced</option>
<option value="NEET" className="bg-background text-foreground font-medium">NEET</option>
</select>
</div>
<div className="space-y-2">
<label className="text-sm font-bold text-foreground">Current Class</label>
<select className="w-full bg-background border border-border rounded-xl px-4 py-3 text-sm focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent transition-all cursor-pointer">
<select
value={studentClass}
onChange={(e) => setStudentClass(e.target.value)}
className="w-full bg-background border border-border rounded-xl px-4 py-3 text-sm focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent transition-all cursor-pointer">
<option value="11" className="bg-background text-foreground font-medium">Class 11th</option>
<option value="12" className="bg-background text-foreground font-medium">Class 12th</option>
<option value="dropper" className="bg-background text-foreground font-medium">Dropper</option>
@@ -225,15 +248,23 @@ export default function SignupPage() {
</div>
<div className="space-y-2">
<label className="text-sm font-bold text-foreground">Study Medium</label>
<select className="w-full bg-background border border-border rounded-xl px-4 py-3 text-sm focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent transition-all cursor-pointer">
<option value="en" className="bg-background text-foreground font-medium">English</option>
<option value="hi" className="bg-background text-foreground font-medium">Hindi (हिंदी)</option>
<option value="bi" className="bg-background text-foreground font-medium">Bilingual (English + Hindi)</option>
<select
value={medium}
onChange={(e) => setMedium(e.target.value)}
className="w-full bg-background border border-border rounded-xl px-4 py-3 text-sm focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent transition-all cursor-pointer">
<option value="English" className="bg-background text-foreground font-medium">English</option>
<option value="Hindi" className="bg-background text-foreground font-medium">Hindi (हिंदी)</option>
<option value="Bilingual" className="bg-background text-foreground font-medium">Bilingual (English + Hindi)</option>
</select>
</div>
<div className="space-y-2">
<label className="text-sm font-bold text-foreground">School / Coaching Name <span className="text-muted-foreground font-normal">(Optional)</span></label>
<input type="text" placeholder="e.g. DPS R.K. Puram" className="w-full bg-background border border-border rounded-xl px-4 py-3 text-sm focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent transition-all"/>
<input
type="text"
value={schoolName}
onChange={(e) => setSchoolName(e.target.value)}
placeholder="e.g. DPS R.K. Puram"
className="w-full bg-background border border-border rounded-xl px-4 py-3 text-sm focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent transition-all"/>
</div>
</div>
)}