Files
teknav-bak/global/src/global-articles.jsx
T
arsam 3427a8cb3e Import source: global/src/global-articles
Import the existing local project snapshot. This commit organizes existing files; it does not represent newly developed functionality.
2026-09-18 23:27:13 +03:30

342 lines
20 KiB
React
Raw Blame History

This file contains ambiguous Unicode characters
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.
// global-articles.jsx — Article detail + article list for Teknav Global (English, LTR)
import { useState, useEffect, useRef, useMemo } from 'react';
import { articles as allArticles, authors, categories } from '../global-data.js';
import {
useNav, useToast, CategoryBadge, TypeBadge, AuthorAvatar, EmptyState,
ReadingProgress, SkeletonLoader, NewsletterForm,
inputStyle, pillBtn, iconBtnStyle,
} from './global-ui.jsx';
import { ArticleCard, sectionTitle, sectionSubtitle } from './global-home.jsx';
// ── Article List Row ─────────────────────────────────────────────────────────
function ArticleRow({ article, idx }) {
const { navigate } = useNav();
const [vis, setVis] = useState(false);
const ref = useRef();
const cat = categories.find(c => c.id === article.category);
const author = authors.find(a => a.id === article.author);
useEffect(() => {
const obs = new IntersectionObserver(([e]) => { if (e.isIntersecting) setVis(true); }, { threshold: 0.05 });
if (ref.current) obs.observe(ref.current);
return () => obs.disconnect();
}, []);
return (
<div ref={ref} className="article-row" style={{
display: 'grid', gridTemplateColumns: '1fr 140px', gap: 24,
padding: '32px 0', borderBottom: '1px solid #E4DDD2',
cursor: 'pointer', opacity: vis ? 1 : 0, transform: vis ? 'none' : 'translateY(16px)',
transition: `opacity 0.4s ${idx * 60}ms, transform 0.4s ${idx * 60}ms`,
}} onClick={() => navigate('/article/' + article.slug)}
onMouseEnter={e => e.currentTarget.style.opacity = '0.85'}
onMouseLeave={e => e.currentTarget.style.opacity = '1'}>
<div>
<div className="article-row-meta" style={{ display: 'flex', gap: 8, alignItems: 'center', marginBottom: 10, flexWrap: 'wrap' }}>
<span style={{ fontSize: 12, color: '#5F6B6D' }}>{article.date}</span>
<span style={{ color: '#E4DDD2' }}>·</span>
{article.type && <TypeBadge type={article.type} small />}
<CategoryBadge name={cat?.name || article.category} color={cat?.color} small />
</div>
<h2 style={{ fontSize: 19, fontWeight: 800, color: '#263238', margin: '0 0 6px', lineHeight: 1.5, fontFamily: 'Inter,sans-serif' }}>{article.title}</h2>
{article.subtitle && <div style={{ fontSize: 14, color: '#0F6B73', fontWeight: 600, margin: '0 0 8px' }}>{article.subtitle}</div>}
<div style={{ fontSize: 13, color: '#5F6B6D', margin: '0 0 8px' }}>
<span style={{ fontWeight: 700, color: '#263238' }}>{author?.name}</span>
{author?.specialty && <span style={{ color: '#E4DDD2' }}> · </span>}
{author?.specialty && <span>{author.specialty}</span>}
</div>
<div className="article-row-stats" style={{ display: 'flex', gap: 16, fontSize: 12, color: '#5F6B6D', alignItems: 'center' }}>
<span style={{ color: '#C46A4D', fontWeight: 600 }}>{article.type}</span>
<span>·</span>
<span>{article.readTime} min read</span>
{article.tags?.slice(0, 2).map(t => (
<span key={t} style={{ background: '#F4EFE6', padding: '2px 8px', borderRadius: 4, fontSize: 11 }}>{t}</span>
))}
</div>
</div>
<div className="article-row-thumb" style={{ background: '#F4EFE6', borderRadius: 10, overflow: 'hidden', height: 120, alignSelf: 'center' }}>
{article.photo && (
<img src={article.photo} alt={article.title} loading="lazy"
style={{ width: '100%', height: '100%', objectFit: 'cover' }}
onError={e => { e.target.style.display = 'none'; }} />
)}
</div>
</div>
);
}
// ── Filter Bar ───────────────────────────────────────────────────────────────
function FilterBar({ filters, onChange }) {
const cats = [{ id: '', name: 'All' }, ...categories];
const sorts = [{ v: 'latest', l: 'Latest' }, { v: 'popular', l: 'Popular' }, { v: 'readtime', l: 'Read time' }];
const chip = (active) => ({
border: active ? '1.5px solid #0F6B73' : '1px solid #E4DDD2',
background: active ? '#0F6B7312' : '#fff',
color: active ? '#0F6B73' : '#5F6B6D',
padding: '5px 12px', borderRadius: 20, fontSize: 12, cursor: 'pointer',
fontFamily: 'Inter,sans-serif', fontWeight: active ? 700 : 400,
transition: 'all 0.15s', whiteSpace: 'nowrap',
});
return (
<div className="filter-bar" style={{ marginBottom: 32, display: 'flex', flexDirection: 'column', gap: 12 }}>
<div className="filter-chip-row" style={{ display: 'flex', gap: 8, flexWrap: 'wrap', alignItems: 'center' }}>
<span style={{ fontSize: 12, color: '#5F6B6D', whiteSpace: 'nowrap' }}>Topic:</span>
{cats.map(c => (
<button key={c.id} style={chip(filters.cat === c.id)} onClick={() => onChange({ ...filters, cat: c.id })}>{c.name}</button>
))}
</div>
<div className="filter-chip-row" style={{ display: 'flex', gap: 16, alignItems: 'center', flexWrap: 'wrap' }}>
<div style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
<span style={{ fontSize: 12, color: '#5F6B6D' }}>Sort:</span>
{sorts.map(s => (
<button key={s.v} style={chip(filters.sort === s.v)} onClick={() => onChange({ ...filters, sort: s.v })}>{s.l}</button>
))}
</div>
{filters.q && (
<div style={{ display: 'flex', alignItems: 'center', gap: 8, background: '#F4EFE6', padding: '5px 12px', borderRadius: 20 }}>
<span style={{ fontSize: 12, color: '#263238' }}>Search: "{filters.q}"</span>
<button onClick={() => onChange({ ...filters, q: '' })} style={{ background: 'none', border: 'none', cursor: 'pointer', color: '#5F6B6D', fontSize: 14 }}>✕</button>
</div>
)}
</div>
</div>
);
}
// ── Article List Page ────────────────────────────────────────────────────────
export function ArticlesListPage({ initQ = '' }) {
const [filters, setFilters] = useState({ cat: '', sort: 'latest', q: initQ });
const [searchInput, setSearchInput] = useState(initQ);
const [page, setPage] = useState(1);
const PER = 8;
const filtered = useMemo(() => {
let list = allArticles;
if (filters.q) {
const q = filters.q.toLowerCase();
list = list.filter(a =>
a.title.toLowerCase().includes(q) ||
(a.subtitle || '').toLowerCase().includes(q) ||
authors.find(au => au.id === a.author)?.name.toLowerCase().includes(q) ||
categories.find(c => c.id === a.category)?.name.toLowerCase().includes(q)
);
}
if (filters.cat) list = list.filter(a => a.category === filters.cat);
if (filters.sort === 'popular') list = [...list].sort((a, b) => (b.views || 0) - (a.views || 0));
else if (filters.sort === 'readtime') list = [...list].sort((a, b) => b.readTime - a.readTime);
else list = [...list].sort((a, b) => (b.dateEn || b.date || '').localeCompare(a.dateEn || a.date || ''));
return list;
}, [filters]);
const visible = filtered.slice(0, page * PER);
const hasMore = visible.length < filtered.length;
const doSearch = e => { e.preventDefault(); setFilters(f => ({ ...f, q: searchInput })); setPage(1); };
return (
<div className="article-list-page" style={{ paddingTop: 80, background: '#FAF7F0', minHeight: '100vh', fontFamily: 'Inter,sans-serif' }}>
<div className="article-list-inner" style={{ maxWidth: 1000, margin: '0 auto', padding: '0 24px' }}>
<div className="article-list-head" style={{ padding: '32px 0 0' }}>
<h1 style={{ fontSize: 28, fontWeight: 900, color: '#263238', margin: '0 0 4px' }}>All Articles</h1>
<p style={{ fontSize: 14, color: '#5F6B6D', margin: '0 0 28px' }}>{filtered.length} articles found</p>
<form className="article-search-form" onSubmit={doSearch} style={{ display: 'flex', gap: 10, marginBottom: 24 }}>
<input value={searchInput} onChange={e => setSearchInput(e.target.value)}
placeholder="Search by title, author, topic..."
style={{ flex: 1, ...inputStyle }} />
<button type="submit" style={{ ...pillBtn, background: '#0F6B73', color: '#fff' }}>Search</button>
</form>
</div>
<FilterBar filters={filters} onChange={f => { setFilters(f); setPage(1); }} />
{visible.length === 0 ? (
<EmptyState title="No articles found" subtitle="Try different filters or search terms" />
) : (
<div>
{visible.map((a, i) => <ArticleRow key={a.id} article={a} idx={i} />)}
</div>
)}
{hasMore && (
<div className="load-more-wrap" style={{ textAlign: 'center', padding: '40px 0' }}>
<button onClick={() => setPage(p => p + 1)} style={{ ...pillBtn, background: '#0F6B73', color: '#fff', padding: '12px 32px' }}>
Load more ({filtered.length - visible.length} remaining)
</button>
</div>
)}
</div>
</div>
);
}
// ── Article Detail Page ──────────────────────────────────────────────────────
export default function ArticlePage({ slug }) {
const { navigate } = useNav();
const article = allArticles.find(a => a.slug === slug);
if (!article) {
return (
<div style={{ paddingTop: 64, minHeight: '100vh', background: '#FAF7F0', fontFamily: 'Inter,sans-serif' }}>
<div style={{ maxWidth: 720, margin: '0 auto', padding: '60px 24px', textAlign: 'center' }}>
<div style={{ fontSize: 48, fontWeight: 900, color: '#0F6B73', opacity: 0.2, marginBottom: 16 }}>404</div>
<h2 style={{ fontSize: 24, fontWeight: 700, marginBottom: 12, color: '#263238' }}>Article not found</h2>
<p style={{ color: '#5F6B6D', marginBottom: 8 }}>This article may be available in the Persian edition.</p>
<div style={{ display: 'flex', gap: 12, justifyContent: 'center', marginTop: 28, flexWrap: 'wrap' }}>
<button onClick={() => navigate('/')} style={{ ...pillBtn, background: '#0F6B73', color: '#fff' }}>Home</button>
<a href="https://www.teknav.ir" style={{ ...pillBtn, background: '#F4EFE6', color: '#263238', border: '1px solid #E4DDD2' }}>فارسی (Persian Edition)</a>
</div>
</div>
</div>
);
}
const author = authors.find(a => a.id === article.author);
const cat = categories.find(c => c.id === article.category);
const related = allArticles.filter(a => a.id !== article.id && (a.category === article.category || (a.tags || []).some(t => (article.tags || []).includes(t)))).slice(0, 3);
return (
<div className="article-detail-page" style={{ paddingTop: 64, background: '#FAF7F0', minHeight: '100vh', fontFamily: 'Inter,sans-serif' }}>
<ReadingProgress />
{/* Breadcrumb */}
<div className="article-breadcrumb" style={{ background: '#F4EFE6', borderBottom: '1px solid #E4DDD2', padding: '10px 24px', fontSize: 12, color: '#5F6B6D' }}>
<div style={{ maxWidth: 1280, margin: '0 auto', display: 'flex', gap: 6, alignItems: 'center' }}>
<button onClick={() => navigate('/')} style={{ background: 'none', border: 'none', color: '#5F6B6D', cursor: 'pointer', fontSize: 12, fontFamily: 'Inter,sans-serif' }}>Home</button>
<span>›</span>
<button onClick={() => navigate('/category/' + article.category)} style={{ background: 'none', border: 'none', color: '#5F6B6D', cursor: 'pointer', fontSize: 12, fontFamily: 'Inter,sans-serif' }}>{cat?.name || article.category}</button>
<span>›</span>
<span style={{ color: '#263238', fontWeight: 600 }}>{article.title.slice(0, 50)}{article.title.length > 50 ? '…' : ''}</span>
</div>
</div>
{/* Layout */}
<div className="article-layout" style={{ maxWidth: 1280, margin: '0 auto', padding: '40px 24px', display: 'grid', gridTemplateColumns: '1fr 300px', gap: 56, alignItems: 'start' }}>
{/* Main content */}
<article className="article-main">
{/* Header */}
<div className="article-header" style={{ marginBottom: 32, paddingBottom: 24, borderBottom: '1px solid #E4DDD2' }}>
<div style={{ display: 'flex', gap: 8, marginBottom: 14, flexWrap: 'wrap' }}>
{article.type && <TypeBadge type={article.type} />}
<CategoryBadge name={cat?.name || article.category} color={cat?.color} />
{article.persianUrl && (
<a href={article.persianUrl} target="_blank" rel="noopener noreferrer" style={{ display: 'inline-flex', alignItems: 'center', gap: 4, padding: '3px 10px', borderRadius: 4, fontSize: 11, fontWeight: 600, background: '#F4EFE6', color: '#5F6B6D', border: '1px solid #E4DDD2', textDecoration: 'none' }}>
فارسی ↗
</a>
)}
</div>
<h1 style={{ fontSize: 'clamp(24px,3.5vw,36px)', fontWeight: 800, color: '#0F2A2E', margin: '0 0 12px', lineHeight: 1.3, fontFamily: 'Inter,sans-serif', letterSpacing: '-0.02em' }}>{article.title}</h1>
{article.subtitle && <p style={{ fontSize: 18, color: '#5F6B6D', lineHeight: 1.7, margin: '0 0 16px' }}>{article.subtitle}</p>}
{/* Byline */}
<div className="article-byline-row" style={{ display: 'flex', alignItems: 'center', gap: 16, paddingTop: 16, borderTop: '1px solid #E4DDD2', flexWrap: 'wrap' }}>
{author && <AuthorAvatar author={author} size={40} />}
<div style={{ flex: 1 }}>
<div style={{ fontSize: 14, fontWeight: 700, color: '#263238' }}>{author?.name}</div>
<div style={{ fontSize: 12, color: '#5F6B6D' }}>{author?.role}</div>
</div>
<div className="article-meta-line" style={{ display: 'flex', gap: 12, fontSize: 12, color: '#5F6B6D', alignItems: 'center', flexWrap: 'wrap' }}>
<span>{article.date}</span>
<span>·</span>
<span>{article.readTime} min read</span>
{article.tags?.slice(0, 2).map(t => (
<span key={t} style={{ background: '#F4EFE6', padding: '2px 8px', borderRadius: 4 }}>{t}</span>
))}
</div>
</div>
</div>
{/* Hero image */}
{article.photo && (
<div style={{ borderRadius: 12, overflow: 'hidden', marginBottom: 28, border: '1px solid #E4DDD2' }}>
<img src={article.photo} alt={article.title} loading="eager"
style={{ width: '100%', height: 'auto', display: 'block', maxHeight: 480, objectFit: 'cover' }}
onError={e => { e.target.parentElement.style.display = 'none'; }} />
</div>
)}
{/* Article body */}
<div className="article-content" style={{ fontSize: 17, lineHeight: 1.9, color: '#263238', fontFamily: 'Inter,sans-serif' }}
dangerouslySetInnerHTML={{ __html: article.content }} />
{/* Author box */}
{author && (
<div className="article-author-box" style={{ display: 'flex', gap: 16, padding: 20, background: '#F4EFE6', borderRadius: 12, marginTop: 40, border: '1px solid #E4DDD2', alignItems: 'flex-start' }}>
<AuthorAvatar author={author} size={52} />
<div>
<div style={{ fontSize: 13, fontWeight: 800, color: '#263238', marginBottom: 4 }}>{author.name}</div>
<div style={{ fontSize: 12, color: '#0F6B73', fontWeight: 600, marginBottom: 8 }}>{author.role}</div>
<div style={{ fontSize: 13, color: '#5F6B6D', lineHeight: 1.7 }}>{author.bio}</div>
</div>
</div>
)}
{/* Related articles */}
{related.length > 0 && (
<div className="article-related" style={{ marginTop: 40 }}>
<h2 style={{ fontSize: 18, fontWeight: 800, color: '#263238', marginBottom: 20 }}>Related Articles</h2>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill,minmax(240px,1fr))', gap: 16 }}>
{related.map(a => <ArticleCard key={a.id} article={a} />)}
</div>
</div>
)}
{/* Newsletter */}
<div style={{ marginTop: 60, background: '#ebe3d2', borderRadius: 16, padding: '40px 28px', textAlign: 'center', border: '1px solid rgba(15,42,46,0.06)' }}>
<h3 style={{ fontSize: 20, fontWeight: 800, color: '#0F2A2E', margin: '0 0 8px', fontFamily: 'Inter,sans-serif' }}>Subscribe to Teknav</h3>
<p style={{ fontSize: 14, color: '#4A5A5C', margin: '0 0 24px', lineHeight: 1.8 }}>Deep tech analysis in your inbox, every week</p>
<NewsletterForm />
</div>
</article>
{/* Sidebar */}
<aside style={{ position: 'sticky', top: 84 }}>
{/* Tags */}
{article.tags?.length > 0 && (
<div style={{ background: '#fff', border: '1px solid #E4DDD2', borderRadius: 12, padding: 20, marginBottom: 20 }}>
<h3 style={{ fontSize: 13, fontWeight: 700, color: '#263238', marginBottom: 14 }}>Tags</h3>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 8 }}>
{article.tags.map(t => (
<span key={t} style={{ background: '#F4EFE6', border: '1px solid #E4DDD2', padding: '4px 10px', borderRadius: 6, fontSize: 12, color: '#5F6B6D' }}>{t}</span>
))}
</div>
</div>
)}
{/* Persian link */}
{article.persianUrl && (
<div style={{ background: '#F4EFE6', border: '1px solid #E4DDD2', borderRadius: 12, padding: 20, marginBottom: 20 }}>
<h3 style={{ fontSize: 13, fontWeight: 700, color: '#263238', marginBottom: 10 }}>Also in Persian</h3>
<p style={{ fontSize: 12, color: '#5F6B6D', marginBottom: 12, lineHeight: 1.6 }}>This article is available in Persian on Teknav.ir</p>
<a href={article.persianUrl} target="_blank" rel="noopener noreferrer" style={{ display: 'block', textAlign: 'center', ...pillBtn, background: '#0F6B73', color: '#fff', justifyContent: 'center', textDecoration: 'none' }}>
Read in Persian ↗
</a>
</div>
)}
{/* More from category */}
{related.length > 0 && (
<div style={{ background: '#fff', border: '1px solid #E4DDD2', borderRadius: 12, padding: 20 }}>
<h3 style={{ fontSize: 13, fontWeight: 700, color: '#263238', marginBottom: 14 }}>More in {cat?.name}</h3>
{related.slice(0, 3).map(a => (
<button key={a.id} onClick={() => navigate('/article/' + a.slug)} style={{ display: 'block', width: '100%', textAlign: 'left', background: 'none', border: 'none', padding: '10px 0', borderBottom: '1px solid #F4EFE6', cursor: 'pointer', fontFamily: 'Inter,sans-serif' }}>
<div style={{ fontSize: 13, fontWeight: 600, color: '#263238', lineHeight: 1.5, marginBottom: 4 }}>{a.title}</div>
<div style={{ fontSize: 11, color: '#5F6B6D' }}>{a.readTime} min · {a.date}</div>
</button>
))}
</div>
)}
</aside>
</div>
<style>{`
@media (max-width: 768px) {
.article-layout { grid-template-columns: 1fr !important; }
aside { display: none; }
}
`}</style>
</div>
);
}