-
Notifications
You must be signed in to change notification settings - Fork 2
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Create a snackbar to notify the user of important events #226
base: master
Are you sure you want to change the base?
Changes from 7 commits
e3ca82e
692c18a
a10d776
2612d3b
b922aa4
6844534
de8d8bf
04d35b3
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
@@ -0,0 +1,23 @@ | ||||||
import styled from 'styled-components'; | ||||||
import { theme } from '../../styling/theme'; | ||||||
|
||||||
export default styled.button` | ||||||
background-color: ${theme.palette.primary.main}; | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This should be
Suggested change
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. See example here |
||||||
border: none; | ||||||
padding-right: 0.5em; | ||||||
float: right; | ||||||
position: absolute; | ||||||
right: 10px; | ||||||
top: 1.2em; | ||||||
color: ${theme.palette.primary.contrast}; | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same here. |
||||||
font-weight: 600; | ||||||
text-transform: uppercase; | ||||||
transition: 0.1s linear; | ||||||
margin-left: 1em; | ||||||
|
||||||
&:hover { | ||||||
color: ${theme.palette.danger.main}; | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same. |
||||||
transition: 0.1s linear; | ||||||
cursor: pointer; | ||||||
} | ||||||
`; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,68 @@ | ||
import React from 'react'; | ||
import styled from 'styled-components'; | ||
import { theme } from '../../styling/theme'; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Here as well. Won't comment anymore on this as I think you get the gist ;) |
||
import SnackBarCloseButton from './SnackBarCloseButton'; | ||
import SnackBarLoader from './SnackBarLoader'; | ||
|
||
interface ISnackBarProps { | ||
className?: string; | ||
clicker?: () => void; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Name should be updated, this doesn't make it very clear what it does. If it's a handler, use the appropriate type. |
||
content: string; | ||
good: boolean; | ||
speed?: string; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Then this could also be of type |
||
} | ||
|
||
const LoaderSpeed = (speed = '6s') => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Shouldn't be capitalized if not a component. You could also move the speed out to a type. Will make the typing a bit clearer, and will be a bit easier to use if you don't know what it does. type Speed = 'fast' | 'medium' | 'slow';
const loaderSpeed = (speed: Speed = 'medium') => {
switch (speed) {
case 'fast':
return '4s';
case 'medium':
return '6s';
case 'slow':
return '8s';
};
}; There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Or even, const speedMap: Record<Speed, string> = {
'fast': '4s',
// ...
}
const loaderSpeed = (speed: Speed = 'medium') => loaderSpeed[speed]; |
||
if (speed === 'fast') { | ||
return '4s'; | ||
} else if (speed === 'slow') { | ||
return '8s'; | ||
} else { | ||
return '6s'; | ||
} | ||
}; | ||
|
||
const SnackBarContainer: React.FC<ISnackBarProps> = ({ | ||
className, | ||
clicker, | ||
content, | ||
good, | ||
speed, | ||
}) => { | ||
return ( | ||
<div className={className}> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. To avoid having an optional |
||
<p>{content}</p> | ||
<SnackBarCloseButton onClick={clicker}>x</SnackBarCloseButton> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Clicker should probably be interface IBlablablaProps {
// ...
onCloseHandler: React.MouseEventHandler<HTMLButtonElement>;
} There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Or if it is on the |
||
<SnackBarLoader | ||
style={{ | ||
animationDuration: LoaderSpeed(speed), | ||
}} | ||
/> | ||
</div> | ||
); | ||
}; | ||
|
||
export default styled(SnackBarContainer)` | ||
color: ${theme.palette.primary.contrast}; | ||
background-color: ${theme.palette.background.default}; | ||
padding: 0.5em; | ||
border-radius: 3px; | ||
border: 3px solid; | ||
border-color: ${props => | ||
props.good ? theme.palette.success.main : theme.palette.danger.main}; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. When updating all other occurrences of the |
||
position: fixed; | ||
bottom: 20px; | ||
left: 20px; | ||
display: inline-block; | ||
min-width: 200px; | ||
max-width: 70vw; | ||
padding-left: 1em; | ||
-webkit-box-shadow: 10px 10px 16px -7px rgba(0, 0, 0, 0.75); | ||
-moz-box-shadow: 10px 10px 16px -7px rgba(0, 0, 0, 0.75); | ||
box-shadow: 10px 10px 16px -7px rgba(0, 0, 0, 0.75); | ||
|
||
& > p { | ||
width: 100%; | ||
padding-right: 4em; | ||
} | ||
`; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
import styled from 'styled-components'; | ||
import { keyframes } from 'styled-components'; | ||
import { theme } from '../../styling/theme'; | ||
|
||
const shrinkBar = keyframes` | ||
from { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
width: 100%; | ||
} | ||
to { | ||
width: 0%; | ||
} | ||
`; | ||
|
||
export default styled.div` | ||
height: 5px; | ||
background-color: ${theme.palette.primary.contrast}; | ||
padding: 0; | ||
margin: 0; | ||
position: absolute; | ||
bottom: 0px; | ||
left: 0px; | ||
animation-name: ${shrinkBar}; | ||
animation-duration: 8s; | ||
animation-iteration-count: 1; | ||
animation-timing-function: linear; | ||
border-radius: 0 3px 3px 0; | ||
`; |
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
@@ -1,15 +1,67 @@ | ||||||
import moment from 'moment'; | ||||||
import React from 'react'; | ||||||
import React, { ReactElement, useReducer } from 'react'; | ||||||
import styled from 'styled-components'; | ||||||
import { useAuthState } from '../../store/contexts/auth'; | ||||||
import { useTransactionDispatch } from '../../store/contexts/transactions'; | ||||||
import { TransactionActions } from '../../store/reducers/transactions'; | ||||||
import Collapsable from '../atoms/Collapsable'; | ||||||
import SnackBarContainer from '../atoms/SnackBarContainer'; | ||||||
import Form from './Form'; | ||||||
|
||||||
const initialState = { snax: <div />, content: '' }; | ||||||
|
||||||
interface IState { | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
snax: ReactElement; | ||||||
content: string; | ||||||
clicker?: () => void; | ||||||
} | ||||||
|
||||||
interface IAction { | ||||||
type: 'clear' | 'good' | 'bad'; | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Argh, already wrote a lot of comments, but updating the file removed them all 🙃 This doesn't have to be an object. type SnAction = 'clear' | 'good' | 'bad'; |
||||||
} | ||||||
|
||||||
const reducer = (state: IState, action: IAction): IState => { | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Okay, so there are quite a few things here that need to be fixed. Already wrote some of them that later disappeared, so I'll be a bit less verbose this time.
I created a simple example of one way to solve snackbars. This handles more than one in a queue system, but reverting it to only storing one shouldn't be hard. Now, how would you implement this in liquidator? Probably one of the easier ways would be to do the same as with There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Note related to the example; If you opt for this method of handling state, remember to clear the timeout that removes the snack if the user clicks it. If not, if a similar toast appears later, it will be removed on the timeout of the first snack. Right now:
|
||||||
switch (action.type) { | ||||||
case 'clear': | ||||||
return initialState; | ||||||
case 'good': | ||||||
return { | ||||||
content: state.content, | ||||||
snax: ( | ||||||
<SnackBarContainer | ||||||
clicker={state.clicker} | ||||||
good={true} | ||||||
content={state.content} | ||||||
/> | ||||||
), | ||||||
}; | ||||||
case 'bad': | ||||||
return { | ||||||
content: state.content, | ||||||
snax: ( | ||||||
<SnackBarContainer | ||||||
clicker={state.clicker} | ||||||
good={false} | ||||||
content={state.content} | ||||||
/> | ||||||
), | ||||||
}; | ||||||
default: | ||||||
return initialState; | ||||||
} | ||||||
}; | ||||||
|
||||||
const AddTransaction: React.FC<{ className?: string }> = props => { | ||||||
const dispatch = useTransactionDispatch(); | ||||||
const auth = useAuthState(); | ||||||
const [state, snaxDispatch] = useReducer(reducer, { | ||||||
content: '', | ||||||
snax: <div />, | ||||||
}); | ||||||
|
||||||
const onButtonClickHandler = () => { | ||||||
snaxDispatch({ type: 'clear' }); | ||||||
}; | ||||||
|
||||||
const onSubmit = async ({ | ||||||
recurring, | ||||||
|
@@ -22,39 +74,50 @@ const AddTransaction: React.FC<{ className?: string }> = props => { | |||||
interval_type, | ||||||
interval, | ||||||
}: any) => { | ||||||
if (!recurring) { | ||||||
await TransactionActions.doCreateTransaction( | ||||||
{ | ||||||
company_id: auth!.selectedCompany!, | ||||||
date, | ||||||
description, | ||||||
money: money * 100, | ||||||
notes, | ||||||
type, | ||||||
}, | ||||||
dispatch | ||||||
); | ||||||
} else { | ||||||
await TransactionActions.doCreateRecurringTransaction( | ||||||
{ | ||||||
company_id: auth!.selectedCompany!, | ||||||
end_date, | ||||||
interval, | ||||||
interval_type, | ||||||
start_date: date, | ||||||
template: { | ||||||
if (!(description.length > 140) && !(notes.length > 140)) { | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Here it's probably better to use guard statements. So instead of wrapping everything in an if (!validLength) {
toast('invalid length');
return;
}
if (!validFormat) {
toast('invalid format');
return;
}
// etc. |
||||||
state.content = 'Transaction added successfully'; | ||||||
state.clicker = onButtonClickHandler; | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Never mutate state in react! All updates to state should be done with |
||||||
snaxDispatch({ type: 'good' }); | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Don't know if you want this, as this will trigger before you get a response from the server. If you want to wait for that, move it to after |
||||||
|
||||||
if (!recurring) { | ||||||
await TransactionActions.doCreateTransaction( | ||||||
{ | ||||||
company_id: auth!.selectedCompany!, | ||||||
date, | ||||||
description, | ||||||
money: money * 100, | ||||||
notes, | ||||||
type, | ||||||
}, | ||||||
}, | ||||||
dispatch | ||||||
); | ||||||
dispatch | ||||||
); | ||||||
} else { | ||||||
await TransactionActions.doCreateRecurringTransaction( | ||||||
{ | ||||||
company_id: auth!.selectedCompany!, | ||||||
end_date, | ||||||
interval, | ||||||
interval_type, | ||||||
start_date: date, | ||||||
template: { | ||||||
description, | ||||||
money: money * 100, | ||||||
type, | ||||||
}, | ||||||
}, | ||||||
dispatch | ||||||
); | ||||||
} | ||||||
} else { | ||||||
state.clicker = onButtonClickHandler; | ||||||
state.content = 'Too many characters.'; | ||||||
snaxDispatch({ type: 'bad' }); | ||||||
setTimeout(() => snaxDispatch({ type: 'clear' }), 6000); | ||||||
} | ||||||
}; | ||||||
|
||||||
return ( | ||||||
<Collapsable heading={<h1>Add new transaction</h1>}> | ||||||
<div>{state.snax}</div> | ||||||
<div className={props.className}> | ||||||
<Form | ||||||
schema={[ | ||||||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Don't import the theme from file, this doesn't allow it to update if the theme changes.