276 lines
8.0 KiB
TypeScript
Raw Normal View History

2022-10-20 11:57:30 -04:00
import KeyboardArrowUpIcon from '@mui/icons-material/KeyboardArrowUp';
import Fab from '@mui/material/Fab';
import { styled } from '@mui/material/styles';
import React, { useCallback, useEffect, useMemo } from 'react';
2022-10-20 11:57:30 -04:00
import { translate } from 'react-polyglot';
import { connect } from 'react-redux';
2023-02-26 15:25:27 +01:00
import { Navigate, Route, Routes, useLocation, useNavigate, useParams } from 'react-router-dom';
2022-10-20 11:57:30 -04:00
import { ScrollSync } from 'react-scroll-sync';
import TopBarProgress from 'react-topbar-progress-indicator';
import { loginUser as loginUserAction } from '@staticcms/core/actions/auth';
import { discardDraft as discardDraftAction } from '@staticcms/core/actions/entries';
import { currentBackend } from '@staticcms/core/backend';
import { colors, GlobalStyles } from '@staticcms/core/components/UI/styles';
import { getDefaultPath } from '../../lib/util/collection.util';
2022-10-20 11:57:30 -04:00
import CollectionRoute from '../Collection/CollectionRoute';
import EditorRoute from '../Editor/EditorRoute';
import MediaLibrary from '../MediaLibrary/MediaLibrary';
2022-11-04 17:41:12 -04:00
import Page from '../page/Page';
2022-10-20 11:57:30 -04:00
import Snackbars from '../snackbar/Snackbars';
import { Alert } from '../UI/Alert';
import { Confirm } from '../UI/Confirm';
import Loader from '../UI/Loader';
import ScrollTop from '../UI/ScrollTop';
import NotFoundPage from './NotFoundPage';
import type { Credentials, TranslatedProps } from '@staticcms/core/interface';
import type { RootState } from '@staticcms/core/store';
2022-10-20 11:57:30 -04:00
import type { ComponentType } from 'react';
import type { ConnectedProps } from 'react-redux';
2022-10-20 11:57:30 -04:00
TopBarProgress.config({
barColors: {
0: colors.active,
'1.0': colors.active,
},
shadowBlur: 0,
barThickness: 2,
});
const AppRoot = styled('div')`
width: 100%;
min-width: 1200px;
height: 100vh;
position: relative;
`;
const AppWrapper = styled('div')`
width: 100%;
min-width: 1200px;
min-height: 100vh;
`;
const ErrorContainer = styled('div')`
margin: 20px;
`;
const ErrorCodeBlock = styled('pre')`
margin-left: 20px;
font-size: 15px;
line-height: 1.5;
`;
function CollectionSearchRedirect() {
const { name } = useParams();
return <Navigate to={`/collections/${name}`} />;
}
function EditEntityRedirect() {
const { name, entryName } = useParams();
return <Navigate to={`/collections/${name}/entries/${entryName}`} />;
}
const App = ({
auth,
user,
config,
collections,
loginUser,
isFetching,
useMediaLibrary,
t,
scrollSyncEnabled,
discardDraft,
2022-10-20 11:57:30 -04:00
}: TranslatedProps<AppProps>) => {
2023-02-26 15:25:27 +01:00
const navigate = useNavigate();
const configError = useCallback(
(error?: string) => {
return (
<ErrorContainer>
<h1>{t('app.app.errorHeader')}</h1>
<div>
<strong>{t('app.app.configErrors')}:</strong>
<ErrorCodeBlock>{error ?? config.error}</ErrorCodeBlock>
<span>{t('app.app.checkConfigYml')}</span>
</div>
</ErrorContainer>
);
},
[config.error, t],
);
2022-10-20 11:57:30 -04:00
const handleLogin = useCallback(
(credentials: Credentials) => {
loginUser(credentials);
},
[loginUser],
);
2022-10-26 09:23:11 -04:00
const AuthComponent = useMemo(() => {
2022-10-20 11:57:30 -04:00
if (!config.config) {
return null;
}
const backend = currentBackend(config.config);
2022-10-26 09:23:11 -04:00
return backend?.authComponent();
}, [config.config]);
const authenticationPage = useMemo(() => {
if (!config.config) {
return null;
}
2022-10-20 11:57:30 -04:00
2022-10-26 09:23:11 -04:00
if (AuthComponent == null) {
2022-10-20 11:57:30 -04:00
return (
<div>
<h1>{t('app.app.waitingBackend')}</h1>
</div>
);
}
return (
2022-10-26 09:23:11 -04:00
<div key="auth-page-wrapper">
<AuthComponent
key="auth-page"
onLogin={handleLogin}
error={auth.error}
inProgress={auth.isFetching}
siteId={config.config.backend.site_domain}
base_url={config.config.backend.base_url}
authEndpoint={config.config.backend.auth_endpoint}
config={config.config}
2023-02-26 15:25:27 +01:00
clearHash={() => navigate('/', { replace: true })}
2022-10-26 09:23:11 -04:00
t={t}
/>
2022-10-20 11:57:30 -04:00
</div>
);
2022-10-26 09:23:11 -04:00
}, [AuthComponent, auth.error, auth.isFetching, config.config, handleLogin, t]);
2022-10-20 11:57:30 -04:00
const defaultPath = useMemo(() => getDefaultPath(collections), [collections]);
const { pathname } = useLocation();
useEffect(() => {
if (!/\/collections\/[a-zA-Z0-9_-]+\/entries\/[a-zA-Z0-9_-]+/g.test(pathname)) {
discardDraft();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [pathname]);
const content = useMemo(() => {
if (!user) {
return authenticationPage;
}
return (
<>
{isFetching && <TopBarProgress />}
<Routes>
<Route path="/" element={<Navigate to={defaultPath} />} />
<Route path="/search" element={<Navigate to={defaultPath} />} />
<Route path="/collections/:name/search/" element={<CollectionSearchRedirect />} />
<Route
path="/error=access_denied&error_description=Signups+not+allowed+for+this+instance"
element={<Navigate to={defaultPath} />}
/>
<Route path="/collections" element={<CollectionRoute collections={collections} />} />
<Route
path="/collections/:name"
element={<CollectionRoute collections={collections} />}
/>
<Route
path="/collections/:name/new"
element={<EditorRoute collections={collections} newRecord />}
/>
<Route
path="/collections/:name/entries/:slug"
element={<EditorRoute collections={collections} />}
/>
<Route
path="/collections/:name/search/:searchTerm"
element={
<CollectionRoute collections={collections} isSearchResults isSingleSearchResult />
}
/>
<Route
path="/collections/:name/filter/:filterTerm"
element={<CollectionRoute collections={collections} />}
/>
<Route
path="/search/:searchTerm"
element={<CollectionRoute collections={collections} isSearchResults />}
/>
<Route path="/edit/:name/:entryName" element={<EditEntityRedirect />} />
2022-11-04 17:41:12 -04:00
<Route path="/page/:id" element={<Page />} />
<Route element={<NotFoundPage />} />
</Routes>
{useMediaLibrary ? <MediaLibrary /> : null}
</>
);
}, [authenticationPage, collections, defaultPath, isFetching, useMediaLibrary, user]);
2022-10-20 11:57:30 -04:00
if (!config.config) {
return configError(t('app.app.configNotFound'));
2022-10-20 11:57:30 -04:00
}
if (config.error) {
return configError();
}
if (config.isFetching) {
return <Loader>{t('app.app.loadingConfig')}</Loader>;
}
return (
<>
<GlobalStyles key="global-styles" />
<ScrollSync key="scroll-sync" enabled={scrollSyncEnabled}>
2022-10-20 11:57:30 -04:00
<>
<div key="back-to-top-anchor" id="back-to-top-anchor" />
<AppRoot key="cms-root" id="cms-root">
<AppWrapper key="cms-wrapper" className="cms-wrapper">
<Snackbars key="snackbars" />
{content}
<Alert key="alert" />
<Confirm key="confirm" />
2022-10-20 11:57:30 -04:00
</AppWrapper>
</AppRoot>
<ScrollTop key="scroll-to-top">
2022-10-20 11:57:30 -04:00
<Fab size="small" aria-label="scroll back to top">
<KeyboardArrowUpIcon />
</Fab>
</ScrollTop>
</>
</ScrollSync>
</>
);
};
function mapStateToProps(state: RootState) {
const { auth, config, collections, globalUI, mediaLibrary, scroll } = state;
const user = auth.user;
const isFetching = globalUI.isFetching;
const useMediaLibrary = !mediaLibrary.externalLibrary;
const scrollSyncEnabled = scroll.isScrolling;
return {
auth,
config,
collections,
user,
isFetching,
useMediaLibrary,
scrollSyncEnabled,
};
}
const mapDispatchToProps = {
loginUser: loginUserAction,
discardDraft: discardDraftAction,
2022-10-20 11:57:30 -04:00
};
const connector = connect(mapStateToProps, mapDispatchToProps);
export type AppProps = ConnectedProps<typeof connector>;
export default connector(translate()(App) as ComponentType<AppProps>);