diff --git a/src/components/shared/ImageUploader.tsx b/src/components/shared/ImageUploader.tsx index 5e4c535..149120d 100644 --- a/src/components/shared/ImageUploader.tsx +++ b/src/components/shared/ImageUploader.tsx @@ -39,6 +39,61 @@ function ImageUploader({ onImageSelect, initialImageUrl, className = '' }: Image setPreviewUrl(objectUrl); }, []); + const handleFileChange = useCallback(async (event: ChangeEvent) => { + const file = event.target.files?.[0]; + if (!file) return; + + if (file.type.startsWith('video/')) { + try { + const video = document.createElement('video'); + const videoObjectUrl = URL.createObjectURL(file); + video.src = videoObjectUrl; + video.crossOrigin = 'anonymous'; + video.muted = true; + + await new Promise((resolve, reject) => { + video.onloadeddata = () => { + video.currentTime = Math.min(1, video.duration / 2); + }; + video.onseeked = () => resolve(); + video.onerror = () => reject(new Error('Failed to load video')); + }); + + const canvas = document.createElement('canvas'); + canvas.width = video.videoWidth; + canvas.height = video.videoHeight; + const ctx = canvas.getContext('2d'); + if (ctx) { + ctx.drawImage(video, 0, 0, canvas.width, canvas.height); + canvas.toBlob( + (blob) => { + if (blob) { + const objectUrl = URL.createObjectURL(blob); + setObjectPreviewUrl(objectUrl); + const optimizedFile = new File([blob], file.name.replace(/\.[^/.]+$/, '.jpg'), { + type: 'image/jpeg', + }); + onImageSelect(optimizedFile); + } + }, + 'image/jpeg', + 0.85, + ); + } + URL.revokeObjectURL(videoObjectUrl); + } catch (error) { + console.error('Video optimization failed:', error); + } + } else if (file.type.startsWith('image/')) { + const objectUrl = URL.createObjectURL(file); + setObjectPreviewUrl(objectUrl); + onImageSelect(file); + } + + if (fileInputRef.current) { + fileInputRef.current.value = ''; + } + }, [onImageSelect, setObjectPreviewUrl]); const handleFileChange = useCallback( async (event: ChangeEvent) => { const file = event.target.files?.[0]; diff --git a/src/hooks/useSearch.tsx b/src/hooks/useSearch.tsx index 753850f..f39b229 100644 --- a/src/hooks/useSearch.tsx +++ b/src/hooks/useSearch.tsx @@ -37,62 +37,65 @@ export function useSearch( fetchFnRef.current = fetchFn; }, [fetchFn]); - const search = useCallback(async (searchQuery: string, cursor?: string) => { - if (!searchQuery.trim()) { - setResults([]); - setNextCursor(undefined); - setHasMore(false); - return; - } - - const cacheKey = searchQuery.trim().toLowerCase(); + const search = useCallback( + async (searchQuery: string, cursor?: string) => { + if (!searchQuery.trim()) { + setResults([]); + setNextCursor(undefined); + setHasMore(false); + return; + } - // Check cache for initial fetch - if (!cursor && cache.current[cacheKey]) { - const cached = cache.current[cacheKey]; - setResults(cached.items); - setNextCursor(cached.nextCursor); - setHasMore(!!cached.nextCursor); - setIsLoading(false); - setError(null); - return; - } + const cacheKey = searchQuery.trim().toLowerCase(); - // Cancel previous request - if (abortController.current) { - abortController.current.abort(); - } - abortController.current = new AbortController(); - - setIsLoading(true); - setError(null); + // Check cache for initial fetch + if (!cursor && cache.current[cacheKey]) { + const cached = cache.current[cacheKey]; + setResults(cached.items); + setNextCursor(cached.nextCursor); + setHasMore(!!cached.nextCursor); + setIsLoading(false); + setError(null); + return; + } - try { - const { items, nextCursor: next } = await fetchFnRef.current( - searchQuery, - cursor, - abortController.current.signal, - ); + // Cancel previous request + if (abortController.current) { + abortController.current.abort(); + } + abortController.current = new AbortController(); - setResults((prev) => (cursor ? [...prev, ...items] : items)); - setNextCursor(next); - setHasMore(!!next); + setIsLoading(true); + setError(null); - // Cache initial result - if (!cursor) { - cache.current[cacheKey] = { items, nextCursor: next }; - } - } catch (err) { - if (err instanceof Error && err.name === 'AbortError') { - return; // Ignore aborted requests - } - setError(err instanceof Error ? err.message : 'Search failed'); - } finally { - if (abortController.current && !abortController.current.signal.aborted) { - setIsLoading(false); + try { + const { items, nextCursor: next } = await fetchFnRef.current( + searchQuery, + cursor, + abortController.current.signal, + ); + + setResults((prev) => (cursor ? [...prev, ...items] : items)); + setNextCursor(next); + setHasMore(!!next); + + // Cache initial result + if (!cursor) { + cache.current[cacheKey] = { items, nextCursor: next }; + } + } catch (err) { + if (err instanceof Error && err.name === 'AbortError') { + return; // Ignore aborted requests + } + setError(err instanceof Error ? err.message : 'Search failed'); + } finally { + if (abortController.current && !abortController.current.signal.aborted) { + setIsLoading(false); + } } - } - }, []); + }, + [], + ); const updateQuery = useCallback( (value: string) => {