import { createTheme, ThemeProvider } from '@mui/material/styles'; import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { translate } from 'react-polyglot'; import { connect } from 'react-redux'; import { Navigate, Route, Routes, useLocation, useNavigate, useParams, useSearchParams, } from 'react-router-dom'; import { ScrollSync } from 'react-scroll-sync'; import TopBarProgress from 'react-topbar-progress-indicator'; import { loginUser as loginUserAction } from '@staticcms/core/actions/auth'; import { discardDraft } from '@staticcms/core/actions/entries'; import { currentBackend } from '@staticcms/core/backend'; import { changeTheme } from '../actions/globalUI'; import { invokeEvent } from '../lib/registry'; import { getDefaultPath } from '../lib/util/collection.util'; import { generateClassNames } from '../lib/util/theming.util'; import { selectTheme } from '../reducers/selectors/globalUI'; import { useAppDispatch, useAppSelector } from '../store/hooks'; import CollectionRoute from './collections/CollectionRoute'; import { Alert } from './common/alert/Alert'; import { Confirm } from './common/confirm/Confirm'; import Loader from './common/progress/Loader'; import EditorRoute from './entry-editor/EditorRoute'; import MediaPage from './media-library/MediaPage'; import NotFoundPage from './NotFoundPage'; import Page from './page/Page'; import Snackbars from './snackbar/Snackbars'; import type { Credentials, TranslatedProps } from '@staticcms/core/interface'; import type { RootState } from '@staticcms/core/store'; import type { ComponentType } from 'react'; import type { ConnectedProps } from 'react-redux'; import './App.css'; export const classes = generateClassNames('App', ['root', 'content']); TopBarProgress.config({ barColors: { 0: '#000', '1.0': '#000', }, shadowBlur: 0, barThickness: 2, }); window.addEventListener('beforeunload', function (event) { event.stopImmediatePropagation(); }); function CollectionSearchRedirect() { const { name } = useParams(); return ; } function EditEntityRedirect() { const { name, ...params } = useParams(); return ; } const App = ({ auth, user, config, collections, loginUser, isFetching, t, scrollSyncEnabled, }: TranslatedProps) => { const navigate = useNavigate(); const dispatch = useAppDispatch(); const mode = useAppSelector(selectTheme); const theme = React.useMemo( () => createTheme({ palette: { mode, primary: { main: 'rgb(37 99 235)', }, ...(mode === 'dark' && { background: { paper: 'rgb(15 23 42)', }, }), }, }), [mode], ); const configError = useCallback( (error?: string) => { return (

{t('app.app.errorHeader')}

{t('app.app.configErrors')}:
{error ?? config.error}
{t('app.app.checkConfigYml')}
); }, [config.error, t], ); const handleLogin = useCallback( (credentials: Credentials) => { loginUser(credentials); }, [loginUser], ); const AuthComponent = useMemo(() => { if (!config.config) { return null; } const backend = currentBackend(config.config); return backend?.authComponent(); }, [config.config]); const authenticationPage = useMemo(() => { if (!config.config) { return null; } if (AuthComponent == null) { return (

{t('app.app.waitingBackend')}

); } return ( navigate('/', { replace: true })} t={t} /> ); }, [AuthComponent, auth.error, auth.isFetching, config.config, handleLogin, navigate, t]); const defaultPath = useMemo(() => getDefaultPath(collections), [collections]); const { pathname } = useLocation(); const [searchParams] = useSearchParams(); useEffect(() => { if ( /\/collections\/[a-zA-Z0-9_-]+\/entries\/[a-zA-Z0-9_-]+/g.test(pathname) || (/\/collections\/[a-zA-Z0-9_-]+\/new/g.test(pathname) && searchParams.get('duplicate') === 'true') ) { return; } dispatch(discardDraft()); }, [dispatch, pathname, searchParams]); useEffect(() => { // On page load or when changing themes, best to add inline in `head` to avoid FOUC if ( localStorage.getItem('color-theme') === 'dark' || (!('color-theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches) ) { document.documentElement.classList.add('dark'); dispatch(changeTheme('dark')); } else { document.documentElement.classList.remove('dark'); dispatch(changeTheme('light')); } }, [dispatch]); const [prevUser, setPrevUser] = useState(user); useEffect(() => { if (!prevUser && user) { invokeEvent({ name: 'login', data: { login: user.login, name: user.name ?? '', }, }); } setPrevUser(user); }, [prevUser, user]); const content = useMemo(() => { if (!user) { return authenticationPage; } return ( <> {isFetching && } } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> ); }, [authenticationPage, collections, defaultPath, isFetching, user]); useEffect(() => { setTimeout(() => { invokeEvent({ name: 'mounted' }); }); }, []); if (!config.config) { return configError(t('app.app.configNotFound')); } if (config.error) { return configError(); } if (config.isFetching) { return {t('app.app.loadingConfig')}; } return ( <>
{content}
); }; function mapStateToProps(state: RootState) { const { auth, config, collections, globalUI, scroll } = state; const user = auth.user; const isFetching = globalUI.isFetching; const scrollSyncEnabled = scroll.isScrolling; return { auth, config, collections, user, isFetching, scrollSyncEnabled, }; } const mapDispatchToProps = { loginUser: loginUserAction, }; const connector = connect(mapStateToProps, mapDispatchToProps); export type AppProps = ConnectedProps; export default connector(translate()(App) as ComponentType);