diff --git a/src/store/goalsSlice.ts b/src/store/goalsSlice.ts index 1ed0276..fdab5f5 100644 --- a/src/store/goalsSlice.ts +++ b/src/store/goalsSlice.ts @@ -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 } const initialState: GoalsState = { - map: {}, - list: [], + goalsMap: {}, } -export const goalsSlice = createSlice({ - name: 'goal', +const goalsSlice = createSlice({ + name: 'goals', initialState, reducers: { - createGoal: (state, action: PayloadAction) => { - state.map[action.payload.id] = action.payload - state.list.push(action.payload.id) + setGoals(state, action: PayloadAction) { + state.goalsMap = action.payload.reduce((map, goal) => { + map[goal.id] = goal + return map + }, {} as Record) }, - - updateGoal: (state, action: PayloadAction) => { - state.map[action.payload.id] = action.payload + addGoal(state, action: PayloadAction) { + state.goalsMap[action.payload.id] = action.payload + }, + updateGoal(state, action: PayloadAction) { + const goal = action.payload + if (state.goalsMap[goal.id]) { + state.goalsMap[goal.id] = goal + } + }, + removeGoal(state, action: PayloadAction) { + 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 \ No newline at end of file diff --git a/src/store/hooks.ts b/src/store/hooks.ts index 597f281..1791bfe 100644 --- a/src/store/hooks.ts +++ b/src/store/hooks.ts @@ -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() -export const useAppSelector: TypedUseSelectorHook = useSelector +export const useAppSelector: TypedUseSelectorHook = useSelector \ No newline at end of file diff --git a/src/store/store.ts b/src/store/store.ts index 6741bd8..5b47d7f 100644 --- a/src/store/store.ts +++ b/src/store/store.ts @@ -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' @@ -13,11 +13,5 @@ export const store = configureStore({ }, }) -export type AppDispatch = typeof store.dispatch export type RootState = ReturnType -export type AppThunk = ThunkAction< - ReturnType, - RootState, - unknown, - Action -> +export type AppDispatch = typeof store.dispatch \ No newline at end of file diff --git a/src/store/type.ts b/src/store/type.ts new file mode 100644 index 0000000..053eacc --- /dev/null +++ b/src/store/type.ts @@ -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 +} \ No newline at end of file diff --git a/src/ui/features/goalmanager/GoalManager.tsx b/src/ui/features/goalmanager/GoalManager.tsx index 0779dda..2f640b3 100644 --- a/src/ui/features/goalmanager/GoalManager.tsx +++ b/src/ui/features/goalmanager/GoalManager.tsx @@ -1,3 +1,4 @@ +// 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' @@ -5,78 +6,92 @@ 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(null) const [targetDate, setTargetDate] = useState(null) const [targetAmount, setTargetAmount] = useState(null) + const [icon, setIcon] = useState(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) => { - 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) => { - const nextTargetAmount = parseFloat(event.target.value) - setTargetAmount(nextTargetAmount) + const updateGoalField = (updatedFields: Partial) => { 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) => + updateGoalField({ name: e.target.value }) + + const updateTargetAmountOnChange = (e: React.ChangeEvent) => + 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 ( + {icon ? ( + + {icon} + setIsPickerOpen(!isPickerOpen)}>Change Icon + + ) : ( + setIsPickerOpen(!isPickerOpen)}>Add Icon + )} + + {isPickerOpen && ( + + + + )} + @@ -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) => ( @@ -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; +` \ No newline at end of file diff --git a/src/ui/pages/Main/goals/GoalsSection.tsx b/src/ui/pages/Main/goals/GoalsSection.tsx index ab871b3..1ceef0e 100644 --- a/src/ui/pages/Main/goals/GoalsSection.tsx +++ b/src/ui/pages/Main/goals/GoalsSection.tsx @@ -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, @@ -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() {