From bd93c83cd145659d6a3a64c54f666fbbfa2df609 Mon Sep 17 00:00:00 2001 From: wyt2616 Date: Tue, 28 Oct 2025 13:17:26 +1100 Subject: [PATCH] Support icons --- package-lock.json | 1 + src/api/types.ts | 1 + src/ui/features/goalmanager/GoalManager.tsx | 105 +++++++++++++++++--- src/ui/pages/Main/goals/GoalCard.tsx | 18 ++++ 4 files changed, 112 insertions(+), 13 deletions(-) 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..f0a3b78 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 0779dda..2fa2e90 100644 --- a/src/ui/features/goalmanager/GoalManager.tsx +++ b/src/ui/features/goalmanager/GoalManager.tsx @@ -11,41 +11,54 @@ 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 'emoji-mart/css/emoji-mart.css' +import { Picker } from 'emoji-mart' 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) + const [icon, setIcon] = useState(goal?.icon || '') + const [showPicker, setShowPicker] = 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(() => { setName(goal.name) }, [goal.name]) + const pushUpdate = (updated: Goal) => { + dispatch(updateGoalRedux(updated)) + updateGoalApi(props.goal.id, updated) + } + const updateNameOnChange = (event: React.ChangeEvent) => { const nextName = event.target.value setName(nextName) const updatedGoal: Goal = { ...props.goal, name: nextName, + targetDate: targetDate ?? props.goal.targetDate, + targetAmount: targetAmount ?? props.goal.targetAmount, + icon: icon || undefined, } - dispatch(updateGoalRedux(updatedGoal)) - updateGoalApi(props.goal.id, updatedGoal) + pushUpdate(updatedGoal) } const updateTargetAmountOnChange = (event: React.ChangeEvent) => { @@ -56,28 +69,60 @@ export function GoalManager(props: Props) { name: name ?? props.goal.name, targetDate: targetDate ?? props.goal.targetDate, targetAmount: nextTargetAmount, + icon: icon || undefined, } - dispatch(updateGoalRedux(updatedGoal)) - updateGoalApi(props.goal.id, updatedGoal) + pushUpdate(updatedGoal) } const pickDateOnChange = (date: MaterialUiPickersDate) => { if (date != null) { - setTargetDate(date) + setTargetDate(date as Date) const updatedGoal: Goal = { ...props.goal, name: name ?? props.goal.name, - targetDate: date ?? props.goal.targetDate, + targetDate: (date as Date) ?? props.goal.targetDate, targetAmount: targetAmount ?? props.goal.targetAmount, + icon: icon || undefined, } - dispatch(updateGoalRedux(updatedGoal)) - updateGoalApi(props.goal.id, updatedGoal) + pushUpdate(updatedGoal) + } + } + + const onEmojiSelect = (emoji: any) => { + const nextIcon = emoji?.native ?? '' + setIcon(nextIcon) + setShowPicker(false) + + const updatedGoal: Goal = { + ...props.goal, + name: name ?? props.goal.name, + targetDate: targetDate ?? props.goal.targetDate, + targetAmount: targetAmount ?? props.goal.targetAmount, + icon: nextIcon || undefined, } + pushUpdate(updatedGoal) } return ( - + + + + setShowPicker(!showPicker)}> + {icon ? {icon} : 'Choose Emoji'} + + {showPicker && ( + + + + )} + + @@ -111,9 +156,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) => ( @@ -132,6 +174,39 @@ const GoalManagerContainer = styled.div` position: relative; ` +const TitleRow = styled.div` + display: flex; + align-items: center; + gap: 1rem; + width: 100%; +` + +const IconArea = styled.div` + position: relative; + display: inline-flex; + align-items: center; +` + +const AddIconButton = styled.button` + padding: 0.4rem 0.8rem; + font-size: 1rem; + border-radius: 8px; + border: 1px solid rgba(174, 174, 174, 0.6); + background: transparent; + color: ${({ theme }: { theme: Theme }) => theme.text}; + cursor: pointer; +` + +const EmojiPickerContainer = styled.div` + position: absolute; + top: 2.8rem; + right: 0; + z-index: 1000; + box-shadow: 0 8px 24px rgba(0,0,0,0.2); + border-radius: 12px; + overflow: hidden; +` + const Group = styled.div` display: flex; flex-direction: row; @@ -139,6 +214,7 @@ const Group = styled.div` margin-top: 1.25rem; margin-bottom: 1.25rem; ` + const NameInput = styled.input` display: flex; background-color: transparent; @@ -155,6 +231,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 +242,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; diff --git a/src/ui/pages/Main/goals/GoalCard.tsx b/src/ui/pages/Main/goals/GoalCard.tsx index e8f6d0a..fe6fb58 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 ( + + {goal.icon && {goal.icon}} + {goal.name} + ${goal.targetAmount} {asLocaleDateString(goal.targetDate)} @@ -46,6 +50,20 @@ const Container = styled(Card)` align-items: center; ` + +const GoalTitle = styled.h3` + font-size: 1.4rem; + margin-bottom: 0.5rem; + display: flex; + align-items: center; + justify-content: center; +` + +const IconSpan = styled.span` + font-size: 1.8rem; + margin-right: 0.4rem; +` + const TargetAmount = styled.h2` font-size: 2rem; `