From 0b4826fdc94da340fedeb45ec0ceb9395fc0a82a Mon Sep 17 00:00:00 2001 From: Nikita Yutanov Date: Sat, 17 Feb 2024 00:12:27 +0300 Subject: [PATCH] Add list component (#15) * Fix nested routes rerender * Add list component --- frontend/src/App.tsx | 7 +- .../buttons/link-button/link-button.tsx | 2 +- frontend/src/components/index.ts | 2 + .../layout/error-boundary/error-boundary.tsx | 38 ++++++++--- frontend/src/components/list/index.ts | 3 + frontend/src/components/list/list.module.scss | 30 +++++++++ frontend/src/components/list/list.tsx | 34 ++++++++++ .../assets => components/list}/not-found.svg | 0 .../parameters-form/parameters-form.tsx | 2 +- .../create-simple-collection/hooks.ts | 22 +----- frontend/src/features/lists/index.ts | 3 +- frontend/src/hooks/index.ts | 11 ++- frontend/src/hooks/use-change-effect.ts | 21 ++++++ frontend/src/pages/collection/collection.tsx | 59 ++++++---------- frontend/src/pages/lists/lists.tsx | 67 +++++++++++-------- 15 files changed, 197 insertions(+), 104 deletions(-) create mode 100644 frontend/src/components/list/index.ts create mode 100644 frontend/src/components/list/list.module.scss create mode 100644 frontend/src/components/list/list.tsx rename frontend/src/{pages/collection/assets => components/list}/not-found.svg (100%) create mode 100644 frontend/src/hooks/use-change-effect.ts diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index db46df5..383e52b 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,18 +1,15 @@ -import { Outlet, ScrollRestoration, useLocation } from 'react-router-dom'; +import { Outlet, ScrollRestoration } from 'react-router-dom'; import { ErrorBoundary, Footer, Header } from './components'; import { withProviders } from './providers'; function Component() { - const { pathname } = useLocation(); - return ( <>
- {/* key to reset on route change */} - + diff --git a/frontend/src/components/buttons/link-button/link-button.tsx b/frontend/src/components/buttons/link-button/link-button.tsx index 6c18876..0b9212f 100644 --- a/frontend/src/components/buttons/link-button/link-button.tsx +++ b/frontend/src/components/buttons/link-button/link-button.tsx @@ -3,7 +3,7 @@ import { Link } from 'react-router-dom'; import { cx } from '@/utils'; -type Props = ButtonProps & { +type Props = Omit & { to: string; }; diff --git a/frontend/src/components/index.ts b/frontend/src/components/index.ts index f248267..15ee641 100644 --- a/frontend/src/components/index.ts +++ b/frontend/src/components/index.ts @@ -4,6 +4,7 @@ import { withAccount, withApi } from './hocs'; import { InfoCard, InfoCardProps } from './info-card'; import { PriceInput, SearchInput } from './inputs'; import { Container, Footer, Header, ErrorBoundary, PrivateRoute, Breadcrumbs, Skeleton } from './layout'; +import { List } from './list'; import { NFTActionFormModal } from './nft-action-form-modal'; import { PriceInfoCard } from './price-info-card'; import { ResponsiveSquareImage } from './responsive-square-image'; @@ -35,6 +36,7 @@ export { LinkButton, ErrorBoundary, Skeleton, + List, }; export type { InfoCardProps }; diff --git a/frontend/src/components/layout/error-boundary/error-boundary.tsx b/frontend/src/components/layout/error-boundary/error-boundary.tsx index f4d54d7..4756d9f 100644 --- a/frontend/src/components/layout/error-boundary/error-boundary.tsx +++ b/frontend/src/components/layout/error-boundary/error-boundary.tsx @@ -1,4 +1,7 @@ import { Component, ReactNode } from 'react'; +import { useLocation } from 'react-router-dom'; + +import { useChangeEffect } from '@/hooks'; import { BackButton } from '../../buttons'; import { Container } from '../container'; @@ -9,10 +12,32 @@ type Props = { children: ReactNode; }; +type FallbackProps = { + message: string; + reset: () => void; +}; + type State = { error: Error | null; }; +function Fallback({ message, reset }: FallbackProps) { + const { pathname } = useLocation(); + + useChangeEffect(() => { + reset(); + }, [pathname]); + + return ( + +

Oops! Something went wrong:

+

{message}

+ + +
+ ); +} + class ErrorBoundary extends Component { constructor(props: Props) { super(props); @@ -23,17 +48,14 @@ class ErrorBoundary extends Component { return { error }; } + reset = () => { + this.setState({ error: null }); + }; + render() { if (!this.state.error) return this.props.children; - return ( - -

Oops! Something went wrong:

-

{this.state.error.message}

- - -
- ); + return ; } } diff --git a/frontend/src/components/list/index.ts b/frontend/src/components/list/index.ts new file mode 100644 index 0000000..80f7134 --- /dev/null +++ b/frontend/src/components/list/index.ts @@ -0,0 +1,3 @@ +import { List } from './list'; + +export { List }; diff --git a/frontend/src/components/list/list.module.scss b/frontend/src/components/list/list.module.scss new file mode 100644 index 0000000..c3b72da --- /dev/null +++ b/frontend/src/components/list/list.module.scss @@ -0,0 +1,30 @@ +.list { + display: grid; + gap: 32px 16px; +} + +.notFound { + margin: 0 auto; + padding: 100px; + + display: flex; + align-items: center; + flex-direction: column; + gap: 16px; + + text-align: center; + + .heading { + font-size: 32px; + font-weight: 600; + line-height: 35px; + letter-spacing: 0.01em; + } + + .text { + font-size: 16px; + font-weight: 500; + line-height: 24px; + color: rgba(#000, 0.7); + } +} diff --git a/frontend/src/components/list/list.tsx b/frontend/src/components/list/list.tsx new file mode 100644 index 0000000..7ca9334 --- /dev/null +++ b/frontend/src/components/list/list.tsx @@ -0,0 +1,34 @@ +import { ReactNode } from 'react'; + +import styles from './list.module.scss'; +import NotFoundSVG from './not-found.svg?react'; + +type Props = { + items: T[] | undefined; + itemsPerRow: number; + emptyText: string; + renderItem: (item: T, index: number) => ReactNode; +}; + +function List({ items, itemsPerRow, emptyText, renderItem }: Props) { + if (!items) return null; + + const renderItems = () => items?.map((item, index) => renderItem(item, index)); + + return items.length ? ( +
    + {renderItems()} +
+ ) : ( +
+ + +

Oops, Nothing Found!

+

+ Looks like we're on a wild goose chase! {emptyText} to have them displayed here. +

+
+ ); +} + +export { List }; diff --git a/frontend/src/pages/collection/assets/not-found.svg b/frontend/src/components/list/not-found.svg similarity index 100% rename from frontend/src/pages/collection/assets/not-found.svg rename to frontend/src/components/list/not-found.svg diff --git a/frontend/src/features/create-simple-collection/components/parameters-form/parameters-form.tsx b/frontend/src/features/create-simple-collection/components/parameters-form/parameters-form.tsx index 42289d2..c260627 100644 --- a/frontend/src/features/create-simple-collection/components/parameters-form/parameters-form.tsx +++ b/frontend/src/features/create-simple-collection/components/parameters-form/parameters-form.tsx @@ -6,10 +6,10 @@ import { z } from 'zod'; import VaraSVG from '@/assets/vara.svg?react'; import { Container } from '@/components'; +import { useChangeEffect } from '@/hooks'; import CrossSVG from '../../assets/cross-tag.svg?react'; import PercentSVG from '../../assets/percent.svg?react'; -import { useChangeEffect } from '../../hooks'; import { ParametersValues } from '../../types'; import styles from './parameters-form.module.scss'; diff --git a/frontend/src/features/create-simple-collection/hooks.ts b/frontend/src/features/create-simple-collection/hooks.ts index ff289c9..1771c73 100644 --- a/frontend/src/features/create-simple-collection/hooks.ts +++ b/frontend/src/features/create-simple-collection/hooks.ts @@ -1,5 +1,5 @@ import { useAlert } from '@gear-js/react-hooks'; -import { useRef, useEffect, useState, ChangeEvent, DependencyList, EffectCallback } from 'react'; +import { useRef, useState, ChangeEvent } from 'react'; import { MAX_IMAGE_SIZE_MB } from './consts'; import { getBytesSize } from './utils'; @@ -52,22 +52,4 @@ function useImageInput(defaultValue: File | undefined, types: string[]) { return { value, props, handleClick, handleReset }; } -function useChangeEffect(callback: EffectCallback, dependencies?: DependencyList) { - const mounted = useRef(false); - - useEffect( - () => () => { - mounted.current = false; - }, - [], - ); - - useEffect(() => { - if (mounted.current) return callback(); - - mounted.current = true; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, dependencies); -} - -export { useImageInput, useChangeEffect }; +export { useImageInput }; diff --git a/frontend/src/features/lists/index.ts b/frontend/src/features/lists/index.ts index 32cb980..0c78f82 100644 --- a/frontend/src/features/lists/index.ts +++ b/frontend/src/features/lists/index.ts @@ -1,4 +1,5 @@ import { GridSize } from './components'; +import { GRID_SIZE } from './consts'; import { useGridSize } from './hooks'; -export { GridSize, useGridSize }; +export { GridSize, GRID_SIZE, useGridSize }; diff --git a/frontend/src/hooks/index.ts b/frontend/src/hooks/index.ts index a09a1bc..c262f3a 100644 --- a/frontend/src/hooks/index.ts +++ b/frontend/src/hooks/index.ts @@ -1,6 +1,15 @@ import { useMarketplaceMessage, useCollectionMessage, useApprovedMessage } from './api'; +import { useChangeEffect } from './use-change-effect'; import { useIsOwner } from './use-is-owner'; import { useLoading } from './use-loading'; import { useModal } from './use-modal'; -export { useMarketplaceMessage, useCollectionMessage, useApprovedMessage, useModal, useIsOwner, useLoading }; +export { + useMarketplaceMessage, + useCollectionMessage, + useApprovedMessage, + useModal, + useIsOwner, + useLoading, + useChangeEffect, +}; diff --git a/frontend/src/hooks/use-change-effect.ts b/frontend/src/hooks/use-change-effect.ts new file mode 100644 index 0000000..6349ec9 --- /dev/null +++ b/frontend/src/hooks/use-change-effect.ts @@ -0,0 +1,21 @@ +import { EffectCallback, DependencyList, useRef, useEffect } from 'react'; + +function useChangeEffect(callback: EffectCallback, dependencies?: DependencyList) { + const mounted = useRef(false); + + useEffect( + () => () => { + mounted.current = false; + }, + [], + ); + + useEffect(() => { + if (mounted.current) return callback(); + + mounted.current = true; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, dependencies); +} + +export { useChangeEffect }; diff --git a/frontend/src/pages/collection/collection.tsx b/frontend/src/pages/collection/collection.tsx index d5573c1..84f5540 100644 --- a/frontend/src/pages/collection/collection.tsx +++ b/frontend/src/pages/collection/collection.tsx @@ -2,15 +2,14 @@ import { useState } from 'react'; import { useForm } from 'react-hook-form'; import { generatePath, useParams } from 'react-router-dom'; -import { Breadcrumbs, Container, FilterButton, InfoCard, SearchInput } from '@/components'; +import { Breadcrumbs, Container, FilterButton, InfoCard, List, SearchInput } from '@/components'; import { ROUTE } from '@/consts'; import { MintLimitInfoCard, MintNFT, NFTCard, Skeleton } from '@/features/collections'; import CollectionHeaderSkeletonSVG from '@/features/collections/assets/collection-header-skeleton.svg?react'; import NFTCardSkeletonSVG from '@/features/collections/assets/nft-card-skeleton.svg?react'; -import { GridSize, useGridSize } from '@/features/lists'; -import { cx, getIpfsLink } from '@/utils'; +import { GRID_SIZE, GridSize, useGridSize } from '@/features/lists'; +import { getIpfsLink } from '@/utils'; -import NotFoundSVG from './assets/not-found.svg?react'; import UserSVG from './assets/user.svg?react'; import styles from './collection.module.scss'; import { SOCIAL_ICON } from './consts'; @@ -29,7 +28,7 @@ type Params = { id: string; }; -const NFT_SKELETONS = new Array(4).fill(null); +const NFT_SKELETONS = new Array(4).fill(null); function Collection() { const { id } = useParams() as Params; @@ -43,39 +42,6 @@ function Collection() { const searchedTokens = nfts?.filter((nft) => nft.name.toLocaleLowerCase().includes(query)); const tokensCount = searchedTokens?.length || 0; - const renderNFTs = () => { - if (!searchedTokens || !collection) - return ( -
    - {NFT_SKELETONS.map((_nft, index) => ( - - - - ))} -
- ); - - if (!searchedTokens.length) - return ( -
- - -

Oops, Nothing Found!

-

- Looks like we're on a wild goose chase! Mint NFTs to have them displayed here. -

-
- ); - - return ( -
    - {searchedTokens.map((nft) => ( - - ))} -
- ); - }; - const socialEntries = Object.entries(additionalLinks || {}); const renderSocials = () => @@ -141,7 +107,22 @@ function Collection() {
- {renderNFTs()} + + nft && collection ? ( + + ) : ( +
  • + + + +
  • + ) + } + /> ); diff --git a/frontend/src/pages/lists/lists.tsx b/frontend/src/pages/lists/lists.tsx index 6ded8dd..70c8ff9 100644 --- a/frontend/src/pages/lists/lists.tsx +++ b/frontend/src/pages/lists/lists.tsx @@ -1,11 +1,12 @@ import { Link, useLocation, useMatch } from 'react-router-dom'; -import { Container } from '@/components'; +import { Container, List } from '@/components'; import { ROUTE } from '@/consts'; import { CollectionCard, NFTCard, Skeleton } from '@/features/collections'; import CollectionCardSkeletonSVG from '@/features/collections/assets/collection-card-skeleton.svg?react'; import NFTCardSkeletonSVG from '@/features/collections/assets/nft-card-skeleton.svg?react'; import { GridSize, useGridSize } from '@/features/lists'; +import { GRID_SIZE } from '@/features/lists/consts'; import { cx } from '@/utils'; import { useCollections, useNFTs } from './hooks'; @@ -16,8 +17,8 @@ const TABS = [ { to: ROUTE.NFTS, text: 'NFTs' }, ]; -const COLLECTION_SKELETONS = new Array(6).fill(null); -const NFT_SKELETONS = new Array(8).fill(null); +const COLLECTION_SKELETONS = new Array(6).fill(null); +const NFT_SKELETONS = new Array(8).fill(null); function Lists() { const { pathname } = useLocation(); @@ -41,28 +42,6 @@ function Lists() { ); }); - const renderCollections = () => - collections - ? collections.map((collection) => ) - : COLLECTION_SKELETONS.map((_collection, index) => ( -
  • - - - -
  • - )); - - const renderNFTs = () => - nfts - ? nfts.map(({ id, ...nft }) => ) - : NFT_SKELETONS.map((_nft, index) => ( -
  • - - - -
  • - )); - return (
    @@ -70,9 +49,41 @@ function Lists() {
    -
      - {(match ? renderNFTs : renderCollections)()} -
    + {match ? ( + + nft ? ( + + ) : ( +
  • + + + +
  • + ) + } + /> + ) : ( + + collection ? ( + + ) : ( +
  • + + + +
  • + ) + } + /> + )}
    ); }