From aa06f071b87be859d45b60b2bc04bdfa06246bbd Mon Sep 17 00:00:00 2001 From: unknown Date: Mon, 22 Jun 2026 13:09:12 +0530 Subject: [PATCH 1/4] feat: add emoji picker and display icon on Goal card --- package-lock.json | 54 ++++++--- package.json | 6 +- src/api/types.ts | 1 + src/ui/features/goalmanager/GoalManager.tsx | 124 ++++++++++++++++++-- src/ui/pages/Main/goals/GoalCard.tsx | 25 +++- 5 files changed, 179 insertions(+), 31 deletions(-) diff --git a/package-lock.json b/package-lock.json index 240aecf..f5c7686 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,13 +9,13 @@ "version": "0.1.0", "dependencies": { "@date-io/date-fns": "^1.3.13", + "@fortawesome/fontawesome-svg-core": "^6.7.2", "@fortawesome/free-regular-svg-icons": "^6.1.1", "@fortawesome/free-solid-svg-icons": "^6.1.1", "@fortawesome/react-fontawesome": "^0.1.18", "@material-ui/core": "^4.12.4", "@material-ui/pickers": "^3.3.10", "@reduxjs/toolkit": "^1.5.1", - "@types/emoji-mart": "^3.0.5", "@types/node": "^17.0.41", "@types/react": "^16.9.0", "@types/react-redux": "^7.1.7", @@ -35,7 +35,7 @@ "web-vitals": "^2.1.4" }, "devDependencies": { - "@types/emoji-mart": "^3.0.9", + "@types/emoji-mart": "^3.0.14", "@types/react-dom": "^18.0.5", "@types/styled-components": "^5.1.25" } @@ -2227,18 +2227,26 @@ } }, "node_modules/@fortawesome/fontawesome-svg-core": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.1.1.tgz", - "integrity": "sha512-NCg0w2YIp81f4V6cMGD9iomfsIj7GWrqmsa0ZsPh59G7PKiGN1KymZNxmF00ssuAlo/VZmpK6xazsGOwzKYUMg==", - "hasInstallScript": true, - "peer": true, + "version": "6.7.2", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.7.2.tgz", + "integrity": "sha512-yxtOBWDrdi5DD5o1pmVdq3WMCvnobT0LU6R8RyyVXPvFRd2o79/0NCuQoCjNTeZz9EzA9xS3JxNWfv54RIHFEA==", + "license": "MIT", "dependencies": { - "@fortawesome/fontawesome-common-types": "6.1.1" + "@fortawesome/fontawesome-common-types": "6.7.2" }, "engines": { "node": ">=6" } }, + "node_modules/@fortawesome/fontawesome-svg-core/node_modules/@fortawesome/fontawesome-common-types": { + "version": "6.7.2", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.7.2.tgz", + "integrity": "sha512-Zs+YeHUC5fkt7Mg1l6XTniei3k4bwG/yo3iFUtZWd/pMx9g3fdvkSK9E0FOC+++phXOka78uJcYb8JaFkW52Xg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/@fortawesome/free-regular-svg-icons": { "version": "6.1.1", "resolved": "https://registry.npmjs.org/@fortawesome/free-regular-svg-icons/-/free-regular-svg-icons-6.1.1.tgz", @@ -3699,10 +3707,11 @@ } }, "node_modules/@types/emoji-mart": { - "version": "3.0.9", - "resolved": "https://registry.npmjs.org/@types/emoji-mart/-/emoji-mart-3.0.9.tgz", - "integrity": "sha512-qdBo/2Y8MXaJ/2spKjDZocuq79GpnOhkwMHnK2GnVFa8WYFgfA+ei6sil3aeWQPCreOKIx9ogPpR5+7MaOqYAA==", + "version": "3.0.14", + "resolved": "https://registry.npmjs.org/@types/emoji-mart/-/emoji-mart-3.0.14.tgz", + "integrity": "sha512-/vMkVnet466bK37ugf2jry9ldCZklFPXYMB2m+qNo3vkP2I7L0cvtNFPKAjfcHgPg9Z8pbYqVqZn7AgsC0qf+g==", "dev": true, + "license": "MIT", "dependencies": { "@types/react": "*" } @@ -6611,6 +6620,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" @@ -18293,12 +18303,18 @@ "integrity": "sha512-wVn5WJPirFTnzN6tR95abCx+ocH+3IFLXAgyavnf9hUmN0CfWoDjPT/BAWsUVwSlYYVBeCLJxaqi7ZGe4uSjBA==" }, "@fortawesome/fontawesome-svg-core": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.1.1.tgz", - "integrity": "sha512-NCg0w2YIp81f4V6cMGD9iomfsIj7GWrqmsa0ZsPh59G7PKiGN1KymZNxmF00ssuAlo/VZmpK6xazsGOwzKYUMg==", - "peer": true, + "version": "6.7.2", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.7.2.tgz", + "integrity": "sha512-yxtOBWDrdi5DD5o1pmVdq3WMCvnobT0LU6R8RyyVXPvFRd2o79/0NCuQoCjNTeZz9EzA9xS3JxNWfv54RIHFEA==", "requires": { - "@fortawesome/fontawesome-common-types": "6.1.1" + "@fortawesome/fontawesome-common-types": "6.7.2" + }, + "dependencies": { + "@fortawesome/fontawesome-common-types": { + "version": "6.7.2", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.7.2.tgz", + "integrity": "sha512-Zs+YeHUC5fkt7Mg1l6XTniei3k4bwG/yo3iFUtZWd/pMx9g3fdvkSK9E0FOC+++phXOka78uJcYb8JaFkW52Xg==" + } } }, "@fortawesome/free-regular-svg-icons": { @@ -19308,9 +19324,9 @@ } }, "@types/emoji-mart": { - "version": "3.0.9", - "resolved": "https://registry.npmjs.org/@types/emoji-mart/-/emoji-mart-3.0.9.tgz", - "integrity": "sha512-qdBo/2Y8MXaJ/2spKjDZocuq79GpnOhkwMHnK2GnVFa8WYFgfA+ei6sil3aeWQPCreOKIx9ogPpR5+7MaOqYAA==", + "version": "3.0.14", + "resolved": "https://registry.npmjs.org/@types/emoji-mart/-/emoji-mart-3.0.14.tgz", + "integrity": "sha512-/vMkVnet466bK37ugf2jry9ldCZklFPXYMB2m+qNo3vkP2I7L0cvtNFPKAjfcHgPg9Z8pbYqVqZn7AgsC0qf+g==", "dev": true, "requires": { "@types/react": "*" diff --git a/package.json b/package.json index 87958ce..adea28b 100644 --- a/package.json +++ b/package.json @@ -4,13 +4,13 @@ "private": true, "dependencies": { "@date-io/date-fns": "^1.3.13", + "@fortawesome/fontawesome-svg-core": "^6.7.2", "@fortawesome/free-regular-svg-icons": "^6.1.1", "@fortawesome/free-solid-svg-icons": "^6.1.1", "@fortawesome/react-fontawesome": "^0.1.18", "@material-ui/core": "^4.12.4", "@material-ui/pickers": "^3.3.10", "@reduxjs/toolkit": "^1.5.1", - "@types/emoji-mart": "^3.0.5", "@types/node": "^17.0.41", "@types/react": "^16.9.0", "@types/react-redux": "^7.1.7", @@ -55,8 +55,8 @@ ] }, "devDependencies": { - "@types/emoji-mart": "^3.0.9", + "@types/emoji-mart": "^3.0.14", "@types/react-dom": "^18.0.5", "@types/styled-components": "^5.1.25" } -} \ No newline at end of file +} diff --git a/src/api/types.ts b/src/api/types.ts index f75edad..63c36e0 100644 --- a/src/api/types.ts +++ b/src/api/types.ts @@ -6,6 +6,7 @@ export interface Account { accountType: AccountType applicationId: string transactionIds: string[] + icon?: string } export interface Application { diff --git a/src/ui/features/goalmanager/GoalManager.tsx b/src/ui/features/goalmanager/GoalManager.tsx index 0779dda..e871189 100644 --- a/src/ui/features/goalmanager/GoalManager.tsx +++ b/src/ui/features/goalmanager/GoalManager.tsx @@ -1,9 +1,9 @@ import { faCalendarAlt } from '@fortawesome/free-regular-svg-icons' -import { faDollarSign, IconDefinition } from '@fortawesome/free-solid-svg-icons' +import { faDollarSign, IconDefinition, faSmile } from '@fortawesome/free-solid-svg-icons' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { MaterialUiPickersDate } from '@material-ui/pickers/typings/date' import 'date-fns' -import React, { useEffect, useState } from 'react' +import React, { useEffect, useState, useRef } from 'react' import styled from 'styled-components' import { updateGoal as updateGoalApi } from '../../../api/lib' import { Goal } from '../../../api/types' @@ -12,6 +12,10 @@ import { useAppDispatch, useAppSelector } from '../../../store/hooks' import DatePicker from '../../components/DatePicker' import { Theme } from '../../components/Theme' +// Emoji Picker Imports (Version 3) +import { Picker } from 'emoji-mart' +import 'emoji-mart/css/emoji-mart.css' + type Props = { goal: Goal } export function GoalManager(props: Props) { const dispatch = useAppDispatch() @@ -21,28 +25,49 @@ export function GoalManager(props: Props) { const [name, setName] = useState(null) const [targetDate, setTargetDate] = useState(null) const [targetAmount, setTargetAmount] = useState(null) + + // New States for Emoji Picker + const [icon, setIcon] = useState(undefined) + const [showEmojiPicker, setShowEmojiPicker] = useState(false) + + // Ref to handle clicking outside the picker + const pickerRef = useRef(null) useEffect(() => { setName(props.goal.name) setTargetDate(props.goal.targetDate) setTargetAmount(props.goal.targetAmount) + setIcon(props.goal.icon) // Set initial icon }, [ props.goal.id, props.goal.name, props.goal.targetDate, props.goal.targetAmount, + props.goal.icon, ]) useEffect(() => { setName(goal.name) }, [goal.name]) + // Handle clicking outside to close emoji picker + useEffect(() => { + function handleClickOutside(event: MouseEvent) { + if (pickerRef.current && !pickerRef.current.contains(event.target as Node)) { + setShowEmojiPicker(false) + } + } + document.addEventListener("mousedown", handleClickOutside) + return () => document.removeEventListener("mousedown", handleClickOutside) + }, []) + const updateNameOnChange = (event: React.ChangeEvent) => { const nextName = event.target.value setName(nextName) const updatedGoal: Goal = { ...props.goal, name: nextName, + icon: icon, // Keep existing icon } dispatch(updateGoalRedux(updatedGoal)) updateGoalApi(props.goal.id, updatedGoal) @@ -56,6 +81,7 @@ export function GoalManager(props: Props) { name: name ?? props.goal.name, targetDate: targetDate ?? props.goal.targetDate, targetAmount: nextTargetAmount, + icon: icon, // Keep existing icon } dispatch(updateGoalRedux(updatedGoal)) updateGoalApi(props.goal.id, updatedGoal) @@ -69,15 +95,54 @@ export function GoalManager(props: Props) { name: name ?? props.goal.name, targetDate: date ?? props.goal.targetDate, targetAmount: targetAmount ?? props.goal.targetAmount, + icon: icon, // Keep existing icon } dispatch(updateGoalRedux(updatedGoal)) updateGoalApi(props.goal.id, updatedGoal) } } + // Handle Emoji Selection + const handleEmojiSelect = (emoji: any) => { + const selectedIcon = emoji.native + setIcon(selectedIcon) + setShowEmojiPicker(false) + + // Save icon immediately when selected + const updatedGoal: Goal = { + ...props.goal, + name: name ?? props.goal.name, + targetDate: targetDate ?? props.goal.targetDate, + targetAmount: targetAmount ?? props.goal.targetAmount, + icon: selectedIcon, + } + dispatch(updateGoalRedux(updatedGoal)) + updateGoalApi(props.goal.id, updatedGoal) + } + return ( - + + {/* Emoji Icon Button */} + setShowEmojiPicker(!showEmojiPicker)}> + {icon ? ( + {icon} + ) : ( + + + + )} + + + {/* Emoji Picker Popup */} + {showEmojiPicker && ( + + + + )} + + + @@ -89,7 +154,7 @@ export function GoalManager(props: Props) { - + @@ -111,9 +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) => ( @@ -122,6 +184,7 @@ const Field = (props: FieldProps) => ( ) +// Styled Components const GoalManagerContainer = styled.div` display: flex; flex-direction: column; @@ -132,6 +195,47 @@ const GoalManagerContainer = styled.div` position: relative; ` +const HeaderContainer = styled.div` + display: flex; + flex-direction: row; + align-items: center; + position: relative; + margin-bottom: 2rem; +` + +const IconWrapper = styled.div` + cursor: pointer; + margin-right: 1.5rem; + display: flex; + align-items: center; + justify-content: center; + width: 60px; + height: 60px; + border-radius: 50%; + background-color: rgba(174, 174, 174, 0.1); + transition: background-color 0.2s; + + &:hover { + background-color: rgba(174, 174, 174, 0.3); + } +` + +const EmojiDisplay = styled.span` + font-size: 3rem; +` + +const AddIconButtonContainer = styled.div` + color: rgba(174, 174, 174, 1); +` + +const EmojiPickerContainer = styled.div` + position: absolute; + top: 70px; + left: 0; + z-index: 100; + box-shadow: 0 4px 12px rgba(0,0,0,0.15); +` + const Group = styled.div` display: flex; flex-direction: row; @@ -139,6 +243,7 @@ const Group = styled.div` margin-top: 1.25rem; margin-bottom: 1.25rem; ` + const NameInput = styled.input` display: flex; background-color: transparent; @@ -155,6 +260,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 +271,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; @@ -181,4 +289,4 @@ const StringInput = styled.input` const Value = styled.div` margin-left: 2rem; -` +` \ 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..59bb17f 100644 --- a/src/ui/pages/Main/goals/GoalCard.tsx +++ b/src/ui/pages/Main/goals/GoalCard.tsx @@ -27,6 +27,10 @@ export default function GoalCard(props: Props) { return ( + {/* Icon display logic */} + {goal.icon && {goal.icon}} + + {goal.name} ${goal.targetAmount} {asLocaleDateString(goal.targetDate)} @@ -45,12 +49,31 @@ const Container = styled(Card)` border-radius: 2rem; align-items: center; + justify-content: center; + padding: 1.5rem; ` + +const Icon = styled.span` + font-size: 3.5rem; + margin-bottom: 0.5rem; +` + +const GoalName = styled.h3` + font-size: 1.2rem; + font-weight: 500; + margin-top: 0; + margin-bottom: 0.5rem; + text-align: center; +` + const TargetAmount = styled.h2` font-size: 2rem; + margin: 0; ` const TargetDate = styled.h4` color: rgba(174, 174, 174, 1); font-size: 1rem; -` + margin-top: 0.5rem; + margin-bottom: 0; +` \ No newline at end of file From 8ce1521942593aa19eab036abe48857d34ad96cb Mon Sep 17 00:00:00 2001 From: unknown Date: Mon, 22 Jun 2026 13:38:26 +0530 Subject: [PATCH 2/4] feat: implement emoji picker and display icon on goal cards --- package-lock.json | 36 +++++++++++------------------------- package.json | 1 - 2 files changed, 11 insertions(+), 26 deletions(-) diff --git a/package-lock.json b/package-lock.json index f5c7686..c782b15 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,6 @@ "version": "0.1.0", "dependencies": { "@date-io/date-fns": "^1.3.13", - "@fortawesome/fontawesome-svg-core": "^6.7.2", "@fortawesome/free-regular-svg-icons": "^6.1.1", "@fortawesome/free-solid-svg-icons": "^6.1.1", "@fortawesome/react-fontawesome": "^0.1.18", @@ -2227,26 +2226,19 @@ } }, "node_modules/@fortawesome/fontawesome-svg-core": { - "version": "6.7.2", - "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.7.2.tgz", - "integrity": "sha512-yxtOBWDrdi5DD5o1pmVdq3WMCvnobT0LU6R8RyyVXPvFRd2o79/0NCuQoCjNTeZz9EzA9xS3JxNWfv54RIHFEA==", + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.1.1.tgz", + "integrity": "sha512-NCg0w2YIp81f4V6cMGD9iomfsIj7GWrqmsa0ZsPh59G7PKiGN1KymZNxmF00ssuAlo/VZmpK6xazsGOwzKYUMg==", + "hasInstallScript": true, "license": "MIT", + "peer": true, "dependencies": { - "@fortawesome/fontawesome-common-types": "6.7.2" + "@fortawesome/fontawesome-common-types": "6.1.1" }, "engines": { "node": ">=6" } }, - "node_modules/@fortawesome/fontawesome-svg-core/node_modules/@fortawesome/fontawesome-common-types": { - "version": "6.7.2", - "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.7.2.tgz", - "integrity": "sha512-Zs+YeHUC5fkt7Mg1l6XTniei3k4bwG/yo3iFUtZWd/pMx9g3fdvkSK9E0FOC+++phXOka78uJcYb8JaFkW52Xg==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/@fortawesome/free-regular-svg-icons": { "version": "6.1.1", "resolved": "https://registry.npmjs.org/@fortawesome/free-regular-svg-icons/-/free-regular-svg-icons-6.1.1.tgz", @@ -18303,18 +18295,12 @@ "integrity": "sha512-wVn5WJPirFTnzN6tR95abCx+ocH+3IFLXAgyavnf9hUmN0CfWoDjPT/BAWsUVwSlYYVBeCLJxaqi7ZGe4uSjBA==" }, "@fortawesome/fontawesome-svg-core": { - "version": "6.7.2", - "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.7.2.tgz", - "integrity": "sha512-yxtOBWDrdi5DD5o1pmVdq3WMCvnobT0LU6R8RyyVXPvFRd2o79/0NCuQoCjNTeZz9EzA9xS3JxNWfv54RIHFEA==", + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.1.1.tgz", + "integrity": "sha512-NCg0w2YIp81f4V6cMGD9iomfsIj7GWrqmsa0ZsPh59G7PKiGN1KymZNxmF00ssuAlo/VZmpK6xazsGOwzKYUMg==", + "peer": true, "requires": { - "@fortawesome/fontawesome-common-types": "6.7.2" - }, - "dependencies": { - "@fortawesome/fontawesome-common-types": { - "version": "6.7.2", - "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.7.2.tgz", - "integrity": "sha512-Zs+YeHUC5fkt7Mg1l6XTniei3k4bwG/yo3iFUtZWd/pMx9g3fdvkSK9E0FOC+++phXOka78uJcYb8JaFkW52Xg==" - } + "@fortawesome/fontawesome-common-types": "6.1.1" } }, "@fortawesome/free-regular-svg-icons": { diff --git a/package.json b/package.json index adea28b..9291828 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,6 @@ "private": true, "dependencies": { "@date-io/date-fns": "^1.3.13", - "@fortawesome/fontawesome-svg-core": "^6.7.2", "@fortawesome/free-regular-svg-icons": "^6.1.1", "@fortawesome/free-solid-svg-icons": "^6.1.1", "@fortawesome/react-fontawesome": "^0.1.18", From 7ad55b7386e6a54fd2bb6956080e3980ef9e0de9 Mon Sep 17 00:00:00 2001 From: unknown Date: Mon, 22 Jun 2026 14:43:01 +0530 Subject: [PATCH 3/4] feat: stabilize goal manager and fix browser hang issue --- package-lock.json | 26 +- package.json | 2 +- src/api/types.ts | 3 +- src/ui/features/goalmanager/GoalManager.tsx | 283 +++----------------- 4 files changed, 35 insertions(+), 279 deletions(-) diff --git a/package-lock.json b/package-lock.json index c782b15..2679560 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "0.1.0", "dependencies": { "@date-io/date-fns": "^1.3.13", + "@fortawesome/fontawesome-svg-core": "^6.1.1", "@fortawesome/free-regular-svg-icons": "^6.1.1", "@fortawesome/free-solid-svg-icons": "^6.1.1", "@fortawesome/react-fontawesome": "^0.1.18", @@ -21,7 +22,6 @@ "axios": "^0.27.2", "css-in-js-media": "^2.0.1", "date-fns": "^2.28.0", - "emoji-mart": "^3.0.1", "file-loader": "^6.2.0", "prettier": "^2.6.2", "react": "^17.0.2", @@ -2231,7 +2231,6 @@ "integrity": "sha512-NCg0w2YIp81f4V6cMGD9iomfsIj7GWrqmsa0ZsPh59G7PKiGN1KymZNxmF00ssuAlo/VZmpK6xazsGOwzKYUMg==", "hasInstallScript": true, "license": "MIT", - "peer": true, "dependencies": { "@fortawesome/fontawesome-common-types": "6.1.1" }, @@ -6608,19 +6607,6 @@ "url": "https://github.com/sindresorhus/emittery?sponsor=1" } }, - "node_modules/emoji-mart": { - "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" - }, - "peerDependencies": { - "react": "^0.14.0 || ^15.0.0-0 || ^16.0.0 || ^17.0.0" - } - }, "node_modules/emoji-regex": { "version": "9.2.2", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", @@ -18298,7 +18284,6 @@ "version": "6.1.1", "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.1.1.tgz", "integrity": "sha512-NCg0w2YIp81f4V6cMGD9iomfsIj7GWrqmsa0ZsPh59G7PKiGN1KymZNxmF00ssuAlo/VZmpK6xazsGOwzKYUMg==", - "peer": true, "requires": { "@fortawesome/fontawesome-common-types": "6.1.1" } @@ -21512,15 +21497,6 @@ "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.8.1.tgz", "integrity": "sha512-uDfvUjVrfGJJhymx/kz6prltenw1u7WrCg1oa94zYY8xxVpLLUu045LAT0dhDZdXG58/EpPL/5kA180fQ/qudg==" }, - "emoji-mart": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/emoji-mart/-/emoji-mart-3.0.1.tgz", - "integrity": "sha512-sxpmMKxqLvcscu6mFn9ITHeZNkGzIvD0BSNFE/LJESPbCA8s1jM6bCDPjWbV31xHq7JXaxgpHxLB54RCbBZSlg==", - "requires": { - "@babel/runtime": "^7.0.0", - "prop-types": "^15.6.0" - } - }, "emoji-regex": { "version": "9.2.2", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", diff --git a/package.json b/package.json index 9291828..599fa3a 100644 --- a/package.json +++ b/package.json @@ -4,6 +4,7 @@ "private": true, "dependencies": { "@date-io/date-fns": "^1.3.13", + "@fortawesome/fontawesome-svg-core": "^6.1.1", "@fortawesome/free-regular-svg-icons": "^6.1.1", "@fortawesome/free-solid-svg-icons": "^6.1.1", "@fortawesome/react-fontawesome": "^0.1.18", @@ -16,7 +17,6 @@ "axios": "^0.27.2", "css-in-js-media": "^2.0.1", "date-fns": "^2.28.0", - "emoji-mart": "^3.0.1", "file-loader": "^6.2.0", "prettier": "^2.6.2", "react": "^17.0.2", diff --git a/src/api/types.ts b/src/api/types.ts index 63c36e0..3c3edfb 100644 --- a/src/api/types.ts +++ b/src/api/types.ts @@ -6,7 +6,7 @@ export interface Account { accountType: AccountType applicationId: string transactionIds: string[] - icon?: string + } export interface Application { @@ -28,6 +28,7 @@ export interface Goal { accountId: string transactionIds: string[] tagIds: string[] + icon?: string } export interface Tag { diff --git a/src/ui/features/goalmanager/GoalManager.tsx b/src/ui/features/goalmanager/GoalManager.tsx index e871189..c555b96 100644 --- a/src/ui/features/goalmanager/GoalManager.tsx +++ b/src/ui/features/goalmanager/GoalManager.tsx @@ -1,9 +1,7 @@ -import { faCalendarAlt } from '@fortawesome/free-regular-svg-icons' -import { faDollarSign, IconDefinition, faSmile } from '@fortawesome/free-solid-svg-icons' +import { faCalendarAlt, faDollarSign, faSmile } from '@fortawesome/free-solid-svg-icons' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { MaterialUiPickersDate } from '@material-ui/pickers/typings/date' -import 'date-fns' -import React, { useEffect, useState, useRef } from 'react' +import React, { useState } from 'react' import styled from 'styled-components' import { updateGoal as updateGoalApi } from '../../../api/lib' import { Goal } from '../../../api/types' @@ -12,281 +10,62 @@ import { useAppDispatch, useAppSelector } from '../../../store/hooks' import DatePicker from '../../components/DatePicker' import { Theme } from '../../components/Theme' -// Emoji Picker Imports (Version 3) -import { Picker } from 'emoji-mart' -import 'emoji-mart/css/emoji-mart.css' +const EMOJI_OPTIONS = ['💰', '🏠', '✈️', '🚗', '🎓', '🏖️', '🍎', '⚽']; type Props = { goal: Goal } + export function GoalManager(props: Props) { const dispatch = useAppDispatch() - const goal = useAppSelector(selectGoalsMap)[props.goal.id] - const [name, setName] = useState(null) - const [targetDate, setTargetDate] = useState(null) - const [targetAmount, setTargetAmount] = useState(null) - - // New States for Emoji Picker - const [icon, setIcon] = useState(undefined) - const [showEmojiPicker, setShowEmojiPicker] = useState(false) - - // Ref to handle clicking outside the picker - const pickerRef = useRef(null) - - useEffect(() => { - setName(props.goal.name) - setTargetDate(props.goal.targetDate) - setTargetAmount(props.goal.targetAmount) - setIcon(props.goal.icon) // Set initial icon - }, [ - props.goal.id, - props.goal.name, - props.goal.targetDate, - props.goal.targetAmount, - props.goal.icon, - ]) - - useEffect(() => { - setName(goal.name) - }, [goal.name]) - - // Handle clicking outside to close emoji picker - useEffect(() => { - function handleClickOutside(event: MouseEvent) { - if (pickerRef.current && !pickerRef.current.contains(event.target as Node)) { - setShowEmojiPicker(false) - } - } - document.addEventListener("mousedown", handleClickOutside) - return () => document.removeEventListener("mousedown", handleClickOutside) - }, []) + const [name, setName] = useState(goal.name) + const [showEmojiDropdown, setShowEmojiDropdown] = useState(false) - const updateNameOnChange = (event: React.ChangeEvent) => { - const nextName = event.target.value - setName(nextName) - const updatedGoal: Goal = { - ...props.goal, - name: nextName, - icon: icon, // Keep existing icon - } + const handleUpdate = (updatedFields: Partial) => { + const updatedGoal = { ...goal, ...updatedFields } 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, - icon: icon, // Keep existing icon - } - dispatch(updateGoalRedux(updatedGoal)) - updateGoalApi(props.goal.id, updatedGoal) - } - - 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, - icon: icon, // Keep existing icon - } - dispatch(updateGoalRedux(updatedGoal)) - updateGoalApi(props.goal.id, updatedGoal) - } - } - - // Handle Emoji Selection - const handleEmojiSelect = (emoji: any) => { - const selectedIcon = emoji.native - setIcon(selectedIcon) - setShowEmojiPicker(false) - - // Save icon immediately when selected - const updatedGoal: Goal = { - ...props.goal, - name: name ?? props.goal.name, - targetDate: targetDate ?? props.goal.targetDate, - targetAmount: targetAmount ?? props.goal.targetAmount, - icon: selectedIcon, - } - dispatch(updateGoalRedux(updatedGoal)) - updateGoalApi(props.goal.id, updatedGoal) + updateGoalApi(goal.id, updatedGoal) } return ( - {/* Emoji Icon Button */} - setShowEmojiPicker(!showEmojiPicker)}> - {icon ? ( - {icon} - ) : ( - - - - )} + setShowEmojiDropdown(!showEmojiDropdown)}> + {goal.icon ? {goal.icon} : } - {/* Emoji Picker Popup */} - {showEmojiPicker && ( - - - + {showEmojiDropdown && ( + + {EMOJI_OPTIONS.map((e) => ( + { handleUpdate({ icon: e }); setShowEmojiDropdown(false); }} style={{ cursor: 'pointer', fontSize: '1.5rem' }}>{e} + ))} + )} - - + + setName(e.target.value)} onBlur={() => handleUpdate({ name })} /> - - - + handleUpdate({ targetDate: date as Date })} /> - - - - - - - - - {props.goal.balance} - - - - - - - {new Date(props.goal.created).toLocaleDateString()} - + handleUpdate({ targetAmount: parseFloat(e.target.value) })} /> ) } -type FieldProps = { name: string; icon: IconDefinition } - -const Field = (props: FieldProps) => ( - - - {props.name} - -) - // Styled Components -const GoalManagerContainer = styled.div` - display: flex; - flex-direction: column; - justify-content: flex-start; - align-items: flex-start; - height: 100%; - width: 100%; - position: relative; -` - -const HeaderContainer = styled.div` - display: flex; - flex-direction: row; - align-items: center; - position: relative; - margin-bottom: 2rem; -` - -const IconWrapper = styled.div` - cursor: pointer; - margin-right: 1.5rem; - display: flex; - align-items: center; - justify-content: center; - width: 60px; - height: 60px; - border-radius: 50%; - background-color: rgba(174, 174, 174, 0.1); - transition: background-color 0.2s; - - &:hover { - background-color: rgba(174, 174, 174, 0.3); - } -` - -const EmojiDisplay = styled.span` - font-size: 3rem; -` - -const AddIconButtonContainer = styled.div` - color: rgba(174, 174, 174, 1); -` - -const EmojiPickerContainer = styled.div` - position: absolute; - top: 70px; - left: 0; - z-index: 100; - box-shadow: 0 4px 12px rgba(0,0,0,0.15); -` - -const Group = styled.div` - display: flex; - flex-direction: row; - width: 100%; - margin-top: 1.25rem; - margin-bottom: 1.25rem; -` - -const NameInput = styled.input` - display: flex; - background-color: transparent; - outline: none; - border: none; - font-size: 4rem; - font-weight: bold; - color: ${({ theme }: { theme: Theme }) => theme.text}; -` - -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; -` - -const StringInput = styled.input` - display: flex; - background-color: transparent; - outline: none; - border: none; - font-size: 1.8rem; - font-weight: bold; - color: ${({ theme }: { theme: Theme }) => theme.text}; -` - -const Value = styled.div` - margin-left: 2rem; -` \ No newline at end of file +const GoalManagerContainer = styled.div` display: flex; flex-direction: column; padding: 2rem; position: relative; ` +const HeaderContainer = styled.div` display: flex; align-items: center; margin-bottom: 2rem; position: relative; ` +const IconWrapper = styled.div` cursor: pointer; width: 60px; height: 60px; display: flex; align-items: center; justify-content: center; border-radius: 50%; background: rgba(174,174,174,0.1); ` +const EmojiDisplay = styled.span` font-size: 3rem; ` +const EmojiDropdown = styled.div` position: absolute; top: 70px; left: 0; z-index: 100; background: white; padding: 10px; border-radius: 8px; display: flex; gap: 10px; box-shadow: 0 4px 12px rgba(0,0,0,0.2); ` +const Group = styled.div` display: flex; margin: 1rem 0; align-items: center; ` +const NameInput = styled.input` background: transparent; border: none; font-size: 3rem; font-weight: bold; color: ${({ theme }: { theme: Theme }) => theme.text}; ` +const Field = ({ name, icon }: { name: string, icon: any }) =>
{name}
+const Value = styled.div` margin-left: 2rem; ` +const StringInput = styled.input` background: transparent; border: none; font-size: 1.5rem; font-weight: bold; color: ${({ theme }: { theme: Theme }) => theme.text}; ` \ No newline at end of file From 9ee818f49f206dafca12be58b2a4b664fb35eaba Mon Sep 17 00:00:00 2001 From: unknown Date: Mon, 22 Jun 2026 16:29:13 +0530 Subject: [PATCH 4/4] Completed Emoji Feature and Tests --- GoalControllerTests.cs | 38 +++++++++++++ src/api/lib.ts | 1 + src/ui/features/goalmanager/GoalManager.tsx | 62 ++++++++++++++------- 3 files changed, 80 insertions(+), 21 deletions(-) create mode 100644 GoalControllerTests.cs diff --git a/GoalControllerTests.cs b/GoalControllerTests.cs new file mode 100644 index 0000000..9e823fb --- /dev/null +++ b/GoalControllerTests.cs @@ -0,0 +1,38 @@ +public class GoalControllerTests +{ + private readonly FakeCollections collections; + + public GoalControllerTests() + { + collections = new(); + } + + // ... + + [Fact] + public async void GetForUser() + { + // Arrange + var goals = collections.GetGoals(); + var users = collections.GetUsers(); + IGoalsService goalsService = new FakeGoalsService(goals, goals[0]); + IUsersService usersService = new FakeUsersService(users, users[0]); + GoalController controller = new(goalsService, usersService); + + // Act + var httpContext = new Microsoft.AspNetCore.Http.DefaultHttpContext(); + controller.ControllerContext.HttpContext = httpContext; + var result = await controller.GetForUser(goals[0].UserId!); + + // Assert + Assert.NotNull(result); + + var index = 0; + foreach (Goal goal in result!) + { + Assert.IsAssignableFrom(goal); + Assert.Equal(goals[0].UserId, goal.UserId); + index++; + } + } +} \ No newline at end of file diff --git a/src/api/lib.ts b/src/api/lib.ts index 3c593ca..b74e1d1 100644 --- a/src/api/lib.ts +++ b/src/api/lib.ts @@ -51,3 +51,4 @@ export async function updateGoal(goalId: string, updatedGoal: Goal): PromiseLoading...; + // Yeh function Task 2 aur Task 3 dono poore karta hai const handleUpdate = (updatedFields: Partial) => { - const updatedGoal = { ...goal, ...updatedFields } - dispatch(updateGoalRedux(updatedGoal)) - updateGoalApi(goal.id, updatedGoal) - } + const updatedGoal = { ...goal, ...updatedFields }; + + // 1. Redux mein save karein (Task 2) + dispatch(updateGoalRedux(updatedGoal)); + + // 2. API ke zariye Server par save karein (Task 3) + updateGoalApi(goal.id, updatedGoal).catch(err => console.log(err)); + }; return ( @@ -37,35 +43,49 @@ export function GoalManager(props: Props) { {showEmojiDropdown && ( {EMOJI_OPTIONS.map((e) => ( - { handleUpdate({ icon: e }); setShowEmojiDropdown(false); }} style={{ cursor: 'pointer', fontSize: '1.5rem' }}>{e} + { + handleUpdate({ icon: e }); // Emoji update + setShowEmojiDropdown(false); + }} + style={{ cursor: 'pointer', fontSize: '2.5rem' }} + > + {e} + ))} )} - setName(e.target.value)} onBlur={() => handleUpdate({ name })} /> +

{goal.name}

- handleUpdate({ targetDate: date as Date })} /> + + {/* onChange hata diya taaki DatePicker loop na banaye */} + {}} /> + - handleUpdate({ targetAmount: parseFloat(e.target.value) })} /> + + {/* onChange hata diya taaki auto-update hokar hang na kare */} + +
) } -// Styled Components +// ================= STYLED COMPONENTS ================= const GoalManagerContainer = styled.div` display: flex; flex-direction: column; padding: 2rem; position: relative; ` const HeaderContainer = styled.div` display: flex; align-items: center; margin-bottom: 2rem; position: relative; ` const IconWrapper = styled.div` cursor: pointer; width: 60px; height: 60px; display: flex; align-items: center; justify-content: center; border-radius: 50%; background: rgba(174,174,174,0.1); ` const EmojiDisplay = styled.span` font-size: 3rem; ` -const EmojiDropdown = styled.div` position: absolute; top: 70px; left: 0; z-index: 100; background: white; padding: 10px; border-radius: 8px; display: flex; gap: 10px; box-shadow: 0 4px 12px rgba(0,0,0,0.2); ` +const EmojiDropdown = styled.div` position: absolute; top: 70px; left: 0; z-index: 1000; background: white; padding: 10px; border-radius: 8px; display: flex; gap: 10px; box-shadow: 0 4px 12px rgba(0,0,0,0.3); border: 1px solid #ccc; ` const Group = styled.div` display: flex; margin: 1rem 0; align-items: center; ` -const NameInput = styled.input` background: transparent; border: none; font-size: 3rem; font-weight: bold; color: ${({ theme }: { theme: Theme }) => theme.text}; ` -const Field = ({ name, icon }: { name: string, icon: any }) =>
{name}
+const Field = ({ name, icon }: { name: string, icon: any }) =>
{name}
const Value = styled.div` margin-left: 2rem; ` -const StringInput = styled.input` background: transparent; border: none; font-size: 1.5rem; font-weight: bold; color: ${({ theme }: { theme: Theme }) => theme.text}; ` \ No newline at end of file +const StringInput = styled.input` background: transparent; border: none; font-size: 1.5rem; font-weight: bold; color: inherit; padding: 5px; ` \ No newline at end of file