Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
167 changes: 161 additions & 6 deletions client/src/components/filters/index.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { useMetadataFiltersAdapter } from "@/hooks/use-metadata-filters-adapter";
import { useMarketFilters } from "@/hooks/market-filters";
import {
MarketplaceFilters,
MarketplaceHeader,
Expand All @@ -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,
Expand All @@ -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<string>("");

// 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 = (
<UserAvatar
username={item.username || ""}
className="h-full w-full"
/>
);
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(() => {
Expand All @@ -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,
}));
};
Expand All @@ -53,7 +120,16 @@ export const Filters = () => {
const clear = useCallback(() => {
resetSelected();
setSearch({});
}, [resetSelected, setSearch]);
setPlayerSearch("");
setCollectionSearch("");
setSelected(undefined);
}, [
resetSelected,
setSearch,
setPlayerSearch,
setCollectionSearch,
setSelected,
]);

return (
<MarketplaceFilters className="h-full w-[calc(100vw-64px)] max-w-[360px] lg:flex lg:min-w-[360px] overflow-hidden">
Expand All @@ -70,6 +146,42 @@ export const Filters = () => {
onClick={() => setActive(1)}
/>
</div>

<MarketplaceHeader label="Search"></MarketplaceHeader>
<div className="w-full pb-4">
<MarketplaceSearch
search={collectionSearch}
setSearch={setCollectionSearch}
selected={undefined}
setSelected={() => {}}
options={[]}
variant="darkest"
className="w-full"
/>
</div>
{/* <MarketplaceHeader label="Owners" />
<div className="w-full pb-4">
{selected ? (
<PlayerCard
selected={selected}
usernames={usernames}
onClose={() => {
setSelected(undefined);
setPlayerSearch("");
}}
/>
) : (
<MarketplaceSearch
search={playerSearch}
setSearch={setPlayerSearch}
selected={selected}
setSelected={(selected) => setSelected(selected as SearchResult)}
options={playerOptions as SearchResult[]}
variant="darkest"
className="w-full"
/>
)}
</div> */}
<MarketplaceHeader label="Properties">
{clearable && <MarketplaceHeaderReset onClick={clear} />}
</MarketplaceHeader>
Expand Down Expand Up @@ -107,9 +219,7 @@ export const Filters = () => {
}
/>
))}
{properties.length === 0 && (
<MarketplacePropertyEmpty />
)}
{properties.length === 0 && <MarketplacePropertyEmpty />}
</div>
</MarketplacePropertyHeader>
);
Expand All @@ -118,3 +228,48 @@ export const Filters = () => {
</MarketplaceFilters>
);
};

// 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 (
// <div className="w-full outline outline-1 border-4 border-background-100 outline-background-300 flex justify-between bg-background-200 rounded px-2 py-2">
// <div className="flex items-center gap-2">
// <div className="w-6 h-6 rounded-full overflow-hidden flex-shrink-0">
// {selected.image}
// </div>
// <div className="flex-1 min-w-0">
// <p className=" font-light text-sm">{selected.label}</p>
// </div>
// </div>
// <div className="flex items-center gap-2 rounded">
// <div className="flex justify-between items-center bg-background-400 rounded py-1 pl-1 pr-2 gap-1">
// <span className="text-foreground-300 text-xs">
// <SparklesIcon variant="solid" size="xs" color="white" />
// </span>
// <span className="text-foreground-100 text-xs">
// {earnings.toLocaleString()}
// </span>
// </div>
// <button
// onClick={onClose}
// className="text-foreground-400 hover:text-foreground-200 w-5 h-5"
// >
// <XIcon size="sm" />
// </button>
// </div>
// </div>
// );
// }
51 changes: 14 additions & 37 deletions client/src/components/items/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import {
cn,
CollectibleCard,
Empty,
MarketplaceSearch,
Separator,
Skeleton,
} from "@cartridge/ui";
Expand Down Expand Up @@ -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");
Expand All @@ -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<string>("");
const [selection, setSelection] = useState<Asset[]>([]);
const parentRef = useRef<HTMLDivElement>(null);
const { chains, provider } = useArcade();
Expand All @@ -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] });
Expand All @@ -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]);
Expand All @@ -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";
Expand All @@ -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)}`;
Expand All @@ -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";
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -227,10 +214,10 @@ export function Items({ edition, collectionAddress }: { edition: EditionModel, c
/>
)}
{isConnected && selection.length > 0 ? (
<p>{`${selection.length} / ${searchFilteredTokens.length} Selected`}</p>
<p>{`${selection.length} / ${filteredTokens.length} Selected`}</p>
) : (
<>
<p>{`${searchFilteredTokens.length} ${tokens && searchFilteredTokens.length < tokens.length ? `of ${tokens.length}` : ''} Items`}</p>
<p>{`${filteredTokens.length} ${tokens && filteredTokens.length < tokens.length ? `of ${tokens.length}` : ''} Items`}</p>
{Object.keys(activeFilters).length > 0 && (
<Button
variant="ghost"
Expand All @@ -244,15 +231,6 @@ export function Items({ edition, collectionAddress }: { edition: EditionModel, c
)}
</div>
</div>
<MarketplaceSearch
search={search}
setSearch={setSearch}
selected={undefined}
setSelected={() => { }}
options={[]}
variant="darkest"
className="w-[200px] lg:w-[240px] absolute top-0 right-0 z-10"
/>
</div>
<div
ref={parentRef}
Expand All @@ -269,9 +247,9 @@ export function Items({ edition, collectionAddress }: { edition: EditionModel, c
const startIndex = virtualRow.index * 4;
const endIndex = Math.min(
startIndex + 4,
searchFilteredTokens.length
filteredTokens.length
);
const rowTokens = searchFilteredTokens.slice(startIndex, endIndex);
const rowTokens = filteredTokens.slice(startIndex, endIndex);

return (
<div
Expand Down Expand Up @@ -382,11 +360,11 @@ function Item({
const currency = token.orders[0].currency;
const metadata = erc20Metadata.find(
(m) =>
getChecksumAddress(m.l2_token_address) === getChecksumAddress(currency),
getChecksumAddress(m.l2_token_address) === getChecksumAddress(currency)
);
const image =
erc20Metadata.find(
(m) => getChecksumAddress(m.l2_token_address) === currency,
(m) => getChecksumAddress(m.l2_token_address) === currency
)?.logo_url || makeBlockie(currency);
const decimals = metadata?.decimals || 0;
const price = token.orders[0].price / 10 ** decimals;
Expand All @@ -403,7 +381,7 @@ function Item({
const metadata = erc20Metadata.find(
(m) =>
getChecksumAddress(m.l2_token_address) ===
getChecksumAddress(order.currency),
getChecksumAddress(order.currency)
);
const image = metadata?.logo_url || makeBlockie(order.currency);
const decimals = metadata?.decimals || 0;
Expand Down Expand Up @@ -466,7 +444,6 @@ const LoadingState = () => {
<div className="flex flex-col gap-y-3 lg:gap-y-4 h-full p-6">
<div className="flex justify-between items-center">
<Skeleton className="min-h-10 w-1/5" />
<Skeleton className="min-h-10 w-1/3" />
</div>
<div className="grid grid-cols-2 lg:grid-cols-4 gap-3 lg:gap-4 place-items-center select-none overflow-hidden h-full">
{Array.from({ length: 20 }).map((_, index) => (
Expand Down
Loading