From 1780571dea2f87af8b19ad708e10c16a21f70ddf Mon Sep 17 00:00:00 2001 From: Harsh Date: Mon, 15 Jun 2026 00:43:01 +0530 Subject: [PATCH] Implement goal icon selection and persistence --- src/api/lib.ts | 15 ++- src/api/types.ts | 1 + src/ui/components/EmojiPicker.tsx | 7 +- src/ui/features/goalmanager/GoalManager.tsx | 105 +++++++++++++++++++- src/ui/pages/Main/goals/GoalCard.tsx | 11 +- 5 files changed, 128 insertions(+), 11 deletions(-) diff --git a/src/api/lib.ts b/src/api/lib.ts index 3c593ca..bb893b3 100644 --- a/src/api/lib.ts +++ b/src/api/lib.ts @@ -2,7 +2,9 @@ import axios from 'axios' import { user } from '../data/user' import { Goal, Transaction, User } from './types' -export const API_ROOT = 'https://fencer-commbank.azurewebsites.net' +//export const API_ROOT = 'https://fencer-commbank.azurewebsites.net' +export const API_ROOT = 'https://localhost:5203' + export async function getUser(): Promise { try { @@ -43,9 +45,16 @@ export async function createGoal(): Promise { } } -export async function updateGoal(goalId: string, updatedGoal: Goal): 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 + ) + return true } catch (error: any) { return false diff --git a/src/api/types.ts b/src/api/types.ts index f75edad..8cbabc0 100644 --- a/src/api/types.ts +++ b/src/api/types.ts @@ -27,6 +27,7 @@ export interface Goal { accountId: string transactionIds: string[] tagIds: string[] + icon: string | null } export interface Tag { diff --git a/src/ui/components/EmojiPicker.tsx b/src/ui/components/EmojiPicker.tsx index 00bb54d..a8f65e3 100644 --- a/src/ui/components/EmojiPicker.tsx +++ b/src/ui/components/EmojiPicker.tsx @@ -1,9 +1,12 @@ +import React from 'react' import { BaseEmoji, Picker } from 'emoji-mart' import 'emoji-mart/css/emoji-mart.css' import { useAppSelector } from '../../store/hooks' import { selectMode } from '../../store/themeSlice' -type Props = { onClick: (emoji: BaseEmoji, event: React.MouseEvent) => void } +type Props = { + onClick: (emoji: BaseEmoji, event: React.MouseEvent) => void +} export default function EmojiPicker(props: Props) { const theme = useAppSelector(selectMode) @@ -17,4 +20,4 @@ export default function EmojiPicker(props: Props) { color="primary" /> ) -} +} \ No newline at end of file diff --git a/src/ui/features/goalmanager/GoalManager.tsx b/src/ui/features/goalmanager/GoalManager.tsx index 0779dda..b4fe757 100644 --- a/src/ui/features/goalmanager/GoalManager.tsx +++ b/src/ui/features/goalmanager/GoalManager.tsx @@ -2,6 +2,7 @@ import { faCalendarAlt } from '@fortawesome/free-regular-svg-icons' import { faDollarSign, IconDefinition } from '@fortawesome/free-solid-svg-icons' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { MaterialUiPickersDate } from '@material-ui/pickers/typings/date' +import { BaseEmoji } from 'emoji-mart' import 'date-fns' import React, { useEffect, useState } from 'react' import styled from 'styled-components' @@ -10,9 +11,16 @@ 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 AddIconButton from './AddIconButton' +import GoalIcon from './GoalIcon' + + + type Props = { goal: Goal } + export function GoalManager(props: Props) { const dispatch = useAppDispatch() @@ -21,16 +29,20 @@ export function GoalManager(props: Props) { const [name, setName] = useState(null) const [targetDate, setTargetDate] = useState(null) const [targetAmount, setTargetAmount] = useState(null) + const [icon, setIcon] = useState(null) + const [emojiPickerIsOpen, setEmojiPickerIsOpen] = useState(false) useEffect(() => { setName(props.goal.name) setTargetDate(props.goal.targetDate) setTargetAmount(props.goal.targetAmount) + setIcon(props.goal.icon) }, [ props.goal.id, props.goal.name, props.goal.targetDate, props.goal.targetAmount, + props.goal.icon, ]) useEffect(() => { @@ -39,24 +51,32 @@ 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) + //updateGoalApi(props.goal.id, updatedGoal) } - const updateTargetAmountOnChange = (event: React.ChangeEvent) => { + 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,19 +84,72 @@ 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 hasIcon = () => icon != null + + const addIconOnClick = (event: React.MouseEvent) => { + event.stopPropagation() + setEmojiPickerIsOpen(true) +} + + 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 ( + + + + + + + + + event.stopPropagation()} + > + + + @@ -89,7 +162,10 @@ export function GoalManager(props: Props) { - + @@ -103,7 +179,9 @@ export function GoalManager(props: Props) { - {new Date(props.goal.created).toLocaleDateString()} + + {new Date(props.goal.created).toLocaleDateString()} + @@ -139,6 +217,7 @@ const Group = styled.div` margin-top: 1.25rem; margin-bottom: 1.25rem; ` + const NameInput = styled.input` display: flex; background-color: transparent; @@ -155,6 +234,7 @@ const FieldName = styled.h1` color: rgba(174, 174, 174, 1); font-weight: normal; ` + const FieldContainer = styled.div` display: flex; flex-direction: row; @@ -165,10 +245,12 @@ const FieldContainer = styled.div` color: rgba(174, 174, 174, 1); } ` + const StringValue = styled.h1` font-size: 1.8rem; font-weight: bold; ` + const StringInput = styled.input` display: flex; background-color: transparent; @@ -182,3 +264,18 @@ const StringInput = styled.input` const Value = styled.div` margin-left: 2rem; ` + +const EmojiPickerContainer = styled.div` + display: ${(props) => (props.isOpen ? 'flex' : 'none')}; + position: absolute; + top: ${(props) => (props.hasIcon ? '10rem' : '2rem')}; + left: 0; +` + +const AddIconButtonContainer = styled.div` + display: ${(props) => (props.shouldShow ? 'none' : 'flex')}; +` + +const GoalIconContainer = styled.div` + display: ${(props) => (props.shouldShow ? 'flex' : 'none')}; +` \ 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..06c6d42 100644 --- a/src/ui/pages/Main/goals/GoalCard.tsx +++ b/src/ui/pages/Main/goals/GoalCard.tsx @@ -23,10 +23,12 @@ export default function GoalCard(props: Props) { dispatch(setIsOpenRedux(true)) } - const asLocaleDateString = (date: Date) => new Date(date).toLocaleDateString() + const asLocaleDateString = (date: Date) => + new Date(date).toLocaleDateString() return ( + {goal.icon} ${goal.targetAmount} {asLocaleDateString(goal.targetDate)} @@ -46,6 +48,11 @@ const Container = styled(Card)` align-items: center; ` + +const Icon = styled.h1` + font-size: 5.5rem; +` + const TargetAmount = styled.h2` font-size: 2rem; ` @@ -53,4 +60,4 @@ const TargetAmount = styled.h2` const TargetDate = styled.h4` color: rgba(174, 174, 174, 1); font-size: 1rem; -` +` \ No newline at end of file