mirror of
https://github.com/cupcakearmy/obolus.git
synced 2025-09-06 00:00:40 +00:00
client
This commit is contained in:
105
www/components/chart.js
Normal file
105
www/components/chart.js
Normal file
@@ -0,0 +1,105 @@
|
||||
import React, { useEffect, useRef } from 'react'
|
||||
import ChartJS from 'chart.js'
|
||||
import { capitalize } from '../utils/misc'
|
||||
|
||||
|
||||
const Chart = ({ stats }) => {
|
||||
|
||||
const canvas = useRef(undefined)
|
||||
|
||||
const { _error, ...users } = stats
|
||||
|
||||
const chartColors = [
|
||||
'rgb(255,74,109)',
|
||||
'rgb(255,194,81)',
|
||||
'rgb(63,197,255)',
|
||||
'rgb(46,192,37)',
|
||||
]
|
||||
|
||||
const formatData = (data) => {
|
||||
const sorted = Object.entries(data).sort((a, b) => a[1] < b[1] ? 1 : -1)
|
||||
const positive = sorted.filter(([name, amount]) => amount >= 0).reverse()
|
||||
const negative = sorted.filter(([name, amount]) => amount < 0)
|
||||
|
||||
const getProgressiveValues = (arr) => {
|
||||
if (arr.length === 0) return []
|
||||
|
||||
const tmp = [arr[0]]
|
||||
let highest = arr[0][1]
|
||||
|
||||
for (const cur of arr.slice(1)) {
|
||||
const delta = cur[1] - highest
|
||||
tmp.push([cur[0], delta])
|
||||
highest += delta
|
||||
}
|
||||
return tmp
|
||||
}
|
||||
|
||||
return [
|
||||
...getProgressiveValues(positive),
|
||||
...getProgressiveValues(negative),
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
console.log(`Error margin: ${_error}`)
|
||||
|
||||
// TODO: Consistent color scheme
|
||||
const data = {
|
||||
labels: ['Current'],
|
||||
datasets: formatData(users).map(([name, amount], i) => ({
|
||||
label: name,
|
||||
backgroundColor: chartColors[i],
|
||||
data: [amount],
|
||||
})),
|
||||
}
|
||||
|
||||
const chart = new ChartJS(canvas.current, {
|
||||
type: 'horizontalBar',
|
||||
data,
|
||||
options: {
|
||||
tooltips: {
|
||||
enabled: false,
|
||||
},
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
scales: {
|
||||
xAxes: [{
|
||||
stacked: true,
|
||||
}],
|
||||
yAxes: [{
|
||||
display: false,
|
||||
stacked: true,
|
||||
}],
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
}, [canvas])
|
||||
|
||||
return <React.Fragment>
|
||||
<div className={'chart-container'}>
|
||||
<canvas ref={canvas}/>
|
||||
</div>
|
||||
<br/>
|
||||
<div className={'text-center'}>
|
||||
{Object.entries(users).map(([user, value], i) => <span key={i} className={'label m-1'}>
|
||||
{capitalize(user)} <b>{value}</b>
|
||||
</span>)}
|
||||
{/*<div className={'mt-2'}><small>Error margin:{_error}</small></div>*/}
|
||||
</div>
|
||||
|
||||
{/* language=CSS */}
|
||||
<style jsx>{`
|
||||
.chart-container {
|
||||
width: 100%;
|
||||
height: 8em;
|
||||
position: relative;
|
||||
}
|
||||
`}</style>
|
||||
</React.Fragment>
|
||||
}
|
||||
|
||||
export default Chart
|
14
www/components/humanize.js
Normal file
14
www/components/humanize.js
Normal file
@@ -0,0 +1,14 @@
|
||||
import React from 'react'
|
||||
import { Duration } from 'uhrwerk'
|
||||
|
||||
|
||||
export const timePassedSinceTimestamp = since => new Duration(Date.now() - since, 'milliseconds').humanize() + ' ago'
|
||||
|
||||
export const formatPrice = price => {
|
||||
const [int, float] = price.toFixed(2).split('.')
|
||||
return <span className={'mr-1'}>
|
||||
<b>{int}
|
||||
<small>,{float}</small>
|
||||
</b>
|
||||
</span>
|
||||
}
|
110
www/components/layout.js
Executable file
110
www/components/layout.js
Executable file
@@ -0,0 +1,110 @@
|
||||
import React from 'react'
|
||||
import Head from 'next/head'
|
||||
import Link from 'next/link'
|
||||
|
||||
import { logout } from '../utils/auth'
|
||||
import { getRandomSlogan } from '../utils/misc'
|
||||
|
||||
|
||||
const Layout = ({ children }) => {
|
||||
const title = getRandomSlogan()
|
||||
|
||||
return <React.Fragment>
|
||||
<Head>
|
||||
<title>{title}</title>
|
||||
|
||||
{/* https://www.flaticon.com/packs/zoo-20 */}
|
||||
<link rel="stylesheet" href="/static/css/spectre.min.css"/>
|
||||
<link rel="stylesheet" href="/static/css/spectre-exp.min.css"/>
|
||||
<link rel="stylesheet" href="/static/css/spectre-icons.min.css"/>
|
||||
<link rel="stylesheet" href="/static/css/main.css"/>
|
||||
|
||||
<meta name="viewport" content="initial-scale=1.0, width=device-width"/>
|
||||
</Head>
|
||||
|
||||
<div id='navbar-container'>
|
||||
<header className="navbar">
|
||||
<section className="navbar-section">
|
||||
<Link href='/'>
|
||||
<a className="btn btn-link">
|
||||
<img src={'/static/icons/ui/stats.svg'} alt={'overview'}/>
|
||||
<span className={'hide-sm'}> Overview</span>
|
||||
</a>
|
||||
</Link>
|
||||
<Link href='/new'>
|
||||
<a className="btn btn-link">
|
||||
<img src={'/static/icons/ui/add.svg'} alt={'add'}/>
|
||||
<span className={'hide-sm'}> New</span>
|
||||
</a>
|
||||
</Link>
|
||||
</section>
|
||||
<section className="navbar-center hide-sm">
|
||||
<b>{title}</b>
|
||||
</section>
|
||||
<section className="navbar-section">
|
||||
<Link href='/me'>
|
||||
<a className="btn btn-link">
|
||||
<img src={'/static/icons/ui/me.svg'} alt={'profile'}/>
|
||||
<span className={'hide-sm'}> Me</span>
|
||||
</a>
|
||||
</Link>
|
||||
<a onClick={logout} className="btn btn-link">
|
||||
<img src={'/static/icons/ui/logout.svg'} alt={'add'}/>
|
||||
<span className={'hide-sm'}> Logout</span>
|
||||
</a>
|
||||
</section>
|
||||
</header>
|
||||
</div>
|
||||
|
||||
<main>
|
||||
<div className="container">
|
||||
<div id='content'>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
{/* language=CSS */}
|
||||
<style jsx>{`
|
||||
main {
|
||||
padding: 6em 0;
|
||||
}
|
||||
|
||||
#navbar-container {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
padding: 1em;
|
||||
box-shadow: 0 -0.4em 1em -0.5em;
|
||||
background-color: var(--clr-white);
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
#navbar-container header {
|
||||
max-width: 50em;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
#content {
|
||||
width: 100%;
|
||||
max-width: 32em;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.navbar a {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.navbar a img {
|
||||
height: 1.5em;
|
||||
width: 1.5em;
|
||||
vertical-align: bottom;
|
||||
}
|
||||
`}</style>
|
||||
|
||||
</React.Fragment>
|
||||
}
|
||||
|
||||
|
||||
export default Layout
|
60
www/components/purchase.js
Normal file
60
www/components/purchase.js
Normal file
@@ -0,0 +1,60 @@
|
||||
import { callAPI } from '../utils/api'
|
||||
import { formatPrice, timePassedSinceTimestamp } from './humanize'
|
||||
import { capitalize, getAvatarOfFallback } from '../utils/misc'
|
||||
import React from 'react'
|
||||
|
||||
|
||||
const deletePurchase = id => callAPI(null, {
|
||||
url: `/api/purchases/${id}`,
|
||||
method: 'delete',
|
||||
}).then(() => location.reload()).catch(() => alert('There was a problem deleting the purchase'))
|
||||
|
||||
const Purchase = ({ purchase, me }) => <div className="purchases-item tile tile-centered">
|
||||
<div className="tile-icon">
|
||||
<figure className="avatar avatar-lg">
|
||||
<img alt="avatar-icon" src={`/static/icons/animals/${getAvatarOfFallback(purchase.payer.avatar)}.svg`}/>
|
||||
</figure>
|
||||
</div>
|
||||
<div className="tile-content">
|
||||
<div className="tile-title">
|
||||
{formatPrice(purchase.price)}
|
||||
<small className={'float-right'}>{timePassedSinceTimestamp(purchase.when)}</small>
|
||||
</div>
|
||||
<small className="tile-subtitle text-gray">
|
||||
<b>{capitalize(purchase.payer.name)}</b>
|
||||
<span className={'float-right'}>
|
||||
{purchase.debtors.map(debtor => debtor.name).map(capitalize).join(' · ')}
|
||||
</span>
|
||||
</small>
|
||||
</div>
|
||||
<div className="tile-action">
|
||||
<div className="dropdown dropdown-right">
|
||||
<a className="btn btn-link dropdown-toggle" tabIndex="0">
|
||||
<i className="icon icon-more-vert"/>
|
||||
</a>
|
||||
<ul className="menu">
|
||||
<li className="menu-item">
|
||||
{purchase.payer.id === me.id
|
||||
? <a onClick={() => deletePurchase(purchase.id)}>
|
||||
<i className="icon icon-delete"/> Delete
|
||||
</a>
|
||||
: <span>Only the payer can cancel the purchase</span>
|
||||
}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* language=CSS */}
|
||||
<style jsx>{`
|
||||
.purchases-item {
|
||||
margin-bottom: 1.5em;
|
||||
}
|
||||
|
||||
.purchases-item .tile-subtitle {
|
||||
display: block;
|
||||
}
|
||||
`}</style>
|
||||
</div>
|
||||
|
||||
export default Purchase
|
Reference in New Issue
Block a user