👷♀️
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",
|
"resolved": "https://registry.npmjs.org/@dank-inc/data-buddy/-/data-buddy-0.1.4.tgz",
|
||||||
"integrity": "sha512-+t/Ctm4Ik2Lh1H9pt8DIDLHCPnRNz38ZcNXu9U+VFZOrLut7wxRoKqdt6IFz6MGwVJtuveJe2dGPCme3N3hryg=="
|
"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": {
|
"@eslint/eslintrc": {
|
||||||
"version": "0.3.0",
|
"version": "0.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-0.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-0.3.0.tgz",
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@dank-inc/data-buddy": "^0.1.4",
|
"@dank-inc/data-buddy": "^0.1.4",
|
||||||
|
"@dank-inc/use-get": "^0.3.1",
|
||||||
"@testing-library/jest-dom": "^5.11.4",
|
"@testing-library/jest-dom": "^5.11.4",
|
||||||
"@testing-library/react": "^11.1.0",
|
"@testing-library/react": "^11.1.0",
|
||||||
"@testing-library/user-event": "^12.1.10",
|
"@testing-library/user-event": "^12.1.10",
|
||||||
|
@ -25,6 +26,7 @@
|
||||||
"start": "react-scripts start",
|
"start": "react-scripts start",
|
||||||
"build": "react-scripts build",
|
"build": "react-scripts build",
|
||||||
"test": "react-scripts test",
|
"test": "react-scripts test",
|
||||||
|
"lint": "tsc",
|
||||||
"eject": "react-scripts eject"
|
"eject": "react-scripts eject"
|
||||||
},
|
},
|
||||||
"eslintConfig": {
|
"eslintConfig": {
|
||||||
|
|
|
@ -7,7 +7,7 @@ import { Api } from './api'
|
||||||
import './scss/app.scss'
|
import './scss/app.scss'
|
||||||
|
|
||||||
const App = () => {
|
const App = () => {
|
||||||
const api = new Api({ mock: true, baseURL: '/api' })
|
const api = new Api({ baseURL: '/api' })
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import Axios, { AxiosInstance } from 'axios'
|
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 { JWT, setJWT, wipeJWT } from '../utils/jwt'
|
||||||
import { DataBuddy } from '@dank-inc/data-buddy'
|
import { DataBuddy } from '@dank-inc/data-buddy'
|
||||||
import { users } from './data/users'
|
import { users } from './data/users'
|
||||||
|
@ -53,21 +53,39 @@ export class Api {
|
||||||
return data
|
return data
|
||||||
}
|
}
|
||||||
|
|
||||||
getAccounts = async () => {
|
updateUser = async (id: uuid, body: Partial<User>) => {
|
||||||
this.axios.get('accounts')
|
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) => {
|
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>) => {
|
updateAccount = async (id: uuid, body: Partial<Account>) => {
|
||||||
const { data } = await this.axios.patch<Account>(`accounts/${id}`, body)
|
const { data } = await this.axios.patch<Account>(`accounts/${id}`, body)
|
||||||
return data
|
return data
|
||||||
}
|
}
|
||||||
createAccount = async () => {}
|
createAccount = async (body: Omit<Account, 'id'>) => {
|
||||||
|
const { data } = await this.axios.post<Transaction>('accounts', body)
|
||||||
|
return data
|
||||||
|
}
|
||||||
deleteAccount = async () => {}
|
deleteAccount = async () => {}
|
||||||
|
|
||||||
getStacks = async () => {
|
getStacks = async (): Promise<Stack[]> => {
|
||||||
this.axios.get('stacks')
|
const { data } = await this.axios.get('stacks')
|
||||||
|
return data
|
||||||
}
|
}
|
||||||
updateStack = async (id: uuid, body: Partial<Stack>) => {
|
updateStack = async (id: uuid, body: Partial<Stack>) => {
|
||||||
const { data } = await this.axios.patch<Stack>(`stacks/${id}`, body)
|
const { data } = await this.axios.patch<Stack>(`stacks/${id}`, body)
|
||||||
|
|
|
@ -1,37 +1,42 @@
|
||||||
import { Layout } from 'antd'
|
|
||||||
import { useUserContext } from '../contexts/UserContext'
|
import { useUserContext } from '../contexts/UserContext'
|
||||||
import { Login } from './pages/Login'
|
import { Redirect, Route, Switch } from 'react-router'
|
||||||
import { Route, Switch } from 'react-router'
|
import { Link } from 'react-router-dom'
|
||||||
import { Dashboard } from './pages/Dashboard'
|
import { Dashboard } from './pages/Dashboard'
|
||||||
import { AppHeader } from './layout/AppHeader'
|
import { UserForm } from './components/UserForm'
|
||||||
import { Profile } from './pages/Profile'
|
import { TransactionList } from './components/TransactionList'
|
||||||
import { NewUser } from './forms/NewUser'
|
import { AccountForm } from './components/AccountForm'
|
||||||
import { ForgotPassword } from './pages/ForgotPassword'
|
|
||||||
|
|
||||||
export const CoreLayout = () => {
|
export const CoreLayout = () => {
|
||||||
const { user } = useUserContext()
|
const { user, accounts, selectedAccount } = useUserContext()
|
||||||
|
|
||||||
if (!user)
|
if (!accounts?.length) <Redirect to="/account/new" />
|
||||||
return (
|
|
||||||
<Layout className="layout">
|
|
||||||
<AppHeader user={user} />
|
|
||||||
<Login />
|
|
||||||
</Layout>
|
|
||||||
)
|
|
||||||
|
|
||||||
// header, sidebar, avatar?
|
|
||||||
return (
|
return (
|
||||||
<Layout className="layout">
|
<div className="app" id="appElement">
|
||||||
<AppHeader user={user} />
|
<nav>
|
||||||
<Layout.Content>
|
<Link to="/">Home</Link>
|
||||||
<Switch>
|
<Link to="/select">Select Budget</Link>
|
||||||
<Route path="/forgot-password" component={ForgotPassword} />
|
<Link to="/account">Budget Details</Link>
|
||||||
<Route path="/login" component={Login} />
|
<Link to="/details">Transactions</Link>
|
||||||
<Route path="/new/user" component={NewUser} />
|
<Link to="/user">Profile</Link>
|
||||||
<Route path="/profile" component={Profile} />
|
</nav>
|
||||||
<Route exact path="/" component={Dashboard} />
|
|
||||||
</Switch>
|
<Switch>
|
||||||
</Layout.Content>
|
<Route path="/user" component={UserForm} />
|
||||||
</Layout>
|
<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>
|
<Button onClick={() => history.push('/new/user')}>New User</Button>
|
||||||
</div>
|
</div>
|
||||||
<div className="header-right">
|
<div className="header-right">
|
||||||
<p>Welcome, {user.username}!!</p>
|
<p>Welcome, {user.name}!!</p>
|
||||||
<Dropdown
|
<Dropdown
|
||||||
overlay={
|
overlay={
|
||||||
<Menu style={{ display: 'flex', flexDirection: 'column' }}>
|
<Menu style={{ display: 'flex', flexDirection: 'column' }}>
|
||||||
|
@ -51,7 +51,7 @@ export const AppHeader = ({ user }: Props) => {
|
||||||
</Menu>
|
</Menu>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Avatar>{user.username[0]}</Avatar>
|
<Avatar>{user.name[0]}</Avatar>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
</div>
|
</div>
|
||||||
</Header>
|
</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 = () => {
|
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, {
|
import React, { createContext, useContext, useState } from 'react'
|
||||||
createContext,
|
|
||||||
useContext,
|
|
||||||
useState,
|
|
||||||
useEffect,
|
|
||||||
Dispatch,
|
|
||||||
SetStateAction,
|
|
||||||
} from 'react'
|
|
||||||
import { message } from 'antd'
|
import { message } from 'antd'
|
||||||
|
|
||||||
import { useHistory } from 'react-router'
|
import { useHistory } from 'react-router'
|
||||||
|
import { Account, User, uuid } from '../types'
|
||||||
import { User } from '../types'
|
|
||||||
import { useAppContext } from './AppContext'
|
import { useAppContext } from './AppContext'
|
||||||
import { logOut } from '../api'
|
import { logOut } from '../api'
|
||||||
|
|
||||||
|
@ -20,9 +11,11 @@ type Props = {
|
||||||
|
|
||||||
type Context = {
|
type Context = {
|
||||||
user: User | null
|
user: User | null
|
||||||
setUser: Dispatch<SetStateAction<User | null>>
|
accounts: Account[] | null
|
||||||
|
selectedAccount: Account | null
|
||||||
handleLogin: (username: string, password: string) => void
|
handleLogin: (username: string, password: string) => void
|
||||||
handleLogout: () => void
|
handleLogout: () => void
|
||||||
|
handleSelectAccount: (id: uuid) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const UserContext = createContext<Context | null>(null)
|
const UserContext = createContext<Context | null>(null)
|
||||||
|
@ -32,15 +25,19 @@ export const UserContextProvider = ({ children }: Props) => {
|
||||||
const history = useHistory()
|
const history = useHistory()
|
||||||
|
|
||||||
const [user, setUser] = useState<User | null>(null)
|
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) => {
|
const handleLogin = async (username: string, password: string) => {
|
||||||
try {
|
try {
|
||||||
const { id } = await api.login(username, password)
|
const { id } = await api.login(username, password)
|
||||||
if (!id) throw new Error('Problem logging in!')
|
if (!id) throw new Error('Problem logging in!')
|
||||||
const user = await api.getUser(id)
|
setUser(await api.getUser(id))
|
||||||
|
|
||||||
setUser(user)
|
const accounts = await api.getAccounts()
|
||||||
message.success(`logged in as ${user?.username}`, 0.5)
|
setAccounts(accounts)
|
||||||
|
|
||||||
|
message.success(`logged in as ${user?.name}`, 0.5)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
message.error('Login Failed!')
|
message.error('Login Failed!')
|
||||||
}
|
}
|
||||||
|
@ -53,8 +50,22 @@ export const UserContextProvider = ({ children }: Props) => {
|
||||||
history.push('/')
|
history.push('/')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleSelectAccount = (id: string) => {
|
||||||
|
const account = accounts?.find((account) => account.id === id)
|
||||||
|
if (account) setSelectedAccount(account)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<UserContext.Provider value={{ user, setUser, handleLogin, handleLogout }}>
|
<UserContext.Provider
|
||||||
|
value={{
|
||||||
|
user,
|
||||||
|
accounts,
|
||||||
|
handleLogin,
|
||||||
|
handleLogout,
|
||||||
|
selectedAccount,
|
||||||
|
handleSelectAccount,
|
||||||
|
}}
|
||||||
|
>
|
||||||
{children}
|
{children}
|
||||||
</UserContext.Provider>
|
</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 {
|
h1,
|
||||||
display: flex;
|
h2,
|
||||||
flex-direction: row;
|
h3,
|
||||||
justify-content: space-between;
|
h4,
|
||||||
vertical-align: middle;
|
h5 {
|
||||||
padding: 0 1rem;
|
color: #222;
|
||||||
|
text-shadow: -1px -1px #444;
|
||||||
|
font-weight: 900;
|
||||||
|
}
|
||||||
|
|
||||||
a {
|
.col1 {
|
||||||
font-size: 1.2rem;
|
grid-column: 1/1;
|
||||||
font-weight: bold;
|
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 {
|
&:active {
|
||||||
display: flex;
|
background: #222;
|
||||||
vertical-align: middle;
|
}
|
||||||
height: 100%;
|
transition: all 0.1s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
* {
|
a {
|
||||||
margin: auto 0.5rem;
|
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 {
|
h3 {
|
||||||
margin: auto 0 !important;
|
color: #666;
|
||||||
}
|
}
|
||||||
|
|
||||||
div.ant-typography {
|
button {
|
||||||
font-size: 1.6rem;
|
margin: 4ch 1ch;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.ant-layout.layout {
|
footer {
|
||||||
min-height: 100vh;
|
position: fixed;
|
||||||
// background-color: #444;
|
left: 0;
|
||||||
// color: white;
|
bottom: 0;
|
||||||
}
|
width: 100%;
|
||||||
|
background: #111a;
|
||||||
|
padding: 0 2ch;
|
||||||
|
font-size: 10pt;
|
||||||
|
|
||||||
.ant-avatar {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
|
||||||
* {
|
p {
|
||||||
margin: auto;
|
margin: 1ch;
|
||||||
padding: 0;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.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[]
|
users?: uuid[]
|
||||||
name: string
|
name: string
|
||||||
details: string
|
details: string
|
||||||
income: string
|
income: number
|
||||||
expenses: string
|
expenses: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Stack = {
|
export type Stack = {
|
||||||
|
@ -32,3 +32,8 @@ export type Transaction = {
|
||||||
amount: number // '30.44'
|
amount: number // '30.44'
|
||||||
created_at: string // '2021-04-15T00:02:45.096071Z'
|
created_at: string // '2021-04-15T00:02:45.096071Z'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type Password = {
|
||||||
|
password: string
|
||||||
|
passwordConfirm: string
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user