feat: good now? :)
This commit is contained in:
parent
18eef39275
commit
a9afee477e
|
@ -1,25 +1,18 @@
|
|||
import { LoginIcon, PaperAirplaneIcon } from '@heroicons/react/outline'
|
||||
import {
|
||||
CheckIcon,
|
||||
DocumentDuplicateIcon,
|
||||
DownloadIcon,
|
||||
HomeIcon,
|
||||
KeyIcon,
|
||||
LibraryIcon,
|
||||
QrcodeIcon,
|
||||
RssIcon,
|
||||
UploadIcon,
|
||||
ShareIcon,
|
||||
} from '@heroicons/react/solid'
|
||||
import clsx from 'clsx'
|
||||
import { AES, enc } from 'crypto-js'
|
||||
import { useRouter } from 'next/router'
|
||||
import { FC, useState } from 'react'
|
||||
|
||||
import { checkBiometrics } from '../lib/biometrics'
|
||||
import computeWorkAsync from '../lib/computeWorkAsync'
|
||||
import { useAccount, useAccounts } from '../lib/context/accountContext'
|
||||
import { useAccount } from '../lib/context/accountContext'
|
||||
import { usePreferences } from '../lib/context/preferencesContext'
|
||||
import { getEncryptedSeed } from '../lib/db/encryptedSeeds'
|
||||
import decryptSeed from '../lib/decryptSeed'
|
||||
import useIsWelcoming from '../lib/hooks/useIsWelcoming'
|
||||
|
||||
export interface Props {
|
||||
|
@ -34,15 +27,6 @@ const BottomMenu: FC<Props> = ({ className }) => {
|
|||
const account = useAccount()
|
||||
|
||||
const [confirmCopyAddress, setConfirmCopyAddress] = useState(false)
|
||||
const [confirmCopySeed, setConfirmCopySeed] = useState(false)
|
||||
|
||||
const onCopySeed = async () => {
|
||||
const seed = await decryptSeed('os')
|
||||
navigator.clipboard.writeText(seed)
|
||||
setConfirmCopySeed(true)
|
||||
setTimeout(() => setConfirmCopySeed(false), 1500)
|
||||
}
|
||||
|
||||
const onCopyAddress = () => {
|
||||
if (account !== undefined) {
|
||||
navigator.clipboard.writeText(account.address)
|
||||
|
@ -51,6 +35,13 @@ const BottomMenu: FC<Props> = ({ className }) => {
|
|||
}
|
||||
}
|
||||
|
||||
const onShare = () => {
|
||||
navigator.share({
|
||||
title: 'This is my nano address!',
|
||||
text: account?.address,
|
||||
})
|
||||
}
|
||||
|
||||
const isWelcoming = useIsWelcoming()
|
||||
|
||||
return (
|
||||
|
@ -75,94 +66,66 @@ const BottomMenu: FC<Props> = ({ className }) => {
|
|||
<div>
|
||||
<div
|
||||
className={clsx(
|
||||
'flex gap-8 items-end',
|
||||
'flex gap-10 items-end',
|
||||
leftHanded ? 'flex-row-reverse' : 'flex-row'
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={clsx('flex gap-6 items-end', {
|
||||
'flex-row-reverse': leftHanded,
|
||||
})}
|
||||
>
|
||||
<div className="flex flex-col h-16 justify-between">
|
||||
<button
|
||||
disabled={isWelcoming || confirmCopySeed}
|
||||
className={clsx(
|
||||
'p-1 h-7 rounded shadow-lg',
|
||||
confirmCopySeed
|
||||
? 'bg-purple-50'
|
||||
: 'bg-purple-500 hover:bg-purple-400 disabled:hover:bg-purple-500 disabled:cursor-default'
|
||||
)}
|
||||
onClick={onCopySeed}
|
||||
>
|
||||
{confirmCopySeed ? (
|
||||
<CheckIcon className="h-full text-purple-500" />
|
||||
) : (
|
||||
<KeyIcon className="h-full text-purple-50 dark:text-gray-900" />
|
||||
)}
|
||||
</button>
|
||||
<button
|
||||
disabled={isWelcoming}
|
||||
className="bg-purple-500 p-1 h-7 rounded hover:bg-purple-400 disabled:hover:bg-purple-500 shadow-lg disabled:cursor-default"
|
||||
>
|
||||
<LibraryIcon className="h-full text-purple-50 dark:text-gray-900" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex flex-col h-16 justify-between">
|
||||
<button
|
||||
disabled={isWelcoming || confirmCopyAddress}
|
||||
className={clsx(
|
||||
'p-1 h-7 rounded shadow-lg',
|
||||
confirmCopyAddress
|
||||
? 'bg-purple-50'
|
||||
: 'bg-purple-500 hover:bg-purple-400 disabled:hover:bg-purple-500 disabled:cursor-default'
|
||||
)}
|
||||
onClick={onCopyAddress}
|
||||
>
|
||||
{confirmCopyAddress ? (
|
||||
<CheckIcon className="h-full text-purple-500" />
|
||||
) : (
|
||||
<DocumentDuplicateIcon className="h-full text-purple-50 dark:text-gray-900" />
|
||||
)}
|
||||
</button>
|
||||
<button
|
||||
disabled={isWelcoming}
|
||||
className="bg-purple-500 p-1 h-7 rounded hover:bg-purple-400 disabled:hover:bg-purple-500 shadow-lg disabled:cursor-default"
|
||||
>
|
||||
<DownloadIcon className="h-full text-purple-50 dark:text-gray-900" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{'share' in navigator ? (
|
||||
<button
|
||||
disabled={isWelcoming}
|
||||
className="p-1 h-16 w-7 rounded shadow-lg bg-purple-500 hover:bg-purple-400 disabled:hover:bg-purple-500 disabled:cursor-default"
|
||||
onClick={onShare}
|
||||
>
|
||||
<ShareIcon className="h-full text-purple-50 dark:text-gray-900" />
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
disabled={isWelcoming || confirmCopyAddress}
|
||||
className={clsx(
|
||||
'p-1 h-16 w-10 rounded shadow-lg',
|
||||
confirmCopyAddress
|
||||
? 'bg-purple-50'
|
||||
: 'bg-purple-500 hover:bg-purple-400 disabled:hover:bg-purple-500 disabled:cursor-default'
|
||||
)}
|
||||
onClick={onCopyAddress}
|
||||
>
|
||||
{confirmCopyAddress ? (
|
||||
<CheckIcon className="text-purple-500" />
|
||||
) : (
|
||||
<DocumentDuplicateIcon className="text-purple-50 dark:text-gray-900" />
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
|
||||
<div className="relative h-16">
|
||||
<button
|
||||
disabled={isWelcoming}
|
||||
className={clsx(
|
||||
'bg-purple-500 absolute top-0 h-16 w-7 rounded-r hover:bg-purple-400 disabled:hover:bg-purple-500 shadow-md disabled:cursor-default',
|
||||
'bg-purple-500 absolute top-0 px-1 h-16 w-10 rounded-r hover:bg-purple-400 disabled:hover:bg-purple-500 shadow-md disabled:cursor-default',
|
||||
leftHanded
|
||||
? 'left-0 -translate-x-2/3 rounded-l'
|
||||
: 'right-0 translate-x-2/3 rounded-r'
|
||||
)}
|
||||
onClick={() => push('/send/qr')}
|
||||
>
|
||||
<UploadIcon className="h-full text-purple-50 dark:text-gray-900 w-full" />
|
||||
<PaperAirplaneIcon className="h-full text-purple-50 dark:text-gray-900 w-full rotate-[30deg] translate-x-1" />
|
||||
</button>
|
||||
|
||||
<div className="border-purple-500 border-t-2 border-b-2 py-1 px-3 h-16 shadow-lg">
|
||||
<div className="border-purple-500 border-t-2 border-b-2 py-1 px-4 h-16 shadow-lg">
|
||||
<QrcodeIcon className="h-full text-gray-900 dark:text-purple-100" />
|
||||
</div>
|
||||
|
||||
<button
|
||||
disabled={isWelcoming}
|
||||
className={clsx(
|
||||
'bg-purple-500 absolute top-0 h-16 w-7 hover:bg-purple-400 disabled:hover:bg-purple-500 shadow-md disabled:cursor-default',
|
||||
'bg-purple-500 absolute top-0 px-1 h-16 w-10 hover:bg-purple-400 disabled:hover:bg-purple-500 shadow-md disabled:cursor-default',
|
||||
leftHanded
|
||||
? 'right-0 translate-x-2/3 rounded-r'
|
||||
: 'left-0 -translate-x-2/3 rounded-l'
|
||||
)}
|
||||
onClick={() => push('/receive/qr')}
|
||||
>
|
||||
<DownloadIcon className="h-full text-purple-50 dark:text-gray-900 w-full" />
|
||||
<LoginIcon className="h-full text-purple-50 dark:text-gray-900 w-full -rotate-90" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
@ -171,7 +134,7 @@ const BottomMenu: FC<Props> = ({ className }) => {
|
|||
disabled={isWelcoming}
|
||||
className="bg-purple-500 p-1 h-7 rounded hover:bg-purple-400 disabled:hover:bg-purple-500 shadow-md disabled:cursor-default"
|
||||
>
|
||||
<UploadIcon className="h-full text-purple-50 dark:text-gray-900" />
|
||||
<PaperAirplaneIcon className="h-full text-purple-50 dark:text-gray-900 rotate-[30deg] translate-x-[2px]" />
|
||||
</button>
|
||||
<button
|
||||
disabled={isWelcoming}
|
||||
|
|
|
@ -5,11 +5,9 @@ import { FC } from 'react'
|
|||
|
||||
import { usePreferences } from '../lib/context/preferencesContext'
|
||||
import useListenToColorMedia from '../lib/hooks/useListenToColorMedia'
|
||||
import useProtectedRoutes from '../lib/hooks/useProtectedRoutes'
|
||||
import useSetupDb from '../lib/hooks/useSetupDb'
|
||||
import Balance from './Balance'
|
||||
import BottomMenu from './BottomMenu'
|
||||
import PreferencesMenu from './PreferencesMenu'
|
||||
import TopMenu from './TopMenu'
|
||||
|
||||
export interface Props {}
|
||||
|
||||
|
@ -41,7 +39,7 @@ const Layout: FC<Props> = ({ children }) => {
|
|||
</h1>
|
||||
<LightningBoltIcon className="text-gray-900 dark:text-purple-100 h-4" />
|
||||
</div>
|
||||
<PreferencesMenu />
|
||||
<TopMenu />
|
||||
</header>
|
||||
{pathname !== '/' ? (
|
||||
<>
|
||||
|
|
|
@ -1,111 +0,0 @@
|
|||
import {
|
||||
CogIcon,
|
||||
FingerPrintIcon,
|
||||
HandIcon,
|
||||
MoonIcon,
|
||||
} from '@heroicons/react/solid'
|
||||
import clsx from 'clsx'
|
||||
import { FC, useRef, useState } from 'react'
|
||||
import colors from 'tailwindcss/colors'
|
||||
|
||||
import { usePreferences } from '../lib/context/preferencesContext'
|
||||
import useClickAway from '../lib/hooks/useClickAway'
|
||||
import useIsiOS from '../lib/hooks/useIsiOS'
|
||||
|
||||
export interface Props {}
|
||||
|
||||
const PreferencesMenu: FC<Props> = () => {
|
||||
const [showMenu, setShowMenu] = useState(false)
|
||||
const toggleMenu = () => setShowMenu(prev => !prev)
|
||||
|
||||
const menuRef = useRef<HTMLDivElement>(null)
|
||||
useClickAway(menuRef, () => setShowMenu(false))
|
||||
const {
|
||||
preferences: { darkMode, biometricsAuth, leftHanded },
|
||||
setPreference,
|
||||
} = usePreferences()
|
||||
|
||||
const isiOS = useIsiOS()
|
||||
|
||||
return (
|
||||
<div className="relative z-20 justify-center" ref={menuRef}>
|
||||
<button
|
||||
className={clsx(
|
||||
'w-10 p-1 rounded bg-purple-500 shadow-md text-purple-50 dark:text-gray-900 hover:cursor-pointer hover:bg-purple-400 transition-colors dark text-purple-50:dark:text-gray-900'
|
||||
)}
|
||||
onClick={toggleMenu}
|
||||
>
|
||||
<CogIcon className="w-full" />
|
||||
</button>
|
||||
<ul
|
||||
role="menu"
|
||||
className={clsx(
|
||||
'transition-all flex flex-col gap-2 absolute w-10 bg-purple-400 p-1 rounded mt-3',
|
||||
showMenu ? 'opacity-100' : 'opacity-0 -translate-y-2'
|
||||
)}
|
||||
style={{
|
||||
boxShadow: `${colors.coolGray[900]} 0px 2px 15px`,
|
||||
}}
|
||||
>
|
||||
{!isiOS && (
|
||||
<li role="menuitem">
|
||||
<button
|
||||
disabled={!showMenu}
|
||||
className={clsx(
|
||||
'p-1 rounded transition-colors duration-100 w-full',
|
||||
biometricsAuth
|
||||
? 'dark:text-purple-400 dark:bg-gray-900 dark:hover:text-purple-400 shadow-md hover:bg-purple-400 bg-purple-50 text-purple-400'
|
||||
: 'hover:text-purple-400 bg-purple-400 dark:text-gray-900 text-purple-50 dark:hover:text-purple-300',
|
||||
showMenu ? 'hover:cursor-pointer' : 'cursor-default'
|
||||
)}
|
||||
onClick={() => {
|
||||
setPreference('biometricsAuth', !biometricsAuth)
|
||||
setShowMenu(false)
|
||||
}}
|
||||
>
|
||||
<FingerPrintIcon className="h-full" />
|
||||
</button>
|
||||
</li>
|
||||
)}
|
||||
<li role="menuitem">
|
||||
<button
|
||||
disabled={!showMenu}
|
||||
className={clsx(
|
||||
'p-1 rounded transition-colors duration-100 w-full',
|
||||
darkMode
|
||||
? 'dark:text-purple-400 dark:bg-gray-900 dark:hover:text-purple-400 shadow-md hover:bg-purple-400 bg-purple-50 text-purple-400'
|
||||
: 'hover:text-purple-400 bg-purple-400 dark:text-gray-900 text-purple-50 dark:hover:text-purple-300',
|
||||
showMenu ? 'hover:cursor-pointer' : 'cursor-default'
|
||||
)}
|
||||
onClick={() => {
|
||||
setPreference('darkMode', !darkMode)
|
||||
setShowMenu(false)
|
||||
}}
|
||||
>
|
||||
<MoonIcon className="h-full" />
|
||||
</button>
|
||||
</li>
|
||||
<li role="menuitem">
|
||||
<button
|
||||
disabled={!showMenu}
|
||||
className={clsx(
|
||||
'p-1 rounded transition-colors duration-100 w-full',
|
||||
leftHanded
|
||||
? 'dark:text-purple-400 dark:bg-gray-900 dark:hover:text-purple-400 shadow-md hover:bg-purple-400 bg-purple-50 text-purple-400'
|
||||
: 'hover:text-purple-400 bg-purple-400 dark:text-gray-900 dark:hover:text-purple-300 text-purple-50',
|
||||
showMenu ? 'hover:cursor-pointer' : 'cursor-default'
|
||||
)}
|
||||
onClick={() => {
|
||||
setPreference('leftHanded', !leftHanded)
|
||||
setShowMenu(false)
|
||||
}}
|
||||
>
|
||||
<HandIcon className="h-full" />
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default PreferencesMenu
|
|
@ -1,16 +1,9 @@
|
|||
import { DownloadIcon, UploadIcon } from '@heroicons/react/solid'
|
||||
import clsx from 'clsx'
|
||||
import { tools } from 'nanocurrency-web'
|
||||
import { FC, useCallback, useMemo } from 'react'
|
||||
import { FC } from 'react'
|
||||
|
||||
import { useAccount } from '../lib/context/accountContext'
|
||||
import fetcher from '../lib/fetcher'
|
||||
import useReceiveNano from '../lib/hooks/useReceiveNano'
|
||||
import {
|
||||
AccountHistoryResponse,
|
||||
AccountPendingResponse,
|
||||
BlocksInfoResponse,
|
||||
} from '../lib/types'
|
||||
|
||||
const rawToNanoDisplay = (raw: string) =>
|
||||
Number(tools.convert(raw, 'RAW', 'NANO').slice(0, 20))
|
||||
|
|
185
components/TopMenu.tsx
Normal file
185
components/TopMenu.tsx
Normal file
|
@ -0,0 +1,185 @@
|
|||
import {
|
||||
CogIcon,
|
||||
FingerPrintIcon,
|
||||
HandIcon,
|
||||
KeyIcon,
|
||||
LibraryIcon,
|
||||
MoonIcon,
|
||||
PlusIcon,
|
||||
UsersIcon,
|
||||
} from '@heroicons/react/solid'
|
||||
import clsx from 'clsx'
|
||||
import { FC, useRef, useState } from 'react'
|
||||
import colors from 'tailwindcss/colors'
|
||||
|
||||
import { usePreferences } from '../lib/context/preferencesContext'
|
||||
import decryptSeed from '../lib/decryptSeed'
|
||||
import useClickAway from '../lib/hooks/useClickAway'
|
||||
import useIsWelcoming from '../lib/hooks/useIsWelcoming'
|
||||
import useIsiOS from '../lib/hooks/useIsiOS'
|
||||
|
||||
export interface Props {}
|
||||
|
||||
const TopMenu: FC<Props> = () => {
|
||||
const [showPreferences, setShowPreferences] = useState(false)
|
||||
const togglePreferences = () => setShowPreferences(prev => !prev)
|
||||
|
||||
const preferencesRef = useRef<HTMLDivElement>(null)
|
||||
useClickAway(preferencesRef, () => setShowPreferences(false))
|
||||
const {
|
||||
preferences: { darkMode, biometricsAuth, leftHanded },
|
||||
setPreference,
|
||||
} = usePreferences()
|
||||
|
||||
const [showAdvanced, setShowAdvanced] = useState(false)
|
||||
const toggleAdvanced = () => setShowAdvanced(prev => !prev)
|
||||
|
||||
const advancedRef = useRef<HTMLDivElement>(null)
|
||||
useClickAway(advancedRef, () => setShowAdvanced(false))
|
||||
|
||||
const isiOS = useIsiOS()
|
||||
const isWelcoming = useIsWelcoming()
|
||||
|
||||
const onCopySeed = async () => {
|
||||
const seed = await decryptSeed('os')
|
||||
navigator.clipboard.writeText(seed)
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={clsx('flex gap-3 justify-center', {
|
||||
'flex-row-reverse': leftHanded,
|
||||
})}
|
||||
>
|
||||
{!isWelcoming && (
|
||||
<div className="relative z-20" ref={advancedRef}>
|
||||
<button
|
||||
className={clsx(
|
||||
'w-10 p-1 rounded bg-purple-500 shadow-md text-purple-50 dark:text-gray-900 hover:cursor-pointer hover:bg-purple-400 transition-colors dark:hover:text-purple-50 dark:text-gray-900'
|
||||
)}
|
||||
onClick={toggleAdvanced}
|
||||
>
|
||||
<PlusIcon className="w-full" />
|
||||
</button>
|
||||
<ul
|
||||
role="menu"
|
||||
className={clsx(
|
||||
'transition-all flex flex-col gap-2 absolute w-10 bg-purple-400 p-1 rounded mt-3 hover:text-white',
|
||||
showAdvanced ? 'opacity-100' : 'opacity-0 -translate-y-2'
|
||||
)}
|
||||
style={{
|
||||
boxShadow: `${colors.coolGray[900]} 0px 2px 15px`,
|
||||
}}
|
||||
>
|
||||
<button
|
||||
className={clsx(
|
||||
'p-1 rounded transition-colors duration-100 w-full hover:text-purple-400 bg-purple-400 dark:text-gray-900 text-purple-50 dark:hover:text-purple-50'
|
||||
)}
|
||||
onClick={onCopySeed}
|
||||
>
|
||||
<KeyIcon className="h-full" />
|
||||
</button>
|
||||
<button
|
||||
className={clsx(
|
||||
'p-1 rounded transition-colors duration-100 w-full hover:text-purple-400 bg-purple-400 dark:text-gray-900 text-purple-50 dark:hover:text-purple-50'
|
||||
)}
|
||||
onClick={() => {
|
||||
// todo
|
||||
}}
|
||||
>
|
||||
<LibraryIcon className="h-full" />
|
||||
</button>
|
||||
<button
|
||||
className={clsx(
|
||||
'p-1 rounded transition-colors duration-100 w-full hover:text-purple-400 bg-purple-400 dark:text-gray-900 text-purple-50 dark:hover:text-purple-50'
|
||||
)}
|
||||
onClick={onCopySeed}
|
||||
>
|
||||
<UsersIcon className="h-full" />
|
||||
</button>
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="relative z-20" ref={preferencesRef}>
|
||||
<button
|
||||
className={clsx(
|
||||
'w-10 p-1 rounded bg-purple-500 shadow-md text-purple-50 hover:cursor-pointer hover:bg-purple-400 transition-colors dark:hover:text-purple-50 dark:text-gray-900'
|
||||
)}
|
||||
onClick={togglePreferences}
|
||||
>
|
||||
<CogIcon className="w-full" />
|
||||
</button>
|
||||
<ul
|
||||
role="menu"
|
||||
className={clsx(
|
||||
'transition-all flex flex-col gap-2 absolute w-10 bg-purple-400 p-1 rounded mt-3',
|
||||
showPreferences ? 'opacity-100' : 'opacity-0 -translate-y-2'
|
||||
)}
|
||||
style={{
|
||||
boxShadow: `${colors.coolGray[900]} 0px 2px 15px`,
|
||||
}}
|
||||
>
|
||||
{!isiOS && (
|
||||
<li role="menuitem">
|
||||
<button
|
||||
disabled={!showPreferences}
|
||||
className={clsx(
|
||||
'p-1 rounded transition-colors duration-100 w-full dark:hover:text-purple-50',
|
||||
biometricsAuth
|
||||
? 'dark:text-purple-400 dark:bg-gray-900 shadow-md hover:bg-purple-400 bg-purple-50 text-purple-400'
|
||||
: 'hover:text-purple-400 bg-purple-400 dark:text-gray-900 text-purple-50',
|
||||
showPreferences ? 'hover:cursor-pointer' : 'cursor-default'
|
||||
)}
|
||||
onClick={() => {
|
||||
setPreference('biometricsAuth', !biometricsAuth)
|
||||
setShowPreferences(false)
|
||||
}}
|
||||
>
|
||||
<FingerPrintIcon className="h-full" />
|
||||
</button>
|
||||
</li>
|
||||
)}
|
||||
<li role="menuitem">
|
||||
<button
|
||||
disabled={!showPreferences}
|
||||
className={clsx(
|
||||
'p-1 rounded transition-colors duration-100 w-full dark:hover:text-purple-50',
|
||||
darkMode
|
||||
? 'dark:text-purple-400 dark:bg-gray-900 shadow-md hover:bg-purple-400 bg-purple-50 text-purple-400'
|
||||
: 'hover:text-purple-400 bg-purple-400 dark:text-gray-900 text-purple-50',
|
||||
showPreferences ? 'hover:cursor-pointer' : 'cursor-default'
|
||||
)}
|
||||
onClick={() => {
|
||||
setPreference('darkMode', !darkMode)
|
||||
setShowPreferences(false)
|
||||
}}
|
||||
>
|
||||
<MoonIcon className="h-full" />
|
||||
</button>
|
||||
</li>
|
||||
<li role="menuitem">
|
||||
<button
|
||||
disabled={!showPreferences}
|
||||
className={clsx(
|
||||
'p-1 rounded transition-colors duration-100 w-full dark:hover:text-purple-50',
|
||||
leftHanded
|
||||
? 'dark:text-purple-400 dark:bg-gray-900 shadow-md hover:bg-purple-400 bg-purple-50 text-purple-400'
|
||||
: 'hover:text-purple-400 bg-purple-400 dark:text-gray-900 text-purple-50',
|
||||
showPreferences ? 'hover:cursor-pointer' : 'cursor-default'
|
||||
)}
|
||||
onClick={() => {
|
||||
setPreference('leftHanded', !leftHanded)
|
||||
setShowPreferences(false)
|
||||
}}
|
||||
>
|
||||
<HandIcon className="h-full" />
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default TopMenu
|
|
@ -15,7 +15,7 @@ export interface PreferenceContextValue {
|
|||
preferences: PreferenceTypes
|
||||
setPreference: <P extends PreferenceName>(
|
||||
preference: P,
|
||||
value: PreferenceTypes[P]
|
||||
value: Exclude<PreferenceTypes[P], undefined>
|
||||
) => void
|
||||
}
|
||||
|
||||
|
@ -53,12 +53,21 @@ export const PreferencesProvider: FC = ({ children }) => {
|
|||
}
|
||||
fetchPreferencesFromIdb()
|
||||
}, [])
|
||||
|
||||
const setDarkMode = useDarkMode()
|
||||
|
||||
const setPreference = useCallback(
|
||||
<P extends PreferenceName>(name: P, value: PreferenceTypes[P]) => {
|
||||
<P extends PreferenceName>(
|
||||
name: P,
|
||||
value: Exclude<PreferenceTypes[P], undefined>
|
||||
) => {
|
||||
setPreferences(prev => ({ ...prev, [name]: value }))
|
||||
putPreference(name, value)
|
||||
|
||||
// ? is there a better way to type this?
|
||||
if (name === 'darkMode') setDarkMode(value as boolean)
|
||||
},
|
||||
[]
|
||||
[setDarkMode]
|
||||
)
|
||||
|
||||
return (
|
||||
|
|
|
@ -1,5 +1,18 @@
|
|||
const fetcher = <T>(...args: Parameters<typeof fetch>) =>
|
||||
fetch(...args).then(res => {
|
||||
const fetcher = <T>(
|
||||
input: Parameters<typeof fetch>[0],
|
||||
init?: Omit<Exclude<Parameters<typeof fetch>[1], undefined>, 'body'> & {
|
||||
method: 'POST' | 'PUT' | 'PATCH'
|
||||
body?: any
|
||||
}
|
||||
) =>
|
||||
fetch(input, {
|
||||
...init,
|
||||
headers:
|
||||
(init?.method ?? 'GET') === 'GET'
|
||||
? []
|
||||
: [['Content-Type', 'application/json']],
|
||||
...(init?.body !== undefined ? { body: JSON.stringify(init?.body) } : {}),
|
||||
}).then(res => {
|
||||
if (!res.ok) throw new Error() // todo improve this error
|
||||
return res.json() as Promise<T>
|
||||
})
|
||||
|
|
|
@ -1,20 +1,25 @@
|
|||
import { useEffect } from 'react'
|
||||
import { useCallback, useEffect } from 'react'
|
||||
|
||||
import { getPreference } from '../db/preferences'
|
||||
|
||||
const useDarkMode = (darkMode?: boolean) => {
|
||||
const useDarkMode = () => {
|
||||
const setDarkMode = useCallback(async (darkMode: boolean) => {
|
||||
const htmlClasses = document.querySelector('html')?.classList
|
||||
if (darkMode) htmlClasses?.add('dark')
|
||||
else htmlClasses?.remove('dark')
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
const setDarkModeClass = async () => {
|
||||
const isDark =
|
||||
darkMode ??
|
||||
const darkModeOnStarup = async () => {
|
||||
const darkMode =
|
||||
(await getPreference('darkMode')) ??
|
||||
window.matchMedia('(prefers-color-scheme: dark)').matches
|
||||
const htmlClasses = document.querySelector('html')?.classList
|
||||
if (isDark) htmlClasses?.add('dark')
|
||||
else htmlClasses?.remove('dark')
|
||||
setDarkMode(darkMode)
|
||||
}
|
||||
setDarkModeClass()
|
||||
}, [darkMode])
|
||||
darkModeOnStarup()
|
||||
}, [setDarkMode])
|
||||
|
||||
return setDarkMode
|
||||
}
|
||||
|
||||
export default useDarkMode
|
||||
|
|
|
@ -1,8 +0,0 @@
|
|||
import { useState } from 'react'
|
||||
|
||||
const useImperativeRender = () => {
|
||||
const [, setValue] = useState(0)
|
||||
return () => setValue(value => value + 1)
|
||||
}
|
||||
|
||||
export default useImperativeRender
|
|
@ -1,16 +1,17 @@
|
|||
import { useEffect, useState } from 'react'
|
||||
import { useEffect } from 'react'
|
||||
|
||||
import { useAccounts } from '../context/accountContext'
|
||||
import { ConfirmationMessage } from '../types'
|
||||
|
||||
const useListenToTxn = () => {
|
||||
const useListenToTxn = (
|
||||
onConfirmation: (confirmation: ConfirmationMessage) => void
|
||||
) => {
|
||||
const { accounts } = useAccounts()
|
||||
const [mostRecentTxn, setMostRecentTxn] = useState<any | undefined>(undefined)
|
||||
useEffect(() => {
|
||||
if (accounts !== undefined) {
|
||||
const ws = new WebSocket('wss://ws.mynano.ninja/')
|
||||
|
||||
ws.onopen = () => {
|
||||
console.log('subscribed')
|
||||
ws.send(
|
||||
JSON.stringify({
|
||||
action: 'subscribe',
|
||||
|
@ -21,16 +22,14 @@ const useListenToTxn = () => {
|
|||
})
|
||||
)
|
||||
ws.addEventListener('message', ({ data }) => {
|
||||
const parsed = JSON.parse(data)
|
||||
console.log(parsed)
|
||||
setMostRecentTxn(parsed)
|
||||
const parsed = JSON.parse(data) as ConfirmationMessage
|
||||
onConfirmation(parsed)
|
||||
})
|
||||
}
|
||||
|
||||
return () => ws.close()
|
||||
}
|
||||
}, [accounts])
|
||||
return mostRecentTxn
|
||||
}, [accounts, onConfirmation])
|
||||
}
|
||||
|
||||
export default useListenToTxn
|
||||
|
|
|
@ -2,7 +2,7 @@ import { useCallback } from 'react'
|
|||
|
||||
import useSetup from './useSetup'
|
||||
|
||||
const useSetupSw = (skip?: boolean) =>
|
||||
const useSetupServiceWorker = (skip?: boolean) =>
|
||||
useSetup(
|
||||
useCallback(() => {
|
||||
if ('serviceWorker' in navigator) {
|
||||
|
@ -16,4 +16,4 @@ const useSetupSw = (skip?: boolean) =>
|
|||
skip
|
||||
)
|
||||
|
||||
export default useSetupSw
|
||||
export default useSetupServiceWorker
|
42
lib/types.ts
42
lib/types.ts
|
@ -1,3 +1,6 @@
|
|||
export type BooleanString = 'true' | 'false'
|
||||
export type TransactionSubtype = 'send' | 'receive' | 'change'
|
||||
|
||||
export interface AccountHistoryResponse {
|
||||
account: string
|
||||
history:
|
||||
|
@ -27,7 +30,7 @@ export interface BlocksInfoResponse {
|
|||
balance: string
|
||||
height: string
|
||||
local_timestamp: string
|
||||
confirmed: 'true' | 'false'
|
||||
confirmed: BooleanString
|
||||
contents: {
|
||||
type: 'state'
|
||||
account: string
|
||||
|
@ -39,7 +42,7 @@ export interface BlocksInfoResponse {
|
|||
signature: string
|
||||
work: string
|
||||
}
|
||||
subtype: 'send' | 'receive' | 'change'
|
||||
subtype: TransactionSubtype
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -55,13 +58,13 @@ export type AccountInfoResponse =
|
|||
account_version: string
|
||||
confirmation_height: string
|
||||
confirmation_height_frontier: string
|
||||
confirmed_balance: '11999999999999999918751838129509869131'
|
||||
confirmed_height: '22966'
|
||||
confirmed_frontier: '80A6745762493FA21A22718ABFA4F635656A707B48B3324198AC7F3938DE6D4F'
|
||||
representative: 'nano_1gyeqc6u5j3oaxbe5qy1hyz3q745a318kh8h9ocnpan7fuxnq85cxqboapu5'
|
||||
confirmed_representative: 'nano_1gyeqc6u5j3oaxbe5qy1hyz3q745a318kh8h9ocnpan7fuxnq85cxqboapu5'
|
||||
confirmed_balance: string
|
||||
confirmed_height: string
|
||||
confirmed_frontier: string
|
||||
representative: string
|
||||
confirmed_representative: string
|
||||
}
|
||||
| { error: 'Account not found' }
|
||||
| { error: string }
|
||||
|
||||
export interface AccountInfoCache {
|
||||
address: string
|
||||
|
@ -85,3 +88,26 @@ export type ProcessResponse =
|
|||
hash: string
|
||||
}
|
||||
| { error: string }
|
||||
|
||||
export interface ConfirmationMessage {
|
||||
topic: 'confirmation'
|
||||
time: string
|
||||
message: {
|
||||
account: string
|
||||
amount: string
|
||||
hash: string
|
||||
confirmation_type: string
|
||||
block: {
|
||||
type: 'state'
|
||||
account: string
|
||||
previous: string
|
||||
representative: string
|
||||
balance: string
|
||||
link: string
|
||||
link_as_account: string
|
||||
signature: string
|
||||
work: string
|
||||
subtype: TransactionSubtype
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
15
lib/xno/fetchAccountHistory.ts
Normal file
15
lib/xno/fetchAccountHistory.ts
Normal file
|
@ -0,0 +1,15 @@
|
|||
import fetcher from '../fetcher'
|
||||
import { AccountHistoryResponse } from '../types'
|
||||
|
||||
const fetchAccountHistory = (address: string, count = 20, head = undefined) =>
|
||||
fetcher('https://mynano.ninja/api/node', {
|
||||
method: 'POST',
|
||||
body: {
|
||||
action: 'account_history',
|
||||
account: address,
|
||||
head,
|
||||
count,
|
||||
},
|
||||
}) as Promise<AccountHistoryResponse>
|
||||
|
||||
export default fetchAccountHistory
|
|
@ -4,13 +4,12 @@ import { AccountInfoResponse } from '../types'
|
|||
const fetchAccountInfo = (address: string) =>
|
||||
fetcher('https://mynano.ninja/api/node', {
|
||||
method: 'POST',
|
||||
headers: [['Content-Type', 'application/json']],
|
||||
body: JSON.stringify({
|
||||
body: {
|
||||
action: 'account_info',
|
||||
account: address,
|
||||
representative: 'true',
|
||||
include_confirmed: 'true',
|
||||
}),
|
||||
},
|
||||
}) as Promise<AccountInfoResponse>
|
||||
|
||||
export default fetchAccountInfo
|
||||
|
|
27
lib/xno/fetchAccountReceivable.ts
Normal file
27
lib/xno/fetchAccountReceivable.ts
Normal file
|
@ -0,0 +1,27 @@
|
|||
import fetcher from '../fetcher'
|
||||
import { AccountHistoryResponse } from '../types'
|
||||
|
||||
const fetchAccountReceivable = (
|
||||
address: string,
|
||||
count = 20,
|
||||
head = undefined,
|
||||
version22 = false
|
||||
) =>
|
||||
fetcher('https://mynano.ninja/api/node', {
|
||||
method: 'POST',
|
||||
body: {
|
||||
action: version22 ? 'account_pending' : 'account_receivable',
|
||||
account: address,
|
||||
head,
|
||||
count,
|
||||
},
|
||||
}) as Promise<AccountHistoryResponse>
|
||||
|
||||
// most nodes haven't upgraded yet https://docs.nano.org/commands/rpc-protocol/#accounts_pending
|
||||
// this will be the future api for this function
|
||||
const withVersionFallback = (address: string, count = 20, head = undefined) =>
|
||||
fetchAccountReceivable(address, count, head).catch(() =>
|
||||
fetchAccountReceivable(address, count, head, true)
|
||||
)
|
||||
|
||||
export default withVersionFallback
|
|
@ -6,9 +6,11 @@ import Layout from '../components/Layout'
|
|||
import MemCacheProvider from '../lib/context/memCacheContextProvider'
|
||||
import useProtectedRoutes from '../lib/hooks/useProtectedRoutes'
|
||||
import useSetupDb from '../lib/hooks/useSetupDb'
|
||||
import useSetupServiceWorker from '../lib/hooks/useSetupServiceWorker'
|
||||
import '../styles/global.css'
|
||||
|
||||
const MyApp: FC<AppProps> = ({ Component, pageProps }) => {
|
||||
useSetupServiceWorker()
|
||||
const ready = useSetupDb(10)
|
||||
const validatingCredential = useProtectedRoutes(!ready)
|
||||
|
||||
|
|
Reference in a new issue