feat: UI indexed nfa view (#230)

* wip: indexed nfa base layout

* refactor: split aside and main components to single files

* feat: indexed nfa aside styling

* feat: add main section header and drescription

* feat: add main section traits

* feat: add verification banner in main fragment

* feat: add externla link icon

* feat: add table stylings and frontends section mocked

* feat: add versions table mocked

* feat: add small layer of responsivity

* feat: add indexed nfa view skeleton

* refactor: create fragments folder

* refactor: split out mock and add todo comment

* feat: add icon on verified banner

* refactor: table stylings

* feat: add chevron right icon

* fix: link paths

* chore: add todo comments

* refactor: set initial position for aside container

* refactor: improve spacings

* fix: remove leftover comment
This commit is contained in:
Felipe Mendes 2023-04-20 14:19:53 -03:00 committed by GitHub
parent 795164b4aa
commit 22a6d70e98
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 756 additions and 4 deletions

View File

@ -41,7 +41,15 @@ query getLatestNFAs {
query getNFA($id: ID!) {
token(id: $id) {
tokenId
owner {
id
}
name
description
ENS
externalURL
logo
color
}
}

View File

@ -3,7 +3,13 @@ import { HashRouter, Navigate, Route, Routes } from 'react-router-dom';
import { themeGlobals } from '@/theme/globals';
import { AppPage, ToastProvider } from './components';
import { ComponentsTest, CreateAP, Explore, Home, Mint } from './views';
import {
ComponentsTest,
CreateAP,
Explore,
IndexedNFAView,
Mint,
} from './views';
export const App: React.FC = () => {
themeGlobals();
@ -17,6 +23,7 @@ export const App: React.FC = () => {
<Route path="/mint" element={<Mint />} />
<Route path="/create-ap" element={<CreateAP />} />
<Route path="/create-ap/:id" element={<CreateAP />} />
<Route path="/nfa/:id" element={<IndexedNFAView />} />
{/** TODO remove for release */}
<Route path="/components-test" element={<ComponentsTest />} />
<Route path="*" element={<Navigate to="/" />} />

View File

@ -4,12 +4,15 @@ import { AiOutlineTwitter } from '@react-icons/all-files/ai/AiOutlineTwitter';
import { BiGitBranch } from '@react-icons/all-files/bi/BiGitBranch';
import { BiSearch } from '@react-icons/all-files/bi/BiSearch';
import { BsFillSquareFill } from '@react-icons/all-files/bs/BsFillSquareFill';
import { FaChevronRight } from '@react-icons/all-files/fa/FaChevronRight';
import { FaExternalLinkAlt } from '@react-icons/all-files/fa/FaExternalLinkAlt';
import { IoArrowBackCircleSharp } from '@react-icons/all-files/io5/IoArrowBackCircleSharp';
import { IoCheckmarkCircleSharp } from '@react-icons/all-files/io5/IoCheckmarkCircleSharp';
import { IoClose } from '@react-icons/all-files/io5/IoClose';
import { IoCloudUploadSharp } from '@react-icons/all-files/io5/IoCloudUploadSharp';
import { IoInformationCircleSharp } from '@react-icons/all-files/io5/IoInformationCircleSharp';
import { IoLogoGithub } from '@react-icons/all-files/io5/IoLogoGithub';
import { MdVerifiedUser } from '@react-icons/all-files/md/MdVerifiedUser';
import {
BetaTag,
@ -28,19 +31,22 @@ export const IconLibrary = Object.freeze({
check: AiOutlineCheck,
'check-circle': IoCheckmarkCircleSharp,
'chevron-down': ChevronDownIcon,
'chevron-right': FaChevronRight,
close: IoClose,
error: ErrorIcon,
ethereum: EthereumIcon,
'external-link': FaExternalLinkAlt,
fleekLogo: FleekLogo,
fleekName: FleekName,
github: IoLogoGithub,
info: IoInformationCircleSharp,
upload: IoCloudUploadSharp,
metamask: MetamaskIcon, //remove if not used
search: BiSearch,
square: BsFillSquareFill,
success: AiFillCheckCircle,
twitter: AiOutlineTwitter,
upload: IoCloudUploadSharp,
verified: MdVerifiedUser,
});
export type IconName = keyof typeof IconLibrary;

View File

@ -32,8 +32,7 @@ export type NFACardProps = Omit<
export const NFACard: React.FC<NFACardProps> = forwardStyledRef<
HTMLAnchorElement,
NFACardProps
// TODO: Set default path to NFA page
>(({ data, to = `/create-ap/${data.tokenId}`, ...props }, ref) => {
>(({ data, to = `/nfa/${data.tokenId}`, ...props }, ref) => {
const { name, color, ENS, logo, accessPoints } = data;
const apCounter = useMemo(() => accessPoints?.length ?? 0, [accessPoints]);

View File

@ -1,3 +1,4 @@
export * from './mint-site';
export * from './detail';
export * from './list';
export * from './nfa';

26
ui/src/mocks/nfa.ts Normal file
View File

@ -0,0 +1,26 @@
export const NFAMock = {
id: '6',
tokenId: '6',
name: 'Polygon',
description:
'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce sit amet velit dolor. Praesent dapibus euismod molestie. Duis maximus porttitor odio. Duis quis lorem id lacus cursus commodo vel vehicula mauris.',
externalURL: 'https://polygon.com',
ENS: 'polygon.eth',
logo: 'data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4KPCEtLSBHZW5lcmF0b3I6IEFkb2JlIElsbHVzdHJhdG9yIDI0LjAuMCwgU1ZHIEV4cG9ydCBQbHVnLUluIC4gU1ZHIFZlcnNpb246IDYuMDAgQnVpbGQgMCkgIC0tPgo8c3ZnIHZlcnNpb249IjEuMSIgaWQ9IkxheWVyXzEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHg9IjBweCIgeT0iMHB4IgoJIHZpZXdCb3g9IjAgMCAzOC40IDMzLjUiIHN0eWxlPSJlbmFibGUtYmFja2dyb3VuZDpuZXcgMCAwIDM4LjQgMzMuNTsiIHhtbDpzcGFjZT0icHJlc2VydmUiPgo8c3R5bGUgdHlwZT0idGV4dC9jc3MiPgoJLnN0MHtmaWxsOiM4MjQ3RTU7fQo8L3N0eWxlPgo8Zz4KCTxwYXRoIGNsYXNzPSJzdDAiIGQ9Ik0yOSwxMC4yYy0wLjctMC40LTEuNi0wLjQtMi40LDBMMjEsMTMuNWwtMy44LDIuMWwtNS41LDMuM2MtMC43LDAuNC0xLjYsMC40LTIuNCwwTDUsMTYuMwoJCWMtMC43LTAuNC0xLjItMS4yLTEuMi0yLjF2LTVjMC0wLjgsMC40LTEuNiwxLjItMi4xbDQuMy0yLjVjMC43LTAuNCwxLjYtMC40LDIuNCwwTDE2LDcuMmMwLjcsMC40LDEuMiwxLjIsMS4yLDIuMXYzLjNsMy44LTIuMlY3CgkJYzAtMC44LTAuNC0xLjYtMS4yLTIuMWwtOC00LjdjLTAuNy0wLjQtMS42LTAuNC0yLjQsMEwxLjIsNUMwLjQsNS40LDAsNi4yLDAsN3Y5LjRjMCwwLjgsMC40LDEuNiwxLjIsMi4xbDguMSw0LjcKCQljMC43LDAuNCwxLjYsMC40LDIuNCwwbDUuNS0zLjJsMy44LTIuMmw1LjUtMy4yYzAuNy0wLjQsMS42LTAuNCwyLjQsMGw0LjMsMi41YzAuNywwLjQsMS4yLDEuMiwxLjIsMi4xdjVjMCwwLjgtMC40LDEuNi0xLjIsMi4xCgkJTDI5LDI4LjhjLTAuNywwLjQtMS42LDAuNC0yLjQsMGwtNC4zLTIuNWMtMC43LTAuNC0xLjItMS4yLTEuMi0yLjFWMjFsLTMuOCwyLjJ2My4zYzAsMC44LDAuNCwxLjYsMS4yLDIuMWw4LjEsNC43CgkJYzAuNywwLjQsMS42LDAuNCwyLjQsMGw4LjEtNC43YzAuNy0wLjQsMS4yLTEuMiwxLjItMi4xVjE3YzAtMC44LTAuNC0xLjYtMS4yLTIuMUwyOSwxMC4yeiIvPgo8L2c+Cjwvc3ZnPgo=',
color: 8668388,
accessPointAutoApproval: true,
owner: {
id: '0x7ED735b7095C05d78dF169F991f2b7f1A1F1A049',
collection: true,
},
mintedBy: '0x7ED735b7095C05d78dF169F991f2b7f1A1F1A049',
controllers: [],
gitRepository: {
id: '',
},
commitHash: '0x7ED735b7095C05d78dF169F991f2b7f1A1F1A049',
accessPoints: [],
verifier: {
id: '0x7ED735b7095C05d78dF169F991f2b7f1A1F1A049',
},
};

View File

@ -3,3 +3,4 @@ export * from './mint';
export * from './components-test';
export * from './explore';
export * from './access-point';
export * from './indexed-nfa';

View File

@ -0,0 +1,75 @@
import { useEffect, useMemo, useRef, useState } from 'react';
import { Link } from 'react-router-dom';
import { Button, Flex, Icon, NFAPreview } from '@/components';
import { IndexedNFA } from '../indexed-nfa.context';
import { IndexedNFAStyles as S } from '../indexed-nfa.styles';
const Preview: React.FC = () => {
const { nfa } = IndexedNFA.useContext();
const color = useMemo(
// TODO: replace with util function
() => `#${`000000${nfa.color.toString(16)}`.slice(-6)}`,
[nfa]
);
return (
<NFAPreview
color={color}
logo={nfa.logo}
ens={nfa.ENS}
name={nfa.name}
size="100%"
css={{
borderRadius: '$lg',
border: '1px solid $slate6',
}}
/>
);
};
const CreateAccessPoint: React.FC = () => {
const { nfa } = IndexedNFA.useContext();
return (
<S.Aside.CreateAccessPoint.Container>
<S.Aside.CreateAccessPoint.Heading>
Host NFA Frontend
</S.Aside.CreateAccessPoint.Heading>
{/* TODO: replace with correct text */}
<S.Aside.CreateAccessPoint.Text>
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce vitae
ante erat. Sed quis finibus diam.
</S.Aside.CreateAccessPoint.Text>
<Flex css={{ gap: '$3' }}>
<Button as={Link} to={`/create-ap/${nfa.tokenId}`} colorScheme="blue">
Host NFA Frontend
</Button>
<S.Aside.CreateAccessPoint.Extra href="">
{/* TODO: place correct href */}
Learn more
<Icon name="chevron-right" />
</S.Aside.CreateAccessPoint.Extra>
</Flex>
</S.Aside.CreateAccessPoint.Container>
);
};
export const IndexedNFAAsideFragment: React.FC = () => {
const ref = useRef<HTMLDivElement>(null);
const [top, setTop] = useState<number>();
useEffect(() => {
setTop(ref.current?.getBoundingClientRect().top);
}, [ref]);
return (
<S.Aside.Container ref={ref} css={{ top }}>
<Preview />
<CreateAccessPoint />
</S.Aside.Container>
);
};

View File

@ -0,0 +1,3 @@
export * from './aside.fragment';
export * from './main.fragment';
export * from './skeleton.fragment';

View File

@ -0,0 +1,285 @@
import React, { useMemo } from 'react';
import { Flex, Icon, IconName, ResolvedAddress, Text } from '@/components';
import { IndexedNFA } from '../indexed-nfa.context';
import { IndexedNFAStyles as S } from '../indexed-nfa.styles';
type HeaderDataProps = {
label: string;
children: React.ReactNode;
};
const HeaderData: React.FC<HeaderDataProps> = ({
label,
children,
}: HeaderDataProps) => (
<Flex css={{ gap: '$2' }}>
<Text css={{ color: '$slate11' }}>{label}</Text>
<Text css={{ color: '$slate12' }}>{children}</Text>
</Flex>
);
const Header: React.FC = () => {
const { nfa } = IndexedNFA.useContext();
return (
<>
<S.Main.Heading>{nfa.name}</S.Main.Heading>
<Flex css={{ justifyContent: 'space-between', alignItems: 'center' }}>
<HeaderData label="Owner">
<ResolvedAddress>{nfa.owner.id}</ResolvedAddress>
</HeaderData>
<S.Main.Divider.Elipse />
<HeaderData label="Created">
{/* TODO: place correct data */}
12/12/22
</HeaderData>
<S.Main.Divider.Elipse />
<HeaderData label="Access Points">
{nfa.accessPoints?.length ?? 0}
</HeaderData>
</Flex>
<S.Main.Divider.Line />
</>
);
};
const Description: React.FC = () => {
const { nfa } = IndexedNFA.useContext();
return (
<>
<S.Main.SectionHeading css={{ marginTop: 0 }}>
Description
</S.Main.SectionHeading>
<S.Main.DataContainer as={S.Main.Paragraph}>
{nfa.description}
</S.Main.DataContainer>
</>
);
};
type DataWrapperProps = React.PropsWithChildren<{
label: string | number;
}>;
const DataWrapper: React.FC<DataWrapperProps> = ({
children,
label,
}: DataWrapperProps) => (
<S.Main.DataContainer key={label} css={{ flex: 1, minWidth: '45%' }}>
<Text css={{ color: '$slate12', fontWeight: 700 }}>{children || '-'}</Text>
<Text css={{ color: '$slate11' }}>{label}</Text>
</S.Main.DataContainer>
);
const Traits: React.FC = () => {
const { nfa } = IndexedNFA.useContext();
// TODO: place correct data
const traitsToShow = useMemo(() => {
return [
[nfa.ENS, 'ENS'],
[nfa.gitRepository.id, 'Repository'],
[10, 'Version'],
[nfa.externalURL, 'Domain'],
[nfa.externalURL, 'Domain 2'],
];
}, [nfa]);
return (
<>
<S.Main.SectionHeading>Traits</S.Main.SectionHeading>
<S.Main.DataList>
{traitsToShow.map(([value, label]) => (
<DataWrapper key={label} label={label}>
{value}
</DataWrapper>
))}
</S.Main.DataList>
</>
);
};
type VerificationBannerProps = {
verified: boolean;
};
const VerificationBanner: React.FC<VerificationBannerProps> = ({
verified,
}: VerificationBannerProps) => {
const [text, icon] = useMemo<[string, IconName]>(() => {
if (verified)
return ['This Non Fungible Application is Verified.', 'verified'];
return ['This Non Fungible Application is not Verified.', 'error'];
}, [verified]);
return (
<S.Main.VerificationBanner verified={verified}>
{text}
<Icon
name={icon}
css={{
fontSize: '3.5rem',
color: '$black',
position: 'absolute',
right: 'calc(8% - 1.75rem)',
zIndex: 1,
}}
/>
</S.Main.VerificationBanner>
);
};
const Verification: React.FC = () => {
return (
<>
<S.Main.SectionHeading>Verification</S.Main.SectionHeading>
{/* TODO: Get verified from context */}
<VerificationBanner verified={Math.random() > 0.5} />
<S.Main.DataList>
{/* TODO: place correct data */}
<DataWrapper label="Verifier">polygon.eth</DataWrapper>
<DataWrapper label="Repository">polygon/fe</DataWrapper>
</S.Main.DataList>
</>
);
};
// TODO: replace mocks with fetched data
const apMocks = new Array(10).fill(0).map((_, index) => ({
approved: Math.random() > 0.5,
domain: `domain${index}.com`,
owner: '0x7ED735b7095C05d78dF169F991f2b7f1A1F1A049',
createdAt: `${Math.floor(Math.random() * 30)}m ago`,
}));
const AccessPoints: React.FC = () => {
return (
<>
<S.Main.SectionHeading>Frontends</S.Main.SectionHeading>
<S.Main.Table.Container>
<S.Main.Table.Root>
<colgroup>
<col span={1} style={{ width: '9.5%' }} />
<col span={1} style={{ width: '32.5%' }} />
<col span={1} style={{ width: '32.5%' }} />
<col span={1} style={{ width: '16%' }} />
<col span={1} style={{ width: '9.5%' }} />
</colgroup>
<S.Main.Table.Head>
<S.Main.Table.Row>
<S.Main.Table.Data>
<S.Main.Table.Marker />
</S.Main.Table.Data>
<S.Main.Table.Data>Domain</S.Main.Table.Data>
<S.Main.Table.Data>Owner</S.Main.Table.Data>
<S.Main.Table.Data>Created</S.Main.Table.Data>
<S.Main.Table.Data />
</S.Main.Table.Row>
</S.Main.Table.Head>
<S.Main.Table.Body>
{apMocks.map((item) => (
<S.Main.Table.Row key={item.domain}>
<S.Main.Table.Data align="center">
<S.Main.Table.Marker
variant={item.approved ? 'active' : 'inactive'}
/>
</S.Main.Table.Data>
<S.Main.Table.Data>{item.domain}</S.Main.Table.Data>
<S.Main.Table.Data>
<ResolvedAddress>{item.owner}</ResolvedAddress>
</S.Main.Table.Data>
<S.Main.Table.Data>{item.createdAt}</S.Main.Table.Data>
<S.Main.Table.Data>
<Icon name="external-link" />
</S.Main.Table.Data>
</S.Main.Table.Row>
))}
</S.Main.Table.Body>
</S.Main.Table.Root>
</S.Main.Table.Container>
</>
);
};
// TODO: replace mocks with fetched data
const versionsMock = new Array(10).fill(0).map((_, index) => ({
live: index === 0,
commit: (Math.random() * 0xfffffffff).toString(16),
preview: `test: subgraph matchstick tests for access points and acl refactor (#150
)
* fix: errors from deprecated entities.`,
time: `${Math.floor(Math.random() * 30)}m ago`,
}));
const Versions: React.FC = () => {
return (
<>
<S.Main.SectionHeading>Versions</S.Main.SectionHeading>
<S.Main.Table.Container>
<S.Main.Table.Root>
<colgroup>
<col span={1} style={{ width: '9.5%' }} />
<col span={1} style={{ width: '15%' }} />
<col span={1} style={{ width: '50%' }} />
<col span={1} style={{ width: '16%' }} />
<col span={1} style={{ width: '9.5%' }} />
</colgroup>
<S.Main.Table.Head>
<S.Main.Table.Row>
<S.Main.Table.Data>
<S.Main.Table.Marker />
</S.Main.Table.Data>
<S.Main.Table.Data>Commit</S.Main.Table.Data>
<S.Main.Table.Data>Preview</S.Main.Table.Data>
<S.Main.Table.Data>Time</S.Main.Table.Data>
<S.Main.Table.Data />
</S.Main.Table.Row>
</S.Main.Table.Head>
<S.Main.Table.Body>
{versionsMock.map((item) => (
<S.Main.Table.Row key={item.commit}>
<S.Main.Table.Data>
<S.Main.Table.Marker
variant={item.live ? 'active' : 'inactive'}
text={item.live}
>
{item.live && 'Live'}
</S.Main.Table.Marker>
</S.Main.Table.Data>
<S.Main.Table.Data>{item.commit.slice(0, 6)}</S.Main.Table.Data>
<S.Main.Table.Data title={item.preview}>
{item.preview}
</S.Main.Table.Data>
<S.Main.Table.Data>{item.time}</S.Main.Table.Data>
<S.Main.Table.Data>
<Icon name="external-link" />
</S.Main.Table.Data>
</S.Main.Table.Row>
))}
</S.Main.Table.Body>
</S.Main.Table.Root>
</S.Main.Table.Container>
</>
);
};
export const IndexedNFAMainFragment: React.FC = () => {
return (
<S.Main.Container>
<Header />
<Description />
<Traits />
<Verification />
<AccessPoints />
<Versions />
</S.Main.Container>
);
};

View File

@ -0,0 +1,16 @@
import { IndexedNFAStyles as S } from '../indexed-nfa.styles';
export const IndexedNFASkeletonFragment: React.FC = () => (
<S.Grid>
<S.Aside.Container>
<S.Skeleton css={{ aspectRatio: 1, width: '100%' }} />
</S.Aside.Container>
<S.Main.Container css={{ justifyContent: 'stretch' }}>
<S.Skeleton css={{ height: '2.875rem' }} />
<S.Skeleton css={{ height: '1.5rem' }} />
<S.Main.Divider.Line />
<S.Skeleton css={{ height: '10rem' }} />
<S.Skeleton css={{ height: '15rem' }} />
</S.Main.Container>
</S.Grid>
);

View File

@ -0,0 +1 @@
export * from './indexed-nfa';

View File

@ -0,0 +1,28 @@
import { Owner, Token } from '@/graphclient';
import { createContext } from '@/utils';
const [Provider, useContext] = createContext<IndexedNFA.Context>({
name: 'IndexedNFA.Context',
hookName: 'IndexedNFA.useContext',
providerName: 'IndexedNFA.Provider',
});
export const IndexedNFA = {
useContext,
Provider: ({ children, nfa }: IndexedNFA.ProviderProps): JSX.Element => {
return <Provider value={{ nfa }}>{children}</Provider>;
},
};
export namespace IndexedNFA {
export type Context = {
nfa: Omit<Token, 'mintTransaction' | 'id' | 'owner'> & {
owner: Pick<Owner, 'id'>;
};
};
export type ProviderProps = {
children: React.ReactNode | React.ReactNode[];
nfa: Context['nfa'];
};
}

View File

@ -0,0 +1,241 @@
import { Skeleton } from '@/components';
import { styled } from '@/theme';
const Spacing = '$5';
export const IndexedNFAStyles = {
Grid: styled('div', {
display: 'grid',
gridTemplateAreas: '"aside main"',
gridTemplateColumns: '24.0625rem 1fr',
gridTemplateRows: 'fit-content',
gap: `calc(2 * ${Spacing})`,
padding: Spacing,
'@media (max-width: 1080px)': {
gridTemplateColumns: '20rem 1fr',
},
'@media (max-width: 580px)': {
gridTemplateAreas: '"aside" "main"',
gridTemplateColumns: '1fr',
},
}),
Aside: {
Container: styled('aside', {
gridArea: 'aside',
position: 'sticky',
display: 'flex',
flexDirection: 'column',
gap: Spacing,
height: 'fit-content',
'@media (max-width: 580px)': {
position: 'static',
},
}),
CreateAccessPoint: {
Container: styled('div', {
display: 'flex',
flexDirection: 'column',
gap: Spacing,
padding: Spacing,
backgroundColor: '$blue1',
borderRadius: '$lg',
}),
Heading: styled('h2', {
fontSize: '$md',
color: '$slate12',
}),
Text: styled('p', {
fontSize: '$sm',
color: '$slate11',
}),
Extra: styled('a', {
display: 'flex',
alignItems: 'center',
color: '$slate11',
fontSize: '$sm',
gap: '$2',
}),
},
},
Main: {
Container: styled('main', {
gridArea: 'main',
display: 'flex',
flexDirection: 'column',
gap: Spacing,
}),
Heading: styled('h1', {
fontSize: '2.125rem',
lineHeight: 1.35,
fontWeight: 700,
}),
SectionHeading: styled('h2', {
fontSize: '$xl',
lineHeight: 1.2,
fontWeight: 700,
marginTop: Spacing,
}),
Divider: {
Line: styled('span', {
width: '100%',
borderBottom: '1px solid $slate6',
}),
Elipse: styled('span', {
width: '0.375rem',
height: '0.375rem',
backgroundColor: '$slate4',
borderRadius: '100%',
}),
},
Paragraph: styled('p', {
color: '$slate11',
lineHeight: 1.43,
}),
DataContainer: styled('div', {
display: 'flex',
flexDirection: 'column',
border: '1px solid $slate6',
borderRadius: '$lg',
padding: Spacing,
gap: `$1`,
}),
DataList: styled('div', {
display: 'flex',
flexWrap: 'wrap',
gap: '$5',
}),
VerificationBanner: styled('div', {
position: 'relative',
display: 'flex',
alignItems: 'center',
border: '1px solid $slate6',
borderRadius: '$lg',
padding: '$8 $5',
fontWeight: 700,
overflow: 'hidden',
'&:after': {
content: '""',
position: 'absolute',
right: '-$5',
top: '-$10',
bottom: '-$10',
left: '84%',
borderRadius: '80% 0 0 80%',
},
variants: {
verified: {
true: {
borderColor: '$green11',
color: '$green11',
'&:after': {
backgroundColor: '$green11',
},
},
false: {
borderColor: '$red11',
color: '$red11',
'&:after': {
backgroundColor: '$red11',
},
},
},
},
}),
Table: {
Container: styled('div', {
border: '1px solid $slate6',
borderRadius: '10px',
padding: '0 $5',
maxHeight: '15.125rem',
overflow: 'auto',
}),
Root: styled('table', {
width: 'calc(100% + 2 * $space$5)',
margin: '0 -$5',
}),
Head: styled('thead', {
position: 'sticky',
top: 0,
backgroundColor: '$black',
'&:after': {
position: 'absolute',
content: '""',
bottom: 0,
left: 0,
right: 0,
borderBottom: '1px solid $slate6',
},
}),
Row: styled('tr'),
Data: styled('td', {
padding: '$3',
maxWidth: '10rem',
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
}),
Body: styled('tbody', {
tr: {
'&:hover': {
backgroundColor: '$slate6',
cursor: 'pointer',
},
},
}),
Marker: styled('span', {
display: 'block',
margin: 'auto',
width: '0.5625rem',
height: '0.5625rem',
borderRadius: '$full',
backgroundColor: '$slate6',
variants: {
variant: {
active: {
backgroundColor: '$green11',
},
inactive: {
backgroundColor: '$slate8',
},
},
text: {
true: {
fontSize: '$xs',
padding: '0 $2',
width: 'fit-content',
height: 'fit-content',
},
},
},
compoundVariants: [
{
variant: 'active',
text: true,
css: {
color: '$green11',
backgroundColor: '$green3',
},
},
],
}),
},
},
Skeleton: styled(Skeleton, {
borderRadius: '$lg',
}),
};

View File

@ -0,0 +1,55 @@
import { useQuery } from '@apollo/client';
import { ethers } from 'ethers';
import { useNavigate, useParams } from 'react-router-dom';
import { getNFADocument } from '@/graphclient';
import { NFAMock } from '@/mocks';
import { AppLog } from '@/utils';
import {
IndexedNFAAsideFragment,
IndexedNFAMainFragment,
IndexedNFASkeletonFragment,
} from './fragments';
import { IndexedNFA } from './indexed-nfa.context';
import { IndexedNFAStyles as S } from './indexed-nfa.styles';
export const IndexedNFAView: React.FC = () => {
const { id } = useParams<{ id: string }>();
const navigate = useNavigate();
const handleError = (error: unknown): void => {
AppLog.errorToast(
`It was not possible to find the NFA with id "${id}"`,
error
);
navigate('/', { replace: true });
};
const { loading, data = { token: {} } } = useQuery(getNFADocument, {
skip: id === undefined,
variables: {
id: ethers.utils.hexlify(Number(id)),
},
onCompleted(data) {
if (!data.token) handleError(new Error('Token not found'));
},
onError(error) {
handleError(error);
},
});
if (loading) {
return <IndexedNFASkeletonFragment />;
}
// TODO: replace NFAMock with real data from useQuery
return (
<IndexedNFA.Provider nfa={{ ...NFAMock, ...data.token }}>
<S.Grid>
<IndexedNFAAsideFragment />
<IndexedNFAMainFragment />
</S.Grid>
</IndexedNFA.Provider>
);
};