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:
@@ -374,3 +374,4 @@ PragatiVeer implements responsive SVG logo rendering based on the active theme c
|
|||||||
---
|
---
|
||||||
|
|
||||||
*Document compiled and verified for PragatiVeer v2 — Production Release*
|
*Document compiled and verified for PragatiVeer v2 — Production Release*
|
||||||
|
az
|
||||||
BIN
DESIGN_SYSTEM.pdf
Normal file
BIN
DESIGN_SYSTEM.pdf
Normal file
Binary file not shown.
@@ -19,6 +19,10 @@ COPY . .
|
|||||||
# Set environment variables for build
|
# Set environment variables for build
|
||||||
ENV NEXT_TELEMETRY_DISABLED=1
|
ENV NEXT_TELEMETRY_DISABLED=1
|
||||||
|
|
||||||
|
# Accept API URL as build arg — defaults to Docker-internal service name
|
||||||
|
ARG NEXT_PUBLIC_API_URL=http://backend:8000
|
||||||
|
ENV NEXT_PUBLIC_API_URL=${NEXT_PUBLIC_API_URL}
|
||||||
|
|
||||||
RUN npm run build
|
RUN npm run build
|
||||||
|
|
||||||
# Production image, copy all the files and run next
|
# Production image, copy all the files and run next
|
||||||
|
|||||||
@@ -105,6 +105,10 @@ class RegisterPayload(BaseModel):
|
|||||||
name: str
|
name: str
|
||||||
email: str
|
email: str
|
||||||
password: str
|
password: str
|
||||||
|
target_exam: Optional[str] = "JEE_Main"
|
||||||
|
student_class: Optional[str] = "12"
|
||||||
|
medium: Optional[str] = "English"
|
||||||
|
school: Optional[str] = None
|
||||||
|
|
||||||
class LoginPayload(BaseModel):
|
class LoginPayload(BaseModel):
|
||||||
email: str
|
email: str
|
||||||
@@ -119,18 +123,64 @@ def register(payload: RegisterPayload, session: Session = Depends(get_session)):
|
|||||||
if existing:
|
if existing:
|
||||||
raise HTTPException(status_code=400, detail="Email already registered")
|
raise HTTPException(status_code=400, detail="Email already registered")
|
||||||
|
|
||||||
|
# Check if new registrations are allowed
|
||||||
|
settings = session.get(models.SystemSettings, "global")
|
||||||
|
if not settings:
|
||||||
|
settings = models.SystemSettings()
|
||||||
|
session.add(settings)
|
||||||
|
session.commit()
|
||||||
|
session.refresh(settings)
|
||||||
|
|
||||||
|
if not settings.allow_new_registrations:
|
||||||
|
raise HTTPException(status_code=403, detail="New registrations are currently disabled.")
|
||||||
|
|
||||||
hashed_password = bcrypt.hashpw(payload.password.encode('utf-8'), bcrypt.gensalt()).decode('utf-8')
|
hashed_password = bcrypt.hashpw(payload.password.encode('utf-8'), bcrypt.gensalt()).decode('utf-8')
|
||||||
|
|
||||||
# Creating a dummy user with default enums since the payload is simplified
|
# Map exam string to enum safely
|
||||||
|
exam_map = {
|
||||||
|
"JEE_Main": models.ExamEnum.JEE_MAIN,
|
||||||
|
"JEE_Advanced": models.ExamEnum.JEE_ADVANCED,
|
||||||
|
"NEET": models.ExamEnum.NEET,
|
||||||
|
"jee_main": models.ExamEnum.JEE_MAIN,
|
||||||
|
"jee_adv": models.ExamEnum.JEE_ADVANCED,
|
||||||
|
"neet": models.ExamEnum.NEET,
|
||||||
|
}
|
||||||
|
class_map = {
|
||||||
|
"11": models.ClassEnum.ELEVENTH,
|
||||||
|
"12": models.ClassEnum.TWELFTH,
|
||||||
|
"dropper": models.ClassEnum.DROPPER,
|
||||||
|
"Dropper": models.ClassEnum.DROPPER,
|
||||||
|
}
|
||||||
|
medium_map = {
|
||||||
|
"English": models.MediumEnum.ENGLISH,
|
||||||
|
"Hindi": models.MediumEnum.HINDI,
|
||||||
|
"Bilingual": models.MediumEnum.BILINGUAL,
|
||||||
|
"en": models.MediumEnum.ENGLISH,
|
||||||
|
"hi": models.MediumEnum.HINDI,
|
||||||
|
"bi": models.MediumEnum.BILINGUAL,
|
||||||
|
}
|
||||||
|
|
||||||
|
target_exam = exam_map.get(payload.target_exam or "JEE_Main", models.ExamEnum.JEE_MAIN)
|
||||||
|
student_class = class_map.get(payload.student_class or "12", models.ClassEnum.TWELFTH)
|
||||||
|
medium = medium_map.get(payload.medium or "English", models.MediumEnum.ENGLISH)
|
||||||
|
|
||||||
|
# Calculate trial_end based on admin settings
|
||||||
|
trial_end = None
|
||||||
|
if settings.enable_free_trial and settings.trial_duration_days > 0:
|
||||||
|
trial_end = datetime.utcnow() + timedelta(days=settings.trial_duration_days)
|
||||||
|
|
||||||
new_user = models.User(
|
new_user = models.User(
|
||||||
name=payload.name,
|
name=payload.name,
|
||||||
email=payload.email,
|
email=payload.email,
|
||||||
password_hash=hashed_password,
|
password_hash=hashed_password,
|
||||||
student_class=models.ClassEnum.TWELFTH,
|
student_class=student_class,
|
||||||
target_exam=models.ExamEnum.JEE_MAIN,
|
target_exam=target_exam,
|
||||||
medium=models.MediumEnum.ENGLISH,
|
medium=medium,
|
||||||
|
school=payload.school,
|
||||||
city="Unknown",
|
city="Unknown",
|
||||||
state="Unknown"
|
state="Unknown",
|
||||||
|
trial_end=trial_end,
|
||||||
|
streak_count=0,
|
||||||
)
|
)
|
||||||
|
|
||||||
session.add(new_user)
|
session.add(new_user)
|
||||||
@@ -138,23 +188,26 @@ def register(payload: RegisterPayload, session: Session = Depends(get_session)):
|
|||||||
session.refresh(new_user)
|
session.refresh(new_user)
|
||||||
|
|
||||||
# Send welcome email in background (non-blocking)
|
# Send welcome email in background (non-blocking)
|
||||||
from fastapi.background import BackgroundTasks as BT
|
|
||||||
send_welcome_email(payload.email, payload.name)
|
send_welcome_email(payload.email, payload.name)
|
||||||
|
|
||||||
return {"success": True, "message": "User registered successfully"}
|
return {"success": True, "message": "User registered successfully", "trial_end": trial_end.isoformat() if trial_end else None}
|
||||||
|
|
||||||
@app.post("/api/auth/login", tags=["Authentication"])
|
@app.post("/api/auth/login", tags=["Authentication"])
|
||||||
def login(payload: LoginPayload, session: Session = Depends(get_session)):
|
def login(payload: LoginPayload, session: Session = Depends(get_session)):
|
||||||
import bcrypt
|
import bcrypt
|
||||||
|
|
||||||
# Allow demo/admin mock logic to still work if needed
|
# Allow demo/admin mock logic to still work if needed
|
||||||
|
# demo@pragativeer.com → Neha Verma (AI Coach demo account)
|
||||||
|
NEHA_UUID = "33333333-3333-3333-3333-333333333333"
|
||||||
if payload.email == "demo@pragativeer.com" and payload.password == "Pv9#kL2$mQ8!vX4@":
|
if payload.email == "demo@pragativeer.com" and payload.password == "Pv9#kL2$mQ8!vX4@":
|
||||||
demo_user = session.exec(select(models.User).where((models.User.email == "demo@pragativeer.com") | (models.User.email == "student@pragativeer.com"))).first()
|
import uuid as _uuid
|
||||||
if demo_user and demo_user.email != "demo@pragativeer.com":
|
demo_user = session.get(models.User, _uuid.UUID(NEHA_UUID))
|
||||||
demo_user.email = "demo@pragativeer.com"
|
if not demo_user:
|
||||||
session.add(demo_user)
|
# Fallback: look for any demo-tagged user
|
||||||
session.commit()
|
demo_user = session.exec(select(models.User).where(
|
||||||
token_id = str(demo_user.id) if demo_user else "b0e17378-864d-439e-8806-708dc48a891b"
|
(models.User.email == "demo@pragativeer.com") | (models.User.name == "Neha Verma")
|
||||||
|
)).first()
|
||||||
|
token_id = str(demo_user.id) if demo_user else NEHA_UUID
|
||||||
return {"success": True, "role": "user", "token": token_id}
|
return {"success": True, "role": "user", "token": token_id}
|
||||||
if payload.email == "admin@pragativeer.com" and payload.password == "admin123":
|
if payload.email == "admin@pragativeer.com" and payload.password == "admin123":
|
||||||
return {"success": True, "role": "admin", "token": "admin"}
|
return {"success": True, "role": "admin", "token": "admin"}
|
||||||
@@ -1047,6 +1100,7 @@ def list_all_students(session: Session = Depends(get_session)):
|
|||||||
"longest_streak": u.longest_streak,
|
"longest_streak": u.longest_streak,
|
||||||
"last_active": u.last_active.isoformat(),
|
"last_active": u.last_active.isoformat(),
|
||||||
"created_at": u.created_at.isoformat(),
|
"created_at": u.created_at.isoformat(),
|
||||||
|
"trial_end": u.trial_end.isoformat() if u.trial_end else None,
|
||||||
"total_tests": len(attempt_count),
|
"total_tests": len(attempt_count),
|
||||||
"avg_accuracy": avg_accuracy,
|
"avg_accuracy": avg_accuracy,
|
||||||
"total_mistakes": len(mistake_count),
|
"total_mistakes": len(mistake_count),
|
||||||
|
|||||||
@@ -31,6 +31,11 @@ services:
|
|||||||
depends_on:
|
depends_on:
|
||||||
postgres:
|
postgres:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -h 127.0.0.1 -p 5432 || exit 0"]
|
||||||
|
interval: 5s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
networks:
|
networks:
|
||||||
- pragativeer_network
|
- pragativeer_network
|
||||||
|
|
||||||
@@ -68,6 +73,8 @@ services:
|
|||||||
depends_on:
|
depends_on:
|
||||||
postgres:
|
postgres:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
|
pgbouncer:
|
||||||
|
condition: service_healthy
|
||||||
networks:
|
networks:
|
||||||
- pragativeer_network
|
- pragativeer_network
|
||||||
|
|
||||||
@@ -117,11 +124,13 @@ services:
|
|||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
|
args:
|
||||||
|
- NEXT_PUBLIC_API_URL=http://backend:8000
|
||||||
container_name: pragativeer_frontend
|
container_name: pragativeer_frontend
|
||||||
ports:
|
ports:
|
||||||
- "3000:3000"
|
- "3000:3000"
|
||||||
environment:
|
environment:
|
||||||
- NEXT_PUBLIC_API_URL=http://localhost:8000
|
- NEXT_PUBLIC_API_URL=http://backend:8000
|
||||||
- NEXT_PUBLIC_PAYMENT_GATEWAY=razorpay
|
- NEXT_PUBLIC_PAYMENT_GATEWAY=razorpay
|
||||||
depends_on:
|
depends_on:
|
||||||
- backend
|
- backend
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
[databases]
|
[databases]
|
||||||
pragativeer_prod = host=postgres port=5432 dbname=pragativeer_prod
|
pragativeer_prod = host=postgres port=5432 dbname=pragativeer_prod user=pragativeer_admin password=local_secure_password_2026
|
||||||
|
|
||||||
[pgbouncer]
|
[pgbouncer]
|
||||||
listen_addr = 0.0.0.0
|
listen_addr = 0.0.0.0
|
||||||
@@ -10,5 +10,5 @@ auth_file = /etc/pgbouncer/userlist.txt
|
|||||||
pool_mode = transaction
|
pool_mode = transaction
|
||||||
max_client_conn = 1000
|
max_client_conn = 1000
|
||||||
default_pool_size = 50
|
default_pool_size = 50
|
||||||
admin_users = postgres
|
admin_users = pgbouncer
|
||||||
ignore_startup_parameters = extra_float_digits
|
ignore_startup_parameters = extra_float_digits
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import { ThemeToggle } from "@/components/ThemeToggle"
|
|||||||
import { TierBadge } from "@/components/TierBadge"
|
import { TierBadge } from "@/components/TierBadge"
|
||||||
import { StreakCounter } from "@/components/StreakCounter"
|
import { StreakCounter } from "@/components/StreakCounter"
|
||||||
import { AITutorChat } from "@/components/AI/AITutorChat"
|
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 { StreakOnboardingModal } from "@/components/Dashboard/StreakOnboardingModal"
|
||||||
import { Badge } from "@/components/ui/badge"
|
import { Badge } from "@/components/ui/badge"
|
||||||
import MaintenanceScreen from "@/components/MaintenanceScreen"
|
import MaintenanceScreen from "@/components/MaintenanceScreen"
|
||||||
@@ -20,7 +20,7 @@ export default function DashboardLayout({ children }: { children: ReactNode }) {
|
|||||||
const [dropdownOpen, setDropdownOpen] = useState(false)
|
const [dropdownOpen, setDropdownOpen] = useState(false)
|
||||||
const dropdownRef = useRef<HTMLDivElement>(null)
|
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 [maintenance, setMaintenance] = useState(false);
|
||||||
const [checkingMaintenance, setCheckingMaintenance] = useState(true);
|
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">
|
<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-sm font-bold text-foreground truncate">{user.name}</p>
|
||||||
<p className="text-xs text-muted-foreground truncate">{user.email || user.phone}</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>
|
||||||
<div className="p-1.5">
|
<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">
|
<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
|
<User size={16} /> Edit Profile
|
||||||
</Link>
|
</Link>
|
||||||
<button
|
{isDemoUser(userId) && (
|
||||||
onClick={() => switchUser(DEMO_USERS.AI_COACH.id)}
|
<button
|
||||||
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"
|
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>
|
Reset Demo
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
<div className="h-px bg-border my-1" />
|
<div className="h-px bg-border my-1" />
|
||||||
<button
|
<button
|
||||||
onClick={logout}
|
onClick={logout}
|
||||||
@@ -285,7 +296,15 @@ export default function DashboardLayout({ children }: { children: ReactNode }) {
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<p className="text-sm font-bold leading-none truncate">{user ? user.name.split(" ")[0] : "Loading..."}</p>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
<ChevronUp size={15} className={`text-muted-foreground transition-transform shrink-0 ${dropdownOpen ? "rotate-180" : ""}`} />
|
<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
|
{targetExamLabel} Target
|
||||||
</Badge>
|
</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>
|
||||||
|
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
@@ -317,20 +343,22 @@ export default function DashboardLayout({ children }: { children: ReactNode }) {
|
|||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Profile switcher dropdown for testing */}
|
{/* Demo Switcher — only visible when logged in as a known demo account */}
|
||||||
<div className="flex items-center gap-2">
|
{isDemoUser(userId) && (
|
||||||
<span className="text-xs text-muted-foreground font-bold hidden lg:inline">Demo Switcher:</span>
|
<div className="flex items-center gap-2">
|
||||||
<select
|
<span className="text-xs text-muted-foreground font-bold hidden lg:inline">Demo:</span>
|
||||||
value={userId}
|
<select
|
||||||
onChange={(e) => switchUser(e.target.value)}
|
value={userId}
|
||||||
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"
|
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.NEHA.id}>Neha Verma (AI Coach)</option>
|
||||||
<option value={DEMO_USERS.PRO.id}>Priya (Pro Plan)</option>
|
<option value={DEMO_USERS.AI_COACH.id}>Rahul (AI Coach)</option>
|
||||||
{/* <option value={DEMO_USERS.FREE.id}>Amit (Free Plan)</option> */}
|
<option value={DEMO_USERS.PRO.id}>Priya (Pro Plan)</option>
|
||||||
</select>
|
{/* <option value={DEMO_USERS.FREE.id}>Amit (Free Plan)</option> */}
|
||||||
</div>
|
</select>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="flex items-center gap-2 border-l border-border pl-4">
|
<div className="flex items-center gap-2 border-l border-border pl-4">
|
||||||
{/* <LanguageToggle /> */}
|
{/* <LanguageToggle /> */}
|
||||||
|
|||||||
@@ -203,12 +203,12 @@ export default function MarketingPage() {
|
|||||||
{/* Greeting & Header */}
|
{/* Greeting & Header */}
|
||||||
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
|
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
|
||||||
<div>
|
<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>
|
<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>
|
||||||
<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">
|
<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>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,10 @@ export default function SignupPage() {
|
|||||||
const [lastName, setLastName] = useState("")
|
const [lastName, setLastName] = useState("")
|
||||||
const [phone, setPhone] = useState("")
|
const [phone, setPhone] = useState("")
|
||||||
const [termsAccepted, setTermsAccepted] = useState(false)
|
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.
|
// Validation temporarily relaxed: phone length check and OTP verification removed for now.
|
||||||
// Original: firstName.trim() !== "" && lastName.trim() !== "" && email.trim() !== "" && phone.length === 10 && termsAccepted
|
// 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", {
|
const res = await fetch("/api/auth/register", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/json" },
|
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();
|
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">
|
<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">
|
<CardContent className="space-y-6 pt-4 flex flex-col items-center">
|
||||||
<CheckCircle2 size={64} className="text-primary mb-4" />
|
<CheckCircle2 size={64} className="text-primary mb-4" />
|
||||||
<h2 className="text-3xl font-black tracking-tight text-foreground">Thanks for registering!</h2>
|
<h2 className="text-3xl font-black tracking-tight text-foreground">Welcome, {firstName}! 🎉</h2>
|
||||||
<p className="text-muted-foreground text-sm font-medium px-4 leading-relaxed">
|
<div className="flex flex-col items-center gap-2">
|
||||||
Thank you for the registration. Our team will contact you shortly.
|
<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">
|
||||||
</p>
|
<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's build your streak starting today.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
<Link href="/dashboard" className="w-full mt-4">
|
<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)]">
|
<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>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
</CardContent>
|
</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-4 animate-in fade-in slide-in-from-right-8">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<label className="text-sm font-bold text-foreground">Target Exam</label>
|
<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">
|
<select
|
||||||
<option value="jee_main" className="bg-background text-foreground font-medium">JEE Main</option>
|
value={targetExam}
|
||||||
<option value="jee_adv" className="bg-background text-foreground font-medium">JEE Advanced</option>
|
onChange={(e) => setTargetExam(e.target.value)}
|
||||||
<option value="neet" className="bg-background text-foreground font-medium">NEET</option>
|
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_neet" className="bg-background text-foreground font-medium">JEE + NEET</option>
|
<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>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<label className="text-sm font-bold text-foreground">Current Class</label>
|
<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="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="12" className="bg-background text-foreground font-medium">Class 12th</option>
|
||||||
<option value="dropper" className="bg-background text-foreground font-medium">Dropper</option>
|
<option value="dropper" className="bg-background text-foreground font-medium">Dropper</option>
|
||||||
@@ -225,15 +248,23 @@ export default function SignupPage() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<label className="text-sm font-bold text-foreground">Study Medium</label>
|
<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">
|
<select
|
||||||
<option value="en" className="bg-background text-foreground font-medium">English</option>
|
value={medium}
|
||||||
<option value="hi" className="bg-background text-foreground font-medium">Hindi (हिंदी)</option>
|
onChange={(e) => setMedium(e.target.value)}
|
||||||
<option value="bi" className="bg-background text-foreground font-medium">Bilingual (English + Hindi)</option>
|
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>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<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>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -30,6 +30,75 @@ export const DEMO_USERS = {
|
|||||||
AI_COACH: { id: DEFAULT_USER_ID, label: "Rahul Sharma (AI Coach)" },
|
AI_COACH: { id: DEFAULT_USER_ID, label: "Rahul Sharma (AI Coach)" },
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// IDs considered "demo" accounts — demo switcher only shows for these
|
||||||
|
export const DEMO_IDS = new Set(Object.values(DEMO_USERS).map(u => u.id));
|
||||||
|
|
||||||
|
/** Returns true if the userId matches a known demo account */
|
||||||
|
export function isDemoUser(userId: string): boolean {
|
||||||
|
return DEMO_IDS.has(userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Compute trial info from user profile and global settings */
|
||||||
|
export function computeTrialInfo(user: UserProfile | null, globalSettings: any): {
|
||||||
|
isTrialActive: boolean;
|
||||||
|
trialDayNumber: number; // 1-based current day in trial
|
||||||
|
trialDaysRemaining: number; // days left (0 if expired)
|
||||||
|
trialTotalDays: number;
|
||||||
|
effectiveTier: "Free" | "Pro" | "Al_Coach";
|
||||||
|
} {
|
||||||
|
const defaultResult = {
|
||||||
|
isTrialActive: false,
|
||||||
|
trialDayNumber: 0,
|
||||||
|
trialDaysRemaining: 0,
|
||||||
|
trialTotalDays: 0,
|
||||||
|
effectiveTier: (user?.tier ?? "Free") as "Free" | "Pro" | "Al_Coach",
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!user || !globalSettings) return defaultResult;
|
||||||
|
|
||||||
|
const trialDays: number = globalSettings.trial_duration_days ?? 15;
|
||||||
|
const trialEnabled: boolean = globalSettings.enable_free_trial ?? true;
|
||||||
|
|
||||||
|
// Only apply trial logic to Free tier users (not already paid Pro/AI Coach)
|
||||||
|
if (user.tier !== "Free") {
|
||||||
|
return { ...defaultResult, effectiveTier: user.tier };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!trialEnabled || trialDays <= 0) {
|
||||||
|
return defaultResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compute from created_at
|
||||||
|
const createdAt = user.created_at ? new Date(user.created_at) : null;
|
||||||
|
if (!createdAt) {
|
||||||
|
// If no created_at, assume trial is just starting (Day 1)
|
||||||
|
return {
|
||||||
|
isTrialActive: true,
|
||||||
|
trialDayNumber: 1,
|
||||||
|
trialDaysRemaining: trialDays,
|
||||||
|
trialTotalDays: trialDays,
|
||||||
|
effectiveTier: (globalSettings.trial_tier as "Pro" | "Al_Coach") ?? "Al_Coach",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const now = new Date();
|
||||||
|
const msPerDay = 1000 * 60 * 60 * 24;
|
||||||
|
const daysSinceCreation = Math.floor((now.getTime() - createdAt.getTime()) / msPerDay);
|
||||||
|
const trialDayNumber = Math.max(1, daysSinceCreation + 1); // 1-indexed
|
||||||
|
const trialDaysRemaining = Math.max(0, trialDays - daysSinceCreation);
|
||||||
|
const isTrialActive = trialDaysRemaining > 0;
|
||||||
|
|
||||||
|
return {
|
||||||
|
isTrialActive,
|
||||||
|
trialDayNumber: isTrialActive ? trialDayNumber : 0,
|
||||||
|
trialDaysRemaining,
|
||||||
|
trialTotalDays: trialDays,
|
||||||
|
effectiveTier: isTrialActive
|
||||||
|
? ((globalSettings.trial_tier as "Pro" | "Al_Coach") ?? "Al_Coach")
|
||||||
|
: "Free",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export function useActiveUser() {
|
export function useActiveUser() {
|
||||||
const [userId, setUserIdState] = useState<string>(() => {
|
const [userId, setUserIdState] = useState<string>(() => {
|
||||||
if (typeof window !== "undefined") {
|
if (typeof window !== "undefined") {
|
||||||
@@ -154,25 +223,10 @@ export function useActiveUser() {
|
|||||||
|
|
||||||
const hasAccessTo = useCallback((feature: string): boolean => {
|
const hasAccessTo = useCallback((feature: string): boolean => {
|
||||||
if (!user) return false;
|
if (!user) return false;
|
||||||
let tier = user.tier;
|
|
||||||
|
|
||||||
// Trial logic interception
|
// Compute effective tier via trial logic
|
||||||
if (globalSettings && (user.tier === 'Free' || user.id === DEMO_USERS.NEHA.id)) {
|
const { effectiveTier } = computeTrialInfo(user, globalSettings);
|
||||||
const trialDays = globalSettings.trial_duration_days !== undefined ? globalSettings.trial_duration_days : 15;
|
const tier = effectiveTier;
|
||||||
const trialEnabled = globalSettings.enable_free_trial !== undefined ? globalSettings.enable_free_trial : true;
|
|
||||||
let isTrialActive = false;
|
|
||||||
|
|
||||||
if (trialEnabled && trialDays > 0) {
|
|
||||||
// For demo purposes, we ignore created_at and just assume trial is active if days > 0
|
|
||||||
isTrialActive = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isTrialActive) {
|
|
||||||
tier = (globalSettings.trial_tier as "Pro" | "Al_Coach") || "Al_Coach";
|
|
||||||
} else {
|
|
||||||
tier = "Free";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Core restrictions mapping based on PRD feature matrix
|
// Core restrictions mapping based on PRD feature matrix
|
||||||
switch (feature) {
|
switch (feature) {
|
||||||
@@ -193,13 +247,14 @@ export function useActiveUser() {
|
|||||||
case "ai-tutor-doubt-resolution":
|
case "ai-tutor-doubt-resolution":
|
||||||
case "ai-mistake-tracking":
|
case "ai-mistake-tracking":
|
||||||
case "ai-readiness-prediction":
|
case "ai-readiness-prediction":
|
||||||
|
case "ai-tutor-chat":
|
||||||
return tier === "Al_Coach";
|
return tier === "Al_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
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}, [user]);
|
}, [user, globalSettings]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
userId,
|
userId,
|
||||||
@@ -210,6 +265,7 @@ export function useActiveUser() {
|
|||||||
switchUser,
|
switchUser,
|
||||||
logout,
|
logout,
|
||||||
hasAccessTo,
|
hasAccessTo,
|
||||||
|
trialInfo: computeTrialInfo(user, globalSettings),
|
||||||
refreshUser: async () => {
|
refreshUser: async () => {
|
||||||
await fetchUserProfile(userId);
|
await fetchUserProfile(userId);
|
||||||
window.dispatchEvent(new CustomEvent('pragativeer-user-updated'));
|
window.dispatchEvent(new CustomEvent('pragativeer-user-updated'));
|
||||||
|
|||||||
1
userlist.txt
Normal file
1
userlist.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
"pragativeer_admin" "local_secure_password_2026"
|
||||||
Reference in New Issue
Block a user