chore: use monorepo organization

This commit is contained in:
Rafael Taranto 2025-05-12 10:52:54 +01:00
parent deaf7d6ecc
commit a687827f7e
1099 changed files with 8184 additions and 11535 deletions

View file

@ -0,0 +1,387 @@
import { useQuery, gql } from '@apollo/client'
import classnames from 'classnames'
import { endOfToday } from 'date-fns'
import { subDays, format, add, startOfWeek } from 'date-fns/fp'
import * as R from 'ramda'
import React, { useState } from 'react'
import TitleSection from 'src/components/layout/TitleSection'
import { Info2, P } from 'src/components/typography'
import DownIcon from 'src/styling/icons/dashboard/down.svg?react'
import EqualIcon from 'src/styling/icons/dashboard/equal.svg?react'
import UpIcon from 'src/styling/icons/dashboard/up.svg?react'
import { Select } from 'src/components/inputs'
import { fromNamespace } from 'src/utils/config'
import { numberToFiatAmount } from 'src/utils/number'
import { DAY, WEEK, MONTH } from 'src/utils/time'
import LegendEntry from './components/LegendEntry'
import HourOfDayWrapper from './components/wrappers/HourOfDayWrapper'
import OverTimeWrapper from './components/wrappers/OverTimeWrapper'
import TopMachinesWrapper from './components/wrappers/TopMachinesWrapper'
import VolumeOverTimeWrapper from './components/wrappers/VolumeOverTimeWrapper'
const MACHINE_OPTIONS = [{ code: 'all', display: 'All machines' }]
const REPRESENTING_OPTIONS = [
{ code: 'overTime', display: 'Over time' },
{ code: 'volumeOverTime', display: 'Volume' },
{ code: 'topMachines', display: 'Top machines' },
{ code: 'hourOfTheDay', display: 'Hour of the day' }
]
const PERIOD_OPTIONS = [
{ code: 'day', display: 'Last 24 hours' },
{ code: 'threeDays', display: 'Last 3 days' },
{ code: 'week', display: 'Last 7 days' },
{ code: 'month', display: 'Last 30 days' }
]
const TIME_OPTIONS = {
day: DAY,
threeDays: 3 * DAY,
week: WEEK,
month: MONTH
}
const DAY_OPTIONS = R.map(
it => ({
code: R.toLower(it),
display: it
}),
Array.from(Array(7)).map((_, i) =>
format('EEEE', add({ days: i }, startOfWeek(new Date())))
)
)
const GET_TRANSACTIONS = gql`
query transactions(
$from: DateTimeISO
$until: DateTimeISO
$excludeTestingCustomers: Boolean
) {
transactions(
from: $from
until: $until
excludeTestingCustomers: $excludeTestingCustomers
) {
id
txClass
txHash
toAddress
commissionPercentage
expired
machineName
operatorCompleted
sendConfirmed
dispense
hasError: error
deviceId
fiat
fixedFee
fiatCode
cryptoAtoms
cryptoCode
toAddress
created
profit
}
}
`
const GET_DATA = gql`
query getData {
config
machines {
name
deviceId
}
fiatRates {
code
name
rate
}
}
`
const VerticalLine = () => (
<div className="h-16 border-solid border border-comet2" />
)
const OverviewEntry = ({ label, value, oldValue, currency }) => {
const _oldValue = !oldValue || R.equals(oldValue, 0) ? 1 : oldValue
const growthRate = ((value - oldValue) * 100) / _oldValue
const growthClasses = {
'font-bold': true,
'text-malachite': R.gt(value, oldValue),
'text-tomato': R.gt(oldValue, value)
}
return (
<div>
<P noMargin>{label}</P>
<Info2 noMargin className="mt-[6px] mb-[6px]">
<span className="text-[24px]">{numberToFiatAmount(value)}</span>
{!!currency && ` ${currency}`}
</Info2>
<span className="flex items-center gap-1">
{R.gt(growthRate, 0) && <UpIcon height={10} />}
{R.lt(growthRate, 0) && <DownIcon height={10} />}
{R.equals(growthRate, 0) && <EqualIcon height={10} />}
<P noMargin className={classnames(growthClasses)}>
{numberToFiatAmount(growthRate)}%
</P>
</span>
</div>
)
}
const Analytics = () => {
const { data: txResponse, loading: txLoading } = useQuery(GET_TRANSACTIONS, {
variables: {
from: subDays(65, endOfToday()),
until: endOfToday(),
excludeTestingCustomers: true
}
})
const { data: configResponse, loading: configLoading } = useQuery(GET_DATA)
const [representing, setRepresenting] = useState(REPRESENTING_OPTIONS[0])
const [period, setPeriod] = useState(PERIOD_OPTIONS[0])
const [machine, setMachine] = useState(MACHINE_OPTIONS[0])
const [selectedDay, setSelectedDay] = useState(
R.equals(representing.code, 'hourOfTheDay') ? DAY_OPTIONS[0] : null
)
const loading = txLoading || configLoading
const transactions = R.path(['transactions'])(txResponse) ?? []
const machines = R.path(['machines'])(configResponse) ?? []
const config = R.path(['config'])(configResponse) ?? []
const rates = R.path(['fiatRates'])(configResponse) ?? []
const fiatLocale = fromNamespace('locale')(config).fiatCurrency
const timezone = config?.locale_timezone
const convertFiatToLocale = item => {
if (item.fiatCode === fiatLocale) return item
const itemRate = R.find(R.propEq('code', item.fiatCode))(rates)
const localeRate = R.find(R.propEq('code', fiatLocale))(rates)
const multiplier = localeRate?.rate / itemRate?.rate
return { ...item, fiat: parseFloat(item.fiat) * multiplier }
}
const data =
R.map(convertFiatToLocale)(
transactions?.filter(
tx =>
(!tx.dispensed || !tx.expired) &&
(tx.sendConfirmed || tx.dispense) &&
!tx.hasError
)
) ?? []
const machineOptions = R.clone(MACHINE_OPTIONS)
R.forEach(
m => machineOptions.push({ code: m.deviceId, display: m.name }),
machines
)
const machineTxs = R.filter(
tx => (machine.code === 'all' ? true : tx.deviceId === machine.code),
data
)
const filteredData = timeInterval => ({
current:
machineTxs.filter(d => {
const txDay = new Date(d.created)
const isSameWeekday = !R.isNil(selectedDay)
? R.equals(R.toLower(format('EEEE', txDay)), selectedDay.code)
: true
return isSameWeekday && txDay >= Date.now() - TIME_OPTIONS[timeInterval]
}) ?? [],
previous:
machineTxs.filter(d => {
const txDay = new Date(d.created)
const isSameWeekday = !R.isNil(selectedDay)
? R.equals(R.toLower(format('EEEE', txDay)), selectedDay.code)
: true
return (
isSameWeekday &&
txDay < Date.now() - TIME_OPTIONS[timeInterval] &&
txDay >= Date.now() - 2 * TIME_OPTIONS[timeInterval]
)
}) ?? []
})
const txs = {
current: filteredData(period.code).current.length,
previous: filteredData(period.code).previous.length
}
const median = values => (values.length === 0 ? 0 : R.median(values))
const medianAmount = {
current: median(R.map(d => d.fiat, filteredData(period.code).current)),
previous: median(R.map(d => d.fiat, filteredData(period.code).previous))
}
const txVolume = {
current: R.sum(R.map(d => d.fiat, filteredData(period.code).current)),
previous: R.sum(R.map(d => d.fiat, filteredData(period.code).previous))
}
const commissions = {
current: R.sum(R.map(d => d.profit, filteredData(period.code).current)),
previous: R.sum(R.map(d => d.profit, filteredData(period.code).previous))
}
const handleRepresentationChange = newRepresentation => {
setRepresenting(newRepresentation)
setSelectedDay(
R.equals(newRepresentation.code, 'hourOfTheDay') ? DAY_OPTIONS[0] : null
)
}
const getGraphInfo = representing => {
switch (representing.code) {
case 'overTime':
return (
<OverTimeWrapper
title="Transactions over time"
representing={representing}
period={period}
data={R.map(convertFiatToLocale)(filteredData(period.code).current)}
machines={machineOptions}
selectedMachine={machine}
handleMachineChange={setMachine}
timezone={timezone}
currency={fiatLocale}
/>
)
case 'volumeOverTime':
return (
<VolumeOverTimeWrapper
title="Transactions volume over time"
representing={representing}
period={period}
data={R.map(convertFiatToLocale)(filteredData(period.code).current)}
machines={machineOptions}
selectedMachine={machine}
handleMachineChange={setMachine}
timezone={timezone}
currency={fiatLocale}
/>
)
case 'topMachines':
return (
<TopMachinesWrapper
title="Top 5 Machines"
representing={representing}
period={period}
data={R.map(convertFiatToLocale)(filteredData(period.code).current)}
machines={machineOptions}
selectedMachine={machine}
handleMachineChange={setMachine}
timezone={timezone}
currency={fiatLocale}
/>
)
case 'hourOfTheDay':
return (
<HourOfDayWrapper
title="Avg. transactions per hour of the day"
representing={representing}
period={period}
data={R.map(convertFiatToLocale)(filteredData(period.code).current)}
machines={machineOptions}
selectedMachine={machine}
handleMachineChange={setMachine}
selectedDay={selectedDay}
dayOptions={DAY_OPTIONS}
handleDayChange={setSelectedDay}
timezone={timezone}
currency={fiatLocale}
/>
)
default:
throw new Error(`There's no graph info to represent ${representing}`)
}
}
return (
!loading && (
<>
<TitleSection title="Analytics">
<div className="flex gap-6 justify-end">
<LegendEntry
IconComponent={UpIcon}
label={'Up since last period'}
/>
<LegendEntry
IconComponent={DownIcon}
label={'Down since last period'}
/>
<LegendEntry
IconComponent={EqualIcon}
label={'Same since last period'}
/>
</div>
</TitleSection>
<div className="flex justify-between items-center mb-4">
<div className="flex gap-6">
<Select
label="Representing"
onSelectedItemChange={handleRepresentationChange}
items={REPRESENTING_OPTIONS}
default={REPRESENTING_OPTIONS[0]}
selectedItem={representing}
defaultAsFilter
/>
<Select
label="Time period"
onSelectedItemChange={setPeriod}
items={PERIOD_OPTIONS}
default={PERIOD_OPTIONS[0]}
selectedItem={period}
defaultAsFilter
/>
</div>
<div className="flex items-center gap-10">
<OverviewEntry
label="Transactions"
value={txs.current}
oldValue={txs.previous}
/>
<VerticalLine />
<OverviewEntry
label="Median amount"
value={medianAmount.current}
oldValue={medianAmount.previous}
currency={fiatLocale}
/>
<VerticalLine />
<OverviewEntry
label="Volume"
value={txVolume.current}
oldValue={txVolume.previous}
currency={fiatLocale}
/>
<VerticalLine />
<OverviewEntry
label="Commissions"
value={commissions.current}
oldValue={commissions.previous}
currency={fiatLocale}
/>
</div>
</div>
{getGraphInfo(representing)}
</>
)
)
}
export default Analytics