fix(demo & streak): fix Neha demo account hang, enhance streak animation for light/dark modes and cries/celebration emojis

This commit is contained in:
2026-09-20 12:37:11 +05:30
parent c17785e38a
commit 6307a24c4c
4 changed files with 866 additions and 433 deletions

View File

@@ -660,7 +660,7 @@ DEMO_DEFINITIONS = {
}, },
UUID("33333333-3333-3333-3333-333333333333"): { UUID("33333333-3333-3333-3333-333333333333"): {
"name": "Neha Verma", "name": "Neha Verma",
"email": "demo@pragativeer.com", "email": "neha.demo@pragativeer.com",
"phone": "9999999997", "phone": "9999999997",
"student_class": models.ClassEnum.TWELFTH, "student_class": models.ClassEnum.TWELFTH,
"medium": models.MediumEnum.ENGLISH, "medium": models.MediumEnum.ENGLISH,
@@ -708,18 +708,30 @@ def ensure_demo_user(user_id: UUID, session: Session) -> Optional[models.User]:
if user: if user:
return user return user
if user_id in DEMO_DEFINITIONS: if user_id in DEMO_DEFINITIONS:
data = DEMO_DEFINITIONS[user_id] data = DEMO_DEFINITIONS[user_id].copy()
import bcrypt import bcrypt
new_demo = models.User( try:
id=user_id, # Defensive duplicate check
password_hash=bcrypt.hashpw("Pv9#kL2$mQ8!vX4@".encode('utf-8'), bcrypt.gensalt()).decode('utf-8'), existing_email = session.exec(select(models.User).where(models.User.email == data["email"])).first()
last_active=datetime.utcnow(), if existing_email and existing_email.id != user_id:
**data data["email"] = f"demo_{str(user_id)[:8]}@pragativeer.com"
) existing_phone = session.exec(select(models.User).where(models.User.phone == data["phone"])).first()
session.add(new_demo) if existing_phone and existing_phone.id != user_id:
session.commit() data["phone"] = f"99{str(int(user_id.int))[-8:]}"
session.refresh(new_demo)
return new_demo new_demo = models.User(
id=user_id,
password_hash=bcrypt.hashpw("Pv9#kL2$mQ8!vX4@".encode('utf-8'), bcrypt.gensalt()).decode('utf-8'),
last_active=datetime.utcnow(),
**data
)
session.add(new_demo)
session.commit()
session.refresh(new_demo)
return new_demo
except Exception:
session.rollback()
return session.get(models.User, user_id)
return None return None
@app.get("/api/user/switch", tags=["Users"]) @app.get("/api/user/switch", tags=["Users"])

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +1,4 @@
"use client" "use client"
import { ReactNode, useState, useEffect, useRef } from "react" import { ReactNode, useState, useEffect, useRef } from "react"
import { BookOpen, LayoutDashboard, Settings, FileText, Activity, LineChart, Trophy, User, ChevronUp, Lock, ShieldAlert, Flame, Target, Gauge, Zap, Calendar, Clock, BrainCircuit, BarChart3, School, CheckSquare } from "lucide-react" import { BookOpen, LayoutDashboard, Settings, FileText, Activity, LineChart, Trophy, User, ChevronUp, Lock, ShieldAlert, Flame, Target, Gauge, Zap, Calendar, Clock, BrainCircuit, BarChart3, School, CheckSquare } from "lucide-react"
@@ -358,9 +358,14 @@ export default function DashboardLayout({ children }: { children: ReactNode }) {
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
{/* Streak count moved to top header */} {/* Streak count moved to top header */}
{user && ( {user && (
<Link href="/dashboard/streaks" className="hover:opacity-85 transition-opacity"> <button
type="button"
onClick={() => window.dispatchEvent(new CustomEvent("pragativeer-show-streak-animation"))}
className="hover:opacity-85 transition-opacity cursor-pointer border-none bg-transparent p-0"
title="View Streak Celebration"
>
<StreakCounter streak={user.streak_count} /> <StreakCounter streak={user.streak_count} />
</Link> </button>
)} )}
{/* Demo Switcher — only visible when logged in as a known demo account */} {/* Demo Switcher — only visible when logged in as a known demo account */}
@@ -381,7 +386,7 @@ export default function DashboardLayout({ children }: { children: ReactNode }) {
<option value={DEMO_USERS.NEHA.id}>Neha Verma (AI Coach)</option> <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.AI_COACH.id}>Rahul (AI Coach)</option>
<option value={DEMO_USERS.PRO.id}>Priya (Pro Plan)</option> <option value={DEMO_USERS.PRO.id}>Priya (Pro Plan)</option>
{/* <option value={DEMO_USERS.FREE.id}>Amit (Free Plan)</option> */} <option value={DEMO_USERS.FREE.id}>Amit (Free Plan)</option>
</select> </select>
</div> </div>
)} )}

View File

@@ -1,6 +1,6 @@
"use client"; "use client";
import { useEffect, useState } from "react"; import { useEffect, useState, useRef } from "react";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Flame, Sparkles, X } from "lucide-react"; import { Flame, Sparkles, X } from "lucide-react";
import { useActiveUser } from "@/hooks/useActiveUser"; import { useActiveUser } from "@/hooks/useActiveUser";
@@ -8,23 +8,48 @@ import { useTheme } from "next-themes";
export function StreakOnboardingModal() { export function StreakOnboardingModal() {
const { user, loading } = useActiveUser(); const { user, loading } = useActiveUser();
const { resolvedTheme, theme } = useTheme(); const { resolvedTheme } = useTheme();
const activeTheme = resolvedTheme || theme || "dark"; const activeTheme = resolvedTheme ?? "light";
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
const lastUserIdRef = useRef<string | null>(null);
const lastThemeRef = useRef<string | null>(null);
useEffect(() => { useEffect(() => {
if (loading || !user) return; if (loading || !user) return;
// Check sessionStorage // Detect theme transitions on dashboard
const isFirstThemeLoad = lastThemeRef.current === null;
const themeChanged = !isFirstThemeLoad && lastThemeRef.current !== activeTheme;
lastThemeRef.current = activeTheme;
// Detect account switches (e.g. demo switcher between Neha, Rahul, Priya, Amit)
const userChanged = lastUserIdRef.current !== null && lastUserIdRef.current !== user.id;
lastUserIdRef.current = user.id;
// First visit check per user account
const hasSeen = sessionStorage.getItem(`hasSeenStreakPopup_${user.id}`); const hasSeen = sessionStorage.getItem(`hasSeenStreakPopup_${user.id}`);
if (!hasSeen) {
// Re-trigger modal when:
// 1) First time user visits (has not seen popup)
// 2) User account switched (e.g., Demo switch between Neha, Priya, Rahul, Amit)
// 3) Theme switched on dashboard (light <-> dark transition for testing & visual feedback)
if (!hasSeen || userChanged || themeChanged) {
setIsOpen(true); setIsOpen(true);
} }
}, [user, loading]); }, [user, loading, activeTheme]);
// Listen for global custom trigger event (e.g. clicking streak counter in top navbar)
useEffect(() => {
const handleOpen = () => setIsOpen(true);
window.addEventListener("pragativeer-show-streak-animation", handleOpen);
return () => window.removeEventListener("pragativeer-show-streak-animation", handleOpen);
}, []);
if (!isOpen || !user) return null; if (!isOpen || !user) return null;
const streak = user.streak_count; const streak = user.streak_count;
const isSad = streak <= 0;
const targetExam = user.target_exam?.replace("_", " ") || "JEE/NEET"; const targetExam = user.target_exam?.replace("_", " ") || "JEE/NEET";
// Streak-based wishes and titles // Streak-based wishes and titles
@@ -56,21 +81,24 @@ export function StreakOnboardingModal() {
trialDaysLeft = Math.ceil(diffTime / (1000 * 60 * 60 * 24)); trialDaysLeft = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
} }
// Key forces iframe remount whenever user or theme changes so animation replays fresh
const iframeKey = `${user.id}-${activeTheme}-${streak}`;
const iframeSrc = `/streak-animation.html?streak=${streak}&theme=${activeTheme}${isSad ? '&mood=sad' : ''}`;
return ( return (
// BUG-01 fix: backdrop click dismisses modal
<div <div
className="fixed inset-0 z-50 flex items-center justify-center bg-black/70 backdrop-blur-md animate-in fade-in duration-300" className="fixed inset-0 z-50 flex items-center justify-center bg-black/70 backdrop-blur-md animate-in fade-in duration-300"
onClick={handleDismiss} onClick={handleDismiss}
> >
{/* Centered Modal Card — stop propagation so inner clicks don't close */} {/* Centered Modal Card */}
<div <div
className="bg-background border border-border shadow-[0_0_60px_-10px_rgba(12,96,96,0.25)] rounded-3xl w-[90vw] max-w-5xl h-[85vh] max-h-[800px] relative overflow-hidden animate-in zoom-in-95 slide-in-from-bottom-4 duration-300 z-20 pointer-events-auto mx-4 flex flex-col justify-start" className="bg-background border border-border shadow-[0_0_60px_-10px_rgba(12,96,96,0.25)] rounded-3xl w-[90vw] max-w-5xl h-[85vh] max-h-[800px] relative overflow-hidden animate-in zoom-in-95 slide-in-from-bottom-4 duration-300 z-20 pointer-events-auto mx-4 flex flex-col justify-start"
onClick={(e) => e.stopPropagation()} onClick={(e) => e.stopPropagation()}
> >
{/* BUG-01 fix: X close button */} {/* Close button */}
<button <button
onClick={handleDismiss} onClick={handleDismiss}
className="absolute top-4 right-4 z-30 size-9 flex items-center justify-center rounded-full bg-background/80 border border-border text-muted-foreground hover:text-foreground hover:bg-secondary transition-all duration-200 shadow-sm" className="absolute top-4 right-4 z-30 size-9 flex items-center justify-center rounded-full bg-background/80 border border-border text-muted-foreground hover:text-foreground hover:bg-secondary transition-all duration-200 shadow-sm cursor-pointer"
aria-label="Close streak modal" aria-label="Close streak modal"
> >
<X size={18} /> <X size={18} />
@@ -80,16 +108,19 @@ export function StreakOnboardingModal() {
<div className="absolute -right-20 -top-20 size-48 bg-[var(--cyan-ai-10)] rounded-full blur-3xl pointer-events-none" /> <div className="absolute -right-20 -top-20 size-48 bg-[var(--cyan-ai-10)] rounded-full blur-3xl pointer-events-none" />
<div className="absolute -left-20 -bottom-20 size-48 bg-primary/5 rounded-full blur-3xl pointer-events-none" /> <div className="absolute -left-20 -bottom-20 size-48 bg-primary/5 rounded-full blur-3xl pointer-events-none" />
{/* Top Celebration Animation Panel (Iframe with height 60%, matching bg) */} {/* Top Celebration Animation Panel
key forces iframe remount when user switches or theme changes
so animation always replays fresh with the correct colours */}
<div className="relative w-full h-[60%] z-10 pointer-events-none overflow-hidden bg-background"> <div className="relative w-full h-[60%] z-10 pointer-events-none overflow-hidden bg-background">
<iframe <iframe
src={`/streak-animation.html?streak=${streak}&theme=${activeTheme === 'light' ? 'light' : 'dark'}`} key={iframeKey}
src={iframeSrc}
className="w-full h-full border-none" className="w-full h-full border-none"
title="Streak Celebration" title="Streak Celebration"
/> />
</div> </div>
{/* Bottom Greetings & Action Panel (height 40%, matching bg) */} {/* Bottom Greetings & Action Panel (height 40%) */}
<div className="p-8 text-center flex-1 flex flex-col justify-between bg-background z-20"> <div className="p-8 text-center flex-1 flex flex-col justify-between bg-background z-20">
<div className="space-y-4"> <div className="space-y-4">
@@ -100,13 +131,13 @@ export function StreakOnboardingModal() {
<Flame size={16} className="fill-current animate-bounce" /> {streak} Day Streak Active <Flame size={16} className="fill-current animate-bounce" /> {streak} Day Streak Active
</div> </div>
) : ( ) : (
<div className="inline-flex items-center gap-2 px-4 py-1.5 bg-blue-500/10 border border-blue-500/30 text-blue-400 text-sm md:text-base font-extrabold rounded-full tracking-wider uppercase"> <div className="inline-flex items-center gap-2 px-4 py-1.5 bg-blue-500/10 border border-blue-500/30 text-blue-500 dark:text-blue-400 text-sm md:text-base font-extrabold rounded-full tracking-wider uppercase">
<Sparkles size={16} className="animate-spin" style={{ animationDuration: '4s' }} /> Day 1 Awaits <Sparkles size={16} className="animate-spin" style={{ animationDuration: '4s' }} /> Day 1 Awaits
</div> </div>
)} )}
{trialDaysLeft > 0 && ( {trialDaysLeft > 0 && (
<div className="inline-flex items-center gap-2 px-4 py-1.5 bg-emerald-500/10 border border-emerald-500/30 text-emerald-400 text-sm md:text-base font-semibold rounded-full tracking-wider"> <div className="inline-flex items-center gap-2 px-4 py-1.5 bg-emerald-500/10 border border-emerald-500/30 text-emerald-600 dark:text-emerald-400 text-sm md:text-base font-semibold rounded-full tracking-wider">
🎁 Free Trial: {trialDaysLeft} {trialDaysLeft === 1 ? "day" : "days"} remaining 🎁 Free Trial: {trialDaysLeft} {trialDaysLeft === 1 ? "day" : "days"} remaining
</div> </div>
)} )}
@@ -114,7 +145,7 @@ export function StreakOnboardingModal() {
{/* Greetings Text */} {/* Greetings Text */}
<div className="space-y-3"> <div className="space-y-3">
<h3 className="text-2xl md:text-3xl font-black tracking-tight bg-gradient-to-r dark:from-white dark:via-white dark:to-gray-400 from-black via-black to-gray-600 bg-clip-text text-transparent"> <h3 className="text-2xl md:text-3xl font-black tracking-tight bg-gradient-to-r dark:from-white dark:via-white dark:to-gray-400 from-gray-900 via-gray-900 to-gray-600 bg-clip-text text-transparent">
{wishTitle} {wishTitle}
</h3> </h3>
<p className="text-muted-foreground text-sm md:text-base leading-relaxed max-w-lg mx-auto"> <p className="text-muted-foreground text-sm md:text-base leading-relaxed max-w-lg mx-auto">
@@ -127,12 +158,14 @@ export function StreakOnboardingModal() {
<div className="pt-2 max-w-sm w-full mx-auto"> <div className="pt-2 max-w-sm w-full mx-auto">
<Button <Button
onClick={handleDismiss} onClick={handleDismiss}
className="w-full h-12 rounded-xl text-base font-black bg-[var(--cyan-ai)] text-black hover:bg-[var(--cyan-ai-80)] shadow-[0_0_15px_-3px_var(--cyan-ai)] hover:shadow-[0_0_25px_var(--cyan-ai)] hover:-translate-y-0.5 transition-all duration-300" className="w-full py-6 text-base font-bold bg-[var(--cyan-ai)] hover:opacity-90 text-white rounded-2xl shadow-lg transition-all transform active:scale-95 cursor-pointer"
> >
Enter Workspace Continue Preparation 🚀
</Button> </Button>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
); );