diff --git a/frontend/src/components/atoms/ProjectionRow.tsx b/frontend/src/components/atoms/ProjectionRow.tsx index 1e30853..58215aa 100644 --- a/frontend/src/components/atoms/ProjectionRow.tsx +++ b/frontend/src/components/atoms/ProjectionRow.tsx @@ -20,6 +20,7 @@ const ProjectionRow = styled.div` align-self: center; margin-top: 0.2em; margin-bottom: 0.2em; + word-break: break-word; } p:nth-last-child(-n + 3), diff --git a/frontend/src/components/atoms/SnackBarCloseButton.jsx b/frontend/src/components/atoms/SnackBarCloseButton.jsx new file mode 100644 index 0000000..96c8927 --- /dev/null +++ b/frontend/src/components/atoms/SnackBarCloseButton.jsx @@ -0,0 +1,22 @@ +import styled from 'styled-components'; + +export default styled.button` + background-color: ${props => props.theme.palette.primary.main}; + border: none; + padding-right: 0.5em; + float: right; + position: absolute; + right: 10px; + top: 1.2em; + color: ${props => props.theme.palette.primary.contrast}; + font-weight: 600; + text-transform: uppercase; + transition: 0.1s linear; + margin-left: 1em; + + &:hover { + color: ${props => props.theme.palette.danger.main}; + transition: 0.1s linear; + cursor: pointer; + } +`; diff --git a/frontend/src/components/atoms/SnackBarContainer.tsx b/frontend/src/components/atoms/SnackBarContainer.tsx new file mode 100644 index 0000000..f1663a0 --- /dev/null +++ b/frontend/src/components/atoms/SnackBarContainer.tsx @@ -0,0 +1,71 @@ +import React from 'react'; +import styled from 'styled-components'; +import SnackBarCloseButton from './SnackBarCloseButton'; +import SnackBarLoader from './SnackBarLoader'; + +interface ISnackBar { + className?: string; + snackBarCloseHandler?: React.MouseEventHandler; + content: string; + good: boolean; + speed?: number; +} + +const loaderSpeed = (speed: number = 6000) => { + switch (speed) { + case 4000: + return '4s'; + case 6000: + return '6s'; + case 8000: + return '8s'; + } +}; + +const SnackBarContainer: React.FC = ({ + className, + snackBarCloseHandler, + content, + speed, +}) => { + return ( +
+

{content}

+ + x + + +
+ ); +}; + +export default styled(SnackBarContainer)` + color: ${props => props.theme.palette.primary.contrast}; + background-color: ${props => props.theme.palette.background.default}; + padding: 0.5em; + border-radius: 3px; + border: 3px solid; + border-color: ${props => + props.good + ? props.theme.palette.success.main + : props.theme.palette.danger.main}; + 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; + } +`; diff --git a/frontend/src/components/atoms/SnackBarLoader.jsx b/frontend/src/components/atoms/SnackBarLoader.jsx new file mode 100644 index 0000000..6d76630 --- /dev/null +++ b/frontend/src/components/atoms/SnackBarLoader.jsx @@ -0,0 +1,26 @@ +import styled from 'styled-components'; +import { keyframes } from 'styled-components'; + +const shrinkBar = keyframes` + from { + width: 100%; + } + to { + width: 0%; + } +`; + +export default styled.div` + height: 5px; + background-color: ${props => props.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; +`; diff --git a/frontend/src/components/molecules/AddTransaction.tsx b/frontend/src/components/molecules/AddTransaction.tsx index 8fd2b19..56683d9 100644 --- a/frontend/src/components/molecules/AddTransaction.tsx +++ b/frontend/src/components/molecules/AddTransaction.tsx @@ -4,12 +4,51 @@ import styled from 'styled-components'; import { useAuthState } from '../../store/contexts/auth'; import { useTransactionDispatch } from '../../store/contexts/transactions'; import { TransactionActions } from '../../store/reducers/transactions'; +import { snackReducer } from '../../store/reducers/transactions'; + import Collapsable from '../atoms/Collapsable'; +import SnackBarContainer from '../atoms/SnackBarContainer'; import Form from './Form'; const AddTransaction: React.FC<{ className?: string }> = props => { const dispatch = useTransactionDispatch(); const auth = useAuthState(); + const [store, snackDispatch] = React.useReducer(snackReducer, [] as Array<{ + content: string; + variant: boolean; + speed: number; + }>); + + let delayTimer = setTimeout(null, 0); + + const onButtonClickHandler = ( + content: string, + speed: number, + variant: boolean + ) => { + snackDispatch({ + payload: { content, speed, variant }, + type: 'REMOVE_SNACK', + }); + // For some reason the delayTimer ID will be different if it is true or false, which is why it needs to either be +1 or -1 to make sure it gets the right ID. + if (variant === true) { + clearTimeout(delayTimer + 1); + } else { + clearTimeout(delayTimer - 1); + } + }; + const createSnack = (content: string, variant: boolean, speed: number) => { + snackDispatch({ + payload: { content, speed, variant }, + type: 'ADD_SNACK', + }); + delayTimer = setTimeout(() => { + snackDispatch({ + payload: { content, speed, variant }, + type: 'REMOVE_SNACK', + }); + }, speed); + }; const onSubmit = async ({ recurring, @@ -22,6 +61,14 @@ const AddTransaction: React.FC<{ className?: string }> = props => { interval_type, interval, }: any) => { + if (description.length > 140 && notes.length > 140) { + createSnack( + 'Too many characters in notes or description. Please try again', + false, + 6000 + ); + return; + } if (!recurring) { await TransactionActions.doCreateTransaction( { @@ -51,10 +98,26 @@ const AddTransaction: React.FC<{ className?: string }> = props => { dispatch ); } + createSnack('Transaction added successfully', true, 6000); }; - return ( Add new transaction}> +
+ {store[0] && ( + + onButtonClickHandler( + store[0].content, + store[0].speed, + store[0].variant + ) + } + speed={store[0].speed} + /> + )} +
= [], + action: ISnackAction +) => { + switch (action.type) { + case 'ADD_SNACK': + return [...state, action.payload]; + case 'REMOVE_SNACK': + const currentSnack = state.findIndex(e => e === action.payload); + + if (currentSnack !== -1) { + return [ + ...state.slice(0, currentSnack), + ...state.slice(currentSnack + 1), + ]; + } + return []; // currently set to yeet the entire array, can probably be modified to only yeet the correct element. + } +}; + /** * The return types of all the elements in ActionCreators * NOTE: Should not be modified! diff --git a/frontend/src/stories/index.stories.tsx b/frontend/src/stories/index.stories.tsx index 8a4900d..e2db7bc 100644 --- a/frontend/src/stories/index.stories.tsx +++ b/frontend/src/stories/index.stories.tsx @@ -10,6 +10,7 @@ import RecurringTransactionOptions, { IntervalType, } from '../components/atoms/RecurringTransactionOptions'; import Select from '../components/atoms/Select'; +import SnackBarContainer from '../components/atoms/SnackBarContainer'; import TextArea from '../components/atoms/TextArea'; import TransactionEntry from '../components/atoms/TransactionEntry'; import AddTransaction from '../components/molecules/AddTransaction'; @@ -299,3 +300,40 @@ storiesOf('Input/Select', module) ); }); + +storiesOf('SnackBarContainer', module) + .addDecorator(fn => ( +
+ {fn()} +
+ )) + .add('Long', () => ( + + )); + +storiesOf('SnackBarContainer', module) + .addDecorator(fn => ( +
+ {fn()} +
+ )) + .add('Fast', () => ( + + )); +storiesOf('SnackBarContainer', module) + .addDecorator(fn => ( +
+ {fn()} +
+ )) + .add('Medium', () => ( + + ));