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
59 changes: 39 additions & 20 deletions src/store/goalsSlice.ts
Original file line number Diff line number Diff line change
@@ -1,39 +1,58 @@
import { createSlice, PayloadAction } from '@reduxjs/toolkit'
import { Goal } from '../api/types'
import { RootState } from './store'

export interface GoalsState {
map: IdToGoal
list: string[]
// Define Goal type directly here
export interface Goal {
id: string
name: string
targetDate: Date
targetAmount: number
balance: number
created: Date
accountId: string
transactionIds: string[]
tagIds: string[]
icon?: string
}

export interface IdToGoal {
[id: string]: Goal
interface GoalsState {
goalsMap: Record<string, Goal>
}

const initialState: GoalsState = {
map: {},
list: [],
goalsMap: {},
}

export const goalsSlice = createSlice({
name: 'goal',
const goalsSlice = createSlice({
name: 'goals',
initialState,
reducers: {
createGoal: (state, action: PayloadAction<Goal>) => {
state.map[action.payload.id] = action.payload
state.list.push(action.payload.id)
setGoals(state, action: PayloadAction<Goal[]>) {
state.goalsMap = action.payload.reduce((map, goal) => {
map[goal.id] = goal
return map
}, {} as Record<string, Goal>)
},

updateGoal: (state, action: PayloadAction<Goal>) => {
state.map[action.payload.id] = action.payload
addGoal(state, action: PayloadAction<Goal>) {
state.goalsMap[action.payload.id] = action.payload
},
updateGoal(state, action: PayloadAction<Goal>) {
const goal = action.payload
if (state.goalsMap[goal.id]) {
state.goalsMap[goal.id] = goal
}
},
removeGoal(state, action: PayloadAction<string>) {
delete state.goalsMap[action.payload]
},
},
})

export const { createGoal, updateGoal } = goalsSlice.actions
export const { setGoals, addGoal, updateGoal, removeGoal } = goalsSlice.actions

export const selectGoalsMap = (state: RootState) => state.goals.map
export const selectGoalsList = (state: RootState) => state.goals.list
// Selectors
export const selectGoalsMap = (state: RootState) => state.goals.goalsMap
export const selectGoalsArray = (state: RootState) =>
Object.values(state.goals.goalsMap)

export default goalsSlice.reducer
export default goalsSlice.reducer
5 changes: 3 additions & 2 deletions src/store/hooks.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
// src/store/hooks.ts
import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux'
import type { AppDispatch, RootState } from './store'
import type { RootState, AppDispatch } from './store'

export const useAppDispatch = () => useDispatch<AppDispatch>()
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector
10 changes: 2 additions & 8 deletions src/store/store.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Action, configureStore, ThunkAction } from '@reduxjs/toolkit'
import { configureStore } from '@reduxjs/toolkit'
import goalsReducer from './goalsSlice'
import modalReducer from './modalSlice'
import themeReducer from './themeSlice'
Expand All @@ -13,11 +13,5 @@ export const store = configureStore({
},
})

export type AppDispatch = typeof store.dispatch
export type RootState = ReturnType<typeof store.getState>
export type AppThunk<ReturnType = void> = ThunkAction<
ReturnType,
RootState,
unknown,
Action<string>
>
export type AppDispatch = typeof store.dispatch
24 changes: 24 additions & 0 deletions src/store/type.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
export interface Goal {
id: string
name: string
targetDate: Date
targetAmount: number
balance: number
created: Date
accountId: string
transactionIds: string[]
tagIds: string[]
icon?: string
}
export interface Goal {
id: string
name: string
targetDate: Date
targetAmount: number
balance: number
created: Date
accountId: string
transactionIds: string[]
tagIds: string[]
icon?: string // add this line
}
122 changes: 69 additions & 53 deletions src/ui/features/goalmanager/GoalManager.tsx
Original file line number Diff line number Diff line change
@@ -1,82 +1,97 @@
// src/ui/features/goalmanager/GoalManager.tsx
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 'date-fns'
import React, { useEffect, useState } from 'react'
import styled from 'styled-components'
import { Picker } from 'emoji-mart'
import 'emoji-mart/css/emoji-mart.css'

import { updateGoal as updateGoalApi } from '../../../api/lib'
import { Goal } from '../../../api/types'
import { Goal } from '../../../store/type'
import { selectGoalsMap, updateGoal as updateGoalRedux } from '../../../store/goalsSlice'
import { useAppDispatch, useAppSelector } from '../../../store/hooks'
import DatePicker from '../../components/DatePicker'
import { Theme } from '../../components/Theme'

type Props = { goal: Goal }

export function GoalManager(props: Props) {
const dispatch = useAppDispatch()

const goal = useAppSelector(selectGoalsMap)[props.goal.id]

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(() => {
setName(goal.name)
}, [goal.name])
if (goal) setName(goal.name)
}, [goal])

const updateNameOnChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const nextName = event.target.value
setName(nextName)
const updatedGoal: Goal = {
...props.goal,
name: nextName,
}
dispatch(updateGoalRedux(updatedGoal))
updateGoalApi(props.goal.id, updatedGoal)
}

const updateTargetAmountOnChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const nextTargetAmount = parseFloat(event.target.value)
setTargetAmount(nextTargetAmount)
const updateGoalField = (updatedFields: Partial<Goal>) => {
const updatedGoal: Goal = {
...props.goal,
name: name ?? props.goal.name,
targetDate: targetDate ?? props.goal.targetDate,
targetAmount: nextTargetAmount,
targetAmount: targetAmount ?? props.goal.targetAmount,
icon: icon ?? props.goal.icon,
...updatedFields,
}
dispatch(updateGoalRedux(updatedGoal))
updateGoalApi(props.goal.id, updatedGoal)
}

const updateNameOnChange = (e: React.ChangeEvent<HTMLInputElement>) =>
updateGoalField({ name: e.target.value })

const updateTargetAmountOnChange = (e: React.ChangeEvent<HTMLInputElement>) =>
updateGoalField({ targetAmount: parseFloat(e.target.value) })

const pickDateOnChange = (date: MaterialUiPickersDate) => {
if (date != null) {
setTargetDate(date)
const updatedGoal: Goal = {
...props.goal,
name: name ?? props.goal.name,
targetDate: date ?? props.goal.targetDate,
targetAmount: targetAmount ?? props.goal.targetAmount,
}
dispatch(updateGoalRedux(updatedGoal))
updateGoalApi(props.goal.id, updatedGoal)
}
if (date) updateGoalField({ targetDate: date })
setTargetDate(date)
}

const onEmojiSelect = (emoji: any) => {
updateGoalField({ icon: emoji.native })
setIcon(emoji.native)
setIsPickerOpen(false)
}

return (
<GoalManagerContainer>
{icon ? (
<GoalIconContainer>
<GoalIcon>{icon}</GoalIcon>
<AddIconButton onClick={() => setIsPickerOpen(!isPickerOpen)}>Change Icon</AddIconButton>
</GoalIconContainer>
) : (
<AddIconButton onClick={() => setIsPickerOpen(!isPickerOpen)}>Add Icon</AddIconButton>
)}

{isPickerOpen && (
<EmojiPickerContainer>
<Picker onSelect={onEmojiSelect} />
</EmojiPickerContainer>
)}

<NameInput value={name ?? ''} onChange={updateNameOnChange} />

<Group>
Expand Down Expand Up @@ -111,9 +126,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) => (
<FieldContainer>
Expand All @@ -125,60 +137,64 @@ const Field = (props: FieldProps) => (
const GoalManagerContainer = styled.div`
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: flex-start;
height: 100%;
width: 100%;
position: relative;
`

const Group = styled.div`
display: flex;
flex-direction: row;
width: 100%;
margin-top: 1.25rem;
margin-bottom: 1.25rem;
margin: 1.25rem 0;
`
const NameInput = styled.input`
display: flex;
background-color: transparent;
outline: none;
background: transparent;
border: none;
font-size: 4rem;
font-weight: bold;
color: ${({ theme }: { theme: Theme }) => theme.text};
`

const FieldName = styled.h1`
font-size: 1.8rem;
margin-left: 1rem;
color: rgba(174, 174, 174, 1);
font-weight: normal;
`
const FieldContainer = styled.div`
display: flex;
flex-direction: row;
align-items: center;
width: 20rem;

svg {
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;
outline: none;
background: transparent;
border: none;
font-size: 1.8rem;
font-weight: bold;
color: ${({ theme }: { theme: Theme }) => theme.text};
`

const Value = styled.div`
margin-left: 2rem;
`
const AddIconButton = styled.button`
padding: 0.5rem 1rem;
background: #ffcc00;
border: none;
cursor: pointer;
margin-bottom: 1rem;
`
const GoalIconContainer = styled.div`
display: flex;
align-items: center;
margin-bottom: 1rem;
`
const GoalIcon = styled.div`
font-size: 3rem;
margin-right: 1rem;
`
const EmojiPickerContainer = styled.div`
position: absolute;
top: 5rem;
z-index: 1000;
`
4 changes: 2 additions & 2 deletions src/ui/pages/Main/goals/GoalsSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import React, { useEffect } from 'react'
import styled from 'styled-components'
import { createGoal as createGoalApi, getGoals } from '../../../../api/lib'
import { createGoal as createGoalRedux, selectGoalsList } from '../../../../store/goalsSlice'
import { addGoal as createGoalRedux, selectGoalsArray } from '../../../../store/goalsSlice'
import { useAppDispatch, useAppSelector } from '../../../../store/hooks'
import {
setContent as setContentRedux,
Expand All @@ -16,7 +16,7 @@ import GoalsContent from './GoalsContent'

export default function GoalsSection() {
const dispatch = useAppDispatch()
const goalIds = useAppSelector(selectGoalsList)
const goalIds = useAppSelector(selectGoalsArray).map((goal) => goal.id)

useEffect(() => {
async function fetch() {
Expand Down