diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1 @@ +{} diff --git a/package-lock.json b/package-lock.json index 240aecf..94349f8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6611,6 +6611,7 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/emoji-mart/-/emoji-mart-3.0.1.tgz", "integrity": "sha512-sxpmMKxqLvcscu6mFn9ITHeZNkGzIvD0BSNFE/LJESPbCA8s1jM6bCDPjWbV31xHq7JXaxgpHxLB54RCbBZSlg==", + "license": "BSD-3-Clause", "dependencies": { "@babel/runtime": "^7.0.0", "prop-types": "^15.6.0" diff --git a/src/api/lib.ts b/src/api/lib.ts index 3c593ca..52d1f72 100644 --- a/src/api/lib.ts +++ b/src/api/lib.ts @@ -45,7 +45,12 @@ export async function createGoal(): Promise { export async function updateGoal(goalId: string, updatedGoal: Goal): Promise { try { - await axios.put(`${API_ROOT}/api/Goal/${goalId}`, updatedGoal) + await axios.put(`${API_ROOT}/api/Goal/${goalId}`, updatedGoal, { + headers: { + 'Content-Type': 'application/json', + }, + }) + return true } catch (error: any) { return false diff --git a/src/api/types.ts b/src/api/types.ts index f75edad..0f89cf8 100644 --- a/src/api/types.ts +++ b/src/api/types.ts @@ -23,6 +23,7 @@ export interface Goal { targetAmount: number balance: number targetDate: Date + icon: string | null created: Date accountId: string transactionIds: string[] diff --git a/src/ui/features/goalmanager/GoalManager.tsx b/src/ui/features/goalmanager/GoalManager.tsx index 0779dda..be7fe07 100644 --- a/src/ui/features/goalmanager/GoalManager.tsx +++ b/src/ui/features/goalmanager/GoalManager.tsx @@ -5,14 +5,20 @@ import { MaterialUiPickersDate } from '@material-ui/pickers/typings/date' import 'date-fns' import React, { useEffect, useState } from 'react' import styled from 'styled-components' + import { updateGoal as updateGoalApi } from '../../../api/lib' import { Goal } from '../../../api/types' import { selectGoalsMap, updateGoal as updateGoalRedux } from '../../../store/goalsSlice' import { useAppDispatch, useAppSelector } from '../../../store/hooks' + import DatePicker from '../../components/DatePicker' +import EmojiPicker from '../../components/EmojiPicker' import { Theme } from '../../components/Theme' +import { BaseEmoji } from 'emoji-mart/dist-es/utils/emoji-index/nimble-emoji-index' + type Props = { goal: Goal } + export function GoalManager(props: Props) { const dispatch = useAppDispatch() @@ -22,16 +28,17 @@ export function GoalManager(props: Props) { const [targetDate, setTargetDate] = useState(null) const [targetAmount, setTargetAmount] = useState(null) + const [icon, setIcon] = useState(props.goal.icon ?? null) + + const [emojiPickerIsOpen, setEmojiPickerIsOpen] = useState(false) + + const hasIcon = () => icon != null + useEffect(() => { setName(props.goal.name) setTargetDate(props.goal.targetDate) setTargetAmount(props.goal.targetAmount) - }, [ - props.goal.id, - props.goal.name, - props.goal.targetDate, - props.goal.targetAmount, - ]) + }, [props.goal.id, props.goal.name, props.goal.targetDate, props.goal.targetAmount]) useEffect(() => { setName(goal.name) @@ -39,24 +46,30 @@ export function GoalManager(props: Props) { const updateNameOnChange = (event: React.ChangeEvent) => { const nextName = event.target.value + setName(nextName) + const updatedGoal: Goal = { ...props.goal, name: nextName, } + dispatch(updateGoalRedux(updatedGoal)) updateGoalApi(props.goal.id, updatedGoal) } const updateTargetAmountOnChange = (event: React.ChangeEvent) => { const nextTargetAmount = parseFloat(event.target.value) + setTargetAmount(nextTargetAmount) + const updatedGoal: Goal = { ...props.goal, name: name ?? props.goal.name, targetDate: targetDate ?? props.goal.targetDate, targetAmount: nextTargetAmount, } + dispatch(updateGoalRedux(updatedGoal)) updateGoalApi(props.goal.id, updatedGoal) } @@ -64,23 +77,46 @@ export function GoalManager(props: Props) { const pickDateOnChange = (date: MaterialUiPickersDate) => { if (date != null) { setTargetDate(date) + const updatedGoal: Goal = { ...props.goal, name: name ?? props.goal.name, targetDate: date ?? props.goal.targetDate, targetAmount: targetAmount ?? props.goal.targetAmount, } + dispatch(updateGoalRedux(updatedGoal)) updateGoalApi(props.goal.id, updatedGoal) } } + const pickEmojiOnClick = (emoji: BaseEmoji, event: React.MouseEvent) => { + event.stopPropagation() + + setIcon(emoji.native) + + setEmojiPickerIsOpen(false) + + const updatedGoal: Goal = { + ...props.goal, + icon: emoji.native ?? props.goal.icon, + name: name ?? props.goal.name, + targetDate: targetDate ?? props.goal.targetDate, + targetAmount: targetAmount ?? props.goal.targetAmount, + } + + dispatch(updateGoalRedux(updatedGoal)) + + updateGoalApi(props.goal.id, updatedGoal) + } + return ( + @@ -88,13 +124,38 @@ export function GoalManager(props: Props) { + + + + + + { + event.stopPropagation() + setEmojiPickerIsOpen(true) + }} + style={{ cursor: 'pointer' }} + > + {icon ?? 'Click to add icon'} + + + + + event.stopPropagation()} + > + + + {props.goal.balance} @@ -102,6 +163,7 @@ export function GoalManager(props: Props) { + {new Date(props.goal.created).toLocaleDateString()} @@ -110,14 +172,20 @@ export function GoalManager(props: Props) { ) } -type FieldProps = { name: string; icon: IconDefinition } -type AddIconButtonContainerProps = { shouldShow: boolean } -type GoalIconContainerProps = { shouldShow: boolean } -type EmojiPickerContainerProps = { isOpen: boolean; hasIcon: boolean } +type FieldProps = { + name: string + icon: IconDefinition +} + +type EmojiPickerContainerProps = { + isOpen: boolean + hasIcon: boolean +} const Field = (props: FieldProps) => ( + {props.name} ) @@ -125,60 +193,45 @@ const Field = (props: FieldProps) => ( const GoalManagerContainer = styled.div` display: flex; flex-direction: column; - justify-content: flex-start; - align-items: flex-start; - height: 100%; - width: 100%; position: relative; ` const Group = styled.div` display: flex; - flex-direction: row; - width: 100%; - margin-top: 1.25rem; - margin-bottom: 1.25rem; + margin-top: 1rem; ` + const NameInput = styled.input` - display: flex; - background-color: transparent; - outline: none; + font-size: 3rem; border: none; - font-size: 4rem; - font-weight: bold; - color: ${({ theme }: { theme: Theme }) => theme.text}; + background: transparent; ` const FieldName = styled.h1` - font-size: 1.8rem; margin-left: 1rem; - color: rgba(174, 174, 174, 1); - font-weight: normal; ` + const FieldContainer = styled.div` display: flex; - flex-direction: row; - align-items: center; - width: 20rem; - - svg { - color: rgba(174, 174, 174, 1); - } ` + const StringValue = styled.h1` - font-size: 1.8rem; - font-weight: bold; + font-size: 1.5rem; ` + const StringInput = styled.input` - display: flex; - background-color: transparent; - outline: none; + font-size: 1.5rem; border: none; - font-size: 1.8rem; - font-weight: bold; - color: ${({ theme }: { theme: Theme }) => theme.text}; + background: transparent; ` const Value = styled.div` - margin-left: 2rem; + margin-left: 1rem; +` + +const EmojiPickerContainer = styled.div` + display: ${(props) => (props.isOpen ? 'flex' : 'none')}; + position: absolute; + top: ${(props) => (props.hasIcon ? '10rem' : '2rem')}; + left: 0; ` diff --git a/src/ui/pages/Main/goals/GoalCard.tsx b/src/ui/pages/Main/goals/GoalCard.tsx index e8f6d0a..12072ce 100644 --- a/src/ui/pages/Main/goals/GoalCard.tsx +++ b/src/ui/pages/Main/goals/GoalCard.tsx @@ -8,6 +8,7 @@ import { setType as setTypeRedux } from '../../../../store/modalSlice' import { Card } from '../../../components/Card' +import Icon from '@material-ui/core/Icon/Icon' type Props = { id: string } @@ -29,6 +30,7 @@ export default function GoalCard(props: Props) { ${goal.targetAmount} {asLocaleDateString(goal.targetDate)} + {goal.icon ?? '➕'} ) } @@ -54,3 +56,13 @@ const TargetDate = styled.h4` color: rgba(174, 174, 174, 1); font-size: 1rem; ` +const GoalIcon = styled.div` + font-size: 4rem; + + display: flex; + justify-content: center; + align-items: center; + + margin-top: 0.5rem; + margin-bottom: 0.5rem; +` \ No newline at end of file