NicholaiVogel 0e0d59f13f feat: implement core search and content management functionality
- Add semantic search API endpoint with mock data and debounced search
- Create useSearch hook for managing search state and API calls
- Implement SearchResults component with rich item display
- Add ImageDropzone component with drag-and-drop file upload support
- Create upload API endpoint for file and URL processing
- Build ItemDetailsModal with comprehensive metadata display and actions
- Add Badge UI component to design system
- Integrate search functionality into main interface with real-time results
- Support multiple image formats with validation and size limits
- Add item management actions (share, download, copy, favorite, edit, delete)

Core features now working:
✅ Real-time semantic search with debouncing
✅ Image upload via drag-and-drop or URL
✅ Detailed item view with metadata and actions
✅ Responsive UI with proper loading states and error handling
2025-10-08 18:01:42 -06:00

176 lines
4.0 KiB
TypeScript

'use client';
import { useState, useCallback, useRef, useEffect } from 'react';
export interface SearchResult {
id: string;
type: 'image' | 'video' | 'link' | 'note';
title: string;
description: string;
url: string;
tags: string[];
source: string;
dateAdded: string;
collection: string;
thumbnail: string;
}
export interface SearchResponse {
results: SearchResult[];
total: number;
query: string;
searchTime?: string;
message?: string;
}
export interface SearchState {
query: string;
results: SearchResult[];
isLoading: boolean;
error: string | null;
hasSearched: boolean;
total: number;
searchTime: string | null;
}
export interface SearchOptions {
type?: 'image' | 'video' | 'link' | 'note' | 'all';
limit?: number;
}
export function useSearch() {
const [state, setState] = useState<SearchState>({
query: '',
results: [],
isLoading: false,
error: null,
hasSearched: false,
total: 0,
searchTime: null,
});
const debounceTimeoutRef = useRef<NodeJS.Timeout>();
const abortControllerRef = useRef<AbortController>();
const search = useCallback(async (
query: string,
options: SearchOptions = {}
) => {
// Clear previous timeout
if (debounceTimeoutRef.current) {
clearTimeout(debounceTimeoutRef.current);
}
// Cancel previous request
if (abortControllerRef.current) {
abortControllerRef.current.abort();
}
// Create new abort controller
abortControllerRef.current = new AbortController();
// Debounce search to avoid too many API calls
return new Promise<SearchResponse>((resolve, reject) => {
debounceTimeoutRef.current = setTimeout(async () => {
if (!query.trim()) {
setState(prev => ({
...prev,
query: '',
results: [],
isLoading: false,
error: null,
hasSearched: false,
total: 0,
searchTime: null,
}));
resolve({ results: [], total: 0, query: '' });
return;
}
setState(prev => ({
...prev,
query: query.trim(),
isLoading: true,
error: null,
}));
try {
const searchParams = new URLSearchParams({
q: query.trim(),
type: options.type || 'all',
limit: (options.limit || 20).toString(),
});
const response = await fetch(`/api/search?${searchParams}`, {
signal: abortControllerRef.current?.signal,
});
if (!response.ok) {
throw new Error(`Search failed: ${response.statusText}`);
}
const data: SearchResponse = await response.json();
setState(prev => ({
...prev,
results: data.results,
isLoading: false,
hasSearched: true,
total: data.total,
searchTime: data.searchTime || null,
error: null,
}));
resolve(data);
} catch (error) {
if (error instanceof Error && error.name === 'AbortError') {
// Request was cancelled, don't update state
return;
}
const errorMessage = error instanceof Error ? error.message : 'Search failed';
setState(prev => ({
...prev,
isLoading: false,
error: errorMessage,
}));
reject(error);
}
}, 300); // 300ms debounce
});
}, []);
// Clear search results
const clearSearch = useCallback(() => {
setState({
query: '',
results: [],
isLoading: false,
error: null,
hasSearched: false,
total: 0,
searchTime: null,
});
}, []);
// Cleanup on unmount
useEffect(() => {
return () => {
if (debounceTimeoutRef.current) {
clearTimeout(debounceTimeoutRef.current);
}
if (abortControllerRef.current) {
abortControllerRef.current.abort();
}
};
}, []);
return {
...state,
search,
clearSearch,
};
}