Files
QueueCube/frontend/src/components/InvidiousSearchModal.tsx

111 lines
3.6 KiB
TypeScript
Raw Normal View History

2025-02-21 22:47:50 -08:00
import React, { useState, KeyboardEvent } from 'react';
import { FaSearch, FaSpinner, FaTimes } from 'react-icons/fa';
2025-02-22 01:09:33 -08:00
import { API, SearchResult } from '../api/player';
2025-02-21 22:47:50 -08:00
interface InvidiousSearchModalProps {
isOpen: boolean;
onClose: () => void;
onSelectVideo: (url: string) => void;
}
2025-02-22 01:09:33 -08:00
const ResultCell: React.FC<{ result: SearchResult, onClick: () => void }> = ({ result, onClick, ...props }) => {
2025-02-21 22:47:50 -08:00
return (
<div className="flex gap-4 bg-black/20 p-2 rounded-lg cursor-pointer hover:bg-black/30 transition-colors" onClick={onClick} {...props}>
2025-02-22 01:09:33 -08:00
<img src={result.thumbnailUrl} alt={result.title} className="w-32 h-18 object-cover rounded" />
2025-02-21 22:47:50 -08:00
<div className="flex flex-col justify-center">
<h3 className="text-white font-semibold">{result.title}</h3>
<p className="text-white/60">{result.author}</p>
</div>
</div>
);
};
const InvidiousSearchModal: React.FC<InvidiousSearchModalProps> = ({ isOpen, onClose, onSelectVideo }) => {
const [searchQuery, setSearchQuery] = useState('');
2025-02-22 01:09:33 -08:00
const [results, setResults] = useState<SearchResult[]>([]);
2025-02-21 22:47:50 -08:00
const [isLoading, setIsLoading] = useState(false);
const handleSearch = async () => {
if (!searchQuery.trim()) return;
setIsLoading(true);
try {
2025-02-22 01:09:33 -08:00
const response = await API.search(searchQuery);
if (response.success) {
setResults(response.results);
} else {
console.error('Search failed:', response);
}
2025-02-21 22:47:50 -08:00
} catch (error) {
console.error('Failed to search:', error);
} finally {
setIsLoading(false);
}
};
const handleKeyDown = (e: KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Enter') {
handleSearch();
}
};
const _onSelectVideo = (url: string) => {
setSearchQuery('');
setResults([]);
onSelectVideo(url);
};
if (!isOpen) return null;
return (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
<div className="bg-violet-900 w-full max-w-xl rounded-lg p-4 shadow-lg">
<div className="flex justify-between items-center mb-4">
<h2 className="text-white text-xl font-bold">Search YouTube (Invidious)</h2>
<button onClick={onClose} className="text-white/60 hover:text-white">
<FaTimes size={24} />
</button>
</div>
<div className="flex gap-2 mb-4">
<input
type="text"
autoFocus
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
onKeyDown={handleKeyDown}
placeholder="Search videos..."
className="p-2 rounded-lg border-2 border-violet-500 flex-grow bg-black/20 text-white"
/>
<button
onClick={handleSearch}
disabled={isLoading}
className="bg-violet-500 text-white p-2 rounded-lg px-4 border-2 border-violet-500 disabled:opacity-50"
>
2025-02-21 22:55:36 -08:00
{isLoading ? <span className="animate-spin"><FaSpinner /></span> : <span><FaSearch /></span>}
2025-02-21 22:47:50 -08:00
</button>
</div>
<div className="max-h-[60vh] overflow-y-auto">
{isLoading ? (
<div className="text-white text-center py-12">Searching...</div>
) : (
<div className="grid gap-4">
{results.map((result) => (
<ResultCell
2025-02-22 01:09:33 -08:00
key={result.mediaUrl}
2025-02-21 22:47:50 -08:00
result={result}
2025-02-22 01:09:33 -08:00
onClick={() => _onSelectVideo(result.mediaUrl)}
2025-02-21 22:47:50 -08:00
/>
))}
</div>
)}
</div>
</div>
</div>
);
};
export default InvidiousSearchModal;