new form and form tweaks
This commit is contained in:
parent
2ebe8bcd1f
commit
4a1507eba4
|
@ -30,7 +30,9 @@ export const MainLayout = () => {
|
||||||
<Switch>
|
<Switch>
|
||||||
<Route path="/sign-up" component={NewUser} />
|
<Route path="/sign-up" component={NewUser} />
|
||||||
<Route path="/login" component={Login} />
|
<Route path="/login" component={Login} />
|
||||||
|
<Route path="/">
|
||||||
<div>loading...</div>
|
<div>loading...</div>
|
||||||
|
</Route>
|
||||||
</Switch>
|
</Switch>
|
||||||
) : (
|
) : (
|
||||||
<Switch>
|
<Switch>
|
||||||
|
|
|
@ -6,11 +6,6 @@
|
||||||
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
h1 {
|
|
||||||
margin-top: 2.5vmin;
|
|
||||||
margin-bottom: 1vmin;
|
|
||||||
}
|
|
||||||
|
|
||||||
main {
|
main {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,7 +9,7 @@ type Props = {
|
||||||
type AppContextInterface = {
|
type AppContextInterface = {
|
||||||
get: <T>(path: string) => Promise<T>
|
get: <T>(path: string) => Promise<T>
|
||||||
patch: <T>(path: string, body: Partial<T>) => Promise<T>
|
patch: <T>(path: string, body: Partial<T>) => Promise<T>
|
||||||
post: <T, R = T>(path: string, body: Partial<T>) => Promise<R>
|
post: <T, R = T>(path: string, body: Partial<T>) => Promise<R | null>
|
||||||
create: <T>(path: string, body: Partial<T>) => Promise<T>
|
create: <T>(path: string, body: Partial<T>) => Promise<T>
|
||||||
destroy: (path: string) => Promise<string>
|
destroy: (path: string) => Promise<string>
|
||||||
baseURL?: string
|
baseURL?: string
|
||||||
|
@ -39,8 +39,12 @@ export const AppContextProvider = ({ children, baseURL }: Props) => {
|
||||||
return res.data
|
return res.data
|
||||||
}
|
}
|
||||||
async function post<T, R = T>(path: string, body: Partial<T>) {
|
async function post<T, R = T>(path: string, body: Partial<T>) {
|
||||||
|
try {
|
||||||
const res = await api.post<T, AxiosResponse<R>>(path, body)
|
const res = await api.post<T, AxiosResponse<R>>(path, body)
|
||||||
return res.data
|
return res.data
|
||||||
|
} catch (err) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
async function create<T>(path: string, body: Partial<T>) {
|
async function create<T>(path: string, body: Partial<T>) {
|
||||||
// unauthed POST
|
// unauthed POST
|
||||||
|
|
|
@ -1,11 +1,30 @@
|
||||||
|
@import '../../scss/variables.scss';
|
||||||
|
|
||||||
.dank-form {
|
.dank-form {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
padding: 3ch;
|
padding: 3ch;
|
||||||
|
|
||||||
|
input {
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
width: 100%;
|
||||||
|
font-size: 2rem;
|
||||||
|
border: 1px solid $color-grey;
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
outline: 2px solid $color-primary;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
button {
|
||||||
|
width: 40%;
|
||||||
|
font-size: 2rem;
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
label {
|
label {
|
||||||
margin: 0.5ch;
|
margin: 0.5ch;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
font-size: 1.5rem;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
|
||||||
|
@ -13,8 +32,4 @@
|
||||||
margin-left: 2ch;
|
margin-left: 2ch;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
button {
|
|
||||||
margin: 4ch 1ch;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { FormItemProps } from 'antd'
|
import { FormItemProps } from 'antd'
|
||||||
import AntFormItem from 'antd/lib/form/FormItem'
|
import AntFormItem from 'antd/lib/form/FormItem'
|
||||||
|
import './style.scss'
|
||||||
|
|
||||||
type Props = FormItemProps
|
type Props = FormItemProps
|
||||||
|
|
||||||
|
|
5
frontend/src/elements/FormItem/style.scss
Normal file
5
frontend/src/elements/FormItem/style.scss
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
.dank-form-item {
|
||||||
|
label {
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,7 +1,9 @@
|
||||||
import { InputNumber as AntInputNumber, InputNumberProps } from 'antd'
|
import { InputNumber as AntInputNumber, InputNumberProps } from 'antd'
|
||||||
|
|
||||||
|
import './style.scss'
|
||||||
|
|
||||||
type Props = InputNumberProps
|
type Props = InputNumberProps
|
||||||
|
|
||||||
export const InputNumber = (props: Props) => {
|
export const InputNumber = (props: Props) => {
|
||||||
return <AntInputNumber {...props} />
|
return <AntInputNumber className="dank-input-number" {...props} />
|
||||||
}
|
}
|
||||||
|
|
33
frontend/src/elements/InputNumber/style.scss
Normal file
33
frontend/src/elements/InputNumber/style.scss
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
.ant-input-number {
|
||||||
|
display: grid;
|
||||||
|
grid-template: 1fr / 1fr;
|
||||||
|
|
||||||
|
div {
|
||||||
|
grid-area: 1/1/2/2;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-handler {
|
||||||
|
&-wrap {
|
||||||
|
z-index: 2;
|
||||||
|
width: 100%;
|
||||||
|
pointer-events: none;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: flex-end;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-up,
|
||||||
|
&-down {
|
||||||
|
pointer-events: all;
|
||||||
|
cursor: pointer;
|
||||||
|
margin: auto 0.5rem auto 0;
|
||||||
|
svg {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&-input-wrap {
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
}
|
53
frontend/src/forms/TransactionForm.tsx
Normal file
53
frontend/src/forms/TransactionForm.tsx
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
import { useForm } from 'antd/lib/form/Form'
|
||||||
|
import { Button } from '../elements/Button'
|
||||||
|
import { Form } from '../elements/Form'
|
||||||
|
import { FormItem } from '../elements/FormItem'
|
||||||
|
import { Input } from '../elements/Input'
|
||||||
|
import { InputNumber } from '../elements/InputNumber'
|
||||||
|
import {
|
||||||
|
useCreateTransaction,
|
||||||
|
NewTransaction,
|
||||||
|
} from '../hooks/create/useCreateTransaction'
|
||||||
|
import { DankFormProps } from '../layout/Modal'
|
||||||
|
import { Row } from '../layout/Row'
|
||||||
|
|
||||||
|
type Props = DankFormProps & {
|
||||||
|
stackId: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TransactionForm = ({ stackId, onSave, onCancel }: Props) => {
|
||||||
|
const [form] = useForm<NewTransaction>()
|
||||||
|
const create = useCreateTransaction()
|
||||||
|
|
||||||
|
const handleFinish = async (tx: NewTransaction) => {
|
||||||
|
console.log('transaction', tx)
|
||||||
|
const res = await create(tx)
|
||||||
|
|
||||||
|
if (!res) {
|
||||||
|
console.log("couldn't create transaction")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
onSave?.()
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form
|
||||||
|
form={form}
|
||||||
|
onFinish={handleFinish}
|
||||||
|
initialValues={{ stack: stackId }}
|
||||||
|
>
|
||||||
|
<h1>New Transaction</h1>
|
||||||
|
<FormItem label="Amount" name="amount">
|
||||||
|
<InputNumber />
|
||||||
|
</FormItem>
|
||||||
|
<FormItem label="Details" name="amount">
|
||||||
|
<Input />
|
||||||
|
</FormItem>
|
||||||
|
<Row>
|
||||||
|
<Button onClick={onCancel}>Cancel</Button>
|
||||||
|
<Button htmlType="submit">Save</Button>
|
||||||
|
</Row>
|
||||||
|
</Form>
|
||||||
|
)
|
||||||
|
}
|
|
@ -4,6 +4,6 @@ import { Account } from '../../types'
|
||||||
export const useCreateAccount = () => {
|
export const useCreateAccount = () => {
|
||||||
const api = useAppContext()
|
const api = useAppContext()
|
||||||
|
|
||||||
return (body: Omit<Account, 'id'>): Promise<Account> =>
|
return (body: Omit<Account, 'id'>): Promise<Account | null> =>
|
||||||
api.post<Account>('/accounts', body)
|
api.post<Account>('/accounts', body)
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,6 @@ import { Stack } from '../../types'
|
||||||
export const useCreateStack = () => {
|
export const useCreateStack = () => {
|
||||||
const api = useAppContext()
|
const api = useAppContext()
|
||||||
|
|
||||||
return (body: Omit<Stack, 'id'>): Promise<Stack> =>
|
return (body: Omit<Stack, 'id'>): Promise<Stack | null> =>
|
||||||
api.post<Stack>('/stacks', body)
|
api.post<Stack>('/stacks', body)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,11 @@
|
||||||
import { useAppContext } from '../../contexts/AppContext'
|
import { useAppContext } from '../../contexts/AppContext'
|
||||||
import { Transaction } from '../../types'
|
import { Transaction } from '../../types'
|
||||||
|
|
||||||
|
export type NewTransaction = Omit<Transaction, 'id'>
|
||||||
|
|
||||||
export const useCreateTransaction = () => {
|
export const useCreateTransaction = () => {
|
||||||
const api = useAppContext()
|
const api = useAppContext()
|
||||||
|
|
||||||
return (body: Omit<Transaction, 'id'>): Promise<Transaction> =>
|
return (body: NewTransaction): Promise<Transaction | null> =>
|
||||||
api.post<Transaction>('/transactions', body)
|
api.post<Transaction>('/transactions/', body)
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,6 @@ import { User } from '../../types'
|
||||||
export const useCreateUser = () => {
|
export const useCreateUser = () => {
|
||||||
const api = useAppContext()
|
const api = useAppContext()
|
||||||
|
|
||||||
return (body: Omit<User, 'id'>): Promise<User> =>
|
return (body: Omit<User, 'id'>): Promise<User | null> =>
|
||||||
api.post<User>('/users', body)
|
api.post<User>('/users', body)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { Transaction } from '../../types'
|
import { Transaction } from '../../types'
|
||||||
import { useGet } from '../util/useGet'
|
import { useGet } from '../util/useGet'
|
||||||
|
|
||||||
export const useTransactions = () => useGet<Transaction[]>(`/transactions/`)
|
export const useTransactions = () =>
|
||||||
|
useGet<Transaction[]>(`/transactions/`, { mode: 'list' })
|
||||||
|
|
|
@ -1,9 +1,16 @@
|
||||||
import { ModalProps } from 'antd'
|
import { ModalProps } from 'antd'
|
||||||
import { ReactNode } from 'react'
|
import { ReactElement, cloneElement } from 'react'
|
||||||
import './style.scss'
|
import './style.scss'
|
||||||
|
|
||||||
type Props = ModalProps & { children: ReactNode }
|
type Props = ModalProps & { children: ReactElement }
|
||||||
|
|
||||||
export const Modal = ({ children, ...props }: Props) => {
|
export type DankFormProps = {
|
||||||
return <div className="dank-modal">{children}</div>
|
onSave?: () => void
|
||||||
|
onCancel?: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Modal = ({ children, visible }: Props) => {
|
||||||
|
const el = cloneElement(children, (props: DankFormProps) => ({ ...props }))
|
||||||
|
|
||||||
|
return visible ? <div className="dank-modal">{el}</div> : null
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,11 +1,20 @@
|
||||||
@import '../../scss/variables.scss';
|
@import '../../scss/variables.scss';
|
||||||
|
|
||||||
.dank-modal {
|
.dank-modal {
|
||||||
display: flex;
|
position: absolute;
|
||||||
|
width: 70%;
|
||||||
|
height: 70%;
|
||||||
|
background-color: transparentize(#222, 0.07);
|
||||||
|
border-radius: 1rem;
|
||||||
|
padding: 1rem;
|
||||||
|
margin: auto;
|
||||||
border: 1px solid $color-grey;
|
border: 1px solid $color-grey;
|
||||||
|
|
||||||
h3 {
|
h1,
|
||||||
text-align: center;
|
h2,
|
||||||
|
h3,
|
||||||
|
h4,
|
||||||
|
h5 {
|
||||||
color: $color-light;
|
color: $color-light;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
10
frontend/src/layout/Row/index.tsx
Normal file
10
frontend/src/layout/Row/index.tsx
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
import React from 'react'
|
||||||
|
import './style.scss'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
children: React.ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Row = ({ children }: Props) => {
|
||||||
|
return <div className="dank-row">{children}</div>
|
||||||
|
}
|
6
frontend/src/layout/Row/style.scss
Normal file
6
frontend/src/layout/Row/style.scss
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
.dank-row {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin: 1rem 0;
|
||||||
|
}
|
|
@ -11,7 +11,7 @@ export const Dashboard = () => {
|
||||||
<Page>
|
<Page>
|
||||||
<h1>Remaining Balances</h1>
|
<h1>Remaining Balances</h1>
|
||||||
{accounts.data?.map((account) => (
|
{accounts.data?.map((account) => (
|
||||||
<div className="account-overview">
|
<div key={account.id} className="account-overview">
|
||||||
<h3>{account.name}</h3>
|
<h3>{account.name}</h3>
|
||||||
<div className="funds">
|
<div className="funds">
|
||||||
{account.stacks.map((stack) => (
|
{account.stacks.map((stack) => (
|
||||||
|
|
|
@ -5,7 +5,6 @@
|
||||||
border: 1px solid $color-grey;
|
border: 1px solid $color-grey;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
text-align: center;
|
|
||||||
margin: 1rem 0;
|
margin: 1rem 0;
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
background: $color-alt;
|
background: $color-alt;
|
||||||
|
|
|
@ -3,4 +3,5 @@ $color-alt: #c9d8b6;
|
||||||
$color-light: #f1ecc3;
|
$color-light: #f1ecc3;
|
||||||
$color-white: #fff;
|
$color-white: #fff;
|
||||||
$color-grey: #515e63;
|
$color-grey: #515e63;
|
||||||
|
$color-black: #111;
|
||||||
$color-dark: #333;
|
$color-dark: #333;
|
||||||
|
|
|
@ -28,7 +28,7 @@ export type Stack = {
|
||||||
account: string //'38485982-87f3-4a11-a963-2202983809e3'
|
account: string //'38485982-87f3-4a11-a963-2202983809e3'
|
||||||
name: string // 'House Fund'
|
name: string // 'House Fund'
|
||||||
details: string //'buy furniture'
|
details: string //'buy furniture'
|
||||||
amount: number // '200.00'
|
amount: string // number // '200.00'
|
||||||
transactions: Transaction[]
|
transactions: Transaction[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -36,7 +36,7 @@ export type Transaction = {
|
||||||
id: string
|
id: string
|
||||||
stack: string // '0058cece-3ff3-4ee1-b71d-075a0bc73bc0'
|
stack: string // '0058cece-3ff3-4ee1-b71d-075a0bc73bc0'
|
||||||
details: string // 'by ghetto couch off Kijiji'
|
details: string // 'by ghetto couch off Kijiji'
|
||||||
amount: number // '30.44'
|
amount: string // number // '30.44'
|
||||||
created_at: string // '2021-04-15T00:02:45.096071Z'
|
created_at: string // '2021-04-15T00:02:45.096071Z'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
import { Link } from 'react-router-dom'
|
import { Link } from 'react-router-dom'
|
||||||
import { message } from 'antd'
|
import { message } from 'antd'
|
||||||
import './style.scss'
|
|
||||||
import { Button } from '../../elements/Button'
|
import { Button } from '../../elements/Button'
|
||||||
import { useAccounts } from '../../hooks/getMany/useAccounts'
|
import { useAccounts } from '../../hooks/getMany/useAccounts'
|
||||||
|
|
||||||
|
import './style.scss'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
selectProfile: (id: string) => void
|
selectProfile: (id: string) => void
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,40 +1,47 @@
|
||||||
import { Stack } from '../../types'
|
import { Stack } from '../../types'
|
||||||
import './style.scss'
|
import './style.scss'
|
||||||
import _ from 'lodash'
|
import _ from 'lodash'
|
||||||
|
import { useState } from 'react'
|
||||||
|
import { Modal } from '../../layout/Modal'
|
||||||
|
import { TransactionForm } from '../../forms/TransactionForm'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
stack: Stack
|
stack: Stack
|
||||||
}
|
}
|
||||||
|
|
||||||
export const FundBar = ({ stack }: Props) => {
|
export const FundBar = ({ stack }: Props) => {
|
||||||
const amount = _.sumBy(stack.transactions, 'amount')
|
const amount = _.sumBy(stack.transactions, (tx) => parseFloat(tx.amount))
|
||||||
console.log('amount', stack.id, amount)
|
const max = parseFloat(stack.amount)
|
||||||
const max = stack.amount
|
|
||||||
const current = max - amount
|
const current = max - amount
|
||||||
const percent = Math.max(current / max, 0)
|
const u = Math.max(current / max, 0)
|
||||||
const hue = percent * 120
|
|
||||||
|
const [newTx, setNewTx] = useState(false)
|
||||||
|
|
||||||
|
const addTransaction = () => {
|
||||||
|
console.log(`adding transaction to => ${stack.name}`)
|
||||||
|
setNewTx(true)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<>
|
||||||
<div className="fundbar">
|
<div className="fundbar">
|
||||||
<div
|
<div className="back" onClick={addTransaction}>
|
||||||
className={`back`}
|
|
||||||
onClick={() => console.log(`adding transaction to => ${stack.name}`)}
|
|
||||||
></div>
|
|
||||||
<div
|
|
||||||
className={`front`}
|
|
||||||
style={{
|
|
||||||
background: `hsl(${hue}, 100%, 50%)`,
|
|
||||||
height: `${percent * 40 + 10}vmin`,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<h3>{stack.name}</h3>
|
<h3>{stack.name}</h3>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className={`totals`}
|
className="front"
|
||||||
style={{ color: `hsl(0, 0%, ${Math.abs(1 - percent) * 100}%)` }}
|
style={{
|
||||||
>
|
background: `hsl(${u * 120}, 100%, 50%)`,
|
||||||
|
height: `${u * 100}%`,
|
||||||
|
}}
|
||||||
|
></div>
|
||||||
|
<div className="totals">
|
||||||
${Math.floor(current)} / ${max}
|
${Math.floor(current)} / ${max}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<Modal visible={newTx} onCancel={() => setNewTx(false)}>
|
||||||
|
<TransactionForm stackId={stack.id} />
|
||||||
|
</Modal>
|
||||||
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,6 +6,7 @@
|
||||||
margin: 1rem;
|
margin: 1rem;
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template: 1fr / 1fr;
|
grid-template: 1fr / 1fr;
|
||||||
|
height: 20vh;
|
||||||
|
|
||||||
.totals {
|
.totals {
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
|
@ -18,17 +19,20 @@
|
||||||
|
|
||||||
.front {
|
.front {
|
||||||
grid-area: 1/1/2/2;
|
grid-area: 1/1/2/2;
|
||||||
|
margin: auto auto 0;
|
||||||
border-radius: 1ch;
|
border-radius: 1ch;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
transition: all 0.2s ease-out;
|
transition: all 0.2s ease-out;
|
||||||
border: 2px solid $color-dark;
|
border: 2px solid $color-dark;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
padding: 2ch;
|
width: 100%;
|
||||||
|
min-height: 1rem;
|
||||||
text-shadow: 2px 2px #2223, -1px -1px #fffa;
|
text-shadow: 2px 2px #2223, -1px -1px #fffa;
|
||||||
}
|
}
|
||||||
|
|
||||||
.back {
|
.back {
|
||||||
|
padding: 0.25rem;
|
||||||
grid-area: 1/1/2/2;
|
grid-area: 1/1/2/2;
|
||||||
background: #222;
|
background: #222;
|
||||||
border-radius: 1ch;
|
border-radius: 1ch;
|
||||||
|
|
Loading…
Reference in New Issue
Block a user