Skip to content

Commit

Permalink
Add Toast logic and UI (#610)
Browse files Browse the repository at this point in the history
* add single Toast logic and UI

* WIP

* add info toasts

* refactor toast logic

* add WarningToast and docs

* fix toggle
  • Loading branch information
morewings authored Feb 19, 2023
1 parent a0de1fa commit 95c6495
Show file tree
Hide file tree
Showing 50 changed files with 496 additions and 127 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -65,3 +65,4 @@ yarn-error.log
.yarn-integrity
.next
build
.fontello-session
8 changes: 7 additions & 1 deletion .stylelintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,13 @@
"order/properties-alphabetical-order": true,
"selector-pseudo-class-no-unknown": [true, {
"ignorePseudoClasses": ["global"]
}]
}],
"selector-class-pattern": [
"^[a-z][a-zA-Z0-9]+$",
{
"message": "Expected \"%s\" class name to be lower camelCase"
}
]
},
"plugins": [
"stylelint-order"
Expand Down
75 changes: 70 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,10 +65,10 @@ export default Object.freeze({
});
```

Describe connection between type and component in `src/components/ModalManager/useModalComponent.js
Describe connection between type and component in `src/components/ModalManager/useModalComponent.js`

```js
import ModalContent from 'src/components/ModalContent';
import ModalContent from '@/components/ModalContent';
import modalTypes from './modalTypes';

const useModalComponent = modalType =>
Expand All @@ -79,12 +79,12 @@ const useModalComponent = modalType =>
}[modalType]);

// ...
````
```

Create action hook in `src/components/ModalManager/` folder.

```js
import {useModalActions} from 'src/features/modal';
import {useModalActions} from '@/features/modal';
import modalTypes from './modalTypes';

const useModalComponent = () => {
Expand All @@ -103,7 +103,7 @@ const useModalComponent = () => {
Use it like this:

```jsx
import {useModalComponent} from 'src/components/ModalManager';
import {useModalComponent} from '@/components/ModalManager';

const Component = () => {
//...
Expand All @@ -112,3 +112,68 @@ const Component = () => {
return <div onClick={showModal} />
}
```

### Add new `Toast`

Add new `Toast` type to `@/components/ToastManager/toastTypes.js`
```js
export default Object.freeze({
// ...
NEW_TOAST_NAME: 'NEW_TOAST_NAME',
// ...
});
```

Create `Toast` UI component.

Describe connection between type and new component in `src/components/ToastManager/useToastComponent.js`

```js
import ToastContent from '@/components/ToastContent';
import toastTypes from './toastTypes';

const useToastComponent = toastType =>
({
// ...
[toastTypes.NEW_TOAST_NAME]: ToastContent,
// ...
}[toastType]);
// ...
```

Create state action hook in the new `Toast` component folder.

```js
import {useCallback} from 'react';

import {useToastActions} from '@/features/toast';
import {toastTypes} from '@/components/ToastManager';

export const useToast = () => {
const {openToast} = useToastActions();
return useCallback(
({text}) => {
openToast({
toastType: toastTypes.NEW_TOAST_NAME,
toastProps: {
text,
},
});
},
[openToast]
);
};
```

Use it like this:

```jsx
import {useToastComponent} from '@/components/ToastComponent';

const Component = () => {
//...
const showToast = useToastComponent();
// ...
return <button onClick={showToast} />
}
```
14 changes: 10 additions & 4 deletions src/components/Board/Board.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,21 @@
import React, {useEffect} from 'react';

import useBoardContent from '@/features/structure/useBoardContent';
import {useLocalStorage} from '@/features/localStorage';
import {useSaveState, useHydrateState} from '@/features/localStorage';
import {useInfoToast} from '@/components/InfoToast';
import {Row, Col} from '@/ui/Grid';
import Column from '@/components/Column';

export const Board = () => {
const hydrateState = useLocalStorage();
useSaveState();
const {hydrateState} = useHydrateState();
const showToast = useInfoToast();
useEffect(() => {
hydrateState();
}, [hydrateState]);
const hasLocalState = hydrateState();
return () => {
hasLocalState && showToast({text: 'Loaded data from local storage'});
};
}, [hydrateState, showToast]);
const {childrenId, siblingsId, parentId} = useBoardContent();
return (
<div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,25 @@ import PropTypes from 'prop-types';
import {Icon} from '@/ui/Icon';
import {Button} from '@/ui/Button';
import {FooterSeparator} from '@/ui/FooterSeparator';
import {useStructureActions} from '@/features/structure';
import {useFocusedParentId, useNodeData, useStructureActions} from '@/features/structure';
import {useAccordionActions} from '@/features/accordion';
import {useInfoToast} from '@/components/InfoToast';

import classes from './ConfirmationDeleteNode.module.css';

const ConfirmationDeleteNode = ({id, onCloseModal}) => {
const {deleteNode} = useStructureActions();
const {deleteAccordion} = useAccordionActions();
const {title} = useNodeData(id);
const parentId = useFocusedParentId();
const {title: parentTitle} = useNodeData(parentId);
const showToast = useInfoToast();
const handleDelete = () => {
deleteNode(id);
deleteAccordion(id);
onCloseModal();
showToast({id, text: `Node ${title || id} was deleted`});
showToast({id, text: `Node ${parentTitle || parentId} was focused`});
};
return (
<div className={classes.confirmation}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,19 @@ import {Button} from '@/ui/Button';
import {FooterSeparator} from '@/ui/FooterSeparator';
import {useStructureActions} from '@/features/structure';
import {useAccordionActions} from '@/features/accordion';
import {useInfoToast} from '@/components/InfoToast';

import classes from './ConfirmationDeleteStructure.module.css';

const ConfirmationDeleteStructure = ({onCloseModal}) => {
const {resetStructure} = useStructureActions();
const {resetAccordion} = useAccordionActions();
const showToast = useInfoToast();
const handleDelete = () => {
resetStructure();
resetAccordion();
onCloseModal();
showToast({text: 'Structure was deleted.'});
};
return (
<div className={classes.confirmation}>
Expand Down
3 changes: 3 additions & 0 deletions src/components/EditNode/EditNode.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {useNodeData, useStructureActions} from '@/features/structure';
import NodeFieldset from '@/ui/NodeFieldset';
import {Button} from '@/ui/Button';
import {FooterSeparator} from '@/ui/FooterSeparator';
import {useInfoToast} from '@/components/InfoToast';

import classes from './EditNode.module.css';

Expand All @@ -16,10 +17,12 @@ const EditNode = ({id, onCloseModal}) => {
const [description, setDescription] = useState(nodeData.description);

const {editNode} = useStructureActions();
const showToast = useInfoToast();

const handleEdit = () => {
editNode({id, description, color, isDone, title});
onCloseModal();
showToast({text: `Node "${title || id}" was updated.`});
};

return (
Expand Down
14 changes: 12 additions & 2 deletions src/components/FloatingActions/FloatingActions.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,27 +3,37 @@ import {saveAs} from 'file-saver';

import {ButtonBig} from '@/ui/ButtonBig';
import {ButtonUpload} from '@/ui/ButtonUpload';
import {useFocusedParentId, useStructureActions, useStructure} from '@/features/structure';
import {useFocusedParentId, useStructureActions, useStructure, useNodeData} from '@/features/structure';
import {useDeleteStructureModal} from '@/components/ModalManager';
import readFile from '@/utils/readFile';
import createBlobFromString from '@/utils/createBlobFromString';
import {useInfoToast} from '@/components/InfoToast';

import classes from './FloatingActions.module.css';

const FloatingActions = () => {
const parentId = useFocusedParentId();

const {focusNode, replaceStructure} = useStructureActions();
const {title} = useNodeData(parentId);

const {focusInitialNode} = useStructureActions();
const showToast = useInfoToast();

const handleFocusInitialNode = () => {
focusInitialNode();
showToast({text: 'Initial node was focused'});
};

const handleFocusParent = () => {
focusNode(parentId);
showToast({text: `Node "${title || parentId}" was focused`});
};

const handleFileUpload = async ([file]) => {
const structure = await readFile(file);
structure && replaceStructure(structure);
showToast({text: 'Structure was replaced'});
};

const structure = useStructure();
Expand All @@ -39,7 +49,7 @@ const FloatingActions = () => {
<div className={classes.floatingActions}>
<ButtonBig onClick={handleFocusParent} title="Focus parent" className={classes.button} icon="rewind" />
<ButtonBig
onClick={focusInitialNode}
onClick={handleFocusInitialNode}
title="Focus initial node"
className={classes.button}
icon="full_rewind"
Expand Down
30 changes: 28 additions & 2 deletions src/components/InfoToast/InfoToast.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,31 @@
import React from 'react';
import PropTypes from 'prop-types';

export const InfoToast = () => {
return <div>Info toast</div>;
import {Toast} from '@/ui/Toast';
import {Icon} from '@/ui/Icon';
import {useActionTimer} from '@/utils/useActionTimer';

import classes from './InfoToast.module.css';

export const InfoToast = ({text, onClose}) => {
const handleClose = () => {
onClose();
};
useActionTimer(handleClose, 10);
return (
<Toast className={classes.toast}>
<div className={classes.iconWrapper}>
<Icon name="info" color="white" className={classes.icon} />
</div>
<div className={classes.textWrapper}>{text}</div>
<div className={classes.closeWrapper} role="button" onClick={handleClose}>
<Icon name="close" color="white" className={classes.close} />
</div>
</Toast>
);
};

InfoToast.propTypes = {
text: PropTypes.string.isRequired,
onClose: PropTypes.func.isRequired,
};
27 changes: 27 additions & 0 deletions src/components/InfoToast/InfoToast.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
.toast {
width: calc(44 * var(--baseUnit));
}

.textWrapper {
margin-left: calc(2 * var(--baseUnit));
margin-right: calc(2 * var(--baseUnit));
}

.iconWrapper {
align-self: flex-start;
margin-right: auto;

& .icon {
font-size: calc(5 * var(--baseUnit));
}
}

.closeWrapper {
align-self: start;
margin-left: auto;

& .close {
cursor: pointer;
font-size: calc(2.5 * var(--baseUnit));
}
}
1 change: 1 addition & 0 deletions src/components/InfoToast/index.js
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export {InfoToast} from './InfoToast';
export {useInfoToast} from './useInfoToast';
19 changes: 19 additions & 0 deletions src/components/InfoToast/useInfoToast.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import {useCallback} from 'react';

import {useToastActions} from '@/features/toast';
import {toastTypes} from '@/components/ToastManager';

export const useInfoToast = () => {
const {openToast} = useToastActions();
return useCallback(
({text}) => {
openToast({
toastType: toastTypes.INFO,
toastProps: {
text,
},
});
},
[openToast]
);
};
2 changes: 2 additions & 0 deletions src/components/Layout/Layout.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import PropTypes from 'prop-types';

import {Container} from '@/ui/Grid';
import ModalManager from '@/components/ModalManager/ModalManager';
import {ToastManager} from '@/components/ToastManager';
import SmallScreenWarning from '@/components/SmallScreenWarning';
import Footer from '@/components/Footer';
import config from '@/config';
Expand All @@ -23,6 +24,7 @@ export const Layout = ({children}) => {
<Header siteTitle="Structure" />
<Container className={classes.main}>{children}</Container>
<Footer className={classes.footer} />
<ToastManager />
<ModalManager />
</Fragment>
) : (
Expand Down
Loading

1 comment on commit 95c6495

@vercel
Copy link

@vercel vercel bot commented on 95c6495 Feb 19, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

structure – ./

structure-git-master-morewings.vercel.app
structure.ninja
structure-morewings.vercel.app

Please sign in to comment.