diff --git a/src/App.tsx b/src/App.tsx
new file mode 100644
index 0000000..5a862a6
--- /dev/null
+++ b/src/App.tsx
@@ -0,0 +1,70 @@
+import { createTheme, ThemeProvider as ThemeProviderMui } from '@material-ui/core'
+import React, { useEffect } from 'react'
+import styled, { ThemeProvider } from 'styled-components'
+import { getUser as getUserApi } from './api/lib'
+import { useAppDispatch, useAppSelector } from './store/hooks'
+import { selectIsOpen, setIsOpen as setIsOpenRedux } from './store/modalSlice'
+import { selectMode } from './store/themeSlice'
+import { setUser as setUserRedux } from './store/userSlice'
+import { GlobalStyle } from './ui/components/GlobalStyles'
+import { DarkTheme, LightTheme } from './ui/components/Theme'
+import Main from './ui/pages/Main/Main'
+import Modal from './ui/surfaces/modal/Modal'
+
+export default function App() {
+ const mode = useAppSelector(selectMode)
+ const modalIsOpen = useAppSelector(selectIsOpen)
+ const dispatch = useAppDispatch()
+
+ const muiTheme = createTheme({ palette: { type: mode } })
+
+ useEffect(() => {
+ async function fetch() {
+ const user = await getUserApi()
+ if (user != null) {
+ dispatch(setUserRedux(user))
+ }
+ }
+
+ fetch()
+ }, [dispatch])
+
+ const onClick = (event: React.MouseEvent) => {
+ event.stopPropagation()
+ dispatch(setIsOpenRedux(false))
+ }
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+ )
+}
+
+const AppContainer = styled.div`
+ position: relative;
+`
+const ModalContainer = styled.div`
+ width: 100vw;
+ height: 100vh;
+ display: ${(props) => (props.isOpen ? 'flex' : 'none')};
+ flex-direction: row;
+ justify-content: center;
+ align-items: center;
+ background-color: ${({ theme }) => theme.overlay};
+ position: absolute;
+ top: 0;
+ left: 0;
+`
+
+type ModalContainerProps = { isOpen: boolean }
diff --git a/src/api/lib.ts b/src/api/lib.ts
new file mode 100644
index 0000000..3c593ca
--- /dev/null
+++ b/src/api/lib.ts
@@ -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
new file mode 100644
index 0000000..b668377
--- /dev/null
+++ b/src/api/types.ts
@@ -0,0 +1,69 @@
+export interface Account {
+ id: string
+ number: number
+ name: string
+ balance: number
+ accountType: AccountType
+ applicationId: string
+ transactionIds: string[]
+}
+
+export interface Application {
+ id: string
+ created: Date
+ modified: Date
+ accountType: AccountType
+ applicationStatus: ApplicationStatus
+ userId: string
+}
+
+export interface Goal {
+ id: string
+ name: string
+ targetAmount: number
+ balance: number
+ targetDate: Date
+ created: Date
+ accountId: string
+ transactionIds: string[]
+ tagIds: string[]
+ icon: string
+}
+
+export interface Tag {
+ id: string
+ name: string
+}
+
+export interface Transaction {
+ id: string
+ transactionType: 'Debit' | 'Credit' | 'Transfer'
+ amount: number
+ dateTime: Date
+ goalId?: string
+ description: string
+ tagIds: string[]
+}
+
+export interface User {
+ id: string
+ name: string
+ email: string
+ applicationIds: string[]
+}
+
+export enum AccountType {
+ GoalSaver,
+ NetBankSaver,
+}
+
+export enum ApplicationStatus {
+ Received,
+ Assigned,
+ UnderReview,
+ Approved,
+ Rejected,
+}
+
+export type ModalContent = Goal
+export type ModalType = 'Goal'
diff --git a/src/assets/images/commbank.svg b/src/assets/images/commbank.svg
new file mode 100644
index 0000000..3eb71a1
--- /dev/null
+++ b/src/assets/images/commbank.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/assets/images/commbank_card.svg b/src/assets/images/commbank_card.svg
new file mode 100644
index 0000000..e6e1ede
--- /dev/null
+++ b/src/assets/images/commbank_card.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/assets/images/tag.png b/src/assets/images/tag.png
new file mode 100644
index 0000000..9b7d0ca
Binary files /dev/null and b/src/assets/images/tag.png differ
diff --git a/src/data/user.ts b/src/data/user.ts
new file mode 100644
index 0000000..85174a9
--- /dev/null
+++ b/src/data/user.ts
@@ -0,0 +1,8 @@
+/**
+ * Hardcoding user for MVP
+ * TODO(Implement auth)
+ */
+
+export const user = {
+ id: '62a29c15f4605c4c9fa7f306',
+}
diff --git a/src/index.scss b/src/index.scss
new file mode 100644
index 0000000..8267333
--- /dev/null
+++ b/src/index.scss
@@ -0,0 +1,21 @@
+@import url('https://fonts.googleapis.com/css?family=Roboto:300,400,500&display=swap');
+
+body {
+ margin: 0;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+}
+
+code {
+ font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
+ monospace;
+}
+
+
+* {
+ font-family: Roboto;
+}
+
+a {
+ cursor: pointer;
+}
\ No newline at end of file
diff --git a/src/index.tsx b/src/index.tsx
new file mode 100644
index 0000000..6b11d2e
--- /dev/null
+++ b/src/index.tsx
@@ -0,0 +1,17 @@
+import React from 'react'
+import ReactDOM from 'react-dom'
+import { Provider } from 'react-redux'
+import App from './App'
+import './index.scss'
+import { store } from './store/store'
+
+ReactDOM.render(
+
+
+
+
+ ,
+ document.getElementById('root'),
+)
+
+export {}
diff --git a/src/react-app-env.d.ts b/src/react-app-env.d.ts
new file mode 100644
index 0000000..6431bc5
--- /dev/null
+++ b/src/react-app-env.d.ts
@@ -0,0 +1 @@
+///
diff --git a/src/store/goalsSlice.ts b/src/store/goalsSlice.ts
new file mode 100644
index 0000000..1ed0276
--- /dev/null
+++ b/src/store/goalsSlice.ts
@@ -0,0 +1,39 @@
+import { createSlice, PayloadAction } from '@reduxjs/toolkit'
+import { Goal } from '../api/types'
+import { RootState } from './store'
+
+export interface GoalsState {
+ map: IdToGoal
+ list: string[]
+}
+
+export interface IdToGoal {
+ [id: string]: Goal
+}
+
+const initialState: GoalsState = {
+ map: {},
+ list: [],
+}
+
+export const goalsSlice = createSlice({
+ name: 'goal',
+ initialState,
+ reducers: {
+ createGoal: (state, action: PayloadAction) => {
+ state.map[action.payload.id] = action.payload
+ state.list.push(action.payload.id)
+ },
+
+ updateGoal: (state, action: PayloadAction) => {
+ state.map[action.payload.id] = action.payload
+ },
+ },
+})
+
+export const { createGoal, updateGoal } = goalsSlice.actions
+
+export const selectGoalsMap = (state: RootState) => state.goals.map
+export const selectGoalsList = (state: RootState) => state.goals.list
+
+export default goalsSlice.reducer
diff --git a/src/store/hooks.ts b/src/store/hooks.ts
new file mode 100644
index 0000000..597f281
--- /dev/null
+++ b/src/store/hooks.ts
@@ -0,0 +1,5 @@
+import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux'
+import type { AppDispatch, RootState } from './store'
+
+export const useAppDispatch = () => useDispatch()
+export const useAppSelector: TypedUseSelectorHook = useSelector
diff --git a/src/store/modalSlice.ts b/src/store/modalSlice.ts
new file mode 100644
index 0000000..c6e2996
--- /dev/null
+++ b/src/store/modalSlice.ts
@@ -0,0 +1,39 @@
+import { createSlice, PayloadAction } from '@reduxjs/toolkit'
+import { ModalContent, ModalType } from '../api/types'
+import { RootState } from './store'
+
+export interface ModalState {
+ isOpen: boolean
+ type: ModalType | null
+ content: ModalContent | null
+}
+
+const initialState: ModalState = {
+ isOpen: false,
+ type: null,
+ content: null,
+}
+
+export const modalSlice = createSlice({
+ name: 'modal',
+ initialState,
+ reducers: {
+ setContent: (state, action: PayloadAction) => {
+ state.content = action.payload
+ },
+ setIsOpen: (state, action: PayloadAction) => {
+ state.isOpen = action.payload
+ },
+ setType: (state, action: PayloadAction) => {
+ state.type = action.payload
+ },
+ },
+})
+
+export const { setContent, setIsOpen, setType } = modalSlice.actions
+
+export const selectIsOpen = (state: RootState) => state.modal.isOpen
+export const selectContent = (state: RootState) => state.modal.content
+export const selectType = (state: RootState) => state.modal.type
+
+export default modalSlice.reducer
diff --git a/src/store/store.ts b/src/store/store.ts
new file mode 100644
index 0000000..6741bd8
--- /dev/null
+++ b/src/store/store.ts
@@ -0,0 +1,23 @@
+import { Action, configureStore, ThunkAction } from '@reduxjs/toolkit'
+import goalsReducer from './goalsSlice'
+import modalReducer from './modalSlice'
+import themeReducer from './themeSlice'
+import userReducer from './userSlice'
+
+export const store = configureStore({
+ reducer: {
+ goals: goalsReducer,
+ modal: modalReducer,
+ theme: themeReducer,
+ user: userReducer,
+ },
+})
+
+export type AppDispatch = typeof store.dispatch
+export type RootState = ReturnType
+export type AppThunk = ThunkAction<
+ ReturnType,
+ RootState,
+ unknown,
+ Action
+>
diff --git a/src/store/themeSlice.ts b/src/store/themeSlice.ts
new file mode 100644
index 0000000..cc4ff15
--- /dev/null
+++ b/src/store/themeSlice.ts
@@ -0,0 +1,29 @@
+import { createSlice } from '@reduxjs/toolkit'
+import { RootState } from './store'
+
+export interface ThemeState {
+ mode: 'light' | 'dark'
+}
+
+const initialState: ThemeState = {
+ mode: 'dark',
+}
+
+export const themeSlice = createSlice({
+ name: 'theme',
+ initialState,
+ reducers: {
+ setLightMode: (state) => {
+ state.mode = 'light'
+ },
+ setDarkMode: (state) => {
+ state.mode = 'dark'
+ },
+ },
+})
+
+export const { setLightMode, setDarkMode } = themeSlice.actions
+
+export const selectMode = (state: RootState) => state.theme.mode
+
+export default themeSlice.reducer
diff --git a/src/store/userSlice.ts b/src/store/userSlice.ts
new file mode 100644
index 0000000..271ca3d
--- /dev/null
+++ b/src/store/userSlice.ts
@@ -0,0 +1,27 @@
+import { createSlice, PayloadAction } from '@reduxjs/toolkit'
+import { User } from '../api/types'
+import { RootState } from './store'
+
+export interface UserState {
+ user: User | null
+}
+
+const initialState: UserState = {
+ user: null,
+}
+
+export const userSlice = createSlice({
+ name: 'user',
+ initialState,
+ reducers: {
+ setUser: (state, action: PayloadAction) => {
+ state.user = action.payload
+ },
+ },
+})
+
+export const { setUser } = userSlice.actions
+
+export const selectUser = (state: RootState) => state.user.user
+
+export default userSlice.reducer
diff --git a/src/ui/colors.ts b/src/ui/colors.ts
new file mode 100644
index 0000000..8d77c98
--- /dev/null
+++ b/src/ui/colors.ts
@@ -0,0 +1,5 @@
+export const GREEN = 'rgba(151, 215, 0, 0.49)'
+export const YELLOW = 'rgba(254, 223, 3, 0.4)'
+export const ORANGE = 'rgba(255, 104, 0, 0.5)'
+export const BLUE = 'rgba(25, 123, 189, 0.46)'
+export const PURPLE = 'rgba(199, 35, 177, 0.49)'
diff --git a/src/ui/components/Card.tsx b/src/ui/components/Card.tsx
new file mode 100644
index 0000000..83921af
--- /dev/null
+++ b/src/ui/components/Card.tsx
@@ -0,0 +1,7 @@
+import styled from 'styled-components'
+import { Theme } from './Theme'
+
+export const Card = styled.div`
+ background-color: ${({ theme }: { theme: Theme }) => theme.cardBackground};
+ box-shadow: ${({ theme }: { theme: Theme }) => theme.boxShadow};
+`
diff --git a/src/ui/components/Chip.tsx b/src/ui/components/Chip.tsx
new file mode 100644
index 0000000..e3532bb
--- /dev/null
+++ b/src/ui/components/Chip.tsx
@@ -0,0 +1,32 @@
+import styled from 'styled-components'
+import { BLUE, GREEN, ORANGE, PURPLE, YELLOW } from '../colors'
+
+type ChipProps = { label: string }
+
+export default function Chip(props: ChipProps) {
+ return (
+
+ {props.label}
+
+ )
+}
+
+const ChipContainer = styled.div`
+ display: flex;
+ background-color: ${(props) => tagToColor[props.label]};
+ border-radius: 2rem;
+ padding: 1rem;
+ font-weight: bold;
+`
+
+interface TagToColor {
+ [label: string]: string
+}
+
+const tagToColor: TagToColor = {
+ Groceries: GREEN,
+ Restaurant: YELLOW,
+ Income: ORANGE,
+ Gas: BLUE,
+ Investment: PURPLE,
+}
diff --git a/src/ui/components/DatePicker.tsx b/src/ui/components/DatePicker.tsx
new file mode 100644
index 0000000..ec2ad59
--- /dev/null
+++ b/src/ui/components/DatePicker.tsx
@@ -0,0 +1,29 @@
+import DateFnsUtils from '@date-io/date-fns'
+import { KeyboardDatePicker, MuiPickersUtilsProvider } from '@material-ui/pickers'
+import { MaterialUiPickersDate } from '@material-ui/pickers/typings/date'
+import 'date-fns'
+import React from 'react'
+
+type Props = { value: Date | null; onChange: (date: MaterialUiPickersDate) => void }
+export default function DatePicker(props: Props) {
+ return (
+
+
+
+ )
+}
diff --git a/src/ui/components/EmojiPicker.tsx b/src/ui/components/EmojiPicker.tsx
new file mode 100644
index 0000000..00bb54d
--- /dev/null
+++ b/src/ui/components/EmojiPicker.tsx
@@ -0,0 +1,20 @@
+import { BaseEmoji, Picker } from 'emoji-mart'
+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 }
+
+export default function EmojiPicker(props: Props) {
+ const theme = useAppSelector(selectMode)
+
+ return (
+
+ )
+}
diff --git a/src/ui/components/GlobalStyles.tsx b/src/ui/components/GlobalStyles.tsx
new file mode 100644
index 0000000..e52fafd
--- /dev/null
+++ b/src/ui/components/GlobalStyles.tsx
@@ -0,0 +1,122 @@
+import { createGlobalStyle } from 'styled-components'
+import { Theme } from './Theme'
+
+export const GlobalStyle = createGlobalStyle`
+:root {
+ --shadow-md: 0 2px 4px 0 rgb(12 0 46 / 4%);
+ --shadow-lg: 0 10px 14px 0 rgb(12 0 46 / 6%);
+
+ --z-sticky: 7777;
+ --z-navbar: 8888;
+ --z-drawer: 9999;
+ --z-modal: 9999;
+}
+
+*,
+*::before,
+*::after {
+ box-sizing: border-box;
+}
+
+body,
+h1,
+h2,
+h3,
+h4,
+p,
+figure,
+blockquote,
+dl,
+dd {
+ margin: 0;
+}
+
+ul[role='list'],
+ol[role='list'] {
+ list-style: none;
+}
+
+html:focus-within {
+ scroll-behavior: smooth;
+}
+
+html {
+ -webkit-font-smoothing: antialiased;
+ touch-action: manipulation;
+ text-rendering: optimizelegibility;
+ text-size-adjust: 100%;
+ font-size: 62.5%;
+
+ @media (max-width: 37.5em) {
+ font-size: 50%;
+ }
+
+ @media (max-width: 48.0625em) {
+ font-size: 55%;
+ }
+
+ @media (max-width: 56.25em) {
+ font-size: 60%;
+ }
+}
+
+/* Set core body defaults */
+
+
+body {
+ min-height: 100vh;
+ line-height: 1.5;
+ font-family: var(--font);
+ color: ${({ theme }: { theme: Theme }) => theme.text};
+ background-color: ${({ theme }: { theme: Theme }) => theme.background};
+}
+
+.background {
+ background-color: ${({ theme }: { theme: Theme }) => theme.background};
+}
+
+.modal {
+ background-color: ${({ theme }: { theme: Theme }) => theme.modalBackground};
+}
+
+.alert {
+ color: ${({ theme }: { theme: Theme }) => theme.alertColor};
+}
+
+svg {
+ color: ${({ theme }: { theme: Theme }) => theme.text};
+}
+
+/* A elements that don't have a class get default styles */
+a:not([class]) {
+ text-decoration-skip-ink: auto;
+}
+
+img,
+picture {
+ max-width: 100%;
+ display: block;
+}
+
+input,
+button,
+textarea,
+select {
+ font: inherit;
+}
+
+@media (prefers-reduced-motion: reduce) {
+ html:focus-within {
+ scroll-behavior: auto;
+ }
+
+ *,
+ *::before,
+ *::after {
+ animation-duration: 0.01ms !important;
+ animation-iteration-count: 1 !important;
+ transition-duration: 0.01ms !important;
+ scroll-behavior: auto !important;
+ }
+
+}`
diff --git a/src/ui/components/Logo.tsx b/src/ui/components/Logo.tsx
new file mode 100644
index 0000000..ef9ef16
--- /dev/null
+++ b/src/ui/components/Logo.tsx
@@ -0,0 +1,10 @@
+import CommBank from '../assets/images/commbank.svg'
+
+type LogoProps = {
+ height: number
+ width: number
+}
+
+export default function Logo(props: LogoProps) {
+ return
+}
diff --git a/src/ui/components/SectionHeading.tsx b/src/ui/components/SectionHeading.tsx
new file mode 100644
index 0000000..82b2535
--- /dev/null
+++ b/src/ui/components/SectionHeading.tsx
@@ -0,0 +1,7 @@
+import styled from 'styled-components'
+
+export const SectionHeading = styled.h1`
+ font-weight: bold;
+ font-family: Roboto;
+ font-size: 3rem;
+`
diff --git a/src/ui/components/Theme.tsx b/src/ui/components/Theme.tsx
new file mode 100644
index 0000000..7cafa2a
--- /dev/null
+++ b/src/ui/components/Theme.tsx
@@ -0,0 +1,62 @@
+export type Theme = {
+ background: string
+ secondBackground: string
+ text: string
+ textSecondary: string
+ primary: string
+ secondary: string
+ tertiary: string
+ cardBackground: string
+ inputBackground: string
+ navbarBackground: string
+ modalBackground: string
+ errorColor: string
+ logoColor: string
+ alertColor: string
+ boxShadow: string
+ overlay: string
+}
+
+export const LightTheme: Theme = {
+ background: 'rgb(251,251,253)',
+ secondBackground: 'rgb(255,255,255)',
+ text: 'rgb(10,18,30)',
+ textSecondary: 'rgb(255,255,255)',
+ primary: 'rgb(22,115,255)',
+ secondary: 'rgb(10,18,30)',
+ tertiary: 'rgb(231,241,251)',
+ cardBackground: 'hsla(0, 100%, 100%, 0.09)',
+ inputBackground: 'rgb(255,255,255)',
+ navbarBackground: 'rgb(255,255,255)',
+ modalBackground: 'rgb(251,251,253)',
+ errorColor: 'rgb(207,34,46)',
+ logoColor: '#000',
+ alertColor: 'rgba(155, 0, 50, 1)',
+ boxShadow: `
+ 0px 12px 17px 2px hsla(0,0%,0%,0.14),
+ 0px 5px 22px 4px hsla(0,0%,0%,0.12),
+ 0px 7px 8px -4px hsla(0,0%,0%,0.2);`,
+ overlay: 'rgba(0,0,0,0.4)',
+}
+
+export const DarkTheme: Theme = {
+ background: 'rgba(18, 18, 18, 1)',
+ secondBackground: 'rgb(45,55,72)',
+ text: 'rgb(237,237,238)',
+ textSecondary: 'rgb(255,255,255)',
+ primary: '22,115,255',
+ secondary: 'rgb(10,18,30)',
+ tertiary: 'rgb(231,241,251)',
+ cardBackground: 'hsla(0, 100%, 100%, 0.09)',
+ inputBackground: 'rgb(45,55,72)',
+ navbarBackground: 'rgb(45,55,72)',
+ modalBackground: 'rgb(39, 39, 39)',
+ errorColor: 'rgb(207,34,46)',
+ logoColor: '#fff',
+ alertColor: 'rgba(155, 0, 50, 1)',
+ boxShadow: `
+ 0px 12px 17px 2px hsla(0,0%,0%,0.14),
+ 0px 5px 22px 4px hsla(0,0%,0%,0.12),
+ 0px 7px 8px -4px hsla(0,0%,0%,0.2);`,
+ overlay: 'rgba(0,0,0,0.7)',
+}
diff --git a/src/ui/components/TransparentButton.tsx b/src/ui/components/TransparentButton.tsx
new file mode 100644
index 0000000..561b504
--- /dev/null
+++ b/src/ui/components/TransparentButton.tsx
@@ -0,0 +1,10 @@
+import styled from 'styled-components'
+
+export const TransparentButton = styled.button`
+ background-color: transparent;
+ outline: none;
+ border: none;
+ padding: 0;
+ margin: 0;
+ cursor: pointer;
+`
diff --git a/src/ui/features/goalmanager/AddIconButton.tsx b/src/ui/features/goalmanager/AddIconButton.tsx
new file mode 100644
index 0000000..d0c8c2c
--- /dev/null
+++ b/src/ui/features/goalmanager/AddIconButton.tsx
@@ -0,0 +1,31 @@
+import { faSmile } from '@fortawesome/free-solid-svg-icons'
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
+import 'date-fns'
+import React from 'react'
+import styled from 'styled-components'
+import { TransparentButton } from '../../components/TransparentButton'
+
+type Props = { hasIcon: boolean; onClick: (event: React.MouseEvent) => void }
+
+export default function AddIconButton(props: Props) {
+ if (props.hasIcon) return null
+
+ return (
+
+
+
+ Add icon
+
+
+ )
+}
+
+const Container = styled.div`
+ flex-direction: row;
+ align-items: flex-end;
+`
+const Text = styled.span`
+ margin-left: 0.6rem;
+ font-size: 1.5rem;
+ color: rgba(174, 174, 174, 1);
+`
diff --git a/src/ui/features/goalmanager/GoalIcon.tsx b/src/ui/features/goalmanager/GoalIcon.tsx
new file mode 100644
index 0000000..b5a0d75
--- /dev/null
+++ b/src/ui/features/goalmanager/GoalIcon.tsx
@@ -0,0 +1,19 @@
+import 'date-fns'
+import React from 'react'
+import styled from 'styled-components'
+import { TransparentButton } from '../../components/TransparentButton'
+
+type Props = { icon: string | null; onClick: (e: React.MouseEvent) => void }
+
+export default function GoalIcon(props: Props) {
+ return (
+
+ {props.icon}
+
+ )
+}
+
+const Icon = styled.h1`
+ font-size: 6rem;
+ cursor: pointer;
+`
diff --git a/src/ui/features/goalmanager/GoalManager.tsx b/src/ui/features/goalmanager/GoalManager.tsx
new file mode 100644
index 0000000..5ad0208
--- /dev/null
+++ b/src/ui/features/goalmanager/GoalManager.tsx
@@ -0,0 +1,278 @@
+import { faCalendarAlt, faSmile } 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 { BaseEmoji } from 'emoji-mart'
+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 EmojiPicker from '../../components/EmojiPicker'
+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 [emojiPickerIsOpen, setEmojiPickerIsOpen] = useState(false)
+
+ useEffect(() => {
+ setName(props.goal.name)
+ setTargetDate(props.goal.targetDate)
+ setTargetAmount(props.goal.targetAmount)
+ setIcon(props.goal.icon)
+ }, [props.goal.id, props.goal.name, props.goal.targetDate, props.goal.targetAmount, props.goal.icon])
+
+ useEffect(() => {
+ setName(goal.name)
+ }, [goal.name])
+
+ const hasIcon = () => icon != null && icon !== ''
+
+ const addIconOnClick = (event: React.MouseEvent) => {
+ event.stopPropagation()
+ setEmojiPickerIsOpen(true)
+ }
+
+ const pickEmojiOnClick = (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))
+ updateGoalApi(props.goal.id, updatedGoal)
+ }
+
+ const updateNameOnChange = (event: React.ChangeEvent) => {
+ const nextName = event.target.value
+ setName(nextName)
+
+ const updatedGoal: Goal = {
+ ...props.goal,
+ icon: icon ?? props.goal.icon,
+ name: nextName,
+ }
+
+ 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,
+ icon: icon ?? props.goal.icon,
+ name: name ?? props.goal.name,
+ targetDate: targetDate ?? props.goal.targetDate,
+ targetAmount: nextTargetAmount,
+ }
+
+ dispatch(updateGoalRedux(updatedGoal))
+ updateGoalApi(props.goal.id, updatedGoal)
+ }
+
+ const pickDateOnChange = (date: MaterialUiPickersDate) => {
+ if (date != null) {
+ setTargetDate(date)
+
+ const updatedGoal: Goal = {
+ ...props.goal,
+ icon: icon ?? props.goal.icon,
+ name: name ?? props.goal.name,
+ targetDate: date ?? props.goal.targetDate,
+ targetAmount: targetAmount ?? props.goal.targetAmount,
+ }
+
+ dispatch(updateGoalRedux(updatedGoal))
+ updateGoalApi(props.goal.id, updatedGoal)
+ }
+ }
+
+ return (
+
+
+
+
+
+
+ Add icon
+
+
+
+
+ {icon}
+
+
+ event.stopPropagation()}
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {props.goal.balance}
+
+
+
+
+
+
+ {new Date(props.goal.created).toLocaleDateString()}
+
+
+
+ )
+}
+
+type FieldProps = { name: string; icon: IconDefinition }
+type AddIconButtonContainerProps = { shouldShow: boolean }
+type GoalIconContainerProps = { shouldShow: boolean }
+type EmojiPickerContainerProps = { isOpen: boolean; hasIcon: boolean }
+
+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 AddIconButtonContainer = styled.div`
+ display: ${(props) => (props.shouldShow ? 'flex' : 'none')};
+ margin-top: 1rem;
+`
+
+const TransparentButton = styled.button`
+ display: flex;
+ align-items: center;
+ background: transparent;
+ border: none;
+ cursor: pointer;
+ color: ${({ theme }: { theme: Theme }) => theme.text};
+`
+
+const AddIconButtonText = styled.span`
+ font-size: 1.5rem;
+ margin-left: 1rem;
+`
+
+const GoalIconContainer = styled.div`
+ display: ${(props) => (props.shouldShow ? 'flex' : 'none')};
+ margin-top: 1rem;
+`
+
+const GoalIcon = styled.div`
+ font-size: 4rem;
+ cursor: pointer;
+`
+
+const EmojiPickerContainer = styled.div`
+ display: ${(props) => (props.isOpen ? 'flex' : 'none')};
+ position: absolute;
+ top: ${(props) => (props.hasIcon ? '10rem' : '2rem')};
+ left: 0;
+ z-index: 10;
+`
+
+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;
+`
\ No newline at end of file
diff --git a/src/ui/features/themeswitcher/ThemeSwitcher.tsx b/src/ui/features/themeswitcher/ThemeSwitcher.tsx
new file mode 100644
index 0000000..556518f
--- /dev/null
+++ b/src/ui/features/themeswitcher/ThemeSwitcher.tsx
@@ -0,0 +1,25 @@
+import { faMoon, faSun } from '@fortawesome/free-regular-svg-icons'
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
+import React from 'react'
+import { useAppDispatch, useAppSelector } from '../../../store/hooks'
+import {
+ selectMode,
+ setDarkMode as setDarkModeRedux,
+ setLightMode as setLightModeRedux,
+} from '../../../store/themeSlice'
+
+export default function ThemeSwitcher() {
+ const mode = useAppSelector(selectMode)
+ const dispatch = useAppDispatch()
+
+ const onClick = () => {
+ if (mode === 'light') dispatch(setDarkModeRedux())
+ else dispatch(setLightModeRedux())
+ }
+
+ return (
+
+
+
+ )
+}
diff --git a/src/ui/pages/Main/Main.tsx b/src/ui/pages/Main/Main.tsx
new file mode 100644
index 0000000..714f091
--- /dev/null
+++ b/src/ui/pages/Main/Main.tsx
@@ -0,0 +1,78 @@
+import React from 'react'
+import styled from 'styled-components'
+import Drawer from '../../surfaces/drawer/Drawer'
+import Navbar from '../../surfaces/navbar/Navbar'
+import { media } from '../../utils/media'
+import AccountsSection from './accounts/AccountsSection'
+import GoalsSection from './goals/GoalsSection'
+import TransactionsSection from './transactions/TransactionsSection'
+
+export default function Main() {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ )
+}
+
+const Container = styled.div`
+ display: flex;
+ flex-direction: row;
+ width: 100vw;
+ height: 100vh;
+ background-color: rgb(var(--background));
+ overflow: hidden;
+`
+
+const MainSection = styled.div`
+ display: flex;
+ flex-direction: column;
+ width: calc(100% - 250px);
+ height: 100%;
+
+ ${media('<=tablet')} {
+ width: 100%;
+ overflow: scroll;
+ }
+`
+
+const Content = styled.div`
+ display: flex;
+ flex-direction: row;
+ width: 100%;
+ height: 100%;
+ justify-content: space-around;
+ align-items: center;
+
+ ${media('
+
+ Accounts
+
+
+
+ )
+}
+
+const Container = styled(Card)`
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ padding: 4rem 2rem;
+ width: 400px;
+ height: 300px;
+ border-radius: 2rem;
+ margin-top: 2rem;
+ margin-bottom: 2rem;
+
+ ${media(' {
+ event.stopPropagation()
+ dispatch(setContentRedux(goal))
+ dispatch(setTypeRedux('Goal'))
+ dispatch(setIsOpenRedux(true))
+ }
+
+ const asLocaleDateString = (date: Date) => new Date(date).toLocaleDateString()
+
+ return (
+
+ ${goal.targetAmount}
+ {asLocaleDateString(goal.targetDate)}
+
+ {/* Goal Icon */}
+ {goal.icon}
+
+ )
+}
+
+const Container = styled(Card)`
+ display: flex;
+ flex-direction: column;
+ min-height: 140px;
+ min-width: 140px;
+ width: 33%;
+ cursor: pointer;
+ margin-left: 2rem;
+ margin-right: 2rem;
+ border-radius: 2rem;
+
+ align-items: center;
+`
+
+const TargetAmount = styled.h2`
+ font-size: 2rem;
+`
+
+const TargetDate = styled.h4`
+ color: rgba(174, 174, 174, 1);
+ font-size: 1rem;
+`
+
+const Icon = styled.div`
+ font-size: 2rem;
+ margin-top: 0.5rem;
+`
\ No newline at end of file
diff --git a/src/ui/pages/Main/goals/GoalsContent.tsx b/src/ui/pages/Main/goals/GoalsContent.tsx
new file mode 100644
index 0000000..75c1f7f
--- /dev/null
+++ b/src/ui/pages/Main/goals/GoalsContent.tsx
@@ -0,0 +1,34 @@
+import React from 'react'
+import styled from 'styled-components'
+import { media } from '../../../utils/media'
+import GoalCard from './GoalCard'
+
+type Props = { ids: string[] | null }
+
+export default function GoalsContent(props: Props) {
+ if (!props.ids) return null
+
+ return (
+
+ {props.ids.map((id) => (
+
+ ))}
+
+ )
+}
+
+const Container = styled.div`
+ display: flex;
+ flex-direction: row;
+ justify-content: flex-start;
+ width: 400px;
+ padding: 4rem;
+ overflow-x: auto;
+
+ ${media(' {
+ async function fetch() {
+ const goals = await getGoals()
+ goals?.forEach((goal) => dispatch(createGoalRedux(goal)))
+ }
+ fetch()
+ }, [dispatch])
+
+ const onClick = async () => {
+ const goal = await createGoalApi()
+
+ if (goal != null) {
+ dispatch(createGoalRedux(goal))
+ dispatch(setContentRedux(goal))
+ dispatch(setTypeRedux('Goal'))
+ dispatch(setIsOpenRedux(true))
+ }
+ }
+
+ return (
+
+
+ Goals
+
+
+
+
+
+
+
+ )
+}
+
+const Container = styled.div`
+ display: flex;
+ flex-direction: column;
+ width: 400px;
+ margin-top: 2rem;
+ margin-bottom: 2rem;
+
+ ${media('(null)
+
+ useEffect(() => {
+ async function fetch(tagId: string): Promise {
+ const response = await axios.get(`${API_ROOT}/api/Tag/${tagId}`)
+ return response.data
+ }
+
+ async function fetchAll() {
+ const tags: Tag[] = []
+ for (const tagId of props.transaction.tagIds) {
+ const tag = await fetch(tagId)
+ tags.push(tag)
+ }
+
+ setTags(tags)
+ }
+
+ fetchAll()
+ })
+
+ return (
+
+
+ {props.transaction.description}
+
+ {tags ? tags.map((tag) => ) : null}
+
+ {`${new Date(
+ props.transaction.dateTime,
+ ).toLocaleDateString()}`}
+
+ {`${
+ props.transaction.transactionType === 'Credit'
+ ? `$${props.transaction.amount}`
+ : `-$${props.transaction.amount}`
+ }`}
+
+
+
+ )
+}
+
+const Container = styled.div`
+ display: flex;
+ flex-direction: column;
+`
+
+const Divider = styled.div`
+ width: 100%;
+ height: 0.2px;
+ background-color: rgba(174, 174, 174, 0.6);
+`
+
+const Content = styled.div`
+ display: flex;
+ flex-direction: row;
+ justify-content: space-between;
+ align-items: center;
+
+ h6 {
+ font-size: 1.2rem;
+ }
+
+ h6.datetime {
+ color: rgba(174, 174, 174, 1);
+ font-weight: bold;
+ }
+`
diff --git a/src/ui/pages/Main/transactions/TransactionsContent.tsx b/src/ui/pages/Main/transactions/TransactionsContent.tsx
new file mode 100644
index 0000000..d898517
--- /dev/null
+++ b/src/ui/pages/Main/transactions/TransactionsContent.tsx
@@ -0,0 +1,20 @@
+import React from 'react'
+import { Transaction } from '../../../../api/types'
+import { TransactionItem } from './TransactionItem'
+
+type Props = { transactions: Transaction[] | null }
+
+export default function TransactionsContent(props: Props) {
+ if (!props.transactions) return null
+ return (
+ <>
+ {props.transactions.sort(sortByDateDesc).map((transaction) => (
+
+ ))}
+ >
+ )
+}
+
+function sortByDateDesc(a: Transaction, b: Transaction) {
+ return new Date(b.dateTime).getTime() - new Date(a.dateTime).getTime()
+}
diff --git a/src/ui/pages/Main/transactions/TransactionsSection.tsx b/src/ui/pages/Main/transactions/TransactionsSection.tsx
new file mode 100644
index 0000000..06b5530
--- /dev/null
+++ b/src/ui/pages/Main/transactions/TransactionsSection.tsx
@@ -0,0 +1,78 @@
+import React, { useEffect, useState } from 'react'
+import styled from 'styled-components'
+import { getTransactions as getTransactionsApi } from '../../../../api/lib'
+import { Transaction } from '../../../../api/types'
+import { Card } from '../../../components/Card'
+import { SectionHeading } from '../../../components/SectionHeading'
+import { TransparentButton } from '../../../components/TransparentButton'
+import { media } from '../../../utils/media'
+import TransactionsContent from './TransactionsContent'
+
+export default function TransactionsSection() {
+ const [transactions, setTransactions] = useState(null)
+
+ useEffect(() => {
+ async function fetch() {
+ const response = await getTransactionsApi()
+
+ if (response !== null) {
+ setTransactions(response)
+ }
+ }
+
+ fetch()
+ }, [])
+
+ return (
+
+
+ Recent Transactions
+
+
+ See All
+
+
+
+
+
+ )
+}
+
+const Container = styled(Card)`
+ display: flex;
+ flex-direction: column;
+ width: 400px;
+ min-height: 400px;
+ height: 80%;
+ padding: 4rem 2rem;
+ overflow-y: hidden;
+ border-radius: 2rem;
+ margin-top: 2rem;
+ margin-bottom: 2rem;
+
+ ${media('
+
+
+
+
+
+
+
+ Dashboard
+
+
+
+
+ Goals
+
+
+
+
+
+ )
+}
+
+const DrawerContainer = styled.div`
+ display: flex;
+ flex-direction: column;
+ justify-content: space-between;
+ overflow: hidden;
+
+ padding: 1.5rem 1.5rem;
+ width: 250px;
+ height: 100%;
+ background-color: rgb(var(--primary));
+ z-index: 2;
+
+ box-shadow: ${({ theme }) => theme.boxShadow};
+
+ ${media('`
+ display: flex;
+ flex-direction: row;
+ justify-content: flex-start;
+ align-items: center;
+ padding: 1rem;
+ width: 100%;
+ border-radius: 12px;
+ margin-top: 4rem;
+ background-color: ${(props) => (props.isSelected ? 'rgba(254, 223, 3, 0.4)' : '')};
+ color: ${(props) => (props.isSelected ? '' : 'rgba(174, 174, 174, 1)')};
+ font-weight: ${(props) => (props.isSelected ? 'bold' : 'light')};
+ span {
+ font-size: 2rem;
+ margin-left: 1rem;
+ }
+
+ svg {
+ color: ${(props) => (props.isSelected ? '' : 'rgba(174, 174, 174, 1)')};
+ }
+`
+
+const LogoWrapper = styled.a`
+ display: flex;
+ margin-right: auto;
+ text-decoration: none;
+ flex-direction: row;
+ justify-content: center;
+ width: 100%;
+
+ margin-bottom: 10rem;
+`
+
+const Logo = styled.img`
+ width: 100px;
+ height: 100px;
+ ${media(' {
+ switch (type) {
+ case 'Goal':
+ return
+ }
+ }
+
+ if (!isOpen || content === null) return null
+
+ const onClick = (event: React.MouseEvent) => event.stopPropagation()
+
+ return {renderSwitch()}
+}
+
+export const Container = styled.div`
+ width: 85%;
+ max-width: 1000px;
+ height: 85%;
+ max-width: 1000px;
+ background-color: ${({ theme }) => theme.modalBackground};
+ border-radius: 2rem;
+ padding: 8rem;
+ z-index: 100;
+`
diff --git a/src/ui/surfaces/navbar/Navbar.tsx b/src/ui/surfaces/navbar/Navbar.tsx
new file mode 100644
index 0000000..35c8358
--- /dev/null
+++ b/src/ui/surfaces/navbar/Navbar.tsx
@@ -0,0 +1,88 @@
+import { faBell, faEnvelope } from '@fortawesome/free-regular-svg-icons'
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
+import React from 'react'
+import styled from 'styled-components'
+import Tag from '../../../assets/images/tag.png'
+import { useAppSelector } from '../../../store/hooks'
+import { selectUser } from '../../../store/userSlice'
+import ThemeSwitcher from '../../features/themeswitcher/ThemeSwitcher'
+import { media } from '../../utils/media'
+
+export default function Navbar() {
+ const user = useAppSelector(selectUser)
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {user?.name}
+ {user?.email}
+
+
+
+
+ )
+}
+
+const Container = styled.div`
+ display: flex;
+ flex-direction: row;
+ justify-content: flex-end;
+ position: sticky;
+ top: 0;
+ padding: 1.5rem 1.5rem;
+ width: 100%;
+ height: 8rem;
+`
+
+const NavbarActions = styled.div`
+ display: flex;
+ flex-direction: row;
+ justify-content: space-around;
+ align-items: center;
+`
+
+const NavbarAction = styled.a`
+ margin: 0.8rem;
+`
+
+const Avatar = styled.img`
+ width: 50px;
+ height: 50px;
+ border-radius: 50%;
+`
+
+const UserGroup = styled.div`
+ display: flex;
+ flex-direction: row;
+ justify-content: space-around;
+ align-items: center;
+`
+
+const UserNameAndEmailGroup = styled.div`
+ display: flex;
+ flex-direction: column;
+ justify-content: space-around;
+ align-items: flex-start;
+
+ ${media('