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:
arsam
2026-09-18 23:25:35 +03:30
parent a2478dff34
commit 0eab8ed756
+121
View File
@@ -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>
);
}