feat(midsummer): glad midsommar banner + one-shot confetti
This commit is contained in:
parent
e7b0f11bb1
commit
c4dd1b7ae7
5 changed files with 81 additions and 0 deletions
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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}
|
||||||
|
|
|
||||||
27
frontend/src/components/midsummer/MidsummerBanner.tsx
Normal file
27
frontend/src/components/midsummer/MidsummerBanner.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
};
|
||||||
17
frontend/src/components/midsummer/confetti.ts
Normal file
17
frontend/src/components/midsummer/confetti.ts
Normal 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);
|
||||||
|
}
|
||||||
|
|
@ -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; }
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue