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
74 changes: 69 additions & 5 deletions package-lock.json

Large diffs are not rendered by default.

32 changes: 31 additions & 1 deletion src/api/lib.ts
Original file line number Diff line number Diff line change
@@ -1,53 +1,83 @@
import axios from 'axios'
// Ensure this matches your project structure for the static user data
import { user } from '../data/user'
import { Goal, Transaction, User } from './types'

/**
* Switch between the Azure URL and localhost depending on your environment.
* If you are running your .NET backend locally, change this to:
* export const API_ROOT = 'http://localhost:5203'
*/
export const API_ROOT = 'https://fencer-commbank.azurewebsites.net'

/**
* Fetches the current user profile from the backend
*/
export async function getUser(): Promise<User | null> {
try {
const response = await axios.get(`${API_ROOT}/api/User/${user.id}`)
return response.data
} catch (error: any) {
console.error("Error fetching user:", error)
return null
}
}

/**
* Fetches all transactions belonging to the current user
*/
export async function getTransactions(): Promise<Transaction[] | null> {
try {
const response = await axios.get(`${API_ROOT}/api/Transaction/User/${user.id}`)
return response.data
} catch (error: any) {
console.error("Error fetching transactions:", error)
return null
}
}

/**
* Fetches all goals belonging to the current user
*/
export async function getGoals(): Promise<Goal[] | null> {
try {
const response = await axios.get(`${API_ROOT}/api/Goal/User/${user.id}`)
return response.data
} catch (error: any) {
console.error("Error fetching goals:", error)
return null
}
}

/**
* Creates a new goal with a default name and current date
*/
export async function createGoal(): Promise<Goal | null> {
try {
const response = await axios.post(`${API_ROOT}/api/Goal`, {
userId: user.id,
name: "New Goal", // Providing a default name helps the backend
targetDate: new Date(),
targetAmount: 0,
})
return response.data
} catch (error: any) {
console.error("Error creating goal:", error)
return null
}
}

/**
* Updates an existing goal (Task 3 Core Requirement)
* This PUT request ensures your Icon changes persist in the Database.
*/
export async function updateGoal(goalId: string, updatedGoal: Goal): Promise<boolean> {
try {
// Sends the full goal object (including the new Icon field) to the server
await axios.put(`${API_ROOT}/api/Goal/${goalId}`, updatedGoal)
return true
} catch (error: any) {
console.error("Error updating goal:", error)
return false
}
}
}
3 changes: 2 additions & 1 deletion 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 // Added optional icon field
}

export interface Tag {
Expand Down Expand Up @@ -65,4 +66,4 @@ export enum ApplicationStatus {
}

export type ModalContent = Goal
export type ModalType = 'Goal'
export type ModalType = 'Goal'
2 changes: 1 addition & 1 deletion src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,4 @@ ReactDOM.render(
document.getElementById('root'),
)

export {}
export { }
75 changes: 64 additions & 11 deletions src/ui/features/goalmanager/GoalManager.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { faCalendarAlt } from '@fortawesome/free-regular-svg-icons'
import { faCalendarAlt, faSmile } 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 'date-fns'
import { Picker } from 'emoji-mart'
import 'emoji-mart/css/emoji-mart.css'
import React, { useEffect, useState } from 'react'
import styled from 'styled-components'
import { updateGoal as updateGoalApi } from '../../../api/lib'
Expand All @@ -21,22 +23,42 @@ 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 | undefined>(undefined)
const [showEmojiPicker, setShowEmojiPicker] = 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 handleIconSelect = (emoji: any) => {
const nextIcon = emoji.native
setIcon(nextIcon)
setShowEmojiPicker(false)

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 updateNameOnChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const nextName = event.target.value
setName(nextName)
Expand Down Expand Up @@ -77,7 +99,18 @@ export function GoalManager(props: Props) {

return (
<GoalManagerContainer>
<NameInput value={name ?? ''} onChange={updateNameOnChange} />
<Row>
<EmojiTrigger onClick={() => setShowEmojiPicker(!showEmojiPicker)}>
{icon ? <SelectedEmoji>{icon}</SelectedEmoji> : <FontAwesomeIcon icon={faSmile} size="3x" />}
</EmojiTrigger>
<NameInput value={name ?? ''} onChange={updateNameOnChange} />
</Row>

{showEmojiPicker && (
<PickerWrapper>
<Picker onSelect={handleIconSelect} theme="dark" />
</PickerWrapper>
)}

<Group>
<Field name="Target Date" icon={faCalendarAlt} />
Expand Down Expand Up @@ -110,12 +143,7 @@ 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) => (
const Field = (props: { name: string; icon: IconDefinition }) => (
<FieldContainer>
<FontAwesomeIcon icon={props.icon} size="2x" />
<FieldName>{props.name}</FieldName>
Expand All @@ -132,6 +160,33 @@ const GoalManagerContainer = styled.div`
position: relative;
`

const Row = styled.div`
display: flex;
flex-direction: row;
align-items: center;
gap: 2rem;
`

const EmojiTrigger = styled.div`
cursor: pointer;
color: rgba(174, 174, 174, 1);
transition: transform 0.2s;
&:hover {
transform: scale(1.1);
}
`

const SelectedEmoji = styled.span`
font-size: 4rem;
`

const PickerWrapper = styled.div`
position: absolute;
top: 6rem;
left: 0;
z-index: 100;
`

const Group = styled.div`
display: flex;
flex-direction: row;
Expand Down Expand Up @@ -160,7 +215,6 @@ const FieldContainer = styled.div`
flex-direction: row;
align-items: center;
width: 20rem;

svg {
color: rgba(174, 174, 174, 1);
}
Expand All @@ -178,7 +232,6 @@ const StringInput = styled.input`
font-weight: bold;
color: ${({ theme }: { theme: Theme }) => theme.text};
`

const Value = styled.div`
margin-left: 2rem;
`
`
13 changes: 11 additions & 2 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}>
<IconDisplay>{goal.icon ?? '🎯'}</IconDisplay>
<TargetAmount>${goal.targetAmount}</TargetAmount>
<TargetDate>{asLocaleDateString(goal.targetDate)}</TargetDate>
</Container>
Expand All @@ -43,14 +44,22 @@ const Container = styled(Card)`
margin-left: 2rem;
margin-right: 2rem;
border-radius: 2rem;

justify-content: center;
align-items: center;
`

const IconDisplay = styled.div`
font-size: 3rem;
margin-bottom: 0.5rem;
`

const TargetAmount = styled.h2`
font-size: 2rem;
margin: 0;
`

const TargetDate = styled.h4`
color: rgba(174, 174, 174, 1);
font-size: 1rem;
`
margin: 0.5rem 0 0 0;
`
2 changes: 1 addition & 1 deletion src/ui/pages/Main/transactions/TransactionItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export function TransactionItem(props: Props) {
}

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

return (
<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