👷♀️
This commit is contained in:
parent
5b4726bee8
commit
8c40f30286
5
frontend/package-lock.json
generated
5
frontend/package-lock.json
generated
|
@ -1245,6 +1245,11 @@
|
|||
"resolved": "https://registry.npmjs.org/@dank-inc/data-buddy/-/data-buddy-0.1.4.tgz",
|
||||
"integrity": "sha512-+t/Ctm4Ik2Lh1H9pt8DIDLHCPnRNz38ZcNXu9U+VFZOrLut7wxRoKqdt6IFz6MGwVJtuveJe2dGPCme3N3hryg=="
|
||||
},
|
||||
"@dank-inc/use-get": {
|
||||
"version": "0.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@dank-inc/use-get/-/use-get-0.3.1.tgz",
|
||||
"integrity": "sha512-O7QREvSarFsARw+DKRip0bTuGRMjlKqQ31FdSq+8tgsBiUDXJQ1OEdumT18mK9DRG4VmVqrvQrxTKcH10cIwxw=="
|
||||
},
|
||||
"@eslint/eslintrc": {
|
||||
"version": "0.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-0.3.0.tgz",
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
"private": true,
|
||||
"dependencies": {
|
||||
"@dank-inc/data-buddy": "^0.1.4",
|
||||
"@dank-inc/use-get": "^0.3.1",
|
||||
"@testing-library/jest-dom": "^5.11.4",
|
||||
"@testing-library/react": "^11.1.0",
|
||||
"@testing-library/user-event": "^12.1.10",
|
||||
|
@ -25,6 +26,7 @@
|
|||
"start": "react-scripts start",
|
||||
"build": "react-scripts build",
|
||||
"test": "react-scripts test",
|
||||
"lint": "tsc",
|
||||
"eject": "react-scripts eject"
|
||||
},
|
||||
"eslintConfig": {
|
||||
|
|
|
@ -7,7 +7,7 @@ import { Api } from './api'
|
|||
import './scss/app.scss'
|
||||
|
||||
const App = () => {
|
||||
const api = new Api({ mock: true, baseURL: '/api' })
|
||||
const api = new Api({ baseURL: '/api' })
|
||||
|
||||
return (
|
||||
<BrowserRouter>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import Axios, { AxiosInstance } from 'axios'
|
||||
import { Stack, Transaction, User, uuid } from '../types'
|
||||
import { Account, Password, Stack, Transaction, User, uuid } from '../types'
|
||||
import { JWT, setJWT, wipeJWT } from '../utils/jwt'
|
||||
import { DataBuddy } from '@dank-inc/data-buddy'
|
||||
import { users } from './data/users'
|
||||
|
@ -53,21 +53,39 @@ export class Api {
|
|||
return data
|
||||
}
|
||||
|
||||
getAccounts = async () => {
|
||||
this.axios.get('accounts')
|
||||
updateUser = async (id: uuid, body: Partial<User>) => {
|
||||
if (this.mock) return this.users.update(id, body)
|
||||
const { data } = await this.axios.patch<User>(`users/${id}`, body)
|
||||
return data
|
||||
}
|
||||
|
||||
createUser = async (body: Omit<User, 'id'> & Password) => {
|
||||
const { data } = await this.axios.post<User>(`users`, body)
|
||||
return data
|
||||
}
|
||||
|
||||
getAccounts = async () => {
|
||||
const { data } = await this.axios.get<Account[]>('accounts')
|
||||
return data
|
||||
}
|
||||
|
||||
getAccount = async (id: uuid) => {
|
||||
this.axios.get(`accounts/${id}`)
|
||||
const data = await this.axios.get<Account>(`accounts/${id}`)
|
||||
return data
|
||||
}
|
||||
updateAccount = async (id: uuid, body: Partial<Account>) => {
|
||||
const { data } = await this.axios.patch<Account>(`accounts/${id}`, body)
|
||||
return data
|
||||
}
|
||||
createAccount = async () => {}
|
||||
createAccount = async (body: Omit<Account, 'id'>) => {
|
||||
const { data } = await this.axios.post<Transaction>('accounts', body)
|
||||
return data
|
||||
}
|
||||
deleteAccount = async () => {}
|
||||
|
||||
getStacks = async () => {
|
||||
this.axios.get('stacks')
|
||||
getStacks = async (): Promise<Stack[]> => {
|
||||
const { data } = await this.axios.get('stacks')
|
||||
return data
|
||||
}
|
||||
updateStack = async (id: uuid, body: Partial<Stack>) => {
|
||||
const { data } = await this.axios.patch<Stack>(`stacks/${id}`, body)
|
||||
|
|
|
@ -1,37 +1,42 @@
|
|||
import { Layout } from 'antd'
|
||||
import { useUserContext } from '../contexts/UserContext'
|
||||
import { Login } from './pages/Login'
|
||||
import { Route, Switch } from 'react-router'
|
||||
import { Redirect, Route, Switch } from 'react-router'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { Dashboard } from './pages/Dashboard'
|
||||
import { AppHeader } from './layout/AppHeader'
|
||||
import { Profile } from './pages/Profile'
|
||||
import { NewUser } from './forms/NewUser'
|
||||
import { ForgotPassword } from './pages/ForgotPassword'
|
||||
import { UserForm } from './components/UserForm'
|
||||
import { TransactionList } from './components/TransactionList'
|
||||
import { AccountForm } from './components/AccountForm'
|
||||
|
||||
export const CoreLayout = () => {
|
||||
const { user } = useUserContext()
|
||||
const { user, accounts, selectedAccount } = useUserContext()
|
||||
|
||||
if (!user)
|
||||
return (
|
||||
<Layout className="layout">
|
||||
<AppHeader user={user} />
|
||||
<Login />
|
||||
</Layout>
|
||||
)
|
||||
if (!accounts?.length) <Redirect to="/account/new" />
|
||||
|
||||
// header, sidebar, avatar?
|
||||
return (
|
||||
<Layout className="layout">
|
||||
<AppHeader user={user} />
|
||||
<Layout.Content>
|
||||
<Switch>
|
||||
<Route path="/forgot-password" component={ForgotPassword} />
|
||||
<Route path="/login" component={Login} />
|
||||
<Route path="/new/user" component={NewUser} />
|
||||
<Route path="/profile" component={Profile} />
|
||||
<Route exact path="/" component={Dashboard} />
|
||||
</Switch>
|
||||
</Layout.Content>
|
||||
</Layout>
|
||||
<div className="app" id="appElement">
|
||||
<nav>
|
||||
<Link to="/">Home</Link>
|
||||
<Link to="/select">Select Budget</Link>
|
||||
<Link to="/account">Budget Details</Link>
|
||||
<Link to="/details">Transactions</Link>
|
||||
<Link to="/user">Profile</Link>
|
||||
</nav>
|
||||
|
||||
<Switch>
|
||||
<Route path="/user" component={UserForm} />
|
||||
<Route path="/details" component={TransactionList} />
|
||||
<Route path="/account/new" component={AccountForm} />
|
||||
<Route path="/account" component={AccountForm} />
|
||||
<Route path="/" component={Dashboard} />
|
||||
</Switch>
|
||||
|
||||
{/* {showingModal && <TransactionModal account={account} />} */}
|
||||
|
||||
<footer>
|
||||
<p>User: {user?.name}</p>
|
||||
<p>|</p>
|
||||
<p>Budget: {selectedAccount?.name}</p>
|
||||
<p>|</p>
|
||||
</footer>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
92
frontend/src/app/components/AccountForm.tsx
Normal file
92
frontend/src/app/components/AccountForm.tsx
Normal file
|
@ -0,0 +1,92 @@
|
|||
import React, { useState } from 'react'
|
||||
import { Account } from '../../types'
|
||||
|
||||
import '../../scss/form.scss'
|
||||
import { useAppContext } from '../../contexts/AppContext'
|
||||
import { message } from 'antd'
|
||||
import { usePromise } from '@dank-inc/use-get'
|
||||
|
||||
type Props = {
|
||||
account?: Account
|
||||
}
|
||||
|
||||
export const AccountForm = ({ account }: Props) => {
|
||||
const { api } = useAppContext()
|
||||
|
||||
const stacks = usePromise(api.getStacks())
|
||||
|
||||
const [name, setName] = useState<string>(account?.name || '')
|
||||
const [details, setDetails] = useState<string>(account?.details || '')
|
||||
const [income, setIncome] = useState<number>(account?.income || 0)
|
||||
const [expenses, setExpenses] = useState<number>(account?.expenses || 0)
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
if (!name || !income || !expenses) message.error(`fill out all the fields!`)
|
||||
|
||||
const body = {
|
||||
name,
|
||||
income,
|
||||
expenses,
|
||||
details,
|
||||
}
|
||||
|
||||
account?.id
|
||||
? await api.updateAccount(account.id, body)
|
||||
: await api.createAccount(body)
|
||||
|
||||
message.success('Yaaa')
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<h1>{account?.id ? 'Budget Details' : 'Create Budget'}</h1>
|
||||
<form className="dank-form" onSubmit={handleSubmit}>
|
||||
<h3>General</h3>
|
||||
<label>
|
||||
Name:
|
||||
<input onChange={(e) => setName(e.target.value)} value={name}></input>
|
||||
</label>
|
||||
<label>
|
||||
Description (optional):
|
||||
<input
|
||||
type="text"
|
||||
onChange={(e) => setDetails(e.target.value)}
|
||||
value={details}
|
||||
></input>
|
||||
</label>
|
||||
<h3>Income & Expenses</h3>
|
||||
<label>
|
||||
Monthly Income:
|
||||
<input
|
||||
onChange={(e) => setIncome(parseInt(e.target.value) || 0)}
|
||||
value={income}
|
||||
></input>
|
||||
</label>
|
||||
<label>
|
||||
Monthly Expenses:
|
||||
<input
|
||||
onChange={(e) => setExpenses(parseInt(e.target.value) || 0)}
|
||||
value={expenses}
|
||||
></input>
|
||||
</label>
|
||||
<h3>Budgets</h3>
|
||||
{stacks.data?.map((stack) => (
|
||||
<div className="form-item">
|
||||
<label>{stack.name}</label>
|
||||
<input
|
||||
type="number"
|
||||
value={stack.amount}
|
||||
onChange={(e) =>
|
||||
api.updateStack(stack.id, {
|
||||
amount: parseInt(e.target.value),
|
||||
})
|
||||
}
|
||||
></input>
|
||||
</div>
|
||||
))}
|
||||
<button type="submit">{account?.id ? 'Save' : 'Create'}</button>
|
||||
</form>
|
||||
</>
|
||||
)
|
||||
}
|
37
frontend/src/app/components/AccountSelect.tsx
Normal file
37
frontend/src/app/components/AccountSelect.tsx
Normal file
|
@ -0,0 +1,37 @@
|
|||
import { Link } from 'react-router-dom'
|
||||
import { message } from 'antd'
|
||||
import { useUserContext } from '../../contexts/UserContext'
|
||||
|
||||
import '../../scss/account-select.scss'
|
||||
|
||||
type Props = {
|
||||
selectProfile: (id: string) => void
|
||||
}
|
||||
|
||||
export const AccountSelect = ({ selectProfile }: Props) => {
|
||||
const { accounts } = useUserContext()
|
||||
|
||||
const handleSelect = (id: string) => {
|
||||
selectProfile(id)
|
||||
message.success(`Selected Account: ${id}`)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<h1>Select Account</h1>
|
||||
<div className="account-select">
|
||||
{accounts?.length
|
||||
? accounts.map((account) => (
|
||||
<button
|
||||
key={`account-${account.name}`}
|
||||
onClick={() => handleSelect(account.id)}
|
||||
>
|
||||
{account.name}
|
||||
</button>
|
||||
))
|
||||
: ''}
|
||||
<Link to="/account/new">Create New Budget!</Link>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
40
frontend/src/app/components/FundBar.tsx
Normal file
40
frontend/src/app/components/FundBar.tsx
Normal file
|
@ -0,0 +1,40 @@
|
|||
import { Stack } from '../../types'
|
||||
import '../../scss/fund-bar.scss'
|
||||
|
||||
type Props = {
|
||||
col: number
|
||||
stack: Stack
|
||||
}
|
||||
|
||||
export const FundBar = ({ stack, col }: Props) => {
|
||||
const amount = 0
|
||||
|
||||
const max = stack.amount
|
||||
const current = max - amount
|
||||
const percent = Math.max(current / max, 0)
|
||||
const hue = percent * 120
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className={`fundbar back col${col}`}
|
||||
onClick={() => console.log(`adding transaction to => ${stack.name}`)}
|
||||
></div>
|
||||
<div
|
||||
className={`fundbar front col${col}`}
|
||||
style={{
|
||||
background: `hsl(${hue}, 100%, 50%)`,
|
||||
height: `${percent * 40 + 10}vmin`,
|
||||
}}
|
||||
>
|
||||
<h3>{stack.name}</h3>
|
||||
</div>
|
||||
<div
|
||||
className={`fundbar totals col${col}`}
|
||||
style={{ color: `hsl(0, 0%, ${Math.abs(1 - percent) * 100}%)` }}
|
||||
>
|
||||
${Math.floor(current)} / ${max}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
71
frontend/src/app/components/Login.tsx
Normal file
71
frontend/src/app/components/Login.tsx
Normal file
|
@ -0,0 +1,71 @@
|
|||
import React, { useState, useEffect } from 'react'
|
||||
import Axios from 'axios'
|
||||
import { Link } from 'react-router-dom'
|
||||
|
||||
type Props = {
|
||||
handleLogin: (v: string) => void
|
||||
}
|
||||
|
||||
export const Login = ({ handleLogin }: Props) => {
|
||||
const [email, setEmail] = useState('')
|
||||
const [password, setPassword] = useState('')
|
||||
const [valid, setValid] = useState(false)
|
||||
const [error, setError] = useState('')
|
||||
|
||||
useEffect(() => {
|
||||
if (window.localStorage.userId) handleLogin(window.localStorage.userId)
|
||||
}, [handleLogin])
|
||||
|
||||
useEffect(() => {
|
||||
email && password ? setValid(true) : setValid(false)
|
||||
}, [email, password])
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
handleLogin(email)
|
||||
return
|
||||
}
|
||||
if (!email || !password) {
|
||||
setError('please fill out both fields')
|
||||
return
|
||||
}
|
||||
try {
|
||||
const { data } = await Axios.post('/api/login', { email, password })
|
||||
handleLogin(data.id)
|
||||
window.localStorage.userId = data.id
|
||||
} catch (err) {
|
||||
setError(err.message)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="login">
|
||||
<form onSubmit={handleSubmit}>
|
||||
<label>
|
||||
Type Yo Email:
|
||||
<input
|
||||
autoFocus
|
||||
onChange={e => setEmail(e.target.value)}
|
||||
value={email}
|
||||
></input>
|
||||
</label>
|
||||
<label>
|
||||
Type Yo Password:
|
||||
<input
|
||||
type="password"
|
||||
onChange={e => setPassword(e.target.value)}
|
||||
value={password}
|
||||
></input>
|
||||
</label>
|
||||
<label>
|
||||
<button disabled={!valid} type="submit">
|
||||
Submit
|
||||
</button>
|
||||
</label>
|
||||
{error && <p>{error}</p>}
|
||||
<Link to="/sign-up">No Account? Sign Up!</Link>
|
||||
</form>
|
||||
</div>
|
||||
)
|
||||
}
|
18
frontend/src/app/components/TransactionList.tsx
Normal file
18
frontend/src/app/components/TransactionList.tsx
Normal file
|
@ -0,0 +1,18 @@
|
|||
import '../../scss/transaction-list.scss'
|
||||
|
||||
export const TransactionList = () => {
|
||||
return (
|
||||
<>
|
||||
<h1>Transactions</h1>
|
||||
<div className="transactions">
|
||||
{[].map((account, i) => {
|
||||
return (
|
||||
<div className="column" key={`${account}-txs-${i}`}>
|
||||
<h3>{account}</h3>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
65
frontend/src/app/components/TransactionModal.tsx
Normal file
65
frontend/src/app/components/TransactionModal.tsx
Normal file
|
@ -0,0 +1,65 @@
|
|||
import { useState } from 'react'
|
||||
|
||||
import '../../scss/transaction-modal.scss'
|
||||
import { useAppContext } from '../../contexts/AppContext'
|
||||
import { uuid } from '../../types'
|
||||
|
||||
type Props = {
|
||||
stackId: uuid
|
||||
}
|
||||
|
||||
export const TransactionModal = ({ stackId }: Props) => {
|
||||
const { api } = useAppContext()
|
||||
|
||||
const [amount, setAmount] = useState('')
|
||||
const [stack, setStack] = useState(stackId)
|
||||
const [details, setDetails] = useState('')
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
|
||||
if (parseFloat(amount)) {
|
||||
api.createTransaction({
|
||||
stack,
|
||||
details,
|
||||
amount: parseFloat(amount),
|
||||
created_at: new Date().toISOString(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="transaction-modal">
|
||||
<form onSubmit={handleSubmit} className="form">
|
||||
<div className="cancel-button">X</div>
|
||||
<h3>Expense for {stack} account</h3>
|
||||
<label>
|
||||
Amount:
|
||||
<input
|
||||
autoFocus
|
||||
value={amount}
|
||||
onChange={(e) => setAmount(e.currentTarget.value)}
|
||||
></input>
|
||||
</label>
|
||||
<label>
|
||||
Details:
|
||||
<input
|
||||
value={details}
|
||||
onChange={(e) => setDetails(e.currentTarget.value)}
|
||||
></input>
|
||||
</label>
|
||||
<label>
|
||||
<select value={stack} onChange={(e) => setStack(e.target.value)}>
|
||||
<option value={-1} disabled>
|
||||
Select Account
|
||||
</option>
|
||||
</select>
|
||||
<button type="button">cancel</button>
|
||||
<button type="submit">Submit</button>
|
||||
</label>
|
||||
</form>
|
||||
{error && <div className="error">{error}</div>}
|
||||
</div>
|
||||
)
|
||||
}
|
90
frontend/src/app/components/UserForm.tsx
Normal file
90
frontend/src/app/components/UserForm.tsx
Normal file
|
@ -0,0 +1,90 @@
|
|||
import { useState, useEffect } from 'react'
|
||||
|
||||
import { Password, User } from '../../types'
|
||||
import '../../scss/form.scss'
|
||||
import { useUserContext } from '../../contexts/UserContext'
|
||||
import { useAppContext } from '../../contexts/AppContext'
|
||||
import { useHistory } from 'react-router'
|
||||
import { message } from 'antd'
|
||||
|
||||
export const UserForm = () => {
|
||||
const history = useHistory()
|
||||
const { api } = useAppContext()
|
||||
const { user } = useUserContext()
|
||||
|
||||
const [name, setName] = useState(user?.name)
|
||||
const [email, setEmail] = useState(user?.email)
|
||||
const [password, setPassword] = useState('')
|
||||
const [passwordConfirm, setPasswordConfirm] = useState('')
|
||||
const [valid, setValid] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
passwordConfirm.length > 4 && password === passwordConfirm && name && email
|
||||
? setValid(true)
|
||||
: setValid(false)
|
||||
}, [password, passwordConfirm, name, email])
|
||||
|
||||
// @ts-ignore
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault()
|
||||
|
||||
if (!name || !email) {
|
||||
message.error('Name and email required!')
|
||||
return
|
||||
}
|
||||
|
||||
const body: Omit<User, 'id'> & Password = {
|
||||
name,
|
||||
email,
|
||||
password,
|
||||
passwordConfirm,
|
||||
}
|
||||
|
||||
try {
|
||||
user?.id
|
||||
? await api.updateUser(user.id, body)
|
||||
: await api.createUser(body)
|
||||
|
||||
if (!user?.id) history.push('/login')
|
||||
} catch (err) {
|
||||
message.error('Something went wrong')
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<h1>{user?.id ? 'Edit' : 'Create'} Profile</h1>
|
||||
<form className="dank-form" onSubmit={handleSubmit}>
|
||||
<label>
|
||||
Name: <input value={name} onChange={(e) => setName(e.target.value)} />
|
||||
</label>
|
||||
<label>
|
||||
Email:
|
||||
<input
|
||||
value={email}
|
||||
type="email"
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
Password:
|
||||
<input
|
||||
value={password}
|
||||
type="password"
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
Confirm Password:
|
||||
<input
|
||||
type="password"
|
||||
onChange={(e) => setPasswordConfirm(e.target.value)}
|
||||
/>
|
||||
</label>
|
||||
<button type="submit" disabled={!valid}>
|
||||
Save
|
||||
</button>
|
||||
</form>
|
||||
</>
|
||||
)
|
||||
}
|
|
@ -38,7 +38,7 @@ export const AppHeader = ({ user }: Props) => {
|
|||
<Button onClick={() => history.push('/new/user')}>New User</Button>
|
||||
</div>
|
||||
<div className="header-right">
|
||||
<p>Welcome, {user.username}!!</p>
|
||||
<p>Welcome, {user.name}!!</p>
|
||||
<Dropdown
|
||||
overlay={
|
||||
<Menu style={{ display: 'flex', flexDirection: 'column' }}>
|
||||
|
@ -51,7 +51,7 @@ export const AppHeader = ({ user }: Props) => {
|
|||
</Menu>
|
||||
}
|
||||
>
|
||||
<Avatar>{user.username[0]}</Avatar>
|
||||
<Avatar>{user.name[0]}</Avatar>
|
||||
</Dropdown>
|
||||
</div>
|
||||
</Header>
|
||||
|
|
|
@ -1,3 +1,21 @@
|
|||
import { FundBar } from '../components/FundBar'
|
||||
import { usePromise } from '@dank-inc/use-get'
|
||||
import { useAppContext } from '../../contexts/AppContext'
|
||||
|
||||
export const Dashboard = () => {
|
||||
return <p>a dashboard</p>
|
||||
const { api } = useAppContext()
|
||||
const stacks = usePromise(api.getStacks())
|
||||
|
||||
if (stacks.loading || !stacks.data) return <div>loading...</div>
|
||||
|
||||
return (
|
||||
<>
|
||||
<h1>Remaining Balances</h1>
|
||||
<div className="funds">
|
||||
{stacks.data.map((stack, i) => (
|
||||
<FundBar stack={stack} col={i + 1} />
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,16 +1,7 @@
|
|||
import React, {
|
||||
createContext,
|
||||
useContext,
|
||||
useState,
|
||||
useEffect,
|
||||
Dispatch,
|
||||
SetStateAction,
|
||||
} from 'react'
|
||||
import React, { createContext, useContext, useState } from 'react'
|
||||
import { message } from 'antd'
|
||||
|
||||
import { useHistory } from 'react-router'
|
||||
|
||||
import { User } from '../types'
|
||||
import { Account, User, uuid } from '../types'
|
||||
import { useAppContext } from './AppContext'
|
||||
import { logOut } from '../api'
|
||||
|
||||
|
@ -20,9 +11,11 @@ type Props = {
|
|||
|
||||
type Context = {
|
||||
user: User | null
|
||||
setUser: Dispatch<SetStateAction<User | null>>
|
||||
accounts: Account[] | null
|
||||
selectedAccount: Account | null
|
||||
handleLogin: (username: string, password: string) => void
|
||||
handleLogout: () => void
|
||||
handleSelectAccount: (id: uuid) => void
|
||||
}
|
||||
|
||||
const UserContext = createContext<Context | null>(null)
|
||||
|
@ -32,15 +25,19 @@ export const UserContextProvider = ({ children }: Props) => {
|
|||
const history = useHistory()
|
||||
|
||||
const [user, setUser] = useState<User | null>(null)
|
||||
const [accounts, setAccounts] = useState<Account[] | null>(null)
|
||||
const [selectedAccount, setSelectedAccount] = useState<Account | null>(null)
|
||||
|
||||
const handleLogin = async (username: string, password: string) => {
|
||||
try {
|
||||
const { id } = await api.login(username, password)
|
||||
if (!id) throw new Error('Problem logging in!')
|
||||
const user = await api.getUser(id)
|
||||
setUser(await api.getUser(id))
|
||||
|
||||
setUser(user)
|
||||
message.success(`logged in as ${user?.username}`, 0.5)
|
||||
const accounts = await api.getAccounts()
|
||||
setAccounts(accounts)
|
||||
|
||||
message.success(`logged in as ${user?.name}`, 0.5)
|
||||
} catch (err) {
|
||||
message.error('Login Failed!')
|
||||
}
|
||||
|
@ -53,8 +50,22 @@ export const UserContextProvider = ({ children }: Props) => {
|
|||
history.push('/')
|
||||
}
|
||||
|
||||
const handleSelectAccount = (id: string) => {
|
||||
const account = accounts?.find((account) => account.id === id)
|
||||
if (account) setSelectedAccount(account)
|
||||
}
|
||||
|
||||
return (
|
||||
<UserContext.Provider value={{ user, setUser, handleLogin, handleLogout }}>
|
||||
<UserContext.Provider
|
||||
value={{
|
||||
user,
|
||||
accounts,
|
||||
handleLogin,
|
||||
handleLogout,
|
||||
selectedAccount,
|
||||
handleSelectAccount,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</UserContext.Provider>
|
||||
)
|
||||
|
|
6
frontend/src/scss/account-select.scss
Normal file
6
frontend/src/scss/account-select.scss
Normal file
|
@ -0,0 +1,6 @@
|
|||
.account-select {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-around;
|
||||
width: 100%;
|
||||
}
|
|
@ -1,50 +1,161 @@
|
|||
.ant-layout-header.app-header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
vertical-align: middle;
|
||||
padding: 0 1rem;
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5 {
|
||||
color: #222;
|
||||
text-shadow: -1px -1px #444;
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
a {
|
||||
font-size: 1.2rem;
|
||||
font-weight: bold;
|
||||
.col1 {
|
||||
grid-column: 1/1;
|
||||
grid-row: 1/1;
|
||||
}
|
||||
.col2 {
|
||||
grid-column: 2/2;
|
||||
grid-row: 1/1;
|
||||
}
|
||||
.col3 {
|
||||
grid-column: 3/3;
|
||||
grid-row: 1/1;
|
||||
}
|
||||
|
||||
.app {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
// justify-content: space-around;
|
||||
font-size: calc(10px + 2vmin);
|
||||
color: white;
|
||||
|
||||
h1 {
|
||||
margin-top: 2.5vmin;
|
||||
margin-bottom: 1vmin;
|
||||
}
|
||||
}
|
||||
|
||||
button {
|
||||
cursor: pointer;
|
||||
padding: 2ch;
|
||||
background: #aaa;
|
||||
border: 0;
|
||||
border-radius: 1ch;
|
||||
|
||||
&:hover {
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
div {
|
||||
display: flex;
|
||||
vertical-align: middle;
|
||||
height: 100%;
|
||||
&:active {
|
||||
background: #222;
|
||||
}
|
||||
transition: all 0.1s ease-out;
|
||||
}
|
||||
|
||||
* {
|
||||
margin: auto 0.5rem;
|
||||
a {
|
||||
color: #61dafb;
|
||||
}
|
||||
|
||||
.error {
|
||||
font-size: 1ch;
|
||||
color: red;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.funds,
|
||||
.transactions {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
// height: 50vh;
|
||||
}
|
||||
|
||||
nav {
|
||||
padding-top: 2ch;
|
||||
padding-bottom: 2ch;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
background: #111a;
|
||||
}
|
||||
|
||||
.totals {
|
||||
z-index: 9;
|
||||
.above {
|
||||
margin-top: 5ch;
|
||||
}
|
||||
.bottom {
|
||||
margin-bottom: 5ch;
|
||||
}
|
||||
}
|
||||
|
||||
.login {
|
||||
color: #ccc;
|
||||
form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-around;
|
||||
height: 100%;
|
||||
text-align: center;
|
||||
|
||||
label {
|
||||
input {
|
||||
margin-left: 2ch;
|
||||
}
|
||||
}
|
||||
}
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 70vmin;
|
||||
height: 70vmin;
|
||||
margin: auto;
|
||||
background: #222;
|
||||
}
|
||||
|
||||
.todo {
|
||||
color: #111a;
|
||||
}
|
||||
|
||||
.dank-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: #1115;
|
||||
border: 1ch #1117 solid;
|
||||
padding: 3ch;
|
||||
border-radius: 1ch;
|
||||
label {
|
||||
margin: 0.5ch;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
|
||||
input {
|
||||
margin-left: 2ch;
|
||||
}
|
||||
}
|
||||
|
||||
h3.ant-typography {
|
||||
margin: auto 0 !important;
|
||||
h3 {
|
||||
color: #666;
|
||||
}
|
||||
|
||||
div.ant-typography {
|
||||
font-size: 1.6rem;
|
||||
button {
|
||||
margin: 4ch 1ch;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-layout.layout {
|
||||
min-height: 100vh;
|
||||
// background-color: #444;
|
||||
// color: white;
|
||||
}
|
||||
footer {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
background: #111a;
|
||||
padding: 0 2ch;
|
||||
font-size: 10pt;
|
||||
|
||||
.ant-avatar {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
* {
|
||||
margin: auto;
|
||||
padding: 0;
|
||||
p {
|
||||
margin: 1ch;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-layout-content {
|
||||
max-width: 900px;
|
||||
margin: auto;
|
||||
}
|
||||
|
|
50
frontend/src/scss/app.scss.old
Normal file
50
frontend/src/scss/app.scss.old
Normal file
|
@ -0,0 +1,50 @@
|
|||
.ant-layout-header.app-header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
vertical-align: middle;
|
||||
padding: 0 1rem;
|
||||
|
||||
a {
|
||||
font-size: 1.2rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
div {
|
||||
display: flex;
|
||||
vertical-align: middle;
|
||||
height: 100%;
|
||||
|
||||
* {
|
||||
margin: auto 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
h3.ant-typography {
|
||||
margin: auto 0 !important;
|
||||
}
|
||||
|
||||
div.ant-typography {
|
||||
font-size: 1.6rem;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-layout.layout {
|
||||
min-height: 100vh;
|
||||
// background-color: #444;
|
||||
// color: white;
|
||||
}
|
||||
|
||||
.ant-avatar {
|
||||
display: flex;
|
||||
|
||||
* {
|
||||
margin: auto;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-layout-content {
|
||||
max-width: 900px;
|
||||
margin: auto;
|
||||
}
|
27
frontend/src/scss/form.scss
Normal file
27
frontend/src/scss/form.scss
Normal file
|
@ -0,0 +1,27 @@
|
|||
.dank-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: #1115;
|
||||
border: 1ch #1117 solid;
|
||||
padding: 3ch;
|
||||
border-radius: 1ch;
|
||||
color: #ccc;
|
||||
label {
|
||||
margin: 0.5ch;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
|
||||
input {
|
||||
margin-left: 2ch;
|
||||
}
|
||||
}
|
||||
|
||||
h3 {
|
||||
color: #666;
|
||||
}
|
||||
|
||||
button {
|
||||
margin: 4ch 1ch;
|
||||
}
|
||||
}
|
33
frontend/src/scss/fund-bar.scss
Normal file
33
frontend/src/scss/fund-bar.scss
Normal file
|
@ -0,0 +1,33 @@
|
|||
.fundbar {
|
||||
cursor: pointer;
|
||||
border-radius: 1ch;
|
||||
text-align: center;
|
||||
margin: auto 2ch 0 2ch;
|
||||
|
||||
&.totals {
|
||||
pointer-events: none;
|
||||
margin-top: 2ch;
|
||||
}
|
||||
|
||||
h3 {
|
||||
margin: auto auto 0 auto;
|
||||
}
|
||||
|
||||
&.front {
|
||||
pointer-events: none;
|
||||
transition: all 0.2s ease-out;
|
||||
border: 2px solid #222a;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 2ch;
|
||||
color: #333;
|
||||
text-shadow: 2px 2px #2223, -1px -1px #fffa;
|
||||
}
|
||||
|
||||
&.back {
|
||||
background: #222;
|
||||
|
||||
border: 4px solid #3334;
|
||||
height: 50vh;
|
||||
}
|
||||
}
|
25
frontend/src/scss/status.scss
Normal file
25
frontend/src/scss/status.scss
Normal file
|
@ -0,0 +1,25 @@
|
|||
.status {
|
||||
position: absolute;
|
||||
left: 1ch;
|
||||
bottom: 4ch;
|
||||
padding: 1ch 2ch;
|
||||
border-radius: 1ch;
|
||||
|
||||
transition: all 0.2s ease-in-out;
|
||||
|
||||
&.error {
|
||||
color: #ffffff;
|
||||
background: #b40303;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
&.good {
|
||||
color: #ffffff;
|
||||
background: #009b46;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
&.off {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
22
frontend/src/scss/transaction-list.scss
Normal file
22
frontend/src/scss/transaction-list.scss
Normal file
|
@ -0,0 +1,22 @@
|
|||
.transactions {
|
||||
.column {
|
||||
h3 {
|
||||
text-align: center;
|
||||
text-decoration: underline;
|
||||
}
|
||||
font-size: 14pt;
|
||||
margin: 2ch;
|
||||
background: #fff1;
|
||||
padding: 0 2ch 1ch;
|
||||
border-radius: 1ch;
|
||||
}
|
||||
|
||||
.transaction-item {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
.details {
|
||||
padding-left: 1ch;
|
||||
}
|
||||
}
|
||||
}
|
50
frontend/src/scss/transaction-modal.scss
Normal file
50
frontend/src/scss/transaction-modal.scss
Normal file
|
@ -0,0 +1,50 @@
|
|||
.transaction-modal {
|
||||
color: #fff;
|
||||
z-index: 100;
|
||||
position: absolute;
|
||||
width: 95vw;
|
||||
height: 95vh;
|
||||
border-radius: 1ch;
|
||||
border: 1ch solid #000;
|
||||
background: #000c;
|
||||
display: flex;
|
||||
pointer-events: visible;
|
||||
|
||||
h3 {
|
||||
margin: 0;
|
||||
text-align: center;
|
||||
color: #fff;
|
||||
text-shadow: 2px 2px #000, -1px -1px #444;
|
||||
}
|
||||
|
||||
.cancel-button {
|
||||
cursor: pointer;
|
||||
background: #000a;
|
||||
border: 1px solid #ccc;
|
||||
width: 4ch;
|
||||
padding: 1ch 1ch;
|
||||
border-radius: 1.5ch;
|
||||
text-align: center;
|
||||
position: relative;
|
||||
top: -9ch;
|
||||
left: -2ch;
|
||||
}
|
||||
|
||||
.form {
|
||||
pointer-events: all;
|
||||
box-sizing: content-box;
|
||||
padding: 3ch 2ch;
|
||||
background: #000a;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 1ch;
|
||||
width: 50%;
|
||||
margin: auto;
|
||||
|
||||
label {
|
||||
display: flex;
|
||||
margin: 1ch;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -12,8 +12,8 @@ export type Account = {
|
|||
users?: uuid[]
|
||||
name: string
|
||||
details: string
|
||||
income: string
|
||||
expenses: string
|
||||
income: number
|
||||
expenses: number
|
||||
}
|
||||
|
||||
export type Stack = {
|
||||
|
@ -32,3 +32,8 @@ export type Transaction = {
|
|||
amount: number // '30.44'
|
||||
created_at: string // '2021-04-15T00:02:45.096071Z'
|
||||
}
|
||||
|
||||
export type Password = {
|
||||
password: string
|
||||
passwordConfirm: string
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue
Block a user