steelforesight/src/pages/Archive/Archive.tsx

419 lines
13 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 { 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>
)
}