diff --git a/ui/graphql/queries.graphql b/ui/graphql/queries.graphql index 3e1f0c5..a7cac93 100644 --- a/ui/graphql/queries.graphql +++ b/ui/graphql/queries.graphql @@ -41,7 +41,15 @@ query getLatestNFAs { query getNFA($id: ID!) { token(id: $id) { tokenId + owner { + id + } name + description + ENS + externalURL + logo + color } } diff --git a/ui/src/app.tsx b/ui/src/app.tsx index 16fc6fb..1a068d4 100644 --- a/ui/src/app.tsx +++ b/ui/src/app.tsx @@ -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 = () => { } /> } /> } /> + } /> {/** TODO remove for release */} } /> } /> diff --git a/ui/src/components/core/icon/icon-library.tsx b/ui/src/components/core/icon/icon-library.tsx index a8c481b..70d5473 100644 --- a/ui/src/components/core/icon/icon-library.tsx +++ b/ui/src/components/core/icon/icon-library.tsx @@ -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; diff --git a/ui/src/components/nfa-card/nfa-card.tsx b/ui/src/components/nfa-card/nfa-card.tsx index 6cd545e..1f09fbd 100644 --- a/ui/src/components/nfa-card/nfa-card.tsx +++ b/ui/src/components/nfa-card/nfa-card.tsx @@ -32,8 +32,7 @@ export type NFACardProps = Omit< export const NFACard: React.FC = 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]); diff --git a/ui/src/mocks/index.ts b/ui/src/mocks/index.ts index 3668f82..8941f0a 100644 --- a/ui/src/mocks/index.ts +++ b/ui/src/mocks/index.ts @@ -1,3 +1,4 @@ export * from './mint-site'; export * from './detail'; export * from './list'; +export * from './nfa'; diff --git a/ui/src/mocks/nfa.ts b/ui/src/mocks/nfa.ts new file mode 100644 index 0000000..85f21a6 --- /dev/null +++ b/ui/src/mocks/nfa.ts @@ -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', + }, +}; diff --git a/ui/src/views/index.ts b/ui/src/views/index.ts index 129b412..acf11b6 100644 --- a/ui/src/views/index.ts +++ b/ui/src/views/index.ts @@ -3,3 +3,4 @@ export * from './mint'; export * from './components-test'; export * from './explore'; export * from './access-point'; +export * from './indexed-nfa'; diff --git a/ui/src/views/indexed-nfa/fragments/aside.fragment.tsx b/ui/src/views/indexed-nfa/fragments/aside.fragment.tsx new file mode 100644 index 0000000..69f07c0 --- /dev/null +++ b/ui/src/views/indexed-nfa/fragments/aside.fragment.tsx @@ -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 ( + + ); +}; + +const CreateAccessPoint: React.FC = () => { + const { nfa } = IndexedNFA.useContext(); + return ( + + + Host NFA Frontend + + {/* TODO: replace with correct text */} + + + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce vitae + ante erat. Sed quis finibus diam. + + + + + + {/* TODO: place correct href */} + Learn more + + + + + ); +}; + +export const IndexedNFAAsideFragment: React.FC = () => { + const ref = useRef(null); + const [top, setTop] = useState(); + + useEffect(() => { + setTop(ref.current?.getBoundingClientRect().top); + }, [ref]); + + return ( + + + + + ); +}; diff --git a/ui/src/views/indexed-nfa/fragments/index.ts b/ui/src/views/indexed-nfa/fragments/index.ts new file mode 100644 index 0000000..d90e1fb --- /dev/null +++ b/ui/src/views/indexed-nfa/fragments/index.ts @@ -0,0 +1,3 @@ +export * from './aside.fragment'; +export * from './main.fragment'; +export * from './skeleton.fragment'; diff --git a/ui/src/views/indexed-nfa/fragments/main.fragment.tsx b/ui/src/views/indexed-nfa/fragments/main.fragment.tsx new file mode 100644 index 0000000..286273e --- /dev/null +++ b/ui/src/views/indexed-nfa/fragments/main.fragment.tsx @@ -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 = ({ + label, + children, +}: HeaderDataProps) => ( + + {label} + {children} + +); + +const Header: React.FC = () => { + const { nfa } = IndexedNFA.useContext(); + + return ( + <> + {nfa.name} + + + {nfa.owner.id} + + + + + + {/* TODO: place correct data */} + 12/12/22 + + + + + + {nfa.accessPoints?.length ?? 0} + + + + + ); +}; + +const Description: React.FC = () => { + const { nfa } = IndexedNFA.useContext(); + + return ( + <> + + Description + + + {nfa.description} + + + ); +}; + +type DataWrapperProps = React.PropsWithChildren<{ + label: string | number; +}>; + +const DataWrapper: React.FC = ({ + children, + label, +}: DataWrapperProps) => ( + + {children || '-'} + {label} + +); + +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 ( + <> + Traits + + {traitsToShow.map(([value, label]) => ( + + {value} + + ))} + + + ); +}; + +type VerificationBannerProps = { + verified: boolean; +}; + +const VerificationBanner: React.FC = ({ + 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 ( + + {text} + + + ); +}; + +const Verification: React.FC = () => { + return ( + <> + Verification + {/* TODO: Get verified from context */} + 0.5} /> + + {/* TODO: place correct data */} + polygon.eth + polygon/fe + + + ); +}; + +// 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 ( + <> + Frontends + + + + + + + + + + + + + + + Domain + Owner + Created + + + + + {apMocks.map((item) => ( + + + + + {item.domain} + + {item.owner} + + {item.createdAt} + + + + + ))} + + + + + ); +}; + +// 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 ( + <> + Versions + + + + + + + + + + + + + + + Commit + Preview + Time + + + + + {versionsMock.map((item) => ( + + + + {item.live && 'Live'} + + + {item.commit.slice(0, 6)} + + {item.preview} + + {item.time} + + + + + ))} + + + + + ); +}; + +export const IndexedNFAMainFragment: React.FC = () => { + return ( + +
+ + + + + + + ); +}; diff --git a/ui/src/views/indexed-nfa/fragments/skeleton.fragment.tsx b/ui/src/views/indexed-nfa/fragments/skeleton.fragment.tsx new file mode 100644 index 0000000..e1e05a4 --- /dev/null +++ b/ui/src/views/indexed-nfa/fragments/skeleton.fragment.tsx @@ -0,0 +1,16 @@ +import { IndexedNFAStyles as S } from '../indexed-nfa.styles'; + +export const IndexedNFASkeletonFragment: React.FC = () => ( + + + + + + + + + + + + +); diff --git a/ui/src/views/indexed-nfa/index.ts b/ui/src/views/indexed-nfa/index.ts new file mode 100644 index 0000000..c044971 --- /dev/null +++ b/ui/src/views/indexed-nfa/index.ts @@ -0,0 +1 @@ +export * from './indexed-nfa'; diff --git a/ui/src/views/indexed-nfa/indexed-nfa.context.tsx b/ui/src/views/indexed-nfa/indexed-nfa.context.tsx new file mode 100644 index 0000000..fc99dac --- /dev/null +++ b/ui/src/views/indexed-nfa/indexed-nfa.context.tsx @@ -0,0 +1,28 @@ +import { Owner, Token } from '@/graphclient'; +import { createContext } from '@/utils'; + +const [Provider, useContext] = createContext({ + name: 'IndexedNFA.Context', + hookName: 'IndexedNFA.useContext', + providerName: 'IndexedNFA.Provider', +}); + +export const IndexedNFA = { + useContext, + Provider: ({ children, nfa }: IndexedNFA.ProviderProps): JSX.Element => { + return {children}; + }, +}; + +export namespace IndexedNFA { + export type Context = { + nfa: Omit & { + owner: Pick; + }; + }; + + export type ProviderProps = { + children: React.ReactNode | React.ReactNode[]; + nfa: Context['nfa']; + }; +} diff --git a/ui/src/views/indexed-nfa/indexed-nfa.styles.ts b/ui/src/views/indexed-nfa/indexed-nfa.styles.ts new file mode 100644 index 0000000..1e1b2c4 --- /dev/null +++ b/ui/src/views/indexed-nfa/indexed-nfa.styles.ts @@ -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', + }), +}; diff --git a/ui/src/views/indexed-nfa/indexed-nfa.tsx b/ui/src/views/indexed-nfa/indexed-nfa.tsx new file mode 100644 index 0000000..88cb844 --- /dev/null +++ b/ui/src/views/indexed-nfa/indexed-nfa.tsx @@ -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 ; + } + + // TODO: replace NFAMock with real data from useQuery + return ( + + + + + + + ); +};