Import source: app/src/pages/SearchPage
Import the existing local project snapshot. This commit organizes existing files; it does not represent newly developed functionality.
This commit is contained in:
@@ -0,0 +1,121 @@
|
|||||||
|
import { useState, useCallback, useEffect } from 'react';
|
||||||
|
import { TeknavData } from '../../teknav-data.js';
|
||||||
|
import {
|
||||||
|
useNav, EmptyState,
|
||||||
|
CategoryBadge, inputStyle, pillBtn,
|
||||||
|
} from '../../teknav-ui.jsx';
|
||||||
|
import { ArticleRow } from '../../teknav-articles.jsx';
|
||||||
|
import { contentApi } from '../../src/lib/content-api.js';
|
||||||
|
|
||||||
|
function SearchResult({ article, q, highlight, idx }) {
|
||||||
|
const { navigate } = useNav();
|
||||||
|
return (
|
||||||
|
<div onClick={() => navigate('/article/' + article.slug)} style={{
|
||||||
|
padding: '24px 0', borderBottom: '1px solid #E4DDD2', cursor: 'pointer',
|
||||||
|
opacity: 0, animation: `fadeIn 0.3s ${idx * 40}ms forwards`,
|
||||||
|
}}>
|
||||||
|
<div style={{ display: 'flex', gap: 8, marginBottom: 10 }}>
|
||||||
|
<CategoryBadge name={article.categoryName} color={article.categoryColor} small />
|
||||||
|
<span style={{ fontSize: 11, color: '#5F6B6D' }}>{article.date} · {article.readTime} دقیقه</span>
|
||||||
|
</div>
|
||||||
|
<h3 style={{ fontSize: 17, fontWeight: 800, color: '#263238', margin: '0 0 8px' }}
|
||||||
|
dangerouslySetInnerHTML={{ __html: highlight(article.title, q) }} />
|
||||||
|
<p style={{ fontSize: 14, color: '#5F6B6D', lineHeight: 1.7, margin: 0 }}
|
||||||
|
dangerouslySetInnerHTML={{ __html: highlight(article.summary.slice(0, 180) + '…', q) }} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SearchPage({ initQ = '' }) {
|
||||||
|
const [q, setQ] = useState(initQ);
|
||||||
|
const [input, setInput] = useState(initQ);
|
||||||
|
const [filters, setFilters] = useState({ category: '', author: '', type: '', tag: '', dateRange: '' });
|
||||||
|
const [items, setItems] = useState([]);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const { navigate } = useNav();
|
||||||
|
|
||||||
|
const load = useCallback(() => {
|
||||||
|
setLoading(true);
|
||||||
|
const params = {
|
||||||
|
q,
|
||||||
|
category: filters.category,
|
||||||
|
author: filters.author,
|
||||||
|
type: filters.type,
|
||||||
|
tag: filters.tag,
|
||||||
|
limit: 50,
|
||||||
|
};
|
||||||
|
const apiCall = q ? contentApi.search(params) : contentApi.listArticles(params);
|
||||||
|
apiCall
|
||||||
|
.then(res => setItems(res.items ?? []))
|
||||||
|
.finally(() => setLoading(false));
|
||||||
|
}, [q, filters]);
|
||||||
|
|
||||||
|
useEffect(() => { load(); }, [load]);
|
||||||
|
|
||||||
|
const doSearch = (e) => { e.preventDefault(); setQ(input); };
|
||||||
|
|
||||||
|
const highlightText = (text, query) => {
|
||||||
|
if (!query) return text;
|
||||||
|
const parts = text.split(new RegExp(`(${query})`, 'gi'));
|
||||||
|
return parts.map((part, i) =>
|
||||||
|
part.toLowerCase() === query.toLowerCase()
|
||||||
|
? `<mark style="background:#D49A2A30;color:#263238;border-radius:2px;padding:0 2px">${part}</mark>`
|
||||||
|
: part
|
||||||
|
).join('');
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ paddingTop: 80, background: '#FAF7F0', minHeight: '100vh', fontFamily: 'Vazirmatn,sans-serif', direction: 'rtl' }}>
|
||||||
|
<div style={{ maxWidth: 1000, margin: '0 auto', padding: '40px 24px' }}>
|
||||||
|
<h1 style={{ fontSize: 28, fontWeight: 900, color: '#263238', margin: '0 0 8px' }}>جستجوی پیشرفته</h1>
|
||||||
|
|
||||||
|
<form onSubmit={doSearch} style={{ display: 'flex', gap: 10, marginBottom: 32 }}>
|
||||||
|
<input value={input} onChange={e => setInput(e.target.value)} placeholder="جستجو در مقالهها..."
|
||||||
|
style={{ flex: 1, ...inputStyle, fontSize: 16 }} dir="rtl" autoFocus />
|
||||||
|
<button type="submit" style={{ ...pillBtn, background: '#0F6B73', color: '#fff', padding: '12px 32px' }}>جستجو</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div style={{ display: 'grid', gridTemplateColumns: '240px 1fr', gap: 32 }} className="search-layout">
|
||||||
|
<aside style={{ display: 'flex', flexDirection: 'column', gap: 24 }}>
|
||||||
|
<div>
|
||||||
|
<div style={{ fontSize: 13, fontWeight: 700, marginBottom: 12, color: '#263238' }}>دستهبندی</div>
|
||||||
|
<select value={filters.category} onChange={e => setFilters(f => ({ ...f, category: e.target.value }))} style={inputStyle}>
|
||||||
|
<option value="">همه دستهها</option>
|
||||||
|
{TeknavData.categories.map(c => <option key={c.id} value={c.slug}>{c.name}</option>)}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div style={{ fontSize: 13, fontWeight: 700, marginBottom: 12, color: '#263238' }}>نویسنده</div>
|
||||||
|
<select value={filters.author} onChange={e => setFilters(f => ({ ...f, author: e.target.value }))} style={inputStyle}>
|
||||||
|
<option value="">همه نویسندگان</option>
|
||||||
|
{TeknavData.authors.map(a => <option key={a.id} value={a.slug}>{a.name}</option>)}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div style={{ fontSize: 13, fontWeight: 700, marginBottom: 12, color: '#263238' }}>نوع محتوا</div>
|
||||||
|
<select value={filters.type} onChange={e => setFilters(f => ({ ...f, type: e.target.value }))} style={inputStyle}>
|
||||||
|
<option value="">همه انواع</option>
|
||||||
|
{['تحلیل عمیق', 'راهنمای فنی', 'دادهنما', 'خبر فوری'].map(t => <option key={t} value={t}>{t}</option>)}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<button onClick={() => setFilters({ category: '', author: '', type: '', tag: '', dateRange: '' })} style={{ ...pillBtn, background: '#F4EFE6', color: '#C94C4C', justifyContent: 'center' }}>پاک کردن فیلترها</button>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
{loading ? <SkeletonLoader lines={5} /> : (
|
||||||
|
<>
|
||||||
|
{items.length === 0 ? <EmptyState title="نتیجهای یافت نشد" /> : (
|
||||||
|
<div>
|
||||||
|
<div style={{ fontSize: 13, color: '#5F6B6D', marginBottom: 20 }}>{items.length.toLocaleString('fa-IR')} نتیجه پیدا شد</div>
|
||||||
|
{items.map((a, i) => <SearchResult key={a.id} article={a} q={q} highlight={highlightText} idx={i} />)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<style>{`.search-layout { grid-template-columns: 240px 1fr; } @media(max-width:768px){ .search-layout { grid-template-columns: 1fr !important; } }`}</style>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user