parent
5b4726bee8
commit
8c40f30286
24 changed files with 887 additions and 86 deletions
@ -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> |
||||
) |
||||
} |
||||
|
@ -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> |
||||
</> |
||||
) |
||||
} |
@ -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> |
||||
</> |
||||
) |
||||
} |
@ -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> |
||||
</> |
||||
) |
||||
} |
@ -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> |
||||
) |
||||
} |
@ -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> |
||||
</> |
||||
) |
||||
} |
@ -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> |
||||
) |
||||
} |
@ -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> |
||||
</> |
||||
) |
||||
} |
@ -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> |
||||
</> |
||||
) |
||||
} |
||||
|
@ -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, |
||||
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: row; |
||||
justify-content: space-between; |
||||
vertical-align: middle; |
||||
padding: 0 1rem; |
||||
flex-direction: column; |
||||
align-items: center; |
||||
// justify-content: space-around; |
||||
font-size: calc(10px + 2vmin); |
||||
color: white; |
||||
|
||||
a { |
||||
font-size: 1.2rem; |
||||
font-weight: bold; |
||||
h1 { |
||||
margin-top: 2.5vmin; |
||||
margin-bottom: 1vmin; |
||||
} |
||||
} |
||||
|
||||
div { |
||||
display: flex; |
||||
vertical-align: middle; |
||||
height: 100%; |
||||
button { |
||||
cursor: pointer; |
||||
padding: 2ch; |
||||
background: #aaa; |
||||
border: 0; |
||||
border-radius: 1ch; |
||||
|
||||
* { |
||||
margin: auto 0.5rem; |
||||
} |
||||
&:hover { |
||||
background: #fff; |
||||
} |
||||
|
||||
h3.ant-typography { |
||||
margin: auto 0 !important; |
||||
&:active { |
||||
background: #222; |
||||
} |
||||
transition: all 0.1s ease-out; |
||||
} |
||||
|
||||
div.ant-typography { |
||||
font-size: 1.6rem; |
||||
} |
||||
a { |
||||
color: #61dafb; |
||||
} |
||||
|
||||
.error { |
||||
font-size: 1ch; |
||||
color: red; |
||||
padding: 0; |
||||
margin: 0; |
||||
} |
||||
|
||||
.ant-layout.layout { |
||||
min-height: 100vh; |
||||
// background-color: #444; |
||||
// color: white; |
||||
.funds, |
||||
.transactions { |
||||
display: grid; |
||||
grid-template-columns: repeat(3, 1fr); |
||||
// height: 50vh; |
||||
} |
||||
|
||||
.ant-avatar { |
||||
nav { |
||||
padding-top: 2ch; |
||||
padding-bottom: 2ch; |
||||
width: 100%; |
||||
display: flex; |
||||
justify-content: space-around; |
||||
background: #111a; |
||||
} |
||||
|
||||
* { |
||||
margin: auto; |
||||
padding: 0; |
||||
.totals { |
||||
z-index: 9; |
||||
.above { |
||||
margin-top: 5ch; |
||||
} |
||||
.bottom { |
||||
margin-bottom: 5ch; |
||||
} |
||||
} |
||||
|
||||
.ant-layout-content { |
||||
max-width: 900px; |
||||
.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 { |
||||
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; |
||||
flex-direction: row; |
||||
|
||||
p { |
||||
margin: 1ch; |
||||
} |
||||
} |
||||
|
@ -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; |
||||
} |
@ -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; |
||||
} |
||||
} |
@ -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; |
||||
} |
||||
} |
@ -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; |
||||
} |
||||
} |
@ -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; |
||||
} |
||||
} |
||||
} |
@ -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; |
||||
} |
||||
} |
||||
} |
Loading…
Reference in new issue