Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 40 additions & 1 deletion src/api/lib.ts
Original file line number Diff line number Diff line change
@@ -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'

Expand Down Expand Up @@ -51,3 +51,42 @@ export async function updateGoal(goalId: string, updatedGoal: Goal): Promise<boo
return false
}
}

export async function updateTransaction(transactionId: string, updatedTransaction: Transaction): Promise<boolean> {
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<boolean> {
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<boolean> {
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<boolean> {
try {
await axios.put(`${API_ROOT}/api/Account/${accountId}`, updatedAccount)
return true
} catch (error: any) {
return false
}
}



1 change: 1 addition & 0 deletions src/api/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export interface Goal {
accountId: string
transactionIds: string[]
tagIds: string[]
icon?: string
}

export interface Tag {
Expand Down
4 changes: 3 additions & 1 deletion src/store/goalsSlice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,9 @@ export const goalsSlice = createSlice({
reducers: {
createGoal: (state, action: PayloadAction<Goal>) => {
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<Goal>) => {
Expand Down
148 changes: 143 additions & 5 deletions src/ui/features/goalmanager/GoalManager.tsx
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -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) {
Expand All @@ -21,16 +23,20 @@ export function GoalManager(props: Props) {
const [name, setName] = useState<string | null>(null)
const [targetDate, setTargetDate] = useState<Date | null>(null)
const [targetAmount, setTargetAmount] = useState<number | null>(null)
const [icon, setIcon] = useState<string | null>(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(() => {
Expand Down Expand Up @@ -69,15 +75,62 @@ 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)
}

const pickEmojiOnClick = (emoji: BaseEmoji, event: React.MouseEvent) => {
updateIcon(emoji.native)
setIsPickerOpen(false)
}


return (
<GoalManagerContainer>
<NameInput value={name ?? ''} onChange={updateNameOnChange} />
<TopContainer>
<IconContainer onClick={() => setIsPickerOpen(!isPickerOpen)}>
{icon ? (
<>
<IconText>{icon}</IconText>
<RemoveIcon onClick={(e) => {
e.stopPropagation();
updateIcon("");
}}>×</RemoveIcon>
Comment on lines +107 to +114
Copy link

Copilot AI Mar 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This new click-target uses non-interactive elements (div) for actions (open picker / remove icon). That breaks keyboard navigation and screen reader semantics. Prefer a button (or the existing TransparentButton) with appropriate aria-labels and focus styles, and keep the remove control as a separate button inside the container.

Copilot uses AI. Check for mistakes.
Comment on lines +111 to +114
Copy link

Copilot AI Mar 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This block introduces semicolons and double quotes (e.g. e.stopPropagation(); and updateIcon("")), but the repo's Prettier config enforces semi: false and singleQuote: true. Please format this section (or run Prettier) so it matches project formatting to avoid CI/style check failures.

Suggested change
<RemoveIcon onClick={(e) => {
e.stopPropagation();
updateIcon("");
}}>×</RemoveIcon>
<RemoveIcon
onClick={e => {
e.stopPropagation()
updateIcon('')
}}
>
×
</RemoveIcon>

Copilot uses AI. Check for mistakes.
</>
) : (
<AddIconContainer>
<FontAwesomeIcon icon={faPlus} size="2x" />
<AddIconText>Add Icon</AddIconText>
</AddIconContainer>
)}
</IconContainer>
Comment on lines +106 to +122
Copy link

Copilot AI Mar 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is an existing GoalIcon component under src/ui/features/goalmanager/GoalIcon.tsx, but this change re-implements icon rendering and click handling inline. Since GoalIcon appears unused now, consider either reusing it here (to keep icon UI consistent) or removing the unused component to avoid dead code.

Copilot uses AI. Check for mistakes.
<NameInput value={name ?? ''} onChange={updateNameOnChange} />

{isPickerOpen && (
<>
<Backdrop onClick={() => setIsPickerOpen(false)} />
<EmojiPickerContainer>
<EmojiPicker onClick={pickEmojiOnClick} />
</EmojiPickerContainer>
</>
)}
</TopContainer>

<Group>
<Field name="Target Date" icon={faCalendarAlt} />
Expand Down Expand Up @@ -110,10 +163,8 @@ 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) => (
<FieldContainer>
Expand All @@ -139,6 +190,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;
Expand Down
6 changes: 6 additions & 0 deletions src/ui/pages/Main/goals/GoalCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export default function GoalCard(props: Props) {

return (
<Container key={goal.id} onClick={onClick}>
{goal.icon && <Icon>{goal.icon}</Icon>}
<TargetAmount>${goal.targetAmount}</TargetAmount>
<TargetDate>{asLocaleDateString(goal.targetDate)}</TargetDate>
</Container>
Expand Down Expand Up @@ -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;
`
20 changes: 7 additions & 13 deletions src/ui/pages/Main/transactions/TransactionItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Comment on lines +20 to +21
Copy link

Copilot AI Mar 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Promise.all(...) will reject on the first failed tag request, and since the returned promise isn't caught, this can surface as an unhandled rejection and leave the component in a perpetual loading state. Wrap the fetch in try/catch (or use Promise.allSettled) and decide on a fallback (e.g., skip failed tags, show none, or surface an error).

Suggested change
const fetchedTags = await Promise.all(props.transaction.tagIds.map(fetch))
setTags(fetchedTags)
try {
const fetchedTags = await Promise.all(props.transaction.tagIds.map(fetch))
setTags(fetchedTags)
} catch (error) {
console.error('Failed to fetch tags for transaction', props.transaction.id, error)
setTags([])
}

Copilot uses AI. Check for mistakes.
}

fetchAll()
})
}, [props.transaction.id, props.transaction.tagIds])

return (
<Container>
Expand All @@ -40,11 +35,10 @@ export function TransactionItem(props: Props) {
props.transaction.dateTime,
).toLocaleDateString()}`}</h6>

<h6 className="price">{`${
props.transaction.transactionType === 'Credit'
? `$${props.transaction.amount}`
: `-$${props.transaction.amount}`
}`}</h6>
<h6 className="price">{`${props.transaction.transactionType === 'Credit'
? `$${props.transaction.amount}`
: `-$${props.transaction.amount}`
}`}</h6>
</Content>
<Divider />
</Container>
Expand Down
4 changes: 2 additions & 2 deletions src/ui/pages/Main/transactions/TransactionsContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ export default function TransactionsContent(props: Props) {
if (!props.transactions) return null
return (
<>
{props.transactions.sort(sortByDateDesc).map((transaction) => (
<TransactionItem transaction={transaction} />
{[...props.transactions].sort(sortByDateDesc).map((transaction) => (
<TransactionItem key={transaction.id} transaction={transaction} />
))}
</>
)
Expand Down
Loading