582 lines
18 KiB
TypeScript
582 lines
18 KiB
TypeScript
import { useState, useMemo } from 'react'
|
||
import { Link } from 'react-router-dom'
|
||
import { Search, ChevronRight, ChevronLeft } from 'lucide-react'
|
||
import { reports } from '@/data/reports'
|
||
import type { ReportCategory, ReportType } from '@/data/reports'
|
||
|
||
/* ─── constants ──────────────────────────────────────────── */
|
||
const CATEGORIES: { label: string; value: ReportCategory | 'همه' }[] = [
|
||
{ label: 'همه', value: 'همه' },
|
||
{ label: 'بازار جهانی', value: 'بازار جهانی' },
|
||
{ label: 'سیاستگذاری', value: 'سیاستگذاری' },
|
||
{ label: 'انرژی و ESG', value: 'انرژی و ESG' },
|
||
{ label: 'صادرات', value: 'صادرات' },
|
||
{ label: 'تولید داخلی', value: 'تولید داخلی' },
|
||
{ label: 'ریسک و بحران', value: 'ریسک و بحران' },
|
||
]
|
||
|
||
const TYPES: { label: string; value: ReportType | 'همه' | 'رایگان' }[] = [
|
||
{ label: 'همه', value: 'همه' },
|
||
{ label: 'رایگان', value: 'رایگان' },
|
||
{ label: 'Flash Report', value: 'flash' },
|
||
{ label: 'فصلی', value: 'quarterly' },
|
||
{ label: 'گزارش ویژه', value: 'special' },
|
||
{ label: 'ریسک', value: 'risk' },
|
||
]
|
||
|
||
const SORT_OPTIONS = [
|
||
{ label: 'جدیدترین', value: 'newest' },
|
||
{ label: 'قدیمیترین', value: 'oldest' },
|
||
{ label: 'گرانترین', value: 'price-desc' },
|
||
{ label: 'ارزانترین', value: 'price-asc' },
|
||
]
|
||
|
||
const TYPE_COLORS: Record<ReportType, string> = {
|
||
quarterly: '#166534',
|
||
flash: '#9b1c1c',
|
||
risk: '#92400e',
|
||
free: '#1d4ed8',
|
||
special: '#4a1d96',
|
||
}
|
||
|
||
const TYPE_LABELS: Record<ReportType, string> = {
|
||
quarterly: 'فصلی',
|
||
flash: 'فلش',
|
||
risk: 'ریسک',
|
||
free: 'رایگان',
|
||
special: 'ویژه',
|
||
}
|
||
|
||
const PAGE_SIZE = 9
|
||
|
||
/* ─── price formatter ────────────────────────────────────── */
|
||
function formatPrice(price: number): string {
|
||
if (price === 0) return 'رایگان'
|
||
return price.toLocaleString('fa-IR') + ' تومان'
|
||
}
|
||
|
||
/* ─── report card ────────────────────────────────────────── */
|
||
function ReportCard({ report }: { report: (typeof reports)[0] }) {
|
||
const [hovered, setHovered] = useState(false)
|
||
|
||
return (
|
||
<article
|
||
style={{
|
||
borderLeft: '1px solid var(--rule-thin)',
|
||
borderBottom: '1px solid var(--rule-thin)',
|
||
padding: '32px',
|
||
display: 'flex',
|
||
flexDirection: 'column',
|
||
gap: 0,
|
||
background: 'var(--paper)',
|
||
cursor: 'default',
|
||
}}
|
||
>
|
||
{/* top meta */}
|
||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 12 }}>
|
||
<span
|
||
style={{
|
||
width: 6,
|
||
height: 6,
|
||
borderRadius: '50%',
|
||
background: TYPE_COLORS[report.type],
|
||
flexShrink: 0,
|
||
}}
|
||
/>
|
||
<span
|
||
style={{
|
||
fontSize: 9,
|
||
fontWeight: 700,
|
||
textTransform: 'uppercase',
|
||
letterSpacing: '1.5px',
|
||
color: 'var(--ink-5)',
|
||
}}
|
||
>
|
||
{report.category}
|
||
</span>
|
||
{report.isFree && (
|
||
<span
|
||
style={{
|
||
marginRight: 'auto',
|
||
marginLeft: 0,
|
||
fontSize: 9,
|
||
fontWeight: 700,
|
||
letterSpacing: '1px',
|
||
color: 'var(--paper)',
|
||
background: 'var(--green-ink)',
|
||
padding: '2px 6px',
|
||
textTransform: 'uppercase',
|
||
}}
|
||
>
|
||
رایگان
|
||
</span>
|
||
)}
|
||
<span
|
||
style={{
|
||
marginRight: report.isFree ? 0 : 'auto',
|
||
fontSize: 9,
|
||
fontWeight: 700,
|
||
letterSpacing: '1px',
|
||
color: TYPE_COLORS[report.type],
|
||
border: `1px solid ${TYPE_COLORS[report.type]}`,
|
||
padding: '2px 6px',
|
||
}}
|
||
>
|
||
{TYPE_LABELS[report.type]}
|
||
</span>
|
||
</div>
|
||
|
||
{/* title */}
|
||
<Link
|
||
to={`/reports/${report.id}`}
|
||
style={{
|
||
fontSize: 15,
|
||
fontWeight: 800,
|
||
lineHeight: 1.4,
|
||
letterSpacing: '-0.3px',
|
||
color: hovered ? 'var(--red)' : 'var(--ink)',
|
||
textDecoration: 'none',
|
||
marginBottom: 10,
|
||
display: 'block',
|
||
transition: 'color 120ms',
|
||
}}
|
||
onMouseEnter={() => setHovered(true)}
|
||
onMouseLeave={() => setHovered(false)}
|
||
>
|
||
{report.title}
|
||
</Link>
|
||
|
||
{/* meta row */}
|
||
<div
|
||
style={{
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
gap: 12,
|
||
flexWrap: 'wrap',
|
||
}}
|
||
>
|
||
<span style={{ fontSize: 11, color: 'var(--ink-5)' }}>{report.author}</span>
|
||
<span style={{ fontSize: 11, color: 'var(--ink-6)' }}>·</span>
|
||
<span style={{ fontSize: 11, color: 'var(--ink-5)' }}>{report.publishDate}</span>
|
||
<span style={{ fontSize: 11, color: 'var(--ink-6)' }}>·</span>
|
||
<span style={{ fontSize: 11, color: 'var(--ink-5)' }}>{report.pages} صفحه</span>
|
||
</div>
|
||
|
||
{/* divider */}
|
||
<div
|
||
style={{
|
||
height: 1,
|
||
background: 'var(--rule-thin)',
|
||
margin: '16px 0',
|
||
}}
|
||
/>
|
||
|
||
{/* price row */}
|
||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||
<span
|
||
style={{
|
||
fontSize: 15,
|
||
fontWeight: 900,
|
||
color: report.isFree ? 'var(--green-ink)' : 'var(--ink)',
|
||
}}
|
||
>
|
||
{formatPrice(report.price)}
|
||
</span>
|
||
<Link
|
||
to={`/reports/${report.id}`}
|
||
style={{
|
||
fontSize: 10,
|
||
fontWeight: 700,
|
||
textTransform: 'uppercase',
|
||
letterSpacing: '1px',
|
||
color: 'var(--red)',
|
||
textDecoration: 'none',
|
||
borderBottom: '1px solid var(--red)',
|
||
paddingBottom: 1,
|
||
transition: 'opacity 120ms',
|
||
}}
|
||
>
|
||
{report.isFree ? 'دانلود' : 'خرید'}
|
||
</Link>
|
||
</div>
|
||
</article>
|
||
)
|
||
}
|
||
|
||
/* ─── main component ─────────────────────────────────────── */
|
||
export default function Reports() {
|
||
const [activeCategory, setActiveCategory] = useState<ReportCategory | 'همه'>('همه')
|
||
const [activeType, setActiveType] = useState<ReportType | 'همه' | 'رایگان'>('همه')
|
||
const [sortBy, setSortBy] = useState('newest')
|
||
const [searchQuery, setSearchQuery] = useState('')
|
||
const [page, setPage] = useState(1)
|
||
|
||
const filtered = useMemo(() => {
|
||
let result = [...reports]
|
||
|
||
if (activeCategory !== 'همه') {
|
||
result = result.filter((r) => r.category === activeCategory)
|
||
}
|
||
|
||
if (activeType !== 'همه') {
|
||
if (activeType === 'رایگان') {
|
||
result = result.filter((r) => r.isFree)
|
||
} else {
|
||
result = result.filter((r) => r.type === activeType)
|
||
}
|
||
}
|
||
|
||
if (searchQuery.trim()) {
|
||
const q = searchQuery.trim().toLowerCase()
|
||
result = result.filter(
|
||
(r) =>
|
||
r.title.toLowerCase().includes(q) ||
|
||
r.author.toLowerCase().includes(q) ||
|
||
r.category.toLowerCase().includes(q) ||
|
||
r.tags.some((t) => t.toLowerCase().includes(q))
|
||
)
|
||
}
|
||
|
||
switch (sortBy) {
|
||
case 'oldest':
|
||
result = result.reverse()
|
||
break
|
||
case 'price-desc':
|
||
result = result.sort((a, b) => b.price - a.price)
|
||
break
|
||
case 'price-asc':
|
||
result = result.sort((a, b) => a.price - b.price)
|
||
break
|
||
}
|
||
|
||
return result
|
||
}, [activeCategory, activeType, sortBy, searchQuery])
|
||
|
||
const totalPages = Math.ceil(filtered.length / PAGE_SIZE)
|
||
const paged = filtered.slice((page - 1) * PAGE_SIZE, page * PAGE_SIZE)
|
||
|
||
// Reset to page 1 on filter change
|
||
const handleCategoryChange = (cat: ReportCategory | 'همه') => {
|
||
setActiveCategory(cat)
|
||
setPage(1)
|
||
}
|
||
const handleTypeChange = (type: ReportType | 'همه' | 'رایگان') => {
|
||
setActiveType(type)
|
||
setPage(1)
|
||
}
|
||
|
||
return (
|
||
<div
|
||
style={{
|
||
maxWidth: 1280,
|
||
margin: '0 auto',
|
||
padding: '64px 48px',
|
||
direction: 'rtl',
|
||
}}
|
||
>
|
||
{/* Page header */}
|
||
<div
|
||
style={{
|
||
borderBottom: '3px solid var(--ink)',
|
||
paddingBottom: 24,
|
||
marginBottom: 48,
|
||
}}
|
||
>
|
||
<h1
|
||
style={{
|
||
fontSize: 36,
|
||
fontWeight: 900,
|
||
letterSpacing: '-1px',
|
||
marginBottom: 6,
|
||
color: 'var(--ink)',
|
||
}}
|
||
>
|
||
گزارشها
|
||
</h1>
|
||
<p style={{ fontSize: 15, fontWeight: 300, color: 'var(--ink-3)' }}>
|
||
تحلیلهای تخصصی بازار فولاد ایران و جهان — گزارشهای فصلی، ریسک، و فلش
|
||
</p>
|
||
</div>
|
||
|
||
{/* ─── Filter bar ──────────────────────────────────── */}
|
||
<div
|
||
style={{
|
||
position: 'sticky',
|
||
top: 56,
|
||
background: 'var(--paper)',
|
||
borderBottom: '1px solid var(--rule-thin)',
|
||
paddingBottom: 16,
|
||
marginBottom: 48,
|
||
zIndex: 10,
|
||
}}
|
||
>
|
||
{/* Category pills */}
|
||
<div
|
||
style={{
|
||
display: 'flex',
|
||
gap: 6,
|
||
overflowX: 'auto',
|
||
paddingBottom: 10,
|
||
scrollbarWidth: 'none',
|
||
}}
|
||
>
|
||
{CATEGORIES.map((cat) => (
|
||
<button
|
||
key={cat.value}
|
||
onClick={() => handleCategoryChange(cat.value as ReportCategory | 'همه')}
|
||
style={{
|
||
padding: '6px 16px',
|
||
fontSize: 12,
|
||
fontWeight: 600,
|
||
borderRadius: 0,
|
||
border:
|
||
activeCategory === cat.value
|
||
? '1px solid var(--ink)'
|
||
: '1px solid var(--ink-4)',
|
||
background: activeCategory === cat.value ? 'var(--ink)' : 'transparent',
|
||
color: activeCategory === cat.value ? 'var(--paper)' : 'var(--ink-4)',
|
||
cursor: 'pointer',
|
||
whiteSpace: 'nowrap',
|
||
fontFamily: 'inherit',
|
||
transition: 'all 120ms',
|
||
flexShrink: 0,
|
||
}}
|
||
>
|
||
{cat.label}
|
||
</button>
|
||
))}
|
||
</div>
|
||
|
||
{/* Second row: type + sort + search */}
|
||
<div
|
||
style={{
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
gap: 12,
|
||
flexWrap: 'wrap',
|
||
paddingTop: 8,
|
||
}}
|
||
>
|
||
{/* Type filter */}
|
||
<div style={{ display: 'flex', gap: 6, overflowX: 'auto', scrollbarWidth: 'none' }}>
|
||
{TYPES.map((t) => (
|
||
<button
|
||
key={t.value}
|
||
onClick={() => handleTypeChange(t.value as ReportType | 'همه' | 'رایگان')}
|
||
style={{
|
||
padding: '5px 12px',
|
||
fontSize: 11,
|
||
fontWeight: 600,
|
||
borderRadius: 0,
|
||
border:
|
||
activeType === t.value
|
||
? '1px solid var(--ink)'
|
||
: '1px solid var(--rule-thin)',
|
||
background: activeType === t.value ? 'var(--ink)' : 'transparent',
|
||
color: activeType === t.value ? 'var(--paper)' : 'var(--ink-5)',
|
||
cursor: 'pointer',
|
||
whiteSpace: 'nowrap',
|
||
fontFamily: 'inherit',
|
||
transition: 'all 120ms',
|
||
flexShrink: 0,
|
||
}}
|
||
>
|
||
{t.label}
|
||
</button>
|
||
))}
|
||
</div>
|
||
|
||
<div style={{ flex: 1 }} />
|
||
|
||
{/* Sort */}
|
||
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||
<span style={{ fontSize: 11, color: 'var(--ink-5)', whiteSpace: 'nowrap' }}>
|
||
مرتبسازی
|
||
</span>
|
||
<select
|
||
value={sortBy}
|
||
onChange={(e) => setSortBy(e.target.value)}
|
||
style={{
|
||
border: '1px solid var(--ink)',
|
||
padding: '6px 12px',
|
||
fontSize: 11,
|
||
fontFamily: 'inherit',
|
||
background: 'var(--paper)',
|
||
color: 'var(--ink)',
|
||
borderRadius: 0,
|
||
cursor: 'pointer',
|
||
outline: 'none',
|
||
}}
|
||
>
|
||
{SORT_OPTIONS.map((o) => (
|
||
<option key={o.value} value={o.value}>
|
||
{o.label}
|
||
</option>
|
||
))}
|
||
</select>
|
||
</div>
|
||
|
||
{/* Search */}
|
||
<div style={{ position: 'relative' }}>
|
||
<input
|
||
type="text"
|
||
placeholder="جستجو..."
|
||
value={searchQuery}
|
||
onChange={(e) => {
|
||
setSearchQuery(e.target.value)
|
||
setPage(1)
|
||
}}
|
||
style={{
|
||
border: '1px solid var(--ink)',
|
||
padding: '8px 36px 8px 12px',
|
||
fontSize: 12,
|
||
fontFamily: 'inherit',
|
||
background: 'transparent',
|
||
color: 'var(--ink)',
|
||
borderRadius: 0,
|
||
outline: 'none',
|
||
width: 200,
|
||
direction: 'rtl',
|
||
}}
|
||
/>
|
||
<Search
|
||
size={14}
|
||
style={{
|
||
position: 'absolute',
|
||
left: 10,
|
||
top: '50%',
|
||
transform: 'translateY(-50%)',
|
||
color: 'var(--ink-4)',
|
||
pointerEvents: 'none',
|
||
}}
|
||
/>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* ─── Report grid ─────────────────────────────────── */}
|
||
{paged.length > 0 ? (
|
||
<div
|
||
style={{
|
||
display: 'grid',
|
||
gridTemplateColumns: 'repeat(3, 1fr)',
|
||
border: '1px solid var(--rule-thin)',
|
||
borderLeft: 'none',
|
||
borderBottom: 'none',
|
||
}}
|
||
className="reports-grid"
|
||
>
|
||
{paged.map((report) => (
|
||
<ReportCard key={report.id} report={report} />
|
||
))}
|
||
{/* Fill empty cells in last row to keep grid consistent */}
|
||
{paged.length % 3 !== 0 &&
|
||
Array.from({ length: 3 - (paged.length % 3) }).map((_, i) => (
|
||
<div
|
||
key={`empty-${i}`}
|
||
style={{
|
||
borderLeft: '1px solid var(--rule-thin)',
|
||
borderBottom: '1px solid var(--rule-thin)',
|
||
}}
|
||
/>
|
||
))}
|
||
</div>
|
||
) : (
|
||
<div
|
||
style={{
|
||
padding: '80px 0',
|
||
textAlign: 'center',
|
||
color: 'var(--ink-4)',
|
||
}}
|
||
>
|
||
<p style={{ fontSize: 18, fontWeight: 300 }}>نتیجهای یافت نشد</p>
|
||
<p style={{ fontSize: 13, marginTop: 8, color: 'var(--ink-5)' }}>
|
||
جستجو یا فیلترهای خود را تغییر دهید
|
||
</p>
|
||
</div>
|
||
)}
|
||
|
||
{/* ─── Pagination ──────────────────────────────────── */}
|
||
{totalPages > 1 && (
|
||
<div
|
||
style={{
|
||
marginTop: 48,
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
justifyContent: 'center',
|
||
gap: 16,
|
||
borderTop: '1px solid var(--rule-thin)',
|
||
paddingTop: 32,
|
||
}}
|
||
>
|
||
<button
|
||
onClick={() => setPage((p) => Math.max(1, p - 1))}
|
||
disabled={page === 1}
|
||
style={{
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
gap: 6,
|
||
padding: '8px 18px',
|
||
fontSize: 12,
|
||
fontWeight: 600,
|
||
border: '1px solid var(--ink)',
|
||
background: 'transparent',
|
||
color: page === 1 ? 'var(--ink-5)' : 'var(--ink)',
|
||
borderColor: page === 1 ? 'var(--rule-thin)' : 'var(--ink)',
|
||
cursor: page === 1 ? 'not-allowed' : 'pointer',
|
||
fontFamily: 'inherit',
|
||
borderRadius: 0,
|
||
opacity: page === 1 ? 0.4 : 1,
|
||
}}
|
||
>
|
||
<ChevronRight size={14} />
|
||
قبلی
|
||
</button>
|
||
|
||
<span style={{ fontSize: 12, color: 'var(--ink-5)' }}>
|
||
صفحه {page.toLocaleString('fa-IR')} از {totalPages.toLocaleString('fa-IR')}
|
||
</span>
|
||
|
||
<button
|
||
onClick={() => setPage((p) => Math.min(totalPages, p + 1))}
|
||
disabled={page === totalPages}
|
||
style={{
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
gap: 6,
|
||
padding: '8px 18px',
|
||
fontSize: 12,
|
||
fontWeight: 600,
|
||
border: '1px solid var(--ink)',
|
||
background: 'transparent',
|
||
color: page === totalPages ? 'var(--ink-5)' : 'var(--ink)',
|
||
borderColor: page === totalPages ? 'var(--rule-thin)' : 'var(--ink)',
|
||
cursor: page === totalPages ? 'not-allowed' : 'pointer',
|
||
fontFamily: 'inherit',
|
||
borderRadius: 0,
|
||
opacity: page === totalPages ? 0.4 : 1,
|
||
}}
|
||
>
|
||
بعدی
|
||
<ChevronLeft size={14} />
|
||
</button>
|
||
</div>
|
||
)}
|
||
|
||
{/* ─── Mobile responsive styles ────────────────────── */}
|
||
<style>{`
|
||
@media (max-width: 767px) {
|
||
.reports-grid {
|
||
grid-template-columns: 1fr !important;
|
||
}
|
||
}
|
||
@media (max-width: 1024px) and (min-width: 768px) {
|
||
.reports-grid {
|
||
grid-template-columns: repeat(2, 1fr) !important;
|
||
}
|
||
}
|
||
`}</style>
|
||
</div>
|
||
)
|
||
}
|