chore: use monorepo organization
This commit is contained in:
parent
deaf7d6ecc
commit
a687827f7e
1099 changed files with 8184 additions and 11535 deletions
|
|
@ -0,0 +1,34 @@
|
|||
import classnames from 'classnames'
|
||||
import React from 'react'
|
||||
import { Label1 } from 'src/components/typography/index'
|
||||
|
||||
const PercentageChart = ({ cashIn, cashOut }) => {
|
||||
const value = cashIn || cashOut !== 0 ? cashIn : 50
|
||||
|
||||
const buildPercentageView = value => {
|
||||
if (value <= 15) return
|
||||
return <Label1 className="text-white">{value}%</Label1>
|
||||
}
|
||||
|
||||
const percentageClasses = {
|
||||
'h-35 rounded-sm flex items-center justify-center': true,
|
||||
'min-w-2 rounded-xs': value < 5 && value > 0
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex h-35 gap-1">
|
||||
<div
|
||||
className={classnames(percentageClasses, 'bg-java')}
|
||||
style={{ width: `${value}%` }}>
|
||||
{buildPercentageView(value, 'cashIn')}
|
||||
</div>
|
||||
<div
|
||||
className={classnames(percentageClasses, 'bg-neon')}
|
||||
style={{ width: `${100 - value}%` }}>
|
||||
{buildPercentageView(100 - value, 'cashOut')}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default PercentageChart
|
||||
|
|
@ -0,0 +1,197 @@
|
|||
import * as d3 from 'd3'
|
||||
import * as R from 'ramda'
|
||||
import React, { useEffect, useRef, useCallback } from 'react'
|
||||
|
||||
const transactionProfit = R.prop('profit')
|
||||
|
||||
const mockPoint = (tx, offsetMs, profit) => {
|
||||
const date = new Date(new Date(tx.created).getTime() + offsetMs).toISOString()
|
||||
return { created: date, profit }
|
||||
}
|
||||
|
||||
// if we're viewing transactions for the past day, then we group by hour. If not, we group by day
|
||||
const formatDay = ({ created }) =>
|
||||
new Date(created).toISOString().substring(0, 10)
|
||||
const formatHour = ({ created }) =>
|
||||
new Date(created).toISOString().substring(0, 13)
|
||||
|
||||
const reducer = (acc, tx) => {
|
||||
const currentProfit = acc.profit || 0
|
||||
return { ...tx, profit: currentProfit + transactionProfit(tx) }
|
||||
}
|
||||
|
||||
const timeFrameMS = {
|
||||
Day: 24 * 3600 * 1000,
|
||||
Week: 7 * 24 * 3600 * 1000,
|
||||
Month: 30 * 24 * 3600 * 1000
|
||||
}
|
||||
|
||||
const RefLineChart = ({
|
||||
data: realData,
|
||||
previousTimeData,
|
||||
previousProfit,
|
||||
timeFrame
|
||||
}) => {
|
||||
const svgRef = useRef()
|
||||
|
||||
const drawGraph = useCallback(() => {
|
||||
const svg = d3.select(svgRef.current)
|
||||
const margin = { top: 0, right: 0, bottom: 0, left: 0 }
|
||||
const width = 336 - margin.left - margin.right
|
||||
const height = 140 - margin.top - margin.bottom
|
||||
|
||||
const massageData = () => {
|
||||
// if we're viewing transactions for the past day, then we group by hour. If not, we group by day
|
||||
const method = timeFrame === 'Day' ? formatHour : formatDay
|
||||
|
||||
const aggregatedTX = R.values(R.reduceBy(reducer, [], method, realData))
|
||||
// if no point exists, then return 2 points at y = 0
|
||||
if (!aggregatedTX.length && !previousTimeData.length) {
|
||||
const mockPoint1 = { created: new Date().toISOString(), profit: 0 }
|
||||
const mockPoint2 = mockPoint(mockPoint1, -3600000, 0)
|
||||
return [[mockPoint1, mockPoint2], true]
|
||||
}
|
||||
// if this time period has no txs, but previous time period has, then % change is -100%
|
||||
if (!aggregatedTX.length && previousTimeData.length) {
|
||||
const mockPoint1 = {
|
||||
created: new Date().toISOString(),
|
||||
profit: 0
|
||||
}
|
||||
const mockPoint2 = mockPoint(mockPoint1, -timeFrameMS[timeFrame], 1)
|
||||
return [[mockPoint1, mockPoint2], false]
|
||||
}
|
||||
// if this time period has txs, but previous doesn't, then % change is +100%
|
||||
if (aggregatedTX.length && !previousTimeData.length) {
|
||||
const mockPoint1 = {
|
||||
created: new Date().toISOString(),
|
||||
profit: 1
|
||||
}
|
||||
const mockPoint2 = mockPoint(mockPoint1, -timeFrameMS[timeFrame], 0)
|
||||
return [[mockPoint1, mockPoint2], false]
|
||||
}
|
||||
// if only one point exists, create point on the left - otherwise the line won't be drawn
|
||||
if (aggregatedTX.length === 1) {
|
||||
return [
|
||||
R.append(
|
||||
{
|
||||
created: new Date(
|
||||
Date.now() - timeFrameMS[timeFrame]
|
||||
).toISOString(),
|
||||
profit: previousProfit
|
||||
},
|
||||
aggregatedTX
|
||||
),
|
||||
false
|
||||
]
|
||||
}
|
||||
// the boolean value is for zeroProfit. It makes the line render at y = 0 instead of y = 50% of container height
|
||||
return [aggregatedTX, false]
|
||||
}
|
||||
|
||||
/* Important step to make the graph look good!
|
||||
This function groups transactions by either day or hour depending on the time frame
|
||||
This makes the line look smooth and not all wonky when there are many transactions in a given time
|
||||
*/
|
||||
const [data, zeroProfit] = massageData()
|
||||
|
||||
// sets width of the graph
|
||||
svg.attr('width', width)
|
||||
|
||||
// background color for the graph
|
||||
svg
|
||||
.append('rect')
|
||||
.attr('x', 0)
|
||||
.attr('y', -margin.top)
|
||||
.attr('width', width + margin.left + margin.right)
|
||||
.attr('height', height + margin.top)
|
||||
.attr('fill', 'var(--ghost)')
|
||||
.attr('transform', `translate(${0},${margin.top})`)
|
||||
|
||||
// gradient color for the graph (creates the "url", the color is applied by calling the url, in the area color fill )
|
||||
svg
|
||||
.append('linearGradient')
|
||||
.attr('id', 'area-gradient')
|
||||
.attr('gradientUnits', 'userSpaceOnUse')
|
||||
.attr('x1', 0)
|
||||
.attr('y1', 0)
|
||||
.attr('x2', 0)
|
||||
.attr('y2', '100%')
|
||||
.selectAll('stop')
|
||||
.data([
|
||||
{ offset: '0%', color: 'var(--zircon)' },
|
||||
{ offset: '25%', color: 'var(--zircon)' },
|
||||
{ offset: '100%', color: 'var(--ghost)' }
|
||||
])
|
||||
.enter()
|
||||
.append('stop')
|
||||
.attr('offset', function (d) {
|
||||
return d.offset
|
||||
})
|
||||
.attr('stop-color', function (d) {
|
||||
return d.color
|
||||
})
|
||||
|
||||
const g = svg
|
||||
.append('g')
|
||||
.attr('transform', `translate(${margin.left},${margin.top})`)
|
||||
|
||||
const xDomain = d3.extent(data, t => t.created)
|
||||
const yDomain = zeroProfit ? [0, 0.1] : [0, d3.max(data, t => t.profit)]
|
||||
|
||||
const y = d3
|
||||
.scaleLinear()
|
||||
// 30 is a margin so that the labels and the percentage change label can fit and not overlay the line path
|
||||
.range([height, 40])
|
||||
.domain([0, yDomain[1]])
|
||||
const x = d3
|
||||
.scaleTime()
|
||||
.domain([new Date(xDomain[0]), new Date(xDomain[1])])
|
||||
.range([0, width])
|
||||
|
||||
const line = d3
|
||||
.line()
|
||||
.x(function (d) {
|
||||
return x(new Date(d.created))
|
||||
})
|
||||
.y(function (d) {
|
||||
return y(d.profit)
|
||||
})
|
||||
|
||||
const area = d3
|
||||
.area()
|
||||
.x(function (d) {
|
||||
return x(new Date(d.created))
|
||||
})
|
||||
.y0(height)
|
||||
.y1(function (d) {
|
||||
return y(d.profit)
|
||||
})
|
||||
|
||||
// area color fill
|
||||
g.append('path')
|
||||
.datum(data)
|
||||
.attr('d', area)
|
||||
.attr('fill', 'url(#area-gradient)')
|
||||
// draw the line
|
||||
g.append('path')
|
||||
.datum(data)
|
||||
.attr('d', line)
|
||||
.attr('fill', 'none')
|
||||
.attr('stroke-width', '2')
|
||||
.attr('stroke-linejoin', 'round')
|
||||
.attr('stroke', 'var(--zodiac)')
|
||||
}, [realData, timeFrame, previousTimeData, previousProfit])
|
||||
|
||||
useEffect(() => {
|
||||
// first we clear old chart DOM elements on component update
|
||||
d3.select(svgRef.current).selectAll('*').remove()
|
||||
drawGraph()
|
||||
}, [drawGraph, realData])
|
||||
|
||||
return (
|
||||
<>
|
||||
<svg ref={svgRef} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
export default RefLineChart
|
||||
|
|
@ -0,0 +1,348 @@
|
|||
import BigNumber from 'bignumber.js'
|
||||
import * as d3 from 'd3'
|
||||
import { getTimezoneOffset } from 'date-fns-tz'
|
||||
import { add, format, startOfWeek, startOfYear } from 'date-fns/fp'
|
||||
import React, { useCallback, useEffect, useMemo, useRef } from 'react'
|
||||
|
||||
import { numberToFiatAmount } from 'src/utils/number'
|
||||
import { MINUTE, DAY, WEEK, MONTH } from 'src/utils/time'
|
||||
|
||||
const Graph = ({ data, timeFrame, timezone }) => {
|
||||
const ref = useRef(null)
|
||||
|
||||
const GRAPH_HEIGHT = 250
|
||||
const GRAPH_WIDTH = 555
|
||||
const GRAPH_MARGIN = useMemo(
|
||||
() => ({
|
||||
top: 20,
|
||||
right: 3.5,
|
||||
bottom: 27,
|
||||
left: 33.5
|
||||
}),
|
||||
[]
|
||||
)
|
||||
|
||||
const offset = getTimezoneOffset(timezone)
|
||||
const NOW = Date.now() + offset
|
||||
|
||||
const periodDomains = {
|
||||
Day: [NOW - DAY, NOW],
|
||||
Week: [NOW - WEEK, NOW],
|
||||
Month: [NOW - MONTH, NOW]
|
||||
}
|
||||
|
||||
const dataPoints = useMemo(
|
||||
() => ({
|
||||
Day: {
|
||||
freq: 24,
|
||||
step: 60 * 60 * 1000,
|
||||
tick: d3.utcHour.every(4),
|
||||
labelFormat: '%H:%M'
|
||||
},
|
||||
Week: {
|
||||
freq: 7,
|
||||
step: 24 * 60 * 60 * 1000,
|
||||
tick: d3.utcDay.every(1),
|
||||
labelFormat: '%a %d'
|
||||
},
|
||||
Month: {
|
||||
freq: 30,
|
||||
step: 24 * 60 * 60 * 1000,
|
||||
tick: d3.utcDay.every(2),
|
||||
labelFormat: '%d'
|
||||
}
|
||||
}),
|
||||
[]
|
||||
)
|
||||
|
||||
const filterDay = useCallback(
|
||||
x => (timeFrame === 'Day' ? x.getUTCHours() === 0 : x.getUTCDate() === 1),
|
||||
[timeFrame]
|
||||
)
|
||||
|
||||
const getPastAndCurrentDayLabels = useCallback(d => {
|
||||
const currentDate = new Date(d)
|
||||
const currentDateDay = currentDate.getUTCDate()
|
||||
const currentDateWeekday = currentDate.getUTCDay()
|
||||
const currentDateMonth = currentDate.getUTCMonth()
|
||||
|
||||
const previousDate = new Date(currentDate.getTime())
|
||||
previousDate.setUTCDate(currentDateDay - 1)
|
||||
|
||||
const previousDateDay = previousDate.getUTCDate()
|
||||
const previousDateWeekday = previousDate.getUTCDay()
|
||||
const previousDateMonth = previousDate.getUTCMonth()
|
||||
|
||||
const daysOfWeek = Array.from(Array(7)).map((_, i) =>
|
||||
format('EEE', add({ days: i }, startOfWeek(new Date())))
|
||||
)
|
||||
|
||||
const months = Array.from(Array(12)).map((_, i) =>
|
||||
format('LLL', add({ months: i }, startOfYear(new Date())))
|
||||
)
|
||||
|
||||
return {
|
||||
previous:
|
||||
currentDateMonth !== previousDateMonth
|
||||
? months[previousDateMonth]
|
||||
: `${daysOfWeek[previousDateWeekday]} ${previousDateDay}`,
|
||||
current:
|
||||
currentDateMonth !== previousDateMonth
|
||||
? months[currentDateMonth]
|
||||
: `${daysOfWeek[currentDateWeekday]} ${currentDateDay}`
|
||||
}
|
||||
}, [])
|
||||
|
||||
const buildTicks = useCallback(
|
||||
domain => {
|
||||
const points = []
|
||||
|
||||
const roundDate = d => {
|
||||
const step = dataPoints[timeFrame].step
|
||||
return new Date(Math.ceil(d.valueOf() / step) * step)
|
||||
}
|
||||
|
||||
for (let i = 0; i <= dataPoints[timeFrame].freq; i++) {
|
||||
const stepDate = new Date(NOW - i * dataPoints[timeFrame].step)
|
||||
if (roundDate(stepDate) > domain[1]) continue
|
||||
if (stepDate < domain[0]) continue
|
||||
points.push(roundDate(stepDate))
|
||||
}
|
||||
|
||||
return points
|
||||
},
|
||||
[NOW, dataPoints, timeFrame]
|
||||
)
|
||||
|
||||
const x = d3
|
||||
.scaleUtc()
|
||||
.domain(periodDomains[timeFrame])
|
||||
.range([GRAPH_MARGIN.left, GRAPH_WIDTH - GRAPH_MARGIN.right])
|
||||
|
||||
const y = d3
|
||||
.scaleLinear()
|
||||
.domain([
|
||||
0,
|
||||
(d3.max(data, d => new BigNumber(d.fiat).toNumber()) ?? 1000) * 1.05
|
||||
])
|
||||
.nice()
|
||||
.range([GRAPH_HEIGHT - GRAPH_MARGIN.bottom, GRAPH_MARGIN.top])
|
||||
|
||||
const buildBackground = useCallback(
|
||||
g => {
|
||||
g.append('rect')
|
||||
.attr('x', 0)
|
||||
.attr('y', GRAPH_MARGIN.top)
|
||||
.attr('width', GRAPH_WIDTH)
|
||||
.attr('height', GRAPH_HEIGHT - GRAPH_MARGIN.top - GRAPH_MARGIN.bottom)
|
||||
.attr('fill', 'var(--ghost)')
|
||||
},
|
||||
[GRAPH_MARGIN]
|
||||
)
|
||||
|
||||
const buildXAxis = useCallback(
|
||||
g =>
|
||||
g
|
||||
.attr(
|
||||
'transform',
|
||||
`translate(0, ${GRAPH_HEIGHT - GRAPH_MARGIN.bottom})`
|
||||
)
|
||||
.call(
|
||||
d3
|
||||
.axisBottom(x)
|
||||
.ticks(dataPoints[timeFrame].tick)
|
||||
.tickFormat(d => {
|
||||
return d3.timeFormat(dataPoints[timeFrame].labelFormat)(
|
||||
d.getTime() + d.getTimezoneOffset() * MINUTE
|
||||
)
|
||||
})
|
||||
)
|
||||
.call(g => g.select('.domain').remove()),
|
||||
[GRAPH_MARGIN, dataPoints, timeFrame, x]
|
||||
)
|
||||
|
||||
const buildYAxis = useCallback(
|
||||
g =>
|
||||
g
|
||||
.attr('transform', `translate(${GRAPH_MARGIN.left}, 0)`)
|
||||
.call(
|
||||
d3
|
||||
.axisLeft(y)
|
||||
.ticks(5)
|
||||
.tickFormat(d => {
|
||||
if (d >= 1000) return numberToFiatAmount(d / 1000) + 'k'
|
||||
|
||||
return numberToFiatAmount(d)
|
||||
})
|
||||
)
|
||||
.call(g => g.select('.domain').remove())
|
||||
.selectAll('text')
|
||||
.attr('dy', '-0.25rem'),
|
||||
[GRAPH_MARGIN, y]
|
||||
)
|
||||
|
||||
const buildGrid = useCallback(
|
||||
g => {
|
||||
g.attr('stroke', 'var(--zircon2)')
|
||||
.attr('fill', 'var(--zircon2)')
|
||||
// Vertical lines
|
||||
.call(g =>
|
||||
g
|
||||
.append('g')
|
||||
.selectAll('line')
|
||||
.data(buildTicks(x.domain()))
|
||||
.join('line')
|
||||
.attr('x1', d => 0.5 + x(d))
|
||||
.attr('x2', d => 0.5 + x(d))
|
||||
.attr('y1', GRAPH_MARGIN.top)
|
||||
.attr('y2', GRAPH_HEIGHT - GRAPH_MARGIN.bottom)
|
||||
.attr('stroke-width', 1)
|
||||
)
|
||||
// Horizontal lines
|
||||
.call(g =>
|
||||
g
|
||||
.append('g')
|
||||
.selectAll('line')
|
||||
.data(d3.axisLeft(y).scale().ticks(5))
|
||||
.join('line')
|
||||
.attr('y1', d => 0.5 + y(d))
|
||||
.attr('y2', d => 0.5 + y(d))
|
||||
.attr('x1', GRAPH_MARGIN.left)
|
||||
.attr('x2', GRAPH_WIDTH)
|
||||
)
|
||||
// Thick vertical lines
|
||||
.call(g =>
|
||||
g
|
||||
.append('g')
|
||||
.selectAll('line')
|
||||
.data(buildTicks(x.domain()).filter(filterDay))
|
||||
.join('line')
|
||||
.attr('class', 'dateSeparator')
|
||||
.attr('x1', d => 0.5 + x(d))
|
||||
.attr('x2', d => 0.5 + x(d))
|
||||
.attr('y1', GRAPH_MARGIN.top - 10)
|
||||
.attr('y2', GRAPH_HEIGHT - GRAPH_MARGIN.bottom)
|
||||
.attr('stroke-width', 2)
|
||||
.join('text')
|
||||
)
|
||||
// Left side breakpoint label
|
||||
.call(g => {
|
||||
const separator = d3?.select('.dateSeparator')?.node()?.getBBox()
|
||||
|
||||
if (!separator) return
|
||||
|
||||
const breakpoint = buildTicks(x.domain()).filter(filterDay)
|
||||
|
||||
const labels = getPastAndCurrentDayLabels(breakpoint)
|
||||
|
||||
return g
|
||||
.append('text')
|
||||
.attr('x', separator.x - 7)
|
||||
.attr('y', separator.y)
|
||||
.attr('text-anchor', 'end')
|
||||
.attr('dy', '.25em')
|
||||
.text(labels.previous)
|
||||
})
|
||||
// Right side breakpoint label
|
||||
.call(g => {
|
||||
const separator = d3?.select('.dateSeparator')?.node()?.getBBox()
|
||||
|
||||
if (!separator) return
|
||||
|
||||
const breakpoint = buildTicks(x.domain()).filter(filterDay)
|
||||
|
||||
const labels = getPastAndCurrentDayLabels(breakpoint)
|
||||
|
||||
return g
|
||||
.append('text')
|
||||
.attr('x', separator.x + 7)
|
||||
.attr('y', separator.y)
|
||||
.attr('text-anchor', 'start')
|
||||
.attr('dy', '.25em')
|
||||
.text(labels.current)
|
||||
})
|
||||
},
|
||||
[GRAPH_MARGIN, buildTicks, getPastAndCurrentDayLabels, x, y, filterDay]
|
||||
)
|
||||
|
||||
const formatTicksText = useCallback(
|
||||
() =>
|
||||
d3
|
||||
.selectAll('.tick text')
|
||||
.style('stroke', 'var(--comet)')
|
||||
.style('fill', 'var(--comet)')
|
||||
.style('stroke-width', 0)
|
||||
.style('font-family', 'var(--museo)'),
|
||||
[]
|
||||
)
|
||||
|
||||
const formatText = useCallback(
|
||||
() =>
|
||||
d3
|
||||
.selectAll('text')
|
||||
.style('stroke', 'var(--comet)')
|
||||
.style('fill', 'var(--comet)')
|
||||
.style('stroke-width', 0)
|
||||
.style('font-family', 'var(--museo)'),
|
||||
[]
|
||||
)
|
||||
|
||||
const formatTicks = useCallback(() => {
|
||||
d3.selectAll('.tick line')
|
||||
.style('stroke', 'transparent')
|
||||
.style('fill', 'transparent')
|
||||
}, [])
|
||||
|
||||
const drawData = useCallback(
|
||||
g => {
|
||||
g.selectAll('circle')
|
||||
.data(data)
|
||||
.join('circle')
|
||||
.attr('cx', d => {
|
||||
const created = new Date(d.created)
|
||||
return x(created.setTime(created.getTime() + offset))
|
||||
})
|
||||
.attr('cy', d => y(new BigNumber(d.fiat).toNumber()))
|
||||
.attr('fill', d =>
|
||||
d.txClass === 'cashIn' ? 'var(--java)' : 'var(--neon)'
|
||||
)
|
||||
.attr('r', 3.5)
|
||||
},
|
||||
[data, offset, x, y]
|
||||
)
|
||||
|
||||
const drawChart = useCallback(() => {
|
||||
const svg = d3
|
||||
.select(ref.current)
|
||||
.attr('viewBox', [0, 0, GRAPH_WIDTH, GRAPH_HEIGHT])
|
||||
|
||||
svg.append('g').call(buildBackground)
|
||||
svg.append('g').call(buildGrid)
|
||||
svg.append('g').call(buildXAxis)
|
||||
svg.append('g').call(buildYAxis)
|
||||
svg.append('g').call(formatTicksText)
|
||||
svg.append('g').call(formatText)
|
||||
svg.append('g').call(formatTicks)
|
||||
svg.append('g').call(drawData)
|
||||
|
||||
return svg.node()
|
||||
}, [
|
||||
buildBackground,
|
||||
buildGrid,
|
||||
buildXAxis,
|
||||
buildYAxis,
|
||||
drawData,
|
||||
formatText,
|
||||
formatTicks,
|
||||
formatTicksText
|
||||
])
|
||||
|
||||
useEffect(() => {
|
||||
d3.select(ref.current).selectAll('*').remove()
|
||||
drawChart()
|
||||
}, [drawChart])
|
||||
|
||||
return <svg ref={ref} />
|
||||
}
|
||||
|
||||
export default Graph
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
import React from 'react'
|
||||
import { Info1, Label1 } from 'src/components/typography/index'
|
||||
const InfoWithLabel = ({ info, label }) => {
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
<Info1 className="mb-0">{info}</Info1>
|
||||
<Label1 className="m-0">{label}</Label1>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default InfoWithLabel
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
import classnames from 'classnames'
|
||||
import * as R from 'ramda'
|
||||
import React, { useState } from 'react'
|
||||
import { H4 } from 'src/components/typography'
|
||||
|
||||
const ranges = ['Month', 'Week', 'Day']
|
||||
|
||||
const Nav = ({ handleSetRange, showPicker }) => {
|
||||
const [clickedItem, setClickedItem] = useState('Day')
|
||||
|
||||
const isSelected = R.equals(clickedItem)
|
||||
const handleClick = range => {
|
||||
setClickedItem(range)
|
||||
handleSetRange(range)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex justify-between items-center">
|
||||
<H4 noMargin>{'System performance'}</H4>
|
||||
{showPicker && (
|
||||
<div className="flex gap-6">
|
||||
{ranges.map((it, idx) => {
|
||||
return (
|
||||
<div
|
||||
key={idx}
|
||||
onClick={e => handleClick(e.target.innerText)}
|
||||
className={classnames({
|
||||
'cursor-pointer text-comet': true,
|
||||
'font-bold text-zodiac border-b-zodiac border-b-2':
|
||||
isSelected(it)
|
||||
})}>
|
||||
{it}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Nav
|
||||
|
|
@ -0,0 +1,283 @@
|
|||
import { useQuery, gql } from '@apollo/client'
|
||||
import BigNumber from 'bignumber.js'
|
||||
import classnames from 'classnames'
|
||||
import { isAfter } from 'date-fns/fp'
|
||||
import * as R from 'ramda'
|
||||
import React, { useState } from 'react'
|
||||
import { Info2, Label1, Label2, P } from 'src/components/typography/index'
|
||||
import PercentDownIcon from 'src/styling/icons/dashboard/down.svg?react'
|
||||
import PercentNeutralIcon from 'src/styling/icons/dashboard/equal.svg?react'
|
||||
import PercentUpIcon from 'src/styling/icons/dashboard/up.svg?react'
|
||||
|
||||
import { EmptyTable } from 'src/components/table'
|
||||
import { java, neon } from 'src/styling/variables'
|
||||
import { fromNamespace } from 'src/utils/config'
|
||||
import { DAY, WEEK, MONTH } from 'src/utils/time'
|
||||
import { timezones } from 'src/utils/timezone-list'
|
||||
import { toTimezone } from 'src/utils/timezones'
|
||||
|
||||
import PercentageChart from './Graphs/PercentageChart'
|
||||
import LineChart from './Graphs/RefLineChart'
|
||||
import Scatterplot from './Graphs/RefScatterplot'
|
||||
import InfoWithLabel from './InfoWithLabel'
|
||||
import Nav from './Nav'
|
||||
|
||||
BigNumber.config({ ROUNDING_MODE: BigNumber.ROUND_HALF_UP })
|
||||
|
||||
const getFiats = R.map(R.prop('fiat'))
|
||||
|
||||
const GET_DATA = gql`
|
||||
query getData($excludeTestingCustomers: Boolean) {
|
||||
transactions(excludeTestingCustomers: $excludeTestingCustomers) {
|
||||
fiatCode
|
||||
fiat
|
||||
fixedFee
|
||||
commissionPercentage
|
||||
created
|
||||
txClass
|
||||
error
|
||||
profit
|
||||
dispense
|
||||
sendConfirmed
|
||||
}
|
||||
fiatRates {
|
||||
code
|
||||
name
|
||||
rate
|
||||
}
|
||||
config
|
||||
}
|
||||
`
|
||||
|
||||
const SystemPerformance = () => {
|
||||
const [selectedRange, setSelectedRange] = useState('Day')
|
||||
const { data, loading } = useQuery(GET_DATA, {
|
||||
variables: { excludeTestingCustomers: true }
|
||||
})
|
||||
const fiatLocale = fromNamespace('locale')(data?.config).fiatCurrency
|
||||
const timezone = fromNamespace('locale')(data?.config).timezone
|
||||
|
||||
const NOW = Date.now()
|
||||
|
||||
const periodDomains = {
|
||||
Day: [NOW - DAY, NOW],
|
||||
Week: [NOW - WEEK, NOW],
|
||||
Month: [NOW - MONTH, NOW]
|
||||
}
|
||||
|
||||
const isInRangeAndNoError = getLastTimePeriod => t => {
|
||||
if (t.error !== null) return false
|
||||
if (t.txClass === 'cashOut' && !t.dispense) return false
|
||||
if (t.txClass === 'cashIn' && !t.sendConfirmed) return false
|
||||
if (!getLastTimePeriod) {
|
||||
return (
|
||||
t.error === null &&
|
||||
isAfter(
|
||||
toTimezone(t.created, timezone),
|
||||
toTimezone(periodDomains[selectedRange][1], timezone)
|
||||
) &&
|
||||
isAfter(
|
||||
toTimezone(periodDomains[selectedRange][0], timezone),
|
||||
toTimezone(t.created, timezone)
|
||||
)
|
||||
)
|
||||
}
|
||||
return (
|
||||
t.error === null &&
|
||||
isAfter(
|
||||
toTimezone(periodDomains[selectedRange][1], timezone),
|
||||
toTimezone(t.created, timezone)
|
||||
) &&
|
||||
isAfter(
|
||||
toTimezone(t.created, timezone),
|
||||
toTimezone(periodDomains[selectedRange][0], timezone)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
const convertFiatToLocale = item => {
|
||||
if (item.fiatCode === fiatLocale) return item
|
||||
const itemRate = R.find(R.propEq('code', item.fiatCode))(data.fiatRates)
|
||||
const localeRate = R.find(R.propEq('code', fiatLocale))(data.fiatRates)
|
||||
const multiplier = localeRate.rate / itemRate.rate
|
||||
return { ...item, fiat: parseFloat(item.fiat) * multiplier }
|
||||
}
|
||||
|
||||
const transactionsToShow = R.map(convertFiatToLocale)(
|
||||
R.filter(isInRangeAndNoError(false), data?.transactions ?? [])
|
||||
)
|
||||
const transactionsLastTimePeriod = R.map(convertFiatToLocale)(
|
||||
R.filter(isInRangeAndNoError(true), data?.transactions ?? [])
|
||||
)
|
||||
|
||||
const getNumTransactions = () => {
|
||||
return R.length(transactionsToShow)
|
||||
}
|
||||
|
||||
const getFiatVolume = () =>
|
||||
new BigNumber(R.sum(getFiats(transactionsToShow))).toFormat(2)
|
||||
|
||||
const getProfit = transactions => {
|
||||
return R.reduce(
|
||||
(acc, value) => acc.plus(value.profit),
|
||||
new BigNumber(0),
|
||||
transactions
|
||||
)
|
||||
}
|
||||
|
||||
const getPercentChange = () => {
|
||||
const thisTimePeriodProfit = getProfit(transactionsToShow)
|
||||
const previousTimePeriodProfit = getProfit(transactionsLastTimePeriod)
|
||||
|
||||
if (thisTimePeriodProfit.eq(previousTimePeriodProfit)) return 0
|
||||
if (previousTimePeriodProfit.eq(0)) return 100
|
||||
|
||||
return thisTimePeriodProfit
|
||||
.minus(previousTimePeriodProfit)
|
||||
.times(100)
|
||||
.div(previousTimePeriodProfit)
|
||||
.toNumber()
|
||||
}
|
||||
|
||||
const getDirectionPercent = () => {
|
||||
const [cashIn, cashOut] = R.partition(R.propEq('txClass', 'cashIn'))(
|
||||
transactionsToShow
|
||||
)
|
||||
const totalLength = cashIn.length + cashOut.length
|
||||
if (totalLength === 0) {
|
||||
return { cashIn: 0, cashOut: 0 }
|
||||
}
|
||||
|
||||
return {
|
||||
cashIn: Math.round((cashIn.length / totalLength) * 100),
|
||||
cashOut: Math.round((cashOut.length / totalLength) * 100)
|
||||
}
|
||||
}
|
||||
|
||||
const percentChange = getPercentChange()
|
||||
|
||||
const percentageClasses = {
|
||||
'text-tomato': percentChange < 0,
|
||||
'text-spring4': percentChange > 0,
|
||||
'text-comet': percentChange === 0,
|
||||
'flex items-center justify-center gap-1': true
|
||||
}
|
||||
|
||||
const getPercentageIcon = () => {
|
||||
const className = 'w-4 h-4'
|
||||
if (percentChange === 0) return <PercentNeutralIcon className={className} />
|
||||
if (percentChange > 0) return <PercentUpIcon className={className} />
|
||||
return <PercentDownIcon className={className} />
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Nav
|
||||
showPicker={!loading && !R.isEmpty(data.transactions)}
|
||||
handleSetRange={setSelectedRange}
|
||||
/>
|
||||
{!loading && R.isEmpty(data.transactions) && (
|
||||
<EmptyTable className="pt-10" message="No transactions so far" />
|
||||
)}
|
||||
{!loading && !R.isEmpty(data.transactions) && (
|
||||
<div className="flex flex-col gap-12">
|
||||
<div className="flex gap-16">
|
||||
<InfoWithLabel info={getNumTransactions()} label={'transactions'} />
|
||||
<InfoWithLabel
|
||||
info={getFiatVolume()}
|
||||
label={`${data?.config.locale_fiatCurrency} volume`}
|
||||
/>
|
||||
</div>
|
||||
<div className="h-62">
|
||||
<div className="flex justify-between mb-4">
|
||||
<Label2 noMargin>Transactions</Label2>
|
||||
<div className="flex items-center">
|
||||
<P noMargin>
|
||||
{timezones[timezone]?.short ?? timezones[timezone]?.long}{' '}
|
||||
timezone
|
||||
</P>
|
||||
<span className="h-4 w-[1px] bg-comet2 mr-4 ml-8" />
|
||||
<div className="flex flex-row gap-4">
|
||||
<div className="flex items-center">
|
||||
<svg width={8} height={8}>
|
||||
<rect width={8} height={8} rx={4} fill={java} />
|
||||
</svg>
|
||||
<Label1 noMargin className="ml-2">
|
||||
In
|
||||
</Label1>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<svg width={8} height={8}>
|
||||
<rect width={8} height={8} rx={4} fill={neon} />
|
||||
</svg>
|
||||
<Label1 noMargin className="ml-2">
|
||||
Out
|
||||
</Label1>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Scatterplot
|
||||
timeFrame={selectedRange}
|
||||
data={transactionsToShow}
|
||||
timezone={timezone}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex h-62">
|
||||
<div className="flex-2">
|
||||
<Label2 noMargin className="mb-4">
|
||||
Profit from commissions
|
||||
</Label2>
|
||||
<div className="flex justify-between mt-6 mr-7 -mb-8 ml-4 relative">
|
||||
<Info2 noMargin>
|
||||
{`${getProfit(transactionsToShow).toFormat(2)} ${
|
||||
data?.config.locale_fiatCurrency
|
||||
}`}
|
||||
</Info2>
|
||||
<Info2 noMargin className={classnames(percentageClasses)}>
|
||||
{getPercentageIcon()}
|
||||
{`${new BigNumber(percentChange).toFormat(2)}%`}
|
||||
</Info2>
|
||||
</div>
|
||||
<LineChart
|
||||
timeFrame={selectedRange}
|
||||
data={transactionsToShow}
|
||||
previousTimeData={transactionsLastTimePeriod}
|
||||
previousProfit={getProfit(transactionsLastTimePeriod)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<Label2 noMargin>Direction</Label2>
|
||||
<div className="flex flex-row gap-4">
|
||||
<div className="flex items-center">
|
||||
<svg width={8} height={8}>
|
||||
<rect width={8} height={8} rx={2} fill={java} />
|
||||
</svg>
|
||||
<Label1 noMargin className="ml-2">
|
||||
In
|
||||
</Label1>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<svg width={8} height={8}>
|
||||
<rect width={8} height={8} rx={2} fill={neon} />
|
||||
</svg>
|
||||
<Label1 noMargin className="ml-2">
|
||||
Out
|
||||
</Label1>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<PercentageChart
|
||||
cashIn={getDirectionPercent().cashIn}
|
||||
cashOut={getDirectionPercent().cashOut}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default SystemPerformance
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
import SystemPerformance from './SystemPerformance'
|
||||
export default SystemPerformance
|
||||
Loading…
Add table
Add a link
Reference in a new issue