Skip to content

Commit

Permalink
Add list component (#15)
Browse files Browse the repository at this point in the history
* Fix nested routes rerender

* Add list component
  • Loading branch information
nikitayutanov authored Feb 16, 2024
1 parent bbde8e8 commit 0b4826f
Show file tree
Hide file tree
Showing 15 changed files with 197 additions and 104 deletions.
7 changes: 2 additions & 5 deletions frontend/src/App.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<>
<Header />

<main>
{/* key to reset on route change */}
<ErrorBoundary key={pathname}>
<ErrorBoundary>
<ScrollRestoration />

<Outlet />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { Link } from 'react-router-dom';

import { cx } from '@/utils';

type Props = ButtonProps & {
type Props = Omit<ButtonProps, 'onClick'> & {
to: string;
};

Expand Down
2 changes: 2 additions & 0 deletions frontend/src/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -35,6 +36,7 @@ export {
LinkButton,
ErrorBoundary,
Skeleton,
List,
};

export type { InfoCardProps };
38 changes: 30 additions & 8 deletions frontend/src/components/layout/error-boundary/error-boundary.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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 (
<Container>
<h2 className={styles.heading}>Oops! Something went wrong:</h2>
<p className={styles.error}>{message}</p>

<BackButton size="small" />
</Container>
);
}

class ErrorBoundary extends Component<Props, State> {
constructor(props: Props) {
super(props);
Expand All @@ -23,17 +48,14 @@ class ErrorBoundary extends Component<Props, State> {
return { error };
}

reset = () => {
this.setState({ error: null });
};

render() {
if (!this.state.error) return this.props.children;

return (
<Container>
<h2 className={styles.heading}>Oops! Something went wrong:</h2>
<p className={styles.error}>{this.state.error.message}</p>

<BackButton size="small" />
</Container>
);
return <Fallback message={this.state.error.message} reset={this.reset} />;
}
}

Expand Down
3 changes: 3 additions & 0 deletions frontend/src/components/list/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { List } from './list';

export { List };
30 changes: 30 additions & 0 deletions frontend/src/components/list/list.module.scss
Original file line number Diff line number Diff line change
@@ -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);
}
}
34 changes: 34 additions & 0 deletions frontend/src/components/list/list.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { ReactNode } from 'react';

import styles from './list.module.scss';
import NotFoundSVG from './not-found.svg?react';

type Props<T> = {
items: T[] | undefined;
itemsPerRow: number;
emptyText: string;
renderItem: (item: T, index: number) => ReactNode;
};

function List<T>({ items, itemsPerRow, emptyText, renderItem }: Props<T>) {
if (!items) return null;

const renderItems = () => items?.map((item, index) => renderItem(item, index));

return items.length ? (
<ul className={styles.list} style={{ gridTemplateColumns: `repeat(${itemsPerRow}, 1fr)` }}>
{renderItems()}
</ul>
) : (
<div className={styles.notFound}>
<NotFoundSVG />

<h3 className={styles.heading}>Oops, Nothing Found!</h3>
<p className={styles.text}>
Looks like we&apos;re on a wild goose chase! {emptyText} to have them displayed here.
</p>
</div>
);
}

export { List };
File renamed without changes
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
22 changes: 2 additions & 20 deletions frontend/src/features/create-simple-collection/hooks.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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 };
3 changes: 2 additions & 1 deletion frontend/src/features/lists/index.ts
Original file line number Diff line number Diff line change
@@ -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 };
11 changes: 10 additions & 1 deletion frontend/src/hooks/index.ts
Original file line number Diff line number Diff line change
@@ -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,
};
21 changes: 21 additions & 0 deletions frontend/src/hooks/use-change-effect.ts
Original file line number Diff line number Diff line change
@@ -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 };
59 changes: 20 additions & 39 deletions frontend/src/pages/collection/collection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -29,7 +28,7 @@ type Params = {
id: string;
};

const NFT_SKELETONS = new Array(4).fill(null);
const NFT_SKELETONS = new Array<null>(4).fill(null);

function Collection() {
const { id } = useParams() as Params;
Expand All @@ -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 (
<ul className={cx(styles.list, styles[gridSize])}>
{NFT_SKELETONS.map((_nft, index) => (
<Skeleton key={index}>
<NFTCardSkeletonSVG />
</Skeleton>
))}
</ul>
);

if (!searchedTokens.length)
return (
<div className={styles.notFound}>
<NotFoundSVG />

<p className={styles.notFoundHeading}>Oops, Nothing Found!</p>
<p className={styles.notFoundText}>
Looks like we&apos;re on a wild goose chase! Mint NFTs to have them displayed here.
</p>
</div>
);

return (
<ul className={cx(styles.list, styles[gridSize])}>
{searchedTokens.map((nft) => (
<NFTCard key={nft.id} {...{ ...nft, collection }} />
))}
</ul>
);
};

const socialEntries = Object.entries(additionalLinks || {});

const renderSocials = () =>
Expand Down Expand Up @@ -141,7 +107,22 @@ function Collection() {
</div>
</header>

{renderNFTs()}
<List
items={searchedTokens || NFT_SKELETONS}
itemsPerRow={gridSize === GRID_SIZE.SMALL ? 4 : 3}
emptyText="Mint NFTs"
renderItem={(nft, index) =>
nft && collection ? (
<NFTCard key={nft.id} {...{ ...nft, collection }} />
) : (
<li key={index}>
<Skeleton>
<NFTCardSkeletonSVG />
</Skeleton>
</li>
)
}
/>
</div>
</Container>
);
Expand Down
Loading

0 comments on commit 0b4826f

Please sign in to comment.