Skip to content

Commit

Permalink
Added NFT list page
Browse files Browse the repository at this point in the history
Added NFT and FT details
  • Loading branch information
Ivan-Mahda committed Nov 6, 2024
1 parent 3b68429 commit 7b179be
Show file tree
Hide file tree
Showing 19 changed files with 457 additions and 23 deletions.
3 changes: 3 additions & 0 deletions packages/browser-wallet/src/assets/svgX/code.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
18 changes: 18 additions & 0 deletions packages/browser-wallet/src/popup/popupX/constants/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,15 @@ export const relativeRoutes = {
},
token: {
path: 'token',
ccd: {
path: 'ccd',
},
details: {
path: ':contractIndex',
raw: {
path: 'raw',
},
},
},
submittedTransaction: {
path: 'submitted/:transactionHash',
Expand Down Expand Up @@ -157,6 +166,15 @@ export const relativeRoutes = {
},
},
},
nft: {
path: 'nft',
details: {
path: ':contractIndex/:id/details',
raw: {
path: 'raw',
},
},
},
/** Routes related to staking for the currently selected account */
earn: {
path: 'earn',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import LinkSimple from '@assets/svgX/link-simple-horizontal.svg';
import Info from '@assets/svgX/info2.svg';
import Restore from '@assets/svgX/arrow-counter-clock.svg';
import Eye from '@assets/svgX/eye.svg';
import NFT from '@assets/svgX/cube-focus.svg';
import IconButton from '@popup/shared/IconButton';
import Text from '@popup/popupX/shared/Text';
import { Link } from 'react-router-dom';
Expand Down Expand Up @@ -101,6 +102,12 @@ export default function MenuTiles({ menuOpen, setMenuOpen }: MenuTilesProps) {
<Text.Capture>{t('oldUI')}</Text.Capture>
</IconButton>
</Link>
<Link to={absoluteRoutes.settings.nft.path}>
<IconButton className="main-header__menu-tiles_tile">
<NFT />
<Text.Capture>{t('nft')}</Text.Capture>
</IconButton>
</Link>
</div>
</div>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ const t = {
about: 'About',
restore: 'Restore',
oldUI: 'Old UI',
nft: 'NFT',
},
accountSelector: {
sortAsc: 'Sort A-Z',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { useTranslation } from 'react-i18next';
import { useAtomValue } from 'jotai';
import { displayAsCcd } from 'wallet-common-helpers';
import { AccountInfoType, Ratio } from '@concordium/web-sdk';
import { relativeRoutes } from '@popup/popupX/constants/routes';
import { absoluteRoutes, relativeRoutes } from '@popup/popupX/constants/routes';
import Img from '@popup/shared/Img';
import { WalletCredential } from '@shared/storage/types';
import { useAccountInfo } from '@popup/shared/AccountInfoListenerContext';
Expand Down Expand Up @@ -122,7 +122,9 @@ function MainPage({ credential }: MainPageProps) {
const navToReceive = () => nav(relativeRoutes.home.receive.path);
const navToTransactionLog = () =>
nav(relativeRoutes.home.transactionLog.path.replace(':account', credential.address));
const navToTokenDetails = () => nav(relativeRoutes.home.token.path);
const navToTokenDetails = (contractIndex: string) => {
return nav(absoluteRoutes.home.token.details.path.replace(':contractIndex', contractIndex));
};
const navToManageToken = () => nav(relativeRoutes.home.manageTokenList.path);

const chainParameters = useBlockChainParameters();
Expand Down Expand Up @@ -151,7 +153,7 @@ function MainPage({ credential }: MainPageProps) {
<div className="main-page-x__tokens">
<div className="main-page-x__tokens-list">
<TokenItem
onClick={() => nav(`${relativeRoutes.home.token.path}/ccd`)}
onClick={() => nav(`${absoluteRoutes.home.token.ccd.path}`)}
thumbnail={<ConcordiumLogo />}
symbol="CCD"
staked={isStaked}
Expand All @@ -161,7 +163,7 @@ function MainPage({ credential }: MainPageProps) {
/>
{tokens.map((token) => (
<TokenItem
onClick={() => navToTokenDetails()}
onClick={() => navToTokenDetails(token.contractIndex)}
key={`${token.contractIndex}.${token.id}`}
thumbnail={token.metadata.thumbnail?.url || ''}
symbol={token.metadata.symbol || ''}
Expand Down
87 changes: 87 additions & 0 deletions packages/browser-wallet/src/popup/popupX/pages/Nft/Nft.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
.nft-x {
.owner {
display: flex;
justify-content: space-between;

.label__main {
color: $color-white;
}
}

&__list {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
gap: rem(7px);
margin-top: rem(8px);

&_item {
display: flex;
flex-direction: column;
width: rem(160px);
height: rem(192px);
padding: rem(8px) rem(8px) rem(16px) rem(8px);
border: unset;
border-radius: rem(12px);
background-color: $color-transaction-bg;

&-img {
width: 100%;
border-radius: rem(8px);
}

.text__main_regular {
width: 100%;
margin-top: rem(8px);
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
text-align: left;
}
}
}
}

.nft-details-x {
.page__top_side {
.button__icon {
svg {
height: rem(16px);
width: rem(16px);
}

&:last-of-type {
svg path {
fill: $color-red-attention;
}
}
}
}

.info-row {
display: flex;
justify-content: space-between;
padding: rem(4px) 0;
border-bottom: 1px solid $color-grey-3;

&:nth-child(3) {
border-bottom: unset;
}

.capture__main_small:last-child {
color: $color-white;
}
}

.details-img {
margin-top: rem(12px);
margin-bottom: rem(16px);
border-radius: rem(16px);
}
}

.nft-raw-x {
.row.details .capture__main_small:first-child {
text-transform: capitalize;
}
}
66 changes: 66 additions & 0 deletions packages/browser-wallet/src/popup/popupX/pages/Nft/Nft.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import Page from '@popup/popupX/shared/Page';
import Text from '@popup/popupX/shared/Text';
import Button from '@popup/popupX/shared/Button';
import { displayNameAndSplitAddress, useSelectedCredential } from '@popup/shared/utils/account-helpers';
import Img from '@popup/shared/Img';
import { WalletCredential } from '@shared/storage/types';
import { useFlattenedAccountTokens } from '@popup/pages/Account/Tokens/utils';
import { getMetadataUnique } from '@shared/utils/token-helpers';
import { useNavigate } from 'react-router-dom';
import { relativeRoutes } from '@popup/popupX/constants/routes';

function useFilteredTokens(account: WalletCredential, unique: boolean) {
const tokens = useFlattenedAccountTokens(account);
return tokens.filter((t) => getMetadataUnique(t.metadata) === unique);
}

function NftList({ account }: { account: WalletCredential }) {
const tokens = useFilteredTokens(account, true);
const nav = useNavigate();
const navToDetails = (contractIndex: string, id: string) =>
nav(relativeRoutes.settings.nft.details.path.replace(':contractIndex', contractIndex).replace(':id', id));

return (
<div className="nft-x__list">
{tokens.map(({ contractIndex, id, metadata }) => (
<Button.Base
key={`${contractIndex}.${id}`}
onClick={() => navToDetails(contractIndex, id)}
className="nft-x__list_item"
>
<Img
className="nft-x__list_item-img"
src={metadata.thumbnail?.url ?? metadata.display?.url}
alt={metadata.name}
withDefaults
/>
<Text.MainRegular>{metadata.name}</Text.MainRegular>
</Button.Base>
))}
</div>
);
}

export default function Nft() {
const { t } = useTranslation('x', { keyPrefix: 'nft' });
const account = useSelectedCredential();

if (account === undefined) {
return null;
}

return (
<Page className="nft-x">
<Page.Top heading={t('nft')} />
<Page.Main>
<span className="owner">
<Text.Label>{t('owned')}</Text.Label>
<Text.Capture>{t('on', { value: displayNameAndSplitAddress(account) })}</Text.Capture>
</span>
<NftList account={account} />
</Page.Main>
</Page>
);
}
90 changes: 90 additions & 0 deletions packages/browser-wallet/src/popup/popupX/pages/Nft/NftDetails.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import React from 'react';
import Page from '@popup/popupX/shared/Page';
import { useTranslation } from 'react-i18next';
import { useNavigate, useParams } from 'react-router-dom';
import { withSelectedCredential } from '@popup/popupX/shared/utils/hoc';
import { WalletCredential } from '@shared/storage/types';
import { useFlattenedAccountTokens } from '@popup/pages/Account/Tokens/utils';
import Button from '@popup/popupX/shared/Button';
import Code from '@assets/svgX/code.svg';
import EyeSlash from '@assets/svgX/eye-slash.svg';
import Text from '@popup/popupX/shared/Text';
import Img from '@popup/shared/Img';
import { contractBalancesFamily, removeTokenFromCurrentAccountAtom } from '@popup/store/token';
import { useAtomValue } from 'jotai';
import { useUpdateAtom } from 'jotai/utils';
import { relativeRoutes } from '@popup/popupX/constants/routes';

const SUB_INDEX = '0';

type Params = {
contractIndex: string;
id: string;
};

function useSelectedToken(credential: WalletCredential) {
const { contractIndex, id } = useParams<Params>();
const token = useFlattenedAccountTokens(credential).find((t) => t.contractIndex === contractIndex && t.id === id);
return token;
}

function useRemoveToken() {
const { contractIndex, id } = useParams<Params>();
const nav = useNavigate();
const removeToken = useUpdateAtom(removeTokenFromCurrentAccountAtom);
if (!contractIndex || !id) return () => {};

return () => {
removeToken({ contractIndex, tokenId: id });
nav(-1);
};
}

type InfoRowProps = {
title: string;
value?: string;
};

function InfoRow({ title, value }: InfoRowProps) {
return (
<span className="info-row">
<Text.Capture>{title}</Text.Capture>
<Text.Capture>{value}</Text.Capture>
</span>
);
}

function NftDetails({ credential }: { credential: WalletCredential }) {
const nav = useNavigate();
const { t } = useTranslation('x', { keyPrefix: 'nft' });
const token = useSelectedToken(credential);
const removeToken = useRemoveToken();
const { contractIndex, id, metadata } = token || { id: '', contractIndex: '' };
const balancesAtom = contractBalancesFamily(credential?.address ?? '', token?.contractIndex ?? '');
const balance = useAtomValue(balancesAtom)[id];

const navToRaw = () => nav(relativeRoutes.settings.nft.details.raw.path);

return (
<Page className="nft-details-x">
<Page.Top heading={metadata?.name}>
<Button.Icon icon={<Code />} onClick={navToRaw} />
<Button.Icon icon={<EyeSlash />} onClick={removeToken} />
</Page.Top>
<Page.Main>
<InfoRow title={t('ownership')} value={balance === 0n ? t('unownedUnique') : t('ownedUnique')} />
<InfoRow title={t('contract')} value={`${contractIndex}, ${SUB_INDEX}`} />
<InfoRow title={t('tokenId')} value={id} />
<Img
className="details-img"
src={metadata?.thumbnail?.url ?? metadata?.display?.url}
alt={metadata?.name}
withDefaults
/>
<Text.Capture>{metadata?.description}</Text.Capture>
</Page.Main>
</Page>
);
}

export default withSelectedCredential(NftDetails);
48 changes: 48 additions & 0 deletions packages/browser-wallet/src/popup/popupX/pages/Nft/NftRaw.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import React from 'react';
import Page from '@popup/popupX/shared/Page';
import { WalletCredential } from '@shared/storage/types';
import { useParams } from 'react-router-dom';
import { useFlattenedAccountTokens } from '@popup/pages/Account/Tokens/utils';
import { withSelectedCredential } from '@popup/popupX/shared/utils/hoc';
import { useTranslation } from 'react-i18next';
import Button from '@popup/popupX/shared/Button';
import Copy from '@assets/svgX/copy.svg';
import { copyToClipboard } from '@popup/popupX/shared/utils/helpers';
import Card from '@popup/popupX/shared/Card';

type Params = {
contractIndex: string;
id: string;
};

function useSelectedToken(credential: WalletCredential) {
const { contractIndex, id } = useParams<Params>();
const token = useFlattenedAccountTokens(credential).find((t) => t.contractIndex === contractIndex && t.id === id);
return token;
}

function NftRaw({ credential }: { credential: WalletCredential }) {
const { t } = useTranslation('x', { keyPrefix: 'nft' });
const token = useSelectedToken(credential);
const metadata = token?.metadata || {};

return (
<Page className="nft-raw-x">
<Page.Top heading={t('rawMetadata')}>
<Button.Icon
icon={<Copy />}
onClick={() => copyToClipboard(JSON.stringify(token?.metadata, null, 2))}
/>
</Page.Top>
<Page.Main>
<Card>
{Object.entries(metadata).map(([k, v]) => (
<Card.RowDetails key={k} title={k} value={JSON.stringify(v)} />
))}
</Card>
</Page.Main>
</Page>
);
}

export default withSelectedCredential(NftRaw);
Loading

0 comments on commit 7b179be

Please sign in to comment.