Skip to content

Commit ddd2bb7

Browse files
authored
Merge pull request #18845 from ethereum/refactor/extract-catalog-component
refactor: extract config-driven FilterableCatalog from ToolsCatalog (find-wallet revamp PR 1)
2 parents 4fa14c8 + 474860f commit ddd2bb7

8 files changed

Lines changed: 804 additions & 288 deletions

File tree

app/[locale]/developers/tools/_components/ToolsCatalog.tsx

Lines changed: 167 additions & 288 deletions
Large diffs are not rendered by default.
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
"use client"
2+
3+
import { useId } from "react"
4+
5+
import Checkbox from "@/components/ui/checkbox"
6+
7+
import { numberFormat } from "@/lib/utils/numbers"
8+
9+
import type { CatalogCheckboxGroupConfig } from "./types"
10+
11+
type CatalogCheckboxGroupProps = {
12+
locale: string
13+
config: CatalogCheckboxGroupConfig
14+
/** Currently selected option ids */
15+
selectedIds: string[]
16+
onToggle: (optionId: string) => void
17+
}
18+
19+
/**
20+
* Controlled sidebar building block: a labelled group of independent checkboxes.
21+
* Purely presentational — value in (`selectedIds`), event out (`onToggle`); it
22+
* holds no filter state of its own and doesn't know how selections are stored or
23+
* how the group combines with others (that's the consumer's `filterFn`).
24+
*/
25+
export default function CatalogCheckboxGroup({
26+
locale,
27+
config,
28+
selectedIds,
29+
onToggle,
30+
}: CatalogCheckboxGroupProps) {
31+
const nf = numberFormat(locale)
32+
const labelId = useId()
33+
34+
return (
35+
<div role="group" aria-labelledby={labelId} className="space-y-1">
36+
<p id={labelId} className="px-3 py-2 text-sm font-bold">
37+
{config.label}
38+
</p>
39+
{config.options.map((option) => (
40+
<label
41+
key={option.id}
42+
className="flex cursor-pointer items-center gap-2 rounded-md px-3 py-1.5 text-sm hover:bg-background-highlight"
43+
>
44+
<Checkbox
45+
checked={selectedIds.includes(option.id)}
46+
onCheckedChange={() => onToggle(option.id)}
47+
/>
48+
<span className="flex-1 select-none">{option.label}</span>
49+
{typeof option.count === "number" && (
50+
<span className="text-xs text-body-medium">
51+
{nf.format(option.count)}
52+
</span>
53+
)}
54+
</label>
55+
))}
56+
</div>
57+
)
58+
}
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
"use client"
2+
3+
import { ChevronRight } from "lucide-react"
4+
5+
import { Button } from "@/components/ui/buttons/Button"
6+
import { BaseLink } from "@/components/ui/Link"
7+
8+
import { cn } from "@/lib/utils/cn"
9+
import { numberFormat } from "@/lib/utils/numbers"
10+
11+
import type { CatalogNavGroupConfig } from "./types"
12+
13+
type CatalogNavGroupProps = {
14+
locale: string
15+
config: CatalogNavGroupConfig
16+
/** Currently selected child id (single-select), or undefined for none */
17+
selectedChildId?: string
18+
onSelectChild: (childId?: string) => void
19+
}
20+
21+
/**
22+
* Controlled sidebar building block: top-level entries are route links to
23+
* server-rendered listing pages; the children of the current entry are a
24+
* single-select filter. Purely presentational — value in (`selectedChildId`),
25+
* event out (`onSelectChild`); it holds no filter state of its own.
26+
*/
27+
export default function CatalogNavGroup({
28+
locale,
29+
config,
30+
selectedChildId,
31+
onSelectChild,
32+
}: CatalogNavGroupProps) {
33+
const nf = numberFormat(locale)
34+
const hasCurrentItem = config.items.some((item) => item.isCurrent)
35+
36+
return (
37+
<div className="space-y-1">
38+
<BaseLink
39+
href={config.allHref}
40+
className={cn(
41+
"flex w-full items-center justify-between rounded-md px-3 py-2 text-start text-sm no-underline hover:bg-background-highlight",
42+
!hasCurrentItem && "bg-background-highlight text-primary"
43+
)}
44+
>
45+
<span>{config.allLabel}</span>
46+
<span className="text-xs text-body-medium">
47+
{nf.format(config.allCount)}
48+
</span>
49+
</BaseLink>
50+
51+
{config.items.map((item) => {
52+
const isItemActive = item.isCurrent && !selectedChildId
53+
54+
return (
55+
<div key={item.id} className="space-y-1">
56+
<BaseLink
57+
href={item.href}
58+
className={cn(
59+
"flex w-full items-center gap-2 rounded-md px-3 py-2 text-start text-sm no-underline hover:bg-background-highlight",
60+
isItemActive && "bg-background-highlight text-primary"
61+
)}
62+
onClick={() => {
63+
if (item.isCurrent) onSelectChild(undefined)
64+
}}
65+
>
66+
<ChevronRight
67+
className={cn(
68+
"size-4 text-body-medium transition-transform",
69+
item.isCurrent && "rotate-90"
70+
)}
71+
/>
72+
<span className="flex-1">{item.label}</span>
73+
<span className="text-xs text-body-medium">
74+
{nf.format(item.count)}
75+
</span>
76+
</BaseLink>
77+
{item.isCurrent && item.children && (
78+
<div className="ms-5 space-y-1 border-s ps-2">
79+
{item.children.map((child) => (
80+
<Button
81+
key={child.id}
82+
variant="ghost"
83+
isSecondary
84+
className={cn(
85+
"flex min-h-0 w-full items-center justify-between rounded-md px-3 py-1.5 text-start text-xs font-normal hover:bg-background-highlight",
86+
selectedChildId === child.id && "bg-background-highlight"
87+
)}
88+
onClick={() => {
89+
onSelectChild(
90+
selectedChildId === child.id ? undefined : child.id
91+
)
92+
}}
93+
>
94+
<span>{child.label}</span>
95+
{typeof child.count === "number" && (
96+
<span className="text-2xs text-body-medium">
97+
{nf.format(child.count)}
98+
</span>
99+
)}
100+
</Button>
101+
))}
102+
</div>
103+
)}
104+
</div>
105+
)
106+
})}
107+
</div>
108+
)
109+
}

0 commit comments

Comments
 (0)