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