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/types.ts b/src/api/types.ts index f75edad..fa74965 100644 --- a/src/api/types.ts +++ b/src/api/types.ts @@ -1,3 +1,5 @@ +import { IconDefinition } from "@fortawesome/free-solid-svg-icons" + export interface Account { id: string number: number @@ -19,6 +21,7 @@ export interface Application { export interface Goal { id: string + icon: string | null name: string targetAmount: number balance: number diff --git a/src/ui/features/goalmanager/GoalManager.tsx b/src/ui/features/goalmanager/GoalManager.tsx index 0779dda..9fa28d0 100644 --- a/src/ui/features/goalmanager/GoalManager.tsx +++ b/src/ui/features/goalmanager/GoalManager.tsx @@ -1,5 +1,5 @@ import { faCalendarAlt } from '@fortawesome/free-regular-svg-icons' -import { faDollarSign, IconDefinition } from '@fortawesome/free-solid-svg-icons' +import { faDollarSign, faSmile, IconDefinition } from '@fortawesome/free-solid-svg-icons' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { MaterialUiPickersDate } from '@material-ui/pickers/typings/date' import 'date-fns' @@ -11,9 +11,32 @@ import { selectGoalsMap, updateGoal as updateGoalRedux } from '../../../store/go import { useAppDispatch, useAppSelector } from '../../../store/hooks' import DatePicker from '../../components/DatePicker' import { Theme } from '../../components/Theme' +import { TransparentButton } from '../../components/TransparentButton' +import { BaseEmoji } from 'emoji-mart' +import EmojiPicker from '../../components/EmojiPicker' +import GoalIcon from './GoalIcon' + type Props = { goal: Goal } +type EmojiPickerContainerProps = { isOpen: boolean; hasIcon: boolean }; + + export function GoalManager(props: Props) { + + const [icon, setIcon] = useState(null); + const [isEmojiPickerOpen, setIsEmojiPickerOpen] = useState(false); + + useEffect(() => { + setIcon(props.goal.icon); + }, [props.goal.icon, props.goal.id] ) + + const hasIcon = () => icon != null; + + const addIconOnClick = (event: React.MouseEvent) => { + event.stopPropagation(); + setIsEmojiPickerOpen(true); + } + const dispatch = useAppDispatch() const goal = useAppSelector(selectGoalsMap)[props.goal.id] @@ -22,6 +45,26 @@ export function GoalManager(props: Props) { const [targetDate, setTargetDate] = useState(null) const [targetAmount, setTargetAmount] = useState(null) + + const pickEmojiOnClick = (emoji: BaseEmoji, event: React.MouseEvent) => { + event.stopPropagation(); + setIcon(emoji.native); + setIsEmojiPickerOpen(false); + + const updatedGoal: Goal = { + ...props.goal, + icon: emoji.native, + } + + updateGoalApi(props.goal.id, updatedGoal).then((ok) => { + if (!ok) { + console.error('Failed to update goal with new icon'); + } + }); + dispatch(updateGoalRedux(updatedGoal)) + + } + useEffect(() => { setName(props.goal.name) setTargetDate(props.goal.targetDate) @@ -73,7 +116,7 @@ export function GoalManager(props: Props) { dispatch(updateGoalRedux(updatedGoal)) updateGoalApi(props.goal.id, updatedGoal) } - } + }; return ( @@ -106,6 +149,26 @@ export function GoalManager(props: Props) { {new Date(props.goal.created).toLocaleDateString()} + + + + + + + + + Add icon + + + + event.stopPropagation()} + > + + + ) } @@ -113,7 +176,6 @@ 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 } const Field = (props: FieldProps) => ( @@ -182,3 +244,26 @@ const StringInput = styled.input` const Value = styled.div` margin-left: 2rem; ` +const AddIconButtonContainer = styled.div` + margin-top: 2rem; + display: ${(props: AddIconButtonContainerProps) => (props.shouldShow ? 'flex' : 'none')}; + align-items: center; +` + +const GoalIconContainer = styled.div` + display: ${(props) => (props.shouldShow ? 'flex' : 'none')}; + align-items: center; + margin-top: 2rem; +` +const EmojiPickerContainer = styled.div` + display: ${(props) => (props.isOpen ? 'flex' : 'none')}; + position: absolute; + top: ${(props) => (props.hasIcon ? '10rem' : '2rem')}; + left: 0; +` + +const AddIconButtonText = styled.span` + margin-left: 1rem; + font-size: 1.5rem; + color: ${({ theme }: { theme: Theme }) => theme.text}; +` \ No newline at end of file diff --git a/src/ui/pages/Main/goals/GoalCard.tsx b/src/ui/pages/Main/goals/GoalCard.tsx index e8f6d0a..f55789b 100644 --- a/src/ui/pages/Main/goals/GoalCard.tsx +++ b/src/ui/pages/Main/goals/GoalCard.tsx @@ -29,10 +29,16 @@ export default function GoalCard(props: Props) { ${goal.targetAmount} {asLocaleDateString(goal.targetDate)} + {goal.icon} ) } +const Icon = styled.div` + font-size: 3rem; + margin-top: 1rem; +` + const Container = styled(Card)` display: flex; flex-direction: column; diff --git a/src/ui/pages/Main/transactions/TransactionItem.tsx b/src/ui/pages/Main/transactions/TransactionItem.tsx index f8d4cb3..ca2c2d2 100644 --- a/src/ui/pages/Main/transactions/TransactionItem.tsx +++ b/src/ui/pages/Main/transactions/TransactionItem.tsx @@ -8,26 +8,40 @@ import Chip from '../../../components/Chip' type Props = { transaction: Transaction } export function TransactionItem(props: Props) { - const [tags, setTags] = useState(null) - - useEffect(() => { - async function fetch(tagId: string): Promise { - const response = await axios.get(`${API_ROOT}/api/Tag/${tagId}`) - return response.data - } - - async function fetchAll() { - const tags: Tag[] = [] - for (const tagId of props.transaction.tagIds) { - const tag = await fetch(tagId) - tags.push(tag) - } - - setTags(tags) - } - - fetchAll() - }) + const [tags, setTags] = useState(null); + const tagIds = props.transaction.tagIds || []; + + useEffect(() => { + const ids = props.transaction.tagIds ?? []; + if (!ids.length) { setTags([]); return; } + + Promise.all(ids.map(id => + axios.get(`${API_ROOT}/api/Tag/${id}`).then(r => r.data as Tag) + )) + .then(setTags) + .catch(console.error); + }, [props.transaction.tagIds]); + + + // Faulty Code Snippet - inifite api call + // useEffect(() => { + // async function fetch(tagId: string): Promise { + // const response = await axios.get(`${API_ROOT}/api/Tag/${tagId}`) + // return response.data + // } + + // async function fetchAll() { + // const tags: Tag[] = [] + // for (const tagId of props.transaction.tagIds) { + // const tag = await fetch(tagId) + // tags.push(tag) + // } + + // setTags(tags) + // } + + // fetchAll() + // }) return (