From 392812d4893a8ba2a7cf403a675aaac720446cd7 Mon Sep 17 00:00:00 2001 From: proddy Date: Fri, 19 Jun 2026 11:52:33 +0200 Subject: [PATCH] own version of toast --- interface/package.json | 1 - interface/pnpm-lock.yaml | 15 --- interface/src/App.tsx | 24 +---- interface/src/AppRouting.tsx | 2 +- interface/src/SignIn.tsx | 2 +- interface/src/app/main/Commands.tsx | 2 +- interface/src/app/main/CommandsDialog.tsx | 2 +- interface/src/app/main/CustomEntities.tsx | 2 +- interface/src/app/main/Customizations.tsx | 2 +- interface/src/app/main/Dashboard.tsx | 2 +- interface/src/app/main/Devices.tsx | 2 +- interface/src/app/main/DevicesDialog.tsx | 2 +- interface/src/app/main/Help.tsx | 2 +- interface/src/app/main/Modules.tsx | 2 +- interface/src/app/main/Scheduler.tsx | 2 +- interface/src/app/main/Sensors.tsx | 2 +- .../src/app/settings/ApplicationSettings.tsx | 2 +- interface/src/app/settings/DownloadUpload.tsx | 2 +- interface/src/app/settings/MqttSettings.tsx | 2 +- interface/src/app/settings/NTPSettings.tsx | 2 +- interface/src/app/settings/Version.tsx | 2 +- .../app/settings/network/NetworkSettings.tsx | 2 +- interface/src/app/status/SystemLog.tsx | 2 +- interface/src/components/toast/Toaster.tsx | 101 ++++++++++++++++++ interface/src/components/toast/index.ts | 3 + interface/src/components/toast/toastStore.ts | 47 ++++++++ interface/src/components/upload/DragNdrop.tsx | 2 +- .../src/components/upload/SingleUpload.tsx | 2 +- .../authentication/Authentication.tsx | 2 +- interface/src/utils/useRest.ts | 2 +- 30 files changed, 177 insertions(+), 62 deletions(-) create mode 100644 interface/src/components/toast/Toaster.tsx create mode 100644 interface/src/components/toast/index.ts create mode 100644 interface/src/components/toast/toastStore.ts diff --git a/interface/package.json b/interface/package.json index 111528624..03edc07af 100644 --- a/interface/package.json +++ b/interface/package.json @@ -39,7 +39,6 @@ "react-dom": "^19.2.7", "react-icons": "^5.6.0", "react-router": "^8.0.1", - "react-toastify": "^11.1.0", "typesafe-i18n": "^5.27.1", "typescript": "^6.0.3" }, diff --git a/interface/pnpm-lock.yaml b/interface/pnpm-lock.yaml index 77e976c5a..8c2375f2d 100644 --- a/interface/pnpm-lock.yaml +++ b/interface/pnpm-lock.yaml @@ -56,9 +56,6 @@ importers: react-router: specifier: ^8.0.1 version: 8.0.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - react-toastify: - specifier: ^11.1.0 - version: 11.1.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7) typesafe-i18n: specifier: ^5.27.1 version: 5.27.1(typescript@6.0.3) @@ -2447,12 +2444,6 @@ packages: react-dom: optional: true - react-toastify@11.1.0: - resolution: {integrity: sha512-e9h23x3phN0wbFeB6yovmWp7lobzV4CaCH0LO8nVP6H7Y+3GbcLpIzMm9dJhcp1RXbpyfvjgpfXqO80QAmn7sg==} - peerDependencies: - react: ^18 || ^19 - react-dom: ^18 || ^19 - react-transition-group@4.4.5: resolution: {integrity: sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==} peerDependencies: @@ -5381,12 +5372,6 @@ snapshots: optionalDependencies: react-dom: 19.2.7(react@19.2.7) - react-toastify@11.1.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7): - dependencies: - clsx: 2.1.1 - react: 19.2.7 - react-dom: 19.2.7(react@19.2.7) - react-transition-group@4.4.5(react-dom@19.2.7(react@19.2.7))(react@19.2.7): dependencies: '@babel/runtime': 7.29.7 diff --git a/interface/src/App.tsx b/interface/src/App.tsx index b2fc2749e..cd78d9539 100644 --- a/interface/src/App.tsx +++ b/interface/src/App.tsx @@ -1,8 +1,8 @@ import { memo, useEffect, useState } from 'react'; -import { ToastContainer, Zoom } from 'react-toastify'; import AppRouting from 'AppRouting'; import CustomTheme from 'CustomTheme'; +import { Toaster } from 'components/toast'; import TypesafeI18n from 'i18n/i18n-react'; import type { Locales } from 'i18n/i18n-types'; import { loadLocaleAsync } from 'i18n/i18n-util.async'; @@ -22,26 +22,6 @@ const AVAILABLE_LOCALES = [ 'cz' ] as Locales[]; -// Static toast configuration - no need to recreate on every render -const TOAST_CONTAINER_PROPS = { - position: 'bottom-left' as const, - autoClose: 3000, - hideProgressBar: false, - newestOnTop: false, - closeOnClick: true, - rtl: false, - pauseOnFocusLoss: true, - draggable: false, - pauseOnHover: false, - transition: Zoom, - closeButton: false, - theme: 'dark' as const, - toastStyle: { - border: '1px solid #177ac9', - width: 'fit-content' - } -}; - const App = memo(() => { const [wasLoaded, setWasLoaded] = useState(false); const [locale, setLocale] = useState('en'); @@ -64,7 +44,7 @@ const App = memo(() => { - + ); diff --git a/interface/src/AppRouting.tsx b/interface/src/AppRouting.tsx index aee79140d..db7292a27 100644 --- a/interface/src/AppRouting.tsx +++ b/interface/src/AppRouting.tsx @@ -1,10 +1,10 @@ import { type FC, memo, useContext, useEffect, useRef } from 'react'; import { Navigate, Route, Routes } from 'react-router'; -import { toast } from 'react-toastify'; import AuthenticatedRouting from 'AuthenticatedRouting'; import SignIn from 'SignIn'; import { RequireAuthenticated, RequireUnauthenticated } from 'components'; +import { toast } from 'components/toast'; import { Authentication, AuthenticationContext } from 'contexts/authentication'; import { useI18nContext } from 'i18n/i18n-react'; diff --git a/interface/src/SignIn.tsx b/interface/src/SignIn.tsx index 0b59d9d70..8f7897769 100644 --- a/interface/src/SignIn.tsx +++ b/interface/src/SignIn.tsx @@ -1,5 +1,4 @@ import { memo, useCallback, useContext, useEffect, useMemo, useState } from 'react'; -import { toast } from 'react-toastify'; import ForwardIcon from '@mui/icons-material/Forward'; import { Box, Button, Paper, Typography } from '@mui/material'; @@ -13,6 +12,7 @@ import { ValidatedPasswordField, ValidatedTextField } from 'components'; +import { toast } from 'components/toast'; import { AuthenticationContext } from 'contexts/authentication'; import { PROJECT_NAME } from 'env'; import { useI18nContext } from 'i18n/i18n-react'; diff --git a/interface/src/app/main/Commands.tsx b/interface/src/app/main/Commands.tsx index ba0fe821b..43cf2e380 100644 --- a/interface/src/app/main/Commands.tsx +++ b/interface/src/app/main/Commands.tsx @@ -1,6 +1,5 @@ import { useState } from 'react'; import { useBlocker } from 'react-router'; -import { toast } from 'react-toastify'; import AddIcon from '@mui/icons-material/Add'; import CancelIcon from '@mui/icons-material/Cancel'; @@ -27,6 +26,7 @@ import { SectionContent, useLayoutTitle } from 'components'; +import { toast } from 'components/toast'; import { useI18nContext } from 'i18n/i18n-react'; import { useInterval } from 'utils'; diff --git a/interface/src/app/main/CommandsDialog.tsx b/interface/src/app/main/CommandsDialog.tsx index 6462d3696..b02e183f2 100644 --- a/interface/src/app/main/CommandsDialog.tsx +++ b/interface/src/app/main/CommandsDialog.tsx @@ -1,5 +1,4 @@ import { useEffect, useState } from 'react'; -import { toast } from 'react-toastify'; import AddIcon from '@mui/icons-material/Add'; import CancelIcon from '@mui/icons-material/Cancel'; @@ -22,6 +21,7 @@ import { useRequest } from 'alova/client'; import type Schema from 'async-validator'; import type { ValidateFieldsError } from 'async-validator'; import { ValidatedTextField } from 'components'; +import { toast } from 'components/toast'; import { useI18nContext } from 'i18n/i18n-react'; import { updateValue } from 'utils'; import { ValidationError, validate } from 'validators'; diff --git a/interface/src/app/main/CustomEntities.tsx b/interface/src/app/main/CustomEntities.tsx index 4e394ba7b..20adc06d1 100644 --- a/interface/src/app/main/CustomEntities.tsx +++ b/interface/src/app/main/CustomEntities.tsx @@ -1,6 +1,5 @@ import { useState } from 'react'; import { useBlocker } from 'react-router'; -import { toast } from 'react-toastify'; import AddIcon from '@mui/icons-material/Add'; import CancelIcon from '@mui/icons-material/Cancel'; @@ -26,6 +25,7 @@ import { SectionContent, useLayoutTitle } from 'components'; +import { toast } from 'components/toast'; import { useI18nContext } from 'i18n/i18n-react'; import { useInterval } from 'utils'; diff --git a/interface/src/app/main/Customizations.tsx b/interface/src/app/main/Customizations.tsx index a127f9a04..99d042732 100644 --- a/interface/src/app/main/Customizations.tsx +++ b/interface/src/app/main/Customizations.tsx @@ -1,6 +1,5 @@ import { useEffect, useState } from 'react'; import { useBlocker, useLocation } from 'react-router'; -import { toast } from 'react-toastify'; import CancelIcon from '@mui/icons-material/Cancel'; import EditIcon from '@mui/icons-material/Edit'; @@ -46,6 +45,7 @@ import { SectionContent, useLayoutTitle } from 'components'; +import { toast } from 'components/toast'; import { useI18nContext } from 'i18n/i18n-react'; import { diff --git a/interface/src/app/main/Dashboard.tsx b/interface/src/app/main/Dashboard.tsx index 3a1c0055e..2bbcebcea 100644 --- a/interface/src/app/main/Dashboard.tsx +++ b/interface/src/app/main/Dashboard.tsx @@ -1,7 +1,6 @@ import { memo, useContext, useEffect, useState } from 'react'; import { IconContext } from 'react-icons/lib'; import { Link } from 'react-router'; -import { toast } from 'react-toastify'; import ChevronRightIcon from '@mui/icons-material/ChevronRight'; import EditIcon from '@mui/icons-material/Edit'; @@ -30,6 +29,7 @@ import { SectionContent, useLayoutTitle } from 'components'; +import { toast } from 'components/toast'; import { AuthenticatedContext } from 'contexts/authentication'; import { useI18nContext } from 'i18n/i18n-react'; import { useInterval, usePersistState } from 'utils'; diff --git a/interface/src/app/main/Devices.tsx b/interface/src/app/main/Devices.tsx index 869006cdd..3b74cf11c 100644 --- a/interface/src/app/main/Devices.tsx +++ b/interface/src/app/main/Devices.tsx @@ -8,7 +8,6 @@ import { } from 'react'; import { IconContext } from 'react-icons'; import { Link, useNavigate } from 'react-router'; -import { toast } from 'react-toastify'; import CommentsDisabledOutlinedIcon from '@mui/icons-material/CommentsDisabledOutlined'; import ConstructionIcon from '@mui/icons-material/Construction'; @@ -64,6 +63,7 @@ import { SectionContent, useLayoutTitle } from 'components'; +import { toast } from 'components/toast'; import { AuthenticatedContext } from 'contexts/authentication'; import { useI18nContext } from 'i18n/i18n-react'; import { useInterval } from 'utils'; diff --git a/interface/src/app/main/DevicesDialog.tsx b/interface/src/app/main/DevicesDialog.tsx index 3b98822d8..1da86a81a 100644 --- a/interface/src/app/main/DevicesDialog.tsx +++ b/interface/src/app/main/DevicesDialog.tsx @@ -1,5 +1,4 @@ import { useEffect, useState } from 'react'; -import { toast } from 'react-toastify'; import CancelIcon from '@mui/icons-material/Cancel'; import PlayArrowIcon from '@mui/icons-material/PlayArrow'; @@ -26,6 +25,7 @@ import { useRequest } from 'alova/client'; import type Schema from 'async-validator'; import type { ValidateFieldsError } from 'async-validator'; import { ValidatedTextField } from 'components'; +import { toast } from 'components/toast'; import { useI18nContext } from 'i18n/i18n-react'; import { numberValue, updateValue } from 'utils'; import { ValidationError, validate } from 'validators'; diff --git a/interface/src/app/main/Help.tsx b/interface/src/app/main/Help.tsx index ff1fe6a56..59d969ad2 100644 --- a/interface/src/app/main/Help.tsx +++ b/interface/src/app/main/Help.tsx @@ -1,6 +1,5 @@ import { memo, useContext, useState } from 'react'; import type { ReactElement } from 'react'; -import { toast } from 'react-toastify'; import CommentIcon from '@mui/icons-material/CommentTwoTone'; import DownloadIcon from '@mui/icons-material/GetApp'; @@ -25,6 +24,7 @@ import type { SxProps, Theme } from '@mui/material/styles'; import { useRequest } from 'alova/client'; import { SectionContent, useLayoutTitle } from 'components'; +import { toast } from 'components/toast'; import { AuthenticatedContext } from 'contexts/authentication'; import { useI18nContext } from 'i18n/i18n-react'; import { saveFile } from 'utils'; diff --git a/interface/src/app/main/Modules.tsx b/interface/src/app/main/Modules.tsx index 82ea16079..7028577d5 100644 --- a/interface/src/app/main/Modules.tsx +++ b/interface/src/app/main/Modules.tsx @@ -1,6 +1,5 @@ import { memo, useState } from 'react'; import { useBlocker } from 'react-router'; -import { toast } from 'react-toastify'; import CancelIcon from '@mui/icons-material/Cancel'; import CircleIcon from '@mui/icons-material/Circle'; @@ -25,6 +24,7 @@ import { SectionContent, useLayoutTitle } from 'components'; +import { toast } from 'components/toast'; import { useI18nContext } from 'i18n/i18n-react'; import { readModules, writeModules } from '../../api/app'; diff --git a/interface/src/app/main/Scheduler.tsx b/interface/src/app/main/Scheduler.tsx index b372cbc9a..9bc53b8a8 100644 --- a/interface/src/app/main/Scheduler.tsx +++ b/interface/src/app/main/Scheduler.tsx @@ -1,6 +1,5 @@ import { useEffect, useState } from 'react'; import { useBlocker } from 'react-router'; -import { toast } from 'react-toastify'; import AddIcon from '@mui/icons-material/Add'; import CancelIcon from '@mui/icons-material/Cancel'; @@ -26,6 +25,7 @@ import { SectionContent, useLayoutTitle } from 'components'; +import { toast } from 'components/toast'; import { useI18nContext } from 'i18n/i18n-react'; import { useInterval } from 'utils'; diff --git a/interface/src/app/main/Sensors.tsx b/interface/src/app/main/Sensors.tsx index b19a5c146..d77eb7521 100644 --- a/interface/src/app/main/Sensors.tsx +++ b/interface/src/app/main/Sensors.tsx @@ -1,5 +1,4 @@ import { useContext, useRef, useState } from 'react'; -import { toast } from 'react-toastify'; import AddCircleOutlineOutlinedIcon from '@mui/icons-material/AddCircleOutlineOutlined'; import KeyboardArrowDownOutlinedIcon from '@mui/icons-material/KeyboardArrowDownOutlined'; @@ -21,6 +20,7 @@ import { useTheme } from '@table-library/react-table-library/theme'; import type { State } from '@table-library/react-table-library/types/common'; import { useRequest } from 'alova/client'; import { SectionContent, useLayoutTitle } from 'components'; +import { toast } from 'components/toast'; import { AuthenticatedContext } from 'contexts/authentication'; import { useI18nContext } from 'i18n/i18n-react'; import { useInterval } from 'utils'; diff --git a/interface/src/app/settings/ApplicationSettings.tsx b/interface/src/app/settings/ApplicationSettings.tsx index 17e55e7e3..d286770ba 100644 --- a/interface/src/app/settings/ApplicationSettings.tsx +++ b/interface/src/app/settings/ApplicationSettings.tsx @@ -1,5 +1,4 @@ import { useState } from 'react'; -import { toast } from 'react-toastify'; import CancelIcon from '@mui/icons-material/Cancel'; import PowerSettingsNewIcon from '@mui/icons-material/PowerSettingsNew'; @@ -32,6 +31,7 @@ import { ValidatedTextField, useLayoutTitle } from 'components'; +import { toast } from 'components/toast'; import { useI18nContext } from 'i18n/i18n-react'; import { numberValue, updateValueDirty, useRest } from 'utils'; import { ValidationError, validate } from 'validators'; diff --git a/interface/src/app/settings/DownloadUpload.tsx b/interface/src/app/settings/DownloadUpload.tsx index b47b7f242..24bbab1e8 100644 --- a/interface/src/app/settings/DownloadUpload.tsx +++ b/interface/src/app/settings/DownloadUpload.tsx @@ -1,5 +1,4 @@ import { useState } from 'react'; -import { toast } from 'react-toastify'; import CancelIcon from '@mui/icons-material/Cancel'; import DownloadIcon from '@mui/icons-material/GetApp'; @@ -27,6 +26,7 @@ import { SingleUpload, useLayoutTitle } from 'components'; +import { toast } from 'components/toast'; import { useI18nContext } from 'i18n/i18n-react'; import { saveFile } from 'utils'; diff --git a/interface/src/app/settings/MqttSettings.tsx b/interface/src/app/settings/MqttSettings.tsx index a1e8efe18..4b26c6459 100644 --- a/interface/src/app/settings/MqttSettings.tsx +++ b/interface/src/app/settings/MqttSettings.tsx @@ -1,5 +1,4 @@ import { useState } from 'react'; -import { toast } from 'react-toastify'; import CancelIcon from '@mui/icons-material/Cancel'; import SettingsBackupRestoreIcon from '@mui/icons-material/SettingsBackupRestore'; @@ -28,6 +27,7 @@ import { ValidatedTextField, useLayoutTitle } from 'components'; +import { toast } from 'components/toast'; import { useI18nContext } from 'i18n/i18n-react'; import type { MqttSettingsType } from 'types'; import { numberValue, updateValueDirty, useRest } from 'utils'; diff --git a/interface/src/app/settings/NTPSettings.tsx b/interface/src/app/settings/NTPSettings.tsx index f85221a0a..f7d4cf6cc 100644 --- a/interface/src/app/settings/NTPSettings.tsx +++ b/interface/src/app/settings/NTPSettings.tsx @@ -1,5 +1,4 @@ import { useState } from 'react'; -import { toast } from 'react-toastify'; import AccessTimeIcon from '@mui/icons-material/AccessTime'; import CancelIcon from '@mui/icons-material/Cancel'; @@ -33,6 +32,7 @@ import { ValidatedTextField, useLayoutTitle } from 'components'; +import { toast } from 'components/toast'; import { useI18nContext } from 'i18n/i18n-react'; import type { NTPSettingsType, Time } from 'types'; import { formatLocalDateTime, updateValueDirty, useRest } from 'utils'; diff --git a/interface/src/app/settings/Version.tsx b/interface/src/app/settings/Version.tsx index 7fb066a59..812daf959 100644 --- a/interface/src/app/settings/Version.tsx +++ b/interface/src/app/settings/Version.tsx @@ -1,6 +1,5 @@ import { memo, useContext, useMemo, useState } from 'react'; import { Link } from 'react-router'; -import { toast } from 'react-toastify'; import CancelIcon from '@mui/icons-material/Cancel'; import CloseIcon from '@mui/icons-material/Close'; @@ -39,6 +38,7 @@ import { SingleUpload, useLayoutTitle } from 'components'; +import { toast } from 'components/toast'; import { AuthenticatedContext } from 'contexts/authentication'; import { useI18nContext } from 'i18n/i18n-react'; import type { TranslationFunctions } from 'i18n/i18n-types'; diff --git a/interface/src/app/settings/network/NetworkSettings.tsx b/interface/src/app/settings/network/NetworkSettings.tsx index 8a8b9c6cd..6d4f44c07 100644 --- a/interface/src/app/settings/network/NetworkSettings.tsx +++ b/interface/src/app/settings/network/NetworkSettings.tsx @@ -1,5 +1,4 @@ import { memo, useCallback, useContext, useEffect, useState } from 'react'; -import { toast } from 'react-toastify'; import CancelIcon from '@mui/icons-material/Cancel'; import DeleteIcon from '@mui/icons-material/Delete'; @@ -37,6 +36,7 @@ import { ValidatedPasswordField, ValidatedTextField } from 'components'; +import { toast } from 'components/toast'; import { useI18nContext } from 'i18n/i18n-react'; import type { NetworkSettingsType } from 'types'; import { updateValueDirty, useRest } from 'utils'; diff --git a/interface/src/app/status/SystemLog.tsx b/interface/src/app/status/SystemLog.tsx index 76d384135..c88adaf61 100644 --- a/interface/src/app/status/SystemLog.tsx +++ b/interface/src/app/status/SystemLog.tsx @@ -1,5 +1,4 @@ import { memo, useEffect, useLayoutEffect, useRef, useState } from 'react'; -import { toast } from 'react-toastify'; import DownloadIcon from '@mui/icons-material/GetApp'; import PlayArrowIcon from '@mui/icons-material/PlayArrow'; @@ -26,6 +25,7 @@ import { SectionContent, useLayoutTitle } from 'components'; +import { toast } from 'components/toast'; import { useI18nContext } from 'i18n/i18n-react'; import type { LogEntry, LogSettings } from 'types'; import { LogLevel } from 'types'; diff --git a/interface/src/components/toast/Toaster.tsx b/interface/src/components/toast/Toaster.tsx new file mode 100644 index 000000000..bd5c7623d --- /dev/null +++ b/interface/src/components/toast/Toaster.tsx @@ -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 ( + removeToast(item.id)}> + 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} + + + + ); +}); + +const Toaster = memo(() => { + const toasts = useSyncExternalStore(subscribe, getSnapshot); + + return ( + theme.zIndex.snackbar, + pointerEvents: 'none', + '& > *': { pointerEvents: 'auto' } + }} + > + {toasts.map((item) => ( + + ))} + + ); +}); + +export default Toaster; diff --git a/interface/src/components/toast/index.ts b/interface/src/components/toast/index.ts new file mode 100644 index 000000000..63733fa50 --- /dev/null +++ b/interface/src/components/toast/index.ts @@ -0,0 +1,3 @@ +export { default as Toaster } from './Toaster'; +export { toast } from './toastStore'; +export type { ToastSeverity } from './toastStore'; diff --git a/interface/src/components/toast/toastStore.ts b/interface/src/components/toast/toastStore.ts new file mode 100644 index 000000000..0c6ef5fcd --- /dev/null +++ b/interface/src/components/toast/toastStore.ts @@ -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) +}; diff --git a/interface/src/components/upload/DragNdrop.tsx b/interface/src/components/upload/DragNdrop.tsx index 9febbfa8e..b5495d63d 100644 --- a/interface/src/components/upload/DragNdrop.tsx +++ b/interface/src/components/upload/DragNdrop.tsx @@ -7,7 +7,6 @@ import { useState } from 'react'; import { Link } from 'react-router'; -import { toast } from 'react-toastify'; import CancelIcon from '@mui/icons-material/Cancel'; import CloudUploadIcon from '@mui/icons-material/CloudUpload'; @@ -28,6 +27,7 @@ import { callAction } from 'api/app'; import { dialogStyle } from '@/CustomTheme'; import { useRequest } from 'alova/client'; +import { toast } from 'components/toast'; import { useI18nContext } from 'i18n/i18n-react'; const DocumentUploader = styled(Box)<{ active?: boolean }>(({ theme, active }) => ({ diff --git a/interface/src/components/upload/SingleUpload.tsx b/interface/src/components/upload/SingleUpload.tsx index 0363cd445..3f698b29e 100644 --- a/interface/src/components/upload/SingleUpload.tsx +++ b/interface/src/components/upload/SingleUpload.tsx @@ -1,5 +1,4 @@ import { useEffect, useState } from 'react'; -import { toast } from 'react-toastify'; import CancelIcon from '@mui/icons-material/Cancel'; import { Box, Button, Typography } from '@mui/material'; @@ -7,6 +6,7 @@ import { Box, Button, Typography } from '@mui/material'; import * as SystemApi from 'api/system'; import { useRequest } from 'alova/client'; +import { toast } from 'components/toast'; import { useI18nContext } from 'i18n/i18n-react'; import DragNdrop from './DragNdrop'; diff --git a/interface/src/contexts/authentication/Authentication.tsx b/interface/src/contexts/authentication/Authentication.tsx index af0a0055c..351caf50a 100644 --- a/interface/src/contexts/authentication/Authentication.tsx +++ b/interface/src/contexts/authentication/Authentication.tsx @@ -1,7 +1,6 @@ import { useCallback, useEffect, useMemo, useState } from 'react'; import type { FC } from 'react'; import { redirect } from 'react-router'; -import { toast } from 'react-toastify'; import { callAction } from 'api/app'; import { ACCESS_TOKEN } from 'api/endpoints'; @@ -10,6 +9,7 @@ import * as AuthenticationApi from 'components/routing/authentication'; import { useRequest } from 'alova/client'; import { LoadingSpinner } from 'components'; import { verifyAuthorization } from 'components/routing/authentication'; +import { toast } from 'components/toast'; import { useI18nContext } from 'i18n/i18n-react'; import type { Me, VersionsResponse } from 'types'; import type { RequiredChildrenProps } from 'utils'; diff --git a/interface/src/utils/useRest.ts b/interface/src/utils/useRest.ts index 791c6e4e0..4573f6592 100644 --- a/interface/src/utils/useRest.ts +++ b/interface/src/utils/useRest.ts @@ -1,9 +1,9 @@ import { useCallback, useState } from 'react'; import { useBlocker } from 'react-router'; -import { toast } from 'react-toastify'; import type { AlovaGenerics, Method } from 'alova'; import { useRequest } from 'alova/client'; +import { toast } from 'components/toast'; import { useI18nContext } from 'i18n/i18n-react'; export interface RestRequestOptions {