-
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 5 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 SnackBarCloseButton from './SnackBarCloseButton'; | ||
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 SnackBarLoader from './SnackBarLoader'; | ||
|
||
interface ISnackBarProps { | ||
content: string; | ||
good: boolean; | ||
className?: string; | ||
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 |
||
clicker?: () => void; | ||
} | ||
|
||
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> = ({ | ||
content, | ||
good, | ||
className, | ||
speed, | ||
clicker, | ||
}) => { | ||
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, { useReducer, ReactElement } 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 Form from './Form'; | ||
import SnackBarContainer from '../atoms/SnackBarContainer'; | ||
|
||
const initialState = { snax: <div></div>, content: '' }; | ||
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. Storing a react element in a store like this is generally not recommended, as it has to recreate this component every time the reducer updates. I must admit I've never seen this done before, so +1 for creativity, but this should probably be changed. I believe there are quite some downsides to doing it like this, especially considering how mounting this onto the shadow-dom and dom would be done. |
||
|
||
type stateType = { | ||
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. All types should be Capitalized. 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. I recommend setting up tslint and prettier in your workspace. If not, run |
||
snax: ReactElement; | ||
content: string; | ||
clicker?: () => void; | ||
}; | ||
|
||
type ActionType = { | ||
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: stateType, action: ActionType): stateType => { | ||
switch (action.type) { | ||
case 'clear': | ||
return initialState; | ||
case 'good': | ||
return { | ||
snax: ( | ||
<SnackBarContainer | ||
clicker={state.clicker} | ||
good={true} | ||
content={state.content} | ||
/> | ||
), | ||
content: state.content, | ||
}; | ||
case 'bad': | ||
return { | ||
snax: ( | ||
<SnackBarContainer | ||
clicker={state.clicker} | ||
good={false} | ||
content={state.content} | ||
/> | ||
), | ||
content: state.content, | ||
}; | ||
default: | ||
return initialState; | ||
} | ||
}; | ||
|
||
const AddTransaction: React.FC<{ className?: string }> = props => { | ||
const dispatch = useTransactionDispatch(); | ||
const auth = useAuthState(); | ||
const [state, snaxDispatch] = useReducer(reducer, { | ||
snax: <div></div>, | ||
content: '', | ||
}); | ||
|
||
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.