diff --git a/frontend/src/components/PlayerDashboardFullPage.tsx b/frontend/src/components/PlayerDashboardFullPage.tsx index a86f3958..c7ef7e21 100644 --- a/frontend/src/components/PlayerDashboardFullPage.tsx +++ b/frontend/src/components/PlayerDashboardFullPage.tsx @@ -1,6 +1,7 @@ import React, { useEffect, useState } from 'react'; import { useLiveData } from '../hooks/useLiveData'; import { PlayerDashboardContent } from './windows/PlayerDashboardWindow'; +import { MidsummerBanner } from './midsummer/MidsummerBanner'; /** * Fullscreen "Player Dashboard" page — rendered when the React app loads @@ -37,6 +38,7 @@ export const PlayerDashboardFullPage: React.FC = () => { return (
+
👥 Player Dashboard {count} online diff --git a/frontend/src/components/map/MapLayout.tsx b/frontend/src/components/map/MapLayout.tsx index 73b11215..800b665c 100644 --- a/frontend/src/components/map/MapLayout.tsx +++ b/frontend/src/components/map/MapLayout.tsx @@ -7,6 +7,7 @@ import { WindowRenderer } from '../windows/WindowRenderer'; import { RareNotification } from '../effects/RareNotification'; import { DeathNotification } from '../effects/DeathNotification'; import { usePlayerColors } from '../../hooks/usePlayerColors'; +import { MidsummerBanner } from '../midsummer/MidsummerBanner'; import type { DashboardState } from '../../hooks/useLiveData'; interface Props { @@ -41,6 +42,7 @@ export const MapLayout: React.FC = ({ data }) => { return (
+ { + const { enabled } = useMidsummer(); + + useEffect(() => { + if (!enabled) return; + if (sessionStorage.getItem(CONFETTI_FLAG)) return; + sessionStorage.setItem(CONFETTI_FLAG, '1'); + if (!window.matchMedia('(prefers-reduced-motion: reduce)').matches) { + burstConfetti(); + } + }, [enabled]); + + if (!enabled) return null; + return ( +
+ 🐸 Glad midsommar! 🌼 Små grodorna, små grodorna… 🥂 +
+ ); +}; diff --git a/frontend/src/components/midsummer/confetti.ts b/frontend/src/components/midsummer/confetti.ts new file mode 100644 index 00000000..04c400f8 --- /dev/null +++ b/frontend/src/components/midsummer/confetti.ts @@ -0,0 +1,17 @@ +const EMOJIS = ['🐸', '🌼', '🌸', '🥂', '🌿']; + +/** One-shot falling-emoji burst. Self-removes after the animation. */ +export function burstConfetti(count = 28): void { + const layer = document.createElement('div'); + layer.className = 'ms-confetti'; + for (let i = 0; i < count; i++) { + const p = document.createElement('span'); + p.textContent = EMOJIS[i % EMOJIS.length]; + p.style.left = Math.floor(Math.random() * 100) + 'vw'; + p.style.animationDelay = (Math.random() * 0.6).toFixed(2) + 's'; + p.style.fontSize = 12 + Math.floor(Math.random() * 14) + 'px'; + layer.appendChild(p); + } + document.body.appendChild(layer); + window.setTimeout(() => layer.remove(), 4200); +} diff --git a/frontend/src/styles/midsummer.css b/frontend/src/styles/midsummer.css index a4616e50..8dbb8a95 100644 --- a/frontend/src/styles/midsummer.css +++ b/frontend/src/styles/midsummer.css @@ -106,3 +106,36 @@ line-height: 1; pointer-events: none; } + +.ms-banner { + position: absolute; + top: 0; + left: 50%; + transform: translateX(-50%); + z-index: 50; + margin-top: 6px; + padding: 4px 16px; + border-radius: 14px; + background: rgba(20, 64, 31, 0.92); + border: 1px solid #7ed957; + color: #eafbe0; + font-size: 0.8rem; + white-space: nowrap; + pointer-events: none; +} +.ms-confetti { + position: fixed; + inset: 0; + pointer-events: none; + overflow: hidden; + z-index: 999998; +} +.ms-confetti span { + position: absolute; + top: -32px; + line-height: 1; + animation: ms-fall 3.6s linear forwards; +} +@keyframes ms-fall { + to { transform: translateY(112vh) rotate(360deg); opacity: 0.25; } +}