feat: мультивыбор колледжей в фильтре публичной ленты

Параметр category в /news принимает несколько slug'ов через запятую
(category_id IN ...), обратно совместим с одиночным slug. FilterBar:
чипы колледжей стали переключателями (можно отметить несколько),
filters.category теперь массив.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
jze9
2026-06-17 18:47:19 +05:00
parent 45368c64bf
commit 3161470a7d
3 changed files with 23 additions and 10 deletions

View File

@@ -62,9 +62,13 @@ async def list_news(
)
if category:
cat = (await db.execute(select(Category).where(Category.slug == category))).scalar_one_or_none()
if cat:
base = base.where(Article.category_id == cat.id)
# Поддержка нескольких колледжей: category="slug1,slug2,..."
slugs = [s for s in (part.strip() for part in category.split(",")) if s]
cat_ids = (
await db.execute(select(Category.id).where(Category.slug.in_(slugs)))
).scalars().all()
if cat_ids:
base = base.where(Article.category_id.in_(cat_ids))
else:
return {"total": 0, "offset": offset, "limit": limit, "has_more": False, "items": []}

View File

@@ -11,19 +11,28 @@ const DATE_PRESETS = [
export default function FilterBar({ categories, tags, filters, onChange }) {
const set = key => val => onChange({ ...filters, [key]: val })
// Мультивыбор колледжей: filters.category — массив slug'ов
const selected = filters.category ?? []
const toggleCategory = slug => {
const next = selected.includes(slug)
? selected.filter(s => s !== slug)
: [...selected, slug]
set('category')(next)
}
return (
<div className="filter-bar">
{/* Категории */}
{/* Категории (можно выбрать несколько) */}
<div className="filter-row">
<button
className={`chip ${!filters.category ? 'chip--active' : 'chip--ghost'}`}
onClick={() => set('category')(null)}
className={`chip ${selected.length === 0 ? 'chip--active' : 'chip--ghost'}`}
onClick={() => set('category')([])}
>Все</button>
{categories.map(c => (
<button
key={c.id}
className={`chip ${filters.category === c.slug ? 'chip--active' : 'chip--ghost'}`}
onClick={() => set('category')(c.slug)}
className={`chip ${selected.includes(c.slug) ? 'chip--active' : 'chip--ghost'}`}
onClick={() => toggleCategory(c.slug)}
>{c.name}</button>
))}
</div>

View File

@@ -26,7 +26,7 @@ export default function NewsFeed() {
const [loading, setLoading] = useState(true)
const [loadingMore, setLoadingMore] = useState(false)
const [hasMore, setHasMore] = useState(false)
const [filters, setFilters] = useState({ category: null, tag: null, datePreset: '' })
const [filters, setFilters] = useState({ category: [], tag: null, datePreset: '' })
const offsetRef = useRef(0)
const busyRef = useRef(false)
@@ -60,7 +60,7 @@ export default function NewsFeed() {
const buildParams = useCallback((offset) => {
const params = { limit: PAGE_SIZE, offset }
if (filters.category) params.category = filters.category
if (filters.category?.length) params.category = filters.category.join(',')
if (filters.tag) params.tag = filters.tag
const df = presetToDateFrom(filters.datePreset)
if (df) params.date_from = df