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:
2025-12-01 19:04:37 +00:00
parent dcb1ce929f
commit 6626a26160
2 changed files with 95 additions and 0 deletions

View File

@@ -118,4 +118,76 @@ export async function updateList(
revalidatePath('/')
revalidatePath(`/list/${listId}`)
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
View 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, '\\$&')
}