diff --git a/package-lock.json b/package-lock.json index 240aecf..94349f8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6611,6 +6611,7 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/emoji-mart/-/emoji-mart-3.0.1.tgz", "integrity": "sha512-sxpmMKxqLvcscu6mFn9ITHeZNkGzIvD0BSNFE/LJESPbCA8s1jM6bCDPjWbV31xHq7JXaxgpHxLB54RCbBZSlg==", + "license": "BSD-3-Clause", "dependencies": { "@babel/runtime": "^7.0.0", "prop-types": "^15.6.0" diff --git a/src/api/lib.md b/src/api/lib.md new file mode 100644 index 0000000..3c593ca --- /dev/null +++ b/src/api/lib.md @@ -0,0 +1,53 @@ +import axios from 'axios' +import { user } from '../data/user' +import { Goal, Transaction, User } from './types' + +export const API_ROOT = 'https://fencer-commbank.azurewebsites.net' + +export async function getUser(): Promise { + try { + const response = await axios.get(`${API_ROOT}/api/User/${user.id}`) + return response.data + } catch (error: any) { + return null + } +} + +export async function getTransactions(): Promise { + try { + const response = await axios.get(`${API_ROOT}/api/Transaction/User/${user.id}`) + return response.data + } catch (error: any) { + return null + } +} + +export async function getGoals(): Promise { + try { + const response = await axios.get(`${API_ROOT}/api/Goal/User/${user.id}`) + return response.data + } catch (error: any) { + return null + } +} + +export async function createGoal(): Promise { + try { + const response = await axios.post(`${API_ROOT}/api/Goal`, { + userId: user.id, + targetDate: new Date(), + }) + return response.data + } catch (error: any) { + return null + } +} + +export async function updateGoal(goalId: string, updatedGoal: Goal): Promise { + try { + await axios.put(`${API_ROOT}/api/Goal/${goalId}`, updatedGoal) + return true + } catch (error: any) { + return false + } +} diff --git a/src/api/lib.py b/src/api/lib.py new file mode 100644 index 0000000..3c593ca --- /dev/null +++ b/src/api/lib.py @@ -0,0 +1,53 @@ +import axios from 'axios' +import { user } from '../data/user' +import { Goal, Transaction, User } from './types' + +export const API_ROOT = 'https://fencer-commbank.azurewebsites.net' + +export async function getUser(): Promise { + try { + const response = await axios.get(`${API_ROOT}/api/User/${user.id}`) + return response.data + } catch (error: any) { + return null + } +} + +export async function getTransactions(): Promise { + try { + const response = await axios.get(`${API_ROOT}/api/Transaction/User/${user.id}`) + return response.data + } catch (error: any) { + return null + } +} + +export async function getGoals(): Promise { + try { + const response = await axios.get(`${API_ROOT}/api/Goal/User/${user.id}`) + return response.data + } catch (error: any) { + return null + } +} + +export async function createGoal(): Promise { + try { + const response = await axios.post(`${API_ROOT}/api/Goal`, { + userId: user.id, + targetDate: new Date(), + }) + return response.data + } catch (error: any) { + return null + } +} + +export async function updateGoal(goalId: string, updatedGoal: Goal): Promise { + try { + await axios.put(`${API_ROOT}/api/Goal/${goalId}`, updatedGoal) + return true + } catch (error: any) { + return false + } +} diff --git a/src/api/types.ts b/src/api/types.ts index f75edad..6193f2a 100644 --- a/src/api/types.ts +++ b/src/api/types.ts @@ -27,6 +27,7 @@ export interface Goal { accountId: string transactionIds: string[] tagIds: string[] + icon?: string } export interface Tag { diff --git a/src/ui/features/goalmanager/GoalManager.md b/src/ui/features/goalmanager/GoalManager.md new file mode 100644 index 0000000..5fe8ab4 --- /dev/null +++ b/src/ui/features/goalmanager/GoalManager.md @@ -0,0 +1,237 @@ +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 { updateGoal as updateGoalApi } from '../../../api/lib' +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 { Theme } from '../../components/Theme' +import 'emoji-mart/css/emoji-mart.css' +import { Picker } from 'emoji-mart' + +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(props.goal.icon || null) + const [showEmojiPicker, setShowEmojiPicker] = 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) + setIcon(goal.icon || null) + }, [goal.name, goal.icon]) + + const updateNameOnChange = (event: React.ChangeEvent) => { + const nextName = event.target.value + setName(nextName) + const updatedGoal: Goal = { + ...props.goal, + name: nextName, + icon: icon ?? undefined, // <-- type-safe + } + dispatch(updateGoalRedux(updatedGoal)) + updateGoalApi(props.goal.id, updatedGoal) + } + + const updateTargetAmountOnChange = (event: React.ChangeEvent) => { + const nextTargetAmount = parseFloat(event.target.value) + setTargetAmount(nextTargetAmount) + const updatedGoal: Goal = { + ...props.goal, + name: name ?? props.goal.name, + targetDate: targetDate ?? props.goal.targetDate, + targetAmount: nextTargetAmount, + icon: icon ?? undefined, // <-- type-safe + } + dispatch(updateGoalRedux(updatedGoal)) + updateGoalApi(props.goal.id, updatedGoal) + } + + 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, + icon: icon ?? undefined, // <-- type-safe + } + dispatch(updateGoalRedux(updatedGoal)) + updateGoalApi(props.goal.id, updatedGoal) + } + } + + const onIconPick = (emoji: any) => { + setIcon(emoji.native) + setShowEmojiPicker(false) + const updatedGoal: Goal = { + ...props.goal, + name: name ?? props.goal.name, + targetDate: targetDate ?? props.goal.targetDate, + targetAmount: targetAmount ?? props.goal.targetAmount, + icon: emoji.native, + } + dispatch(updateGoalRedux(updatedGoal)) + updateGoalApi(props.goal.id, updatedGoal) + } + + return ( + + + {icon && {icon}} + setShowEmojiPicker((v) => !v)}> + {icon ? 'Change Icon' : 'Add Icon'} + + + {showEmojiPicker && ( + + )} + + + + + + + + + + + + + + + + + + + + + {props.goal.balance} + + + + + + + {new Date(props.goal.created).toLocaleDateString()} + + + + ) +} + +type FieldProps = { name: string; icon: IconDefinition } +const Field = (props: FieldProps) => ( + + + {props.name} + +) + +const GoalManagerContainer = styled.div` + display: flex; + flex-direction: column; + justify-content: flex-start; + align-items: flex-start; + height: 100%; + width: 100%; + position: relative; +` + +const IconRow = styled.div` + display: flex; + align-items: center; + margin-bottom: 1rem; +` +const IconDisplay = styled.span` + font-size: 2.5rem; + margin-right: 1rem; +` +const IconButton = styled.button` + background: #f2f2f2; + border: 1px solid #ccc; + border-radius: 5px; + font-size: 1.2rem; + padding: 0.3rem 1rem; + cursor: pointer; +` + +const Group = styled.div` + display: flex; + flex-direction: row; + width: 100%; + margin-top: 1.25rem; + margin-bottom: 1.25rem; +` +const NameInput = styled.input` + display: flex; + background-color: transparent; + outline: none; + 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; + border: none; + font-size: 1.8rem; + font-weight: bold; + color: ${({ theme }: { theme: Theme }) => theme.text}; +` + +const Value = styled.div` + margin-left: 2rem; +` diff --git a/src/ui/features/goalmanager/GoalManager.tsx b/src/ui/features/goalmanager/GoalManager.tsx index 0779dda..5fe8ab4 100644 --- a/src/ui/features/goalmanager/GoalManager.tsx +++ b/src/ui/features/goalmanager/GoalManager.tsx @@ -11,6 +11,8 @@ import { selectGoalsMap, updateGoal as updateGoalRedux } from '../../../store/go import { useAppDispatch, useAppSelector } from '../../../store/hooks' import DatePicker from '../../components/DatePicker' import { Theme } from '../../components/Theme' +import 'emoji-mart/css/emoji-mart.css' +import { Picker } from 'emoji-mart' type Props = { goal: Goal } export function GoalManager(props: Props) { @@ -21,21 +23,26 @@ export function GoalManager(props: Props) { const [name, setName] = useState(null) const [targetDate, setTargetDate] = useState(null) const [targetAmount, setTargetAmount] = useState(null) + const [icon, setIcon] = useState(props.goal.icon || null) + const [showEmojiPicker, setShowEmojiPicker] = 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]) + setIcon(goal.icon || null) + }, [goal.name, goal.icon]) const updateNameOnChange = (event: React.ChangeEvent) => { const nextName = event.target.value @@ -43,6 +50,7 @@ export function GoalManager(props: Props) { const updatedGoal: Goal = { ...props.goal, name: nextName, + icon: icon ?? undefined, // <-- type-safe } dispatch(updateGoalRedux(updatedGoal)) updateGoalApi(props.goal.id, updatedGoal) @@ -56,6 +64,7 @@ export function GoalManager(props: Props) { name: name ?? props.goal.name, targetDate: targetDate ?? props.goal.targetDate, targetAmount: nextTargetAmount, + icon: icon ?? undefined, // <-- type-safe } dispatch(updateGoalRedux(updatedGoal)) updateGoalApi(props.goal.id, updatedGoal) @@ -69,14 +78,44 @@ export function GoalManager(props: Props) { name: name ?? props.goal.name, targetDate: date ?? props.goal.targetDate, targetAmount: targetAmount ?? props.goal.targetAmount, + icon: icon ?? undefined, // <-- type-safe } dispatch(updateGoalRedux(updatedGoal)) updateGoalApi(props.goal.id, updatedGoal) } } + const onIconPick = (emoji: any) => { + setIcon(emoji.native) + setShowEmojiPicker(false) + const updatedGoal: Goal = { + ...props.goal, + name: name ?? props.goal.name, + targetDate: targetDate ?? props.goal.targetDate, + targetAmount: targetAmount ?? props.goal.targetAmount, + icon: emoji.native, + } + dispatch(updateGoalRedux(updatedGoal)) + updateGoalApi(props.goal.id, updatedGoal) + } + return ( + + {icon && {icon}} + setShowEmojiPicker((v) => !v)}> + {icon ? 'Change Icon' : 'Add Icon'} + + + {showEmojiPicker && ( + + )} + @@ -111,10 +150,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) => ( @@ -132,6 +167,24 @@ const GoalManagerContainer = styled.div` position: relative; ` +const IconRow = styled.div` + display: flex; + align-items: center; + margin-bottom: 1rem; +` +const IconDisplay = styled.span` + font-size: 2.5rem; + margin-right: 1rem; +` +const IconButton = styled.button` + background: #f2f2f2; + border: 1px solid #ccc; + border-radius: 5px; + font-size: 1.2rem; + padding: 0.3rem 1rem; + cursor: pointer; +` + const Group = styled.div` display: flex; flex-direction: row;