diff --git a/global/src/global-articles.jsx b/global/src/global-articles.jsx new file mode 100644 index 0000000..21568ea --- /dev/null +++ b/global/src/global-articles.jsx @@ -0,0 +1,341 @@ +// 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 ( +
navigate('/article/' + article.slug)} + onMouseEnter={e => e.currentTarget.style.opacity = '0.85'} + onMouseLeave={e => e.currentTarget.style.opacity = '1'}> +
+
+ {article.date} + · + {article.type && } + +
+

{article.title}

+ {article.subtitle &&
{article.subtitle}
} +
+ {author?.name} + {author?.specialty && · } + {author?.specialty && {author.specialty}} +
+
+ {article.type} + · + {article.readTime} min read + {article.tags?.slice(0, 2).map(t => ( + {t} + ))} +
+
+
+ {article.photo && ( + {article.title} { e.target.style.display = 'none'; }} /> + )} +
+
+ ); +} + +// ── 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 ( +
+
+ Topic: + {cats.map(c => ( + + ))} +
+
+
+ Sort: + {sorts.map(s => ( + + ))} +
+ {filters.q && ( +
+ Search: "{filters.q}" + +
+ )} +
+
+ ); +} + +// ── 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 ( +
+
+
+

All Articles

+

{filtered.length} articles found

+
+ setSearchInput(e.target.value)} + placeholder="Search by title, author, topic..." + style={{ flex: 1, ...inputStyle }} /> + +
+
+ { setFilters(f); setPage(1); }} /> + + {visible.length === 0 ? ( + + ) : ( +
+ {visible.map((a, i) => )} +
+ )} + + {hasMore && ( +
+ +
+ )} +
+
+ ); +} + +// ── Article Detail Page ────────────────────────────────────────────────────── +export default function ArticlePage({ slug }) { + const { navigate } = useNav(); + + const article = allArticles.find(a => a.slug === slug); + + if (!article) { + return ( +
+
+
404
+

Article not found

+

This article may be available in the Persian edition.

+
+ + فارسی (Persian Edition) +
+
+
+ ); + } + + 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 ( +
+ + + {/* Breadcrumb */} +
+
+ + › + + › + {article.title.slice(0, 50)}{article.title.length > 50 ? '…' : ''} +
+
+ + {/* Layout */} +
+ + {/* Main content */} +
+ {/* Header */} +
+
+ {article.type && } + + {article.persianUrl && ( + + فارسی ↗ + + )} +
+

{article.title}

+ {article.subtitle &&

{article.subtitle}

} + + {/* Byline */} +
+ {author && } +
+
{author?.name}
+
{author?.role}
+
+
+ {article.date} + · + {article.readTime} min read + {article.tags?.slice(0, 2).map(t => ( + {t} + ))} +
+
+
+ + {/* Hero image */} + {article.photo && ( +
+ {article.title} { e.target.parentElement.style.display = 'none'; }} /> +
+ )} + + {/* Article body */} +
+ + {/* Author box */} + {author && ( +
+ +
+
{author.name}
+
{author.role}
+
{author.bio}
+
+
+ )} + + {/* Related articles */} + {related.length > 0 && ( +
+

Related Articles

+
+ {related.map(a => )} +
+
+ )} + + {/* Newsletter */} +
+

Subscribe to Teknav

+

Deep tech analysis in your inbox, every week

+ +
+
+ + {/* Sidebar */} + +
+ + +
+ ); +}