support nfts as profile pics
This commit is contained in:
parent
a3fc5b2a57
commit
2aeff1032e
|
@ -12,15 +12,17 @@ import {
|
||||||
CurrencyDollarIcon,
|
CurrencyDollarIcon,
|
||||||
DuplicateIcon,
|
DuplicateIcon,
|
||||||
LogoutIcon,
|
LogoutIcon,
|
||||||
|
UserCircleIcon,
|
||||||
} from '@heroicons/react/outline'
|
} from '@heroicons/react/outline'
|
||||||
import { notify } from 'utils/notifications'
|
import { notify } from 'utils/notifications'
|
||||||
import { abbreviateAddress, copyToClipboard } from 'utils'
|
import { abbreviateAddress, copyToClipboard } from 'utils'
|
||||||
import useMangoStore from 'stores/useMangoStore'
|
import useMangoStore from 'stores/useMangoStore'
|
||||||
import { ProfileIcon, WalletIcon } from './icons'
|
import { WalletIcon, ProfileIcon } from './icons'
|
||||||
import { useTranslation } from 'next-i18next'
|
import { useTranslation } from 'next-i18next'
|
||||||
import { WalletSelect } from 'components/WalletSelect'
|
import { WalletSelect } from 'components/WalletSelect'
|
||||||
import AccountsModal from './AccountsModal'
|
import AccountsModal from './AccountsModal'
|
||||||
import uniqBy from 'lodash/uniqBy'
|
import uniqBy from 'lodash/uniqBy'
|
||||||
|
import NftProfilePicModal from './NftProfilePicModal'
|
||||||
|
|
||||||
export const handleWalletConnect = (wallet: Wallet) => {
|
export const handleWalletConnect = (wallet: Wallet) => {
|
||||||
if (!wallet) {
|
if (!wallet) {
|
||||||
|
@ -43,11 +45,15 @@ export const handleWalletConnect = (wallet: Wallet) => {
|
||||||
|
|
||||||
export const ConnectWalletButton: React.FC = () => {
|
export const ConnectWalletButton: React.FC = () => {
|
||||||
const { connected, publicKey, wallet, wallets, select } = useWallet()
|
const { connected, publicKey, wallet, wallets, select } = useWallet()
|
||||||
const { t } = useTranslation('common')
|
const { t } = useTranslation(['common', 'profile'])
|
||||||
const pfp = useMangoStore((s) => s.wallet.pfp)
|
const pfp = useMangoStore((s) => s.wallet.pfp)
|
||||||
|
const loadingTransaction = useMangoStore(
|
||||||
|
(s) => s.wallet.nfts.loadingTransaction
|
||||||
|
)
|
||||||
const set = useMangoStore((s) => s.set)
|
const set = useMangoStore((s) => s.set)
|
||||||
const mangoGroup = useMangoStore((s) => s.selectedMangoGroup.current)
|
const mangoGroup = useMangoStore((s) => s.selectedMangoGroup.current)
|
||||||
const [showAccountsModal, setShowAccountsModal] = useState(false)
|
const [showAccountsModal, setShowAccountsModal] = useState(false)
|
||||||
|
const [showProfilePicModal, setShowProfilePicModal] = useState(false)
|
||||||
|
|
||||||
const installedWallets = useMemo(() => {
|
const installedWallets = useMemo(() => {
|
||||||
const installed: Wallet[] = []
|
const installed: Wallet[] = []
|
||||||
|
@ -77,6 +83,10 @@ export const ConnectWalletButton: React.FC = () => {
|
||||||
setShowAccountsModal(false)
|
setShowAccountsModal(false)
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
const handleCloseProfilePicModal = useCallback(() => {
|
||||||
|
setShowProfilePicModal(false)
|
||||||
|
}, [])
|
||||||
|
|
||||||
const handleDisconnect = useCallback(() => {
|
const handleDisconnect = useCallback(() => {
|
||||||
wallet?.adapter?.disconnect()
|
wallet?.adapter?.disconnect()
|
||||||
set((state) => {
|
set((state) => {
|
||||||
|
@ -107,11 +117,21 @@ export const ConnectWalletButton: React.FC = () => {
|
||||||
<Menu>
|
<Menu>
|
||||||
{({ open }) => (
|
{({ open }) => (
|
||||||
<div className="relative" id="profile-menu-tip">
|
<div className="relative" id="profile-menu-tip">
|
||||||
<Menu.Button className="flex h-10 w-10 items-center justify-center rounded-full bg-th-bkg-4 text-white hover:bg-th-bkg-4 hover:text-th-fgd-3 focus:outline-none">
|
<Menu.Button
|
||||||
|
className={`flex h-10 w-10 items-center justify-center rounded-full bg-th-bkg-button hover:bg-th-bkg-4 hover:bg-th-bkg-4 hover:text-th-fgd-3 focus:outline-none ${
|
||||||
|
loadingTransaction ? 'animate-pulse bg-th-bkg-4' : ''
|
||||||
|
}`}
|
||||||
|
>
|
||||||
{pfp?.isAvailable ? (
|
{pfp?.isAvailable ? (
|
||||||
<img alt="" src={pfp.url} className="rounded-full" />
|
<img
|
||||||
|
alt=""
|
||||||
|
src={pfp.url}
|
||||||
|
className={`default-transition h-10 w-10 rounded-full hover:opacity-60 ${
|
||||||
|
loadingTransaction ? 'opacity-40' : ''
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
) : (
|
) : (
|
||||||
<ProfileIcon className="h-6 w-6" />
|
<ProfileIcon className="h-6 w-6 text-th-fgd-3" />
|
||||||
)}
|
)}
|
||||||
</Menu.Button>
|
</Menu.Button>
|
||||||
<Transition
|
<Transition
|
||||||
|
@ -135,6 +155,19 @@ export const ConnectWalletButton: React.FC = () => {
|
||||||
<div className="pl-2 text-left">{t('accounts')}</div>
|
<div className="pl-2 text-left">{t('accounts')}</div>
|
||||||
</button>
|
</button>
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
|
<Menu.Item>
|
||||||
|
<button
|
||||||
|
className="flex w-full flex-row items-center rounded-none py-0.5 font-normal hover:cursor-pointer hover:text-th-primary focus:outline-none"
|
||||||
|
onClick={() => setShowProfilePicModal(true)}
|
||||||
|
>
|
||||||
|
<UserCircleIcon className="h-4 w-4" />
|
||||||
|
<div className="pl-2 text-left">
|
||||||
|
{pfp?.isAvailable
|
||||||
|
? t('profile:edit-profile-pic')
|
||||||
|
: t('profile:set-profile-pic')}
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
</Menu.Item>
|
||||||
<Menu.Item>
|
<Menu.Item>
|
||||||
<button
|
<button
|
||||||
className="flex w-full flex-row items-center rounded-none py-0.5 font-normal hover:cursor-pointer hover:text-th-primary focus:outline-none"
|
className="flex w-full flex-row items-center rounded-none py-0.5 font-normal hover:cursor-pointer hover:text-th-primary focus:outline-none"
|
||||||
|
@ -198,6 +231,12 @@ export const ConnectWalletButton: React.FC = () => {
|
||||||
isOpen={showAccountsModal}
|
isOpen={showAccountsModal}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{showProfilePicModal && (
|
||||||
|
<NftProfilePicModal
|
||||||
|
onClose={handleCloseProfilePicModal}
|
||||||
|
isOpen={showProfilePicModal}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,260 @@
|
||||||
|
import { useState, useEffect } from 'react'
|
||||||
|
import { PublicKey } from '@solana/web3.js'
|
||||||
|
import { notify } from 'utils/notifications'
|
||||||
|
import useMangoStore from '../stores/useMangoStore'
|
||||||
|
import { useWallet } from '@solana/wallet-adapter-react'
|
||||||
|
import { PhotographIcon } from '@heroicons/react/outline'
|
||||||
|
import Modal from './Modal'
|
||||||
|
import { ElementTitle } from './styles'
|
||||||
|
import {
|
||||||
|
createRemoveProfilePictureTransaction,
|
||||||
|
createSetProfilePictureTransaction,
|
||||||
|
} from '@solflare-wallet/pfp'
|
||||||
|
import Button from './Button'
|
||||||
|
import { useTranslation } from 'next-i18next'
|
||||||
|
import { connectionSelector } from '../stores/selectors'
|
||||||
|
import { LinkButton } from 'components'
|
||||||
|
|
||||||
|
interface SelectedNft {
|
||||||
|
mint: PublicKey
|
||||||
|
tokenAddress: PublicKey
|
||||||
|
}
|
||||||
|
|
||||||
|
const ImgWithLoader = (props) => {
|
||||||
|
const [isLoading, setIsLoading] = useState(true)
|
||||||
|
return (
|
||||||
|
<div className="relative">
|
||||||
|
{isLoading && (
|
||||||
|
<PhotographIcon className="absolute left-1/2 top-1/2 z-10 h-1/4 w-1/4 -translate-x-1/2 -translate-y-1/2 transform animate-pulse text-th-fgd-4" />
|
||||||
|
)}
|
||||||
|
<img {...props} onLoad={() => setIsLoading(false)} />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const NftProfilePicModal = ({ isOpen, onClose }) => {
|
||||||
|
const { t } = useTranslation(['common', 'profile'])
|
||||||
|
const connection = useMangoStore(connectionSelector)
|
||||||
|
const { publicKey, wallet } = useWallet()
|
||||||
|
const pfp = useMangoStore((s) => s.wallet.pfp)
|
||||||
|
const nfts = useMangoStore((s) => s.wallet.nfts.data)
|
||||||
|
const nftAccounts = useMangoStore((s) => s.wallet.nfts.accounts)
|
||||||
|
const initialLoad = useMangoStore((s) => s.wallet.nfts.initialLoad)
|
||||||
|
const nftsLoading = useMangoStore((s) => s.wallet.nfts.loading)
|
||||||
|
const [selectedProfile, setSelectedProfile] = useState<SelectedNft | null>(
|
||||||
|
null
|
||||||
|
)
|
||||||
|
const actions = useMangoStore((s) => s.actions)
|
||||||
|
const mangoClient = useMangoStore.getState().connection.client
|
||||||
|
const [offset, setOffset] = useState(0)
|
||||||
|
const setMangoStore = useMangoStore((s) => s.set)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (publicKey) {
|
||||||
|
actions.fetchNftAccounts(connection, publicKey)
|
||||||
|
}
|
||||||
|
}, [publicKey])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!initialLoad && publicKey && nftAccounts.length > 0) {
|
||||||
|
actions.fetchNfts(connection, publicKey)
|
||||||
|
}
|
||||||
|
}, [publicKey, initialLoad, nftAccounts])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (pfp?.isAvailable && pfp.mintAccount && pfp.tokenAccount) {
|
||||||
|
setSelectedProfile({
|
||||||
|
mint: pfp.mintAccount,
|
||||||
|
tokenAddress: pfp.tokenAccount,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}, [pfp])
|
||||||
|
|
||||||
|
const handleSaveProfilePic = async () => {
|
||||||
|
if (!publicKey || !selectedProfile || !wallet) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
setMangoStore((state) => {
|
||||||
|
state.wallet.nfts.loadingTransaction = true
|
||||||
|
})
|
||||||
|
onClose()
|
||||||
|
const transaction = await createSetProfilePictureTransaction(
|
||||||
|
publicKey,
|
||||||
|
selectedProfile.mint,
|
||||||
|
selectedProfile.tokenAddress
|
||||||
|
)
|
||||||
|
if (transaction) {
|
||||||
|
const txid = await mangoClient.sendTransaction(
|
||||||
|
transaction,
|
||||||
|
wallet.adapter,
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
if (txid) {
|
||||||
|
notify({
|
||||||
|
title: t('profile:profile-pic-success'),
|
||||||
|
description: '',
|
||||||
|
txid,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
notify({
|
||||||
|
title: t('profile:profile-pic-failure'),
|
||||||
|
description: t('transaction-failed'),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
notify({
|
||||||
|
title: t('profile:profile-pic-failure'),
|
||||||
|
description: t('transaction-failed'),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
notify({
|
||||||
|
title: t('profile:profile-pic-failure'),
|
||||||
|
description: e.message,
|
||||||
|
txid: e.txid,
|
||||||
|
type: 'error',
|
||||||
|
})
|
||||||
|
} finally {
|
||||||
|
actions.fetchProfilePicture(wallet)
|
||||||
|
setMangoStore((state) => {
|
||||||
|
state.wallet.nfts.loadingTransaction = false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleRemoveProfilePic = async () => {
|
||||||
|
if (!publicKey || !wallet) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
setMangoStore((state) => {
|
||||||
|
state.wallet.nfts.loadingTransaction = true
|
||||||
|
})
|
||||||
|
onClose()
|
||||||
|
const transaction = await createRemoveProfilePictureTransaction(publicKey)
|
||||||
|
if (transaction) {
|
||||||
|
const txid = await mangoClient.sendTransaction(
|
||||||
|
transaction,
|
||||||
|
wallet.adapter,
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
if (txid) {
|
||||||
|
notify({
|
||||||
|
title: t('profile:profile-pic-remove-success'),
|
||||||
|
description: '',
|
||||||
|
txid,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
notify({
|
||||||
|
title: t('profile:profile-pic-remove-failure'),
|
||||||
|
description: t('transaction-failed'),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
notify({
|
||||||
|
title: t('profile:profile-pic-remove-failure'),
|
||||||
|
description: t('transaction-failed'),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
notify({
|
||||||
|
title: t('profile:profile-pic-remove-failure'),
|
||||||
|
description: e.message,
|
||||||
|
txid: e.txid,
|
||||||
|
type: 'error',
|
||||||
|
})
|
||||||
|
} finally {
|
||||||
|
actions.fetchProfilePicture(wallet)
|
||||||
|
setMangoStore((state) => {
|
||||||
|
state.wallet.nfts.loadingTransaction = false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleLoadMore = async () => {
|
||||||
|
const offsetNfts = offset + 9
|
||||||
|
await actions.fetchNfts(connection, publicKey, offsetNfts)
|
||||||
|
setOffset(offsetNfts)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal isOpen={isOpen} onClose={onClose}>
|
||||||
|
<Modal.Header>
|
||||||
|
<div className="my-3 flex w-full items-center justify-between">
|
||||||
|
<ElementTitle noMarginBottom>
|
||||||
|
{t('profile:choose-profile')}
|
||||||
|
</ElementTitle>
|
||||||
|
<div className="flex items-center space-x-4">
|
||||||
|
<Button
|
||||||
|
disabled={!selectedProfile}
|
||||||
|
onClick={() => handleSaveProfilePic()}
|
||||||
|
>
|
||||||
|
{t('save')}
|
||||||
|
</Button>
|
||||||
|
{pfp?.isAvailable ? (
|
||||||
|
<LinkButton
|
||||||
|
className="text-xs"
|
||||||
|
onClick={() => handleRemoveProfilePic()}
|
||||||
|
>
|
||||||
|
{t('profile:remove')}
|
||||||
|
</LinkButton>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Modal.Header>
|
||||||
|
{nftAccounts.length > 0 ? (
|
||||||
|
<div className="flex flex-col items-center">
|
||||||
|
<div className="mb-4 grid w-full grid-flow-row grid-cols-3 gap-4">
|
||||||
|
{nfts.map((n) => {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
className={`default-transitions col-span-1 flex items-center justify-center rounded-md border bg-th-bkg-3 py-4 hover:bg-th-bkg-4 ${
|
||||||
|
selectedProfile?.tokenAddress.toString() === n.tokenAddress
|
||||||
|
? 'border-th-primary'
|
||||||
|
: 'border-th-bkg-3'
|
||||||
|
}`}
|
||||||
|
key={n.tokenAddress}
|
||||||
|
onClick={() =>
|
||||||
|
setSelectedProfile({
|
||||||
|
mint: new PublicKey(n.mint),
|
||||||
|
tokenAddress: new PublicKey(n.tokenAddress),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<ImgWithLoader
|
||||||
|
className="h-20 w-20 flex-shrink-0 rounded-full"
|
||||||
|
src={n.val.image}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
{nftsLoading
|
||||||
|
? [
|
||||||
|
...Array(
|
||||||
|
nftAccounts.length - nfts.length > 9
|
||||||
|
? 9
|
||||||
|
: nftAccounts.length - nfts.length
|
||||||
|
),
|
||||||
|
].map((i) => (
|
||||||
|
<div
|
||||||
|
className="col-span-1 h-28 animate-pulse rounded-md bg-th-bkg-3"
|
||||||
|
key={i}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
: null}
|
||||||
|
</div>
|
||||||
|
{nftAccounts.length !== nfts.length ? (
|
||||||
|
<LinkButton onClick={() => handleLoadMore()}>
|
||||||
|
{t('show-more')}
|
||||||
|
</LinkButton>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</Modal>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default NftProfilePicModal
|
|
@ -21,6 +21,7 @@
|
||||||
"@headlessui/react": "^0.0.0-insiders.2dbc38c",
|
"@headlessui/react": "^0.0.0-insiders.2dbc38c",
|
||||||
"@heroicons/react": "^1.0.0",
|
"@heroicons/react": "^1.0.0",
|
||||||
"@jup-ag/react-hook": "^1.0.0-beta.22",
|
"@jup-ag/react-hook": "^1.0.0-beta.22",
|
||||||
|
"@nfteyez/sol-rayz": "^0.10.2",
|
||||||
"@notifi-network/notifi-react-hooks": "^0.12.1",
|
"@notifi-network/notifi-react-hooks": "^0.12.1",
|
||||||
"@project-serum/serum": "0.13.55",
|
"@project-serum/serum": "0.13.55",
|
||||||
"@project-serum/sol-wallet-adapter": "0.2.0",
|
"@project-serum/sol-wallet-adapter": "0.2.0",
|
||||||
|
|
|
@ -54,6 +54,8 @@ import { Menu, Transition } from '@headlessui/react'
|
||||||
import { useWallet } from '@solana/wallet-adapter-react'
|
import { useWallet } from '@solana/wallet-adapter-react'
|
||||||
import { handleWalletConnect } from 'components/ConnectWalletButton'
|
import { handleWalletConnect } from 'components/ConnectWalletButton'
|
||||||
import { MangoAccountLookup } from 'components/account_page/MangoAccountLookup'
|
import { MangoAccountLookup } from 'components/account_page/MangoAccountLookup'
|
||||||
|
import NftProfilePicModal from 'components/NftProfilePicModal'
|
||||||
|
import { ProfileIcon } from 'components'
|
||||||
|
|
||||||
export async function getStaticProps({ locale }) {
|
export async function getStaticProps({ locale }) {
|
||||||
return {
|
return {
|
||||||
|
@ -64,6 +66,7 @@ export async function getStaticProps({ locale }) {
|
||||||
'delegate',
|
'delegate',
|
||||||
'alerts',
|
'alerts',
|
||||||
'share-modal',
|
'share-modal',
|
||||||
|
'profile',
|
||||||
])),
|
])),
|
||||||
// Will be passed to the page component as props
|
// Will be passed to the page component as props
|
||||||
},
|
},
|
||||||
|
@ -92,6 +95,11 @@ export default function Account() {
|
||||||
const [mngoAccrued, setMngoAccrued] = useState(ZERO_BN)
|
const [mngoAccrued, setMngoAccrued] = useState(ZERO_BN)
|
||||||
const [viewIndex, setViewIndex] = useState(0)
|
const [viewIndex, setViewIndex] = useState(0)
|
||||||
const [activeTab, setActiveTab] = useState(TABS[0])
|
const [activeTab, setActiveTab] = useState(TABS[0])
|
||||||
|
const [showProfilePicModal, setShowProfilePicModal] = useState(false)
|
||||||
|
const pfp = useMangoStore((s) => s.wallet.pfp)
|
||||||
|
const loadingTransaction = useMangoStore(
|
||||||
|
(s) => s.wallet.nfts.loadingTransaction
|
||||||
|
)
|
||||||
|
|
||||||
const connecting = wallet?.adapter?.connecting
|
const connecting = wallet?.adapter?.connecting
|
||||||
const isMobile = width ? width < breakpoints.sm : false
|
const isMobile = width ? width < breakpoints.sm : false
|
||||||
|
@ -120,6 +128,10 @@ export default function Account() {
|
||||||
setShowDelegateModal(false)
|
setShowDelegateModal(false)
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
const handleCloseProfilePicModal = useCallback(() => {
|
||||||
|
setShowProfilePicModal(false)
|
||||||
|
}, [])
|
||||||
|
|
||||||
const handleConnect = useCallback(() => {
|
const handleConnect = useCallback(() => {
|
||||||
if (wallet) {
|
if (wallet) {
|
||||||
handleWalletConnect(wallet)
|
handleWalletConnect(wallet)
|
||||||
|
@ -265,41 +277,67 @@ export default function Account() {
|
||||||
<div className="flex flex-col pt-4 pb-6 md:flex-row md:items-end md:justify-between md:pb-4 md:pt-10">
|
<div className="flex flex-col pt-4 pb-6 md:flex-row md:items-end md:justify-between md:pb-4 md:pt-10">
|
||||||
{mangoAccount ? (
|
{mangoAccount ? (
|
||||||
<>
|
<>
|
||||||
<div className="pb-3 md:pb-0">
|
<div className="flex items-center pb-3 md:pb-0">
|
||||||
<div className="mb-1 flex items-center">
|
<button
|
||||||
<h1 className={`mr-3`}>
|
disabled={!!pubkey}
|
||||||
{mangoAccount?.name || t('account')}
|
className={`relative mr-4 flex h-20 w-20 items-center justify-center rounded-full ${
|
||||||
</h1>
|
loadingTransaction
|
||||||
{!pubkey ? (
|
? 'animate-pulse bg-th-bkg-4'
|
||||||
<IconButton
|
: 'bg-th-bkg-button'
|
||||||
className="h-7 w-7"
|
}`}
|
||||||
onClick={() => setShowNameModal(true)}
|
onClick={() => setShowProfilePicModal(true)}
|
||||||
|
>
|
||||||
|
{pfp?.isAvailable ? (
|
||||||
|
<img
|
||||||
|
alt=""
|
||||||
|
src={pfp.url}
|
||||||
|
className={`default-transition h-20 w-20 rounded-full hover:opacity-60 ${
|
||||||
|
loadingTransaction ? 'opacity-40' : ''
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<ProfileIcon className="h-12 w-12 text-th-fgd-3" />
|
||||||
|
)}
|
||||||
|
<div className="default-transition absolute bottom-0 top-0 left-0 right-0 flex h-full w-full items-center justify-center rounded-full bg-[rgba(0,0,0,0.6)] opacity-0 hover:opacity-100">
|
||||||
|
<PencilIcon className="h-5 w-5 text-th-fgd-1" />
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
<div>
|
||||||
|
<div className="mb-1 flex items-center">
|
||||||
|
<h1 className={`mr-3`}>
|
||||||
|
{mangoAccount?.name || t('account')}
|
||||||
|
</h1>
|
||||||
|
{!pubkey ? (
|
||||||
|
<IconButton
|
||||||
|
className="h-7 w-7"
|
||||||
|
onClick={() => setShowNameModal(true)}
|
||||||
|
>
|
||||||
|
<PencilIcon className="h-3.5 w-3.5" />
|
||||||
|
</IconButton>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
<div className="flex h-4 items-center">
|
||||||
|
<LinkButton
|
||||||
|
className="flex items-center text-th-fgd-4 no-underline"
|
||||||
|
onClick={() =>
|
||||||
|
handleCopyAddress(mangoAccount.publicKey.toString())
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<PencilIcon className="h-3.5 w-3.5" />
|
<span className="text-xxs font-normal sm:text-xs">
|
||||||
</IconButton>
|
{mangoAccount.publicKey.toBase58()}
|
||||||
) : null}
|
</span>
|
||||||
</div>
|
<DuplicateIcon className="ml-1.5 h-4 w-4" />
|
||||||
<div className="flex h-4 items-center">
|
</LinkButton>
|
||||||
<LinkButton
|
{isCopied ? (
|
||||||
className="flex items-center text-th-fgd-4 no-underline"
|
<span className="ml-2 rounded bg-th-bkg-3 px-1.5 py-0.5 text-xs">
|
||||||
onClick={() =>
|
Copied
|
||||||
handleCopyAddress(mangoAccount.publicKey.toString())
|
</span>
|
||||||
}
|
) : null}
|
||||||
>
|
</div>
|
||||||
<span className="text-xxs font-normal sm:text-xs">
|
<div className="flex items-center text-xxs text-th-fgd-4">
|
||||||
{mangoAccount.publicKey.toBase58()}
|
<ExclamationCircleIcon className="mr-1.5 h-4 w-4" />
|
||||||
</span>
|
{t('account-address-warning')}
|
||||||
<DuplicateIcon className="ml-1.5 h-4 w-4" />
|
</div>
|
||||||
</LinkButton>
|
|
||||||
{isCopied ? (
|
|
||||||
<span className="ml-2 rounded bg-th-bkg-3 px-1.5 py-0.5 text-xs">
|
|
||||||
Copied
|
|
||||||
</span>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center text-xxs text-th-fgd-4">
|
|
||||||
<ExclamationCircleIcon className="mr-1.5 h-4 w-4" />
|
|
||||||
{t('account-address-warning')}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{!pubkey ? (
|
{!pubkey ? (
|
||||||
|
@ -502,6 +540,12 @@ export default function Account() {
|
||||||
onClose={handleCloseDelegateModal}
|
onClose={handleCloseDelegateModal}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
|
{showProfilePicModal ? (
|
||||||
|
<NftProfilePicModal
|
||||||
|
isOpen={showProfilePicModal}
|
||||||
|
onClose={handleCloseProfilePicModal}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,7 +11,7 @@ import { useTranslation } from 'next-i18next'
|
||||||
export async function getStaticProps({ locale }) {
|
export async function getStaticProps({ locale }) {
|
||||||
return {
|
return {
|
||||||
props: {
|
props: {
|
||||||
...(await serverSideTranslations(locale, ['common'])),
|
...(await serverSideTranslations(locale, ['common', 'profile'])),
|
||||||
// Will be passed to the page component as props
|
// Will be passed to the page component as props
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,7 +22,7 @@ import { useWallet } from '@solana/wallet-adapter-react'
|
||||||
export async function getStaticProps({ locale }) {
|
export async function getStaticProps({ locale }) {
|
||||||
return {
|
return {
|
||||||
props: {
|
props: {
|
||||||
...(await serverSideTranslations(locale, ['common'])),
|
...(await serverSideTranslations(locale, ['common', 'profile'])),
|
||||||
// Will be passed to the page component as props
|
// Will be passed to the page component as props
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
@ -31,6 +31,7 @@ export async function getStaticProps({ locale }) {
|
||||||
'tv-chart',
|
'tv-chart',
|
||||||
'alerts',
|
'alerts',
|
||||||
'share-modal',
|
'share-modal',
|
||||||
|
'profile',
|
||||||
])),
|
])),
|
||||||
// Will be passed to the page component as props
|
// Will be passed to the page component as props
|
||||||
},
|
},
|
||||||
|
|
|
@ -11,7 +11,7 @@ const TABS = ['perp', 'spot']
|
||||||
export async function getStaticProps({ locale }) {
|
export async function getStaticProps({ locale }) {
|
||||||
return {
|
return {
|
||||||
props: {
|
props: {
|
||||||
...(await serverSideTranslations(locale, ['common'])),
|
...(await serverSideTranslations(locale, ['common', 'profile'])),
|
||||||
// Will be passed to the page component as props
|
// Will be passed to the page component as props
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
@ -52,7 +52,11 @@ import { useWallet } from '@solana/wallet-adapter-react'
|
||||||
export async function getStaticProps({ locale }) {
|
export async function getStaticProps({ locale }) {
|
||||||
return {
|
return {
|
||||||
props: {
|
props: {
|
||||||
...(await serverSideTranslations(locale, ['common', 'referrals'])),
|
...(await serverSideTranslations(locale, [
|
||||||
|
'common',
|
||||||
|
'referrals',
|
||||||
|
'profile',
|
||||||
|
])),
|
||||||
// Will be passed to the page component as props
|
// Will be passed to the page component as props
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
@ -34,7 +34,11 @@ import { useWallet } from '@solana/wallet-adapter-react'
|
||||||
export async function getStaticProps({ locale }) {
|
export async function getStaticProps({ locale }) {
|
||||||
return {
|
return {
|
||||||
props: {
|
props: {
|
||||||
...(await serverSideTranslations(locale, ['common', 'calculator'])),
|
...(await serverSideTranslations(locale, [
|
||||||
|
'common',
|
||||||
|
'calculator',
|
||||||
|
'profile',
|
||||||
|
])),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,7 +11,11 @@ import TopBar from '../components/TopBar'
|
||||||
export async function getStaticProps({ locale }) {
|
export async function getStaticProps({ locale }) {
|
||||||
return {
|
return {
|
||||||
props: {
|
props: {
|
||||||
...(await serverSideTranslations(locale, ['common', 'tv-chart'])),
|
...(await serverSideTranslations(locale, [
|
||||||
|
'common',
|
||||||
|
'tv-chart',
|
||||||
|
'profile',
|
||||||
|
])),
|
||||||
// Will be passed to the page component as props
|
// Will be passed to the page component as props
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,7 +16,7 @@ import { useTranslation } from 'next-i18next'
|
||||||
export async function getStaticProps({ locale }) {
|
export async function getStaticProps({ locale }) {
|
||||||
return {
|
return {
|
||||||
props: {
|
props: {
|
||||||
...(await serverSideTranslations(locale, ['common'])),
|
...(await serverSideTranslations(locale, ['common', 'profile'])),
|
||||||
// Will be passed to the page component as props
|
// Will be passed to the page component as props
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,7 +13,7 @@ import { useWallet } from '@solana/wallet-adapter-react'
|
||||||
export async function getStaticProps({ locale }) {
|
export async function getStaticProps({ locale }) {
|
||||||
return {
|
return {
|
||||||
props: {
|
props: {
|
||||||
...(await serverSideTranslations(locale, ['common', 'swap'])),
|
...(await serverSideTranslations(locale, ['common', 'swap', 'profile'])),
|
||||||
// Will be passed to the page component as props
|
// Will be passed to the page component as props
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
@ -83,7 +83,7 @@
|
||||||
"connect-wallet-tip-desc": "We'll show you around...",
|
"connect-wallet-tip-desc": "We'll show you around...",
|
||||||
"connect-wallet-tip-title": "Connect your wallet",
|
"connect-wallet-tip-title": "Connect your wallet",
|
||||||
"connected-to": "Connected to wallet ",
|
"connected-to": "Connected to wallet ",
|
||||||
"copy-address": "Copy address",
|
"copy-address": "Copy Address",
|
||||||
"country-not-allowed": "Country Not Allowed",
|
"country-not-allowed": "Country Not Allowed",
|
||||||
"country-not-allowed-tooltip": "You are using an open-source frontend facilitated by the Mango DAO. As such, it restricts access to certain regions out of an abundance of caution, due to regulatory uncertainty.",
|
"country-not-allowed-tooltip": "You are using an open-source frontend facilitated by the Mango DAO. As such, it restricts access to certain regions out of an abundance of caution, due to regulatory uncertainty.",
|
||||||
"create-account": "Create Account",
|
"create-account": "Create Account",
|
||||||
|
@ -356,7 +356,7 @@
|
||||||
"short": "short",
|
"short": "short",
|
||||||
"show-all": "Show all in Nav",
|
"show-all": "Show all in Nav",
|
||||||
"show-less": "Show less",
|
"show-less": "Show less",
|
||||||
"show-more": "Show more",
|
"show-more": "Show More",
|
||||||
"show-tips": "Show Tips",
|
"show-tips": "Show Tips",
|
||||||
"show-zero": "Show zero balances",
|
"show-zero": "Show zero balances",
|
||||||
"side": "Side",
|
"side": "Side",
|
||||||
|
|
|
@ -0,0 +1,10 @@
|
||||||
|
{
|
||||||
|
"choose-profile": "Choose a Profile Pic",
|
||||||
|
"edit-profile-pic": "Edit Profile Pic",
|
||||||
|
"profile-pic-failure": "Failed to set profile pic",
|
||||||
|
"profile-pic-success": "Successfully set profile pic",
|
||||||
|
"profile-pic-remove-failure": "Failed to remove profile pic",
|
||||||
|
"profile-pic-remove-success": "Successfully removed profile pic",
|
||||||
|
"remove": "Remove",
|
||||||
|
"set-profile-pic": "Set Profile Pic"
|
||||||
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
{
|
||||||
|
"choose-profile": "Choose a Profile Pic",
|
||||||
|
"edit-profile-pic": "Edit Profile Pic",
|
||||||
|
"profile-pic-failure": "Failed to set profile pic",
|
||||||
|
"profile-pic-success": "Successfully set profile pic",
|
||||||
|
"profile-pic-remove-failure": "Failed to remove profile pic",
|
||||||
|
"profile-pic-remove-success": "Successfully removed profile pic",
|
||||||
|
"remove": "Remove",
|
||||||
|
"set-profile-pic": "Set Profile Pic"
|
||||||
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
{
|
||||||
|
"choose-profile": "Choose a Profile Pic",
|
||||||
|
"edit-profile-pic": "Edit Profile Pic",
|
||||||
|
"profile-pic-failure": "Failed to set profile pic",
|
||||||
|
"profile-pic-success": "Successfully set profile pic",
|
||||||
|
"profile-pic-remove-failure": "Failed to remove profile pic",
|
||||||
|
"profile-pic-remove-success": "Successfully removed profile pic",
|
||||||
|
"remove": "Remove",
|
||||||
|
"set-profile-pic": "Set Profile Pic"
|
||||||
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
{
|
||||||
|
"choose-profile": "Choose a Profile Pic",
|
||||||
|
"edit-profile-pic": "Edit Profile Pic",
|
||||||
|
"profile-pic-failure": "Failed to set profile pic",
|
||||||
|
"profile-pic-success": "Successfully set profile pic",
|
||||||
|
"profile-pic-remove-failure": "Failed to remove profile pic",
|
||||||
|
"profile-pic-remove-success": "Successfully removed profile pic",
|
||||||
|
"remove": "Remove",
|
||||||
|
"set-profile-pic": "Set Profile Pic"
|
||||||
|
}
|
|
@ -40,6 +40,8 @@ import { getProfilePicture, ProfilePicture } from '@solflare-wallet/pfp'
|
||||||
import { decodeBook } from '../hooks/useHydrateStore'
|
import { decodeBook } from '../hooks/useHydrateStore'
|
||||||
import { IOrderLineAdapter } from '../public/charting_library/charting_library'
|
import { IOrderLineAdapter } from '../public/charting_library/charting_library'
|
||||||
import { Wallet } from '@solana/wallet-adapter-react'
|
import { Wallet } from '@solana/wallet-adapter-react'
|
||||||
|
import { getParsedNftAccountsByOwner } from '@nfteyez/sol-rayz'
|
||||||
|
import { getTokenAccountsByMint } from 'utils/tokens'
|
||||||
|
|
||||||
export const ENDPOINTS: EndpointInfo[] = [
|
export const ENDPOINTS: EndpointInfo[] = [
|
||||||
{
|
{
|
||||||
|
@ -130,6 +132,32 @@ export interface AlertRequest {
|
||||||
notifiAlertId: string | undefined
|
notifiAlertId: string | undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface NFTFiles {
|
||||||
|
type: string
|
||||||
|
uri: string
|
||||||
|
}
|
||||||
|
interface NFTProperties {
|
||||||
|
category: string
|
||||||
|
files: NFTFiles[]
|
||||||
|
}
|
||||||
|
|
||||||
|
interface NFTData {
|
||||||
|
image: string
|
||||||
|
name: string
|
||||||
|
description: string
|
||||||
|
properties: NFTProperties
|
||||||
|
collection: {
|
||||||
|
family: string
|
||||||
|
name: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface NFTWithMint {
|
||||||
|
val: NFTData
|
||||||
|
mint: string
|
||||||
|
tokenAddress: string
|
||||||
|
}
|
||||||
|
|
||||||
export type MangoStore = {
|
export type MangoStore = {
|
||||||
notificationIdCounter: number
|
notificationIdCounter: number
|
||||||
notifications: Array<Notification>
|
notifications: Array<Notification>
|
||||||
|
@ -197,6 +225,13 @@ export type MangoStore = {
|
||||||
wallet: {
|
wallet: {
|
||||||
tokens: WalletToken[] | any[]
|
tokens: WalletToken[] | any[]
|
||||||
pfp: ProfilePicture | undefined
|
pfp: ProfilePicture | undefined
|
||||||
|
nfts: {
|
||||||
|
data: NFTWithMint[] | []
|
||||||
|
initialLoad: boolean
|
||||||
|
loading: boolean
|
||||||
|
accounts: any[]
|
||||||
|
loadingTransaction: boolean
|
||||||
|
}
|
||||||
}
|
}
|
||||||
settings: {
|
settings: {
|
||||||
uiLocked: boolean
|
uiLocked: boolean
|
||||||
|
@ -211,6 +246,12 @@ export type MangoStore = {
|
||||||
actions: {
|
actions: {
|
||||||
fetchWalletTokens: (wallet: Wallet) => void
|
fetchWalletTokens: (wallet: Wallet) => void
|
||||||
fetchProfilePicture: (wallet: Wallet) => void
|
fetchProfilePicture: (wallet: Wallet) => void
|
||||||
|
fetchNfts: (
|
||||||
|
connection: Connection,
|
||||||
|
walletPk: PublicKey | null,
|
||||||
|
offset?: number
|
||||||
|
) => void
|
||||||
|
fetchNftAccounts: (connection: Connection, walletPk: PublicKey) => void
|
||||||
fetchAllMangoAccounts: (wallet: Wallet) => Promise<void>
|
fetchAllMangoAccounts: (wallet: Wallet) => Promise<void>
|
||||||
fetchMangoGroup: () => Promise<void>
|
fetchMangoGroup: () => Promise<void>
|
||||||
fetchTradeHistory: () => void
|
fetchTradeHistory: () => void
|
||||||
|
@ -341,6 +382,13 @@ const useMangoStore = create<
|
||||||
wallet: {
|
wallet: {
|
||||||
tokens: [],
|
tokens: [],
|
||||||
pfp: undefined,
|
pfp: undefined,
|
||||||
|
nfts: {
|
||||||
|
data: [],
|
||||||
|
initialLoad: false,
|
||||||
|
loading: false,
|
||||||
|
accounts: [],
|
||||||
|
loadingTransaction: false,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
settings: {
|
settings: {
|
||||||
uiLocked: true,
|
uiLocked: true,
|
||||||
|
@ -423,6 +471,82 @@ const useMangoStore = create<
|
||||||
console.log('Could not get profile picture', e)
|
console.log('Could not get profile picture', e)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
async fetchNftAccounts(connection: Connection, walletPk: PublicKey) {
|
||||||
|
const set = get().set
|
||||||
|
try {
|
||||||
|
const nfts = await getParsedNftAccountsByOwner({
|
||||||
|
publicAddress: walletPk.toBase58(),
|
||||||
|
connection: connection,
|
||||||
|
})
|
||||||
|
const data = Object.keys(nfts)
|
||||||
|
.map((key) => nfts[key])
|
||||||
|
.filter((data) => data.primarySaleHappened)
|
||||||
|
set((state) => {
|
||||||
|
state.wallet.nfts.accounts = data
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async fetchNfts(
|
||||||
|
connection: Connection,
|
||||||
|
walletPk: PublicKey,
|
||||||
|
offset = 0
|
||||||
|
) {
|
||||||
|
const set = get().set
|
||||||
|
set((state) => {
|
||||||
|
state.wallet.nfts.loading = true
|
||||||
|
})
|
||||||
|
try {
|
||||||
|
const nftAccounts = get().wallet.nfts.accounts
|
||||||
|
const loadedNfts = get().wallet.nfts.data
|
||||||
|
const arr: NFTWithMint[] = []
|
||||||
|
if (loadedNfts.length < nftAccounts.length) {
|
||||||
|
for (let i = offset; i < offset + 9; i++) {
|
||||||
|
try {
|
||||||
|
const response = await fetch(nftAccounts[i].data.uri)
|
||||||
|
const val = await response.json()
|
||||||
|
const tokenAccounts = await getTokenAccountsByMint(
|
||||||
|
connection,
|
||||||
|
nftAccounts[i].mint
|
||||||
|
)
|
||||||
|
arr.push({
|
||||||
|
val,
|
||||||
|
mint: nftAccounts[i].mint,
|
||||||
|
tokenAddress: tokenAccounts
|
||||||
|
.find(
|
||||||
|
(x) =>
|
||||||
|
x.account.owner.toBase58() === walletPk.toBase58()
|
||||||
|
)!
|
||||||
|
.publicKey.toBase58(),
|
||||||
|
})
|
||||||
|
} catch (e) {
|
||||||
|
console.log(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (loadedNfts.length === 0) {
|
||||||
|
set((state) => {
|
||||||
|
state.wallet.nfts.data = arr
|
||||||
|
state.wallet.nfts.initialLoad = true
|
||||||
|
state.wallet.nfts.loading = false
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
set((state) => {
|
||||||
|
state.wallet.nfts.data = [...loadedNfts, ...arr]
|
||||||
|
state.wallet.nfts.loading = false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
notify({
|
||||||
|
title: 'Unable to fetch nfts',
|
||||||
|
description: '',
|
||||||
|
})
|
||||||
|
set((state) => {
|
||||||
|
state.wallet.nfts.loading = false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
async fetchAllMangoAccounts(wallet) {
|
async fetchAllMangoAccounts(wallet) {
|
||||||
const set = get().set
|
const set = get().set
|
||||||
const mangoGroup = get().selectedMangoGroup.current
|
const mangoGroup = get().selectedMangoGroup.current
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { TokenAccountLayout } from '@blockworks-foundation/mango-client'
|
import { TokenAccountLayout } from '@blockworks-foundation/mango-client'
|
||||||
import { PublicKey } from '@solana/web3.js'
|
import { PublicKey, Connection } from '@solana/web3.js'
|
||||||
|
|
||||||
export const TOKEN_PROGRAM_ID = new PublicKey(
|
export const TOKEN_PROGRAM_ID = new PublicKey(
|
||||||
'TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA'
|
'TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA'
|
||||||
|
@ -22,3 +22,28 @@ export function parseTokenAccountData(data: Buffer): {
|
||||||
amount,
|
amount,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getTokenAccountsByMint(
|
||||||
|
connection: Connection,
|
||||||
|
mint: string
|
||||||
|
): Promise<ProgramAccount<any>[]> {
|
||||||
|
const results = await connection.getProgramAccounts(TOKEN_PROGRAM_ID, {
|
||||||
|
filters: [
|
||||||
|
{
|
||||||
|
dataSize: 165,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
memcmp: {
|
||||||
|
offset: 0,
|
||||||
|
bytes: mint,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
return results.map((r) => {
|
||||||
|
const publicKey = r.pubkey
|
||||||
|
const data = Buffer.from(r.account.data)
|
||||||
|
const account = parseTokenAccountData(data)
|
||||||
|
return { publicKey, account }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue