mirror of
https://github.com/emsesp/EMS-ESP32.git
synced 2026-06-21 23:36:26 +03:00
own version of toast
This commit is contained in:
101
interface/src/components/toast/Toaster.tsx
Normal file
101
interface/src/components/toast/Toaster.tsx
Normal file
@@ -0,0 +1,101 @@
|
||||
import { memo, useEffect, useRef, useState, useSyncExternalStore } from 'react';
|
||||
|
||||
import Alert from '@mui/material/Alert';
|
||||
import Grow from '@mui/material/Grow';
|
||||
import LinearProgress from '@mui/material/LinearProgress';
|
||||
import Stack from '@mui/material/Stack';
|
||||
|
||||
import { type ToastItem, getSnapshot, removeToast, subscribe } from './toastStore';
|
||||
|
||||
const AUTO_CLOSE_MS = 3000;
|
||||
const TICK_MS = 50;
|
||||
|
||||
// Single toast row: owns its auto-dismiss timer + countdown progress bar, pauses
|
||||
// while the window is unfocused (matching react-toastify's pauseOnFocusLoss).
|
||||
const ToastRow = memo(({ item }: { item: ToastItem }) => {
|
||||
const [open, setOpen] = useState(true);
|
||||
const [remaining, setRemaining] = useState(AUTO_CLOSE_MS);
|
||||
const remainingRef = useRef(AUTO_CLOSE_MS);
|
||||
|
||||
useEffect(() => {
|
||||
let paused = document.hidden;
|
||||
const onVisibility = () => {
|
||||
paused = document.hidden;
|
||||
};
|
||||
document.addEventListener('visibilitychange', onVisibility);
|
||||
|
||||
const timer = setInterval(() => {
|
||||
if (paused) return;
|
||||
remainingRef.current = Math.max(0, remainingRef.current - TICK_MS);
|
||||
setRemaining(remainingRef.current);
|
||||
if (remainingRef.current === 0) setOpen(false);
|
||||
}, TICK_MS);
|
||||
|
||||
return () => {
|
||||
clearInterval(timer);
|
||||
document.removeEventListener('visibilitychange', onVisibility);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Grow in={open} onExited={() => removeToast(item.id)}>
|
||||
<Alert
|
||||
severity={item.severity}
|
||||
variant="filled"
|
||||
onClick={() => setOpen(false)}
|
||||
sx={{
|
||||
width: 'fit-content',
|
||||
maxWidth: 360,
|
||||
minHeight: 64,
|
||||
cursor: 'pointer',
|
||||
border: '1px solid #177ac9',
|
||||
boxShadow: 6,
|
||||
overflow: 'hidden',
|
||||
alignItems: 'center',
|
||||
'& .MuiAlert-icon': { py: 0 },
|
||||
'& .MuiAlert-message': { py: 0, textAlign: 'center', fontSize: '1rem' }
|
||||
}}
|
||||
>
|
||||
{item.message}
|
||||
<LinearProgress
|
||||
variant="determinate"
|
||||
value={(remaining / AUTO_CLOSE_MS) * 100}
|
||||
color="inherit"
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
height: 3,
|
||||
opacity: 0.7,
|
||||
backgroundColor: 'transparent'
|
||||
}}
|
||||
/>
|
||||
</Alert>
|
||||
</Grow>
|
||||
);
|
||||
});
|
||||
|
||||
const Toaster = memo(() => {
|
||||
const toasts = useSyncExternalStore(subscribe, getSnapshot);
|
||||
|
||||
return (
|
||||
<Stack
|
||||
spacing={1}
|
||||
sx={{
|
||||
position: 'fixed',
|
||||
bottom: 16,
|
||||
left: 16,
|
||||
zIndex: (theme) => theme.zIndex.snackbar,
|
||||
pointerEvents: 'none',
|
||||
'& > *': { pointerEvents: 'auto' }
|
||||
}}
|
||||
>
|
||||
{toasts.map((item) => (
|
||||
<ToastRow key={item.id} item={item} />
|
||||
))}
|
||||
</Stack>
|
||||
);
|
||||
});
|
||||
|
||||
export default Toaster;
|
||||
3
interface/src/components/toast/index.ts
Normal file
3
interface/src/components/toast/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export { default as Toaster } from './Toaster';
|
||||
export { toast } from './toastStore';
|
||||
export type { ToastSeverity } from './toastStore';
|
||||
47
interface/src/components/toast/toastStore.ts
Normal file
47
interface/src/components/toast/toastStore.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
export type ToastSeverity = 'success' | 'error' | 'info' | 'warning';
|
||||
|
||||
export interface ToastItem {
|
||||
id: number;
|
||||
severity: ToastSeverity;
|
||||
message: string;
|
||||
}
|
||||
|
||||
let toasts: ToastItem[] = [];
|
||||
let nextId = 1;
|
||||
const listeners = new Set<() => void>();
|
||||
|
||||
const emit = () => {
|
||||
for (const listener of listeners) listener();
|
||||
};
|
||||
|
||||
export const subscribe = (listener: () => void): (() => void) => {
|
||||
listeners.add(listener);
|
||||
return () => {
|
||||
listeners.delete(listener);
|
||||
};
|
||||
};
|
||||
|
||||
export const getSnapshot = (): ToastItem[] => toasts;
|
||||
|
||||
const add = (severity: ToastSeverity, message: string): number => {
|
||||
const id = nextId++;
|
||||
toasts = [...toasts, { id, severity, message }];
|
||||
emit();
|
||||
return id;
|
||||
};
|
||||
|
||||
export const removeToast = (id: number): void => {
|
||||
const next = toasts.filter((t) => t.id !== id);
|
||||
if (next.length !== toasts.length) {
|
||||
toasts = next;
|
||||
emit();
|
||||
}
|
||||
};
|
||||
|
||||
// Imperative API mirroring the subset of react-toastify used across the app.
|
||||
export const toast = {
|
||||
success: (message: string) => add('success', message),
|
||||
error: (message: string) => add('error', message),
|
||||
info: (message: string) => add('info', message),
|
||||
warning: (message: string) => add('warning', message)
|
||||
};
|
||||
Reference in New Issue
Block a user