👷‍♀️

This commit is contained in:
Elijah Lucian 2021-04-14 20:04:37 -06:00
parent 5b4726bee8
commit 8c40f30286
24 changed files with 891 additions and 90 deletions

View File

@ -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",

View File

@ -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": {

View File

@ -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>

View File

@ -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)

View File

@ -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>
<Link to="/select">Select Budget</Link>
<Link to="/account">Budget Details</Link>
<Link to="/details">Transactions</Link>
<Link to="/user">Profile</Link>
</nav>
<Switch> <Switch>
<Route path="/forgot-password" component={ForgotPassword} /> <Route path="/user" component={UserForm} />
<Route path="/login" component={Login} /> <Route path="/details" component={TransactionList} />
<Route path="/new/user" component={NewUser} /> <Route path="/account/new" component={AccountForm} />
<Route path="/profile" component={Profile} /> <Route path="/account" component={AccountForm} />
<Route exact path="/" component={Dashboard} /> <Route path="/" component={Dashboard} />
</Switch> </Switch>
</Layout.Content>
</Layout> {/* {showingModal && <TransactionModal account={account} />} */}
<footer>
<p>User: {user?.name}</p>
<p>|</p>
<p>Budget: {selectedAccount?.name}</p>
<p>|</p>
</footer>
</div>
) )
} }

View 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 &amp; 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>
</>
)
}

View 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>
</>
)
}

View 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>
</>
)
}

View 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>
)
}

View 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>
</>
)
}

View 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>
)
}

View 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>
</>
)
}

View File

@ -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>

View File

@ -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>
</>
)
} }

View File

@ -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>
) )

View File

@ -0,0 +1,6 @@
.account-select {
display: flex;
flex-direction: row;
justify-content: space-around;
width: 100%;
}

View File

@ -1,50 +1,161 @@
.ant-layout-header.app-header { h1,
h2,
h3,
h4,
h5 {
color: #222;
text-shadow: -1px -1px #444;
font-weight: 900;
}
.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;
}
&:active {
background: #222;
}
transition: all 0.1s ease-out;
}
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; display: flex;
flex-direction: row; flex-direction: row;
justify-content: space-between; justify-content: space-between;
vertical-align: middle;
padding: 0 1rem;
a { input {
font-size: 1.2rem; margin-left: 2ch;
font-weight: bold; }
} }
div { h3 {
color: #666;
}
button {
margin: 4ch 1ch;
}
}
footer {
position: fixed;
left: 0;
bottom: 0;
width: 100%;
background: #111a;
padding: 0 2ch;
font-size: 10pt;
display: flex; display: flex;
vertical-align: middle; flex-direction: row;
height: 100%;
* { p {
margin: auto 0.5rem; margin: 1ch;
} }
} }
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;
}

View 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;
}

View 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;
}
}

View 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;
}
}

View 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;
}
}

View 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;
}
}
}

View 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;
}
}
}

View File

@ -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
}