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
5 changes: 3 additions & 2 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 Expand Up @@ -59,10 +60,10 @@ export enum AccountType {
export enum ApplicationStatus {
Received,
Assigned,
UnderReview,
UnderReview,
Approved,
Rejected,
}

export type ModalContent = Goal
export type ModalType = 'Goal'
export type ModalType = 'Goal'
53 changes: 45 additions & 8 deletions src/ui/pages/Main/goals/GoalsContent.tsx
Original file line number Diff line number Diff line change
@@ -1,33 +1,70 @@
import React from 'react'
import React, { useMemo, useState } from 'react'
import styled from 'styled-components'
import { useAppSelector } from '../../../../store/hooks'
import { selectGoalsMap } from '../../../../store/goalsSlice'
import { media } from '../../../utils/media'
import GoalCard from './GoalCard'
import { filterGoalsBySearch } from './filterGoals'

type Props = { ids: string[] | null }

export default function GoalsContent(props: Props) {
const [search, setSearch] = useState('')
const goalsMap = useAppSelector(selectGoalsMap)

if (!props.ids) return null

const filteredIds = useMemo(() => {
const goals = props.ids.map((id) => goalsMap[id]).filter(Boolean)
return filterGoalsBySearch(goals, search).map((goal) => goal.id)
}, [goalsMap, props.ids, search])

return (
<Container>
{props.ids.map((id) => (
<GoalCard key={id} id={id} />
))}
<SearchInput
value={search}
onChange={(event) => setSearch(event.target.value)}
placeholder="Search goals"
aria-label="Search goals"
/>
<CardsContainer>
{filteredIds.map((id) => (
<GoalCard key={id} id={id} />
))}
</CardsContainer>
</Container>
)
}

const Container = styled.div`
display: flex;
flex-direction: row;
justify-content: flex-start;
flex-direction: column;
width: 400px;
padding: 4rem;
overflow-x: auto;
padding: 2rem 4rem 4rem;

${media('<tablet')} {
width: 100%;
padding-left: 0;
padding-right: 0;
}
`

const SearchInput = styled.input`
width: 100%;
padding: 0.75rem 1rem;
margin-bottom: 1rem;
border: 1px solid rgba(174, 174, 174, 0.4);
border-radius: 999px;
font-size: 1rem;
`

const CardsContainer = styled.div`
display: flex;
flex-direction: row;
justify-content: flex-start;
overflow-x: auto;

${media('<tablet')} {
padding-left: 0;
padding-right: 0;
}
Expand Down
14 changes: 14 additions & 0 deletions src/ui/pages/Main/goals/filterGoals.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { filterGoalsBySearch } from './filterGoals'

describe('filterGoalsBySearch', () => {
const goals = [
{ id: '1', name: 'Holiday', targetAmount: 5000, balance: 1500, targetDate: new Date('2026-12-31'), created: new Date('2025-01-01'), accountId: 'acct-1', transactionIds: [], tagIds: [] },
{ id: '2', name: 'Emergency Fund', targetAmount: 1200, balance: 300, targetDate: new Date('2026-06-30'), created: new Date('2025-01-01'), accountId: 'acct-1', transactionIds: [], tagIds: [] },
]

it('matches goals by name and target amount', () => {
expect(filterGoalsBySearch(goals as any, 'holiday')).toEqual([goals[0]])
expect(filterGoalsBySearch(goals as any, '1200')).toEqual([goals[1]])
expect(filterGoalsBySearch(goals as any, '$5,000')).toEqual([goals[0]])
})
})
23 changes: 23 additions & 0 deletions src/ui/pages/Main/goals/filterGoals.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { Goal } from '../../../../api/types'

export function filterGoalsBySearch(goals: Goal[], query: string) {
const normalizedQuery = query.trim().toLowerCase()

if (!normalizedQuery) {
return goals
}

return goals.filter((goal) => {
const name = goal.name?.toLowerCase() ?? ''
const amount = goal.targetAmount?.toString() ?? ''
const formattedAmount = new Intl.NumberFormat('en-AU', {
style: 'currency',
currency: 'AUD',
maximumFractionDigits: 0,
})
.format(goal.targetAmount)
.toLowerCase()

return name.includes(normalizedQuery) || amount.includes(normalizedQuery) || formattedAmount.includes(normalizedQuery)
})
}