@@ -282,7 +283,7 @@
}
applyThemeClass(isLight);
- // Poll for parent theme switch (e.g. user toggles theme on dashboard)
+ // Poll for parent theme switch (instant visual update)
setInterval(() => {
const nowLight = detectLight();
if (nowLight !== isLight) {
@@ -292,7 +293,7 @@
rebuildColors();
play();
}
- }, 200);
+ }, 180);
// ── 2. Canvases & Sizing ────────────────────────────────────────────────
const cv = document.getElementById('fw');
@@ -301,21 +302,28 @@
const ectx = ec.getContext('2d');
let W, H, DPR;
+ let keepoutRadiusSq = 0;
+
function resize() {
DPR = Math.min(2, window.devicePixelRatio || 1);
W = cv.width = ec.width = Math.floor(innerWidth * DPR);
H = cv.height = ec.height = Math.floor(innerHeight * DPR);
cv.style.width = ec.style.width = innerWidth + 'px';
cv.style.height = ec.style.height = innerHeight + 'px';
+
+ // Logo radius is 120px CSS, keep-out radius is 155px CSS
+ // STRICT KEEP-OUT ZONE around center (W/2, H/2)
+ const keepoutR = 155 * DPR;
+ keepoutRadiusSq = keepoutR * keepoutR;
}
resize();
window.addEventListener('resize', resize);
- // ── 3. High-Contrast Palettes ───────────────────────────────────────────
- let SPARK_COLORS, CONFETTI_COLORS, FLASH_COLORS;
+ // ── 3. High-Contrast Jewel / Neon Palettes ───────────────────────────────
+ let SPARK_COLORS, CONFETTI_COLORS;
function rebuildColors() {
if (isLight) {
- // Saturated jewel tones that POP vividly against white background
+ // Saturated jewel tones that POP cleanly on white cards without looking pale
SPARK_COLORS = [
'#0c6060', // Cyan AI / Deep Teal
'#e11d48', // Vivid Crimson
@@ -323,16 +331,14 @@
'#d97706', // Amber Gold
'#7c3aed', // Purple
'#059669', // Emerald
- '#dc2626', // Bright Red
- '#ea580c', // Tangerine
+ '#dc2626', // Coral Fire
];
CONFETTI_COLORS = [
'#0c6060', '#e11d48', '#2563eb', '#d97706',
- '#7c3aed', '#059669', '#ea580c', '#0284c7'
+ '#7c3aed', '#059669', '#0284c7'
];
- FLASH_COLORS = ['#ffffff', '#fde047', '#38bdf8'];
} else {
- // Luminous neon & gold that glow against dark background
+ // Radiant gold & neon that glow brilliantly against dark background
SPARK_COLORS = [
'#fde047', // Brilliant Gold
'#f59e0b', // Warm Gold
@@ -345,14 +351,13 @@
];
CONFETTI_COLORS = [
'#fde047', '#00F0FF', '#f43f5e', '#a855f7',
- '#34d399', '#ffffff', '#fbbf24'
+ '#34d399', '#ffffff'
];
- FLASH_COLORS = ['#ffffff', '#fef08a', '#7dd3fc'];
}
}
rebuildColors();
- // ── 4. Particle Systems ─────────────────────────────────────────────────
+ // ── 4. Particle Systems & STRICT LOGO KEEP-OUT ─────────────────────────
let particles = [];
let emojiParticles = [];
let shockwaves = [];
@@ -363,85 +368,107 @@
let fwTimers = [];
let mode = 'fireworks'; // 'fireworks' | 'faces'
- // Firecracker Shell Burst
- function firecrackerBurst(xr, yr, count) {
+ // Grand Pyrotechnic Shell Burst — FIRED STRICTLY ON FLANKS (Left & Right)
+ // NEVER ON TOP OF THE LOGO!
+ function flankBurst(isRightSide, count) {
+ // Left burst: x = 10% to 18%, y = 30% to 52%
+ // Right burst: x = 82% to 90%, y = 30% to 52%
+ const xr = isRightSide ? (0.81 + Math.random() * 0.09) : (0.10 + Math.random() * 0.09);
+ const yr = 0.32 + Math.random() * 0.22;
const x = xr * W;
const y = yr * H;
const n = count | 0;
- // Center shockwave
+ // Outer flank shockwave
shockwaves.push({
x, y,
r: 6 * DPR,
- maxR: (35 + Math.random() * 25) * DPR,
- vr: 2.5 * DPR,
+ maxR: (34 + Math.random() * 20) * DPR,
+ vr: 2.2 * DPR,
life: 1,
- col: isLight ? 'rgba(12, 96, 96, 0.45)' : 'rgba(253, 224, 71, 0.6)'
+ col: isLight ? 'rgba(12, 96, 96, 0.40)' : 'rgba(253, 224, 71, 0.55)'
});
for (let i = 0; i < n; i++) {
- const a = (Math.PI * 2 * i) / n + (Math.random() - 0.5) * 0.35;
- const sp = (2.2 + Math.random() * 4.8) * DPR;
+ // Angle: bias outward away from the center logo!
+ let a;
+ if (isRightSide) {
+ // Explode mostly up, right, and down-right (away from center)
+ a = (Math.random() - 0.25) * Math.PI * 1.5;
+ } else {
+ // Explode mostly up, left, and down-left (away from center)
+ a = Math.PI + (Math.random() - 0.5) * Math.PI * 1.5;
+ }
+
+ const sp = (2.2 + Math.random() * 4.6) * DPR;
const color = SPARK_COLORS[(Math.random() * SPARK_COLORS.length) | 0];
particles.push({
t: 'spark',
x, y,
vx: Math.cos(a) * sp,
- vy: Math.sin(a) * sp - 1.2 * DPR,
+ vy: Math.sin(a) * sp - 1.1 * DPR,
life: 1,
- decay: 0.012 + Math.random() * 0.014,
+ decay: 0.013 + Math.random() * 0.014,
col: color,
- size: (1.8 + Math.random() * 2.2) * DPR,
- isCrackle: Math.random() < 0.25 // occasional crackle spark
+ size: (1.8 + Math.random() * 2.0) * DPR,
+ isCrackle: Math.random() < 0.2
});
}
}
- // Spinning Confetti Strips
- function spawnConfetti(count) {
+ // Spinning Confetti Ribbons — FALL ONLY ON LEFT AND RIGHT FLANKS
+ function spawnFlankConfetti(count) {
for (let i = 0; i < count; i++) {
+ const isRight = Math.random() < 0.5;
+ // Left corridor: 4% to 24%; Right corridor: 76% to 96%
+ const xr = isRight ? (0.76 + Math.random() * 0.20) : (0.04 + Math.random() * 0.20);
+
particles.push({
t: 'conf',
- x: (0.2 + Math.random() * 0.6) * W,
+ x: xr * W,
y: -25 * DPR,
- vx: (-1.4 + Math.random() * 2.8) * DPR,
- vy: (1.6 + Math.random() * 2.6) * DPR,
+ vx: (isRight ? 1 : -1) * (0.2 + Math.random() * 1.6) * DPR,
+ vy: (1.6 + Math.random() * 2.4) * DPR,
rot: Math.random() * Math.PI * 2,
- vr: (-0.15 + Math.random() * 0.3),
+ vr: (-0.14 + Math.random() * 0.28),
life: 1,
w: (4.5 + Math.random() * 4) * DPR,
- h: (8 + Math.random() * 7) * DPR,
+ h: (8 + Math.random() * 6) * DPR,
col: CONFETTI_COLORS[(Math.random() * CONFETTI_COLORS.length) | 0]
});
}
}
- // ── 5. Emojis Raining System (Celebration OR Crying) ─────────────────────
+ // ── 5. Emojis Raining System — STRICTLY ON FLANKS ────────────────────────
const CELEBRATE_EMOJIS = ['✨', '🎉', '🎊', '🔥', '⚡', '🌟', '💥', '🚀', '🏆', '💫'];
- const CRYING_EMOJIS = ['😭', '😢', '💧', '🥺', '💔', '🌧️', '😿', '⚡'];
+ const CRYING_EMOJIS = ['😭', '😢', '💧', '🥺', '💔', '🌧️', '😿'];
- function spawnEmojiRain(count, isSad) {
+ function spawnFlankEmojiRain(count, isSad) {
const list = isSad ? CRYING_EMOJIS : CELEBRATE_EMOJIS;
for (let i = 0; i < count; i++) {
+ const isRight = Math.random() < 0.5;
+ // Spawn strictly in side corridors: 4% to 24% or 76% to 96%
+ const xr = isRight ? (0.76 + Math.random() * 0.20) : (0.04 + Math.random() * 0.20);
+
emojiParticles.push({
- x: (0.08 + Math.random() * 0.84) * W,
+ x: xr * W,
y: -35 * DPR,
- vx: (-0.8 + Math.random() * 1.6) * DPR,
- vy: (1.5 + Math.random() * 2.6) * DPR,
- rot: (-25 + Math.random() * 50) * Math.PI / 180,
+ vx: (isRight ? 0.3 : -0.3) + (Math.random() - 0.5) * 0.8 * DPR,
+ vy: (1.5 + Math.random() * 2.2) * DPR,
+ rot: (-20 + Math.random() * 40) * Math.PI / 180,
vr: (-0.02 + Math.random() * 0.04),
life: 1,
- decay: 0.0035 + Math.random() * 0.004,
- size: (18 + Math.random() * 14) * DPR,
+ decay: 0.0035 + Math.random() * 0.0035,
+ size: (18 + Math.random() * 12) * DPR,
emoji: list[(Math.random() * list.length) | 0],
swayPhase: Math.random() * Math.PI * 2,
- swaySpeed: 0.03 + Math.random() * 0.03
+ swaySpeed: 0.028 + Math.random() * 0.025
});
}
}
- // ── 6. Sad Cartoon Faces with Animated Tears ────────────────────────────
+ // ── 6. Sad Cartoon Faces (Flank Positioned, Outside Logo) ────────────────
function drawSadFace(p) {
const r = p.r;
const s = p.s;
@@ -453,7 +480,6 @@
ctx.globalAlpha = alpha;
const line = Math.max(2, r * 0.08);
- // Light mode: deep slate navy; Dark mode: glowing soft blue-white
const stroke = isLight ? '#0f172a' : 'rgba(186, 210, 245, 0.95)';
const fill = isLight ? '#f1f5f9' : 'rgba(148, 163, 184, 0.18)';
@@ -466,7 +492,7 @@
ctx.strokeStyle = stroke;
ctx.stroke();
- // Eyebrows (drooping down sad angle)
+ // Eyebrows (drooping down)
const ex = r * 0.42, ey = -r * 0.12, er = r * 0.11;
const bl = r * 0.32, tilt = (0.2 + s * 0.5);
@@ -495,7 +521,7 @@
ctx.arc(ex, ey, er, 0, Math.PI * 2);
ctx.fill();
- // Downward curved trembling sad mouth
+ // Downward sad mouth
const mw = r * 0.44;
const my = r * 0.52;
const dip = r * (0.12 + s * 0.55);
@@ -506,7 +532,7 @@
ctx.strokeStyle = stroke;
ctx.stroke();
- // Animated Tear Drops Streaming Down
+ // Animated Tear Drops
const drop1 = (p.tp % 1);
const drop2 = ((p.tp + 0.5) % 1);
const tearColor = isLight ? '#0284c7' : '#38bdf8';
@@ -529,11 +555,13 @@
ctx.restore();
}
- // ── 7. Animation Loop (Fireworks Canvas) ─────────────────────────────────
+ // ── 7. Animation Loop (Fireworks with STRICT LOGO KEEP-OUT) ──────────────
function tick() {
- // ALWAYS clearRect so modal background remains 100% crystal clean!
ctx.clearRect(0, 0, W, H);
+ const cx = W / 2;
+ const cy = H / 2;
+
// Shockwaves
for (let i = shockwaves.length - 1; i >= 0; i--) {
const sw = shockwaves[i];
@@ -544,7 +572,7 @@
continue;
}
ctx.save();
- ctx.globalAlpha = Math.max(0, sw.life * 0.6);
+ ctx.globalAlpha = Math.max(0, sw.life * 0.55);
ctx.lineWidth = 2 * DPR;
ctx.strokeStyle = sw.col;
ctx.beginPath();
@@ -559,7 +587,7 @@
if (p.t === 'spark') {
p.vx *= 0.982;
- p.vy = p.vy * 0.982 + 0.06 * DPR; // gravity
+ p.vy = p.vy * 0.982 + 0.06 * DPR;
p.x += p.vx;
p.y += p.vy;
p.life -= p.decay;
@@ -569,10 +597,17 @@
continue;
}
+ // STRICT LOGO EXCLUSION ZONE: Never draw over the central logo!
+ const dx = p.x - cx;
+ const dy = p.y - cy;
+ if (dx * dx + dy * dy < keepoutRadiusSq) {
+ continue;
+ }
+
ctx.save();
ctx.globalAlpha = Math.max(0, p.life);
- // Photorealistic Spark Trail (drawn directly with math — no canvas haze!)
+ // Photorealistic Spark Velocity Trail
ctx.strokeStyle = p.col;
ctx.lineWidth = Math.max(1, p.size * p.life * 0.75);
ctx.lineCap = 'round';
@@ -581,17 +616,17 @@
ctx.lineTo(p.x - p.vx * 2.8, p.y - p.vy * 2.8);
ctx.stroke();
- // Glowing Core / Head of spark
+ // Glowing Spark Head
ctx.fillStyle = p.col;
ctx.beginPath();
ctx.arc(p.x, p.y, p.size * p.life * 0.9, 0, Math.PI * 2);
ctx.fill();
- // Crackle mini twinkle for select sparks
- if (p.isCrackle && Math.random() < 0.3) {
+ // Subtle crackle flash
+ if (p.isCrackle && Math.random() < 0.25) {
ctx.fillStyle = '#ffffff';
ctx.beginPath();
- ctx.arc(p.x, p.y, p.size * 1.4, 0, Math.PI * 2);
+ ctx.arc(p.x, p.y, p.size * 1.3, 0, Math.PI * 2);
ctx.fill();
}
@@ -615,6 +650,13 @@
continue;
}
+ // STRICT LOGO EXCLUSION ZONE: Never draw confetti over central logo!
+ const dx = p.x - cx;
+ const dy = p.y - cy;
+ if (dx * dx + dy * dy < keepoutRadiusSq) {
+ continue;
+ }
+
ctx.save();
ctx.globalAlpha = Math.max(0, Math.min(1, p.life));
ctx.translate(p.x, p.y);
@@ -633,14 +675,17 @@
}
}
- // ── 8. Animation Loop (Emojis Canvas) ───────────────────────────────────
+ // ── 8. Animation Loop (Emojis Canvas with STRICT LOGO KEEP-OUT) ───────────
function emojiTick() {
ectx.clearRect(0, 0, W, H);
+ const cx = W / 2;
+ const cy = H / 2;
+
for (let i = emojiParticles.length - 1; i >= 0; i--) {
const p = emojiParticles[i];
p.swayPhase += p.swaySpeed;
- p.x += p.vx + Math.sin(p.swayPhase) * 0.8 * DPR;
+ p.x += p.vx + Math.sin(p.swayPhase) * 0.6 * DPR;
p.y += p.vy;
p.rot += p.vr;
p.life -= p.decay;
@@ -650,6 +695,13 @@
continue;
}
+ // STRICT LOGO EXCLUSION ZONE: Emojis NEVER overlap the central badge!
+ const dx = p.x - cx;
+ const dy = p.y - cy;
+ if (dx * dx + dy * dy < keepoutRadiusSq) {
+ continue;
+ }
+
ectx.save();
ectx.globalAlpha = Math.min(1, Math.max(0, p.life));
ectx.translate(p.x, p.y);
@@ -684,35 +736,31 @@
}
}
- // ── 10. Celebrate (Firecrackers + Sparkles + Emojis) ────────────────────
- function celebrate(t) {
+ // ── 10. Celebrate (Side Flank Pyrotechnics & Flank Emojis) ───────────────
+ function celebrate(intensity) {
stopAll();
mode = 'fireworks';
running = true;
if (!rafFW) rafFW = requestAnimationFrame(tick);
- // Emoji celebration rain
emojiRunning = true;
if (!rafEmoji) rafEmoji = requestAnimationFrame(emojiTick);
- const rounds = Math.round(5 + t * 9);
- const gap = Math.round(330 - t * 140);
- const base = Math.round(32 + t * 40);
+ const rounds = Math.round(5 + intensity * 8);
+ const gap = Math.round(340 - intensity * 130);
+ const base = Math.round(30 + intensity * 36);
for (let k = 0; k < rounds; k++) {
const d = k * gap;
- // Alternating left & right burst
- fwTimers.push(setTimeout(() => firecrackerBurst(0.12 + Math.random() * 0.08, 0.28 + Math.random() * 0.22, base), d));
- fwTimers.push(setTimeout(() => firecrackerBurst(0.80 + Math.random() * 0.08, 0.28 + Math.random() * 0.22, base), d + 60));
+ const isRight = (k % 2 === 1);
- // Center burst on higher streak intensity
- if (t > 0.35) {
- fwTimers.push(setTimeout(() => firecrackerBurst(0.44 + Math.random() * 0.12, 0.18 + Math.random() * 0.14, base + 8), d + 120));
- }
+ // Alternating left and right flank grand fireworks
+ fwTimers.push(setTimeout(() => flankBurst(false, base), d));
+ fwTimers.push(setTimeout(() => flankBurst(true, base), d + 90));
- // Confetti & celebratory emoji waves
- fwTimers.push(setTimeout(() => spawnConfetti(4 + Math.round(t * 5)), d + 40));
- fwTimers.push(setTimeout(() => spawnEmojiRain(2 + Math.round(t * 3), false), d + 100));
+ // Flank confetti and celebratory emojis (strictly on left/right wings)
+ fwTimers.push(setTimeout(() => spawnFlankConfetti(3 + Math.round(intensity * 4)), d + 40));
+ fwTimers.push(setTimeout(() => spawnFlankEmojiRain(2 + Math.round(intensity * 3), false), d + 80));
}
fwTimers.push(setTimeout(() => {
@@ -721,31 +769,30 @@
}, rounds * gap + 1200));
}
- // ── 11. Crying Sad Mood (Broken Streak / Streak = 0) ────────────────────
- function sadFaces(s) {
+ // ── 11. Crying Sad Mood (Streak <= 2: 0, 1, or 2 Days) ───────────────────
+ function sadFaces(sadness) {
stopAll();
mode = 'faces';
running = true;
if (!rafFW) rafFW = requestAnimationFrame(tick);
- // Sad crying emojis rain
emojiRunning = true;
if (!rafEmoji) rafEmoji = requestAnimationFrame(emojiTick);
- const r = Math.min(W, H) * 0.095 + s * Math.min(W, H) * 0.02;
+ const r = Math.min(W, H) * 0.09 + sadness * Math.min(W, H) * 0.02;
const y = H * 0.42;
- // Two persistent weeping cartoon faces
- particles.push({ t: 'sadface', x: W * 0.16, y, r, s, phase: 0, tp: 0.1, fi: 0, life: 1 });
- particles.push({ t: 'sadface', x: W * 0.84, y, r, s, phase: 1.6, tp: 0.6, fi: 0, life: 1 });
+ // Two weeping cartoon faces on the side wings (completely clear of badge)
+ particles.push({ t: 'sadface', x: W * 0.14, y, r, s: sadness, phase: 0, tp: 0.1, fi: 0, life: 1 });
+ particles.push({ t: 'sadface', x: W * 0.86, y, r, s: sadness, phase: 1.6, tp: 0.6, fi: 0, life: 1 });
- // Ongoing waves of crying emojis and raindrops
+ // Ongoing waves of crying emojis down the side corridors
for (let k = 0; k < 8; k++) {
- fwTimers.push(setTimeout(() => spawnEmojiRain(3 + Math.round(s * 3), true), k * 650 + 400));
+ fwTimers.push(setTimeout(() => spawnFlankEmojiRain(3 + Math.round(sadness * 3), true), k * 650 + 400));
}
}
- // ── 12. Orchestrate Play ────────────────────────────────────────────────
+ // ── 12. Orchestrate Play (1-Day & 2-Day Fragile Streak Logic) ─────────────
const stage = document.getElementById('stage');
const reduce = window.matchMedia('(prefers-reduced-motion: reduce)');
let moodTimer = null;
@@ -761,8 +808,9 @@
const params = new URLSearchParams(window.location.search);
if (params.get('mood') === 'sad') return true;
if (params.get('mood') === 'happy') return false;
- // Streak 0 is Day 1 awaits / broken streak — triggers crying mood!
- return streakVal <= 0;
+ // Streak <= 2 (0 days, 1 day, 2 days) is the fragile streak -> crying/weeping mood!
+ // Streak >= 3 (3+ days) -> triumphant celebration!
+ return streakVal <= 2;
}
function play() {
@@ -771,9 +819,8 @@
const streak = parseStreak();
const isSad = parseIsSad(streak);
- const intensity = isSad ? 0 : Math.min(1, Math.log10(Math.max(streak, 1) + 1) / 2.5);
+ const intensity = isSad ? 0 : Math.min(1, Math.log10(Math.max(streak, 3) / 2) / 2.5);
- // Mood class updates
stage.classList.remove('play', 'happy', 'sad', 'tier1', 'tier2', 'tier3');
document.body.classList.remove('sad');
@@ -787,17 +834,16 @@
stage.style.setProperty('--halo-w', (160 + intensity * 70).toFixed(0) + '%');
}
- // Force CSS reflow & trigger coin flip animation
void stage.offsetWidth;
stage.classList.add('play');
- const sadness = isSad ? 1 : 0;
+ const sadness = isSad ? Math.max(0.4, (3 - streak) / 3) : 0;
if (reduce.matches) {
if (isSad) setTimeout(() => sadFaces(sadness), 0);
return;
}
- // Trigger after coin spin settles (~2400ms)
+ // Settle after 3D coin spin (~2400ms)
moodTimer = setTimeout(() => {
if (isSad) {
sadFaces(sadness);
@@ -807,7 +853,7 @@
}, 2450);
}
- // Click badge/stage to replay animation anytime
+ // Click badge/stage to replay celebration anytime
stage.addEventListener('click', () => {
play();
});
diff --git a/src/components/Dashboard/StreakOnboardingModal.tsx b/src/components/Dashboard/StreakOnboardingModal.tsx
index 3767522..6277dce 100644
--- a/src/components/Dashboard/StreakOnboardingModal.tsx
+++ b/src/components/Dashboard/StreakOnboardingModal.tsx
@@ -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
(null);
- const lastThemeRef = useRef(null);
+ const [renderCount, setRenderCount] = useState(0);
+ // Keep track of the active user id to trigger modal whenever it changes
+ const prevUserIdRef = useRef(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 (
{/* Centered Modal Card */}
@@ -108,9 +154,7 @@ export function StreakOnboardingModal() {
- {/* 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) */}