advanced filtering and search capabilities
This commit is contained in:
94
pages/api/search/advanced.ts
Normal file
94
pages/api/search/advanced.ts
Normal file
@@ -0,0 +1,94 @@
|
||||
import type { NextApiRequest, NextApiResponse } from "next"
|
||||
import sqlite3 from "sqlite3"
|
||||
import path from "path"
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
if (req.method !== "GET") return res.status(405).json({ error: "Method not allowed" })
|
||||
|
||||
const {
|
||||
q,
|
||||
category,
|
||||
risk_level_min,
|
||||
risk_level_max,
|
||||
status = 'verified',
|
||||
page = '1',
|
||||
limit = '20'
|
||||
} = req.query
|
||||
|
||||
const dbPath = path.join(process.cwd(), "database", "antihoax.db")
|
||||
const db = new sqlite3.Database(dbPath)
|
||||
|
||||
try {
|
||||
let whereConditions = ["s.status = ?"]
|
||||
let params: any[] = [status]
|
||||
|
||||
if (q) {
|
||||
whereConditions.push("(s.domain LIKE ? OR s.title LIKE ? OR s.description LIKE ?)")
|
||||
params.push(`%${q}%`, `%${q}%`, `%${q}%`)
|
||||
}
|
||||
|
||||
if (category) {
|
||||
whereConditions.push("EXISTS (SELECT 1 FROM source_categories sc JOIN categories c ON sc.category_id = c.id WHERE sc.source_id = s.id AND c.name = ?)")
|
||||
params.push(category)
|
||||
}
|
||||
|
||||
if (risk_level_min) {
|
||||
whereConditions.push("s.risk_level >= ?")
|
||||
params.push(parseInt(risk_level_min as string))
|
||||
}
|
||||
|
||||
if (risk_level_max) {
|
||||
whereConditions.push("s.risk_level <= ?")
|
||||
params.push(parseInt(risk_level_max as string))
|
||||
}
|
||||
|
||||
const offset = (parseInt(page as string) - 1) * parseInt(limit as string)
|
||||
|
||||
const query = `
|
||||
SELECT s.*, GROUP_CONCAT(c.name) as categories,
|
||||
COUNT(*) OVER() as total_count
|
||||
FROM sources s
|
||||
LEFT JOIN source_categories sc ON s.id = sc.source_id
|
||||
LEFT JOIN categories c ON sc.category_id = c.id
|
||||
WHERE ${whereConditions.join(' AND ')}
|
||||
GROUP BY s.id
|
||||
ORDER BY s.risk_level DESC, s.created_at DESC
|
||||
LIMIT ? OFFSET ?
|
||||
`
|
||||
|
||||
params.push(parseInt(limit as string), offset)
|
||||
|
||||
const results = await new Promise<any[]>((resolve, reject) => {
|
||||
db.all(query, params, (err, rows) => {
|
||||
if (err) reject(err)
|
||||
else resolve(rows)
|
||||
})
|
||||
})
|
||||
|
||||
const total = results.length > 0 ? results[0].total_count : 0
|
||||
const totalPages = Math.ceil(total / parseInt(limit as string))
|
||||
|
||||
res.json({
|
||||
results: results.map(row => ({
|
||||
id: row.id,
|
||||
domain: row.domain,
|
||||
title: row.title,
|
||||
risk_level: row.risk_level,
|
||||
categories: row.categories ? row.categories.split(',') : [],
|
||||
description: row.description,
|
||||
created_at: row.created_at
|
||||
})),
|
||||
pagination: {
|
||||
page: parseInt(page as string),
|
||||
limit: parseInt(limit as string),
|
||||
total,
|
||||
totalPages
|
||||
}
|
||||
})
|
||||
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: "Search failed" })
|
||||
} finally {
|
||||
db.close()
|
||||
}
|
||||
}
|
||||
142
pages/search.tsx
Normal file
142
pages/search.tsx
Normal file
@@ -0,0 +1,142 @@
|
||||
import { useState } from "react"
|
||||
import type { NextPage } from "next"
|
||||
import Head from "next/head"
|
||||
import Link from "next/link"
|
||||
|
||||
const SearchPage: NextPage = () => {
|
||||
const [query, setQuery] = useState("")
|
||||
const [category, setCategory] = useState("")
|
||||
const [riskLevel, setRiskLevel] = useState({ min: '', max: '' })
|
||||
const [results, setResults] = useState<any[]>([])
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [pagination, setPagination] = useState<any>(null)
|
||||
|
||||
const handleSearch = async (page = 1) => {
|
||||
if (!query.trim()) return
|
||||
|
||||
setLoading(true)
|
||||
const params = new URLSearchParams({
|
||||
q: query,
|
||||
page: page.toString(),
|
||||
limit: '10'
|
||||
})
|
||||
|
||||
if (category) params.set('category', category)
|
||||
if (riskLevel.min) params.set('risk_level_min', riskLevel.min)
|
||||
if (riskLevel.max) params.set('risk_level_max', riskLevel.max)
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/search/advanced?${params}`)
|
||||
const data = await response.json()
|
||||
setResults(data.results || [])
|
||||
setPagination(data.pagination)
|
||||
} catch (error) {
|
||||
console.error('Search error:', error)
|
||||
}
|
||||
setLoading(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Head>
|
||||
<title>Advanced Search - Infohliadka</title>
|
||||
</Head>
|
||||
|
||||
<div style={{ padding: '20px' }}>
|
||||
<div style={{ marginBottom: '20px' }}>
|
||||
<Link href="/">← Home</Link>
|
||||
</div>
|
||||
|
||||
<h1>Advanced Search</h1>
|
||||
|
||||
<div style={{ marginBottom: '20px' }}>
|
||||
<div style={{ marginBottom: '10px' }}>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search domains, titles, descriptions..."
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
style={{ width: '400px', padding: '8px', marginRight: '10px' }}
|
||||
/>
|
||||
<button onClick={() => handleSearch()} disabled={loading}>
|
||||
{loading ? 'Searching...' : 'Search'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<select
|
||||
value={category}
|
||||
onChange={(e) => setCategory(e.target.value)}
|
||||
style={{ marginRight: '10px', padding: '5px' }}
|
||||
>
|
||||
<option value="">All Categories</option>
|
||||
<option value="hoax">Hoax</option>
|
||||
<option value="hate_speech">Hate Speech</option>
|
||||
<option value="spam">Spam</option>
|
||||
</select>
|
||||
|
||||
<input
|
||||
type="number"
|
||||
placeholder="Min Risk"
|
||||
value={riskLevel.min}
|
||||
onChange={(e) => setRiskLevel({...riskLevel, min: e.target.value})}
|
||||
style={{ width: '80px', padding: '5px', marginRight: '5px' }}
|
||||
min="1" max="5"
|
||||
/>
|
||||
<input
|
||||
type="number"
|
||||
placeholder="Max Risk"
|
||||
value={riskLevel.max}
|
||||
onChange={(e) => setRiskLevel({...riskLevel, max: e.target.value})}
|
||||
style={{ width: '80px', padding: '5px' }}
|
||||
min="1" max="5"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{results.length > 0 && (
|
||||
<div>
|
||||
<h3>Search Results ({pagination?.total || 0})</h3>
|
||||
{results.map((result) => (
|
||||
<div key={result.id} style={{
|
||||
border: '1px solid #ddd',
|
||||
padding: '15px',
|
||||
marginBottom: '10px',
|
||||
backgroundColor: '#f9f9f9'
|
||||
}}>
|
||||
<h4>{result.domain}</h4>
|
||||
<p><strong>Risk Level:</strong> {result.risk_level}/5</p>
|
||||
<p><strong>Categories:</strong> {result.categories.join(', ')}</p>
|
||||
{result.description && <p>{result.description}</p>}
|
||||
<small>{new Date(result.created_at).toLocaleDateString()}</small>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{pagination && pagination.totalPages > 1 && (
|
||||
<div style={{ marginTop: '20px', textAlign: 'center' }}>
|
||||
{Array.from({length: pagination.totalPages}, (_, i) => i + 1).map(page => (
|
||||
<button
|
||||
key={page}
|
||||
onClick={() => handleSearch(page)}
|
||||
disabled={page === pagination.page}
|
||||
style={{
|
||||
margin: '0 5px',
|
||||
padding: '5px 10px',
|
||||
backgroundColor: page === pagination.page ? '#007bff' : '#f8f9fa',
|
||||
color: page === pagination.page ? 'white' : 'black',
|
||||
border: '1px solid #ddd'
|
||||
}}
|
||||
>
|
||||
{page}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default SearchPage
|
||||
Reference in New Issue
Block a user