From 938aa22a748af090141c84b9f1e3fe016672b084 Mon Sep 17 00:00:00 2001 From: Pablo Pettinari Date: Mon, 8 Sep 2025 22:04:08 +0200 Subject: [PATCH 01/21] optimize WalletInfo --- .../FindWalletProductTable/WalletInfo.tsx | 129 +++++++++++------- .../FindWalletProductTable/index.tsx | 1 - 2 files changed, 81 insertions(+), 49 deletions(-) diff --git a/src/components/FindWalletProductTable/WalletInfo.tsx b/src/components/FindWalletProductTable/WalletInfo.tsx index 8bfb24ccc17..e1ce0834c17 100644 --- a/src/components/FindWalletProductTable/WalletInfo.tsx +++ b/src/components/FindWalletProductTable/WalletInfo.tsx @@ -1,6 +1,7 @@ +import { memo, useMemo } from "react" import { ChevronDown, ChevronUp } from "lucide-react" -import { ChainName, Wallet } from "@/lib/types" +import type { ChainName, Wallet } from "@/lib/types" import { ChainImages } from "@/components/ChainImages" import { DevicesIcon, LanguagesIcon } from "@/components/icons/wallets" @@ -10,6 +11,8 @@ import { Tag } from "@/components/ui/tag" import { formatStringList, getWalletPersonas } from "@/lib/utils/wallets" +import { NUMBER_OF_SUPPORTED_LANGUAGES_SHOWN } from "@/lib/constants" + import { ButtonLink } from "../ui/buttons/Button" import { useTranslation } from "@/hooks/useTranslation" @@ -21,22 +24,69 @@ interface WalletInfoProps { const WalletInfo = ({ wallet, isExpanded }: WalletInfoProps) => { const { t } = useTranslation("page-wallets-find-wallet") - const walletPersonas = getWalletPersonas(wallet) - const deviceLabels: Array = [] - - wallet.ios && deviceLabels.push(t("page-find-wallet-iOS")) - wallet.android && deviceLabels.push(t("page-find-wallet-android")) - wallet.linux && deviceLabels.push(t("page-find-wallet-linux")) - wallet.windows && deviceLabels.push(t("page-find-wallet-windows")) - wallet.macOS && deviceLabels.push(t("page-find-wallet-macOS")) - wallet.chromium && deviceLabels.push(t("page-find-wallet-chromium")) - wallet.firefox && deviceLabels.push(t("page-find-wallet-firefox")) - wallet.hardware && deviceLabels.push(t("page-find-wallet-hardware")) + + const walletPersonas = useMemo(() => { + return getWalletPersonas(wallet) + }, [wallet]) + + const deviceLabels = useMemo(() => { + const labels: Array = [] + if (wallet.ios) labels.push(t("page-find-wallet-iOS")) + if (wallet.android) labels.push(t("page-find-wallet-android")) + if (wallet.linux) labels.push(t("page-find-wallet-linux")) + if (wallet.windows) labels.push(t("page-find-wallet-windows")) + if (wallet.macOS) labels.push(t("page-find-wallet-macOS")) + if (wallet.chromium) labels.push(t("page-find-wallet-chromium")) + if (wallet.firefox) labels.push(t("page-find-wallet-firefox")) + if (wallet.hardware) labels.push(t("page-find-wallet-hardware")) + return labels + }, [wallet, t]) + + const deviceLabelsText = useMemo(() => { + return deviceLabels.join(" · ") + }, [deviceLabels]) + + const formattedLanguages = useMemo(() => { + return formatStringList( + wallet.supportedLanguages, + NUMBER_OF_SUPPORTED_LANGUAGES_SHOWN + ) + }, [wallet.supportedLanguages]) + + const hasExtraLanguages = useMemo(() => { + return ( + wallet.supportedLanguages.length > NUMBER_OF_SUPPORTED_LANGUAGES_SHOWN + ) + }, [wallet.supportedLanguages]) + + const PersonaTags = useMemo(() => { + if (walletPersonas.length === 0) return null + + return ( +
+ {walletPersonas.map((persona) => ( + + {t(persona)} + + ))} +
+ ) + }, [walletPersonas, t]) + + const ChainImagesComponent = useMemo(() => { + return ( + + ) + }, [wallet.supported_chains, walletPersonas.length]) return (
+ {/* Desktop layout */}
{ />

{wallet.name}

- {walletPersonas.length > 0 && ( -
- {walletPersonas.map((persona) => ( - - {t(persona)} - - ))} -
- )} - + {PersonaTags} +
+ {ChainImagesComponent} +
+ + {/* Mobile layout */}
{ />

{wallet.name}

-
- {walletPersonas.length > 0 && ( -
- {walletPersonas.map((persona) => ( - - {t(persona)} - - ))} -
- )} -
- + {PersonaTags &&
{PersonaTags}
} + {ChainImagesComponent}
+
{ {deviceLabels.length > 0 && (
-

{deviceLabels.join(" · ")}

+

{deviceLabelsText}

)}

- {formatStringList(wallet.supportedLanguages, 5)}{" "} - + {formattedLanguages}{" "} + {hasExtraLanguages && ( + + )}

@@ -150,4 +183,4 @@ const WalletInfo = ({ wallet, isExpanded }: WalletInfoProps) => { ) } -export default WalletInfo +export default memo(WalletInfo) diff --git a/src/components/FindWalletProductTable/index.tsx b/src/components/FindWalletProductTable/index.tsx index dedaea9a93f..db2a6940019 100644 --- a/src/components/FindWalletProductTable/index.tsx +++ b/src/components/FindWalletProductTable/index.tsx @@ -24,7 +24,6 @@ import WalletSubComponent from "./WalletSubComponent" import { useTranslation } from "@/hooks/useTranslation" const FindWalletProductTable = ({ wallets }: { wallets: Wallet[] }) => { - console.log({ wallets }) const { t } = useTranslation("page-wallets-find-wallet") const walletPersonas = useWalletPersonaPresets() const walletFilterOptions = useWalletFilters() From f363041fd12fffb5a4d00df61236b72775d90c3e Mon Sep 17 00:00:00 2001 From: Pablo Pettinari Date: Tue, 9 Sep 2025 14:50:51 +0200 Subject: [PATCH 02/21] move filters and filtering data state to ProductTable --- .../FindWalletProductTable/index.tsx | 129 ++++++++---------- src/components/ProductTable/index.tsx | 115 ++++++++++++---- 2 files changed, 139 insertions(+), 105 deletions(-) diff --git a/src/components/FindWalletProductTable/index.tsx b/src/components/FindWalletProductTable/index.tsx index db2a6940019..9b16452de24 100644 --- a/src/components/FindWalletProductTable/index.tsx +++ b/src/components/FindWalletProductTable/index.tsx @@ -1,96 +1,77 @@ "use client" -import { useMemo, useState } from "react" +import { ChainName, FilterOption, Lang, Wallet } from "@/lib/types" -import { - ChainName, - FilterOption, - Lang, - Wallet, - WalletFilter, -} from "@/lib/types" - -import { useWalletColumns } from "@/components/FindWalletProductTable/hooks/useWalletColumns" +// import { useWalletColumns } from "@/components/FindWalletProductTable/hooks/useWalletColumns" import { useWalletFilters } from "@/components/FindWalletProductTable/hooks/useWalletFilters" import { useWalletPersonaPresets } from "@/components/FindWalletProductTable/hooks/useWalletPersonaPresets" import ProductTable from "@/components/ProductTable" import { trackCustomEvent } from "@/lib/utils/matomo" -import { getFilteredWalletsCount } from "@/lib/utils/wallets" import FindWalletsNoResults from "./FindWalletsNoResults" import WalletSubComponent from "./WalletSubComponent" import { useTranslation } from "@/hooks/useTranslation" -const FindWalletProductTable = ({ wallets }: { wallets: Wallet[] }) => { - const { t } = useTranslation("page-wallets-find-wallet") - const walletPersonas = useWalletPersonaPresets() - const walletFilterOptions = useWalletFilters() - const [filters, setFilters] = useState(walletFilterOptions) - - const activeFilterKeys = useMemo(() => { - const keys: string[] = [] - filters.forEach((filter) => { - filter.items.forEach((item) => { - if (item.inputState === true && item.options.length === 0) { - keys.push(item.filterKey) - } - if (item.options?.length > 0) { - item.options.forEach((option) => { - if (option.inputState === true) { - keys.push(option.filterKey) - } - }) - } - }) +function getActiveFilterKeys(filters: FilterOption[]): string[] { + const keys: string[] = [] + filters.forEach((filter) => { + filter.items.forEach((item) => { + if (item.inputState === true && item.options.length === 0) { + keys.push(item.filterKey) + } + if (item.options?.length > 0) { + item.options.forEach((option) => { + if (option.inputState === true) { + keys.push(option.filterKey) + } + }) + } }) - return keys - }, [filters]) + }) + return keys +} - const filteredData = useMemo(() => { - if (!Array.isArray(wallets)) return [] +const filterFn = (data: Wallet[], filters: FilterOption[]) => { + let selectedLanguage: string = "" + let selectedLayer2: ChainName[] = [] - let selectedLanguage: string = "" - let selectedLayer2: ChainName[] = [] + const activeFilterKeys = getActiveFilterKeys(filters) - filters.forEach((filter) => { - filter.items.forEach((item) => { - if (item.filterKey === "languages") { - selectedLanguage = item.inputState as string - } else if (item.filterKey === "layer_2_support") { - selectedLayer2 = (item.inputState as ChainName[]) || [] - } - }) + filters.forEach((filter) => { + filter.items.forEach((item) => { + if (item.filterKey === "languages") { + selectedLanguage = item.inputState as string + } else if (item.filterKey === "layer_2_support") { + selectedLayer2 = (item.inputState as ChainName[]) || [] + } }) + }) - return wallets - .filter((item) => { - return item.languages_supported.includes(selectedLanguage as Lang) - }) - .filter((item) => { - return ( - selectedLayer2.length === 0 || - selectedLayer2.every((chain) => item.supported_chains.includes(chain)) - ) - }) - .filter((item) => { - return activeFilterKeys.every((key) => item[key]) - }) - }, [wallets, filters, activeFilterKeys]) - - const personasWalletCounts = useMemo(() => { - return walletPersonas.map((persona) => - getFilteredWalletsCount( - filteredData, - persona.presetFilters as WalletFilter + return data + .filter((item) => { + return item.languages_supported.includes(selectedLanguage as Lang) + }) + .filter((item) => { + return ( + selectedLayer2.length === 0 || + selectedLayer2.every((chain) => item.supported_chains.includes(chain)) ) - ) - }, [filteredData, walletPersonas]) + }) + .filter((item) => { + return activeFilterKeys.every((key) => item[key]) + }) +} + +const FindWalletProductTable = ({ wallets }: { wallets: Wallet[] }) => { + const { t } = useTranslation("page-wallets-find-wallet") + const walletPersonas = useWalletPersonaPresets() + const walletFilterOptions = useWalletFilters() // Reset filters const resetFilters = () => { - setFilters(walletFilterOptions) + // setFilters(walletFilterOptions) trackCustomEvent({ eventCategory: "WalletFilterSidebar", eventAction: "Reset button", @@ -104,16 +85,14 @@ const FindWalletProductTable = ({ wallets }: { wallets: Wallet[] }) => { return ( - columns={useWalletColumns} - data={filteredData} + data={wallets} allDataLength={wallets.length} matomoEventCategory="find-wallet" - filters={filters} + filters={walletFilterOptions} + filterFn={filterFn} presetFilters={walletPersonas} - presetFiltersCounts={personasWalletCounts} resetFilters={resetFilters} - setFilters={setFilters} - subComponent={(wallet, listIdx) => ( + subComponent={(wallet, filters, listIdx) => ( { - columns: ColumnDef[] data: T[] allDataLength: number filters: FilterOption[] + filterFn: (data: T[], filters: FilterOption[]) => T[] presetFilters: TPresetFilters - presetFiltersCounts?: number[] resetFilters: () => void - setFilters: Dispatch> - subComponent?: FC + subComponent?: ( + item: T, + filters: FilterOption[], + listIdx: number + ) => React.ReactNode noResultsComponent?: React.FC mobileFiltersLabel: string matomoEventCategory: string @@ -35,25 +32,28 @@ interface ProductTableProps { } const ProductTable = ({ - columns, data, - allDataLength, - filters, + // allDataLength, + filters: initialFilters, + filterFn, presetFilters, - presetFiltersCounts, resetFilters, - setFilters, - subComponent, - noResultsComponent, + // subComponent, + // noResultsComponent, mobileFiltersLabel, - matomoEventCategory, - meta, + // matomoEventCategory, + // meta, }: ProductTableProps) => { const searchParams = useSearchParams() + const [filters, setFilters] = useState(initialFilters) const [activePresets, setActivePresets] = useState([]) const [mobileFiltersOpen, setMobileFiltersOpen] = useState(false) + const filteredData = useMemo(() => { + return filterFn(data, filters) + }, [data, filters, filterFn]) + const parseQueryParams = (queryValue: unknown) => { // Handle boolean values if (queryValue === "true") return true @@ -265,6 +265,27 @@ const ProductTable = ({ }, 0) }, [filters]) + const presetFiltersCounts = useMemo(() => { + return presetFilters.map((persona) => { + const activeFilters = Object.entries(persona.presetFilters).filter( + ([, value]) => value === true + ) + + return data.filter((item) => { + return activeFilters.every(([feature]) => item[feature] === true) + }).length + }) + }, [data, presetFilters]) + + const parentRef = useRef(null) + + const virtualizer = useWindowVirtualizer({ + count: filteredData.length, + estimateSize: () => 250, + overscan: 5, + scrollMargin: parentRef.current?.offsetTop ?? 0, + }) + return (
{presetFilters.length ? ( @@ -287,7 +308,7 @@ const ProductTable = ({ presetFiltersCounts={presetFiltersCounts} activePresets={activePresets} handleSelectPreset={handleSelectPreset} - dataCount={data.length} + dataCount={filteredData.length} activeFiltersCount={activeFiltersCount} mobileFiltersOpen={mobileFiltersOpen} setMobileFiltersOpen={setMobileFiltersOpen} @@ -296,15 +317,15 @@ const ProductTable = ({ />
- + /> */}
- ({ activeFiltersCount={activeFiltersCount} meta={meta} matomoEventCategory={matomoEventCategory} - /> + /> */} + +
+ {/* {data.map((item) => ( +
+ +
+ ))} */} + {virtualizer.getVirtualItems().map((item) => ( +
+ +
+ ))} +
From 2f4e8f218bfa9b0c695a25d473d6a7f74dc53456 Mon Sep 17 00:00:00 2001 From: Pablo Pettinari Date: Tue, 9 Sep 2025 15:01:34 +0200 Subject: [PATCH 03/21] refactor resetFilters --- .../FindWalletProductTable/index.tsx | 4 +--- src/components/ProductTable/index.tsx | 18 +++++++++++------- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/src/components/FindWalletProductTable/index.tsx b/src/components/FindWalletProductTable/index.tsx index 9b16452de24..ae2a03d5496 100644 --- a/src/components/FindWalletProductTable/index.tsx +++ b/src/components/FindWalletProductTable/index.tsx @@ -2,7 +2,6 @@ import { ChainName, FilterOption, Lang, Wallet } from "@/lib/types" -// import { useWalletColumns } from "@/components/FindWalletProductTable/hooks/useWalletColumns" import { useWalletFilters } from "@/components/FindWalletProductTable/hooks/useWalletFilters" import { useWalletPersonaPresets } from "@/components/FindWalletProductTable/hooks/useWalletPersonaPresets" import ProductTable from "@/components/ProductTable" @@ -71,7 +70,6 @@ const FindWalletProductTable = ({ wallets }: { wallets: Wallet[] }) => { // Reset filters const resetFilters = () => { - // setFilters(walletFilterOptions) trackCustomEvent({ eventCategory: "WalletFilterSidebar", eventAction: "Reset button", @@ -91,7 +89,7 @@ const FindWalletProductTable = ({ wallets }: { wallets: Wallet[] }) => { filters={walletFilterOptions} filterFn={filterFn} presetFilters={walletPersonas} - resetFilters={resetFilters} + onResetFilters={resetFilters} subComponent={(wallet, filters, listIdx) => ( { filters: FilterOption[] filterFn: (data: T[], filters: FilterOption[]) => T[] presetFilters: TPresetFilters - resetFilters: () => void + onResetFilters?: () => void subComponent?: ( item: T, filters: FilterOption[], @@ -37,7 +36,7 @@ const ProductTable = ({ filters: initialFilters, filterFn, presetFilters, - resetFilters, + onResetFilters, // subComponent, // noResultsComponent, mobileFiltersLabel, @@ -101,6 +100,11 @@ const ProductTable = ({ // eslint-disable-next-line react-hooks/exhaustive-deps }, [searchParams]) + const resetFilters = useCallback(() => { + setFilters(initialFilters) + onResetFilters?.() + }, [initialFilters, onResetFilters]) + // Update or remove preset filters const handleSelectPreset = (idx: number) => { if (activePresets.includes(idx)) { @@ -317,12 +321,12 @@ const ProductTable = ({ />
- {/* */} + />
{/*
Date: Tue, 9 Sep 2025 15:19:25 +0200 Subject: [PATCH 04/21] unify filters update to render once --- src/components/ProductTable/MobileFilters.tsx | 2 +- src/components/ProductTable/index.tsx | 98 ++++++++++--------- 2 files changed, 53 insertions(+), 47 deletions(-) diff --git a/src/components/ProductTable/MobileFilters.tsx b/src/components/ProductTable/MobileFilters.tsx index 9e37d1f9189..36c045c4741 100644 --- a/src/components/ProductTable/MobileFilters.tsx +++ b/src/components/ProductTable/MobileFilters.tsx @@ -21,7 +21,7 @@ import { useTranslation } from "@/hooks/useTranslation" interface MobileFiltersProps { filters: FilterOption[] - setFilters: React.Dispatch> + setFilters: (filters: FilterOption[]) => void presets: TPresetFilters presetFiltersCounts?: number[] activePresets: number[] diff --git a/src/components/ProductTable/index.tsx b/src/components/ProductTable/index.tsx index 149815c4067..40d8c6a894f 100644 --- a/src/components/ProductTable/index.tsx +++ b/src/components/ProductTable/index.tsx @@ -100,6 +100,7 @@ const ProductTable = ({ // eslint-disable-next-line react-hooks/exhaustive-deps }, [searchParams]) + // Reset filters const resetFilters = useCallback(() => { setFilters(initialFilters) onResetFilters?.() @@ -189,50 +190,55 @@ const ProductTable = ({ } // Update activePresets based on current filters - useEffect(() => { - const currentFilters = {} - - filters.forEach((filter) => { - filter.items.forEach((item) => { - if (item.inputState === true) { - currentFilters[item.filterKey] = item.inputState - } - - if (item.options && item.options.length > 0) { - item.options.forEach((option) => { - if (option.inputState === true) { - currentFilters[option.filterKey] = option.inputState - } - }) - } + const updateFilters = useCallback( + (filters: FilterOption[]) => { + const currentFilters = {} + + filters.forEach((filter) => { + filter.items.forEach((item) => { + if (item.inputState === true) { + currentFilters[item.filterKey] = item.inputState + } + + if (item.options && item.options.length > 0) { + item.options.forEach((option) => { + if (option.inputState === true) { + currentFilters[option.filterKey] = option.inputState + } + }) + } + }) }) - }) - const presetsToApply = presetFilters.reduce( - (acc, preset, idx) => { - const presetFilters = preset.presetFilters - const activePresetKeys = Object.keys(presetFilters).filter( - (key) => presetFilters[key] - ) - const allItemsInCurrentFilters = activePresetKeys.every( - (key) => currentFilters[key] !== undefined - ) - - if (allItemsInCurrentFilters) { - acc.push(idx) - } - return acc - }, - [] - ) - - setActivePresets((prevActivePresets) => { - const newActivePresets = [ - ...new Set([...prevActivePresets, ...presetsToApply]), - ] - return newActivePresets.filter((idx) => presetsToApply.includes(idx)) - }) - }, [filters, presetFilters]) + const presetsToApply = presetFilters.reduce( + (acc, preset, idx) => { + const presetFilters = preset.presetFilters + const activePresetKeys = Object.keys(presetFilters).filter( + (key) => presetFilters[key] + ) + const allItemsInCurrentFilters = activePresetKeys.every( + (key) => currentFilters[key] !== undefined + ) + + if (allItemsInCurrentFilters) { + acc.push(idx) + } + return acc + }, + [] + ) + + setFilters(filters) + + setActivePresets((prevActivePresets) => { + const newActivePresets = [ + ...new Set([...prevActivePresets, ...presetsToApply]), + ] + return newActivePresets.filter((idx) => presetsToApply.includes(idx)) + }) + }, + [presetFilters, setFilters, setActivePresets] + ) // Count active filters const activeFiltersCount = useMemo(() => { @@ -275,11 +281,11 @@ const ProductTable = ({ ([, value]) => value === true ) - return data.filter((item) => { + return filteredData.filter((item) => { return activeFilters.every(([feature]) => item[feature] === true) }).length }) - }, [data, presetFilters]) + }, [filteredData, presetFilters]) const parentRef = useRef(null) @@ -307,7 +313,7 @@ const ProductTable = ({
({
From 5754c8d110ba0ffd52cbe7b901d8f6f5569a04ae Mon Sep 17 00:00:00 2001 From: Pablo Pettinari Date: Tue, 9 Sep 2025 20:37:38 +0200 Subject: [PATCH 05/21] use react-virtual to virtualize product list --- .../FindWalletProductTable/WalletInfo.tsx | 18 ++--- src/components/ProductTable/index.tsx | 75 ++++++++++++------- 2 files changed, 55 insertions(+), 38 deletions(-) diff --git a/src/components/FindWalletProductTable/WalletInfo.tsx b/src/components/FindWalletProductTable/WalletInfo.tsx index e1ce0834c17..03fa5c8aeff 100644 --- a/src/components/FindWalletProductTable/WalletInfo.tsx +++ b/src/components/FindWalletProductTable/WalletInfo.tsx @@ -19,10 +19,9 @@ import { useTranslation } from "@/hooks/useTranslation" interface WalletInfoProps { wallet: Wallet - isExpanded: boolean } -const WalletInfo = ({ wallet, isExpanded }: WalletInfoProps) => { +const WalletInfo = ({ wallet }: WalletInfoProps) => { const { t } = useTranslation("page-wallets-find-wallet") const walletPersonas = useMemo(() => { @@ -121,7 +120,7 @@ const WalletInfo = ({ wallet, isExpanded }: WalletInfoProps) => {
{
@@ -174,6 +170,10 @@ const WalletInfo = ({ wallet, isExpanded }: WalletInfoProps) => { eventAction: "Tap main button", eventName: `${wallet.name}`, }} + onClick={(e) => { + // Prevent expanding the wallet more info section when clicking on the "Visit website" button + e.stopPropagation() + }} > {t("page-find-wallet-visit-website")} diff --git a/src/components/ProductTable/index.tsx b/src/components/ProductTable/index.tsx index 40d8c6a894f..bcffa1606cc 100644 --- a/src/components/ProductTable/index.tsx +++ b/src/components/ProductTable/index.tsx @@ -1,4 +1,11 @@ -import { useCallback, useEffect, useMemo, useRef, useState } from "react" +import { + useCallback, + useEffect, + useLayoutEffect, + useMemo, + useRef, + useState, +} from "react" import { useSearchParams } from "next/navigation" import { useWindowVirtualizer } from "@tanstack/react-virtual" @@ -11,6 +18,11 @@ import PresetFilters from "@/components/ProductTable/PresetFilters" import { trackCustomEvent } from "@/lib/utils/matomo" import WalletInfo from "../FindWalletProductTable/WalletInfo" +import { + Collapsible, + CollapsibleContent, + CollapsibleTrigger, +} from "../ui/collapsible" interface ProductTableProps { data: T[] @@ -37,7 +49,7 @@ const ProductTable = ({ filterFn, presetFilters, onResetFilters, - // subComponent, + subComponent, // noResultsComponent, mobileFiltersLabel, // matomoEventCategory, @@ -289,11 +301,17 @@ const ProductTable = ({ const parentRef = useRef(null) + const parentOffsetRef = useRef(0) + + useLayoutEffect(() => { + parentOffsetRef.current = parentRef.current?.offsetTop ?? 0 + }, []) + const virtualizer = useWindowVirtualizer({ count: filteredData.length, estimateSize: () => 250, overscan: 5, - scrollMargin: parentRef.current?.offsetTop ?? 0, + scrollMargin: parentOffsetRef.current, }) return ( @@ -350,36 +368,35 @@ const ProductTable = ({
- {/* {data.map((item) => ( -
- -
- ))} */} - {virtualizer.getVirtualItems().map((item) => ( -
- -
- ))} + {virtualizer.getVirtualItems().map((virtualItem) => { + const item = filteredData[virtualItem.index] + + return ( + + +
+ +
+
+ + {subComponent?.(item as T, filters, virtualItem.index)} + +
+ ) + })}
From 179d8d9a1a97da9fcbcd4a38fcc3c568f98b105c Mon Sep 17 00:00:00 2001 From: Pablo Pettinari Date: Tue, 9 Sep 2025 21:54:10 +0200 Subject: [PATCH 06/21] reimplement sticky list header --- .../hooks/useWalletColumns.tsx | 35 ------------------- .../FindWalletProductTable/index.tsx | 1 - src/components/ProductTable/index.tsx | 17 ++++++--- 3 files changed, 12 insertions(+), 41 deletions(-) delete mode 100644 src/components/FindWalletProductTable/hooks/useWalletColumns.tsx diff --git a/src/components/FindWalletProductTable/hooks/useWalletColumns.tsx b/src/components/FindWalletProductTable/hooks/useWalletColumns.tsx deleted file mode 100644 index 6ad7a0710bc..00000000000 --- a/src/components/FindWalletProductTable/hooks/useWalletColumns.tsx +++ /dev/null @@ -1,35 +0,0 @@ -"use client" - -import { ColumnDef } from "@tanstack/react-table" - -import { Wallet } from "@/lib/types" - -import type { TableMeta } from "@/components/DataTable" -import WalletInfo from "@/components/FindWalletProductTable/WalletInfo" -import Translation from "@/components/Translation" -import { TableCell } from "@/components/ui/table" - -export const useWalletColumns: ColumnDef[] = [ - { - id: "walletInfo", - header: ({ table }) => { - const meta = table.options.meta as TableMeta - - return ( -
-

- {" "} - ({meta.dataLength}) -

-
- ) - }, - cell: ({ row }) => { - return ( - - - - ) - }, - }, -] diff --git a/src/components/FindWalletProductTable/index.tsx b/src/components/FindWalletProductTable/index.tsx index ae2a03d5496..71f64d4abfb 100644 --- a/src/components/FindWalletProductTable/index.tsx +++ b/src/components/FindWalletProductTable/index.tsx @@ -84,7 +84,6 @@ const FindWalletProductTable = ({ wallets }: { wallets: Wallet[] }) => { return ( data={wallets} - allDataLength={wallets.length} matomoEventCategory="find-wallet" filters={walletFilterOptions} filterFn={filterFn} diff --git a/src/components/ProductTable/index.tsx b/src/components/ProductTable/index.tsx index bcffa1606cc..8358b0be1e9 100644 --- a/src/components/ProductTable/index.tsx +++ b/src/components/ProductTable/index.tsx @@ -11,13 +11,14 @@ import { useWindowVirtualizer } from "@tanstack/react-virtual" import type { FilterOption, TPresetFilters, Wallet } from "@/lib/types" -import Filters from "@/components/ProductTable/Filters" +// import Filters from "@/components/ProductTable/Filters" import MobileFilters from "@/components/ProductTable/MobileFilters" import PresetFilters from "@/components/ProductTable/PresetFilters" import { trackCustomEvent } from "@/lib/utils/matomo" import WalletInfo from "../FindWalletProductTable/WalletInfo" +import Translation from "../Translation" import { Collapsible, CollapsibleContent, @@ -26,7 +27,6 @@ import { interface ProductTableProps { data: T[] - allDataLength: number filters: FilterOption[] filterFn: (data: T[], filters: FilterOption[]) => T[] presetFilters: TPresetFilters @@ -44,7 +44,6 @@ interface ProductTableProps { const ProductTable = ({ data, - // allDataLength, filters: initialFilters, filterFn, presetFilters, @@ -345,12 +344,12 @@ const ProductTable = ({ />
- + /> */}
{/*
({ meta={meta} matomoEventCategory={matomoEventCategory} /> */} +
+
+

+ {" "} + ({filteredData.length}) +

+
+
Date: Tue, 9 Sep 2025 22:56:39 +0200 Subject: [PATCH 07/21] implement no results and tracking events for expanded rows --- app/[locale]/wallets/find-wallet/page.tsx | 1 + .../FindWalletProductTable/index.tsx | 10 ++-- src/components/ProductTable/index.tsx | 55 ++++++++++++------- 3 files changed, 42 insertions(+), 24 deletions(-) diff --git a/app/[locale]/wallets/find-wallet/page.tsx b/app/[locale]/wallets/find-wallet/page.tsx index e67d8986713..e5a41bf6636 100644 --- a/app/[locale]/wallets/find-wallet/page.tsx +++ b/app/[locale]/wallets/find-wallet/page.tsx @@ -35,6 +35,7 @@ const Page = async ({ params }: { params: Promise<{ locale: Lang }> }) => { const wallets = walletsData.map((wallet) => ({ ...wallet, + id: wallet.name, supportedLanguages: getSupportedLanguages( wallet.languages_supported, locale! diff --git a/src/components/FindWalletProductTable/index.tsx b/src/components/FindWalletProductTable/index.tsx index 71f64d4abfb..8bbd9ee9e43 100644 --- a/src/components/FindWalletProductTable/index.tsx +++ b/src/components/FindWalletProductTable/index.tsx @@ -32,7 +32,7 @@ function getActiveFilterKeys(filters: FilterOption[]): string[] { return keys } -const filterFn = (data: Wallet[], filters: FilterOption[]) => { +const filterFn = (data: WalletRow[], filters: FilterOption[]) => { let selectedLanguage: string = "" let selectedLayer2: ChainName[] = [] @@ -63,7 +63,9 @@ const filterFn = (data: Wallet[], filters: FilterOption[]) => { }) } -const FindWalletProductTable = ({ wallets }: { wallets: Wallet[] }) => { +type WalletRow = Wallet & { id: string } + +const FindWalletProductTable = ({ wallets }: { wallets: WalletRow[] }) => { const { t } = useTranslation("page-wallets-find-wallet") const walletPersonas = useWalletPersonaPresets() const walletFilterOptions = useWalletFilters() @@ -82,7 +84,7 @@ const FindWalletProductTable = ({ wallets }: { wallets: Wallet[] }) => { } return ( - + data={wallets} matomoEventCategory="find-wallet" filters={walletFilterOptions} @@ -96,7 +98,7 @@ const FindWalletProductTable = ({ wallets }: { wallets: Wallet[] }) => { listIdx={listIdx} /> )} - noResultsComponent={() => ( + noResultsComponent={(resetFilters) => ( )} mobileFiltersLabel={t("page-find-wallet-see-wallets")} diff --git a/src/components/ProductTable/index.tsx b/src/components/ProductTable/index.tsx index 8358b0be1e9..871858ec868 100644 --- a/src/components/ProductTable/index.tsx +++ b/src/components/ProductTable/index.tsx @@ -25,7 +25,7 @@ import { CollapsibleTrigger, } from "../ui/collapsible" -interface ProductTableProps { +interface ProductTableProps { data: T[] filters: FilterOption[] filterFn: (data: T[], filters: FilterOption[]) => T[] @@ -36,23 +36,21 @@ interface ProductTableProps { filters: FilterOption[], listIdx: number ) => React.ReactNode - noResultsComponent?: React.FC + noResultsComponent?: (resetFilters: () => void) => React.ReactNode mobileFiltersLabel: string matomoEventCategory: string - meta?: Record } -const ProductTable = ({ +const ProductTable = ({ data, filters: initialFilters, filterFn, presetFilters, onResetFilters, subComponent, - // noResultsComponent, + noResultsComponent, mobileFiltersLabel, - // matomoEventCategory, - // meta, + matomoEventCategory, }: ProductTableProps) => { const searchParams = useSearchParams() @@ -313,6 +311,30 @@ const ProductTable = ({ scrollMargin: parentOffsetRef.current, }) + const previousExpandedRef = useRef>({}) + + const handleExpandedChange = useCallback( + (open: boolean, item: T) => { + if (!open) return + + const expandedOnce = previousExpandedRef.current[item.id] + + if (!expandedOnce) { + trackCustomEvent({ + eventCategory: matomoEventCategory, + eventAction: "expanded", + eventName: item.id, + }) + } + + previousExpandedRef.current = { + ...previousExpandedRef.current, + [item.id]: true, + } + }, + [matomoEventCategory] + ) + return (
{presetFilters.length ? ( @@ -352,18 +374,6 @@ const ProductTable = ({ /> */}
- {/*
*/}

@@ -373,6 +383,10 @@ const ProductTable = ({

+ {filteredData.length === 0 && + noResultsComponent && + noResultsComponent(resetFilters)} +
({ style={{ transform: `translateY(${virtualItem.start - virtualizer.options.scrollMargin}px)`, }} + onOpenChange={(open) => handleExpandedChange(open, item)} >
- +
From d4f209f51462bdc7a112dc781d6299e81e91660d Mon Sep 17 00:00:00 2001 From: Pablo Pettinari Date: Tue, 9 Sep 2025 22:57:19 +0200 Subject: [PATCH 08/21] install react-virtual --- package.json | 1 + pnpm-lock.yaml | 20 ++++++++++++++++++++ 2 files changed, 21 insertions(+) diff --git a/package.json b/package.json index 401ed5f7981..6f3c6a03d34 100644 --- a/package.json +++ b/package.json @@ -59,6 +59,7 @@ "@socialgouv/matomo-next": "^1.8.0", "@tanstack/react-query": "^5.66.7", "@tanstack/react-table": "^8.19.3", + "@tanstack/react-virtual": "^3.13.12", "@types/canvas-confetti": "^1.9.0", "@types/three": "^0.177.0", "@wagmi/core": "^2.17.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4804aa54230..dbf8602a0d1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -95,6 +95,9 @@ importers: '@tanstack/react-table': specifier: ^8.19.3 version: 8.21.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@tanstack/react-virtual': + specifier: ^3.13.12 + version: 3.13.12(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@types/canvas-confetti': specifier: ^1.9.0 version: 1.9.0 @@ -3710,10 +3713,19 @@ packages: react: '>=16.8' react-dom: '>=16.8' + '@tanstack/react-virtual@3.13.12': + resolution: {integrity: sha512-Gd13QdxPSukP8ZrkbgS2RwoZseTTbQPLnQEn7HY/rqtM+8Zt95f7xKC7N0EsKs7aoz0WzZ+fditZux+F8EzYxA==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + '@tanstack/table-core@8.21.3': resolution: {integrity: sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg==} engines: {node: '>=12'} + '@tanstack/virtual-core@3.13.12': + resolution: {integrity: sha512-1YBOJfRHV4sXUmWsFSf5rQor4Ss82G8dQWLRbnk3GA4jeP8hQt1hxXh0tmflpC0dz3VgEv/1+qwPyLeWkQuPFA==} + '@testing-library/dom@10.4.0': resolution: {integrity: sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ==} engines: {node: '>=18'} @@ -13749,8 +13761,16 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) + '@tanstack/react-virtual@3.13.12(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@tanstack/virtual-core': 3.13.12 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + '@tanstack/table-core@8.21.3': {} + '@tanstack/virtual-core@3.13.12': {} + '@testing-library/dom@10.4.0': dependencies: '@babel/code-frame': 7.27.1 From 13752fe5933f01f8f9fd072f7016eaefc694a12f Mon Sep 17 00:00:00 2001 From: Pablo Pettinari Date: Wed, 10 Sep 2025 09:51:01 +0200 Subject: [PATCH 09/21] optimize WalletInfo for device resolutions --- src/components/ChainImages/index.tsx | 6 +- .../FindWalletProductTable/PersonaTags.tsx | 27 ++++++ .../FindWalletProductTable/WalletInfo.tsx | 96 +++++++++---------- 3 files changed, 79 insertions(+), 50 deletions(-) create mode 100644 src/components/FindWalletProductTable/PersonaTags.tsx diff --git a/src/components/ChainImages/index.tsx b/src/components/ChainImages/index.tsx index 39953b71fe9..86e648f2999 100644 --- a/src/components/ChainImages/index.tsx +++ b/src/components/ChainImages/index.tsx @@ -1,3 +1,5 @@ +import { memo } from "react" + import { ChainName } from "@/lib/types" import { Image } from "@/components/Image" @@ -11,7 +13,7 @@ interface ChainImagesProps { className?: string } -export const ChainImages = ({ +const ChainImages = ({ chains, size = 24, className = "", @@ -48,3 +50,5 @@ export const ChainImages = ({
) } + +export default memo(ChainImages) diff --git a/src/components/FindWalletProductTable/PersonaTags.tsx b/src/components/FindWalletProductTable/PersonaTags.tsx new file mode 100644 index 00000000000..7619ac50eae --- /dev/null +++ b/src/components/FindWalletProductTable/PersonaTags.tsx @@ -0,0 +1,27 @@ +import { memo } from "react" + +import { Tag } from "../ui/tag" + +import { useTranslation } from "@/hooks/useTranslation" + +type PersonaTagsProps = { + walletPersonas: string[] +} + +const PersonaTags = ({ walletPersonas }: PersonaTagsProps) => { + const { t } = useTranslation("page-wallets-find-wallet") + + if (walletPersonas.length === 0) return null + + return ( +
+ {walletPersonas.map((persona) => ( + + {t(persona)} + + ))} +
+ ) +} + +export default memo(PersonaTags) diff --git a/src/components/FindWalletProductTable/WalletInfo.tsx b/src/components/FindWalletProductTable/WalletInfo.tsx index 03fa5c8aeff..ef39fc77b94 100644 --- a/src/components/FindWalletProductTable/WalletInfo.tsx +++ b/src/components/FindWalletProductTable/WalletInfo.tsx @@ -3,18 +3,21 @@ import { ChevronDown, ChevronUp } from "lucide-react" import type { ChainName, Wallet } from "@/lib/types" -import { ChainImages } from "@/components/ChainImages" +import ChainImages from "@/components/ChainImages" import { DevicesIcon, LanguagesIcon } from "@/components/icons/wallets" import { Image } from "@/components/Image" import { SupportedLanguagesTooltip } from "@/components/SupportedLanguagesTooltip" -import { Tag } from "@/components/ui/tag" +import { breakpointAsNumber } from "@/lib/utils/screen" import { formatStringList, getWalletPersonas } from "@/lib/utils/wallets" import { NUMBER_OF_SUPPORTED_LANGUAGES_SHOWN } from "@/lib/constants" +import MediaQuery from "../MediaQuery" import { ButtonLink } from "../ui/buttons/Button" +import PersonaTags from "./PersonaTags" + import { useTranslation } from "@/hooks/useTranslation" interface WalletInfoProps { @@ -58,64 +61,59 @@ const WalletInfo = ({ wallet }: WalletInfoProps) => { ) }, [wallet.supportedLanguages]) - const PersonaTags = useMemo(() => { - if (walletPersonas.length === 0) return null - - return ( -
- {walletPersonas.map((persona) => ( - - {t(persona)} - - ))} -
- ) - }, [walletPersonas, t]) - - const ChainImagesComponent = useMemo(() => { - return ( - - ) - }, [wallet.supported_chains, walletPersonas.length]) - return (
{/* Desktop layout */} -
- -
-

{wallet.name}

- {PersonaTags} -
- {ChainImagesComponent} + +
+ +
+

{wallet.name}

+ + + +
+ +
-
+ {/* Mobile layout */} -
-
- +
+
+ +

{wallet.name}

+
+
+ +
+ -

{wallet.name}

- {PersonaTags &&
{PersonaTags}
} - {ChainImagesComponent} -
+
From 45166822cc3fc5d109ea8386baa25e43e53e33e9 Mon Sep 17 00:00:00 2001 From: Pablo Pettinari Date: Wed, 10 Sep 2025 18:23:03 +0200 Subject: [PATCH 10/21] prevent unnecessary re-renders by memoizing Filter and using stable update callback --- src/components/ProductTable/Filter.tsx | 113 ++++++++++++++++++ src/components/ProductTable/Filters.tsx | 113 ++---------------- src/components/ProductTable/MobileFilters.tsx | 2 +- src/components/ProductTable/index.tsx | 87 ++++++++------ 4 files changed, 175 insertions(+), 140 deletions(-) create mode 100644 src/components/ProductTable/Filter.tsx diff --git a/src/components/ProductTable/Filter.tsx b/src/components/ProductTable/Filter.tsx new file mode 100644 index 00000000000..96ce0800b74 --- /dev/null +++ b/src/components/ProductTable/Filter.tsx @@ -0,0 +1,113 @@ +import { Fragment, memo } from "react" + +import { FilterInputState, FilterOption } from "@/lib/types" + +import { + AccordionContent, + AccordionItem, + AccordionTrigger, +} from "@/components/ui/accordion" + +interface FilterProps { + filter: FilterOption + filterIndex: number + onChange: (updatedFilter: FilterOption, filterIndex: number) => void +} + +const Filter = ({ filter, filterIndex, onChange }: FilterProps) => { + const handleChange = ( + filterIndex: number, + itemIndex: number, + newInputState: FilterInputState, + optionIndex?: number + ) => { + const updatedItems = filter.items.map((item, i) => { + if (i === itemIndex) { + if (typeof optionIndex !== "undefined") { + const updatedOptions = item.options.map((option, j) => { + if (j === optionIndex) { + return { + ...option, + inputState: newInputState, + } + } + return option + }) + return { + ...item, + options: updatedOptions, + } + } + return { + ...item, + inputState: newInputState, + options: item.options.map((option) => { + return { + ...option, + inputState: newInputState, + } + }), + } + } + return item + }) + + const updatedFilter = { + ...filter, + items: updatedItems, + } + + onChange(updatedFilter, filterIndex) + } + + if (!filter.showFilterOption) { + return null + } + + return ( + + +

{filter.title}

+
+ + {filter.items.map((item, itemIndex) => { + return ( +
+ {item.input( + filterIndex, + itemIndex, + item.inputState, + handleChange + )} + {item.inputState === true && item.options.length ? ( +
+ {item.options.map((option, optionIndex) => { + return ( + + {option.input( + filterIndex, + itemIndex, + optionIndex, + option.inputState, + handleChange + )} + + ) + })} +
+ ) : null} +
+ ) + })} +
+
+ ) +} + +export default memo(Filter) diff --git a/src/components/ProductTable/Filters.tsx b/src/components/ProductTable/Filters.tsx index ba0388f2cad..628992b43ba 100644 --- a/src/components/ProductTable/Filters.tsx +++ b/src/components/ProductTable/Filters.tsx @@ -1,21 +1,18 @@ import { RotateCcw } from "lucide-react" -import { FilterInputState, FilterOption } from "@/lib/types" +import { FilterOption } from "@/lib/types" -import { - Accordion, - AccordionContent, - AccordionItem, - AccordionTrigger, -} from "@/components/ui/accordion" +import { Accordion } from "@/components/ui/accordion" import { Button } from "@/components/ui/buttons/Button" +import Filter from "./Filter" + import { useTranslation } from "@/hooks/useTranslation" interface PresetFiltersProps { filters: FilterOption[] activeFiltersCount: number - setFilters: (filterOptions: FilterOption[]) => void + setFilters: (filter: FilterOption, filterIndex: number) => void resetFilters: () => void } @@ -27,53 +24,6 @@ const Filters = ({ }: PresetFiltersProps) => { const { t } = useTranslation("table") - const updateFilterState = ( - filterIndex: number, - itemIndex: number, - newInputState: FilterInputState, - optionIndex?: number - ) => { - const updatedFilters = filters.map((filter, idx) => { - if (idx !== filterIndex) return filter - - const updatedItems = filter.items.map((item, i) => { - if (i === itemIndex) { - if (typeof optionIndex !== "undefined") { - const updatedOptions = item.options.map((option, j) => { - if (j === optionIndex) { - return { - ...option, - inputState: newInputState, - } - } - return option - }) - return { - ...item, - options: updatedOptions, - } - } - return { - ...item, - inputState: newInputState, - options: item.options.map((option) => { - return { - ...option, - inputState: newInputState, - } - }), - } - } - return item - }) - return { - ...filter, - items: updatedItems, - } - }) - setFilters(updatedFilters) - } - return (
@@ -95,51 +45,14 @@ const Filters = ({ defaultValue={filters.map((_, idx) => `item ${idx}`)} > {filters.map((filter, filterIndex) => { - if (filter.showFilterOption) { - return ( - - -

- {filter.title} -

-
- - {filter.items.map((item, itemIndex) => { - return ( -
- {item.input( - filterIndex, - itemIndex, - item.inputState, - updateFilterState - )} - {item.inputState === true && item.options.length ? ( -
- {item.options.map((option, optionIndex) => { - return option.input( - filterIndex, - itemIndex, - optionIndex, - option.inputState, - updateFilterState - ) - })} -
- ) : null} -
- ) - })} -
-
- ) - } + return ( + + ) })}
diff --git a/src/components/ProductTable/MobileFilters.tsx b/src/components/ProductTable/MobileFilters.tsx index 36c045c4741..4fbeb4b9149 100644 --- a/src/components/ProductTable/MobileFilters.tsx +++ b/src/components/ProductTable/MobileFilters.tsx @@ -21,7 +21,7 @@ import { useTranslation } from "@/hooks/useTranslation" interface MobileFiltersProps { filters: FilterOption[] - setFilters: (filters: FilterOption[]) => void + setFilters: (filter: FilterOption, filterIndex: number) => void presets: TPresetFilters presetFiltersCounts?: number[] activePresets: number[] diff --git a/src/components/ProductTable/index.tsx b/src/components/ProductTable/index.tsx index 871858ec868..36613ede719 100644 --- a/src/components/ProductTable/index.tsx +++ b/src/components/ProductTable/index.tsx @@ -41,6 +41,40 @@ interface ProductTableProps { matomoEventCategory: string } +const getActiveFiltersCount = (filters: FilterOption[]) => { + return filters.reduce((count, filter) => { + return ( + count + + filter.items.reduce((itemCount, item) => { + if (item.options && item.options.length > 0) { + return ( + itemCount + + item.options.filter( + (option) => + typeof option.inputState === "boolean" && option.inputState + ).length + ) + } + if (Array.isArray(item.inputState) && item.inputState.length > 0) { + return itemCount + 1 + } + + if ( + typeof item.inputState === "string" && + item.filterKey !== "languages" + ) { + return itemCount + 1 + } + + return ( + itemCount + + (typeof item.inputState === "boolean" && item.inputState ? 1 : 0) + ) + }, 0) + ) + }, 0) +} + const ProductTable = ({ data, filters: initialFilters, @@ -200,7 +234,14 @@ const ProductTable = ({ // Update activePresets based on current filters const updateFilters = useCallback( - (filters: FilterOption[]) => { + (filter: FilterOption, filterIndex: number) => { + setFilters((prevFilters) => { + return prevFilters.map((prevFilter, idx) => { + if (idx !== filterIndex) return prevFilter + return filter + }) + }) + const currentFilters = {} filters.forEach((filter) => { @@ -237,8 +278,6 @@ const ProductTable = ({ [] ) - setFilters(filters) - setActivePresets((prevActivePresets) => { const newActivePresets = [ ...new Set([...prevActivePresets, ...presetsToApply]), @@ -246,44 +285,9 @@ const ProductTable = ({ return newActivePresets.filter((idx) => presetsToApply.includes(idx)) }) }, - [presetFilters, setFilters, setActivePresets] + [] ) - // Count active filters - const activeFiltersCount = useMemo(() => { - return filters.reduce((count, filter) => { - return ( - count + - filter.items.reduce((itemCount, item) => { - if (item.options && item.options.length > 0) { - return ( - itemCount + - item.options.filter( - (option) => - typeof option.inputState === "boolean" && option.inputState - ).length - ) - } - if (Array.isArray(item.inputState) && item.inputState.length > 0) { - return itemCount + 1 - } - - if ( - typeof item.inputState === "string" && - item.filterKey !== "languages" - ) { - return itemCount + 1 - } - - return ( - itemCount + - (typeof item.inputState === "boolean" && item.inputState ? 1 : 0) - ) - }, 0) - ) - }, 0) - }, [filters]) - const presetFiltersCounts = useMemo(() => { return presetFilters.map((persona) => { const activeFilters = Object.entries(persona.presetFilters).filter( @@ -335,6 +339,11 @@ const ProductTable = ({ [matomoEventCategory] ) + const activeFiltersCount = useMemo( + () => getActiveFiltersCount(filters), + [filters] + ) + return (
{presetFilters.length ? ( From 35af979ee2e1a9f82fd7c6408145a8a29ab30d89 Mon Sep 17 00:00:00 2001 From: Pablo Pettinari Date: Wed, 10 Sep 2025 19:06:13 +0200 Subject: [PATCH 11/21] refactor preset filters: derive it from filters and place it in the PresetFilters scope --- src/components/ProductTable/Filter.tsx | 6 +- src/components/ProductTable/Filters.tsx | 2 +- src/components/ProductTable/MobileFilters.tsx | 10 +- src/components/ProductTable/PresetFilters.tsx | 173 +++++++++++++++--- src/components/ProductTable/index.tsx | 145 +-------------- 5 files changed, 162 insertions(+), 174 deletions(-) diff --git a/src/components/ProductTable/Filter.tsx b/src/components/ProductTable/Filter.tsx index 96ce0800b74..37ee1e48e26 100644 --- a/src/components/ProductTable/Filter.tsx +++ b/src/components/ProductTable/Filter.tsx @@ -11,12 +11,12 @@ import { interface FilterProps { filter: FilterOption filterIndex: number - onChange: (updatedFilter: FilterOption, filterIndex: number) => void + onChange: (updatedFilter: FilterOption) => void } const Filter = ({ filter, filterIndex, onChange }: FilterProps) => { const handleChange = ( - filterIndex: number, + _: number, itemIndex: number, newInputState: FilterInputState, optionIndex?: number @@ -57,7 +57,7 @@ const Filter = ({ filter, filterIndex, onChange }: FilterProps) => { items: updatedItems, } - onChange(updatedFilter, filterIndex) + onChange(updatedFilter) } if (!filter.showFilterOption) { diff --git a/src/components/ProductTable/Filters.tsx b/src/components/ProductTable/Filters.tsx index 628992b43ba..cecd854fe37 100644 --- a/src/components/ProductTable/Filters.tsx +++ b/src/components/ProductTable/Filters.tsx @@ -12,7 +12,7 @@ import { useTranslation } from "@/hooks/useTranslation" interface PresetFiltersProps { filters: FilterOption[] activeFiltersCount: number - setFilters: (filter: FilterOption, filterIndex: number) => void + setFilters: (filter: FilterOption) => void resetFilters: () => void } diff --git a/src/components/ProductTable/MobileFilters.tsx b/src/components/ProductTable/MobileFilters.tsx index 4fbeb4b9149..124834d9634 100644 --- a/src/components/ProductTable/MobileFilters.tsx +++ b/src/components/ProductTable/MobileFilters.tsx @@ -21,11 +21,9 @@ import { useTranslation } from "@/hooks/useTranslation" interface MobileFiltersProps { filters: FilterOption[] - setFilters: (filter: FilterOption, filterIndex: number) => void + setFilters: (filters: FilterOption | FilterOption[]) => void presets: TPresetFilters presetFiltersCounts?: number[] - activePresets: number[] - handleSelectPreset: (index: number) => void dataCount: number activeFiltersCount: number mobileFiltersOpen: boolean @@ -39,8 +37,6 @@ const MobileFilters = ({ setFilters, presets, presetFiltersCounts, - activePresets, - handleSelectPreset, dataCount, activeFiltersCount, mobileFiltersOpen, @@ -90,9 +86,9 @@ const MobileFilters = ({
void + filters: FilterOption[] + setFilters: (filters: FilterOption[]) => void showMobileSidebar?: boolean presetFiltersCounts?: number[] } +const colors = { + text: [ + "text-primary", + "text-accent-b", + "text-accent-c", + "text-accent-a", + "text-[#BEBF3B]", + ], + border: [ + "border-primary", + "border-accent-b", + "border-accent-c", + "border-accent-a", + "border-[#BEBF3B]", + ], + bg: [ + "bg-primary", + "bg-accent-b", + "bg-accent-c", + "bg-accent-a", + "bg-[#BEBF3B]", + ], +} + const PresetFilters = ({ presets, - activePresets, - handleSelectPreset, + filters, + setFilters, showMobileSidebar = false, presetFiltersCounts, }: PresetFiltersProps) => { - const colors = { - text: [ - "text-primary", - "text-accent-b", - "text-accent-c", - "text-accent-a", - "text-[#BEBF3B]", - ], - border: [ - "border-primary", - "border-accent-b", - "border-accent-c", - "border-accent-a", - "border-[#BEBF3B]", - ], - bg: [ - "bg-primary", - "bg-accent-b", - "bg-accent-c", - "bg-accent-a", - "bg-[#BEBF3B]", - ], - } + const activePresets = useMemo(() => { + const currentFilters = {} + + filters.forEach((filter) => { + filter.items.forEach((item) => { + if (item.inputState === true) { + currentFilters[item.filterKey] = item.inputState + } + + if (item.options && item.options.length > 0) { + item.options.forEach((option) => { + if (option.inputState === true) { + currentFilters[option.filterKey] = option.inputState + } + }) + } + }) + }) + + const presetsToApply = presets.reduce((acc, preset, idx) => { + const presetFilters = preset.presetFilters + const activePresetKeys = Object.keys(presetFilters).filter( + (key) => presetFilters[key] + ) + const allItemsInCurrentFilters = activePresetKeys.every( + (key) => currentFilters[key] !== undefined + ) + + if (allItemsInCurrentFilters) { + acc.push(idx) + } + return acc + }, []) + + return presetsToApply + }, [filters, presets]) + + const handleSelectPreset = useCallback( + (idx: number) => { + if (activePresets.includes(idx)) { + trackCustomEvent({ + eventCategory: "UserPersona", + eventAction: `${presets[idx].title}`, + eventName: `${presets[idx].title} false`, + }) + // Get filters that are true for the preset being removed + const presetToRemove = presets[idx].presetFilters + const filtersToRemove = Object.keys(presetToRemove).filter( + (key) => presetToRemove[key] + ) + // Filter out keys that are present in other active presets + const finalFiltersToRemove = filtersToRemove.filter((key) => { + return !activePresets + .filter((preset) => preset !== idx) + .some((preset) => presets[preset].presetFilters[key]) + }) + // Set inputState of filters to false for the filters being removed + const updatedFilters = filters.map((filter) => ({ + ...filter, + items: filter.items.map((item) => ({ + ...item, + inputState: finalFiltersToRemove.includes(item.filterKey) + ? false + : item.inputState, + options: item.options.map((option) => ({ + ...option, + inputState: finalFiltersToRemove.includes(option.filterKey) + ? false + : option.inputState, + })), + })), + })) + setFilters(updatedFilters) + } else { + const newActivePresets = activePresets.concat(idx) + trackCustomEvent({ + eventCategory: "UserPersona", + eventAction: `${presets[idx].title}`, + eventName: `${presets[idx].title} true`, + }) + // Apply the filters for the selected preset + const combinedPresetFilters = newActivePresets.reduce((acc, idx) => { + const preset = presets[idx].presetFilters + Object.keys(preset).forEach((key) => { + acc[key] = acc[key] || preset[key] + }) + return acc + }, {}) + const updatedFilters = filters.map((filter) => ({ + ...filter, + items: filter.items.map((item) => ({ + ...item, + // Keep existing inputState if true, otherwise apply preset filter + inputState: + item.inputState || + (item.ignoreFilterReset + ? item.inputState + : combinedPresetFilters[item.filterKey] || false), + options: item.options.map((option) => ({ + ...option, + // Keep existing inputState if true, otherwise apply preset filter + inputState: + option.inputState || + (option.ignoreFilterReset + ? option.inputState + : combinedPresetFilters[option.filterKey] || false), + })), + })), + })) + setFilters(updatedFilters) + } + }, + [activePresets, filters, presets, setFilters] + ) return (
diff --git a/src/components/ProductTable/index.tsx b/src/components/ProductTable/index.tsx index 36613ede719..af8ed35d5c3 100644 --- a/src/components/ProductTable/index.tsx +++ b/src/components/ProductTable/index.tsx @@ -89,7 +89,6 @@ const ProductTable = ({ const searchParams = useSearchParams() const [filters, setFilters] = useState(initialFilters) - const [activePresets, setActivePresets] = useState([]) const [mobileFiltersOpen, setMobileFiltersOpen] = useState(false) const filteredData = useMemo(() => { @@ -149,141 +148,19 @@ const ProductTable = ({ onResetFilters?.() }, [initialFilters, onResetFilters]) - // Update or remove preset filters - const handleSelectPreset = (idx: number) => { - if (activePresets.includes(idx)) { - trackCustomEvent({ - eventCategory: "UserPersona", - eventAction: `${presetFilters[idx].title}`, - eventName: `${presetFilters[idx].title} false`, - }) - // Get filters that are true for the preset being removed - const presetToRemove = presetFilters[idx].presetFilters - const filtersToRemove = Object.keys(presetToRemove).filter( - (key) => presetToRemove[key] - ) - - // Filter out keys that are present in other active presets - const finalFiltersToRemove = filtersToRemove.filter((key) => { - return !activePresets - .filter((preset) => preset !== idx) - .some((preset) => presetFilters[preset].presetFilters[key]) - }) - - // Set inputState of filters to false for the filters being removed - const updatedFilters = filters.map((filter) => ({ - ...filter, - items: filter.items.map((item) => ({ - ...item, - inputState: finalFiltersToRemove.includes(item.filterKey) - ? false - : item.inputState, - options: item.options.map((option) => ({ - ...option, - inputState: finalFiltersToRemove.includes(option.filterKey) - ? false - : option.inputState, - })), - })), - })) - setFilters(updatedFilters) - - setActivePresets(activePresets.filter((item) => item !== idx)) - } else { - const newActivePresets = activePresets.concat(idx) - trackCustomEvent({ - eventCategory: "UserPersona", - eventAction: `${presetFilters[idx].title}`, - eventName: `${presetFilters[idx].title} true`, - }) - setActivePresets(newActivePresets) - - // Apply the filters for the selected preset - const combinedPresetFilters = newActivePresets.reduce((acc, idx) => { - const preset = presetFilters[idx].presetFilters - Object.keys(preset).forEach((key) => { - acc[key] = acc[key] || preset[key] - }) - return acc - }, {}) - - const updatedFilters = filters.map((filter) => ({ - ...filter, - items: filter.items.map((item) => ({ - ...item, - // Keep existing inputState if true, otherwise apply preset filter - inputState: - item.inputState || - (item.ignoreFilterReset - ? item.inputState - : combinedPresetFilters[item.filterKey] || false), - options: item.options.map((option) => ({ - ...option, - // Keep existing inputState if true, otherwise apply preset filter - inputState: - option.inputState || - (option.ignoreFilterReset - ? option.inputState - : combinedPresetFilters[option.filterKey] || false), - })), - })), - })) - setFilters(updatedFilters) - } - } - - // Update activePresets based on current filters const updateFilters = useCallback( - (filter: FilterOption, filterIndex: number) => { + (filters: FilterOption | FilterOption[]) => { setFilters((prevFilters) => { - return prevFilters.map((prevFilter, idx) => { - if (idx !== filterIndex) return prevFilter + return prevFilters.map((prevFilter) => { + const filter = Array.isArray(filters) + ? filters.find((f) => f.title === prevFilter.title) + : filters.title === prevFilter.title + ? filters + : prevFilter + if (!filter) return prevFilter return filter }) }) - - const currentFilters = {} - - filters.forEach((filter) => { - filter.items.forEach((item) => { - if (item.inputState === true) { - currentFilters[item.filterKey] = item.inputState - } - - if (item.options && item.options.length > 0) { - item.options.forEach((option) => { - if (option.inputState === true) { - currentFilters[option.filterKey] = option.inputState - } - }) - } - }) - }) - - const presetsToApply = presetFilters.reduce( - (acc, preset, idx) => { - const presetFilters = preset.presetFilters - const activePresetKeys = Object.keys(presetFilters).filter( - (key) => presetFilters[key] - ) - const allItemsInCurrentFilters = activePresetKeys.every( - (key) => currentFilters[key] !== undefined - ) - - if (allItemsInCurrentFilters) { - acc.push(idx) - } - return acc - }, - [] - ) - - setActivePresets((prevActivePresets) => { - const newActivePresets = [ - ...new Set([...prevActivePresets, ...presetsToApply]), - ] - return newActivePresets.filter((idx) => presetsToApply.includes(idx)) - }) }, [] ) @@ -349,8 +226,8 @@ const ProductTable = ({ {presetFilters.length ? ( ) : ( @@ -364,8 +241,6 @@ const ProductTable = ({ setFilters={updateFilters} presets={presetFilters} presetFiltersCounts={presetFiltersCounts} - activePresets={activePresets} - handleSelectPreset={handleSelectPreset} dataCount={filteredData.length} activeFiltersCount={activeFiltersCount} mobileFiltersOpen={mobileFiltersOpen} From 4e2e6d89de5e1b56736247f37f010fc485de298c Mon Sep 17 00:00:00 2001 From: Pablo Pettinari Date: Wed, 10 Sep 2025 19:18:49 +0200 Subject: [PATCH 12/21] separate the product list into its own file --- src/components/ProductTable/List.tsx | 109 +++++++++++++++++ src/components/ProductTable/index.tsx | 162 +++++++------------------- 2 files changed, 150 insertions(+), 121 deletions(-) create mode 100644 src/components/ProductTable/List.tsx diff --git a/src/components/ProductTable/List.tsx b/src/components/ProductTable/List.tsx new file mode 100644 index 00000000000..4e3c4671bfc --- /dev/null +++ b/src/components/ProductTable/List.tsx @@ -0,0 +1,109 @@ +import { useLayoutEffect, useRef } from "react" +import { useCallback } from "react" +import { useWindowVirtualizer } from "@tanstack/react-virtual" + +import type { FilterOption, Wallet } from "@/lib/types" + +import { trackCustomEvent } from "@/lib/utils/matomo" + +import WalletInfo from "../FindWalletProductTable/WalletInfo" +import { + Collapsible, + CollapsibleContent, + CollapsibleTrigger, +} from "../ui/collapsible" + +type ListProps = { + data: T[] + subComponent?: ( + item: T, + filters: FilterOption[], + listIdx: number + ) => React.ReactNode + matomoEventCategory: string + filters: FilterOption[] +} + +const List = ({ + data, + subComponent, + matomoEventCategory, + filters, +}: ListProps) => { + const parentRef = useRef(null) + + const parentOffsetRef = useRef(0) + + useLayoutEffect(() => { + parentOffsetRef.current = parentRef.current?.offsetTop ?? 0 + }, []) + + const virtualizer = useWindowVirtualizer({ + count: data.length, + estimateSize: () => 250, + overscan: 5, + scrollMargin: parentOffsetRef.current, + }) + + const previousExpandedRef = useRef>({}) + + const handleExpandedChange = useCallback( + (open: boolean, item: T) => { + if (!open) return + + const expandedOnce = previousExpandedRef.current[item.id] + + if (!expandedOnce) { + trackCustomEvent({ + eventCategory: matomoEventCategory, + eventAction: "expanded", + eventName: item.id, + }) + } + + previousExpandedRef.current = { + ...previousExpandedRef.current, + [item.id]: true, + } + }, + [matomoEventCategory] + ) + + return ( +
+ {virtualizer.getVirtualItems().map((virtualItem) => { + const item = data[virtualItem.index] + + return ( + handleExpandedChange(open, item)} + > + +
+ +
+
+ + {subComponent?.(item as T, filters, virtualItem.index)} + +
+ ) + })} +
+ ) +} + +export default List diff --git a/src/components/ProductTable/index.tsx b/src/components/ProductTable/index.tsx index af8ed35d5c3..11b07256ece 100644 --- a/src/components/ProductTable/index.tsx +++ b/src/components/ProductTable/index.tsx @@ -1,29 +1,15 @@ -import { - useCallback, - useEffect, - useLayoutEffect, - useMemo, - useRef, - useState, -} from "react" +import { useCallback, useEffect, useMemo, useState } from "react" import { useSearchParams } from "next/navigation" -import { useWindowVirtualizer } from "@tanstack/react-virtual" -import type { FilterOption, TPresetFilters, Wallet } from "@/lib/types" +import type { FilterOption, TPresetFilters } from "@/lib/types" // import Filters from "@/components/ProductTable/Filters" import MobileFilters from "@/components/ProductTable/MobileFilters" import PresetFilters from "@/components/ProductTable/PresetFilters" -import { trackCustomEvent } from "@/lib/utils/matomo" - -import WalletInfo from "../FindWalletProductTable/WalletInfo" import Translation from "../Translation" -import { - Collapsible, - CollapsibleContent, - CollapsibleTrigger, -} from "../ui/collapsible" + +import List from "./List" interface ProductTableProps { data: T[] @@ -75,6 +61,27 @@ const getActiveFiltersCount = (filters: FilterOption[]) => { }, 0) } +const parseQueryParams = (queryValue: unknown) => { + // Handle boolean values + if (queryValue === "true") return true + if (queryValue === "false") return false + + // Handle array values + if ( + typeof queryValue === "string" && + queryValue.startsWith("[") && + queryValue.endsWith("]") + ) { + try { + return JSON.parse(decodeURIComponent(queryValue)) + } catch { + return undefined + } + } + + return undefined +} + const ProductTable = ({ data, filters: initialFilters, @@ -91,31 +98,6 @@ const ProductTable = ({ const [filters, setFilters] = useState(initialFilters) const [mobileFiltersOpen, setMobileFiltersOpen] = useState(false) - const filteredData = useMemo(() => { - return filterFn(data, filters) - }, [data, filters, filterFn]) - - const parseQueryParams = (queryValue: unknown) => { - // Handle boolean values - if (queryValue === "true") return true - if (queryValue === "false") return false - - // Handle array values - if ( - typeof queryValue === "string" && - queryValue.startsWith("[") && - queryValue.endsWith("]") - ) { - try { - return JSON.parse(decodeURIComponent(queryValue)) - } catch { - return undefined - } - } - - return undefined - } - // Update filters based on router query useEffect(() => { const query = Object.fromEntries(searchParams?.entries() ?? []) @@ -142,12 +124,6 @@ const ProductTable = ({ // eslint-disable-next-line react-hooks/exhaustive-deps }, [searchParams]) - // Reset filters - const resetFilters = useCallback(() => { - setFilters(initialFilters) - onResetFilters?.() - }, [initialFilters, onResetFilters]) - const updateFilters = useCallback( (filters: FilterOption | FilterOption[]) => { setFilters((prevFilters) => { @@ -165,6 +141,16 @@ const ProductTable = ({ [] ) + const resetFilters = useCallback(() => { + setFilters(initialFilters) + onResetFilters?.() + }, [initialFilters, onResetFilters]) + + // Calculated data + const filteredData = useMemo(() => { + return filterFn(data, filters) + }, [data, filters, filterFn]) + const presetFiltersCounts = useMemo(() => { return presetFilters.map((persona) => { const activeFilters = Object.entries(persona.presetFilters).filter( @@ -177,45 +163,6 @@ const ProductTable = ({ }) }, [filteredData, presetFilters]) - const parentRef = useRef(null) - - const parentOffsetRef = useRef(0) - - useLayoutEffect(() => { - parentOffsetRef.current = parentRef.current?.offsetTop ?? 0 - }, []) - - const virtualizer = useWindowVirtualizer({ - count: filteredData.length, - estimateSize: () => 250, - overscan: 5, - scrollMargin: parentOffsetRef.current, - }) - - const previousExpandedRef = useRef>({}) - - const handleExpandedChange = useCallback( - (open: boolean, item: T) => { - if (!open) return - - const expandedOnce = previousExpandedRef.current[item.id] - - if (!expandedOnce) { - trackCustomEvent({ - eventCategory: matomoEventCategory, - eventAction: "expanded", - eventName: item.id, - }) - } - - previousExpandedRef.current = { - ...previousExpandedRef.current, - [item.id]: true, - } - }, - [matomoEventCategory] - ) - const activeFiltersCount = useMemo( () => getActiveFiltersCount(filters), [filters] @@ -271,39 +218,12 @@ const ProductTable = ({ noResultsComponent && noResultsComponent(resetFilters)} -
- {virtualizer.getVirtualItems().map((virtualItem) => { - const item = filteredData[virtualItem.index] - - return ( - handleExpandedChange(open, item)} - > - -
- -
-
- - {subComponent?.(item as T, filters, virtualItem.index)} - -
- ) - })} -
+
From 0352c35b6312e2e00c15a0d33d5d5de2e7ea8126 Mon Sep 17 00:00:00 2001 From: Pablo Pettinari Date: Wed, 10 Sep 2025 19:24:56 +0200 Subject: [PATCH 13/21] set media queries for desktop filters in mobile --- src/components/ProductTable/index.tsx | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/src/components/ProductTable/index.tsx b/src/components/ProductTable/index.tsx index 11b07256ece..a5a85c27120 100644 --- a/src/components/ProductTable/index.tsx +++ b/src/components/ProductTable/index.tsx @@ -3,10 +3,13 @@ import { useSearchParams } from "next/navigation" import type { FilterOption, TPresetFilters } from "@/lib/types" -// import Filters from "@/components/ProductTable/Filters" +import Filters from "@/components/ProductTable/Filters" import MobileFilters from "@/components/ProductTable/MobileFilters" import PresetFilters from "@/components/ProductTable/PresetFilters" +import { breakpointAsNumber } from "@/lib/utils/screen" + +import MediaQuery from "../MediaQuery" import Translation from "../Translation" import List from "./List" @@ -196,14 +199,16 @@ const ProductTable = ({ mobileFiltersLabel={mobileFiltersLabel} />
-
- {/* */} -
+ +
+ +
+
From 36701080a1ee6bc4202f6329177b1bd64ba4eb41 Mon Sep 17 00:00:00 2001 From: Pablo Pettinari Date: Thu, 11 Sep 2025 09:09:09 +0200 Subject: [PATCH 14/21] feat(ProductTable): add children render function to customize content --- .../FindWalletProductTable/index.tsx | 64 +++++++++----- .../hooks/useNetworkColumns.tsx | 2 +- src/components/Layer2NetworksTable/index.tsx | 86 +++++++++++-------- src/components/ProductTable/index.tsx | 53 +++++------- 4 files changed, 114 insertions(+), 91 deletions(-) diff --git a/src/components/FindWalletProductTable/index.tsx b/src/components/FindWalletProductTable/index.tsx index 8bbd9ee9e43..a0496153e47 100644 --- a/src/components/FindWalletProductTable/index.tsx +++ b/src/components/FindWalletProductTable/index.tsx @@ -8,6 +8,8 @@ import ProductTable from "@/components/ProductTable" import { trackCustomEvent } from "@/lib/utils/matomo" +import List from "../ProductTable/List" + import FindWalletsNoResults from "./FindWalletsNoResults" import WalletSubComponent from "./WalletSubComponent" @@ -70,15 +72,6 @@ const FindWalletProductTable = ({ wallets }: { wallets: WalletRow[] }) => { const walletPersonas = useWalletPersonaPresets() const walletFilterOptions = useWalletFilters() - // Reset filters - const resetFilters = () => { - trackCustomEvent({ - eventCategory: "WalletFilterSidebar", - eventAction: "Reset button", - eventName: "reset_click", - }) - } - if (!Array.isArray(wallets)) { return
Error loading wallets
} @@ -86,23 +79,50 @@ const FindWalletProductTable = ({ wallets }: { wallets: WalletRow[] }) => { return ( data={wallets} - matomoEventCategory="find-wallet" filters={walletFilterOptions} filterFn={filterFn} presetFilters={walletPersonas} - onResetFilters={resetFilters} - subComponent={(wallet, filters, listIdx) => ( - - )} - noResultsComponent={(resetFilters) => ( - - )} mobileFiltersLabel={t("page-find-wallet-see-wallets")} - /> + > + {({ filteredData, filters, resetFilters }) => ( + <> +
+
+

+ {t("page-find-wallet-showing-all-wallets")}{" "} + ({filteredData.length}) +

+
+
+ + {filteredData.length === 0 && ( + { + resetFilters() + trackCustomEvent({ + eventCategory: "WalletFilterSidebar", + eventAction: "Reset button", + eventName: "reset_click", + }) + }} + /> + )} + + ( + + )} + filters={filters} + matomoEventCategory="find-wallet" + /> + + )} + ) } diff --git a/src/components/Layer2NetworksTable/hooks/useNetworkColumns.tsx b/src/components/Layer2NetworksTable/hooks/useNetworkColumns.tsx index ed9bb70fb89..5af5d6184bc 100644 --- a/src/components/Layer2NetworksTable/hooks/useNetworkColumns.tsx +++ b/src/components/Layer2NetworksTable/hooks/useNetworkColumns.tsx @@ -17,7 +17,7 @@ import { TableCell, TableHead } from "@/components/ui/table" import { cn } from "@/lib/utils/cn" import { trackCustomEvent } from "@/lib/utils/matomo" -export const useNetworkColumns: ColumnDef[] = [ +export const useNetworkColumns: ColumnDef[] = [ { id: "l2Info", header: ({ table }) => { diff --git a/src/components/Layer2NetworksTable/index.tsx b/src/components/Layer2NetworksTable/index.tsx index e289b041544..082b5f9d687 100644 --- a/src/components/Layer2NetworksTable/index.tsx +++ b/src/components/Layer2NetworksTable/index.tsx @@ -1,5 +1,3 @@ -import { useMemo, useState } from "react" - import { ExtendedRollup, FilterOption, Lang } from "@/lib/types" import { useNetworkColumns } from "@/components/Layer2NetworksTable/hooks/useNetworkColumns" @@ -10,6 +8,8 @@ import ProductTable from "@/components/ProductTable" import { trackCustomEvent } from "@/lib/utils/matomo" +import DataTable from "../DataTable" + import useTranslation from "@/hooks/useTranslation" const Layer2NetworksTable = ({ @@ -22,15 +22,20 @@ const Layer2NetworksTable = ({ mainnetData: ExtendedRollup }) => { const networkFilterOptions = useNetworkFilters() - const [filters, setFilters] = useState(networkFilterOptions) const { t } = useTranslation("page-layer-2-networks") - const filteredData = useMemo(() => { - const networks = [mainnetData, ...layer2Data] + const networks = [mainnetData, ...layer2Data].map((network) => ({ + ...network, + id: network.name, + })) - const filteredData = networks + const filterFn = ( + networks: (ExtendedRollup & { id: string })[], + filters: FilterOption[] + ) => { + return networks .filter((network) => { - if (network === mainnetData) return true + if (network.name === mainnetData.name) return true const maturityFilter = filters[1].items.find( (item) => item.filterKey === network.networkMaturity @@ -43,40 +48,49 @@ const Layer2NetworksTable = ({ filters[0].items[0].inputState as string ) }) - - return filteredData - }, [layer2Data, mainnetData, filters]) - - const resetFilters = () => { - setFilters(networkFilterOptions) - trackCustomEvent({ - eventCategory: "Layer2NetworksTable", - eventAction: "Reset button", - eventName: "reset_click", - }) } return ( - + data={networks} + filters={networkFilterOptions} presetFilters={[]} - resetFilters={resetFilters} - setFilters={setFilters} - subComponent={(network) => { - return - }} - noResultsComponent={() => ( - - )} + filterFn={filterFn} mobileFiltersLabel={t("page-layer-2-networks-transaction-see-networks")} - /> + > + {({ + filteredData, + setMobileFiltersOpen, + resetFilters, + activeFiltersCount, + }) => ( + + variant="product" + data={filteredData} + columns={useNetworkColumns} + subComponent={(network) => } + noResultsComponent={() => ( + { + resetFilters() + trackCustomEvent({ + eventCategory: "Layer2NetworksTable", + eventAction: "Reset button", + eventName: "reset_click", + }) + }} + /> + )} + matomoEventCategory="l2_networks" + allDataLength={layer2Data.length} + activeFiltersCount={activeFiltersCount} + setMobileFiltersOpen={setMobileFiltersOpen} + meta={{ + locale: locale, + }} + /> + )} + ) } diff --git a/src/components/ProductTable/index.tsx b/src/components/ProductTable/index.tsx index a5a85c27120..505b90517fc 100644 --- a/src/components/ProductTable/index.tsx +++ b/src/components/ProductTable/index.tsx @@ -10,9 +10,6 @@ import PresetFilters from "@/components/ProductTable/PresetFilters" import { breakpointAsNumber } from "@/lib/utils/screen" import MediaQuery from "../MediaQuery" -import Translation from "../Translation" - -import List from "./List" interface ProductTableProps { data: T[] @@ -20,14 +17,20 @@ interface ProductTableProps { filterFn: (data: T[], filters: FilterOption[]) => T[] presetFilters: TPresetFilters onResetFilters?: () => void - subComponent?: ( - item: T, - filters: FilterOption[], - listIdx: number - ) => React.ReactNode - noResultsComponent?: (resetFilters: () => void) => React.ReactNode mobileFiltersLabel: string - matomoEventCategory: string + children: ({ + filteredData, + filters, + setMobileFiltersOpen, + resetFilters, + activeFiltersCount, + }: { + filteredData: T[] + filters: FilterOption[] + setMobileFiltersOpen: (open: boolean) => void + resetFilters: () => void + activeFiltersCount: number + }) => React.ReactNode | undefined } const getActiveFiltersCount = (filters: FilterOption[]) => { @@ -91,10 +94,8 @@ const ProductTable = ({ filterFn, presetFilters, onResetFilters, - subComponent, - noResultsComponent, mobileFiltersLabel, - matomoEventCategory, + children, }: ProductTableProps) => { const searchParams = useSearchParams() @@ -210,25 +211,13 @@ const ProductTable = ({
-
-
-

- {" "} - ({filteredData.length}) -

-
-
- - {filteredData.length === 0 && - noResultsComponent && - noResultsComponent(resetFilters)} - - + {children({ + filteredData, + filters, + setMobileFiltersOpen, + resetFilters, + activeFiltersCount, + })}
From f13245fc88b21c542ce11c86b70f424130bb0ef5 Mon Sep 17 00:00:00 2001 From: Pablo Pettinari Date: Thu, 11 Sep 2025 14:29:02 +0200 Subject: [PATCH 15/21] avoid layout shift around ChainImages --- src/components/ChainImages/index.tsx | 10 ++++++++-- src/components/ProductTable/List.tsx | 12 ++++++------ 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/src/components/ChainImages/index.tsx b/src/components/ChainImages/index.tsx index 86e648f2999..d6f64cd2ba0 100644 --- a/src/components/ChainImages/index.tsx +++ b/src/components/ChainImages/index.tsx @@ -31,12 +31,18 @@ const ChainImages = ({ (network) => network.chainName === chain ) return ( -
+
({ const parentOffsetRef = useRef(0) - useLayoutEffect(() => { - parentOffsetRef.current = parentRef.current?.offsetTop ?? 0 - }, []) - const virtualizer = useWindowVirtualizer({ count: data.length, - estimateSize: () => 250, + estimateSize: () => 300, overscan: 5, scrollMargin: parentOffsetRef.current, }) + useLayoutEffect(() => { + parentOffsetRef.current = parentRef.current?.offsetTop ?? 0 + }, []) + const previousExpandedRef = useRef>({}) const handleExpandedChange = useCallback( @@ -72,7 +72,7 @@ const List = ({ return (
Date: Thu, 11 Sep 2025 14:32:39 +0200 Subject: [PATCH 16/21] fix autoscroll when item expands or collapse --- src/components/ProductTable/List.tsx | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/components/ProductTable/List.tsx b/src/components/ProductTable/List.tsx index 9882fc71f12..ce6420a7102 100644 --- a/src/components/ProductTable/List.tsx +++ b/src/components/ProductTable/List.tsx @@ -49,6 +49,13 @@ const List = ({ const handleExpandedChange = useCallback( (open: boolean, item: T) => { + // Disable scroll position adjustment during expansion or collapse + // ref https://github.com/TanStack/virtual/issues/562#issuecomment-2065858040 + virtualizer.shouldAdjustScrollPositionOnItemSizeChange = () => false + setTimeout(() => { + virtualizer.shouldAdjustScrollPositionOnItemSizeChange = undefined + }, 0) + if (!open) return const expandedOnce = previousExpandedRef.current[item.id] @@ -66,7 +73,7 @@ const List = ({ [item.id]: true, } }, - [matomoEventCategory] + [matomoEventCategory, virtualizer] ) return ( From f8c813d8b5beedd0d090f995bf982e07f0aa6bcb Mon Sep 17 00:00:00 2001 From: Pablo Pettinari Date: Thu, 11 Sep 2025 14:54:09 +0200 Subject: [PATCH 17/21] add dialog title and description for sr support --- src/components/ProductTable/MobileFilters.tsx | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/components/ProductTable/MobileFilters.tsx b/src/components/ProductTable/MobileFilters.tsx index 124834d9634..c5679e4a3a6 100644 --- a/src/components/ProductTable/MobileFilters.tsx +++ b/src/components/ProductTable/MobileFilters.tsx @@ -9,7 +9,10 @@ import { Drawer, DrawerClose, DrawerContent, + DrawerDescription, DrawerFooter, + DrawerHeader, + DrawerTitle, DrawerTrigger, } from "@/components/ui/drawer" @@ -83,6 +86,12 @@ const MobileFilters = ({
+ + Filters + + {`${activeFiltersCount} ${t("table-active")}`} + +
Date: Thu, 11 Sep 2025 15:04:07 +0200 Subject: [PATCH 18/21] control expanded state in List to keep state when virtualizer unmounts the items --- src/components/ProductTable/List.tsx | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/src/components/ProductTable/List.tsx b/src/components/ProductTable/List.tsx index ce6420a7102..4acf413aa89 100644 --- a/src/components/ProductTable/List.tsx +++ b/src/components/ProductTable/List.tsx @@ -1,4 +1,4 @@ -import { useLayoutEffect, useRef } from "react" +import { useLayoutEffect, useRef, useState } from "react" import { useCallback } from "react" import { useWindowVirtualizer } from "@tanstack/react-virtual" @@ -30,8 +30,9 @@ const List = ({ matomoEventCategory, filters, }: ListProps) => { - const parentRef = useRef(null) + const [expanded, setExpanded] = useState>({}) + const parentRef = useRef(null) const parentOffsetRef = useRef(0) const virtualizer = useWindowVirtualizer({ @@ -56,6 +57,11 @@ const List = ({ virtualizer.shouldAdjustScrollPositionOnItemSizeChange = undefined }, 0) + setExpanded((prev) => ({ + ...prev, + [item.id]: open, + })) + if (!open) return const expandedOnce = previousExpandedRef.current[item.id] @@ -92,11 +98,14 @@ const List = ({ key={virtualItem.key} data-index={virtualItem.index} ref={virtualizer.measureElement} + // the virtualizer will re-render the item and reset the open state + // so we need to preserve the open state when the item is unmounted + open={expanded[item.id]} + onOpenChange={(open) => handleExpandedChange(open, item)} className="group/collapsible absolute left-0 top-0 flex w-full cursor-pointer flex-col border-b hover:bg-background-highlight data-[state=open]:bg-background-highlight" style={{ transform: `translateY(${virtualItem.start - virtualizer.options.scrollMargin}px)`, }} - onOpenChange={(open) => handleExpandedChange(open, item)} >
From 2272bc12455772544076e847f2bfabc15b633f8a Mon Sep 17 00:00:00 2001 From: Pablo Pettinari Date: Thu, 11 Sep 2025 15:14:31 +0200 Subject: [PATCH 19/21] fix type erros --- app/[locale]/apps/[application]/page.tsx | 2 +- .../FindWalletProductTable/FindWalletProductTable.stories.tsx | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/app/[locale]/apps/[application]/page.tsx b/app/[locale]/apps/[application]/page.tsx index 67886ad74a3..959b288d04c 100644 --- a/app/[locale]/apps/[application]/page.tsx +++ b/app/[locale]/apps/[application]/page.tsx @@ -8,7 +8,7 @@ import { import { ChainName } from "@/lib/types" -import { ChainImages } from "@/components/ChainImages" +import ChainImages from "@/components/ChainImages" import { ChevronNext } from "@/components/Chevron" import I18nProvider from "@/components/I18nProvider" import Discord from "@/components/icons/discord.svg" diff --git a/src/components/FindWalletProductTable/FindWalletProductTable.stories.tsx b/src/components/FindWalletProductTable/FindWalletProductTable.stories.tsx index 3b6e064c9dc..46b5ac53c5b 100644 --- a/src/components/FindWalletProductTable/FindWalletProductTable.stories.tsx +++ b/src/components/FindWalletProductTable/FindWalletProductTable.stories.tsx @@ -134,6 +134,7 @@ export const WalletProductTableStory: Story = { wallets: walletsData.map((wallet) => { return { ...wallet, + id: wallet.name, languages_supported: wallet.languages_supported as Lang[], supportedLanguages: wallet.languages_supported as Lang[], supported_chains: [], From d03dc5da50650d81f74ff61b43cd490b831827ef Mon Sep 17 00:00:00 2001 From: Pablo Pettinari Date: Thu, 11 Sep 2025 18:57:39 +0200 Subject: [PATCH 20/21] cleanup functions --- .../FindWalletProductTable/index.tsx | 55 +--------- src/components/ProductTable/List.tsx | 1 + src/components/ProductTable/PresetFilters.tsx | 39 +------ src/components/ProductTable/index.tsx | 57 +--------- src/lib/product-table/index.ts | 96 +++++++++++++++++ src/lib/types.ts | 2 + src/lib/utils/wallets.ts | 101 ++++++++++-------- 7 files changed, 161 insertions(+), 190 deletions(-) create mode 100644 src/lib/product-table/index.ts diff --git a/src/components/FindWalletProductTable/index.tsx b/src/components/FindWalletProductTable/index.tsx index a0496153e47..d0138578ee3 100644 --- a/src/components/FindWalletProductTable/index.tsx +++ b/src/components/FindWalletProductTable/index.tsx @@ -1,12 +1,13 @@ "use client" -import { ChainName, FilterOption, Lang, Wallet } from "@/lib/types" +import { WalletRow } from "@/lib/types" import { useWalletFilters } from "@/components/FindWalletProductTable/hooks/useWalletFilters" import { useWalletPersonaPresets } from "@/components/FindWalletProductTable/hooks/useWalletPersonaPresets" import ProductTable from "@/components/ProductTable" import { trackCustomEvent } from "@/lib/utils/matomo" +import { filterFn } from "@/lib/utils/wallets" import List from "../ProductTable/List" @@ -15,58 +16,6 @@ import WalletSubComponent from "./WalletSubComponent" import { useTranslation } from "@/hooks/useTranslation" -function getActiveFilterKeys(filters: FilterOption[]): string[] { - const keys: string[] = [] - filters.forEach((filter) => { - filter.items.forEach((item) => { - if (item.inputState === true && item.options.length === 0) { - keys.push(item.filterKey) - } - if (item.options?.length > 0) { - item.options.forEach((option) => { - if (option.inputState === true) { - keys.push(option.filterKey) - } - }) - } - }) - }) - return keys -} - -const filterFn = (data: WalletRow[], filters: FilterOption[]) => { - let selectedLanguage: string = "" - let selectedLayer2: ChainName[] = [] - - const activeFilterKeys = getActiveFilterKeys(filters) - - filters.forEach((filter) => { - filter.items.forEach((item) => { - if (item.filterKey === "languages") { - selectedLanguage = item.inputState as string - } else if (item.filterKey === "layer_2_support") { - selectedLayer2 = (item.inputState as ChainName[]) || [] - } - }) - }) - - return data - .filter((item) => { - return item.languages_supported.includes(selectedLanguage as Lang) - }) - .filter((item) => { - return ( - selectedLayer2.length === 0 || - selectedLayer2.every((chain) => item.supported_chains.includes(chain)) - ) - }) - .filter((item) => { - return activeFilterKeys.every((key) => item[key]) - }) -} - -type WalletRow = Wallet & { id: string } - const FindWalletProductTable = ({ wallets }: { wallets: WalletRow[] }) => { const { t } = useTranslation("page-wallets-find-wallet") const walletPersonas = useWalletPersonaPresets() diff --git a/src/components/ProductTable/List.tsx b/src/components/ProductTable/List.tsx index 4acf413aa89..68d48ff4c60 100644 --- a/src/components/ProductTable/List.tsx +++ b/src/components/ProductTable/List.tsx @@ -64,6 +64,7 @@ const List = ({ if (!open) return + // the following code is used to track the first time a wallet is expanded const expandedOnce = previousExpandedRef.current[item.id] if (!expandedOnce) { diff --git a/src/components/ProductTable/PresetFilters.tsx b/src/components/ProductTable/PresetFilters.tsx index cf37d64cd56..f6df925ccab 100644 --- a/src/components/ProductTable/PresetFilters.tsx +++ b/src/components/ProductTable/PresetFilters.tsx @@ -6,6 +6,8 @@ import type { FilterOption, TPresetFilters } from "@/lib/types" import { cn } from "@/lib/utils/cn" import { trackCustomEvent } from "@/lib/utils/matomo" +import { getActivePresets } from "@/lib/product-table" + export interface PresetFiltersProps { presets: TPresetFilters filters: FilterOption[] @@ -46,41 +48,8 @@ const PresetFilters = ({ presetFiltersCounts, }: PresetFiltersProps) => { const activePresets = useMemo(() => { - const currentFilters = {} - - filters.forEach((filter) => { - filter.items.forEach((item) => { - if (item.inputState === true) { - currentFilters[item.filterKey] = item.inputState - } - - if (item.options && item.options.length > 0) { - item.options.forEach((option) => { - if (option.inputState === true) { - currentFilters[option.filterKey] = option.inputState - } - }) - } - }) - }) - - const presetsToApply = presets.reduce((acc, preset, idx) => { - const presetFilters = preset.presetFilters - const activePresetKeys = Object.keys(presetFilters).filter( - (key) => presetFilters[key] - ) - const allItemsInCurrentFilters = activePresetKeys.every( - (key) => currentFilters[key] !== undefined - ) - - if (allItemsInCurrentFilters) { - acc.push(idx) - } - return acc - }, []) - - return presetsToApply - }, [filters, presets]) + return getActivePresets(presets, filters) + }, [presets, filters]) const handleSelectPreset = useCallback( (idx: number) => { diff --git a/src/components/ProductTable/index.tsx b/src/components/ProductTable/index.tsx index 505b90517fc..0467c21eeb2 100644 --- a/src/components/ProductTable/index.tsx +++ b/src/components/ProductTable/index.tsx @@ -11,6 +11,8 @@ import { breakpointAsNumber } from "@/lib/utils/screen" import MediaQuery from "../MediaQuery" +import { getActiveFiltersCount, parseQueryParams } from "@/lib/product-table" + interface ProductTableProps { data: T[] filters: FilterOption[] @@ -33,61 +35,6 @@ interface ProductTableProps { }) => React.ReactNode | undefined } -const getActiveFiltersCount = (filters: FilterOption[]) => { - return filters.reduce((count, filter) => { - return ( - count + - filter.items.reduce((itemCount, item) => { - if (item.options && item.options.length > 0) { - return ( - itemCount + - item.options.filter( - (option) => - typeof option.inputState === "boolean" && option.inputState - ).length - ) - } - if (Array.isArray(item.inputState) && item.inputState.length > 0) { - return itemCount + 1 - } - - if ( - typeof item.inputState === "string" && - item.filterKey !== "languages" - ) { - return itemCount + 1 - } - - return ( - itemCount + - (typeof item.inputState === "boolean" && item.inputState ? 1 : 0) - ) - }, 0) - ) - }, 0) -} - -const parseQueryParams = (queryValue: unknown) => { - // Handle boolean values - if (queryValue === "true") return true - if (queryValue === "false") return false - - // Handle array values - if ( - typeof queryValue === "string" && - queryValue.startsWith("[") && - queryValue.endsWith("]") - ) { - try { - return JSON.parse(decodeURIComponent(queryValue)) - } catch { - return undefined - } - } - - return undefined -} - const ProductTable = ({ data, filters: initialFilters, diff --git a/src/lib/product-table/index.ts b/src/lib/product-table/index.ts new file mode 100644 index 00000000000..1a447ebfa1d --- /dev/null +++ b/src/lib/product-table/index.ts @@ -0,0 +1,96 @@ +import { FilterOption, TPresetFilters } from "../types" + +export const parseQueryParams = (queryValue: unknown) => { + // Handle boolean values + if (queryValue === "true") return true + if (queryValue === "false") return false + + // Handle array values + if ( + typeof queryValue === "string" && + queryValue.startsWith("[") && + queryValue.endsWith("]") + ) { + try { + return JSON.parse(decodeURIComponent(queryValue)) + } catch { + return undefined + } + } + + return undefined +} + +export const getActiveFiltersCount = (filters: FilterOption[]) => { + return filters.reduce((count, filter) => { + return ( + count + + filter.items.reduce((itemCount, item) => { + if (item.options && item.options.length > 0) { + return ( + itemCount + + item.options.filter( + (option) => + typeof option.inputState === "boolean" && option.inputState + ).length + ) + } + if (Array.isArray(item.inputState) && item.inputState.length > 0) { + return itemCount + 1 + } + + if ( + typeof item.inputState === "string" && + item.filterKey !== "languages" + ) { + return itemCount + 1 + } + + return ( + itemCount + + (typeof item.inputState === "boolean" && item.inputState ? 1 : 0) + ) + }, 0) + ) + }, 0) +} + +export const getActivePresets = ( + presets: TPresetFilters, + filters: FilterOption[] +) => { + const currentFilters = {} + + filters.forEach((filter) => { + filter.items.forEach((item) => { + if (item.inputState === true) { + currentFilters[item.filterKey] = item.inputState + } + + if (item.options && item.options.length > 0) { + item.options.forEach((option) => { + if (option.inputState === true) { + currentFilters[option.filterKey] = option.inputState + } + }) + } + }) + }) + + const presetsToApply = presets.reduce((acc, preset, idx) => { + const presetFilters = preset.presetFilters + const activePresetKeys = Object.keys(presetFilters).filter( + (key) => presetFilters[key] + ) + const allItemsInCurrentFilters = activePresetKeys.every( + (key) => currentFilters[key] !== undefined + ) + + if (allItemsInCurrentFilters) { + acc.push(idx) + } + return acc + }, []) + + return presetsToApply +} diff --git a/src/lib/types.ts b/src/lib/types.ts index a3c106de838..4ce714d8f6b 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -731,6 +731,8 @@ export type Wallet = WalletData & { supportedLanguages: string[] } +export type WalletRow = Wallet & { id: string } + export type WalletFilter = typeof WALLETS_FILTERS_DEFAULT export interface WalletFilterData { diff --git a/src/lib/utils/wallets.ts b/src/lib/utils/wallets.ts index 69faac9c849..4f62afc490b 100644 --- a/src/lib/utils/wallets.ts +++ b/src/lib/utils/wallets.ts @@ -13,7 +13,13 @@ import { NEW_TO_CRYPTO_FEATURES, NFTS_FEATURES, } from "../constants" -import type { Lang, WalletData, WalletFilter } from "../types" +import type { + ChainName, + FilterOption, + Lang, + WalletData, + WalletRow, +} from "../types" export const getSupportedLocaleWallets = (locale: string) => shuffle( @@ -153,42 +159,6 @@ export const getAllWalletsLanguages = (locale: string) => { ) } -// Get a list of top n wallets languages -export const getWalletsTopLanguages = (n: number, locale: string) => { - const compareFn = (a: string, b: string) => { - return getLanguageTotalCount(b) - getLanguageTotalCount(a) - } - - return walletsData - .reduce( - (allLanguagesList, current) => - // `union` lodash method merges all arrays removing duplicates - union(allLanguagesList, current.languages_supported), - [] as string[] - ) - .sort(compareFn) - .map((languageCode) => { - // Get supported language name - const supportedLanguageName = getLanguageCodeName(languageCode, locale) - // Return {code, capitalized language name} - return { - code: languageCode, - langName: `${capitalize( - supportedLanguageName! - )} (${getLanguageTotalCount(languageCode)})`, - } - }) - .slice(0, n) -} - -// Get wallets listing count after applying filters -export const walletsListingCount = (filters: WalletFilter) => { - return Object.values(filters).reduce( - (acc, filter) => (filter ? acc + 1 : acc), - 0 - ) -} - export const getLanguageCountWalletsData = (locale: string) => { const languageCountWalletsData = getAllWalletsLanguages(locale).map( (language) => ({ @@ -201,15 +171,52 @@ export const getLanguageCountWalletsData = (locale: string) => { return languageCountWalletsData } -export const getFilteredWalletsCount = ( - wallets: WalletData[], - filters: WalletFilter -) => { - return wallets.filter((wallet) => { - const activeFilters = Object.entries(filters).filter( - ([, value]) => value === true - ) +function getActiveFilterKeys(filters: FilterOption[]): string[] { + const keys: string[] = [] + filters.forEach((filter) => { + filter.items.forEach((item) => { + if (item.inputState === true && item.options.length === 0) { + keys.push(item.filterKey) + } + if (item.options?.length > 0) { + item.options.forEach((option) => { + if (option.inputState === true) { + keys.push(option.filterKey) + } + }) + } + }) + }) + return keys +} + +export const filterFn = (data: WalletRow[], filters: FilterOption[]) => { + let selectedLanguage: string = "" + let selectedLayer2: ChainName[] = [] + + const activeFilterKeys = getActiveFilterKeys(filters) + + filters.forEach((filter) => { + filter.items.forEach((item) => { + if (item.filterKey === "languages") { + selectedLanguage = item.inputState as string + } else if (item.filterKey === "layer_2_support") { + selectedLayer2 = (item.inputState as ChainName[]) || [] + } + }) + }) - return activeFilters.every(([feature]) => wallet[feature] === true) - }).length + return data + .filter((item) => { + return item.languages_supported.includes(selectedLanguage as Lang) + }) + .filter((item) => { + return ( + selectedLayer2.length === 0 || + selectedLayer2.every((chain) => item.supported_chains.includes(chain)) + ) + }) + .filter((item) => { + return activeFilterKeys.every((key) => item[key]) + }) } From bcb9576cade3932f04031a2dc3ca0ee0e54578b3 Mon Sep 17 00:00:00 2001 From: wackerow <54227730+wackerow@users.noreply.github.com> Date: Mon, 15 Sep 2025 13:56:00 +0100 Subject: [PATCH 21/21] chore: intl patch, directional margins, type import --- src/components/FindWalletProductTable/WalletInfo.tsx | 6 +++--- src/components/FindWalletProductTable/index.tsx | 2 +- src/components/ProductTable/MobileFilters.tsx | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/components/FindWalletProductTable/WalletInfo.tsx b/src/components/FindWalletProductTable/WalletInfo.tsx index ef39fc77b94..30a197d27e4 100644 --- a/src/components/FindWalletProductTable/WalletInfo.tsx +++ b/src/components/FindWalletProductTable/WalletInfo.tsx @@ -79,11 +79,11 @@ const WalletInfo = ({ wallet }: WalletInfoProps) => {
@@ -110,7 +110,7 @@ const WalletInfo = ({ wallet }: WalletInfoProps) => {
diff --git a/src/components/FindWalletProductTable/index.tsx b/src/components/FindWalletProductTable/index.tsx index d0138578ee3..9c5c0f1da98 100644 --- a/src/components/FindWalletProductTable/index.tsx +++ b/src/components/FindWalletProductTable/index.tsx @@ -1,6 +1,6 @@ "use client" -import { WalletRow } from "@/lib/types" +import type { WalletRow } from "@/lib/types" import { useWalletFilters } from "@/components/FindWalletProductTable/hooks/useWalletFilters" import { useWalletPersonaPresets } from "@/components/FindWalletProductTable/hooks/useWalletPersonaPresets" diff --git a/src/components/ProductTable/MobileFilters.tsx b/src/components/ProductTable/MobileFilters.tsx index c5679e4a3a6..3ca6714b111 100644 --- a/src/components/ProductTable/MobileFilters.tsx +++ b/src/components/ProductTable/MobileFilters.tsx @@ -87,7 +87,7 @@ const MobileFilters = ({
- Filters + {t("table-filters")} {`${activeFiltersCount} ${t("table-active")}`}