fix(demo & streak): fix Neha demo account hang, enhance streak animation for light/dark modes and cries/celebration emojis
This commit is contained in:
@@ -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
@@ -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>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user