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
2 changes: 2 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

15 changes: 14 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -58,5 +58,18 @@
"@types/emoji-mart": "^3.0.9",
"@types/react-dom": "^18.0.5",
"@types/styled-components": "^5.1.25"
},
"description": "",
"main": "index.js",
"repository": {
"type": "git",
"url": "git+https://github.com/richfish85/CommBank-Web.git"
},
"keywords": [],
"author": "",
"license": "ISC",
"type": "commonjs",
"bugs": {
"url": "https://github.com/richfish85/CommBank-Web/issues"
}
}
}
18 changes: 12 additions & 6 deletions src/api/lib.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ 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 const API_ROOT = 'https://fencer-commbank.azurewebsites.net'
export const API_ROOT =
process.env.REACT_APP_API_ROOT ?? 'http://localhost:5203';

export async function getUser(): Promise<User | null> {
try {
Expand Down Expand Up @@ -43,11 +45,15 @@ export async function createGoal(): Promise<Goal | null> {
}
}

export async function updateGoal(goalId: string, updatedGoal: Goal): Promise<boolean> {
export async function updateGoal(
goalId: string,
updatedGoal: Goal
): Promise<boolean> {
try {
await axios.put(`${API_ROOT}/api/Goal/${goalId}`, updatedGoal)
return true
} catch (error: any) {
return false
await axios.put(`${API_ROOT}/api/Goal/${goalId}`, updatedGoal);
return true;
} catch (e) {
console.error('PUT /api/Goal failed', e);
return false;
}
}
1 change: 1 addition & 0 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 | null
}

export interface Tag {
Expand Down
8 changes: 4 additions & 4 deletions src/ui/components/EmojiPicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,17 @@ import 'emoji-mart/css/emoji-mart.css'
import { useAppSelector } from '../../store/hooks'
import { selectMode } from '../../store/themeSlice'

type Props = { onClick: (emoji: BaseEmoji, event: React.MouseEvent) => void }
type Props = { onClick: (emoji: BaseEmoji, event: React.MouseEvent) => void; }

export default function EmojiPicker(props: Props) {
const theme = useAppSelector(selectMode)
export default function EmojiPicker({ onClick }: Props) {
const theme = useAppSelector(selectMode);

return (
<Picker
theme={theme}
showPreview={false}
showSkinTones={false}
onClick={props.onClick}
onClick={onClick}
color="primary"
/>
)
Expand Down
12 changes: 6 additions & 6 deletions src/ui/features/goalmanager/GoalIcon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,15 @@ import { TransparentButton } from '../../components/TransparentButton'

type Props = { icon: string | null; onClick: (e: React.MouseEvent) => void }

const Icon = styled.h1`
font-size: 6rem;
cursor: pointer;
`

export default function GoalIcon(props: Props) {
return (
<TransparentButton onClick={props.onClick}>
<Icon>{props.icon}</Icon>
</TransparentButton>
)
}

const Icon = styled.h1`
font-size: 6rem;
cursor: pointer;
`
}
104 changes: 101 additions & 3 deletions src/ui/features/goalmanager/GoalManager.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { BaseEmoji, Picker } from 'emoji-mart'
import { faCalendarAlt } from '@fortawesome/free-regular-svg-icons'
import { faDollarSign, IconDefinition } from '@fortawesome/free-solid-svg-icons'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
Expand All @@ -11,17 +12,64 @@ 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 EmojiPicker from '../../components/EmojiPicker'
import GoalIcon from './GoalIcon'
import { TransparentButton } from '../../components/TransparentButton'
import { faSmile } from '@fortawesome/free-regular-svg-icons'

type GoalIconContainerProps = { shouldShow: boolean }

const GoalIconContainer = styled.div<GoalIconContainerProps>`
display: ${(props) => (props.shouldShow ? 'flex' : 'none')};
`
type Props = { goal: Goal }
export function GoalManager(props: Props) {
const dispatch = useAppDispatch()

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

const [emojiPickerIsOpen, setEmojiPickerIsOpen] = useState(false)
const [icon, setIcon] = useState<string | null>(null)
// 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 hasIcon = () => icon != null

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

const addIconOnClick = (event: React.MouseEvent) => {
event.stopPropagation();
setEmojiPickerIsOpen(!emojiPickerIsOpen);
}
const pickEmojiOnClick = async (emoji: BaseEmoji, event: React.MouseEvent) => {
event.stopPropagation();

setIcon(emoji.native);
setEmojiPickerIsOpen(false);

const updatedGoal: Goal = {
...props.goal,
icon: emoji.native ?? props.goal.icon,
name: name ?? props.goal.name,
targetDate: targetDate ?? props.goal.targetDate,
targetAmount: targetAmount ?? props.goal.targetAmount,
};

dispatch(updateGoalRedux(updatedGoal));

const ok = await updateGoalApi(props.goal.id, updatedGoal);
if (!ok) {
console.error('Failed to update goal with new icon');
dispatch(updateGoalRedux(props.goal)); // revert to original goal if update fails
setIcon(props.goal.icon ?? null); // reset icon to original
setEmojiPickerIsOpen(false); // close emoji picker
}
}

useEffect(() => {
setIcon(props.goal.icon)
}, [props.goal.id, props.goal.icon])

useEffect(() => {
setName(props.goal.name)
setTargetDate(props.goal.targetDate)
Expand Down Expand Up @@ -79,6 +127,25 @@ export function GoalManager(props: Props) {
<GoalManagerContainer>
<NameInput value={name ?? ''} onChange={updateNameOnChange} />

<GoalIconContainer shouldShow={hasIcon()}>
<GoalIcon icon={icon ?? goal.icon} onClick={addIconOnClick} />
</GoalIconContainer>

<EmojiPickerContainer
isOpen={emojiPickerIsOpen}
hasIcon={hasIcon()}
onClick={(event) => event.stopPropagation()}
>
<EmojiPicker onClick={pickEmojiOnClick} />
</EmojiPickerContainer>

<AddIconButtonContainer shouldShow={!hasIcon()}>
<TransparentButton onClick={addIconOnClick}>
<FontAwesomeIcon icon={faSmile} size="2x" />
<AddIconButtonText>Add icon</AddIconButtonText>
</TransparentButton>
</AddIconButtonContainer>

<Group>
<Field name="Target Date" icon={faCalendarAlt} />
<Value>
Expand Down Expand Up @@ -112,9 +179,15 @@ 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 EmojiPickerContainer = styled.div<EmojiPickerContainerProps>`
display: ${(props) => (props.isOpen ? 'flex' : 'none')};
position: absolute;
top: ${(props) => (props.hasIcon ? '10rem' : '2rem')};
left: 0;
z-index: 100; // ensure it overlays cleanly
`
const Field = (props: FieldProps) => (
<FieldContainer>
<FontAwesomeIcon icon={props.icon} size="2x" />
Expand Down Expand Up @@ -182,3 +255,28 @@ const StringInput = styled.input`
const Value = styled.div`
margin-left: 2rem;
`
// -------------------------------------------------------------------------------
const AddIconButtonContainer = styled.div<AddIconButtonContainerProps>`
display: ${(p) => (p.shouldShow ? 'flex' : 'none')};
flex-direction: column;
align-items: center;
margin-top: 1rem;
`;

// const TransparentButton = styled.button`
// background: transparent;
// border: none;
// cursor: pointer;
// display: flex;
// flex-direction: column;
// align-items: center;
// color: ${({ theme }: { theme: Theme }) => theme.text};
// `;

const AddIconButtonText = styled.span`
margin-top: 0.25rem;
font-size: 0.85rem;
font-weight: 500;
`;
// -------------------------------------------------------------------------------
;
3 changes: 3 additions & 0 deletions src/ui/pages/Main/goals/GoalCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import {
import { Card } from '../../../components/Card'

type Props = { id: string }
const Icon = styled.h1`
font-size: 5.5rem;`

export default function GoalCard(props: Props) {
const dispatch = useAppDispatch()
Expand All @@ -29,6 +31,7 @@ export default function GoalCard(props: Props) {
<Container key={goal.id} onClick={onClick}>
<TargetAmount>${goal.targetAmount}</TargetAmount>
<TargetDate>{asLocaleDateString(goal.targetDate)}</TargetDate>
<Icon>{goal.icon}</Icon>
</Container>
)
}
Expand Down