diff --git a/ui/graphql/queries.graphql b/ui/graphql/queries.graphql index ff09712..6bcec26 100644 --- a/ui/graphql/queries.graphql +++ b/ui/graphql/queries.graphql @@ -22,6 +22,7 @@ query lastNFAsPaginated( accessPoints { id } + verified } } @@ -31,41 +32,52 @@ query totalTokens($contractId: ID!) { } } -query getLatestNFAs { - tokens { - id - name - } -} - query getNFADetail($id: ID!) { token(id: $id) { - tokenId - owner { + accessPoints { id } - name description - ENS - externalURL - logo color createdAt - accessPoints { - createdAt - contentVerified - owner { - id - } + ENS + externalURL + gitRepository { + id + } + logo + name + owner { id } verified verifier { id } - gitRepository { + tokenId + } +} + +query getAccessPointsNFA( + $tokenId: String! + $orderBy: AccessPoint_orderBy + $orderDirection: OrderDirection + $pageSize: Int + $skip: Int +) { + accessPoints( + where: { token: $tokenId } + orderDirection: $orderDirection + orderBy: $orderBy + first: $pageSize + skip: $skip + ) { + contentVerified + createdAt + owner { id } + id } } diff --git a/ui/src/app.context.tsx b/ui/src/app.context.tsx new file mode 100644 index 0000000..b84f47e --- /dev/null +++ b/ui/src/app.context.tsx @@ -0,0 +1,33 @@ +import { useState } from 'react'; + +import { createContext } from './utils'; + +export type AppContext = { + backgroundColor: string; + setBackgroundColor: (color: string) => void; +}; + +const [AppProvider, useContext] = createContext({ + name: 'App.Context', + hookName: 'App.useContext', + providerName: 'App.Provider', +}); + +export abstract class App { + static readonly useContext = useContext; + static readonly Provider: React.FC = ({ children }) => { + const [backgroundColor, setBackgroundColor] = useState(''); + + return ( + + {children} + + ); + }; +} + +export namespace App { + export type AppProps = { + children: React.ReactNode; + }; +} diff --git a/ui/src/app.tsx b/ui/src/app.tsx index 634b2b2..d5a93cd 100644 --- a/ui/src/app.tsx +++ b/ui/src/app.tsx @@ -2,6 +2,7 @@ import { HashRouter, Navigate, Route, Routes } from 'react-router-dom'; import { themeGlobals } from '@/theme/globals'; +import { App as AppContext } from './app.context'; import { AppPage, ToastProvider } from './components'; import { ComponentsTest, @@ -17,17 +18,19 @@ export const App: React.FC = () => { <> - - - } /> - } /> - } /> - } /> - {/** TODO remove for release */} - } /> - } /> - - + + + + } /> + } /> + } /> + } /> + {/** TODO remove for release */} + } /> + } /> + + + ); diff --git a/ui/src/assets/Rectangle-199.png b/ui/src/assets/Rectangle-199.png new file mode 100644 index 0000000..a8a5525 Binary files /dev/null and b/ui/src/assets/Rectangle-199.png differ diff --git a/ui/src/assets/Rectangle-200.png b/ui/src/assets/Rectangle-200.png new file mode 100644 index 0000000..fc00c0a Binary files /dev/null and b/ui/src/assets/Rectangle-200.png differ diff --git a/ui/src/assets/Rectangle-201.png b/ui/src/assets/Rectangle-201.png new file mode 100644 index 0000000..81a8e7b Binary files /dev/null and b/ui/src/assets/Rectangle-201.png differ diff --git a/ui/src/components/core/icon/custom/index.ts b/ui/src/components/core/icon/custom/index.ts index f113c6a..735c6c0 100644 --- a/ui/src/components/core/icon/custom/index.ts +++ b/ui/src/components/core/icon/custom/index.ts @@ -5,3 +5,4 @@ export * from './fleek-name-icon'; export * from './beta-tag-icon'; export * from './error-icon'; export * from './fleek-logo-icon'; +export * from './opensea-icon'; diff --git a/ui/src/components/core/icon/custom/opensea-icon.tsx b/ui/src/components/core/icon/custom/opensea-icon.tsx new file mode 100644 index 0000000..3a7e7e1 --- /dev/null +++ b/ui/src/components/core/icon/custom/opensea-icon.tsx @@ -0,0 +1,15 @@ +import { IconStyles as IS } from '../icon.styles'; + +export const OpenseaIcon: React.FC = (props) => ( + + + +); diff --git a/ui/src/components/core/icon/custom/share-icon.tsx b/ui/src/components/core/icon/custom/share-icon.tsx new file mode 100644 index 0000000..eff3f7e --- /dev/null +++ b/ui/src/components/core/icon/custom/share-icon.tsx @@ -0,0 +1,18 @@ +import { IconStyles as IS } from '../icon.styles'; + +export const Share: React.FC = (props) => ( + + + +); diff --git a/ui/src/components/core/icon/icon-library.tsx b/ui/src/components/core/icon/icon-library.tsx index b350ded..22de597 100644 --- a/ui/src/components/core/icon/icon-library.tsx +++ b/ui/src/components/core/icon/icon-library.tsx @@ -7,6 +7,7 @@ import { BsFillSquareFill } from '@react-icons/all-files/bs/BsFillSquareFill'; import { FaBars } from '@react-icons/all-files/fa/FaBars'; import { FaChevronRight } from '@react-icons/all-files/fa/FaChevronRight'; import { FaExternalLinkAlt } from '@react-icons/all-files/fa/FaExternalLinkAlt'; +import { HiOutlineDotsHorizontal } from '@react-icons/all-files/hi/HiOutlineDotsHorizontal'; 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'; @@ -23,7 +24,9 @@ import { FleekLogo, FleekName, MetamaskIcon, + OpenseaIcon, } from './custom'; +import { Share } from './custom/share-icon'; export const IconLibrary = Object.freeze({ back: IoArrowBackCircleSharp, @@ -43,10 +46,13 @@ export const IconLibrary = Object.freeze({ info: IoInformationCircleSharp, menu: FaBars, metamask: MetamaskIcon, //remove if not used + opensea: OpenseaIcon, search: BiSearch, square: BsFillSquareFill, + share: Share, success: AiFillCheckCircle, twitter: AiOutlineTwitter, + 'three-dots': HiOutlineDotsHorizontal, upload: IoCloudUploadSharp, verified: MdVerifiedUser, }); diff --git a/ui/src/components/core/index.ts b/ui/src/components/core/index.ts index 947be14..eb6efae 100644 --- a/ui/src/components/core/index.ts +++ b/ui/src/components/core/index.ts @@ -7,3 +7,4 @@ export * from './separator.styles'; export * from './text'; export * from './switch'; export * from './color-picker'; +export * from './menu'; diff --git a/ui/src/components/core/menu/index.ts b/ui/src/components/core/menu/index.ts new file mode 100644 index 0000000..d96f145 --- /dev/null +++ b/ui/src/components/core/menu/index.ts @@ -0,0 +1 @@ +export * from './menu'; diff --git a/ui/src/components/core/menu/menu.styles.ts b/ui/src/components/core/menu/menu.styles.ts new file mode 100644 index 0000000..179e2c8 --- /dev/null +++ b/ui/src/components/core/menu/menu.styles.ts @@ -0,0 +1,45 @@ +import { Menu } from '@headlessui/react'; + +import { styled } from '@/theme'; + +export const MenuStyles = { + Wrapper: styled('div', { + position: 'relative', + }), + Items: styled(Menu.Items, { + width: '100%', + display: 'flex', + flexDirection: 'column', + position: 'absolute', + border: '1px solid $slate6', + backgroundColor: '$black', + boxSizing: 'border-box', + left: 0, + right: 0, + top: 'calc(100% + $3)', + padding: '$3', + gap: '$2', + borderRadius: '$lg', + zIndex: '$dropdown', + maxHeight: '30vh', + overflow: 'auto', + }), + Item: styled(Menu.Item, { + width: '100%', + position: 'relative', + display: 'flex', + alignItems: 'center', + gap: '$3', + cursor: 'pointer', + padding: '$2 $3', + borderRadius: '$lg', + color: '$slate11', + transition: '$all-200', + fontSize: '$sm', + + '&[data-headlessui-state*="active"]': { + backgroundColor: '$slate2', + color: '$slate12', + }, + }), +}; diff --git a/ui/src/components/core/menu/menu.tsx b/ui/src/components/core/menu/menu.tsx new file mode 100644 index 0000000..a0753fd --- /dev/null +++ b/ui/src/components/core/menu/menu.tsx @@ -0,0 +1,41 @@ +import { Menu as MenuHeadless } from '@headlessui/react'; +import React from 'react'; + +import { forwardStyledRef } from '@/theme'; + +import { MenuStyles as MS } from './menu.styles'; + +export abstract class Menu { + static readonly Root = ({ children }: Menu.MenuProps): JSX.Element => { + return {children}; + }; + + static readonly Items = forwardStyledRef( + ({ children, ...props }, ref): JSX.Element => { + return ( + + {children.map((child, index) => ( + {child} + ))} + + ); + } + ); + + static readonly Button = MenuHeadless.Button; +} + +export namespace Menu { + export type ItemsProps = { + children: React.ReactNode[]; + } & React.ComponentPropsWithRef; + + export type Elements = { + Button: React.FC>; + Items: React.FC; + }; + + export type MenuProps = { + children: React.ReactNode; + } & React.ComponentPropsWithRef; +} diff --git a/ui/src/components/index.ts b/ui/src/components/index.ts index 62f959e..351ed16 100644 --- a/ui/src/components/index.ts +++ b/ui/src/components/index.ts @@ -6,6 +6,7 @@ export * from './spinner'; export * from './toast'; export * from './step'; export * from './nfa-card'; +export * from './nfa-icon'; export * from './nfa-preview'; export * from './card-tag'; export * from './resolved-address'; diff --git a/ui/src/components/layout/nav-bar/nav-bar.styles.ts b/ui/src/components/layout/nav-bar/nav-bar.styles.ts index aeb838b..08266c3 100644 --- a/ui/src/components/layout/nav-bar/nav-bar.styles.ts +++ b/ui/src/components/layout/nav-bar/nav-bar.styles.ts @@ -19,7 +19,6 @@ export const NavBarStyles = { content: '""', position: 'absolute', inset: 0, - backgroundColor: alphaColor('black', 0.8), backdropFilter: 'blur(4px)', zIndex: -1, }, diff --git a/ui/src/components/layout/page/app-page.tsx b/ui/src/components/layout/page/app-page.tsx index 0b2587a..ad9e81f 100644 --- a/ui/src/components/layout/page/app-page.tsx +++ b/ui/src/components/layout/page/app-page.tsx @@ -1,3 +1,4 @@ +import { App } from '@/app.context'; import { NavBar } from '@/components'; import { PageStyles as PS } from './page.styles'; @@ -7,8 +8,15 @@ export type AppPageProps = { }; export const AppPage: React.FC = ({ children }: AppPageProps) => { + const { backgroundColor } = App.useContext(); + const background = `linear-gradient(180deg, #${backgroundColor}59 0%, #000000 30%)`; + return ( - + {children} diff --git a/ui/src/components/nfa-card/nfa-card.tsx b/ui/src/components/nfa-card/nfa-card.tsx index 3883dd0..4f95991 100644 --- a/ui/src/components/nfa-card/nfa-card.tsx +++ b/ui/src/components/nfa-card/nfa-card.tsx @@ -61,8 +61,7 @@ export const NFACard: React.FC = forwardStyledRef< }} > {data.name} - {/* TODO: set correct value when it gets available on contract side */} - 0.5} /> + diff --git a/ui/src/views/access-point/nfa-icon/index.ts b/ui/src/components/nfa-icon/index.ts similarity index 100% rename from ui/src/views/access-point/nfa-icon/index.ts rename to ui/src/components/nfa-icon/index.ts diff --git a/ui/src/views/access-point/nfa-icon/nfa-icon.styles.ts b/ui/src/components/nfa-icon/nfa-icon.styles.ts similarity index 100% rename from ui/src/views/access-point/nfa-icon/nfa-icon.styles.ts rename to ui/src/components/nfa-icon/nfa-icon.styles.ts diff --git a/ui/src/components/nfa-icon/nfa-icon.tsx b/ui/src/components/nfa-icon/nfa-icon.tsx new file mode 100644 index 0000000..5c88449 --- /dev/null +++ b/ui/src/components/nfa-icon/nfa-icon.tsx @@ -0,0 +1,20 @@ +import { NFAIconStyles as NS } from './nfa-icon.styles'; + +type NFAIconProps = { + image: string; + color: string; +}; + +export const NFAIcon: React.FC = ({ + image, + color, +}: NFAIconProps) => { + return ( + + (event.currentTarget.style.display = 'none')} + /> + + ); +}; diff --git a/ui/src/constants/env.ts b/ui/src/constants/env.ts index 0ec5d8d..b1b1b98 100644 --- a/ui/src/constants/env.ts +++ b/ui/src/constants/env.ts @@ -1,4 +1,5 @@ export const env = Object.freeze({ + environment: import.meta.env.MODE, alchemy: { id: import.meta.env.VITE_ALCHEMY_API_KEY || '', appName: import.meta.env.VITE_ALCHEMY_APP_NAME || '', diff --git a/ui/src/providers/apollo-provider.tsx b/ui/src/providers/apollo-provider.tsx index 77d2d5b..5e9e9f0 100644 --- a/ui/src/providers/apollo-provider.tsx +++ b/ui/src/providers/apollo-provider.tsx @@ -46,6 +46,10 @@ const client = new ApolloClient({ keyArgs: ['where', 'orderBy', 'orderDirection'], merge: mergeByKey('id'), }, + accessPoints: { + keyArgs: ['where', 'orderBy', 'orderDirection'], + merge: mergeByKey('id'), + }, }, }, }, diff --git a/ui/src/utils/format.ts b/ui/src/utils/format.ts index d5eeb18..653b946 100644 --- a/ui/src/utils/format.ts +++ b/ui/src/utils/format.ts @@ -13,7 +13,9 @@ export const contractAddress = (address: string): string => { export const getRepositoryFromURL = (url: string): string => { const urlSplitted = url.split('/'); - return `${urlSplitted[3]}/${urlSplitted[4]}`; + return urlSplitted[3] && urlSplitted[4] + ? `${urlSplitted[3]}/${urlSplitted[4]}` + : ''; }; export const getDate = (date: number): string => { diff --git a/ui/src/views/access-point/ap-form-step/create-ap-form-body.tsx b/ui/src/views/access-point/ap-form-step/create-ap-form-body.tsx index b00c1bc..932e393 100644 --- a/ui/src/views/access-point/ap-form-step/create-ap-form-body.tsx +++ b/ui/src/views/access-point/ap-form-step/create-ap-form-body.tsx @@ -9,6 +9,7 @@ import { CardTag, Flex, Form, + NFAIcon, RowData, Spinner, Stepper, @@ -18,9 +19,9 @@ import { getNFADocument } from '@/graphclient'; import { useAppDispatch } from '@/store'; import { bunnyCDNActions, useBunnyCDNStore } from '@/store/features/bunny-cdn'; import { AppLog } from '@/utils'; +import { parseNumberToHexColor } from '@/utils/color'; import { CreateAccessPoint } from '../create-ap.context'; -import { NFAIconFragment } from '../nfa-icon'; import { useAccessPointFormContext } from './create-ap.form.context'; export const SelectedNFA: React.FC = () => { @@ -28,7 +29,12 @@ export const SelectedNFA: React.FC = () => { return ( } + leftIcon={ + + } label={nfa.name} rightComponent={Selected NFA} /> diff --git a/ui/src/views/access-point/nfa-icon/nfa-icon.tsx b/ui/src/views/access-point/nfa-icon/nfa-icon.tsx deleted file mode 100644 index 3caddb1..0000000 --- a/ui/src/views/access-point/nfa-icon/nfa-icon.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import { parseNumberToHexColor } from '@/utils/color'; - -import { NFAIconStyles as NS } from './nfa-icon.styles'; - -type NFAIconProps = { - image: string; - color: number; -}; - -export const NFAIconFragment: React.FC = ({ - image, - color, -}: NFAIconProps) => { - return ( - - - - ); -}; diff --git a/ui/src/views/explore/explore-list/nfa-list.fragment.tsx b/ui/src/views/explore/explore-list/nfa-list.fragment.tsx index 22593cf..bfe5df1 100644 --- a/ui/src/views/explore/explore-list/nfa-list.fragment.tsx +++ b/ui/src/views/explore/explore-list/nfa-list.fragment.tsx @@ -46,7 +46,9 @@ export const NFAListFragment: React.FC = () => { skip: pageNumber * pageSize, //skip is for the pagination }, onCompleted: (data) => { - if (data.tokens.length - tokens.length < pageSize) setEndReached(true); + if (data.tokens.length - tokens.length < pageSize) { + setEndReached(true); + } }, }); diff --git a/ui/src/views/explore/explore.context.tsx b/ui/src/views/explore/explore.context.tsx index a02d2e2..ea04e78 100644 --- a/ui/src/views/explore/explore.context.tsx +++ b/ui/src/views/explore/explore.context.tsx @@ -10,7 +10,7 @@ export type ExploreContext = { pageNumber: number; endReached: boolean; setSearch: (search: string) => void; - setOrderBy: (orderBy: string) => void; + setOrderBy: (orderBy: Token_orderBy) => void; setOrderDirection: (orderDirection: OrderDirection) => void; setPageNumber: (pageNumber: number) => void; setEndReached: (isEndReaced: boolean) => void; @@ -29,7 +29,7 @@ export abstract class Explore { children, }: Explore.ProviderProps) => { const [search, setSearch] = useState(''); - const [orderBy, setOrderBy] = useState('tokenId'); + const [orderBy, setOrderBy] = useState('tokenId'); const [orderDirection, setOrderDirection] = useState('desc'); const [pageNumber, setPageNumber] = useState(0); diff --git a/ui/src/views/indexed-nfa/fragments/aside.fragment.tsx b/ui/src/views/indexed-nfa/fragments/aside.fragment.tsx deleted file mode 100644 index b28c640..0000000 --- a/ui/src/views/indexed-nfa/fragments/aside.fragment.tsx +++ /dev/null @@ -1,75 +0,0 @@ -import { useEffect, useMemo, useRef, useState } from 'react'; -import { Link } from 'react-router-dom'; - -import { Button, Flex, Icon, NFAPreview } from '@/components'; -import { parseNumberToHexColor } from '@/utils/color'; - -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( - () => `#${parseNumberToHexColor(nfa.color)}`, - [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/aside/aside-buttons.fragment.tsx b/ui/src/views/indexed-nfa/fragments/aside/aside-buttons.fragment.tsx new file mode 100644 index 0000000..28867f9 --- /dev/null +++ b/ui/src/views/indexed-nfa/fragments/aside/aside-buttons.fragment.tsx @@ -0,0 +1,89 @@ +import { Button, Flex, Icon, IconName, Menu } from '@/components'; +import { env } from '@/constants'; +import { FleekERC721 } from '@/integrations/ethereum/contracts'; +import { forwardStyledRef } from '@/theme'; +import { AppLog } from '@/utils'; + +import { IndexedNFA } from '../../indexed-nfa.context'; +import { IndexedNFAStyles as S } from '../../indexed-nfa.styles'; + +const openseaLink = `https://${ + env.environment === 'development' ? 'testnets' : '' +}.opensea.io/assets/${ + env.environment === 'development' ? 'goerli' : 'ethereum' +}/${FleekERC721.address}`; + +type CustomButtonProps = { + icon: IconName; +}; + +const CustomButon = forwardStyledRef( + ({ icon, ...props }, ref) => ( + + ) +); + +type MenuItemProps = { + label: string; + iconName: IconName; + href: string; +}; + +const MenuItem: React.FC = ({ + label, + iconName, + href, +}: MenuItemProps) => { + return ( + + + + {label} + + + ); +}; + +export const ButtonsFragment: React.FC = () => { + const { nfa } = IndexedNFA.useContext(); + + const handleShareOnClick = (): void => { + const location = window.location.href; + navigator.clipboard.writeText(location); + AppLog.successToast('Link copied to clipboard'); + }; + + return ( + + + + + {/* TODO remove span and render as fragment */} + + + + + + + + + + {/* TODO add tooltip to copy link */} + + + ); +}; diff --git a/ui/src/views/indexed-nfa/fragments/aside/aside-header.fragment.tsx b/ui/src/views/indexed-nfa/fragments/aside/aside-header.fragment.tsx new file mode 100644 index 0000000..85ece31 --- /dev/null +++ b/ui/src/views/indexed-nfa/fragments/aside/aside-header.fragment.tsx @@ -0,0 +1,44 @@ +import { useMemo } from 'react'; + +import { Flex, Icon, NFAIcon, ResolvedAddress } from '@/components'; + +import { IndexedNFA } from '../../indexed-nfa.context'; +import { IndexedNFAStyles as S } from '../../indexed-nfa.styles'; + +type BadgeProps = { + verified: boolean; +}; + +const Badge: React.FC = ({ verified }: BadgeProps) => { + const text = useMemo( + () => (verified ? 'Verified' : 'Unverified'), + [verified] + ); + + const icon = useMemo(() => (verified ? 'verified' : 'error'), [verified]); + const color = useMemo(() => (verified ? '$green10' : '$red10'), [verified]); + return ( + + + {text} + + ); +}; + +export const Header: React.FC = () => { + const { nfa } = IndexedNFA.useContext(); + + return ( + + + {nfa.name} + + + + + + {nfa.owner.id} + + + ); +}; diff --git a/ui/src/views/indexed-nfa/fragments/aside/aside-nfa-info.fragment.tsx b/ui/src/views/indexed-nfa/fragments/aside/aside-nfa-info.fragment.tsx new file mode 100644 index 0000000..0759cc6 --- /dev/null +++ b/ui/src/views/indexed-nfa/fragments/aside/aside-nfa-info.fragment.tsx @@ -0,0 +1,35 @@ +import { Flex, Text } from '@/components'; +import { getDate } from '@/utils'; + +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} + +); + +export const NFAInfo: React.FC = () => { + const { nfa } = IndexedNFA.useContext(); + return ( + + + {nfa.accessPoints?.length ?? 0} + + + + + {getDate(nfa.createdAt)} + + ); +}; diff --git a/ui/src/views/indexed-nfa/fragments/aside/aside-preview.fragment.tsx b/ui/src/views/indexed-nfa/fragments/aside/aside-preview.fragment.tsx new file mode 100644 index 0000000..8facad7 --- /dev/null +++ b/ui/src/views/indexed-nfa/fragments/aside/aside-preview.fragment.tsx @@ -0,0 +1,29 @@ +import { useMemo } from 'react'; + +import { NFAPreview } from '@/components'; +import { parseNumberToHexColor } from '@/utils/color'; + +import { IndexedNFA } from '../../indexed-nfa.context'; + +export const Preview: React.FC = () => { + const { nfa } = IndexedNFA.useContext(); + + const color = useMemo( + () => `#${parseNumberToHexColor(nfa.color ?? '')}`, + [nfa] + ); + + return ( + + ); +}; diff --git a/ui/src/views/indexed-nfa/fragments/aside/aside-tabs.fragment.tsx b/ui/src/views/indexed-nfa/fragments/aside/aside-tabs.fragment.tsx new file mode 100644 index 0000000..87d0b00 --- /dev/null +++ b/ui/src/views/indexed-nfa/fragments/aside/aside-tabs.fragment.tsx @@ -0,0 +1,92 @@ +import { useMemo, useState } from 'react'; + +import { Flex } from '@/components'; +import { getRepositoryFromURL } from '@/utils'; + +import { IndexedNFA } from '../../indexed-nfa.context'; +import { IndexedNFAStyles as S } from '../../indexed-nfa.styles'; +import { Tab, TabContainer } from '../../tabs'; + +const OverviewFragment: React.FC = () => { + const { nfa } = IndexedNFA.useContext(); + + return ( + + + Token ID + {nfa.tokenId} + + + + Network + Mainnet + + + + Standard + ERC_721 + + + + Description + + + {nfa.description} + + + ); +}; + +const PropertiesFragment: React.FC = () => { + const { nfa } = IndexedNFA.useContext(); + + const traitsToShow = useMemo(() => { + return [ + [nfa.ENS, 'ENS'], + [getRepositoryFromURL(nfa.gitRepository.id), 'Repository'], + [nfa.externalURL, 'Domain'], + ]; + }, [nfa]); + + return ( + + {traitsToShow.map(([value, label], index) => ( + + + {value || '-'} + + {label} + + ))} + + ); +}; + +export const TabFragment: React.FC = () => { + const [tabSelected, setTabSelected] = useState(0); + const handleClick = (index: number): void => { + setTabSelected(index); + }; + + return ( + <> + + {['Overview', 'Properties'].map((label, index) => ( + + ))} + + {tabSelected === 0 ? : } + + ); +}; diff --git a/ui/src/views/indexed-nfa/fragments/aside/aside.fragment.tsx b/ui/src/views/indexed-nfa/fragments/aside/aside.fragment.tsx new file mode 100644 index 0000000..ae4f766 --- /dev/null +++ b/ui/src/views/indexed-nfa/fragments/aside/aside.fragment.tsx @@ -0,0 +1,48 @@ +import { useEffect, useRef, useState } from 'react'; +import { Link } from 'react-router-dom'; + +import { App } from '@/app.context'; +import { Button } from '@/components'; +import { parseNumberToHexColor } from '@/utils/color'; + +import { IndexedNFA } from '../../indexed-nfa.context'; +import { IndexedNFAStyles as S } from '../../indexed-nfa.styles'; +import { ButtonsFragment } from './aside-buttons.fragment'; +import { Header } from './aside-header.fragment'; +import { NFAInfo } from './aside-nfa-info.fragment'; +import { Preview } from './aside-preview.fragment'; +import { TabFragment } from './aside-tabs.fragment'; + +export const IndexedNFAAsideFragment: React.FC = () => { + const ref = useRef(null); + const [top, setTop] = useState(); + const { nfa } = IndexedNFA.useContext(); + + const { backgroundColor } = App.useContext(); + const background = `radial-gradient(closest-corner circle at 90% 45%, #${backgroundColor}8c 1% ,#${backgroundColor}57 20%, transparent 40%), radial-gradient(closest-corner circle at 60% 25%, #${backgroundColor} 3%, #${backgroundColor}73 30%, #181818 70%)`; + + 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 index d90e1fb..e31d087 100644 --- a/ui/src/views/indexed-nfa/fragments/index.ts +++ b/ui/src/views/indexed-nfa/fragments/index.ts @@ -1,3 +1,3 @@ -export * from './aside.fragment'; -export * from './main.fragment'; +export * from './aside/aside.fragment'; +export * from './main/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 deleted file mode 100644 index ba08ed2..0000000 --- a/ui/src/views/indexed-nfa/fragments/main.fragment.tsx +++ /dev/null @@ -1,231 +0,0 @@ -import React, { useMemo } from 'react'; - -import { Flex, Icon, IconName, ResolvedAddress, Text } from '@/components'; -import { getDate, getRepositoryFromURL, getTimeSince } from '@/utils'; - -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} - - - - - {getDate(nfa.createdAt)} - - - - - {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(); - - const traitsToShow = useMemo(() => { - return [ - [nfa.ENS, 'ENS'], - [getRepositoryFromURL(nfa.gitRepository.id), 'Repository'], - ['', 'Version'], - [nfa.externalURL, 'Domain'], - ]; - }, [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 = () => { - const { nfa } = IndexedNFA.useContext(); - - return ( - <> - Verification - - - - {nfa.verifier ? ( - {nfa.verifier?.id} - ) : ( - '-' - )} - - - {getRepositoryFromURL(nfa.gitRepository.id)} - - - - ); -}; - -const AccessPoints: React.FC = () => { - const { - nfa: { accessPoints }, - } = IndexedNFA.useContext(); - - return ( - <> - Frontends - - - - - - - - - - - - - - - Domain - Owner - Created - - - - - {accessPoints && accessPoints.length > 0 ? ( - accessPoints.map((item) => ( - - - - - {item.id} - - {item.owner.id} - - - {getTimeSince(item.createdAt)} - - - - - - )) - ) : ( - - - No results - - - )} - - - - - ); -}; - -export const IndexedNFAMainFragment: React.FC = () => { - return ( - -
- - - - - - ); -}; diff --git a/ui/src/views/indexed-nfa/fragments/main/main-ap-list.fragment.tsx b/ui/src/views/indexed-nfa/fragments/main/main-ap-list.fragment.tsx new file mode 100644 index 0000000..743a1e5 --- /dev/null +++ b/ui/src/views/indexed-nfa/fragments/main/main-ap-list.fragment.tsx @@ -0,0 +1,117 @@ +import { useQuery } from '@apollo/client'; +import { ethers } from 'ethers'; +import { useEffect } from 'react'; + +import Rectangle1 from '@/assets/Rectangle-199.png'; +import { Flex, ResolvedAddress, Text } from '@/components'; +import { + AccessPoint as AccessPointType, + getAccessPointsNFADocument, + Owner, +} from '@/graphclient'; +import { useWindowScrollEnd } from '@/hooks'; +import { AppLog, getTimeSince } from '@/utils'; + +import { IndexedNFA } from '../../indexed-nfa.context'; +import { IndexedNFAStyles as S } from '../../indexed-nfa.styles'; +import { SkeletonAccessPointsListFragment } from './skeleton.ap-list'; + +type AccessPointProps = { + data: Pick & { + owner: Pick; + }; +}; + +const AccessPoint: React.FC = ({ + data, +}: AccessPointProps) => { + const { id: name, owner, createdAt } = data; + return ( + + + + + + {name} + + + {owner.id} + + + {/* TODO get from bunny CDN */} + 220 views + + {getTimeSince(createdAt)} + + + + ); +}; + +const pageSize = 10; //Set this size to test pagination + +export const AccessPointsListFragment: React.FC = () => { + const { + nfa: { tokenId }, + orderDirection, + pageNumber, + endReached, + setEndReached, + setPageNumber, + } = IndexedNFA.useContext(); + + const handleError = (error: unknown): void => { + AppLog.errorToast( + 'There was an error trying to get the access points', + error + ); + }; + + const { + loading: isLoading, + data: { accessPoints } = { accessPoints: [] }, + error: queryError, + } = useQuery(getAccessPointsNFADocument, { + skip: tokenId === undefined, + fetchPolicy: 'cache-and-network', + variables: { + tokenId: ethers.utils.hexlify(Number(tokenId)), + orderDirection: orderDirection, + orderBy: 'createdAt', + pageSize, + skip: pageNumber * pageSize, //skip is for the pagination + }, + onCompleted(data) { + if (data.accessPoints.length - accessPoints.length < pageSize) + setEndReached(true); + }, + onError(error) { + handleError(error); + }, + }); + + useEffect(() => { + // Update page number when there are cached tokens + setPageNumber(Math.ceil(accessPoints.length / pageSize)); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + useWindowScrollEnd(() => { + if (isLoading || endReached || queryError) return; + setPageNumber(pageNumber + 1); + }); + + return ( + + {accessPoints.map((item, index) => ( + + ))} + {isLoading && } + {!isLoading && accessPoints.length === 0 && ( + +

No hosted NFAs

+
+ )} +
+ ); +}; diff --git a/ui/src/views/indexed-nfa/fragments/main/main-header.fragment.tsx b/ui/src/views/indexed-nfa/fragments/main/main-header.fragment.tsx new file mode 100644 index 0000000..feb44a8 --- /dev/null +++ b/ui/src/views/indexed-nfa/fragments/main/main-header.fragment.tsx @@ -0,0 +1,66 @@ +import { useState } from 'react'; + +import { OrderDirection } from '@/../.graphclient'; +import { Combobox, Flex } from '@/components'; +import { AppLog } from '@/utils'; + +import { IndexedNFA } from '../../indexed-nfa.context'; +import { IndexedNFAStyles as S } from '../../indexed-nfa.styles'; + +type SortItem = { + value: OrderDirection; + label: string; +}; + +const orderResults: SortItem[] = [ + { value: 'desc', label: 'Newest' }, + { value: 'asc', label: 'Oldest' }, +]; + +export const Header: React.FC = () => { + const { setPageNumber, setOrderDirection, setEndReached } = + IndexedNFA.useContext(); + const [selectedValue, setSelectedValue] = useState(orderResults[0]); + + const handleSortChange = (item: SortItem | undefined): void => { + if (item) { + setSelectedValue(item); + setPageNumber(0); + setEndReached(false); + setOrderDirection(item.value); + } else { + AppLog.errorToast('Error selecting sort option. Try again'); + } + }; + + return ( + <> + + Hosted NFAs + + {({ Field, Options }) => ( + <> + + {(selected) => selected?.label || 'Select'} + + + {(item) => item.label} + + + )} + + + + ); +}; diff --git a/ui/src/views/indexed-nfa/fragments/main/main.fragment.tsx b/ui/src/views/indexed-nfa/fragments/main/main.fragment.tsx new file mode 100644 index 0000000..2cdb4bc --- /dev/null +++ b/ui/src/views/indexed-nfa/fragments/main/main.fragment.tsx @@ -0,0 +1,12 @@ +import { IndexedNFAStyles as S } from '../../indexed-nfa.styles'; +import { AccessPointsListFragment } from './main-ap-list.fragment'; +import { Header } from './main-header.fragment'; + +export const IndexedNFAMainFragment: React.FC = () => { + return ( + +
+ + + ); +}; diff --git a/ui/src/views/indexed-nfa/fragments/main/skeleton.ap-list.tsx b/ui/src/views/indexed-nfa/fragments/main/skeleton.ap-list.tsx new file mode 100644 index 0000000..ce64a83 --- /dev/null +++ b/ui/src/views/indexed-nfa/fragments/main/skeleton.ap-list.tsx @@ -0,0 +1,21 @@ +import { IndexedNFAStyles as S } from '../../indexed-nfa.styles'; + +const SkeletonAccessPoint: React.FC = () => ( + + + + + + + + + +); + +export const SkeletonAccessPointsListFragment: React.FC = () => ( + + + + + +); diff --git a/ui/src/views/indexed-nfa/fragments/skeleton.fragment.tsx b/ui/src/views/indexed-nfa/fragments/skeleton.fragment.tsx index e1e05a4..930d451 100644 --- a/ui/src/views/indexed-nfa/fragments/skeleton.fragment.tsx +++ b/ui/src/views/indexed-nfa/fragments/skeleton.fragment.tsx @@ -1,16 +1,14 @@ import { IndexedNFAStyles as S } from '../indexed-nfa.styles'; +import { SkeletonAccessPointsListFragment } from './main/skeleton.ap-list'; export const IndexedNFASkeletonFragment: React.FC = () => ( - + - - - - + ); diff --git a/ui/src/views/indexed-nfa/indexed-nfa.context.tsx b/ui/src/views/indexed-nfa/indexed-nfa.context.tsx index fc99dac..ef044b9 100644 --- a/ui/src/views/indexed-nfa/indexed-nfa.context.tsx +++ b/ui/src/views/indexed-nfa/indexed-nfa.context.tsx @@ -1,4 +1,6 @@ -import { Owner, Token } from '@/graphclient'; +import { useState } from 'react'; + +import { OrderDirection, Owner, Token } from '@/graphclient'; import { createContext } from '@/utils'; const [Provider, useContext] = createContext({ @@ -10,7 +12,21 @@ const [Provider, useContext] = createContext({ export const IndexedNFA = { useContext, Provider: ({ children, nfa }: IndexedNFA.ProviderProps): JSX.Element => { - return {children}; + const [orderDirection, setOrderDirection] = + useState('desc'); + const [pageNumber, setPageNumber] = useState(0); + const [endReached, setEndReached] = useState(false); + + const context = { + nfa, + orderDirection, + pageNumber, + endReached, + setOrderDirection, + setPageNumber, + setEndReached, + }; + return {children}; }, }; @@ -19,6 +35,12 @@ export namespace IndexedNFA { nfa: Omit & { owner: Pick; }; + orderDirection: OrderDirection; + pageNumber: number; + endReached: boolean; + setOrderDirection: (orderDirection: OrderDirection) => void; + setPageNumber: (pageNumber: number) => void; + setEndReached: (isEndReaced: boolean) => void; }; export type ProviderProps = { diff --git a/ui/src/views/indexed-nfa/indexed-nfa.styles.ts b/ui/src/views/indexed-nfa/indexed-nfa.styles.ts index 1e1b2c4..feb521f 100644 --- a/ui/src/views/indexed-nfa/indexed-nfa.styles.ts +++ b/ui/src/views/indexed-nfa/indexed-nfa.styles.ts @@ -1,7 +1,7 @@ -import { Skeleton } from '@/components'; +import { Button, Flex, Skeleton, Text } from '@/components'; import { styled } from '@/theme'; -const Spacing = '$5'; +const Spacing = '$6'; export const IndexedNFAStyles = { Grid: styled('div', { @@ -16,9 +16,11 @@ export const IndexedNFAStyles = { gridTemplateColumns: '20rem 1fr', }, - '@media (max-width: 580px)': { + '@media (max-width: 640px)': { gridTemplateAreas: '"aside" "main"', gridTemplateColumns: '1fr', + justifyItems: 'center', + padding: '0', }, }), @@ -32,34 +34,110 @@ export const IndexedNFAStyles = { gap: Spacing, height: 'fit-content', - '@media (max-width: 580px)': { + borderRadius: '$lg', + padding: Spacing, + maxWidth: '24rem', + mixBlendMode: 'screen', + + '@media (max-width: 640px)': { position: 'static', }, }), + Header: { + Wrapper: styled(Flex, { + flexDirection: 'column', + gap: '$2h', + color: '$slate12', + }), + Container: styled(Flex, { + justifyContent: 'space-between', + alignItems: 'center', + }), + Header: styled('h1', { + fontSize: '2.125rem', + lineHeight: 1.35, + fontWeight: 700, - CreateAccessPoint: { + overflow: 'hidden', + textOverflow: 'ellipsis', + whiteSpace: 'nowrap', + }), + Badge: styled('span', { + height: 'fit-content', + width: 'fit-content', + fontSize: '$xs', + fontWeight: '$bold', + padding: '$0h $2', + borderRadius: '$full', + backgroundColor: '#131313', + display: 'flex', + gap: '$1h', + + variants: { + verified: { + true: { + color: '$green10', + }, + false: { + color: '$red10', + }, + }, + }, + }), + }, + Divider: { + Line: styled('span', { + width: '100%', + borderBottom: '1px solid $slate6', + }), + Elipse: styled('span', { + width: '0.375rem', + height: '0.375rem', + backgroundColor: '$slate8', + borderRadius: '100%', + }), + }, + Button: { + Container: styled(Flex, { + gap: '$3', + fontSize: '16px', + + [`${Button}`]: { + borderRadius: '0.375rem', + }, + }), + }, + Overview: { Container: styled('div', { display: 'flex', flexDirection: 'column', - gap: Spacing, - padding: Spacing, - backgroundColor: '$blue1', + backgroundColor: 'rgba(255, 255, 255, 0.06)', borderRadius: '$lg', + fontSize: '14px', }), - Heading: styled('h2', { - fontSize: '$md', - color: '$slate12', - }), - Text: styled('p', { - fontSize: '$sm', + Row: { + Container: styled(Flex, { + justifyContent: 'space-between', + }), + Label: styled(Text, { + color: '$slate11', + }), + Value: styled(Text, { + fontWeight: '$bold', + }), + }, + Description: styled('p', { color: '$slate11', + overflowY: 'scroll', + pr: '1rem', }), - Extra: styled('a', { + }, + Properties: { + Container: styled('div', { display: 'flex', - alignItems: 'center', - color: '$slate11', - fontSize: '$sm', - gap: '$2', + flexDirection: 'column', + gap: '$1', + padding: '$2h $4', }), }, }, @@ -70,169 +148,61 @@ export const IndexedNFAStyles = { display: 'flex', flexDirection: 'column', gap: Spacing, + width: '100%', }), - Heading: styled('h1', { - fontSize: '2.125rem', + Heading: styled('h2', { + fontSize: '1.625rem', lineHeight: 1.35, fontWeight: 700, }), - SectionHeading: styled('h2', { - fontSize: '$xl', - lineHeight: 1.2, - fontWeight: 700, - marginTop: Spacing, - }), + AccessPoint: { + List: styled('div', { + display: 'flex', + flexDirection: 'column', + gap: Spacing, + }), + Grid: styled('div', { + display: 'grid', + gridTemplateAreas: '"thumbnail data"', + gap: '$4h', + alignItems: 'center', + gridTemplateColumns: '7rem 1fr', + }), + Thumbnail: styled('div', { + gridArea: 'thumbnail', + }), + Data: { + Container: styled('div', { + gridArea: 'data', + + display: 'flex', + flexDirection: 'column', + gap: '$2', + }), + }, + Title: styled('h3', { + color: '$slate12', + fontSize: '$lg', + }), + NoResults: styled('div', { + display: 'flex', + justifyContent: 'center', + + fontSize: '$lg', + }), + }, Divider: { Line: styled('span', { width: '100%', borderBottom: '1px solid $slate6', }), Elipse: styled('span', { - width: '0.375rem', - height: '0.375rem', - backgroundColor: '$slate4', + minWidth: '0.375rem', + minHeight: '0.375rem', + backgroundColor: '$slate11', 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, { diff --git a/ui/src/views/indexed-nfa/indexed-nfa.tsx b/ui/src/views/indexed-nfa/indexed-nfa.tsx index 232909a..51fdd49 100644 --- a/ui/src/views/indexed-nfa/indexed-nfa.tsx +++ b/ui/src/views/indexed-nfa/indexed-nfa.tsx @@ -1,9 +1,12 @@ import { useQuery } from '@apollo/client'; import { ethers } from 'ethers'; +import { useEffect } from 'react'; import { useNavigate, useParams } from 'react-router-dom'; +import { App } from '@/app.context'; import { getNFADetailDocument } from '@/graphclient'; import { AppLog } from '@/utils'; +import { parseNumberToHexColor } from '@/utils/color'; import { IndexedNFAAsideFragment, @@ -15,8 +18,15 @@ import { IndexedNFAStyles as S } from './indexed-nfa.styles'; export const IndexedNFAView: React.FC = () => { const { id } = useParams<{ id: string }>(); + const { setBackgroundColor } = App.useContext(); const navigate = useNavigate(); + useEffect(() => { + return () => { + setBackgroundColor('000000'); + }; + }, [setBackgroundColor]); + const handleError = (error: unknown): void => { AppLog.errorToast( `It was not possible to find the NFA with id "${id}"`, @@ -32,6 +42,8 @@ export const IndexedNFAView: React.FC = () => { }, onCompleted(data) { if (!data.token) handleError(new Error('Token not found')); + if (data.token?.color) + setBackgroundColor(parseNumberToHexColor(data.token.color)); }, onError(error) { handleError(error); @@ -42,11 +54,6 @@ export const IndexedNFAView: React.FC = () => { return ; } - if (!data.token) { - //TODO add 404 page - return
Token not found
; - } - return ( diff --git a/ui/src/views/indexed-nfa/tabs/index.ts b/ui/src/views/indexed-nfa/tabs/index.ts new file mode 100644 index 0000000..6dd7856 --- /dev/null +++ b/ui/src/views/indexed-nfa/tabs/index.ts @@ -0,0 +1 @@ +export * from './tabs'; diff --git a/ui/src/views/indexed-nfa/tabs/tabs.styles.ts b/ui/src/views/indexed-nfa/tabs/tabs.styles.ts new file mode 100644 index 0000000..132343e --- /dev/null +++ b/ui/src/views/indexed-nfa/tabs/tabs.styles.ts @@ -0,0 +1,48 @@ +import { Flex } from '@/components'; +import { styled } from '@/theme'; + +export const TabsStyles = { + Container: styled(Flex, { + width: '100%', + }), + Tab: { + Container: styled(Flex, { + flexDirection: 'column', + flex: 1, + alignItems: 'center', + cursor: 'pointer', + + variants: { + active: { + true: { + color: 'white', + }, + false: { + color: '$slate8', + }, + }, + }, + }), + Label: styled('span', { + padding: '$2h', + }), + Line: styled('span', { + width: '100%', + borderRadius: '3px', + + variants: { + active: { + true: { + color: 'white', + borderBottom: '3px solid white', + }, + false: { + color: '$slate8', + borderBottom: '2px solid $slate8', + mt: '0.046875rem', + }, + }, + }, + }), + }, +}; diff --git a/ui/src/views/indexed-nfa/tabs/tabs.tsx b/ui/src/views/indexed-nfa/tabs/tabs.tsx new file mode 100644 index 0000000..b83b652 --- /dev/null +++ b/ui/src/views/indexed-nfa/tabs/tabs.tsx @@ -0,0 +1,35 @@ +import { forwardStyledRef } from '@/theme'; + +import { TabsStyles as S } from './tabs.styles'; + +type TabProps = { + label: string; + index: number; + onTabClick: (index: number) => void; +} & React.ComponentPropsWithRef; + +export const Tab = forwardStyledRef( + ({ label, index, onTabClick, ...props }, ref) => { + const { active } = props; + const handleClick = (): void => { + onTabClick(index); + }; + + return ( + + {label} + + + ); + } +); + +type TabContainerProps = { + children: React.ReactNode; +}; + +export const TabContainer: React.FC = ({ + children, +}: TabContainerProps) => { + return {children}; +};