From 36402a1b953fed526c40f4fddbe6914c34550855 Mon Sep 17 00:00:00 2001 From: Saniya1976 Date: Sat, 28 Mar 2026 17:20:33 +0530 Subject: [PATCH 1/2] feat: add emoji icon support to goals and optimize transaction tag fetching and rendering --- src/api/types.ts | 1 + src/store/goalsSlice.ts | 4 +- src/ui/features/goalmanager/GoalManager.tsx | 144 +++++++++++++++++- src/ui/pages/Main/goals/GoalCard.tsx | 6 + .../Main/transactions/TransactionItem.tsx | 20 +-- .../Main/transactions/TransactionsContent.tsx | 4 +- 6 files changed, 158 insertions(+), 21 deletions(-) diff --git a/src/api/types.ts b/src/api/types.ts index f75edad..6193f2a 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 } export interface Tag { diff --git a/src/store/goalsSlice.ts b/src/store/goalsSlice.ts index 1ed0276..6e74270 100644 --- a/src/store/goalsSlice.ts +++ b/src/store/goalsSlice.ts @@ -22,7 +22,9 @@ export const goalsSlice = createSlice({ reducers: { createGoal: (state, action: PayloadAction) => { state.map[action.payload.id] = action.payload - state.list.push(action.payload.id) + if (!state.list.includes(action.payload.id)) { + state.list.push(action.payload.id) + } }, updateGoal: (state, action: PayloadAction) => { diff --git a/src/ui/features/goalmanager/GoalManager.tsx b/src/ui/features/goalmanager/GoalManager.tsx index 0779dda..2694786 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, IconDefinition, faPlus } from '@fortawesome/free-solid-svg-icons' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { MaterialUiPickersDate } from '@material-ui/pickers/typings/date' import 'date-fns' @@ -10,7 +10,9 @@ 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' type Props = { goal: Goal } export function GoalManager(props: Props) { @@ -21,16 +23,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 [isPickerOpen, setIsPickerOpen] = useState(false) useEffect(() => { setName(props.goal.name) setTargetDate(props.goal.targetDate) setTargetAmount(props.goal.targetAmount) + setIcon(props.goal.icon ?? null) }, [ props.goal.id, props.goal.name, props.goal.targetDate, props.goal.targetAmount, + props.goal.icon, ]) useEffect(() => { @@ -69,15 +75,59 @@ export function GoalManager(props: Props) { name: name ?? props.goal.name, targetDate: date ?? props.goal.targetDate, targetAmount: targetAmount ?? props.goal.targetAmount, + icon: icon ?? props.goal.icon, } dispatch(updateGoalRedux(updatedGoal)) updateGoalApi(props.goal.id, updatedGoal) } } + const updateIcon = (nextIcon: string) => { + setIcon(nextIcon) + const updatedGoal: Goal = { + ...props.goal, + name: name ?? props.goal.name, + targetDate: targetDate ?? props.goal.targetDate, + targetAmount: targetAmount ?? props.goal.targetAmount, + icon: nextIcon, + } + dispatch(updateGoalRedux(updatedGoal)) + updateGoalApi(props.goal.id, updatedGoal) + } + return ( - + + setIsPickerOpen(!isPickerOpen)}> + {icon ? ( + <> + {icon} + { + e.stopPropagation(); + updateIcon(""); + }}>× + + ) : ( + + + Add Icon + + )} + + + + {isPickerOpen && ( + <> + setIsPickerOpen(false)} /> + + { + updateIcon(emoji.native) + setIsPickerOpen(false) + }} /> + + + )} + @@ -111,9 +161,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) => ( @@ -139,6 +186,93 @@ const Group = styled.div` margin-top: 1.25rem; margin-bottom: 1.25rem; ` + +const TopContainer = styled.div` + display: flex; + flex-direction: row; + align-items: center; + margin-bottom: 2rem; +` + +const IconContainer = styled.div` + display: flex; + justify-content: center; + align-items: center; + width: 6rem; + height: 6rem; + background-color: rgba(174, 174, 174, 0.1); + border-radius: 1.5rem; + margin-right: 2rem; + cursor: pointer; + transition: background-color 0.2s ease; + position: relative; + + &:hover { + background-color: rgba(174, 174, 174, 0.2); + } +` + +const RemoveIcon = styled.div` + position: absolute; + top: -0.5rem; + right: -0.5rem; + background-color: ${({ theme }) => theme.alertColor}; + color: white; + width: 1.5rem; + height: 1.5rem; + border-radius: 50%; + display: flex; + justify-content: center; + align-items: center; + font-size: 1.2rem; + font-weight: bold; + cursor: pointer; + box-shadow: var(--shadow-md); + + &:hover { + transform: scale(1.1); + } +` + +const IconText = styled.span` + font-size: 3rem; +` + +const AddIconContainer = styled.div` + display: flex; + flex-direction: column; + align-items: center; + color: rgba(174, 174, 174, 1); + + svg { + font-size: 1.5rem; + margin-bottom: 0.2rem; + } +` + +const AddIconText = styled.span` + font-size: 0.8rem; + font-weight: bold; +` + +const EmojiPickerContainer = styled.div` + position: absolute; + top: 8rem; + left: 0; + z-index: 1001; + box-shadow: var(--shadow-lg); + border-radius: 1rem; + overflow: hidden; +` + +const Backdrop = styled.div` + position: fixed; + top: 0; + left: 0; + width: 100vw; + height: 100vh; + z-index: 1000; +` const NameInput = styled.input` display: flex; background-color: transparent; diff --git a/src/ui/pages/Main/goals/GoalCard.tsx b/src/ui/pages/Main/goals/GoalCard.tsx index e8f6d0a..fb2908e 100644 --- a/src/ui/pages/Main/goals/GoalCard.tsx +++ b/src/ui/pages/Main/goals/GoalCard.tsx @@ -27,6 +27,7 @@ export default function GoalCard(props: Props) { return ( + {goal.icon && {goal.icon}} ${goal.targetAmount} {asLocaleDateString(goal.targetDate)} @@ -54,3 +55,8 @@ const TargetDate = styled.h4` color: rgba(174, 174, 174, 1); font-size: 1rem; ` + +const Icon = styled.span` + font-size: 3rem; + margin-bottom: 0.5rem; +` diff --git a/src/ui/pages/Main/transactions/TransactionItem.tsx b/src/ui/pages/Main/transactions/TransactionItem.tsx index f8d4cb3..0332a93 100644 --- a/src/ui/pages/Main/transactions/TransactionItem.tsx +++ b/src/ui/pages/Main/transactions/TransactionItem.tsx @@ -17,17 +17,12 @@ export function TransactionItem(props: Props) { } async function fetchAll() { - const tags: Tag[] = [] - for (const tagId of props.transaction.tagIds) { - const tag = await fetch(tagId) - tags.push(tag) - } - - setTags(tags) + const fetchedTags = await Promise.all(props.transaction.tagIds.map(fetch)) + setTags(fetchedTags) } fetchAll() - }) + }, [props.transaction.id, props.transaction.tagIds]) return ( @@ -40,11 +35,10 @@ export function TransactionItem(props: Props) { props.transaction.dateTime, ).toLocaleDateString()}`} -
{`${ - props.transaction.transactionType === 'Credit' - ? `$${props.transaction.amount}` - : `-$${props.transaction.amount}` - }`}
+
{`${props.transaction.transactionType === 'Credit' + ? `$${props.transaction.amount}` + : `-$${props.transaction.amount}` + }`}
diff --git a/src/ui/pages/Main/transactions/TransactionsContent.tsx b/src/ui/pages/Main/transactions/TransactionsContent.tsx index d898517..851f69f 100644 --- a/src/ui/pages/Main/transactions/TransactionsContent.tsx +++ b/src/ui/pages/Main/transactions/TransactionsContent.tsx @@ -8,8 +8,8 @@ export default function TransactionsContent(props: Props) { if (!props.transactions) return null return ( <> - {props.transactions.sort(sortByDateDesc).map((transaction) => ( - + {[...props.transactions].sort(sortByDateDesc).map((transaction) => ( + ))} ) From 028da202862c08ef9d44a4ebc20f9b75e3acc15a Mon Sep 17 00:00:00 2001 From: Saniya1976 Date: Sun, 29 Mar 2026 11:01:34 +0530 Subject: [PATCH 2/2] feat: add update API methods for transactions, users, tags, and accounts, and refactor GoalManager emoji picker handler --- src/api/lib.ts | 41 ++++++++++++++++++++- src/ui/features/goalmanager/GoalManager.tsx | 12 ++++-- 2 files changed, 48 insertions(+), 5 deletions(-) diff --git a/src/api/lib.ts b/src/api/lib.ts index 3c593ca..a446ba5 100644 --- a/src/api/lib.ts +++ b/src/api/lib.ts @@ -1,6 +1,6 @@ import axios from 'axios' import { user } from '../data/user' -import { Goal, Transaction, User } from './types' +import { Account, Goal, Tag, Transaction, User } from './types' export const API_ROOT = 'https://fencer-commbank.azurewebsites.net' @@ -51,3 +51,42 @@ export async function updateGoal(goalId: string, updatedGoal: Goal): Promise { + try { + await axios.put(`${API_ROOT}/api/Transaction/${transactionId}`, updatedTransaction) + return true + } catch (error: any) { + return false + } +} + +export async function updateUser(userId: string, updatedUser: User): Promise { + try { + await axios.put(`${API_ROOT}/api/User/${userId}`, updatedUser) + return true + } catch (error: any) { + return false + } +} + +export async function updateTag(tagId: string, updatedTag: Tag): Promise { + try { + await axios.put(`${API_ROOT}/api/Tag/${tagId}`, updatedTag) + return true + } catch (error: any) { + return false + } +} + +export async function updateAccount(accountId: string, updatedAccount: Account): Promise { + try { + await axios.put(`${API_ROOT}/api/Account/${accountId}`, updatedAccount) + return true + } catch (error: any) { + return false + } +} + + + diff --git a/src/ui/features/goalmanager/GoalManager.tsx b/src/ui/features/goalmanager/GoalManager.tsx index 2694786..f2035c9 100644 --- a/src/ui/features/goalmanager/GoalManager.tsx +++ b/src/ui/features/goalmanager/GoalManager.tsx @@ -95,6 +95,12 @@ export function GoalManager(props: Props) { updateGoalApi(props.goal.id, updatedGoal) } + const pickEmojiOnClick = (emoji: BaseEmoji, event: React.MouseEvent) => { + updateIcon(emoji.native) + setIsPickerOpen(false) + } + + return ( @@ -120,10 +126,7 @@ export function GoalManager(props: Props) { <> setIsPickerOpen(false)} /> - { - updateIcon(emoji.native) - setIsPickerOpen(false) - }} /> + )} @@ -160,6 +163,7 @@ export function GoalManager(props: Props) { ) } + type FieldProps = { name: string; icon: IconDefinition } const Field = (props: FieldProps) => (