fix(streak-animation): enforce strict logo keep-out sanctuary, side flank pyrotechnics, 1-2 day fragile streak crying logic, and reliable demo/theme switch modal triggers

This commit is contained in:
2026-09-20 12:58:12 +05:30
parent 6307a24c4c
commit 3adfc9b0fd
4 changed files with 268 additions and 163 deletions

View File

@@ -2,54 +2,97 @@
import { useEffect, useState, useRef } from "react";
import { Button } from "@/components/ui/button";
import { Flame, Sparkles, X } from "lucide-react";
import { useActiveUser } from "@/hooks/useActiveUser";
import { useTheme } from "next-themes";
import { Flame, Sparkles, X, Droplets } from "lucide-react";
import { useActiveUser, isDemoUser } from "@/hooks/useActiveUser";
export function StreakOnboardingModal() {
const { user, loading } = useActiveUser();
const { resolvedTheme } = useTheme();
const activeTheme = resolvedTheme ?? "light";
const [currentTheme, setCurrentTheme] = useState<"light" | "dark">("light");
const [isOpen, setIsOpen] = useState(false);
const lastUserIdRef = useRef<string | null>(null);
const lastThemeRef = useRef<string | null>(null);
const [renderCount, setRenderCount] = useState(0);
// Keep track of the active user id to trigger modal whenever it changes
const prevUserIdRef = useRef<string | null>(null);
// Sync theme directly from document.documentElement
useEffect(() => {
if (typeof window === "undefined") return;
const checkTheme = () => {
const isDark = document.documentElement.classList.contains("dark");
return isDark ? "dark" : "light";
};
setCurrentTheme(checkTheme());
// 1. Observe HTML class mutations (for immediate theme change detection)
const observer = new MutationObserver(() => {
const newTheme = checkTheme();
setCurrentTheme(newTheme);
// Re-trigger streak celebration whenever theme transitions on dashboard
setIsOpen(true);
setRenderCount((c) => c + 1);
});
observer.observe(document.documentElement, {
attributes: true,
attributeFilter: ["class"],
});
// 2. Custom event listener from ThemeToggle
const handleThemeSwitched = (e: any) => {
const t = e.detail?.theme || checkTheme();
setCurrentTheme(t);
setIsOpen(true);
setRenderCount((c) => c + 1);
};
window.addEventListener("pragativeer-theme-switched", handleThemeSwitched);
// 3. Custom event listener for user account switch (demo switcher)
const handleUserSwitched = () => {
setIsOpen(true);
setRenderCount((c) => c + 1);
};
window.addEventListener("pragativeer-user-switched", handleUserSwitched);
// 4. Custom event from top navigation flame streak counter
const handleManualOpen = () => {
setIsOpen(true);
setRenderCount((c) => c + 1);
};
window.addEventListener("pragativeer-show-streak-animation", handleManualOpen);
return () => {
observer.disconnect();
window.removeEventListener("pragativeer-theme-switched", handleThemeSwitched);
window.removeEventListener("pragativeer-user-switched", handleUserSwitched);
window.removeEventListener("pragativeer-show-streak-animation", handleManualOpen);
};
}, []);
// Account switch & initial load trigger
useEffect(() => {
if (loading || !user) return;
// Detect theme transitions on dashboard
const isFirstThemeLoad = lastThemeRef.current === null;
const themeChanged = !isFirstThemeLoad && lastThemeRef.current !== activeTheme;
lastThemeRef.current = activeTheme;
const isDifferentUser = prevUserIdRef.current !== null && prevUserIdRef.current !== user.id;
prevUserIdRef.current = user.id;
// 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
// Show popup if:
// 1. User account switched (demo plans: Neha, Rahul, Priya, Amit)
// 2. First time user visits (not yet dismissed in session)
// 3. User is on a demo account
const hasSeen = sessionStorage.getItem(`hasSeenStreakPopup_${user.id}`);
// 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) {
if (isDifferentUser || !hasSeen || isDemoUser(user.id)) {
setIsOpen(true);
}
}, [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);
}, []);
}, [user, loading]);
if (!isOpen || !user) return null;
const streak = user.streak_count;
const isSad = streak <= 0;
const streak = user.streak_count || 0;
// 1-day & 2-days (and 0-days) are the fragile streak zone -> weeping/crying emojis animation
// 3+ days is established streak -> triumphant pyrotechnics celebration!
const isSad = streak <= 2;
const targetExam = user.target_exam?.replace("_", " ") || "JEE/NEET";
// Streak-based wishes and titles
@@ -59,7 +102,10 @@ export function StreakOnboardingModal() {
if (streak === 0) {
wishTitle = "Start Your Streak Today! 🚀";
wishMessage = "Today is Day 1 of your journey. Establish your streak now and build the consistency needed for IIT/AIIMS!";
} else if (streak < 4) {
} else if (streak <= 2) {
wishTitle = "Don't Break the Chain! 💧";
wishMessage = `You are on Day ${streak} — the most fragile stage! Consistency in the first 3 days builds unstoppable momentum. Practice today!`;
} else if (streak < 7) {
wishTitle = "Keep the Fire Burning! 🔥";
wishMessage = `Great job! You have maintained a ${streak}-day study streak. Keep practicing daily to build solid muscle memory!`;
} else {
@@ -81,13 +127,13 @@ 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' : ''}`;
// Key forces iframe remount whenever user, theme, or streak changes
const iframeKey = `${user.id}-${currentTheme}-${streak}-${renderCount}`;
const iframeSrc = `/streak-animation.html?streak=${streak}&theme=${currentTheme}&mood=${isSad ? 'sad' : 'happy'}&v=${renderCount}`;
return (
<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/75 backdrop-blur-md animate-in fade-in duration-300"
onClick={handleDismiss}
>
{/* Centered Modal Card */}
@@ -108,9 +154,7 @@ 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 -left-20 -bottom-20 size-48 bg-primary/5 rounded-full blur-3xl pointer-events-none" />
{/* Top Celebration Animation Panel
key forces iframe remount when user switches or theme changes
so animation always replays fresh with the correct colours */}
{/* Top Celebration Animation Panel (Clean, symphony-professional pyrotechnics) */}
<div className="relative w-full h-[60%] z-10 pointer-events-none overflow-hidden bg-background">
<iframe
key={iframeKey}
@@ -126,10 +170,14 @@ export function StreakOnboardingModal() {
<div className="space-y-4">
{/* Badge Indicators */}
<div className="flex flex-col items-center gap-3">
{streak > 0 ? (
{streak >= 3 ? (
<div className="inline-flex items-center gap-2 px-4 py-1.5 bg-orange-500/10 border border-orange-500/30 text-orange-500 text-sm md:text-base font-extrabold rounded-full tracking-wider uppercase">
<Flame size={16} className="fill-current animate-bounce" /> {streak} Day Streak Active
</div>
) : streak > 0 ? (
<div className="inline-flex items-center gap-2 px-4 py-1.5 bg-blue-500/10 border border-blue-500/30 text-blue-600 dark:text-blue-400 text-sm md:text-base font-extrabold rounded-full tracking-wider uppercase">
<Droplets size={16} className="animate-pulse" /> Day {streak} Fragile Streak — Stay Strong!
</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-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

View File

@@ -19,14 +19,17 @@ export function ThemeToggle() {
}, [])
const toggleTheme = () => {
if (isDark) {
document.documentElement.classList.remove('dark')
localStorage.setItem('theme', 'light');
setIsDark(false)
} else {
const nextIsDark = !isDark
if (nextIsDark) {
document.documentElement.classList.add('dark')
localStorage.setItem('theme', 'dark');
localStorage.setItem('theme', 'dark')
setIsDark(true)
window.dispatchEvent(new CustomEvent("pragativeer-theme-switched", { detail: { theme: 'dark' } }))
} else {
document.documentElement.classList.remove('dark')
localStorage.setItem('theme', 'light')
setIsDark(false)
window.dispatchEvent(new CustomEvent("pragativeer-theme-switched", { detail: { theme: 'light' } }))
}
}

View File

@@ -238,6 +238,11 @@ export function useActiveUser() {
setUserIdState(newId);
setUser(null); // Clear old user immediately so stale data isn't shown
// Notify listeners immediately of account switch
if (typeof window !== "undefined") {
window.dispatchEvent(new CustomEvent("pragativeer-user-switched", { detail: { userId: newId } }));
}
try {
// ── Run session update + new user profile fetch IN PARALLEL ─────────
const [_, newUserRes] = await Promise.all([
@@ -254,6 +259,9 @@ export function useActiveUser() {
if (newUserRes && newUserRes.ok) {
const data = await newUserRes.json();
setUser(data);
if (typeof window !== "undefined") {
window.dispatchEvent(new CustomEvent("pragativeer-user-switched", { detail: { userId: newId, user: data } }));
}
} else {
// Fallback: use the regular fetch path
await fetchUserProfile(newId);