From e259196942977547ac24b40e069add057c19db57 Mon Sep 17 00:00:00 2001 From: Jeremy Kahn Date: Fri, 23 Sep 2022 15:40:34 -0700 Subject: [PATCH] [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 --- src/Bootstrap.tsx | 63 ++++++---- .../ConfirmDialog/ConfirmDialog.tsx | 59 +++++++++ src/components/ConfirmDialog/index.ts | 1 + src/components/Shell/Drawer.tsx | 17 ++- src/components/Shell/Shell.tsx | 7 +- src/config/routes.ts | 1 + src/contexts/StorageContext.ts | 10 ++ src/pages/Settings/Settings.tsx | 114 ++++++++++++++++++ src/pages/Settings/index.ts | 1 + 9 files changed, 245 insertions(+), 28 deletions(-) create mode 100644 src/components/ConfirmDialog/ConfirmDialog.tsx create mode 100644 src/components/ConfirmDialog/index.ts create mode 100644 src/contexts/StorageContext.ts create mode 100644 src/pages/Settings/Settings.tsx create mode 100644 src/pages/Settings/index.ts diff --git a/src/Bootstrap.tsx b/src/Bootstrap.tsx index c99a3db..e30dcd9 100644 --- a/src/Bootstrap.tsx +++ b/src/Bootstrap.tsx @@ -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({ @@ -46,14 +49,14 @@ function Bootstrap({ if (hasLoadedSettings) return const persistedUserSettings = - await persistedStorage.getItem( + await persistedStorageProp.getItem( 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) => { @@ -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 ( - - - {hasLoadedSettings ? ( - - {[routes.ROOT, routes.INDEX_HTML].map(path => ( + + + + {hasLoadedSettings ? ( + + {[routes.ROOT, routes.INDEX_HTML].map(path => ( + } + /> + ))} + } /> } + path={routes.SETTINGS} + element={} /> - ))} - } /> - } - /> - - ) : ( - <> - )} - - + } + /> + + ) : ( + <> + )} + + + ) } diff --git a/src/components/ConfirmDialog/ConfirmDialog.tsx b/src/components/ConfirmDialog/ConfirmDialog.tsx new file mode 100644 index 0000000..5a42b4f --- /dev/null +++ b/src/components/ConfirmDialog/ConfirmDialog.tsx @@ -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 ( + + + + ({ + color: theme.palette.warning.main, + mr: theme.spacing(1), + })} + /> + Are you sure? + + + + + This action cannot be undone. + + + + + + + + ) +} diff --git a/src/components/ConfirmDialog/index.ts b/src/components/ConfirmDialog/index.ts new file mode 100644 index 0000000..f38a653 --- /dev/null +++ b/src/components/ConfirmDialog/index.ts @@ -0,0 +1 @@ +export * from './ConfirmDialog' diff --git a/src/components/Shell/Drawer.tsx b/src/components/Shell/Drawer.tsx index 81de0a5..43c062c 100644 --- a/src/components/Shell/Drawer.tsx +++ b/src/components/Shell/Drawer.tsx @@ -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 = ({ + + + + + + + + + + diff --git a/src/components/Shell/Shell.tsx b/src/components/Shell/Shell.tsx index f4a4560..c2f4c0c 100644 --- a/src/components/Shell/Shell.tsx +++ b/src/components/Shell/Shell.tsx @@ -107,6 +107,10 @@ export const Shell = ({ appNeedsUpdate, children, userPeerId }: ShellProps) => { setIsDrawerOpen(false) } + const handleSettingsLinkClick = () => { + setIsDrawerOpen(false) + } + return ( @@ -135,9 +139,10 @@ export const Shell = ({ appNeedsUpdate, children, userPeerId }: ShellProps) => { /> diff --git a/src/config/routes.ts b/src/config/routes.ts index 42593af..4497da6 100644 --- a/src/config/routes.ts +++ b/src/config/routes.ts @@ -4,4 +4,5 @@ export enum routes { INDEX_HTML = '/index.html', PUBLIC_ROOM = '/public/:roomId', ROOT = '/', + SETTINGS = '/settings', } diff --git a/src/contexts/StorageContext.ts b/src/contexts/StorageContext.ts new file mode 100644 index 0000000..4594251 --- /dev/null +++ b/src/contexts/StorageContext.ts @@ -0,0 +1,10 @@ +import { createContext } from 'react' +import localforage from 'localforage' + +interface StorageContextProps { + getPersistedStorage: () => typeof localforage +} + +export const StorageContext = createContext({ + getPersistedStorage: () => localforage, +}) diff --git a/src/pages/Settings/Settings.tsx b/src/pages/Settings/Settings.tsx new file mode 100644 index 0000000..1741ba4 --- /dev/null +++ b/src/pages/Settings/Settings.tsx @@ -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 ( + + ({ + fontSize: theme.typography.h3.fontSize, + fontWeight: theme.typography.fontWeightMedium, + mb: 2, + })} + > + Data + + ({ + fontSize: theme.typography.h5.fontSize, + fontWeight: theme.typography.fontWeightMedium, + mb: 1.5, + })} + > + Delete all settings data + + ({ + mb: 2, + })} + > + Be careful with this. This will cause your user name to + change from{' '} + + ({ + fontWeight: theme.typography.fontWeightMedium, + })} + > + {userId} + + {' '} + to a new, randomly-assigned name. It will also reset all of your saved + Chitchatter application preferences. + + + + ({ + 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. + + + + ) +} diff --git a/src/pages/Settings/index.ts b/src/pages/Settings/index.ts new file mode 100644 index 0000000..6db39fa --- /dev/null +++ b/src/pages/Settings/index.ts @@ -0,0 +1 @@ +export * from './Settings'