419 lines
13 KiB
TypeScript
419 lines
13 KiB
TypeScript
import { useState, useMemo } from 'react'
|
||
import { Link } from 'react-router-dom'
|
||
import { reports } from '@/data/reports'
|
||
import type { ReportCategory, ReportType } from '@/data/reports'
|
||
|
||
/* ─── constants ──────────────────────────────────────────── */
|
||
const TYPE_LABELS: Record<ReportType, string> = {
|
||
quarterly: 'فصلی',
|
||
flash: 'فلش',
|
||
risk: 'ریسک',
|
||
free: 'رایگان',
|
||
special: 'ویژه',
|
||
}
|
||
|
||
const TYPE_COLORS: Record<ReportType, string> = {
|
||
quarterly: '#166534',
|
||
flash: '#9b1c1c',
|
||
risk: '#92400e',
|
||
free: '#1d4ed8',
|
||
special: '#4a1d96',
|
||
}
|
||
|
||
/* Persian month order for sorting date groups */
|
||
const MONTH_ORDER: Record<string, number> = {
|
||
'فروردین': 1, 'اردیبهشت': 2, 'خرداد': 3, 'تیر': 4,
|
||
'مرداد': 5, 'شهریور': 6, 'مهر': 7, 'آبان': 8,
|
||
'آذر': 9, 'دی': 10, 'بهمن': 11, 'اسفند': 12,
|
||
}
|
||
|
||
function parseDateGroup(dateStr: string): number {
|
||
const parts = dateStr.trim().split(' ')
|
||
if (parts.length < 2) return 0
|
||
const month = MONTH_ORDER[parts[0]] ?? 0
|
||
const year = parseInt(parts[1]) || 0
|
||
return year * 100 + month
|
||
}
|
||
|
||
/* ─── unique categories from data ───────────────────────── */
|
||
const ALL_CATEGORIES: (ReportCategory | 'همه')[] = [
|
||
'همه',
|
||
...Array.from(new Set(reports.map((r) => r.category))) as ReportCategory[],
|
||
]
|
||
|
||
/* ─── price formatter ────────────────────────────────────── */
|
||
function formatPrice(price: number): string {
|
||
if (price === 0) return 'رایگان'
|
||
return price.toLocaleString('fa-IR') + ' ت'
|
||
}
|
||
|
||
/* ─── report row ─────────────────────────────────────────── */
|
||
function ReportRow({ report }: { report: (typeof reports)[0] }) {
|
||
const [hovered, setHovered] = useState(false)
|
||
const typeColor = TYPE_COLORS[report.type]
|
||
const typeLabel = TYPE_LABELS[report.type]
|
||
|
||
return (
|
||
<div
|
||
className="grid grid-cols-[64px_1fr_140px_52px_80px] max-md:grid-cols-[56px_1fr_70px] archive-row items-center border-b border-[var(--rule-thin)] min-h-[40px]"
|
||
style={{
|
||
background: hovered ? 'var(--paper-2)' : 'transparent',
|
||
transition: 'background 100ms',
|
||
}}
|
||
onMouseEnter={() => setHovered(true)}
|
||
onMouseLeave={() => setHovered(false)}
|
||
>
|
||
{/* type label */}
|
||
<div
|
||
style={{
|
||
padding: '8px 12px',
|
||
borderLeft: '1px solid var(--rule-thin)',
|
||
}}
|
||
>
|
||
<span
|
||
style={{
|
||
fontSize: 9,
|
||
fontWeight: 700,
|
||
color: typeColor,
|
||
border: `1px solid ${typeColor}`,
|
||
padding: '2px 5px',
|
||
whiteSpace: 'nowrap',
|
||
}}
|
||
>
|
||
{typeLabel}
|
||
</span>
|
||
</div>
|
||
|
||
{/* title */}
|
||
<div style={{ padding: '8px 16px', borderLeft: '1px solid var(--rule-thin)', overflow: 'hidden' }}>
|
||
<Link
|
||
to={`/reports/${report.id}`}
|
||
style={{
|
||
fontSize: 13,
|
||
fontWeight: 700,
|
||
color: hovered ? 'var(--red)' : 'var(--ink)',
|
||
textDecoration: 'none',
|
||
transition: 'color 100ms',
|
||
display: 'block',
|
||
whiteSpace: 'nowrap',
|
||
overflow: 'hidden',
|
||
textOverflow: 'ellipsis',
|
||
}}
|
||
>
|
||
{report.title}
|
||
</Link>
|
||
</div>
|
||
|
||
{/* author */}
|
||
<div
|
||
style={{
|
||
padding: '8px 12px',
|
||
borderLeft: '1px solid var(--rule-thin)',
|
||
overflow: 'hidden',
|
||
}}
|
||
className="max-md:hidden"
|
||
>
|
||
<span
|
||
style={{
|
||
fontSize: 11,
|
||
color: 'var(--ink-5)',
|
||
fontWeight: 400,
|
||
whiteSpace: 'nowrap',
|
||
overflow: 'hidden',
|
||
textOverflow: 'ellipsis',
|
||
display: 'block',
|
||
}}
|
||
>
|
||
{report.author}
|
||
</span>
|
||
</div>
|
||
|
||
{/* pages */}
|
||
<div
|
||
style={{
|
||
padding: '8px 10px',
|
||
borderLeft: '1px solid var(--rule-thin)',
|
||
textAlign: 'center',
|
||
}}
|
||
className="max-md:hidden"
|
||
>
|
||
<span style={{ fontSize: 11, color: 'var(--ink-5)', fontWeight: 400 }}>
|
||
{report.pages.toLocaleString('fa-IR')} ص
|
||
</span>
|
||
</div>
|
||
|
||
{/* price */}
|
||
<div style={{ padding: '8px 12px', textAlign: 'left' }}>
|
||
<span
|
||
style={{
|
||
fontSize: 11,
|
||
fontWeight: 700,
|
||
color: report.isFree ? 'var(--green-ink)' : 'var(--ink-3)',
|
||
whiteSpace: 'nowrap',
|
||
}}
|
||
>
|
||
{formatPrice(report.price)}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
/* ─── main component ─────────────────────────────────────── */
|
||
export default function Archive() {
|
||
const [search, setSearch] = useState('')
|
||
const [activeCategory, setActiveCategory] = useState<ReportCategory | 'همه'>('همه')
|
||
|
||
const filtered = useMemo(() => {
|
||
let result = [...reports]
|
||
|
||
if (activeCategory !== 'همه') {
|
||
result = result.filter((r) => r.category === activeCategory)
|
||
}
|
||
|
||
if (search.trim()) {
|
||
const q = search.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))
|
||
)
|
||
}
|
||
|
||
return result
|
||
}, [search, activeCategory])
|
||
|
||
/* group by publishDate, sorted newest first */
|
||
const groups = useMemo(() => {
|
||
const map = new Map<string, (typeof reports)[0][]>()
|
||
for (const report of filtered) {
|
||
const key = report.publishDate
|
||
if (!map.has(key)) map.set(key, [])
|
||
map.get(key)!.push(report)
|
||
}
|
||
return Array.from(map.entries()).sort(
|
||
([a], [b]) => parseDateGroup(b) - parseDateGroup(a)
|
||
)
|
||
}, [filtered])
|
||
|
||
return (
|
||
<div
|
||
style={{
|
||
maxWidth: 1280,
|
||
margin: '0 auto',
|
||
padding: 'clamp(24px,5vw,64px) clamp(16px,4vw,48px)',
|
||
direction: 'rtl',
|
||
}}
|
||
>
|
||
{/* ─── Page header ───────────────────────────────────── */}
|
||
<div
|
||
style={{
|
||
display: 'flex',
|
||
alignItems: 'stretch',
|
||
borderBottom: '3px solid var(--ink)',
|
||
marginBottom: 36,
|
||
}}
|
||
>
|
||
<div
|
||
style={{
|
||
background: 'var(--ink)',
|
||
padding: '14px 28px',
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
gap: 14,
|
||
}}
|
||
>
|
||
<h1
|
||
style={{
|
||
fontSize: 24,
|
||
fontWeight: 900,
|
||
color: 'var(--paper)',
|
||
letterSpacing: '-0.5px',
|
||
whiteSpace: 'nowrap',
|
||
}}
|
||
>
|
||
آرشیو گزارشها
|
||
</h1>
|
||
<span
|
||
style={{
|
||
fontSize: 12,
|
||
fontWeight: 700,
|
||
color: 'var(--ink)',
|
||
background: 'var(--paper)',
|
||
padding: '2px 10px',
|
||
letterSpacing: '0.3px',
|
||
}}
|
||
>
|
||
{reports.length.toLocaleString('fa-IR')} گزارش
|
||
</span>
|
||
</div>
|
||
<div style={{ flex: 1 }} />
|
||
</div>
|
||
|
||
{/* ─── Search ────────────────────────────────────────── */}
|
||
<div style={{ marginBottom: 20 }}>
|
||
<input
|
||
type="text"
|
||
placeholder="جستجو در آرشیو..."
|
||
value={search}
|
||
onChange={(e) => setSearch(e.target.value)}
|
||
style={{
|
||
width: '100%',
|
||
border: '1px solid var(--ink)',
|
||
borderRadius: 0,
|
||
padding: '12px 18px',
|
||
fontSize: 14,
|
||
fontFamily: 'inherit',
|
||
background: 'transparent',
|
||
color: 'var(--ink)',
|
||
outline: 'none',
|
||
direction: 'rtl',
|
||
}}
|
||
/>
|
||
</div>
|
||
|
||
{/* ─── Category filter chips ─────────────────────────── */}
|
||
<div
|
||
style={{
|
||
display: 'flex',
|
||
gap: 6,
|
||
flexWrap: 'wrap',
|
||
marginBottom: 40,
|
||
paddingBottom: 20,
|
||
borderBottom: '1px solid var(--rule-thin)',
|
||
}}
|
||
>
|
||
{ALL_CATEGORIES.map((cat) => {
|
||
const isActive = activeCategory === cat
|
||
return (
|
||
<button
|
||
key={cat}
|
||
onClick={() => setActiveCategory(cat as ReportCategory | 'همه')}
|
||
style={{
|
||
padding: '6px 16px',
|
||
fontSize: 12,
|
||
fontWeight: isActive ? 700 : 500,
|
||
border: isActive ? '1px solid var(--ink)' : '1px solid var(--rule-thin)',
|
||
background: isActive ? 'var(--ink)' : 'transparent',
|
||
color: isActive ? 'var(--paper)' : 'var(--ink-4)',
|
||
cursor: 'pointer',
|
||
fontFamily: 'inherit',
|
||
whiteSpace: 'nowrap',
|
||
transition: 'all 120ms',
|
||
outline: 'none',
|
||
borderRadius: 0,
|
||
}}
|
||
>
|
||
{cat}
|
||
</button>
|
||
)
|
||
})}
|
||
</div>
|
||
|
||
{/* ─── Date groups ───────────────────────────────────── */}
|
||
{groups.length > 0 ? (
|
||
<div style={{ display: 'flex', flexDirection: 'column', gap: 32 }}>
|
||
{groups.map(([date, groupReports]) => (
|
||
<section key={date}>
|
||
{/* section divider */}
|
||
<div
|
||
style={{
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
gap: 14,
|
||
marginBottom: 0,
|
||
}}
|
||
>
|
||
<span
|
||
style={{
|
||
fontSize: 9,
|
||
fontWeight: 700,
|
||
letterSpacing: '2px',
|
||
textTransform: 'uppercase',
|
||
color: 'var(--ink-5)',
|
||
whiteSpace: 'nowrap',
|
||
}}
|
||
>
|
||
{date}
|
||
</span>
|
||
<div
|
||
style={{
|
||
flex: 1,
|
||
height: 1,
|
||
background: 'var(--rule-thin)',
|
||
}}
|
||
/>
|
||
<span style={{ fontSize: 9, color: 'var(--ink-6)', whiteSpace: 'nowrap' }}>
|
||
{groupReports.length.toLocaleString('fa-IR')} گزارش
|
||
</span>
|
||
</div>
|
||
|
||
{/* column headers */}
|
||
<div
|
||
className="grid grid-cols-[64px_1fr_140px_52px_80px] max-md:grid-cols-[56px_1fr_70px] archive-row items-center border-t-2 border-b border-[var(--ink)] mt-2 bg-[var(--paper-2)]"
|
||
>
|
||
{[
|
||
{ label: 'نوع', style: { padding: '6px 12px', borderLeft: '1px solid var(--rule-thin)' } },
|
||
{ label: 'عنوان گزارش', style: { padding: '6px 16px', borderLeft: '1px solid var(--rule-thin)' } },
|
||
{ label: 'نویسنده', style: { padding: '6px 12px', borderLeft: '1px solid var(--rule-thin)' }, hideMobile: true },
|
||
{ label: 'صفحه', style: { padding: '6px 10px', borderLeft: '1px solid var(--rule-thin)', textAlign: 'center' as const }, hideMobile: true },
|
||
{ label: 'قیمت', style: { padding: '6px 12px', textAlign: 'left' as const } },
|
||
].map((col) => (
|
||
<div key={col.label} style={col.style} className={col.hideMobile ? "max-md:hidden" : ""}>
|
||
<span
|
||
style={{
|
||
fontSize: 9,
|
||
fontWeight: 700,
|
||
color: 'var(--ink-4)',
|
||
textTransform: 'uppercase',
|
||
}}
|
||
>
|
||
{col.label}
|
||
</span>
|
||
</div>
|
||
))}
|
||
</div>
|
||
|
||
{/* report rows */}
|
||
<div
|
||
style={{
|
||
border: '1px solid var(--rule-thin)',
|
||
borderTop: 'none',
|
||
}}
|
||
>
|
||
{groupReports.map((report) => (
|
||
<ReportRow key={report.id} report={report} />
|
||
))}
|
||
</div>
|
||
</section>
|
||
))}
|
||
</div>
|
||
) : (
|
||
<div
|
||
style={{
|
||
padding: '80px 0',
|
||
textAlign: 'center',
|
||
color: 'var(--ink-5)',
|
||
}}
|
||
>
|
||
<p style={{ fontSize: 15, fontWeight: 300 }}>نتیجهای یافت نشد</p>
|
||
<p style={{ fontSize: 13, marginTop: 8, color: 'var(--ink-6)' }}>
|
||
عبارت جستجو یا فیلتر دستهبندی را تغییر دهید
|
||
</p>
|
||
</div>
|
||
)}
|
||
|
||
{/* responsive: hide author/pages columns on small screens */}
|
||
<style>{`
|
||
@media (max-width: 767px) {
|
||
.archive-row {
|
||
grid-template-columns: 56px 1fr 70px !important;
|
||
}
|
||
}
|
||
`}</style>
|
||
</div>
|
||
)
|
||
}
|