new web ract

This commit is contained in:
jze9
2026-05-15 03:31:28 +05:00
parent de78624495
commit 2335497226
58 changed files with 7397 additions and 406 deletions

View 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)} />
)}
</>
)
}