From ce094b446103865f236b905051160886e9229b60 Mon Sep 17 00:00:00 2001 From: AdinathBorse Date: Sat, 1 Aug 2026 16:36:14 +0530 Subject: [PATCH] Support icons --- GoalControllerTests.cs | 91 +++++++++++++++++++++ src/api/types.ts | 1 + src/ui/features/goalmanager/GoalManager.tsx | 62 +++++++++++++- src/ui/pages/Main/goals/GoalCard.tsx | 5 ++ 4 files changed, 158 insertions(+), 1 deletion(-) create mode 100644 GoalControllerTests.cs diff --git a/GoalControllerTests.cs b/GoalControllerTests.cs new file mode 100644 index 00000000..8ceabe9b --- /dev/null +++ b/GoalControllerTests.cs @@ -0,0 +1,91 @@ +using CommBank.Controllers; +using CommBank.Services; +using CommBank.Models; +using CommBank.Tests.Fake; +using Microsoft.AspNetCore.Mvc; + +namespace CommBank.Tests; + +public class GoalControllerTests +{ + private readonly FakeCollections collections; + + public GoalControllerTests() + { + collections = new(); + } + + [Fact] + public async void GetAll() + { + // 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.Get(); + + // Assert + var index = 0; + foreach (Goal goal in result) + { + Assert.IsAssignableFrom(goal); + Assert.Equal(goals[index].Id, goal.Id); + Assert.Equal(goals[index].Name, goal.Name); + index++; + } + } + + [Fact] + public async void Get() + { + // 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.Get(goals[0].Id!); + + // Assert + Assert.IsAssignableFrom(result.Value); + Assert.Equal(goals[0], result.Value); + Assert.NotEqual(goals[1], result.Value); + } + + [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++; + } + } +} diff --git a/src/api/types.ts b/src/api/types.ts index f75edad0..8cbabc05 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/features/goalmanager/GoalManager.tsx b/src/ui/features/goalmanager/GoalManager.tsx index 0779ddaf..943104bc 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,7 +11,10 @@ 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) { @@ -21,6 +25,8 @@ 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) @@ -37,6 +43,12 @@ export function GoalManager(props: Props) { setName(goal.name) }, [goal.name]) + useEffect(() => { + setIcon(props.goal.icon) + }, [props.goal.id, props.goal.icon]) + + const hasIcon = () => icon != null + const updateNameOnChange = (event: React.ChangeEvent) => { const nextName = event.target.value setName(nextName) @@ -75,10 +87,48 @@ export function GoalManager(props: Props) { } } + 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()} + > + + + @@ -111,7 +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 } @@ -182,3 +231,14 @@ const StringInput = styled.input` const Value = styled.div` margin-left: 2rem; ` + +const GoalIconContainer = styled.div` + display: ${(props) => (props.shouldShow ? 'flex' : 'none')}; +` + +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 e8f6d0a1..c56e3784 100644 --- a/src/ui/pages/Main/goals/GoalCard.tsx +++ b/src/ui/pages/Main/goals/GoalCard.tsx @@ -29,6 +29,7 @@ export default function GoalCard(props: Props) { ${goal.targetAmount} {asLocaleDateString(goal.targetDate)} + {goal.icon} ) } @@ -54,3 +55,7 @@ const TargetDate = styled.h4` color: rgba(174, 174, 174, 1); font-size: 1rem; ` + +const Icon = styled.h1` + font-size: 5.5rem; +`