new web ract
This commit is contained in:
190
web-react/src/pages/NewsFeed.jsx
Normal file
190
web-react/src/pages/NewsFeed.jsx
Normal file
@@ -0,0 +1,190 @@
|
||||
import React, { useState, useEffect, useCallback, useRef } from 'react'
|
||||
import { useTheme } from '../hooks/useTheme'
|
||||
import { getNews, getCategories, getTags, getArticle } from '../api/index'
|
||||
import NewsCard from '../components/NewsCard'
|
||||
import FilterBar from '../components/FilterBar'
|
||||
import Loader from '../components/Loader'
|
||||
import ArticleModal from '../components/ArticleModal'
|
||||
|
||||
const PAGE_SIZE = 20
|
||||
|
||||
function presetToDateFrom(preset) {
|
||||
if (!preset) return null
|
||||
const now = new Date()
|
||||
const days = { week: 7, month: 30, '3months': 90, year: 365 }[preset]
|
||||
if (!days) return null
|
||||
now.setDate(now.getDate() - days)
|
||||
return now.toISOString()
|
||||
}
|
||||
|
||||
export default function NewsFeed() {
|
||||
const { dark, toggle } = useTheme()
|
||||
const [articles, setArticles] = useState([])
|
||||
const [total, setTotal] = useState(0)
|
||||
const [categories, setCategories] = useState([])
|
||||
const [tags, setTags] = useState([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [filters, setFilters] = useState({ category: null, tag: null, datePreset: '', page: 1 })
|
||||
|
||||
// Поиск с debounce
|
||||
const [searchInput, setSearchInput] = useState('')
|
||||
const [searchQuery, setSearchQuery] = useState('')
|
||||
const debounceRef = useRef(null)
|
||||
|
||||
const handleSearch = e => {
|
||||
const val = e.target.value
|
||||
setSearchInput(val)
|
||||
clearTimeout(debounceRef.current)
|
||||
debounceRef.current = setTimeout(() => {
|
||||
setSearchQuery(val.trim())
|
||||
setFilters(f => ({ ...f, page: 1 }))
|
||||
}, 400)
|
||||
}
|
||||
|
||||
const clearSearch = () => {
|
||||
setSearchInput('')
|
||||
setSearchQuery('')
|
||||
setFilters(f => ({ ...f, page: 1 }))
|
||||
}
|
||||
|
||||
// Модальное окно
|
||||
const [modalArticle, setModalArticle] = useState(null)
|
||||
|
||||
useEffect(() => {
|
||||
Promise.all([getCategories(), getTags(30)]).then(([cats, tgs]) => {
|
||||
setCategories(cats)
|
||||
setTags(tgs)
|
||||
}).catch(() => {})
|
||||
}, [])
|
||||
|
||||
const loadNews = useCallback(async () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
const params = {
|
||||
limit: PAGE_SIZE,
|
||||
offset: (filters.page - 1) * PAGE_SIZE,
|
||||
}
|
||||
if (filters.category) params.category = filters.category
|
||||
if (filters.tag) params.tag = filters.tag
|
||||
const df = presetToDateFrom(filters.datePreset)
|
||||
if (df) params.date_from = df
|
||||
if (searchQuery) params.q = searchQuery
|
||||
|
||||
const data = await getNews(params)
|
||||
setArticles(data.items ?? [])
|
||||
setTotal(data.total ?? 0)
|
||||
} catch {
|
||||
setArticles([])
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}, [filters, searchQuery])
|
||||
|
||||
useEffect(() => { loadNews() }, [loadNews])
|
||||
|
||||
const openArticle = async (slug) => {
|
||||
setModalArticle({ _loading: true })
|
||||
try {
|
||||
const data = await getArticle(slug)
|
||||
setModalArticle(data)
|
||||
} catch {
|
||||
setModalArticle(null)
|
||||
}
|
||||
}
|
||||
|
||||
const totalPages = Math.max(1, Math.ceil(total / PAGE_SIZE))
|
||||
|
||||
return (
|
||||
<>
|
||||
<header className="public-header">
|
||||
<div className="public-header__inner">
|
||||
<span className="public-header__logo">📰 Новости колледжей</span>
|
||||
<button className="public-header__theme" onClick={toggle} title="Тема">
|
||||
{dark ? '☀️' : '🌙'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Строка поиска */}
|
||||
<div className="search-bar-wrap">
|
||||
<div className="search-bar">
|
||||
<svg className="search-bar__icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
||||
<circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/>
|
||||
</svg>
|
||||
<input
|
||||
className="search-bar__input"
|
||||
type="text"
|
||||
placeholder="Поиск по заголовку, тексту, тегам..."
|
||||
value={searchInput}
|
||||
onChange={handleSearch}
|
||||
/>
|
||||
{searchInput && (
|
||||
<button className="search-bar__clear" onClick={clearSearch}>✕</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div className="news-feed">
|
||||
<FilterBar
|
||||
categories={categories}
|
||||
tags={tags}
|
||||
filters={filters}
|
||||
onChange={setFilters}
|
||||
/>
|
||||
|
||||
{/* Счётчик результатов при поиске */}
|
||||
{searchQuery && !loading && (
|
||||
<p className="search-results-hint">
|
||||
{total > 0
|
||||
? `Найдено: ${total} ${total === 1 ? 'новость' : total < 5 ? 'новости' : 'новостей'} по запросу «${searchQuery}»`
|
||||
: `По запросу «${searchQuery}» ничего не найдено`
|
||||
}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{loading
|
||||
? <Loader />
|
||||
: articles.length === 0
|
||||
? <p style={{ textAlign: 'center', color: 'var(--c-text-2)', marginTop: '3rem' }}>
|
||||
Новостей не найдено
|
||||
</p>
|
||||
: <div className="news-list">
|
||||
{articles.map(a => (
|
||||
<NewsCard key={a.id} article={a} onClick={() => openArticle(a.slug)} />
|
||||
))}
|
||||
</div>
|
||||
}
|
||||
|
||||
{!loading && totalPages > 1 && (
|
||||
<div className="pagination">
|
||||
<button
|
||||
className="btn btn-ghost btn-sm"
|
||||
disabled={filters.page <= 1}
|
||||
onClick={() => setFilters(f => ({ ...f, page: f.page - 1 }))}
|
||||
>← Назад</button>
|
||||
<span className="pagination__info">{filters.page} / {totalPages}</span>
|
||||
<button
|
||||
className="btn btn-ghost btn-sm"
|
||||
disabled={filters.page >= totalPages}
|
||||
onClick={() => setFilters(f => ({ ...f, page: f.page + 1 }))}
|
||||
>Вперёд →</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Модальное окно статьи */}
|
||||
{modalArticle && (
|
||||
modalArticle._loading
|
||||
? (
|
||||
<div className="article-modal-overlay" onClick={() => setModalArticle(null)}>
|
||||
<div className="article-modal" style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', minHeight: 200 }}
|
||||
onClick={e => e.stopPropagation()}>
|
||||
<Loader />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
: <ArticleModal article={modalArticle} onClose={() => setModalArticle(null)} />
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user