Add search to videos

This commit is contained in:
Ryan Whytsell 2025-10-18 15:06:30 -04:00
parent 7052fc0077
commit ad83f0643c
Signed by: Epithium
GPG Key ID: 940AC18C08E925EA
4 changed files with 112 additions and 48 deletions

View File

@ -126,6 +126,20 @@ export const videoOperations = {
.all(status) as Video[]; .all(status) as Video[];
}, },
search: (query: string, status?: string) => {
const searchPattern = `%${query}%`;
if (status) {
return db.prepare(
'SELECT * FROM videos WHERE title LIKE ? COLLATE NOCASE AND status = ? ORDER BY addedAt DESC'
).all(searchPattern, status) as Video[];
} else {
return db.prepare(
'SELECT * FROM videos WHERE title LIKE ? COLLATE NOCASE ORDER BY addedAt DESC'
).all(searchPattern) as Video[];
}
},
getNextPending: () => { getNextPending: () => {
return db.prepare('SELECT * FROM videos WHERE status = ? ORDER BY addedAt ASC LIMIT 1') return db.prepare('SELECT * FROM videos WHERE status = ? ORDER BY addedAt ASC LIMIT 1')
.get('pending') as Video | undefined; .get('pending') as Video | undefined;

View File

@ -9,10 +9,17 @@ const router = Router();
// Get all videos // Get all videos
router.get('/', (req, res) => { router.get('/', (req, res) => {
try { try {
const { status } = req.query; const { status, search } = req.query;
let videos; let videos;
if (status && typeof status === 'string') {
// If search query is provided, use search
if (search && typeof search === 'string') {
const statusFilter = status && typeof status === 'string' ? status : undefined;
videos = videoOperations.search(search, statusFilter);
}
// Otherwise use status filter or get all
else if (status && typeof status === 'string') {
videos = videoOperations.getByStatus(status); videos = videoOperations.getByStatus(status);
} else { } else {
videos = videoOperations.getAll(); videos = videoOperations.getAll();

View File

@ -10,15 +10,18 @@ function VideosPage() {
const [addUrl, setAddUrl] = useState(''); const [addUrl, setAddUrl] = useState('');
const [adding, setAdding] = useState(false); const [adding, setAdding] = useState(false);
const [filter, setFilter] = useState<string>('all'); const [filter, setFilter] = useState<string>('all');
const [searchQuery, setSearchQuery] = useState('');
useEffect(() => { useEffect(() => {
loadVideos(); loadVideos();
}, [filter]); }, [filter, searchQuery]);
const loadVideos = async () => { const loadVideos = async () => {
try { try {
setLoading(true); setLoading(true);
const data = filter === 'all' ? await videosAPI.getAll() : await videosAPI.getAll(filter); const statusFilter = filter === 'all' ? undefined : filter;
const search = searchQuery.trim() || undefined;
const data = await videosAPI.getAll(statusFilter, search);
setVideos(data); setVideos(data);
setError(''); setError('');
} catch (err) { } catch (err) {
@ -127,47 +130,82 @@ function VideosPage() {
</form> </form>
</div> </div>
<div className="mt-6 flex gap-2"> <div className="mt-6 space-y-4">
<button <div className="flex gap-2">
onClick={() => setFilter('all')} <button
className={`px-4 py-2 rounded ${ onClick={() => setFilter('all')}
filter === 'all' ? 'bg-blue-600 text-white' : 'bg-white dark:bg-gray-800 text-gray-700 dark:text-gray-300' className={`px-4 py-2 rounded ${
}`} filter === 'all' ? 'bg-blue-600 text-white' : 'bg-white dark:bg-gray-800 text-gray-700 dark:text-gray-300'
> }`}
All >
</button> All
<button </button>
onClick={() => setFilter('completed')} <button
className={`px-4 py-2 rounded ${ onClick={() => setFilter('completed')}
filter === 'completed' ? 'bg-blue-600 text-white' : 'bg-white dark:bg-gray-800 text-gray-700 dark:text-gray-300' className={`px-4 py-2 rounded ${
}`} filter === 'completed' ? 'bg-blue-600 text-white' : 'bg-white dark:bg-gray-800 text-gray-700 dark:text-gray-300'
> }`}
Completed >
</button> Completed
<button </button>
onClick={() => setFilter('pending')} <button
className={`px-4 py-2 rounded ${ onClick={() => setFilter('pending')}
filter === 'pending' ? 'bg-blue-600 text-white' : 'bg-white dark:bg-gray-800 text-gray-700 dark:text-gray-300' className={`px-4 py-2 rounded ${
}`} filter === 'pending' ? 'bg-blue-600 text-white' : 'bg-white dark:bg-gray-800 text-gray-700 dark:text-gray-300'
> }`}
Pending >
</button> Pending
<button </button>
onClick={() => setFilter('downloading')} <button
className={`px-4 py-2 rounded ${ onClick={() => setFilter('downloading')}
filter === 'downloading' ? 'bg-blue-600 text-white' : 'bg-white dark:bg-gray-800 text-gray-700 dark:text-gray-300' className={`px-4 py-2 rounded ${
}`} filter === 'downloading' ? 'bg-blue-600 text-white' : 'bg-white dark:bg-gray-800 text-gray-700 dark:text-gray-300'
> }`}
Downloading >
</button> Downloading
<button </button>
onClick={() => setFilter('failed')} <button
className={`px-4 py-2 rounded ${ onClick={() => setFilter('failed')}
filter === 'failed' ? 'bg-blue-600 text-white' : 'bg-white dark:bg-gray-800 text-gray-700 dark:text-gray-300' className={`px-4 py-2 rounded ${
}`} filter === 'failed' ? 'bg-blue-600 text-white' : 'bg-white dark:bg-gray-800 text-gray-700 dark:text-gray-300'
> }`}
Failed >
</button> Failed
</button>
</div>
<div className="flex gap-2 items-center">
<div className="flex-1 relative">
<input
type="text"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
placeholder="Search videos by title..."
className="w-full rounded-md border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-white placeholder-gray-400 dark:placeholder-gray-400 shadow-sm focus:border-blue-500 focus:ring-blue-500 pl-10"
/>
<svg
className="absolute left-3 top-1/2 transform -translate-y-1/2 h-5 w-5 text-gray-400"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
/>
</svg>
</div>
{searchQuery && (
<button
onClick={() => setSearchQuery('')}
className="px-4 py-2 text-sm text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white"
>
Clear
</button>
)}
</div>
</div> </div>
<div className="mt-6 bg-white dark:bg-gray-800 shadow overflow-hidden sm:rounded-md"> <div className="mt-6 bg-white dark:bg-gray-800 shadow overflow-hidden sm:rounded-md">

View File

@ -56,9 +56,14 @@ export const channelsAPI = {
// Videos API // Videos API
export const videosAPI = { export const videosAPI = {
getAll: (status?: string) => { getAll: (status?: string, search?: string) => {
const url = status const params = new URLSearchParams();
? `${API_BASE}/videos?status=${status}` if (status) params.append('status', status);
if (search) params.append('search', search);
const queryString = params.toString();
const url = queryString
? `${API_BASE}/videos?${queryString}`
: `${API_BASE}/videos`; : `${API_BASE}/videos`;
return fetchJSON<Video[]>(url); return fetchJSON<Video[]>(url);
}, },