Implement search functionality - Add searchItems server action with list-scoped and global search - Add highlightText utility function for search result highlighting - Search across title, description, notes, and tags fields - Requirements: 8.1, 8.2, 8.5
This commit is contained in:
@@ -118,4 +118,76 @@ export async function updateList(
|
|||||||
revalidatePath('/')
|
revalidatePath('/')
|
||||||
revalidatePath(`/list/${listId}`)
|
revalidatePath(`/list/${listId}`)
|
||||||
return updated
|
return updated
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function searchItems(query: string, listId?: string) {
|
||||||
|
if (!query.trim()) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
const searchTerm = query.toLowerCase()
|
||||||
|
|
||||||
|
// Build the where clause
|
||||||
|
const whereClause: any = {
|
||||||
|
OR: [
|
||||||
|
{
|
||||||
|
title: {
|
||||||
|
contains: query,
|
||||||
|
mode: 'insensitive' as const,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: {
|
||||||
|
contains: query,
|
||||||
|
mode: 'insensitive' as const,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
notes: {
|
||||||
|
contains: query,
|
||||||
|
mode: 'insensitive' as const,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add list scope if specified
|
||||||
|
if (listId) {
|
||||||
|
whereClause.listId = listId
|
||||||
|
}
|
||||||
|
|
||||||
|
const items = await prisma.item.findMany({
|
||||||
|
where: whereClause,
|
||||||
|
include: {
|
||||||
|
list: true,
|
||||||
|
},
|
||||||
|
orderBy: {
|
||||||
|
updatedAt: 'desc',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// Also search in tags (stored as JSON)
|
||||||
|
const allItems = listId
|
||||||
|
? await prisma.item.findMany({
|
||||||
|
where: { listId },
|
||||||
|
include: { list: true },
|
||||||
|
})
|
||||||
|
: await prisma.item.findMany({
|
||||||
|
include: { list: true },
|
||||||
|
})
|
||||||
|
|
||||||
|
const tagMatches = allItems.filter((item) => {
|
||||||
|
const tags = Array.isArray(item.tags) ? item.tags : []
|
||||||
|
return tags.some((tag: string) =>
|
||||||
|
tag.toLowerCase().includes(searchTerm)
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Combine results and remove duplicates
|
||||||
|
const combinedResults = [...items, ...tagMatches]
|
||||||
|
const uniqueResults = Array.from(
|
||||||
|
new Map(combinedResults.map((item) => [item.id, item])).values()
|
||||||
|
)
|
||||||
|
|
||||||
|
return uniqueResults
|
||||||
}
|
}
|
||||||
23
lib/utils.ts
Normal file
23
lib/utils.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
/**
|
||||||
|
* Highlights matching text in a string by wrapping matches with a marker
|
||||||
|
* @param text - The text to search in
|
||||||
|
* @param query - The search query to highlight
|
||||||
|
* @returns The text with highlighted matches wrapped in <mark> tags
|
||||||
|
*/
|
||||||
|
export function highlightText(text: string, query: string): string {
|
||||||
|
if (!query.trim() || !text) {
|
||||||
|
return text
|
||||||
|
}
|
||||||
|
|
||||||
|
const regex = new RegExp(`(${escapeRegExp(query)})`, 'gi')
|
||||||
|
return text.replace(regex, '<mark>$1</mark>')
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Escapes special regex characters in a string
|
||||||
|
* @param string - The string to escape
|
||||||
|
* @returns The escaped string safe for use in RegExp
|
||||||
|
*/
|
||||||
|
function escapeRegExp(string: string): string {
|
||||||
|
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user