Skip to content

Commit 65e5c1c

Browse files
Add search input for Directory list (#8673)
* feat(directory): Added directory search input * feat(directory): Added nuqs for a search state management. Refactor searchFn - includes the description in the search criteria * fear(directory): Added default query value. Added useQueryState limitUrlUpdates 250ms --------- Co-authored-by: shadcn <[email protected]>
1 parent 8a7f05f commit 65e5c1c

File tree

6 files changed

+185
-54
lines changed

6 files changed

+185
-54
lines changed

apps/v4/app/layout.tsx

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { Metadata } from "next"
2+
import { NuqsAdapter } from "nuqs/adapters/next/app"
23

34
import { META_THEME_COLORS, siteConfig } from "@/lib/config"
45
import { fontVariables } from "@/lib/fonts"
@@ -90,12 +91,14 @@ export default function RootLayout({
9091
>
9192
<ThemeProvider>
9293
<LayoutProvider>
93-
<ActiveThemeProvider>
94-
{children}
95-
<TailwindIndicator />
96-
<Toaster position="top-center" />
97-
<Analytics />
98-
</ActiveThemeProvider>
94+
<NuqsAdapter>
95+
<ActiveThemeProvider initialTheme="blue">
96+
{children}
97+
<TailwindIndicator />
98+
<Toaster position="top-center" />
99+
<Analytics />
100+
</ActiveThemeProvider>
101+
</NuqsAdapter>
99102
</LayoutProvider>
100103
</ThemeProvider>
101104
</body>
Lines changed: 60 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
1+
"use client"
2+
13
import * as React from "react"
24
import { IconArrowUpRight } from "@tabler/icons-react"
35

6+
import { useSearchRegistry } from "@/hooks/use-search-registry"
47
import { DirectoryAddButton } from "@/components/directory-add-button"
5-
import registries from "@/registry/directory.json"
8+
import globalRegistries from "@/registry/directory.json"
69
import { Button } from "@/registry/new-york-v4/ui/button"
710
import {
811
Item,
@@ -16,6 +19,8 @@ import {
1619
ItemTitle,
1720
} from "@/registry/new-york-v4/ui/item"
1821

22+
import { SearchDirectory } from "./search-directory"
23+
1924
function getHomepageUrl(homepage: string) {
2025
const url = new URL(homepage)
2126
url.searchParams.set("utm_source", "ui.shadcn.com")
@@ -25,54 +30,61 @@ function getHomepageUrl(homepage: string) {
2530
}
2631

2732
export function DirectoryList() {
33+
const { registries } = useSearchRegistry()
34+
2835
return (
29-
<ItemGroup className="my-8">
30-
{registries.map((registry, index) => (
31-
<React.Fragment key={index}>
32-
<Item className="group/item relative gap-6 px-0 sm:px-4">
33-
<ItemMedia
34-
variant="image"
35-
dangerouslySetInnerHTML={{ __html: registry.logo }}
36-
className="*:[svg]:fill-foreground grayscale *:[svg]:size-8"
37-
/>
38-
<ItemContent>
39-
<ItemTitle>
40-
<a
41-
href={getHomepageUrl(registry.homepage)}
42-
target="_blank"
43-
rel="noopener noreferrer external"
44-
>
45-
{registry.name}
46-
</a>
47-
</ItemTitle>
48-
{registry.description && (
49-
<ItemDescription className="text-pretty">
50-
{registry.description}
51-
</ItemDescription>
52-
)}
53-
</ItemContent>
54-
<ItemActions className="relative z-10 hidden self-start sm:flex">
55-
<Button size="sm" variant="outline" asChild>
56-
<a
57-
href={getHomepageUrl(registry.homepage)}
58-
target="_blank"
59-
rel="noopener noreferrer external"
60-
>
36+
<div className="mt-6">
37+
<SearchDirectory />
38+
<ItemGroup className="my-8">
39+
{registries.map((registry, index) => (
40+
<React.Fragment key={index}>
41+
<Item className="group/item relative gap-6 px-0 sm:px-4">
42+
<ItemMedia
43+
variant="image"
44+
dangerouslySetInnerHTML={{ __html: registry.logo }}
45+
className="*:[svg]:fill-foreground grayscale *:[svg]:size-8"
46+
/>
47+
<ItemContent>
48+
<ItemTitle>
49+
<a
50+
href={getHomepageUrl(registry.homepage)}
51+
target="_blank"
52+
rel="noopener noreferrer external"
53+
>
54+
{registry.name}
55+
</a>
56+
</ItemTitle>
57+
{registry.description && (
58+
<ItemDescription className="text-pretty">
59+
{registry.description}
60+
</ItemDescription>
61+
)}
62+
</ItemContent>
63+
<ItemActions className="relative z-10 hidden self-start sm:flex">
64+
<Button size="sm" variant="outline" asChild>
65+
<a
66+
href={getHomepageUrl(registry.homepage)}
67+
target="_blank"
68+
rel="noopener noreferrer external"
69+
>
70+
View <IconArrowUpRight />
71+
</a>
72+
</Button>
73+
<DirectoryAddButton registry={registry} />
74+
</ItemActions>
75+
<ItemFooter className="justify-start pl-16 sm:hidden">
76+
<Button size="sm" variant="outline">
6177
View <IconArrowUpRight />
62-
</a>
63-
</Button>
64-
<DirectoryAddButton registry={registry} />
65-
</ItemActions>
66-
<ItemFooter className="justify-start pl-16 sm:hidden">
67-
<Button size="sm" variant="outline">
68-
View <IconArrowUpRight />
69-
</Button>
70-
<DirectoryAddButton registry={registry} />
71-
</ItemFooter>
72-
</Item>
73-
{index < registries.length - 1 && <ItemSeparator className="my-1" />}
74-
</React.Fragment>
75-
))}
76-
</ItemGroup>
78+
</Button>
79+
<DirectoryAddButton registry={registry} />
80+
</ItemFooter>
81+
</Item>
82+
{index < globalRegistries.length - 1 && (
83+
<ItemSeparator className="my-1" />
84+
)}
85+
</React.Fragment>
86+
))}
87+
</ItemGroup>
88+
</div>
7789
)
7890
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import * as React from "react"
2+
import { Search, X } from "lucide-react"
3+
4+
import { useSearchRegistry } from "@/hooks/use-search-registry"
5+
import { Field } from "@/registry/new-york-v4/ui/field"
6+
import {
7+
InputGroup,
8+
InputGroupAddon,
9+
InputGroupButton,
10+
InputGroupInput,
11+
} from "@/registry/new-york-v4/ui/input-group"
12+
13+
export const SearchDirectory = () => {
14+
const { query, setQuery } = useSearchRegistry()
15+
16+
const onQueryChange = (e: React.ChangeEvent<HTMLInputElement>) => {
17+
const value = e.target.value
18+
setQuery(value === "" ? null : value)
19+
}
20+
21+
return (
22+
<Field>
23+
<InputGroup>
24+
<InputGroupAddon>
25+
<Search />
26+
</InputGroupAddon>
27+
<InputGroupInput
28+
placeholder="Search directory by name..."
29+
value={query ?? ""}
30+
onChange={onQueryChange}
31+
/>
32+
<InputGroupAddon align="inline-end">
33+
<InputGroupButton
34+
aria-label="Clear"
35+
title="Clear"
36+
size="icon-xs"
37+
onClick={() => setQuery(null)}
38+
>
39+
{query?.length > 0 && <X />}
40+
</InputGroupButton>
41+
</InputGroupAddon>
42+
</InputGroup>
43+
</Field>
44+
)
45+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { debounce, useQueryState } from "nuqs"
2+
3+
import globalRegistries from "@/registry/directory.json"
4+
5+
const normalizeQuery = (query: string) =>
6+
query.toLowerCase().replaceAll(" ", "").replaceAll("@", "")
7+
8+
function finderFn<T extends (typeof globalRegistries)[0]>(
9+
registry: T,
10+
query: string
11+
) {
12+
const normilizedName = normalizeQuery(registry.name)
13+
const normilizedDecription = normalizeQuery(registry.description)
14+
const normilizedQuery = normalizeQuery(query)
15+
16+
return (
17+
normilizedName.includes(normilizedQuery) ||
18+
normilizedDecription.includes(normilizedQuery)
19+
)
20+
}
21+
22+
const searchDirectory = (query: string | null) => {
23+
if (!query) return globalRegistries
24+
25+
return globalRegistries.filter((registry) => finderFn(registry, query))
26+
}
27+
28+
export const useSearchRegistry = () => {
29+
const [query, setQuery] = useQueryState("q", {
30+
defaultValue: "",
31+
limitUrlUpdates: debounce(250),
32+
})
33+
34+
return {
35+
query,
36+
registries: searchDirectory(query),
37+
setQuery,
38+
}
39+
}

apps/v4/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@
8080
"motion": "^12.12.1",
8181
"next": "16.0.0",
8282
"next-themes": "0.4.6",
83+
"nuqs": "^2.7.2",
8384
"postcss": "^8.5.1",
8485
"react": "19.2.0",
8586
"react-day-picker": "^9.7.0",

pnpm-lock.yaml

Lines changed: 31 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)