steelforesight/src/pages/Reports/Reports.tsx

582 lines
18 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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