diff --git a/client/src/components/filters/index.tsx b/client/src/components/filters/index.tsx index 8cdf2717..b9c89870 100644 --- a/client/src/components/filters/index.tsx +++ b/client/src/components/filters/index.tsx @@ -1,4 +1,5 @@ import { useMetadataFiltersAdapter } from "@/hooks/use-metadata-filters-adapter"; +import { useMarketFilters } from "@/hooks/market-filters"; import { MarketplaceFilters, MarketplaceHeader, @@ -7,14 +8,22 @@ import { MarketplacePropertyFilter, MarketplacePropertyHeader, MarketplaceRadialItem, + MarketplaceSearch, MarketplaceSearchEngine, + SearchResult, } from "@cartridge/ui"; -import { useCallback, useMemo, useState } from "react"; +import { useCallback, useEffect, useMemo, useState } from "react"; +import { useProject } from "@/hooks/project"; +import { useBalances } from "@/hooks/market-collections"; +import { useUsernames } from "@/hooks/account"; +import { UserAvatar } from "@/components/user/avatar"; export const Filters = () => { const { active, setActive, + collectionSearch, + setCollectionSearch, filteredMetadata, clearable, addSelected, @@ -23,7 +32,65 @@ export const Filters = () => { precomputedAttributes, precomputedProperties, } = useMetadataFiltersAdapter(); + + const { selected, setSelected } = useMarketFilters(); const [search, setSearch] = useState<{ [key: string]: string }>({}); + const [playerSearch, setPlayerSearch] = useState(""); + + // Player search functionality + const { collection: collectionAddress, filter } = useProject(); + const { balances } = useBalances(collectionAddress || "", 1000); + + const accounts = useMemo(() => { + if (!balances || balances.length === 0) return []; + const owners = balances + .filter((balance) => parseInt(balance.balance, 16) > 0) + .map((balance) => `0x${BigInt(balance.account_address).toString(16)}`); + return Array.from(new Set(owners)); + }, [balances]); + + const { usernames } = useUsernames({ addresses: accounts }); + + const searchResults = useMemo(() => { + return usernames + .filter((item) => !!item.username) + .map((item) => { + const image = ( + + ); + return { + image, + label: item.username, + }; + }); + }, [usernames]); + + const playerOptions = useMemo(() => { + if (!playerSearch) return []; + return searchResults + .filter((item) => + item.label?.toLowerCase().startsWith(playerSearch.toLowerCase()) + ) + .slice(0, 3); + }, [searchResults, playerSearch]); + + useEffect(() => { + const selection = searchResults.find( + (option) => option.label?.toLowerCase() === filter?.toLowerCase() + ); + if ( + !filter || + !searchResults.length || + selected?.label === selection?.label + ) + return; + if (selection) { + setSelected(selection as SearchResult); + } + }, [filter, searchResults, setSelected, selected]); // Build filtered properties with search and dynamic counts const getFilteredProperties = useMemo(() => { @@ -44,7 +111,7 @@ export const Filters = () => { order: prop.order, count: filteredMetadata.find( - (m) => m.trait_type === attribute && m.value === prop.property, + (m) => m.trait_type === attribute && m.value === prop.property )?.tokens.length || 0, })); }; @@ -53,7 +120,16 @@ export const Filters = () => { const clear = useCallback(() => { resetSelected(); setSearch({}); - }, [resetSelected, setSearch]); + setPlayerSearch(""); + setCollectionSearch(""); + setSelected(undefined); + }, [ + resetSelected, + setSearch, + setPlayerSearch, + setCollectionSearch, + setSelected, + ]); return ( @@ -70,6 +146,42 @@ export const Filters = () => { onClick={() => setActive(1)} /> + + +
+ {}} + options={[]} + variant="darkest" + className="w-full" + /> +
+ {/* +
+ {selected ? ( + { + setSelected(undefined); + setPlayerSearch(""); + }} + /> + ) : ( + setSelected(selected as SearchResult)} + options={playerOptions as SearchResult[]} + variant="darkest" + className="w-full" + /> + )} +
*/} {clearable && } @@ -107,9 +219,7 @@ export const Filters = () => { } /> ))} - {properties.length === 0 && ( - - )} + {properties.length === 0 && } ); @@ -118,3 +228,48 @@ export const Filters = () => {
); }; + +// function PlayerCard({ +// selected, +// onClose, +// usernames, +// }: { +// selected: SearchResult; +// onClose: () => void; +// usernames: { username: string | undefined; address: string | undefined }[]; +// }) { +// const account = usernames.find( +// (item) => item.username === selected?.label +// )?.address; + +// const { earnings } = usePlayerStats(account || undefined); + +// return ( +//
+//
+//
+// {selected.image} +//
+//
+//

{selected.label}

+//
+//
+//
+//
+// +// +// +// +// {earnings.toLocaleString()} +// +//
+// +//
+//
+// ); +// } diff --git a/client/src/components/items/index.tsx b/client/src/components/items/index.tsx index 2220b3ba..d55a85c7 100644 --- a/client/src/components/items/index.tsx +++ b/client/src/components/items/index.tsx @@ -4,7 +4,6 @@ import { cn, CollectibleCard, Empty, - MarketplaceSearch, Separator, Skeleton, } from "@cartridge/ui"; @@ -41,11 +40,11 @@ const getEntrypoints = async (provider: RpcProvider, address: string) => { const code = await provider.getClassAt(address); if (!code) return; const interfaces = code.abi.filter( - (element) => element.type === "interface", + (element) => element.type === "interface" ); if (interfaces.length > 0) { return interfaces.flatMap((element: InterfaceAbi) => - element.items.map((item: FunctionAbi) => item.name), + element.items.map((item: FunctionAbi) => item.name) ); } const functions = code.abi.filter((element) => element.type === "function"); @@ -59,7 +58,6 @@ export function Items({ edition, collectionAddress }: { edition: EditionModel, c const { connector, address, isConnected } = useAccount(); const { connect, connectors } = useConnect(); const { sales, getCollectionOrders } = useMarketplace(); - const [search, setSearch] = useState(""); const [selection, setSelection] = useState([]); const parentRef = useRef(null); const { chains, provider } = useArcade(); @@ -83,17 +81,6 @@ export function Items({ edition, collectionAddress }: { edition: EditionModel, c address: collectionAddress }); - // Apply search filtering on top of metadata filters - const searchFilteredTokens = useMemo(() => { - if (!search.trim()) return filteredTokens; - - const searchLower = search.toLowerCase(); - - return filteredTokens.filter(token => { - const tokenName = (token.metadata as any)?.name || token.name || ''; - return tokenName.toLowerCase().includes(searchLower); - }); - }, [filteredTokens, search]); const connectWallet = useCallback(async () => { connect({ connector: connectors[0] }); @@ -102,7 +89,7 @@ export function Items({ edition, collectionAddress }: { edition: EditionModel, c const chain: Chain = useMemo(() => { return ( chains.find( - (chain) => chain.rpcUrls.default.http[0] === edition?.config.rpc, + (chain) => chain.rpcUrls.default.http[0] === edition?.config.rpc ) || mainnet ); }, [chains, edition]); @@ -124,7 +111,7 @@ export function Items({ edition, collectionAddress }: { edition: EditionModel, c const entrypoints = await getEntrypoints( provider.provider, - contractAddress, + contractAddress ); const isERC1155 = entrypoints?.includes(ERC1155_ENTRYPOINT); const subpath = isERC1155 ? "collectible" : "collection"; @@ -151,7 +138,7 @@ export function Items({ edition, collectionAddress }: { edition: EditionModel, c if (!isConnected || !connector) return; const orders = tokens.map((token) => token.orders).flat(); const contractAddresses = new Set( - tokens.map((token) => token.contract_address), + tokens.map((token) => token.contract_address) ); if (!edition || contractAddresses.size !== 1) return; const contractAddress = `0x${BigInt(Array.from(contractAddresses)[0]).toString(16)}`; @@ -164,7 +151,7 @@ export function Items({ edition, collectionAddress }: { edition: EditionModel, c const entrypoints = await getEntrypoints( provider.provider, - contractAddress, + contractAddress ); const isERC1155 = entrypoints?.includes(ERC1155_ENTRYPOINT); const subpath = isERC1155 ? "collectible" : "collection"; @@ -196,7 +183,7 @@ export function Items({ edition, collectionAddress }: { edition: EditionModel, c // Set up virtualizer for rows const virtualizer = useVirtualizer({ - count: searchFilteredTokens.length, + count: filteredTokens.length, getScrollElement: () => parentRef.current, estimateSize: () => ROW_HEIGHT + 16, // ROW_HEIGHT + gap overscan: 2, @@ -227,10 +214,10 @@ export function Items({ edition, collectionAddress }: { edition: EditionModel, c /> )} {isConnected && selection.length > 0 ? ( -

{`${selection.length} / ${searchFilteredTokens.length} Selected`}

+

{`${selection.length} / ${filteredTokens.length} Selected`}

) : ( <> -

{`${searchFilteredTokens.length} ${tokens && searchFilteredTokens.length < tokens.length ? `of ${tokens.length}` : ''} Items`}

+

{`${filteredTokens.length} ${tokens && filteredTokens.length < tokens.length ? `of ${tokens.length}` : ''} Items`}

{Object.keys(activeFilters).length > 0 && (