feat: lots of stuff
This commit is contained in:
parent
aeeb56d605
commit
ec32e26cc0
30
components/AddressInput.tsx
Normal file
30
components/AddressInput.tsx
Normal file
|
@ -0,0 +1,30 @@
|
|||
import { FC } from 'react'
|
||||
|
||||
// delete if not needed
|
||||
export interface Props {
|
||||
value: string
|
||||
onChange: (value: string) => void
|
||||
}
|
||||
|
||||
const AddressInput: FC<Props> = ({ value, onChange }) => {
|
||||
return (
|
||||
<div className="flex items-center w-full gap-3 text-2xl rounded transition-colors dark:bg-gray-800 bg-purple-50 focus-within:bg-purple-100 border-2 border-purple-200 dark:border-gray-700 py-2 px-4 overflow-hidden dark:focus-within:bg-gray-700">
|
||||
<label htmlFor="xno-address">@</label>
|
||||
<input
|
||||
name="xno-address"
|
||||
id="xno-address"
|
||||
maxLength={65}
|
||||
minLength={65}
|
||||
className="bg-transparent focus:outline-none w-full"
|
||||
value={value}
|
||||
pattern="^(nano|xrb)_[13]{1}[13456789abcdefghijkmnopqrstuwxyz]{59}$"
|
||||
onChange={({ target: { value, validity } }) => {
|
||||
console.log(value, validity.patternMismatch)
|
||||
if (!validity.patternMismatch) onChange(value)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default AddressInput
|
|
@ -41,22 +41,20 @@ const Balance: FC<Props> = ({ className }) => {
|
|||
const xnoBalanceDisplay = rawToNanoDisplay(account?.balance ?? '0')
|
||||
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
'dark:text-purple-50 transition-colors text-gray-900 flex',
|
||||
className
|
||||
)}
|
||||
>
|
||||
<div className={clsx('dark:text-purple-50 text-gray-900 flex', className)}>
|
||||
<div
|
||||
onClick={() =>
|
||||
setPreference('showCurrencyDash', nextShowCurrency(showCurrencyDash))
|
||||
}
|
||||
className="hover:cursor-pointer active:translate-y-0.5"
|
||||
className="hover:cursor-pointer"
|
||||
>
|
||||
<h3 className="text-4xl">
|
||||
Ӿ
|
||||
<span className="transition-colors">Ӿ</span>
|
||||
<span
|
||||
className={clsx(showXnoBalance ? 'font-medium' : 'font-semibold')}
|
||||
className={clsx(
|
||||
'transition-colors',
|
||||
showXnoBalance ? 'font-medium' : 'font-semibold'
|
||||
)}
|
||||
>
|
||||
{showXnoBalance ? (
|
||||
account?.balance === null ? (
|
||||
|
@ -76,7 +74,7 @@ const Balance: FC<Props> = ({ className }) => {
|
|||
</span>
|
||||
</h3>
|
||||
{showFiatBalance && (
|
||||
<h4 className="text-xl">
|
||||
<h4 className="text-xl transition-colors">
|
||||
$ {(Number(xnoBalance) * xnoPrice).toFixed(2)}
|
||||
</h4>
|
||||
)}
|
||||
|
|
|
@ -117,7 +117,7 @@ const BottomMenu: FC<Props> = ({ className }) => {
|
|||
<div className="h-16 p-1 border-t-2 border-b-2 border-purple-400 shadow-lg">
|
||||
<QrcodeIcon className="h-full text-gray-900 dark:text-purple-100 transition-colors" />
|
||||
</div>
|
||||
<Link href={isWelcoming ? '#' : '/send/qr'}>
|
||||
<Link href={isWelcoming ? '#' : '/send/qrOrAddress'}>
|
||||
<a
|
||||
className={clsx(
|
||||
'bg-purple-400 transition-colors px-1 h-16 w-10 hover:bg-purple-400 disabled:hover:bg-purple-400 shadow-md disabled:cursor-default',
|
||||
|
|
|
@ -1,11 +1,13 @@
|
|||
import { ChevronUpIcon, ClockIcon } from '@heroicons/react/outline'
|
||||
import { DownloadIcon, UploadIcon } from '@heroicons/react/solid'
|
||||
import clsx from 'clsx'
|
||||
import { FC, useState } from 'react'
|
||||
import { Unit, convert } from 'nanocurrency'
|
||||
import { FC, useEffect, useState } from 'react'
|
||||
|
||||
import useAccountHistory from '../lib/hooks/useAccountHistory'
|
||||
import useAccountReceivable from '../lib/hooks/useAccountReceivable'
|
||||
import useReceiveNano from '../lib/hooks/useReceiveNano'
|
||||
import showNotification from '../lib/showNotification'
|
||||
import rawToNanoDisplay from '../lib/xno/rawToNanoDisplay'
|
||||
|
||||
export interface Props {}
|
||||
|
@ -20,6 +22,7 @@ const RecentTransactions: FC<Props> = () => {
|
|||
hasMore,
|
||||
loading: loadingHistory,
|
||||
hasHistory,
|
||||
revalidate: refetchHistory,
|
||||
} = useAccountHistory()
|
||||
|
||||
const hasReceivable =
|
||||
|
@ -33,11 +36,34 @@ const RecentTransactions: FC<Props> = () => {
|
|||
|
||||
const initialLoading = loadingHistory && accountHistory === undefined
|
||||
|
||||
const onIncomingClick = async (receivable: {
|
||||
hash: string
|
||||
amount: string
|
||||
from: string
|
||||
timestamp: string
|
||||
}) => {
|
||||
await receive(receivable.hash, receivable.amount)
|
||||
onBlockReceived(receivable.hash)
|
||||
refetchHistory()
|
||||
|
||||
showNotification({
|
||||
title: 'received!',
|
||||
body: `received Ӿ${convert(receivable.amount, {
|
||||
from: Unit.raw,
|
||||
to: Unit.Nano,
|
||||
})} from ${receivable.from}`,
|
||||
tag: 'received',
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{hasReceivable && (
|
||||
<section className="flex flex-col w-full gap-3">
|
||||
<div className="flex items-center justify-between gap-1">
|
||||
<div
|
||||
className="flex items-center justify-between gap-1"
|
||||
onClick={() => setReceivablesExpanded(prev => !prev)}
|
||||
>
|
||||
<h2 className="flex-1 text-2xl font-semibold transition-colors text-gray-900 dark:text-purple-50">
|
||||
incoming
|
||||
</h2>
|
||||
|
@ -46,7 +72,6 @@ const RecentTransactions: FC<Props> = () => {
|
|||
{receivableBlocks.length}
|
||||
</span>
|
||||
<ChevronUpIcon
|
||||
onClick={() => setReceivablesExpanded(prev => !prev)}
|
||||
className={clsx(
|
||||
'h-8 transition-transform-child origin-center-child',
|
||||
{
|
||||
|
@ -68,10 +93,7 @@ const RecentTransactions: FC<Props> = () => {
|
|||
>
|
||||
<button
|
||||
className="contents"
|
||||
onClick={async () => {
|
||||
await receive(receivable.hash, receivable.amount)
|
||||
onBlockReceived(receivable.hash)
|
||||
}}
|
||||
onClick={() => onIncomingClick(receivable)}
|
||||
>
|
||||
<ClockIcon className="flex-shrink-0 w-6 text-yellow-400" />
|
||||
|
||||
|
@ -165,7 +187,7 @@ const RecentTransactions: FC<Props> = () => {
|
|||
)}
|
||||
</ol>
|
||||
) : (
|
||||
<div className="pt-8 text-center text-gray-900 dark:text-purple-50">
|
||||
<div className="pt-8 text-center text-xl text-gray-900 dark:text-purple-50">
|
||||
<p className="pb-4">no transactions yet...</p>
|
||||
<p>
|
||||
get your first nano
|
||||
|
|
|
@ -33,7 +33,7 @@ const XnoInput: FC<Props> = ({ value, onChange }) => {
|
|||
|
||||
return (
|
||||
<div className="flex items-center gap-3 text-2xl rounded transition-colors dark:bg-gray-800 bg-purple-50 focus-within:bg-purple-100 border-2 border-purple-200 dark:border-gray-700 py-2 px-4 w-48 overflow-hidden dark:focus-within:bg-gray-700">
|
||||
<label htmlFor="xnoToSend">Ӿ</label>
|
||||
<label htmlFor="xno-amount">Ӿ</label>
|
||||
<input
|
||||
name="xno-amount"
|
||||
id="xno-amount"
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import { useCallback, useEffect } from 'react'
|
||||
import useSWRInfinite from 'swr/infinite'
|
||||
|
||||
import { useCurrentAccount } from '../context/accountContext'
|
||||
|
@ -23,6 +22,7 @@ const useAccountHistory = (pageSize = 20) => {
|
|||
size,
|
||||
setSize,
|
||||
isValidating,
|
||||
mutate,
|
||||
} = useSWRInfinite<AccountHistoryResponse>(getKey, (key: string) =>
|
||||
fetchAccountHistory(
|
||||
account!.address,
|
||||
|
@ -44,6 +44,7 @@ const useAccountHistory = (pageSize = 20) => {
|
|||
hasMore,
|
||||
loading: isValidating,
|
||||
hasHistory,
|
||||
revalidate: mutate,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
import { Unit, convert } from 'nanocurrency'
|
||||
import { useCallback } from 'react'
|
||||
import useSWR from 'swr'
|
||||
import useSWR, { mutate } from 'swr'
|
||||
|
||||
import { useCurrentAccount } from '../context/accountContext'
|
||||
import showNotification from '../showNotification'
|
||||
import type { ConfirmationMessage } from '../types'
|
||||
import fetchAccountReceivable from '../xno/fetchAccountReceivable'
|
||||
import fetchBlocksInfo from '../xno/fetchBlocksInfo'
|
||||
|
@ -43,11 +45,12 @@ const useAccountReceivable = () => {
|
|||
const onBlockReceived = useCallback(
|
||||
(hash: string) => {
|
||||
mutateReceivable(current => {
|
||||
if (current === undefined || account === undefined) return current
|
||||
if (current === undefined) return current
|
||||
return current.filter(block => block.hash !== hash)
|
||||
})
|
||||
mutate('history')
|
||||
},
|
||||
[mutateReceivable, account]
|
||||
[mutateReceivable]
|
||||
)
|
||||
|
||||
const addReceivable = useCallback(
|
||||
|
@ -61,7 +64,7 @@ const useAccountReceivable = () => {
|
|||
source: string
|
||||
}) => {
|
||||
mutateReceivable(async current => {
|
||||
if (current === undefined || account === undefined) return current
|
||||
if (current === undefined) return current
|
||||
const blocksInfo = await fetchBlocksInfo([hash])
|
||||
return [
|
||||
...current,
|
||||
|
@ -74,16 +77,28 @@ const useAccountReceivable = () => {
|
|||
]
|
||||
})
|
||||
},
|
||||
[mutateReceivable, account]
|
||||
[mutateReceivable]
|
||||
)
|
||||
|
||||
const onNewReceivable = useCallback(
|
||||
(confirmation: ConfirmationMessage) =>
|
||||
(confirmation: ConfirmationMessage) => {
|
||||
addReceivable({
|
||||
hash: confirmation.message.hash,
|
||||
amount: confirmation.message.amount,
|
||||
source: confirmation.message.account,
|
||||
}),
|
||||
})
|
||||
showNotification({
|
||||
title: 'incoming!',
|
||||
body: `new incoming transaction: Ӿ${convert(
|
||||
confirmation.message.amount,
|
||||
{
|
||||
from: Unit.raw,
|
||||
to: Unit.Nano,
|
||||
}
|
||||
)} from ${confirmation.message.account}`,
|
||||
tag: 'received',
|
||||
})
|
||||
},
|
||||
[addReceivable]
|
||||
)
|
||||
|
||||
|
|
15
lib/showNotification.ts
Normal file
15
lib/showNotification.ts
Normal file
|
@ -0,0 +1,15 @@
|
|||
const showNotification = async (params: {
|
||||
title: string
|
||||
body: string
|
||||
tag?: string
|
||||
}) => {
|
||||
const sw = await navigator.serviceWorker.getRegistration()
|
||||
sw?.showNotification(params.title, {
|
||||
body: params.body,
|
||||
renotify: params.tag !== undefined,
|
||||
tag: params.tag,
|
||||
icon: '/images/icon-small.png',
|
||||
})
|
||||
}
|
||||
|
||||
export default showNotification
|
|
@ -8,6 +8,7 @@ import { useCallback, useEffect, useState } from 'react'
|
|||
|
||||
import XnoInput from '../../components/XnoInput'
|
||||
import useSendNano from '../../lib/hooks/useSendNano'
|
||||
import showNotification from '../../lib/showNotification'
|
||||
|
||||
const Send: NextPage = () => {
|
||||
const { query, push } = useRouter()
|
||||
|
@ -34,15 +35,17 @@ const Send: NextPage = () => {
|
|||
useEffect(() => {
|
||||
if (sliderPercentage === 1) {
|
||||
const sendNano = async () => {
|
||||
try {
|
||||
await send(address as string, amount as string)
|
||||
navigator.serviceWorker
|
||||
.getRegistration()
|
||||
.then(sw => sw?.showNotification('sent!'))
|
||||
push('/dashboard')
|
||||
} catch {
|
||||
backToBase()
|
||||
}
|
||||
backToBase()
|
||||
await send(address as string, amount as string)
|
||||
showNotification({
|
||||
title: 'sent!',
|
||||
body: `sent Ӿ${convert(amount as string, {
|
||||
from: Unit.raw,
|
||||
to: Unit.Nano,
|
||||
})} to ${address}`,
|
||||
tag: 'send',
|
||||
})
|
||||
push('/dashboard')
|
||||
}
|
||||
sendNano()
|
||||
}
|
||||
|
@ -59,7 +62,7 @@ const Send: NextPage = () => {
|
|||
return (
|
||||
<div className="flex flex-col h-full gap-8 pb-4">
|
||||
<span className="flex items-center gap-2">
|
||||
<PaperAirplaneIcon className=" dark:text-purple-50 h-7 xs:h-10 text-gray-900 rotate-[30deg] translate-x-1" />
|
||||
<PaperAirplaneIcon className="transition-colors dark:text-purple-50 h-7 xs:h-10 text-gray-900 rotate-[30deg] translate-x-1" />
|
||||
<h1 className="text-3xl sm:text-5xl">send</h1>
|
||||
</span>
|
||||
<form
|
||||
|
|
|
@ -2,8 +2,10 @@ import { PaperAirplaneIcon } from '@heroicons/react/solid'
|
|||
import clsx from 'clsx'
|
||||
import type { NextPage } from 'next'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useCallback } from 'react'
|
||||
import { useCallback, useState } from 'react'
|
||||
|
||||
import AddressInput from '../../components/AddressInput'
|
||||
import XnoInput from '../../components/XnoInput'
|
||||
import useReadQrFromVideo from '../../lib/hooks/useReadQrFromVideo'
|
||||
import isTxnUrl from '../../lib/xno/isTxnUrl'
|
||||
import txnUrlToParts from '../../lib/xno/txnUrlToParts'
|
||||
|
@ -21,19 +23,34 @@ const ReadQrCode: NextPage = () => {
|
|||
)
|
||||
const { videoLive, videoRef } = useReadQrFromVideo(onQrCodeRead)
|
||||
|
||||
const [address, setAddress] = useState('')
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full min-h-0 gap-8">
|
||||
<div className="flex flex-col h-full min-h-0 gap-8 pb-4">
|
||||
<span className="flex items-center gap-2">
|
||||
<PaperAirplaneIcon className=" dark:text-purple-50 h-7 xs:h-10 text-gray-900 rotate-[30deg] translate-x-1" />
|
||||
<PaperAirplaneIcon className=" dark:text-purple-50 h-7 xs:h-10 text-gray-900 rotate-[30deg] transition-colors translate-x-1" />
|
||||
<h1 className="text-3xl sm:text-5xl">send</h1>
|
||||
</span>
|
||||
<video
|
||||
className={clsx('rounded shadow-md min-h-0', { hidden: !videoLive })}
|
||||
className={clsx('rounded flex-1 shadow-md min-h-0', {
|
||||
hidden: !videoLive,
|
||||
})}
|
||||
ref={videoRef}
|
||||
/>
|
||||
{!videoLive && (
|
||||
<div className="w-full h-64 rounded dark:bg-gray-800 bg-purple-50 animate-pulse"></div>
|
||||
<div className="w-full flex-1 h-64 rounded dark:bg-gray-800 bg-purple-50 animate-pulse"></div>
|
||||
)}
|
||||
<div className="flex flex-col justify-self-end items-center gap-2 text-gray-900 dark:text-purple-50">
|
||||
<span className="text-2xl">or</span>
|
||||
<form
|
||||
onSubmit={e => {
|
||||
e.preventDefault()
|
||||
if (address !== '') push(`/send?address=${address}`)
|
||||
}}
|
||||
>
|
||||
<AddressInput value={address} onChange={setAddress} />
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
|
@ -5,5 +5,6 @@
|
|||
"75edd150e09bada0f5c68fab14c0b385239cc396ef6600029dea0f7b66be63fa6a7d8ebe30aaff66634f583c4e68067c0de136e27dddc96be76d2ac5bb14ef71",
|
||||
"b6203f57196d244f7eb6971cef41a4cd4ce961e81ae8046f4f8274a4057cb77b4ef97ad3c25e14b50598c1243fa3253644b44e89c8648fd9c18b3703d36372bb",
|
||||
"penalty parrot sure holiday target what human sadness crack picnic idle pluck mean salad frog usual grab aspect pottery faith staff crash umbrella lumber",
|
||||
"6bfddd0bfd00bf13eb17bae35627a2dfdf49cb56027fc26988faff0ab8a20b31efb16d706ceac8e458d4d5c342f2459d24746521a6e6a62a0adb29b323455ed9"
|
||||
"6bfddd0bfd00bf13eb17bae35627a2dfdf49cb56027fc26988faff0ab8a20b31efb16d706ceac8e458d4d5c342f2459d24746521a6e6a62a0adb29b323455ed9",
|
||||
"9bbaa2391a256c8d5c318790b86a142d4b13b1dc6d209567cb854e1b956f1c1385b863229a34e970d1f4c0c4a66f7ac7a17b5fd3a21e72716dddec842e0a6727"
|
||||
]
|
||||
|
|
Reference in a new issue