Skip to content

Commit

Permalink
feat: add search functionality to NFTs (#1039)
Browse files Browse the repository at this point in the history
* feat: add search functionality to NFTs

* refactor: rename variables, improve readability

* refactor: modify search to include partial keywords

* refactor: rewrite hook and add markup prop

* refactor: remove query selector

* fix: separator for breadcrumb in dark mode

* refactor: remove breadcrumbs for popup mode
  • Loading branch information
shawnbusuttil authored May 7, 2024
1 parent dd13595 commit 4046387
Show file tree
Hide file tree
Showing 19 changed files with 643 additions and 87 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,17 @@ import { NftDetail as NftDetailView } from '@lace/core';
import { Wallet } from '@lace/cardano';
import { useTranslation } from 'react-i18next';
import { SendFlowTriggerPoints, useOutputInitialState } from '@src/views/browser-view/features/send-transaction';
import { DEFAULT_WALLET_BALANCE, SEND_NFT_DEFAULT_AMOUNT } from '@src/utils/constants';
import { APP_MODE_POPUP, DEFAULT_WALLET_BALANCE, SEND_NFT_DEFAULT_AMOUNT } from '@src/utils/constants';
import { PostHogAction } from '@providers/AnalyticsProvider/analyticsTracker';
import { useAnalyticsContext } from '@providers';
import { buttonIds } from '@hooks/useEnterKeyPress';
import { withNftsFoldersContext } from '../context';

export const NftDetail = (): React.ReactElement => {
const { inMemoryWallet } = useWalletStore();
export const NftDetail = withNftsFoldersContext((): React.ReactElement => {
const {
inMemoryWallet,
walletUI: { appMode }
} = useWalletStore();
const { t } = useTranslation();
const analytics = useAnalyticsContext();

Expand All @@ -33,12 +37,6 @@ export const NftDetail = (): React.ReactElement => {

const amount = useMemo(() => Wallet.util.calculateAssetBalance(bigintBalance, assetInfo), [assetInfo, bigintBalance]);

const nftDetailTranslation = {
tokenInformation: t('core.nftDetail.tokenInformation'),
attributes: t('core.nftDetail.attributes'),
setAsAvatar: t('core.nftDetail.setAsAvatar')
};

const handleOpenSend = () => {
// eslint-disable-next-line camelcase
analytics.sendEventToPostHog(PostHogAction.SendClick, { trigger_point: SendFlowTriggerPoints.NFTS });
Expand All @@ -64,11 +62,11 @@ export const NftDetail = (): React.ReactElement => {
{assetInfo && (
<NftDetailView
{...nftDetailSelector(assetInfo)}
isPopup={appMode === APP_MODE_POPUP}
amount={amount}
translations={nftDetailTranslation}
title={<h2 className={styles.secondaryTitle}>{assetInfo.nftMetadata?.name ?? assetInfo.fingerprint}</h2>}
/>
)}
</Drawer>
);
};
});
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
/* eslint-disable unicorn/no-useless-undefined */
/* eslint-disable unicorn/no-useless-undefined, max-statements */
import { useAssetInfo, useRedirection } from '@hooks';
import { useWalletStore } from '@src/stores';
import { Button, useObservable } from '@lace/common';
Expand All @@ -8,7 +8,7 @@ import isNil from 'lodash/isNil';
import React, { useCallback, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import styles from './Nfts.module.scss';
import { NftFolderItemProps, NftItemProps, NftList, NftListProps, NftsItemsTypes } from '@lace/core';
import { ListEmptyState, NftFolderItemProps, NftItemProps, NftList, NftListProps, NftsItemsTypes } from '@lace/core';
import { ContentLayout } from '@src/components/Layout';
import { FundWalletBanner } from '@src/views/browser-view/components';
import { walletRoutePaths } from '@routes';
Expand All @@ -23,6 +23,11 @@ import { RenameFolderType } from '@views/browser/features/nfts';
import { NftFolderConfirmationModal } from '@views/browser/features/nfts/components/NftFolderConfirmationModal';
import RemoveFolderIcon from '@assets/icons/remove-folder.component.svg';
import { useAnalyticsContext, useCurrencyStore } from '@providers';
import { SearchBox } from '@lace/ui';
import { Skeleton } from 'antd';
import { useNftSearch } from '@hooks/useNftSearch';

const MIN_ASSET_COUNT_FOR_SEARCH = 10;

export const Nfts = withNftsFoldersContext((): React.ReactElement => {
const redirectToNftDetail = useRedirection<{ params: { id: string } }>(walletRoutePaths.nftDetail);
Expand All @@ -33,6 +38,7 @@ export const Nfts = withNftsFoldersContext((): React.ReactElement => {
const { walletInfo, inMemoryWallet } = useWalletStore();
const { t } = useTranslation();
const assetsInfo = useAssetInfo();
const { isSearching, handleSearch, filteredResults } = useNftSearch(assetsInfo);
const assetsBalance = useObservable(inMemoryWallet.balance.utxo.total$, DEFAULT_WALLET_BALANCE.utxo.total$);
const analytics = useAnalyticsContext();
const { fiatCurrency } = useCurrencyStore();
Expand All @@ -44,6 +50,10 @@ export const Nfts = withNftsFoldersContext((): React.ReactElement => {
utils: { deleteRecord }
} = useNftsFoldersContext();

const [searchValue, setSearchValue] = useState('');

const [hasRecordedAnalytics, setHasRecordedAnalytics] = useState(false);

const onSelectNft = useCallback(
(nft) => {
analytics.sendEventToPostHog(PostHogAction.NFTsImageClick);
Expand Down Expand Up @@ -121,6 +131,16 @@ export const Nfts = withNftsFoldersContext((): React.ReactElement => {
setSelectedFolderId(undefined);
}, []);

const handleNftSearch = (searchItems: NftItemProps[], value: string) => {
setSearchValue(value);
if (!hasRecordedAnalytics) {
analytics.sendEventToPostHog(PostHogAction.NFTsSearchType);
setHasRecordedAnalytics(true);
}

handleSearch(searchItems, value);
};

return (
<>
<ContentLayout
Expand Down Expand Up @@ -153,7 +173,24 @@ export const Nfts = withNftsFoldersContext((): React.ReactElement => {
<div className={styles.nfts}>
<div className={styles.content} data-testid="nft-list-container">
{items.length > 0 ? (
<NftList items={items} rows={2} />
<>
{items.length >= MIN_ASSET_COUNT_FOR_SEARCH && (
<SearchBox
placeholder={t('browserView.nfts.searchPlaceholder')}
onChange={(value) => handleNftSearch(nfts, value)}
data-testid="nft-search-input"
value={searchValue}
onClear={() => setSearchValue('')}
/>
)}
<Skeleton loading={isSearching}>
{searchValue !== '' && filteredResults.length > 0 && <NftList items={filteredResults} rows={2} />}
{searchValue !== '' && filteredResults.length === 0 && (
<ListEmptyState message={t('core.assetSelectorOverlay.noMatchingResult')} icon="sad-face" />
)}
{searchValue === '' && <NftList items={items} rows={2} />}
</Skeleton>
</>
) : (
<FundWalletBanner
title={t('browserView.nfts.fundWalletBanner.title')}
Expand Down
46 changes: 46 additions & 0 deletions apps/browser-extension-wallet/src/hooks/useNftSearch.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { useMemo, useState } from 'react';
import { NftItemProps } from '@lace/core';
import { AssetOrHandleInfoMap } from './useAssetInfo';
import { Cardano } from '@cardano-sdk/core';
import debounce from 'lodash/debounce';

const DEBOUNCE_TIME = 1000;

interface NftSearchResultProps {
isSearching: boolean;
filteredResults: NftItemProps[];
handleSearch: (items: NftItemProps[], searchValue: string) => void;
}

export const searchNfts = (
data: NftItemProps[],
searchValue: string,
assetsInfo: AssetOrHandleInfoMap
): NftItemProps[] =>
data.filter(
(item) =>
item.name.toLowerCase().includes(searchValue.toLowerCase()) ||
item.assetId === searchValue ||
assetsInfo.get(Cardano.AssetId(item.assetId)).policyId === searchValue
);

export const useNftSearch = (assetsInfo: AssetOrHandleInfoMap): NftSearchResultProps => {
const [isSearching, setIsSearching] = useState(false);
const [filteredResults, setFilteredResults] = useState<NftItemProps[]>([]);
const searchDebounced = useMemo(
() =>
debounce((items: NftItemProps[], searchValue: string) => {
const filteredNfts = searchNfts(items, searchValue, assetsInfo);
setFilteredResults(filteredNfts);
setIsSearching(false);
}, DEBOUNCE_TIME),
[assetsInfo]
);

const handleSearch = (items: NftItemProps[], searchValue: string) => {
setIsSearching(true);
searchDebounced(items, searchValue);
};

return { isSearching, filteredResults, handleSearch };
};
Loading

0 comments on commit 4046387

Please sign in to comment.