feat(midsummer): glad midsommar banner + one-shot confetti

This commit is contained in:
Erik 2026-06-19 09:28:34 +02:00
parent e7b0f11bb1
commit c4dd1b7ae7
5 changed files with 81 additions and 0 deletions

View file

@ -1,6 +1,7 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { useLiveData } from '../hooks/useLiveData'; import { useLiveData } from '../hooks/useLiveData';
import { PlayerDashboardContent } from './windows/PlayerDashboardWindow'; import { PlayerDashboardContent } from './windows/PlayerDashboardWindow';
import { MidsummerBanner } from './midsummer/MidsummerBanner';
/** /**
* Fullscreen "Player Dashboard" page rendered when the React app loads * Fullscreen "Player Dashboard" page rendered when the React app loads
@ -37,6 +38,7 @@ export const PlayerDashboardFullPage: React.FC = () => {
return ( return (
<div className="ml-dashboard-page"> <div className="ml-dashboard-page">
<MidsummerBanner />
<header className="ml-dashboard-header"> <header className="ml-dashboard-header">
<span className="ml-dashboard-title">👥 Player Dashboard</span> <span className="ml-dashboard-title">👥 Player Dashboard</span>
<span className="ml-dashboard-count">{count} online</span> <span className="ml-dashboard-count">{count} online</span>

View file

@ -7,6 +7,7 @@ import { WindowRenderer } from '../windows/WindowRenderer';
import { RareNotification } from '../effects/RareNotification'; import { RareNotification } from '../effects/RareNotification';
import { DeathNotification } from '../effects/DeathNotification'; import { DeathNotification } from '../effects/DeathNotification';
import { usePlayerColors } from '../../hooks/usePlayerColors'; import { usePlayerColors } from '../../hooks/usePlayerColors';
import { MidsummerBanner } from '../midsummer/MidsummerBanner';
import type { DashboardState } from '../../hooks/useLiveData'; import type { DashboardState } from '../../hooks/useLiveData';
interface Props { interface Props {
@ -41,6 +42,7 @@ export const MapLayout: React.FC<Props> = ({ data }) => {
return ( return (
<WindowManagerProvider> <WindowManagerProvider>
<div className="ml-layout"> <div className="ml-layout">
<MidsummerBanner />
<Sidebar <Sidebar
players={players} players={players}
vitals={vitalsMap} vitals={vitalsMap}

View file

@ -0,0 +1,27 @@
import React, { useEffect } from 'react';
import { useMidsummer } from '../../hooks/useMidsummer';
import { burstConfetti } from './confetti';
const CONFETTI_FLAG = 'mo-midsummer-confetti';
/** Festive top strip + a one-shot confetti burst on the first load of a
* session while the theme is on. */
export const MidsummerBanner: React.FC = () => {
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 (
<div className="ms-banner" role="status">
🐸 Glad midsommar! 🌼 Små grodorna, små grodorna 🥂
</div>
);
};

View file

@ -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);
}

View file

@ -106,3 +106,36 @@
line-height: 1; line-height: 1;
pointer-events: none; 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; }
}