feat: v0.17.0 — ISS-008 toasts for errors/events + browser notifications
CI / build + unit + e2e (pull_request) Failing after 6m59s
CI / build + unit + e2e (pull_request) Failing after 6m59s
This commit is contained in:
@@ -347,3 +347,70 @@ body {
|
||||
padding: 6px 0 0;
|
||||
border-top: 1px solid rgba(148, 163, 184, 0.2);
|
||||
}
|
||||
|
||||
/* v0.17.0 — ISS-008: bottom-right toast stack (action errors + game events) */
|
||||
.toasts {
|
||||
position: fixed;
|
||||
bottom: 12px;
|
||||
right: 12px;
|
||||
z-index: 50; /* above map popups/controls and the app header */
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
max-width: 340px;
|
||||
}
|
||||
|
||||
.toast {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
padding: 8px 12px;
|
||||
border-radius: 6px;
|
||||
font-size: 13px;
|
||||
background: rgba(15, 23, 42, 0.95);
|
||||
color: #e2e8f0;
|
||||
border: 1px solid rgba(148, 163, 184, 0.4);
|
||||
}
|
||||
|
||||
.toast-error {
|
||||
border-color: rgba(239, 68, 68, 0.7);
|
||||
color: #fecaca;
|
||||
}
|
||||
|
||||
.toast-info {
|
||||
border-color: rgba(148, 163, 184, 0.5);
|
||||
}
|
||||
|
||||
.toast-close {
|
||||
flex-shrink: 0;
|
||||
font-size: 14px;
|
||||
line-height: 1;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: #94a3b8;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.toast-close:hover {
|
||||
color: #e2e8f0;
|
||||
}
|
||||
|
||||
/* ISS-008 — the Time section's notifications opt-in button (same look as .time-controls buttons) */
|
||||
.notif-toggle {
|
||||
margin-top: 8px;
|
||||
font-size: 12px;
|
||||
padding: 4px 10px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid rgba(148, 163, 184, 0.4);
|
||||
background: rgba(30, 41, 59, 0.9);
|
||||
color: #e2e8f0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.notif-toggle:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
+26
-3
@@ -1,11 +1,14 @@
|
||||
'use client';
|
||||
|
||||
import dynamic from 'next/dynamic';
|
||||
import { useState } from 'react';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import LeftPane from '@/components/LeftPane';
|
||||
import RightPane from '@/components/RightPane';
|
||||
import ToastStack from '@/components/Toasts';
|
||||
import type { HoverTarget } from '@/components/hoverTypes';
|
||||
import { useGameState } from '@/lib/useGameState';
|
||||
import { useToasts } from '@/lib/useToasts';
|
||||
import { diffSnapshots } from '@/lib/deriveEvents';
|
||||
|
||||
const Map = dynamic(() => import('@/components/Map'), { ssr: false });
|
||||
|
||||
@@ -19,12 +22,32 @@ export default function Home() {
|
||||
// either pane highlights the matching plane/route/airports on the map.
|
||||
const [hovered, setHovered] = useState<HoverTarget | null>(null);
|
||||
|
||||
// ISS-008 (v0.17.0): bottom-right toasts — action errors from both panes plus game events
|
||||
// derived by diffing consecutive snapshots (no server event feed exists).
|
||||
const { toasts, push, dismiss } = useToasts();
|
||||
const onError = (message: string) => push('error', message);
|
||||
|
||||
// Diff each new snapshot against the previous one → info toasts. When the window is hidden
|
||||
// and notifications are granted, mirror them as browser notifications too.
|
||||
const prevSnapshotRef = useRef<typeof state>(null);
|
||||
useEffect(() => {
|
||||
if (state === null) return;
|
||||
for (const event of diffSnapshots(prevSnapshotRef.current, state)) {
|
||||
push('info', event.message);
|
||||
if (document.hidden && 'Notification' in window && Notification.permission === 'granted') {
|
||||
new Notification('Flight Ops', { body: event.message });
|
||||
}
|
||||
}
|
||||
prevSnapshotRef.current = state;
|
||||
}, [state, push]);
|
||||
|
||||
return (
|
||||
<div className="app-shell">
|
||||
<LeftPane state={state} loading={loading} error={error} onRefresh={refresh} onHover={setHovered} />
|
||||
<LeftPane state={state} loading={loading} error={error} onRefresh={refresh} onHover={setHovered} onError={onError} />
|
||||
{/* v0.11.0 — the same polled snapshot drives the map's plane markers + route lines */}
|
||||
<Map state={state} hovered={hovered} />
|
||||
<RightPane state={state} loading={loading} applySnapshot={applySnapshot} onHover={setHovered} />
|
||||
<RightPane state={state} loading={loading} applySnapshot={applySnapshot} onHover={setHovered} onError={onError} />
|
||||
<ToastStack toasts={toasts} onDismiss={dismiss} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user