Skip to content

Commit ca48005

Browse files
authored
feat(search): add package search functionality (#1009)
* chore: snippet_id -> package_id * Update SearchComponent.tsx * feat(search): add package search functionality Introduce a new endpoint `/packages/search` to search for packages by name, description, or content. Update the SearchComponent to use this endpoint and display package search results instead of snippets. This change aligns the search functionality with the package-centric model of the application. * refactor(api): change search endpoint from GET to POST The search endpoint was updated from GET to POST to better handle the search query in the request body. This change aligns with RESTful practices for complex queries and improves security by avoiding query parameters in URLs. The response now includes an `ok` field to indicate success.
1 parent 0d5c5cc commit ca48005

3 files changed

Lines changed: 79 additions & 16 deletions

File tree

fake-snippets-api/lib/db/db-client.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -945,6 +945,50 @@ const initializer = combine(databaseSchema.parse({}), (set, get) => ({
945945
})
946946
.filter((snippet): snippet is Snippet => snippet !== null)
947947
},
948+
searchPackages: (query: string): Package[] => {
949+
const state = get()
950+
const lowercaseQuery = query.toLowerCase()
951+
952+
// Get all packages that are public
953+
const packages = state.packages.filter((pkg) => pkg.is_public === true)
954+
955+
// Find packages that match by name or description
956+
const matchingPackagesByMetadata = packages.filter(
957+
(pkg) =>
958+
pkg.name.toLowerCase().includes(lowercaseQuery) ||
959+
pkg.description?.toLowerCase().includes(lowercaseQuery),
960+
)
961+
962+
// Find packages that match by code content in any file
963+
const matchingFilesByContent = state.packageFiles.filter(
964+
(file) =>
965+
file.content_text?.toLowerCase().includes(lowercaseQuery) ?? false,
966+
)
967+
968+
// Get the packages for matching files
969+
const matchingPackagesByContent = matchingFilesByContent
970+
.map((file) => {
971+
// Find the package release for this file
972+
const packageRelease = state.packageReleases.find(
973+
(pr) => pr.package_release_id === file.package_release_id,
974+
)
975+
if (!packageRelease) return null
976+
977+
// Find the package for this release
978+
return packages.find(
979+
(pkg) =>
980+
pkg.latest_package_release_id === packageRelease.package_release_id,
981+
)
982+
})
983+
.filter((pkg): pkg is NonNullable<typeof pkg> => pkg !== null)
984+
985+
// Combine both sets of matching packages and remove duplicates
986+
const matchingPackages = [
987+
...new Set([...matchingPackagesByMetadata, ...matchingPackagesByContent]),
988+
]
989+
990+
return matchingPackages
991+
},
948992
deleteSnippet: (snippetId: string): boolean => {
949993
let deleted = false
950994
set((state) => {
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { withRouteSpec } from "fake-snippets-api/lib/middleware/with-winter-spec"
2+
import { z } from "zod"
3+
import { packageSchema } from "fake-snippets-api/lib/db/schema"
4+
5+
export default withRouteSpec({
6+
methods: ["POST"],
7+
auth: "none",
8+
jsonBody: z.object({
9+
query: z.string(),
10+
}),
11+
jsonResponse: z.object({
12+
ok: z.boolean(),
13+
packages: z.array(packageSchema),
14+
}),
15+
})(async (req, ctx) => {
16+
const { query } = req.jsonBody
17+
const packages = ctx.db.searchPackages(query)
18+
return ctx.json({ packages, ok: true })
19+
})

src/components/SearchComponent.tsx

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { useSnippetsBaseApiUrl } from "@/hooks/use-snippets-base-api-url"
88
import { PrefetchPageLink } from "./PrefetchPageLink"
99

1010
interface SearchComponentProps {
11-
onResultsFetched?: (results: any[]) => void // optional
11+
onResultsFetched?: (results: any[]) => void
1212
autofocus?: boolean
1313
}
1414

@@ -52,19 +52,19 @@ const SearchComponent: React.FC<SearchComponentProps> = ({
5252
const resultsRef = useRef<HTMLDivElement>(null)
5353
const inputRef = useRef<HTMLInputElement>(null)
5454
const [location] = useLocation()
55-
const { snippetsBaseApiUrl } = useSnippetsBaseApiUrl()
55+
const snippetsBaseApiUrl = useSnippetsBaseApiUrl()
5656

5757
const { data: searchResults, isLoading } = useQuery(
58-
["snippetSearch", searchQuery],
58+
["packageSearch", searchQuery],
5959
async () => {
6060
if (!searchQuery) return []
61-
const { data } = await axios.get("/snippets/search", {
62-
params: { q: searchQuery },
61+
const { data } = await axios.post("/packages/search", {
62+
query: searchQuery,
6363
})
6464
if (onResultsFetched) {
65-
onResultsFetched(data.snippets)
65+
onResultsFetched(data.packages)
6666
}
67-
return data.snippets
67+
return data.packages
6868
},
6969
{ enabled: Boolean(searchQuery) },
7070
)
@@ -128,31 +128,31 @@ const SearchComponent: React.FC<SearchComponentProps> = ({
128128
>
129129
{searchResults.length > 0 ? (
130130
<ul className="divide-y divide-gray-200">
131-
{searchResults.map((snippet: any) => (
132-
<li key={snippet.snippet_id} className="p-2 hover:bg-gray-50">
131+
{searchResults.map((pkg: any) => (
132+
<li key={pkg.package_id} className="p-2 hover:bg-gray-50">
133133
<LinkWithNewTabHandling
134134
href={
135135
shouldOpenInEditor
136-
? `/editor?snippet_id=${snippet.snippet_id}`
137-
: `/${snippet.owner_name}/${snippet.unscoped_name}`
136+
? `/editor?package_id=${pkg.package_id}`
137+
: `/${pkg.name}`
138138
}
139139
shouldOpenInNewTab={shouldOpenInNewTab}
140140
className="flex"
141141
>
142142
<div className="w-12 h-12 overflow-hidden mr-2 flex-shrink-0 rounded-sm">
143143
<img
144-
src={`${useSnippetsBaseApiUrl()}/snippets/images/${snippet.owner_name}/${snippet.unscoped_name}/pcb.svg`}
145-
alt={`PCB preview for ${snippet.name}`}
144+
src={`${snippetsBaseApiUrl}/snippets/images/${pkg.name}/pcb.svg`}
145+
alt={`PCB preview for ${pkg.name}`}
146146
className="w-12 h-12 object-contain p-1 scale-[4] rotate-45"
147147
/>
148148
</div>
149149
<div className="flex-grow">
150150
<div className="font-medium text-blue-600 break-words text-xs">
151-
{snippet.name}
151+
{pkg.name}
152152
</div>
153-
{snippet.description && (
153+
{pkg.description && (
154154
<div className="text-xs text-gray-500 break-words h-8 overflow-hidden">
155-
{snippet.description}
155+
{pkg.description}
156156
</div>
157157
)}
158158
</div>

0 commit comments

Comments
 (0)