diff --git a/backend/main.py b/backend/main.py
index 6ef6431..87b0650 100644
--- a/backend/main.py
+++ b/backend/main.py
@@ -660,7 +660,7 @@ DEMO_DEFINITIONS = {
},
UUID("33333333-3333-3333-3333-333333333333"): {
"name": "Neha Verma",
- "email": "demo@pragativeer.com",
+ "email": "neha.demo@pragativeer.com",
"phone": "9999999997",
"student_class": models.ClassEnum.TWELFTH,
"medium": models.MediumEnum.ENGLISH,
@@ -708,18 +708,30 @@ def ensure_demo_user(user_id: UUID, session: Session) -> Optional[models.User]:
if user:
return user
if user_id in DEMO_DEFINITIONS:
- data = DEMO_DEFINITIONS[user_id]
+ data = DEMO_DEFINITIONS[user_id].copy()
import bcrypt
- 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
+ try:
+ # Defensive duplicate check
+ existing_email = session.exec(select(models.User).where(models.User.email == data["email"])).first()
+ if existing_email and existing_email.id != user_id:
+ data["email"] = f"demo_{str(user_id)[:8]}@pragativeer.com"
+ existing_phone = session.exec(select(models.User).where(models.User.phone == data["phone"])).first()
+ if existing_phone and existing_phone.id != user_id:
+ data["phone"] = f"99{str(int(user_id.int))[-8:]}"
+
+ 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
@app.get("/api/user/switch", tags=["Users"])
diff --git a/public/streak-animation.html b/public/streak-animation.html
index 597e128..c105054 100644
--- a/public/streak-animation.html
+++ b/public/streak-animation.html
@@ -1,450 +1,833 @@
+
+
+
+
-
- under 5 = sad · higher = bigger celebration
+
✨ Click badge to replay
+ // Click badge/stage to replay animation anytime
+ stage.addEventListener('click', () => {
+ play();
+ });
-
-
\ No newline at end of file
+
+
\ No newline at end of file
diff --git a/src/app/dashboard/layout.tsx b/src/app/dashboard/layout.tsx
index 0f9fe62..3dde3d1 100644
--- a/src/app/dashboard/layout.tsx
+++ b/src/app/dashboard/layout.tsx
@@ -1,4 +1,4 @@
-"use client"
+"use client"
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"
@@ -358,9 +358,14 @@ export default function DashboardLayout({ children }: { children: ReactNode }) {
{/* Streak count moved to top header */}
{user && (
-
+ 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"
+ >
-
+
)}
{/* Demo Switcher — only visible when logged in as a known demo account */}
@@ -381,7 +386,7 @@ export default function DashboardLayout({ children }: { children: ReactNode }) {
Neha Verma (AI Coach)
Rahul (AI Coach)
Priya (Pro Plan)
- {/* Amit (Free Plan) */}
+ Amit (Free Plan)
)}
diff --git a/src/components/Dashboard/StreakOnboardingModal.tsx b/src/components/Dashboard/StreakOnboardingModal.tsx
index 48c608b..3767522 100644
--- a/src/components/Dashboard/StreakOnboardingModal.tsx
+++ b/src/components/Dashboard/StreakOnboardingModal.tsx
@@ -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 { Flame, Sparkles, X } from "lucide-react";
import { useActiveUser } from "@/hooks/useActiveUser";
@@ -8,23 +8,48 @@ import { useTheme } from "next-themes";
export function StreakOnboardingModal() {
const { user, loading } = useActiveUser();
- const { resolvedTheme, theme } = useTheme();
- const activeTheme = resolvedTheme || theme || "dark";
+ const { resolvedTheme } = useTheme();
+ const activeTheme = resolvedTheme ?? "light";
+
const [isOpen, setIsOpen] = useState(false);
+ const lastUserIdRef = useRef
(null);
+ const lastThemeRef = useRef(null);
useEffect(() => {
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}`);
- 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);
}
- }, [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;
const streak = user.streak_count;
+ const isSad = streak <= 0;
const targetExam = user.target_exam?.replace("_", " ") || "JEE/NEET";
// Streak-based wishes and titles
@@ -56,21 +81,24 @@ export function StreakOnboardingModal() {
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 (
- // BUG-01 fix: backdrop click dismisses modal
- {/* Centered Modal Card — stop propagation so inner clicks don't close */}
+ {/* Centered Modal Card */}
e.stopPropagation()}
>
- {/* BUG-01 fix: X close button */}
+ {/* Close button */}
@@ -80,16 +108,19 @@ export function StreakOnboardingModal() {
- {/* 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 */}
- {/* Bottom Greetings & Action Panel (height 40%, matching bg) */}
+ {/* Bottom Greetings & Action Panel (height 40%) */}
@@ -100,13 +131,13 @@ export function StreakOnboardingModal() {
{streak} Day Streak Active
) : (
-
+
Day 1 Awaits
)}
{trialDaysLeft > 0 && (
-
+
🎁 Free Trial: {trialDaysLeft} {trialDaysLeft === 1 ? "day" : "days"} remaining
)}
@@ -114,7 +145,7 @@ export function StreakOnboardingModal() {
{/* Greetings Text */}
-
+
{wishTitle}
@@ -127,12 +158,14 @@ export function StreakOnboardingModal() {
- Enter Workspace
+ Continue Preparation 🚀
+
+
);