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:
@@ -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
|
||||
|
||||
@@ -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' } }))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user