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 { 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 (
<div className="ml-dashboard-page">
<MidsummerBanner />
<header className="ml-dashboard-header">
<span className="ml-dashboard-title">👥 Player Dashboard</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 { 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<Props> = ({ data }) => {
return (
<WindowManagerProvider>
<div className="ml-layout">
<MidsummerBanner />
<Sidebar
players={players}
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);
}