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:
parent
795164b4aa
commit
22a6d70e98
|
|
@ -41,7 +41,15 @@ query getLatestNFAs {
|
|||
query getNFA($id: ID!) {
|
||||
token(id: $id) {
|
||||
tokenId
|
||||
owner {
|
||||
id
|
||||
}
|
||||
name
|
||||
description
|
||||
ENS
|
||||
externalURL
|
||||
logo
|
||||
color
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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="/" />} />
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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]);
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
export * from './mint-site';
|
||||
export * from './detail';
|
||||
export * from './list';
|
||||
export * from './nfa';
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
},
|
||||
};
|
||||
|
|
@ -3,3 +3,4 @@ export * from './mint';
|
|||
export * from './components-test';
|
||||
export * from './explore';
|
||||
export * from './access-point';
|
||||
export * from './indexed-nfa';
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
export * from './aside.fragment';
|
||||
export * from './main.fragment';
|
||||
export * from './skeleton.fragment';
|
||||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
@ -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>
|
||||
);
|
||||
|
|
@ -0,0 +1 @@
|
|||
export * from './indexed-nfa';
|
||||
|
|
@ -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'];
|
||||
};
|
||||
}
|
||||
|
|
@ -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',
|
||||
}),
|
||||
};
|
||||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
Loading…
Reference in New Issue