[closes #24] Settings UI (#26)

* feat: [#24] wire up settings page
* feat: [#24] stand up settings UI
* feat: [#24] implement storage deletion
* feat: [#24] confirm deletion of settings data
This commit is contained in:
Jeremy Kahn 2022-09-23 15:40:34 -07:00 committed by GitHub
parent 20d8337082
commit e259196942
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 245 additions and 28 deletions

View File

@ -4,10 +4,12 @@ import { v4 as uuid } from 'uuid'
import localforage from 'localforage'
import * as serviceWorkerRegistration from 'serviceWorkerRegistration'
import { StorageContext } from 'contexts/StorageContext'
import { SettingsContext } from 'contexts/SettingsContext'
import { routes } from 'config/routes'
import { Home } from 'pages/Home'
import { About } from 'pages/About'
import { Settings } from 'pages/Settings'
import { PublicRoom } from 'pages/PublicRoom'
import { UserSettings } from 'models/settings'
import { PersistedStorageKeys } from 'models/storage'
@ -19,12 +21,13 @@ export interface BootstrapProps {
}
function Bootstrap({
persistedStorage = localforage.createInstance({
persistedStorage: persistedStorageProp = localforage.createInstance({
name: 'chitchatter',
description: 'Persisted settings data for chitchatter',
}),
getUuid = uuid,
}: BootstrapProps) {
const [persistedStorage] = useState(persistedStorageProp)
const [appNeedsUpdate, setAppNeedsUpdate] = useState(false)
const [hasLoadedSettings, setHasLoadedSettings] = useState(false)
const [userSettings, setUserSettings] = useState<UserSettings>({
@ -46,14 +49,14 @@ function Bootstrap({
if (hasLoadedSettings) return
const persistedUserSettings =
await persistedStorage.getItem<UserSettings>(
await persistedStorageProp.getItem<UserSettings>(
PersistedStorageKeys.USER_SETTINGS
)
if (persistedUserSettings) {
setUserSettings(persistedUserSettings)
} else {
await persistedStorage.setItem(
await persistedStorageProp.setItem(
PersistedStorageKeys.USER_SETTINGS,
userSettings
)
@ -61,7 +64,7 @@ function Bootstrap({
setHasLoadedSettings(true)
})()
}, [hasLoadedSettings, persistedStorage, userSettings, userId])
}, [hasLoadedSettings, persistedStorageProp, userSettings, userId])
const settingsContextValue = {
updateUserSettings: async (changedSettings: Partial<UserSettings>) => {
@ -70,7 +73,7 @@ function Bootstrap({
...changedSettings,
}
await persistedStorage.setItem(
await persistedStorageProp.setItem(
PersistedStorageKeys.USER_SETTINGS,
newSettings
)
@ -80,30 +83,40 @@ function Bootstrap({
getUserSettings: () => ({ ...userSettings }),
}
const storageContextValue = {
getPersistedStorage: () => persistedStorage,
}
return (
<Router>
<SettingsContext.Provider value={settingsContextValue}>
<Shell appNeedsUpdate={appNeedsUpdate} userPeerId={userId}>
{hasLoadedSettings ? (
<Routes>
{[routes.ROOT, routes.INDEX_HTML].map(path => (
<StorageContext.Provider value={storageContextValue}>
<SettingsContext.Provider value={settingsContextValue}>
<Shell appNeedsUpdate={appNeedsUpdate} userPeerId={userId}>
{hasLoadedSettings ? (
<Routes>
{[routes.ROOT, routes.INDEX_HTML].map(path => (
<Route
key={path}
path={path}
element={<Home userId={userId} />}
/>
))}
<Route path={routes.ABOUT} element={<About />} />
<Route
key={path}
path={path}
element={<Home userId={userId} />}
path={routes.SETTINGS}
element={<Settings userId={userId} />}
/>
))}
<Route path={routes.ABOUT} element={<About />} />
<Route
path={routes.PUBLIC_ROOM}
element={<PublicRoom userId={userId} />}
/>
</Routes>
) : (
<></>
)}
</Shell>
</SettingsContext.Provider>
<Route
path={routes.PUBLIC_ROOM}
element={<PublicRoom userId={userId} />}
/>
</Routes>
) : (
<></>
)}
</Shell>
</SettingsContext.Provider>
</StorageContext.Provider>
</Router>
)
}

View File

@ -0,0 +1,59 @@
import Box from '@mui/material/Box'
import Button from '@mui/material/Button'
import Dialog from '@mui/material/Dialog'
import DialogActions from '@mui/material/DialogActions'
import DialogContent from '@mui/material/DialogContent'
import DialogContentText from '@mui/material/DialogContentText'
import DialogTitle from '@mui/material/DialogTitle'
import WarningIcon from '@mui/icons-material/Warning'
interface ConfirmDialogProps {
isOpen: boolean
onCancel: () => void
onConfirm: () => void
}
export const ConfirmDialog = ({
isOpen,
onCancel,
onConfirm,
}: ConfirmDialogProps) => {
const handleDialogClose = () => {
onCancel()
}
return (
<Dialog
open={isOpen}
aria-labelledby="alert-dialog-title"
aria-describedby="alert-dialog-description"
onClose={handleDialogClose}
>
<DialogTitle id="alert-dialog-title">
<Box sx={{ display: 'flex', alignItems: 'center' }}>
<WarningIcon
fontSize="medium"
sx={theme => ({
color: theme.palette.warning.main,
mr: theme.spacing(1),
})}
/>
Are you sure?
</Box>
</DialogTitle>
<DialogContent>
<DialogContentText id="alert-dialog-description">
This action cannot be undone.
</DialogContentText>
</DialogContent>
<DialogActions>
<Button onClick={onCancel} autoFocus>
Cancel
</Button>
<Button onClick={onConfirm} color="error">
Confirm
</Button>
</DialogActions>
</Dialog>
)
}

View File

@ -0,0 +1 @@
export * from './ConfirmDialog'

View File

@ -13,6 +13,7 @@ import ListItemButton from '@mui/material/ListItemButton'
import ListItemIcon from '@mui/material/ListItemIcon'
import ListItemText from '@mui/material/ListItemText'
import Home from '@mui/icons-material/Home'
import SettingsApplications from '@mui/icons-material/SettingsRounded'
import QuestionMark from '@mui/icons-material/QuestionMark'
import Brightness4Icon from '@mui/icons-material/Brightness4'
import Brightness7Icon from '@mui/icons-material/Brightness7'
@ -27,18 +28,20 @@ export const drawerWidth = 240
export interface DrawerProps extends PropsWithChildren {
isDrawerOpen: boolean
onAboutLinkClick: () => void
onDrawerClose: () => void
onHomeLinkClick: () => void
onAboutLinkClick: () => void
onSettingsLinkClick: () => void
theme: Theme
userPeerId: string
}
export const Drawer = ({
isDrawerOpen,
onAboutLinkClick,
onDrawerClose,
onHomeLinkClick,
onAboutLinkClick,
onSettingsLinkClick,
theme,
userPeerId,
}: DrawerProps) => {
@ -101,6 +104,16 @@ export const Drawer = ({
</ListItemButton>
</ListItem>
</Link>
<Link to={routes.SETTINGS} onClick={onSettingsLinkClick}>
<ListItem disablePadding>
<ListItemButton>
<ListItemIcon>
<SettingsApplications />
</ListItemIcon>
<ListItemText primary="Settings" />
</ListItemButton>
</ListItem>
</Link>
<Link to={routes.ABOUT} onClick={onAboutLinkClick}>
<ListItem disablePadding>
<ListItemButton>

View File

@ -107,6 +107,10 @@ export const Shell = ({ appNeedsUpdate, children, userPeerId }: ShellProps) => {
setIsDrawerOpen(false)
}
const handleSettingsLinkClick = () => {
setIsDrawerOpen(false)
}
return (
<ShellContext.Provider value={shellContextValue}>
<ThemeProvider theme={theme}>
@ -135,9 +139,10 @@ export const Shell = ({ appNeedsUpdate, children, userPeerId }: ShellProps) => {
/>
<Drawer
isDrawerOpen={isDrawerOpen}
onAboutLinkClick={handleAboutLinkClick}
onDrawerClose={handleDrawerClose}
onHomeLinkClick={handleHomeLinkClick}
onAboutLinkClick={handleAboutLinkClick}
onSettingsLinkClick={handleSettingsLinkClick}
theme={theme}
userPeerId={userPeerId}
/>

View File

@ -4,4 +4,5 @@ export enum routes {
INDEX_HTML = '/index.html',
PUBLIC_ROOM = '/public/:roomId',
ROOT = '/',
SETTINGS = '/settings',
}

View File

@ -0,0 +1,10 @@
import { createContext } from 'react'
import localforage from 'localforage'
interface StorageContextProps {
getPersistedStorage: () => typeof localforage
}
export const StorageContext = createContext<StorageContextProps>({
getPersistedStorage: () => localforage,
})

View File

@ -0,0 +1,114 @@
import { useContext, useEffect, useState } from 'react'
import Box from '@mui/material/Box'
import Button from '@mui/material/Button'
import Typography from '@mui/material/Typography'
import Divider from '@mui/material/Divider'
import { ShellContext } from 'contexts/ShellContext'
import { StorageContext } from 'contexts/StorageContext'
import { PeerNameDisplay } from 'components/PeerNameDisplay'
import { ConfirmDialog } from '../../components/ConfirmDialog'
interface SettingsProps {
userId: string
}
export const Settings = ({ userId }: SettingsProps) => {
const { setTitle } = useContext(ShellContext)
const { getPersistedStorage } = useContext(StorageContext)
const [
isDeleteSettingsConfirmDiaglogOpen,
setIsDeleteSettingsConfirmDiaglogOpen,
] = useState(false)
const persistedStorage = getPersistedStorage()
useEffect(() => {
setTitle('Settings')
}, [setTitle])
const handleDeleteSettingsClick = () => {
setIsDeleteSettingsConfirmDiaglogOpen(true)
}
const handleDeleteSettingsCancel = () => {
setIsDeleteSettingsConfirmDiaglogOpen(false)
}
const handleDeleteSettingsConfirm = async () => {
await persistedStorage.clear()
window.location.reload()
}
return (
<Box className="max-w-3xl mx-auto p-4">
<Typography
variant="h2"
sx={theme => ({
fontSize: theme.typography.h3.fontSize,
fontWeight: theme.typography.fontWeightMedium,
mb: 2,
})}
>
Data
</Typography>
<Typography
variant="h2"
sx={theme => ({
fontSize: theme.typography.h5.fontSize,
fontWeight: theme.typography.fontWeightMedium,
mb: 1.5,
})}
>
Delete all settings data
</Typography>
<Typography
variant="body1"
sx={_theme => ({
mb: 2,
})}
>
<strong>Be careful with this</strong>. This will cause your user name to
change from{' '}
<strong>
<PeerNameDisplay
sx={theme => ({
fontWeight: theme.typography.fontWeightMedium,
})}
>
{userId}
</PeerNameDisplay>
</strong>{' '}
to a new, randomly-assigned name. It will also reset all of your saved
Chitchatter application preferences.
</Typography>
<Button
variant="outlined"
color="error"
sx={_theme => ({
mb: 2,
})}
onClick={handleDeleteSettingsClick}
>
Delete all data and restart
</Button>
<ConfirmDialog
isOpen={isDeleteSettingsConfirmDiaglogOpen}
onCancel={handleDeleteSettingsCancel}
onConfirm={handleDeleteSettingsConfirm}
/>
<Typography
variant="subtitle2"
sx={_theme => ({
mb: 2,
})}
>
Chitchatter only stores user preferences and never message content of
any kind. This preference data is only stored locally on your device and
not a server.
</Typography>
<Divider sx={{ my: 2 }} />
</Box>
)
}

View File

@ -0,0 +1 @@
export * from './Settings'