Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
938aa22
optimize WalletInfo
pettinarip Sep 8, 2025
f363041
move filters and filtering data state to ProductTable
pettinarip Sep 9, 2025
2f4e8f2
refactor resetFilters
pettinarip Sep 9, 2025
9fb016a
unify filters update to render once
pettinarip Sep 9, 2025
5754c8d
use react-virtual to virtualize product list
pettinarip Sep 9, 2025
179d8d9
reimplement sticky list header
pettinarip Sep 9, 2025
47061e6
implement no results and tracking events for expanded rows
pettinarip Sep 9, 2025
d4f209f
install react-virtual
pettinarip Sep 9, 2025
13752fe
optimize WalletInfo for device resolutions
pettinarip Sep 10, 2025
4516682
prevent unnecessary re-renders by memoizing Filter and using stable u…
pettinarip Sep 10, 2025
35af979
refactor preset filters: derive it from filters and place it in the P…
pettinarip Sep 10, 2025
4e2e6d8
separate the product list into its own file
pettinarip Sep 10, 2025
0352c35
set media queries for desktop filters in mobile
pettinarip Sep 10, 2025
3670108
feat(ProductTable): add children render function to customize content
pettinarip Sep 11, 2025
f13245f
avoid layout shift around ChainImages
pettinarip Sep 11, 2025
7f708c5
fix autoscroll when item expands or collapse
pettinarip Sep 11, 2025
26fd0de
Merge branch 'dev' into optimize-find-wallets
pettinarip Sep 11, 2025
f8c813d
add dialog title and description for sr support
pettinarip Sep 11, 2025
7729c00
control expanded state in List to keep state when virtualizer unmount…
pettinarip Sep 11, 2025
2272bc1
fix type erros
pettinarip Sep 11, 2025
d03dc5d
cleanup functions
pettinarip Sep 11, 2025
bcb9576
chore: intl patch, directional margins, type import
wackerow Sep 15, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion app/[locale]/apps/[application]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
1 change: 1 addition & 0 deletions app/[locale]/wallets/find-wallet/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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!
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
20 changes: 20 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

16 changes: 13 additions & 3 deletions src/components/ChainImages/index.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { memo } from "react"

import { ChainName } from "@/lib/types"

import { Image } from "@/components/Image"
Expand All @@ -11,7 +13,7 @@ interface ChainImagesProps {
className?: string
}

export const ChainImages = ({
const ChainImages = ({
chains,
size = 24,
className = "",
Expand All @@ -29,12 +31,18 @@ export const ChainImages = ({
(network) => network.chainName === chain
)
return (
<div key={chain} className="overflow-hidden rounded-full">
<div
key={chain}
className="inline-flex items-center justify-center overflow-hidden rounded-full leading-none"
style={{
height: `${size}px`,
}}
>
<Tooltip content={chainData?.name || ""}>
<Image
src={chainData?.logo || ""}
alt={`${chain} blockchain network`}
className="rounded-full"
className="block rounded-full"
style={{
objectFit: "contain",
width: `${size}px`,
Expand All @@ -48,3 +56,5 @@ export const ChainImages = ({
</div>
)
}

export default memo(ChainImages)
Original file line number Diff line number Diff line change
Expand Up @@ -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: [],
Expand Down
27 changes: 27 additions & 0 deletions src/components/FindWalletProductTable/PersonaTags.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className="flex flex-row flex-wrap gap-1">
{walletPersonas.map((persona) => (
<Tag key={persona} variant="high-contrast" size="small">
{t(persona)}
</Tag>
))}
</div>
)
}

export default memo(PersonaTags)
177 changes: 104 additions & 73 deletions src/components/FindWalletProductTable/WalletInfo.tsx
Original file line number Diff line number Diff line change
@@ -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<string> = []

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<string> = []
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 (
<div className="flex flex-col gap-4">
<div className="flex flex-row items-center justify-between gap-4">
<div className="flex flex-col gap-4">
<div className="hidden flex-row gap-4 lg:flex">
<Image
src={wallet.image}
alt=""
style={{ objectFit: "contain", width: "56px", height: "56px" }}
/>
<div className="flex flex-col gap-2">
<p className="text-xl font-bold">{wallet.name}</p>
{walletPersonas.length > 0 && (
<div className="flex flex-row flex-wrap gap-1">
{walletPersonas.map((persona) => (
<Tag key={persona} variant="high-contrast" size="small">
{t(persona)}
</Tag>
))}
</div>
)}
<ChainImages
chains={wallet.supported_chains as ChainName[]}
className={`ml-2 mt-1 ${
walletPersonas.length === 0 ? "mb-4" : ""
}`}
/>
</div>
</div>
<div className="flex flex-col gap-4 lg:hidden">
<div className="flex flex-row items-center gap-4">
{/* Desktop layout */}
<MediaQuery queries={[`(min-width: ${breakpointAsNumber.lg}px)`]}>
<div className="hidden flex-row gap-4 lg:flex">
<Image
src={wallet.image}
alt=""
style={{ objectFit: "contain", width: "24px", height: "24px" }}
style={{ objectFit: "contain", width: "56px", height: "56px" }}
/>
<p className="text-xl font-bold">{wallet.name}</p>
</div>
<div>
{walletPersonas.length > 0 && (
<div className="flex flex-row flex-wrap gap-1">
{walletPersonas.map((persona) => (
<Tag key={persona} variant="high-contrast" size="small">
{t(persona)}
</Tag>
))}
<div className="flex flex-col gap-2">
<p className="text-xl font-bold">{wallet.name}</p>

<PersonaTags walletPersonas={walletPersonas} />

<div
className={`ms-2 ${walletPersonas.length === 0 ? "mb-4" : ""} mt-1`}
>
<ChainImages
chains={wallet.supported_chains as ChainName[]}
className={`ms-2 ${walletPersonas.length === 0 ? "mb-4" : ""}`}
/>
</div>
)}
</div>
</div>
<ChainImages
chains={wallet.supported_chains as ChainName[]}
className={`ml-2 ${walletPersonas.length === 0 ? "mb-4" : ""}`}
/>
</div>
</MediaQuery>

{/* Mobile layout */}
<MediaQuery queries={[`(max-width: ${breakpointAsNumber.lg - 1}px)`]}>
<div className="flex flex-col gap-4 lg:hidden">
<div className="flex flex-row items-center gap-4">
<Image
src={wallet.image}
alt=""
style={{
objectFit: "contain",
width: "24px",
height: "24px",
}}
/>
<p className="text-xl font-bold">{wallet.name}</p>
</div>
<div>
<PersonaTags walletPersonas={walletPersonas} />
</div>
<ChainImages
chains={wallet.supported_chains as ChainName[]}
className={`ms-2 ${walletPersonas.length === 0 ? "mb-4" : ""}`}
/>
</div>
</MediaQuery>

<div className="flex flex-row gap-4">
<div className="relative hidden w-14 lg:block">
<div
className={`${isExpanded ? "block" : "hidden"} absolute -bottom-9 -top-0 left-1/2 w-1 -translate-x-1/2 transform ${wallet.twBackgroundColor}`}
className={`absolute -bottom-9 -top-0 left-1/2 hidden w-1 -translate-x-1/2 transform group-data-[state=open]/collapsible:block ${wallet.twBackgroundColor}`}
/>
</div>
<div
Expand All @@ -99,35 +127,34 @@ const WalletInfo = ({ wallet, isExpanded }: WalletInfoProps) => {
{deviceLabels.length > 0 && (
<div className="flex flex-row gap-2">
<DevicesIcon className="size-6" />
<p className="text-md">{deviceLabels.join(" · ")}</p>
<p className="text-md">{deviceLabelsText}</p>
</div>
)}
<div className="flex flex-row gap-2">
<LanguagesIcon className="size-6" />
<p className="text-md">
{formatStringList(wallet.supportedLanguages, 5)}{" "}
<SupportedLanguagesTooltip
supportedLanguages={wallet.supportedLanguages}
/>
{formattedLanguages}{" "}
{hasExtraLanguages && (
<SupportedLanguagesTooltip
supportedLanguages={wallet.supportedLanguages}
/>
)}
</p>
</div>
</div>
</div>
</div>
<div>
<button className="text-primary">
{isExpanded ? (
<ChevronUp className="text-2xl" />
) : (
<ChevronDown className="text-2xl" />
)}
<ChevronUp className="text-2xl group-data-[state=closed]/collapsible:hidden" />
<ChevronDown className="text-2xl group-data-[state=open]/collapsible:hidden" />
</button>
</div>
</div>
<div className="flex flex-row gap-4">
<div className="relative hidden w-14 lg:block">
<div
className={`${isExpanded ? "block" : "hidden"} absolute -bottom-9 -top-0 left-1/2 w-1 -translate-x-1/2 transform ${wallet.twBackgroundColor}`}
className={`absolute -bottom-9 -top-0 left-1/2 hidden w-1 -translate-x-1/2 transform group-data-[state=open]/collapsible:block ${wallet.twBackgroundColor}`}
/>
</div>
<div className="flex flex-1">
Expand All @@ -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")}
</ButtonLink>
Expand All @@ -150,4 +181,4 @@ const WalletInfo = ({ wallet, isExpanded }: WalletInfoProps) => {
)
}

export default WalletInfo
export default memo(WalletInfo)
Loading