Skip to content

Commit

Permalink
Use banner for app update prompt (#100)
Browse files Browse the repository at this point in the history
* Added new prompt for update UI

* Fixed navigation progress position

* Use Collapse instead of Fade

* Fixed top padding for main content container

* Fixed banner border bottom styles

* Do not show update banner if closed until page refreshed

* Removed old prompt for update

* Show UpdateAppBanner on login page

* features/user/updateApp → features/updateApp

* Fixed banner styles, added supporting illustration

* Moved subheaderElevation to SubheaderProps
  • Loading branch information
pkirilin authored May 9, 2024
1 parent a7c6d78 commit 7104acb
Show file tree
Hide file tree
Showing 13 changed files with 181 additions and 66 deletions.
7 changes: 5 additions & 2 deletions src/frontend/src/app/RootProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { AdapterDateFns } from '@mui/x-date-pickers/AdapterDateFns';
import { type PropsWithChildren, type FC } from 'react';
import { Provider } from 'react-redux';
import { type Store } from 'redux';
import { updateAppModel } from '@/features/updateApp';
import { theme } from './theme';

interface Props {
Expand All @@ -15,8 +16,10 @@ export const RootProvider: FC<PropsWithChildren<Props>> = ({ children, store })
<ThemeProvider theme={theme}>
<LocalizationProvider dateAdapter={AdapterDateFns}>
<Provider store={store}>
<CssBaseline />
{children}
<updateAppModel.Provider>
<CssBaseline />
{children}
</updateAppModel.Provider>
</Provider>
</LocalizationProvider>
</ThemeProvider>
Expand Down
2 changes: 2 additions & 0 deletions src/frontend/src/features/updateApp/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './ui';
export * as updateAppModel from './model';
53 changes: 53 additions & 0 deletions src/frontend/src/features/updateApp/model.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { useRegisterSW } from 'virtual:pwa-register/react';
import {
type FC,
type PropsWithChildren,
useCallback,
createContext,
useContext,
useMemo,
} from 'react';

export interface State {
updateAvailable: boolean;
reload: () => Promise<void>;
close: () => void;
}

export const Context = createContext<State>({
updateAvailable: false,
reload: () => Promise.resolve(),
close: () => {},
});

export const useUpdateApp = (): State => useContext(Context);

export const Provider: FC<PropsWithChildren> = ({ children }) => {
const registerSW = useRegisterSW({
onRegisteredSW: async (_, registration) => {
await registration?.update();
},
});

const {
needRefresh: [updateAvailable, setUpdateAvailable],
updateServiceWorker,
} = registerSW;

const reload = useCallback((): Promise<void> => updateServiceWorker(true), [updateServiceWorker]);

const close = useCallback((): void => {
setUpdateAvailable(false);
}, [setUpdateAvailable]);

const state = useMemo<State>(
() => ({
updateAvailable,
reload,
close,
}),
[close, updateAvailable, reload],
);

return <Context.Provider value={state}>{children}</Context.Provider>;
};
62 changes: 62 additions & 0 deletions src/frontend/src/features/updateApp/ui/UpdateAppBanner.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import BrowserUpdatedIcon from '@mui/icons-material/BrowserUpdated';
import {
Avatar,
Box,
Button,
Collapse,
Container,
Divider,
Paper,
Typography,
} from '@mui/material';
import { type FC } from 'react';
import { useUpdateApp } from '../model';

export const UpdateAppBanner: FC = () => {
const { updateAvailable, reload, close } = useUpdateApp();

return (
<Collapse in={updateAvailable}>
<Container disableGutters>
<Box
component={Paper}
role="dialog"
aria-modal="false"
aria-label="Update app banner"
square
variant="elevation"
tabIndex={-1}
elevation={0}
pt={3}
pb={1}
display="flex"
flexDirection={{ xs: 'column', sm: 'row' }}
justifyContent={'space-between'}
alignItems={{ xs: 'flex-start', sm: 'flex-end' }}
gap={{ xs: 0, sm: 1, md: '90px' }}
>
<Box display="flex" pl={2} pr={{ xs: 2, sm: 0 }} gap={{ xs: 2, sm: 3 }}>
<Avatar sx={theme => ({ bgcolor: theme.palette.primary.main })}>
<BrowserUpdatedIcon />
</Avatar>
<Typography paragraph marginBottom={1}>
<Typography fontWeight="bold">New update available</Typography>
<Typography variant="body2">
Click on reload button to update the application
</Typography>
</Typography>
</Box>
<Box display="flex" px={{ xs: 1, sm: 2 }} gap={1} alignSelf="flex-end">
<Button onClick={close} variant="text">
Close
</Button>
<Button onClick={reload} variant="text">
Reload
</Button>
</Box>
</Box>
</Container>
<Divider />
</Collapse>
);
};
1 change: 1 addition & 0 deletions src/frontend/src/features/updateApp/ui/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './UpdateAppBanner';
20 changes: 13 additions & 7 deletions src/frontend/src/pages/ui/LoginPage.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Paper, Stack } from '@mui/material';
import { Box, Paper, Stack } from '@mui/material';
import { type FC } from 'react';
import {
type LoaderFunction,
Expand All @@ -8,6 +8,7 @@ import {
} from 'react-router-dom';
import { store } from '@/app/store';
import { authApi, SignInForm } from '@/features/auth';
import { UpdateAppBanner } from '@/features/updateApp';
import { API_URL, FAKE_AUTH_ENABLED } from '@/shared/config';
import { createUrl } from '@/shared/lib';
import { AppName } from '@/shared/ui';
Expand Down Expand Up @@ -40,10 +41,15 @@ export const action: ActionFunction = async ({ request }) => {
};

export const Component: FC = () => (
<CenteredLayout>
<Paper p={{ xs: 3, sm: 4 }} spacing={3} width="100%" alignItems="center" component={Stack}>
<AppName />
<SignInForm />
</Paper>
</CenteredLayout>
<>
<Box component={Paper} elevation={0} position="fixed" top={0} left={0} right={0}>
<UpdateAppBanner />
</Box>
<CenteredLayout>
<Paper p={{ xs: 3, sm: 4 }} spacing={3} width="100%" alignItems="center" component={Stack}>
<AppName />
<SignInForm />
</Paper>
</CenteredLayout>
</>
);
2 changes: 0 additions & 2 deletions src/frontend/src/pages/ui/RootPage.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
import { type PropsWithChildren, type FC } from 'react';
import { Outlet, ScrollRestoration } from 'react-router-dom';
import { ReloadPrompt } from '@/widgets/pwa';

export const RootPage: FC<PropsWithChildren> = ({ children }) => (
<>
<ScrollRestoration />
<ReloadPrompt />
<Outlet />
{children}
</>
Expand Down
25 changes: 17 additions & 8 deletions src/frontend/src/shared/ui/AppShell/AppShell.styles.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { AppBar, LinearProgress, type LinearProgressProps } from '@mui/material';
import { AppBar, Container, LinearProgress, Paper, type LinearProgressProps } from '@mui/material';
import { styled } from '@mui/material';
import { APP_BAR_HEIGHT_SM, APP_BAR_HEIGHT_XS, SIDEBAR_DRAWER_WIDTH } from '../../constants';

Expand All @@ -12,7 +12,7 @@ interface NavigationProgressStyledProps extends LinearProgressProps {
export const NavigationProgressStyled = styled(LinearProgress, {
shouldForwardProp,
})<NavigationProgressStyledProps>(({ theme, $withSidebar, $withHeader }) => ({
position: 'absolute',
position: 'fixed',
zIndex: theme.zIndex.drawer + 1,
top: $withHeader ? `${APP_BAR_HEIGHT_XS}px` : 0,
left: 0,
Expand All @@ -29,7 +29,9 @@ export const HeaderStyled = styled(AppBar)(({ theme }) => ({
zIndex: theme.zIndex.drawer + 1,
}));

export const SubheaderStyled = styled(AppBar)(({ theme }) => ({
export const SubheaderStyled = styled(Paper)(({ theme }) => ({
zIndex: theme.zIndex.drawer,
position: 'sticky',
top: APP_BAR_HEIGHT_XS,

[theme.breakpoints.up('sm')]: {
Expand All @@ -39,17 +41,24 @@ export const SubheaderStyled = styled(AppBar)(({ theme }) => ({

interface MainStyledProps {
$withSidebar: boolean;
$withSubheader: boolean;
}

export const MainStyled = styled('main', {
shouldForwardProp,
})<MainStyledProps>(({ theme, $withSidebar, $withSubheader }) => ({
paddingTop: $withSubheader ? 0 : theme.spacing(3),
paddingBottom: theme.spacing(3),

})<MainStyledProps>(({ theme, $withSidebar }) => ({
[theme.breakpoints.up('md')]: {
marginLeft: $withSidebar ? `${SIDEBAR_DRAWER_WIDTH}px` : 0,
width: $withSidebar ? `calc(100% - ${SIDEBAR_DRAWER_WIDTH}px)` : '100%',
},
}));

interface MainContainerStyledProps {
$withPaddingTop: boolean;
}

export const MainContainerStyled = styled(Container, {
shouldForwardProp,
})<MainContainerStyledProps>(({ theme, $withPaddingTop }) => ({
paddingTop: $withPaddingTop ? 0 : theme.spacing(3),
paddingBottom: theme.spacing(3),
}));
32 changes: 22 additions & 10 deletions src/frontend/src/shared/ui/AppShell/AppShell.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { Container, Toolbar } from '@mui/material';
import { AppBar, Container, Toolbar } from '@mui/material';
import { type PropsWithChildren, type FC, type ReactElement } from 'react';
import {
HeaderStyled,
MainContainerStyled,
MainStyled,
NavigationProgressStyled,
SubheaderStyled,
Expand All @@ -12,12 +13,17 @@ interface HeaderProps {
navigationDrawer: ReactElement;
}

interface SubheaderProps {
banner: ReactElement;
navigationBar?: ReactElement;
navigationBarElevation?: number;
}

interface Props extends PropsWithChildren {
withNavigationProgress: boolean;
withSidebar: boolean;
header?: HeaderProps;
subheader?: ReactElement;
subheaderElevation?: number;
subheader?: SubheaderProps;
}

export const AppShell: FC<Props> = ({
Expand All @@ -26,7 +32,6 @@ export const AppShell: FC<Props> = ({
withSidebar,
header,
subheader,
subheaderElevation,
}) => (
<>
{header && (
Expand All @@ -40,16 +45,23 @@ export const AppShell: FC<Props> = ({
{withNavigationProgress && (
<NavigationProgressStyled $withSidebar={withSidebar} $withHeader={!!header} />
)}
<MainStyled $withSidebar={withSidebar} $withSubheader={!!subheader}>
<MainStyled $withSidebar={withSidebar}>
{header && <Toolbar />}
{subheader && (
<SubheaderStyled position="sticky" color="inherit" elevation={subheaderElevation}>
<Container disableGutters>
<Toolbar>{subheader}</Toolbar>
</Container>
<SubheaderStyled elevation={0}>
{subheader.banner}
{subheader.navigationBar && (
<AppBar position="static" color="inherit" elevation={subheader.navigationBarElevation}>
<Container disableGutters>
<Toolbar>{subheader.navigationBar}</Toolbar>
</Container>
</AppBar>
)}
</SubheaderStyled>
)}
<Container>{children}</Container>
<MainContainerStyled $withPaddingTop={!!subheader?.navigationBar}>
{children}
</MainContainerStyled>
</MainStyled>
</>
);
8 changes: 6 additions & 2 deletions src/frontend/src/widgets/layout/ui/PrivateLayout.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { type Theme, useMediaQuery, useScrollTrigger } from '@mui/material';
import { type PropsWithChildren, type FC, type ReactElement } from 'react';
import { useAuthStatusCheckEffect } from '@/features/auth';
import { UpdateAppBanner } from '@/features/updateApp';
import { APP_BAR_HEIGHT_SM, APP_BAR_HEIGHT_XS } from '@/shared/constants';
import { useToggle } from '@/shared/hooks';
import { AppShell } from '@/shared/ui';
Expand Down Expand Up @@ -32,8 +33,11 @@ export const PrivateLayout: FC<Props> = ({ children, subheader }) => {
navigationBar: <NavigationBar menuOpened={menuOpened} toggleMenu={toggleMenu} />,
navigationDrawer: <NavigationDrawer menuOpened={menuOpened} toggleMenu={toggleMenu} />,
}}
subheader={subheader}
subheaderElevation={pageScrolled ? 1 : 0}
subheader={{
banner: <UpdateAppBanner />,
navigationBar: subheader,
navigationBarElevation: pageScrolled ? 1 : 0,
}}
>
{children}
</AppShell>
Expand Down
1 change: 0 additions & 1 deletion src/frontend/src/widgets/pwa/index.ts

This file was deleted.

33 changes: 0 additions & 33 deletions src/frontend/src/widgets/pwa/ui/ReloadPrompt.tsx

This file was deleted.

1 change: 0 additions & 1 deletion src/frontend/src/widgets/pwa/ui/index.ts

This file was deleted.

0 comments on commit 7104acb

Please sign in to comment.