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/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/package.json b/package.json
index 0a549528985..b965c70dff5 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 2a9b972d16a..86cf3771532 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'}
@@ -13746,8 +13758,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
diff --git a/src/components/ChainImages/index.tsx b/src/components/ChainImages/index.tsx
index 39953b71fe9..d6f64cd2ba0 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 = "",
@@ -29,12 +31,18 @@ export const ChainImages = ({
(network) => network.chainName === chain
)
return (
-
+
)
}
+
+export default memo(ChainImages)
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: [],
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 8bfb24ccc17..30a197d27e4 100644
--- a/src/components/FindWalletProductTable/WalletInfo.tsx
+++ b/src/components/FindWalletProductTable/WalletInfo.tsx
@@ -1,96 +1,124 @@
+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 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 {
wallet: Wallet
- isExpanded: boolean
}
-const WalletInfo = ({ wallet, isExpanded }: WalletInfoProps) => {
+const WalletInfo = ({ wallet }: 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])
return (
-
-
-
-
{wallet.name}
- {walletPersonas.length > 0 && (
-
- {walletPersonas.map((persona) => (
-
- {t(persona)}
-
- ))}
-
- )}
-
-
-
-
-
+ {/* Desktop layout */}
+
+
-
- {walletPersonas.length > 0 && (
-
- {walletPersonas.map((persona) => (
-
- {t(persona)}
-
- ))}
+
+
{wallet.name}
+
+
+
+
+
- )}
+
-
-
+
+
+ {/* Mobile layout */}
+
+
+
+
{
{deviceLabels.length > 0 && (
-
{deviceLabels.join(" · ")}
+
{deviceLabelsText}
)}
- {formatStringList(wallet.supportedLanguages, 5)}{" "}
-
+ {formattedLanguages}{" "}
+ {hasExtraLanguages && (
+
+ )}
@@ -116,18 +146,15 @@ const WalletInfo = ({ wallet, isExpanded }: WalletInfoProps) => {
@@ -141,6 +168,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")}
@@ -150,4 +181,4 @@ const WalletInfo = ({ wallet, isExpanded }: WalletInfoProps) => {
)
}
-export default WalletInfo
+export default memo(WalletInfo)
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 dedaea9a93f..9c5c0f1da98 100644
--- a/src/components/FindWalletProductTable/index.tsx
+++ b/src/components/FindWalletProductTable/index.tsx
@@ -1,131 +1,77 @@
"use client"
-import { useMemo, useState } from "react"
+import type { WalletRow } from "@/lib/types"
-import {
- ChainName,
- FilterOption,
- Lang,
- Wallet,
- WalletFilter,
-} 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"
import { trackCustomEvent } from "@/lib/utils/matomo"
-import { getFilteredWalletsCount } from "@/lib/utils/wallets"
+import { filterFn } from "@/lib/utils/wallets"
+
+import List from "../ProductTable/List"
import FindWalletsNoResults from "./FindWalletsNoResults"
import WalletSubComponent from "./WalletSubComponent"
import { useTranslation } from "@/hooks/useTranslation"
-const FindWalletProductTable = ({ wallets }: { wallets: Wallet[] }) => {
- console.log({ wallets })
+const FindWalletProductTable = ({ wallets }: { wallets: WalletRow[] }) => {
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)
- }
- })
- }
- })
- })
- return keys
- }, [filters])
-
- const filteredData = useMemo(() => {
- if (!Array.isArray(wallets)) return []
-
- let selectedLanguage: string = ""
- let selectedLayer2: 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
- )
- )
- }, [filteredData, walletPersonas])
-
- // Reset filters
- const resetFilters = () => {
- setFilters(walletFilterOptions)
- trackCustomEvent({
- eventCategory: "WalletFilterSidebar",
- eventAction: "Reset button",
- eventName: "reset_click",
- })
- }
if (!Array.isArray(wallets)) {
return Error loading wallets
}
return (
-
- columns={useWalletColumns}
- data={filteredData}
- allDataLength={wallets.length}
- matomoEventCategory="find-wallet"
- filters={filters}
+
+ data={wallets}
+ filters={walletFilterOptions}
+ filterFn={filterFn}
presetFilters={walletPersonas}
- presetFiltersCounts={personasWalletCounts}
- resetFilters={resetFilters}
- setFilters={setFilters}
- subComponent={(wallet, listIdx) => (
-
- )}
- noResultsComponent={() => (
-
- )}
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/Filter.tsx b/src/components/ProductTable/Filter.tsx
new file mode 100644
index 00000000000..37ee1e48e26
--- /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) => void
+}
+
+const Filter = ({ filter, filterIndex, onChange }: FilterProps) => {
+ const handleChange = (
+ _: 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)
+ }
+
+ 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..cecd854fe37 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) => 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/List.tsx b/src/components/ProductTable/List.tsx
new file mode 100644
index 00000000000..68d48ff4c60
--- /dev/null
+++ b/src/components/ProductTable/List.tsx
@@ -0,0 +1,126 @@
+import { useLayoutEffect, useRef, useState } 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 [expanded, setExpanded] = useState>({})
+
+ const parentRef = useRef(null)
+ const parentOffsetRef = useRef(0)
+
+ const virtualizer = useWindowVirtualizer({
+ count: data.length,
+ estimateSize: () => 300,
+ overscan: 5,
+ scrollMargin: parentOffsetRef.current,
+ })
+
+ useLayoutEffect(() => {
+ parentOffsetRef.current = parentRef.current?.offsetTop ?? 0
+ }, [])
+
+ const previousExpandedRef = useRef>({})
+
+ 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)
+
+ setExpanded((prev) => ({
+ ...prev,
+ [item.id]: open,
+ }))
+
+ 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) {
+ trackCustomEvent({
+ eventCategory: matomoEventCategory,
+ eventAction: "expanded",
+ eventName: item.id,
+ })
+ }
+
+ previousExpandedRef.current = {
+ ...previousExpandedRef.current,
+ [item.id]: true,
+ }
+ },
+ [matomoEventCategory, virtualizer]
+ )
+
+ return (
+
+ {virtualizer.getVirtualItems().map((virtualItem) => {
+ const item = data[virtualItem.index]
+
+ return (
+
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)`,
+ }}
+ >
+
+
+
+
+
+
+ {subComponent?.(item as T, filters, virtualItem.index)}
+
+
+ )
+ })}
+
+ )
+}
+
+export default List
diff --git a/src/components/ProductTable/MobileFilters.tsx b/src/components/ProductTable/MobileFilters.tsx
index 9e37d1f9189..3ca6714b111 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"
@@ -21,11 +24,9 @@ import { useTranslation } from "@/hooks/useTranslation"
interface MobileFiltersProps {
filters: FilterOption[]
- setFilters: React.Dispatch>
+ setFilters: (filters: FilterOption | FilterOption[]) => void
presets: TPresetFilters
presetFiltersCounts?: number[]
- activePresets: number[]
- handleSelectPreset: (index: number) => void
dataCount: number
activeFiltersCount: number
mobileFiltersOpen: boolean
@@ -39,8 +40,6 @@ const MobileFilters = ({
setFilters,
presets,
presetFiltersCounts,
- activePresets,
- handleSelectPreset,
dataCount,
activeFiltersCount,
mobileFiltersOpen,
@@ -87,12 +86,18 @@ const MobileFilters = ({
+
+ {t("table-filters")}
+
+ {`${activeFiltersCount} ${t("table-active")}`}
+
+
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(() => {
+ return getActivePresets(presets, filters)
+ }, [presets, filters])
+
+ 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 209fb8bc527..0467c21eeb2 100644
--- a/src/components/ProductTable/index.tsx
+++ b/src/components/ProductTable/index.tsx
@@ -1,80 +1,54 @@
-import {
- Dispatch,
- FC,
- SetStateAction,
- useEffect,
- useMemo,
- useState,
-} from "react"
+import { useCallback, useEffect, useMemo, useState } from "react"
import { useSearchParams } from "next/navigation"
-import { ColumnDef } from "@tanstack/react-table"
import type { FilterOption, TPresetFilters } from "@/lib/types"
-import Table from "@/components/DataTable"
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 { breakpointAsNumber } from "@/lib/utils/screen"
-interface ProductTableProps
{
- columns: ColumnDef[]
+import MediaQuery from "../MediaQuery"
+
+import { getActiveFiltersCount, parseQueryParams } from "@/lib/product-table"
+
+interface ProductTableProps {
data: T[]
- allDataLength: number
filters: FilterOption[]
+ filterFn: (data: T[], filters: FilterOption[]) => T[]
presetFilters: TPresetFilters
- presetFiltersCounts?: number[]
- resetFilters: () => void
- setFilters: Dispatch>
- subComponent?: FC
- noResultsComponent?: React.FC
+ onResetFilters?: () => void
mobileFiltersLabel: string
- matomoEventCategory: string
- meta?: Record
+ children: ({
+ filteredData,
+ filters,
+ setMobileFiltersOpen,
+ resetFilters,
+ activeFiltersCount,
+ }: {
+ filteredData: T[]
+ filters: FilterOption[]
+ setMobileFiltersOpen: (open: boolean) => void
+ resetFilters: () => void
+ activeFiltersCount: number
+ }) => React.ReactNode | undefined
}
-const ProductTable = ({
- columns,
+const ProductTable = ({
data,
- allDataLength,
- filters,
+ filters: initialFilters,
+ filterFn,
presetFilters,
- presetFiltersCounts,
- resetFilters,
- setFilters,
- subComponent,
- noResultsComponent,
+ onResetFilters,
mobileFiltersLabel,
- matomoEventCategory,
- meta,
+ children,
}: ProductTableProps) => {
const searchParams = useSearchParams()
- const [activePresets, setActivePresets] = useState([])
+ const [filters, setFilters] = useState(initialFilters)
const [mobileFiltersOpen, setMobileFiltersOpen] = useState(false)
- 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() ?? [])
@@ -101,177 +75,57 @@ const ProductTable = ({
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [searchParams])
- // 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]
+ const updateFilters = useCallback(
+ (filters: FilterOption | FilterOption[]) => {
+ setFilters((prevFilters) => {
+ 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
})
- 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
- 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 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
- },
- []
- )
+ const resetFilters = useCallback(() => {
+ setFilters(initialFilters)
+ onResetFilters?.()
+ }, [initialFilters, onResetFilters])
- setActivePresets((prevActivePresets) => {
- const newActivePresets = [
- ...new Set([...prevActivePresets, ...presetsToApply]),
- ]
- return newActivePresets.filter((idx) => presetsToApply.includes(idx))
- })
- }, [filters, presetFilters])
+ // Calculated data
+ const filteredData = useMemo(() => {
+ return filterFn(data, filters)
+ }, [data, filters, filterFn])
- // 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
- }
+ const presetFiltersCounts = useMemo(() => {
+ return presetFilters.map((persona) => {
+ const activeFilters = Object.entries(persona.presetFilters).filter(
+ ([, value]) => value === true
+ )
- if (
- typeof item.inputState === "string" &&
- item.filterKey !== "languages"
- ) {
- return itemCount + 1
- }
+ return filteredData.filter((item) => {
+ return activeFilters.every(([feature]) => item[feature] === true)
+ }).length
+ })
+ }, [filteredData, presetFilters])
- return (
- itemCount +
- (typeof item.inputState === "boolean" && item.inputState ? 1 : 0)
- )
- }, 0)
- )
- }, 0)
- }, [filters])
+ const activeFiltersCount = useMemo(
+ () => getActiveFiltersCount(filters),
+ [filters]
+ )
return (
{presetFilters.length ? (
) : (
@@ -282,12 +136,10 @@ const ProductTable =
({
({
mobileFiltersLabel={mobileFiltersLabel}
/>
-
-
-
+
+
+
+
+
-
+ {children({
+ filteredData,
+ filters,
+ setMobileFiltersOpen,
+ resetFilters,
+ activeFiltersCount,
+ })}
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])
+ })
}