diff --git a/public/streak-animation.html b/public/streak-animation.html index c105054..ecedf67 100644 --- a/public/streak-animation.html +++ b/public/streak-animation.html @@ -36,20 +36,20 @@ inset: 0; pointer-events: none; } - #fw { z-index: 5; } - #ec { z-index: 7; } + #fw { z-index: 4; } + #ec { z-index: 6; } - /* Euphoric flash overlay on big burst */ + /* Subtle flash overlay on big burst */ .flash { position: fixed; inset: 0; - z-index: 6; + z-index: 5; pointer-events: none; opacity: 0; - background: radial-gradient(circle at 50% 46%, rgba(255, 240, 190, 0.6), rgba(212, 175, 55, 0.15) 45%, transparent 70%); + background: radial-gradient(circle at 50% 46%, rgba(255, 240, 190, 0.45), rgba(212, 175, 55, 0.10) 45%, transparent 70%); } body.light-theme .flash { - background: radial-gradient(circle at 50% 46%, rgba(12, 96, 96, 0.22), rgba(12, 96, 96, 0.08) 45%, transparent 70%); + background: radial-gradient(circle at 50% 46%, rgba(12, 96, 96, 0.18), rgba(12, 96, 96, 0.05) 45%, transparent 70%); } .stage.play.tier3 .flash { animation: flash 760ms ease-out 2600ms; } @@ -74,6 +74,7 @@ display: flex; align-items: center; justify-content: center; + z-index: 20; /* High z-index keeps logo completely clear and prominent */ } .stage.play.tier3 .scene { animation: shake 520ms ease-out 2620ms; } @@ -87,7 +88,7 @@ transform: translate(-50%, -50%) scale(0.3); opacity: 0; border-radius: 50%; - filter: blur(6px); + filter: blur(8px); z-index: 0; background: radial-gradient(circle, rgba(246, 221, 147, 0.55) 0%, @@ -97,9 +98,9 @@ } body.light-theme .halo { background: radial-gradient(circle, - rgba(12, 96, 96, 0.35) 0%, - rgba(12, 96, 96, 0.18) 34%, - rgba(12, 96, 96, 0.05) 58%, + rgba(12, 96, 96, 0.32) 0%, + rgba(12, 96, 96, 0.16) 34%, + rgba(12, 96, 96, 0.04) 58%, transparent 72%); } .stage.play.happy .halo { @@ -107,14 +108,14 @@ } .stage.sad .halo { background: radial-gradient(circle, - rgba(59, 130, 246, 0.25) 0%, - rgba(100, 116, 139, 0.20) 36%, + rgba(59, 130, 246, 0.22) 0%, + rgba(100, 116, 139, 0.18) 36%, transparent 70%); } body.light-theme .stage.sad .halo { background: radial-gradient(circle, - rgba(37, 99, 235, 0.22) 0%, - rgba(148, 163, 184, 0.16) 36%, + rgba(37, 99, 235, 0.20) 0%, + rgba(148, 163, 184, 0.14) 36%, transparent 70%); } .stage.play.sad .halo { @@ -160,7 +161,7 @@ background: url('/PragatiVeer_Logo/SVG_Logos/Square_light_background.svg') center/cover no-repeat !important; } .stage.sad .badge { - filter: saturate(0.8) brightness(0.9); + filter: saturate(0.85) brightness(0.92); } /* Replay tooltip / test hint */ @@ -171,7 +172,7 @@ font-weight: 600; opacity: 0; transition: opacity 0.3s ease; - color: rgba(148, 163, 184, 0.8); + color: rgba(148, 163, 184, 0.75); pointer-events: none; letter-spacing: 0.04em; } @@ -202,23 +203,23 @@ } @keyframes droop { 0% { transform: scale(1) translateY(0); } - 100% { transform: scale(0.93) translateY(8px); } + 100% { transform: scale(0.94) translateY(6px); } } @keyframes litglow { 0% { filter: drop-shadow(0 0 0 rgba(212, 175, 55, 0)); } - 100% { filter: drop-shadow(0 0 28px rgba(233, 196, 99, 0.65)); } + 100% { filter: drop-shadow(0 0 24px rgba(233, 196, 99, 0.65)); } } @keyframes litglow-teal { 0% { filter: drop-shadow(0 0 0 rgba(12, 96, 96, 0)); } - 100% { filter: drop-shadow(0 0 26px rgba(12, 96, 96, 0.55)); } + 100% { filter: drop-shadow(0 0 24px rgba(12, 96, 96, 0.50)); } } @keyframes dimglow { 0% { filter: drop-shadow(0 0 0 rgba(100, 116, 139, 0)); } - 100% { filter: drop-shadow(0 6px 18px rgba(30, 41, 59, 0.4)); } + 100% { filter: drop-shadow(0 4px 14px rgba(30, 41, 59, 0.35)); } } @keyframes flash { 0% { opacity: 0; } - 18% { opacity: 0.85; } + 18% { opacity: 0.8; } 100% { opacity: 0; } } @keyframes shake { @@ -232,7 +233,7 @@ @media (prefers-reduced-motion: reduce) { .stage.play .spinner { animation: none; transform: rotateY(0) scale(1); } .stage.play.happy .scaler { animation: none; transform: scale(var(--grow)); } - .stage.play.sad .scaler { animation: none; transform: scale(0.93); } + .stage.play.sad .scaler { animation: none; transform: scale(0.94); } .stage.play.tier3 .scene, .stage.play.tier3 .flash { animation: none; } } @@ -243,7 +244,7 @@
-
+
@@ -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) */}