386 lines
21 KiB
TypeScript
386 lines
21 KiB
TypeScript
import { useParams, Link, useNavigate } from 'react-router-dom'
|
|
import { motion } from 'framer-motion'
|
|
import { Calendar, Tag, MessageCircle, ArrowLeft, MoreHorizontal, Heart, Reply, Plus, UserPlus, LogIn, Send, Share2, Globe, Mail } from 'lucide-react'
|
|
import { useLang } from '@/context/LangContext'
|
|
import { useAuth } from '@/context/AuthContext'
|
|
import { getPost, type Post } from '@/content/posts'
|
|
import { MagicTextGroup } from '@/components/ui/magic-text'
|
|
|
|
const NAVY = '#032340'
|
|
const GOLD = '#CD9E53'
|
|
const BLUE = '#1f8fff'
|
|
const PAPER = '#faf7f4'
|
|
const EASE = [0.22, 1, 0.36, 1] as const
|
|
|
|
/* ════════ Pareto chart (bars desc + cumulative line + 80% rule) ════════ */
|
|
function ParetoChart() {
|
|
const bars = [46, 30, 22, 14, 10, 7, 5, 3]
|
|
const total = bars.reduce((a, b) => a + b, 0)
|
|
let acc = 0
|
|
const cum = bars.map((b) => { acc += b; return (acc / total) * 100 })
|
|
const W = 560, H = 240, padL = 36, padR = 40, padB = 44, padT = 14
|
|
const cw = W - padL - padR, ch = H - padB - padT
|
|
const maxBar = 60
|
|
const bw = cw / bars.length
|
|
const x = (i: number) => padL + i * bw + bw / 2
|
|
const yBar = (v: number) => padT + ch - (v / maxBar) * ch
|
|
const yCum = (v: number) => padT + ch - (v / 100) * ch
|
|
const labels = ['Defect Type A','Defect Type B','Defect Type C','Defect Type D','Defect Type E','Defect Type F','Defect Type G','Defect Type H']
|
|
const linePts = cum.map((v, i) => `${x(i)},${yCum(v)}`).join(' ')
|
|
|
|
return (
|
|
<div style={{ background: '#fff', borderRadius: 14, border: '1px solid rgba(7,29,73,0.08)', padding: 18, boxShadow: '0 10px 30px -24px rgba(7,29,73,0.3)' }}>
|
|
<svg viewBox={`0 0 ${W} ${H}`} width="100%" style={{ display: 'block', direction: 'ltr' }}>
|
|
{/* y grid + left axis labels */}
|
|
{[0, 15, 30, 45, 60].map((v) => (
|
|
<g key={v}>
|
|
<line x1={padL} y1={yBar(v)} x2={W - padR} y2={yBar(v)} stroke="rgba(7,29,73,0.08)" strokeWidth="1" />
|
|
<text x={padL - 6} y={yBar(v) + 3} fontSize="9" fill="rgba(7,29,73,0.5)" textAnchor="end">{v}</text>
|
|
</g>
|
|
))}
|
|
{/* right axis (cumulative %) */}
|
|
{[0, 25, 50, 75, 100].map((v) => (
|
|
<text key={v} x={W - padR + 6} y={yCum(v) + 3} fontSize="9" fill="rgba(7,29,73,0.5)" textAnchor="start">{v}</text>
|
|
))}
|
|
{/* 80% dashed rule */}
|
|
<line x1={padL} y1={yCum(80)} x2={W - padR} y2={yCum(80)} stroke="rgba(7,29,73,0.35)" strokeWidth="1" strokeDasharray="4 4" />
|
|
{/* bars */}
|
|
{bars.map((v, i) => (
|
|
<rect key={i} x={x(i) - bw * 0.32} y={yBar(v)} width={bw * 0.64} height={padT + ch - yBar(v)} rx="2" fill={GOLD} opacity={0.92} />
|
|
))}
|
|
{/* cumulative line + dots */}
|
|
<polyline points={linePts} fill="none" stroke={BLUE} strokeWidth="2" />
|
|
{cum.map((v, i) => <circle key={i} cx={x(i)} cy={yCum(v)} r="3" fill={BLUE} />)}
|
|
{/* x labels */}
|
|
{labels.map((l, i) => (
|
|
<text key={i} x={x(i)} y={H - padB + 14} fontSize="7.5" fill="rgba(7,29,73,0.55)" textAnchor="end" transform={`rotate(-35 ${x(i)} ${H - padB + 14})`}>{l}</text>
|
|
))}
|
|
</svg>
|
|
{/* legend */}
|
|
<div style={{ display: 'flex', justifyContent: 'center', gap: 20, margin: '4px 0 14px', direction: 'ltr' }}>
|
|
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 6, fontSize: 11, color: 'rgba(7,29,73,0.7)' }}><span style={{ width: 14, height: 3, background: 'rgba(7,29,73,0.4)', display: 'inline-block' }} />80% Line</span>
|
|
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 6, fontSize: 11, color: 'rgba(7,29,73,0.7)' }}><span style={{ width: 12, height: 8, background: BLUE, display: 'inline-block', borderRadius: 2 }} />Cumulative %</span>
|
|
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 6, fontSize: 11, color: 'rgba(7,29,73,0.7)' }}><span style={{ width: 12, height: 8, background: GOLD, display: 'inline-block', borderRadius: 2 }} />Frequency</span>
|
|
</div>
|
|
{/* caption box */}
|
|
<div style={{ background: PAPER, borderRadius: 10, padding: '12px 16px', display: 'flex', gap: 40, direction: 'ltr', flexWrap: 'wrap' }}>
|
|
<div style={{ fontSize: 12, fontWeight: 700, color: NAVY }}>Pareto Analysis (80/20 Rule)</div>
|
|
<div><div style={{ fontSize: 10, color: 'rgba(7,29,73,0.5)' }}>Top 20% categories:</div><div style={{ fontSize: 13, fontWeight: 800, color: NAVY }}>2 of 8</div></div>
|
|
<div><div style={{ fontSize: 10, color: 'rgba(7,29,73,0.5)' }}>Contribute:</div><div style={{ fontSize: 13, fontWeight: 800, color: GOLD }}>68.8% of total</div></div>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
/* ════════ Grouped monthly bar chart (expenses / profit / revenue) ════════ */
|
|
function BarsChart() {
|
|
const months = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec']
|
|
const data = [
|
|
[3000, 2000, 4500], [3200, 1800, 5200], [2800, 2400, 4800], [3500, 2600, 6000],
|
|
[3100, 2200, 5500], [3400, 2800, 6200], [3000, 2500, 5800], [3600, 3000, 6800],
|
|
[3300, 2700, 6400], [3800, 3200, 7600], [3500, 2900, 8200], [3200, 3400, 9000],
|
|
]
|
|
const W = 560, H = 250, padL = 40, padR = 12, padB = 36, padT = 14
|
|
const cw = W - padL - padR, ch = H - padB - padT
|
|
const maxV = 10000
|
|
const gw = cw / months.length
|
|
const colors = [NAVY, GOLD, BLUE]
|
|
const y = (v: number) => padT + ch - (v / maxV) * ch
|
|
|
|
return (
|
|
<div style={{ background: '#fff', borderRadius: 14, border: '1px solid rgba(7,29,73,0.08)', padding: 18, boxShadow: '0 10px 30px -24px rgba(7,29,73,0.3)' }}>
|
|
<svg viewBox={`0 0 ${W} ${H}`} width="100%" style={{ display: 'block', direction: 'ltr' }}>
|
|
{[0, 2500, 5000, 7500, 10000].map((v) => (
|
|
<g key={v}>
|
|
<line x1={padL} y1={y(v)} x2={W - padR} y2={y(v)} stroke="rgba(7,29,73,0.08)" strokeWidth="1" />
|
|
<text x={padL - 6} y={y(v) + 3} fontSize="8.5" fill="rgba(7,29,73,0.5)" textAnchor="end">{v}</text>
|
|
</g>
|
|
))}
|
|
{data.map((grp, i) => {
|
|
const x0 = padL + i * gw
|
|
const innerBw = (gw * 0.7) / 3
|
|
return grp.map((v, j) => (
|
|
<rect key={j} x={x0 + gw * 0.15 + j * innerBw} y={y(v)} width={innerBw - 1} height={padT + ch - y(v)} fill={colors[j]} rx="1" />
|
|
))
|
|
})}
|
|
{months.map((m, i) => (
|
|
<text key={m} x={padL + i * gw + gw / 2} y={H - padB + 14} fontSize="8.5" fill="rgba(7,29,73,0.55)" textAnchor="middle">{m}</text>
|
|
))}
|
|
</svg>
|
|
<div style={{ display: 'flex', justifyContent: 'center', gap: 22, marginTop: 8, direction: 'ltr' }}>
|
|
{[['expenses', NAVY], ['profit', GOLD], ['revenue', BLUE]].map(([l, c]) => (
|
|
<span key={l} style={{ display: 'inline-flex', alignItems: 'center', gap: 6, fontSize: 11, color: 'rgba(7,29,73,0.7)' }}>
|
|
<span style={{ width: 12, height: 9, background: c as string, display: 'inline-block', borderRadius: 2 }} />{l}
|
|
</span>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
/* ════════ floating related sidebar (sticky → follows scroll) ════════ */
|
|
function RelatedSidebar({ related, lang }: { related: Post['related']; lang: 'fa' | 'en' }) {
|
|
return (
|
|
<aside style={{ position: 'sticky', top: 88, alignSelf: 'flex-start' }} className="max-lg:!static">
|
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
|
|
{related.map((r) => (
|
|
<Link key={r.id} to={`/posts/${r.id}`} style={{ textDecoration: 'none' }}>
|
|
<article
|
|
style={{
|
|
background: '#fff', borderRadius: 14, overflow: 'hidden', cursor: 'pointer',
|
|
border: '1px solid rgba(205,158,83,0.3)', borderBottom: `3px solid ${GOLD}`,
|
|
display: 'flex', flexDirection: 'column', transition: 'transform 180ms, box-shadow 180ms',
|
|
}}
|
|
onMouseEnter={(e) => { const d = e.currentTarget; d.style.transform = 'translateY(-3px)'; d.style.boxShadow = '0 16px 34px -22px rgba(7,29,73,0.42)' }}
|
|
onMouseLeave={(e) => { const d = e.currentTarget; d.style.transform = 'translateY(0)'; d.style.boxShadow = 'none' }}
|
|
>
|
|
<img src={r.img} alt="" style={{ width: '100%', aspectRatio: '16 / 10', objectFit: 'cover', display: 'block' }} />
|
|
<div style={{ padding: '12px 14px', textAlign: lang === 'fa' ? 'right' : 'left' }}>
|
|
<h4 style={{ fontSize: 13, fontWeight: 800, color: NAVY, lineHeight: 1.6, margin: '0 0 6px' }}>{r[lang].title}</h4>
|
|
<p style={{ fontSize: 11.5, color: 'rgba(7,29,73,0.55)', lineHeight: 1.7, margin: '0 0 12px' }}>{r[lang].excerpt}</p>
|
|
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
|
<span style={{ width: 28, height: 28, borderRadius: '50%', background: 'rgba(205,158,83,0.15)', display: 'inline-flex', alignItems: 'center', justifyContent: 'center' }}>
|
|
<ArrowLeft size={14} color={GOLD} strokeWidth={2.5} className="no-rtl-flip" />
|
|
</span>
|
|
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 5, fontSize: 10.5, color: 'rgba(7,29,73,0.5)', fontWeight: 600 }}>
|
|
<Calendar size={11} color={GOLD} />{r[lang].date}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</article>
|
|
</Link>
|
|
))}
|
|
</div>
|
|
</aside>
|
|
)
|
|
}
|
|
|
|
/* ════════ a single comment ════════ */
|
|
function CommentCard({ c, lang }: { c: Post['comments'][number]; lang: 'fa' | 'en' }) {
|
|
const isFa = lang === 'fa'
|
|
return (
|
|
<div style={{ background: '#fff', borderRadius: 14, padding: 18, border: '1px solid rgba(7,29,73,0.08)' }}>
|
|
<div style={{ display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between', gap: 12 }}>
|
|
{/* identity */}
|
|
<div style={{ display: 'flex', gap: 12, minWidth: 0 }}>
|
|
<div style={{ flexShrink: 0, width: 46, height: 46, borderRadius: '50%', background: NAVY, color: GOLD, display: 'flex', alignItems: 'center', justifyContent: 'center', fontWeight: 800, fontSize: 17 }}>
|
|
{c.name.trim().charAt(0)}
|
|
</div>
|
|
<div style={{ minWidth: 0 }}>
|
|
<div style={{ fontSize: 14, fontWeight: 800, color: NAVY }}>{c.name}</div>
|
|
<div style={{ fontSize: 11.5, color: 'rgba(7,29,73,0.55)' }}>{isFa ? c.roleFa : c.roleEn}</div>
|
|
</div>
|
|
</div>
|
|
{/* actions */}
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: 12, direction: 'ltr', flexShrink: 0 }}>
|
|
<MoreHorizontal size={16} color="rgba(7,29,73,0.4)" />
|
|
<Reply size={15} color="rgba(7,29,73,0.4)" />
|
|
<Heart size={15} color="rgba(7,29,73,0.4)" />
|
|
<span style={{ fontSize: 11, color: 'rgba(7,29,73,0.45)', whiteSpace: 'nowrap' }}>{c.date}</span>
|
|
</div>
|
|
</div>
|
|
{/* tag pills */}
|
|
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 8, margin: '12px 0' }}>
|
|
{c.tags.map((tg, i) => (
|
|
<span key={i} style={{ display: 'inline-flex', alignItems: 'center', gap: 6, fontSize: 11, fontWeight: 700, color: NAVY, background: 'rgba(205,158,83,0.15)', borderRadius: 999, padding: '4px 12px' }}>
|
|
<span style={{ width: 5, height: 5, borderRadius: '50%', background: GOLD }} />
|
|
{isFa ? tg.fa : tg.en}
|
|
</span>
|
|
))}
|
|
</div>
|
|
<p style={{ fontSize: 13.5, lineHeight: 2, color: 'rgba(7,29,73,0.75)', margin: 0, textAlign: 'justify' }}>{c[lang]}</p>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
/* ════════ membership gate (shown to logged-out readers) ════════ */
|
|
function MembershipGate({ lang, onPreviewLogin }: { lang: 'fa' | 'en'; onPreviewLogin: () => void }) {
|
|
const isFa = lang === 'fa'
|
|
const copy = isFa
|
|
? 'تحلیلهای راهبردی، آمارهای تخصصی و گزارشهای اختصاصی اندیشکده فولاد آینده تنها در اختیار اعضای شبکه قرار میگیرد. برای دسترسی به این محتوا، وارد حساب کاربری خود شوید یا به جمع اعضای اندیشکده بپیوندید.'
|
|
: 'At the Future Steel Institute we craft our strategic outputs for members of our expert network. Membership unlocks full reports, foresight scenarios and high-level analysis — and a seat in the conversation with industry pioneers.'
|
|
return (
|
|
<div
|
|
style={{
|
|
position: 'relative', background: '#fff', borderRadius: 18, padding: 'clamp(20px, 3vw, 34px)',
|
|
boxShadow: '0 30px 70px -30px rgba(7,29,73,0.35)', border: '1px solid rgba(7,29,73,0.06)',
|
|
maxWidth: 720, margin: '0 auto', direction: isFa ? 'rtl' : 'ltr',
|
|
}}
|
|
>
|
|
<p style={{ fontSize: 14.5, lineHeight: 2.1, color: 'rgba(7,29,73,0.78)', margin: '0 0 22px', textAlign: 'justify' }}>{copy}</p>
|
|
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 16, flexWrap: 'wrap' }}>
|
|
{/* socials */}
|
|
<div style={{ display: 'flex', gap: 10, direction: 'ltr' }}>
|
|
{[Send, Share2, Globe, Mail].map((Icon, i) => (
|
|
<a key={i} href="#" aria-label="social" style={{ width: 36, height: 36, borderRadius: '50%', background: NAVY, color: GOLD, display: 'inline-flex', alignItems: 'center', justifyContent: 'center' }}>
|
|
<Icon size={16} />
|
|
</a>
|
|
))}
|
|
</div>
|
|
{/* actions */}
|
|
<div style={{ display: 'flex', gap: 12, flexWrap: 'wrap' }}>
|
|
<button onClick={onPreviewLogin} style={{
|
|
display: 'inline-flex', alignItems: 'center', gap: 8, direction: 'ltr',
|
|
background: 'transparent', color: NAVY, border: `1.5px solid ${NAVY}`, borderRadius: 10,
|
|
padding: '12px 26px', fontFamily: 'inherit', fontSize: 14, fontWeight: 800, cursor: 'pointer',
|
|
}}>
|
|
<LogIn size={16} strokeWidth={2.5} />
|
|
<span>{isFa ? 'ورود' : 'Log in'}</span>
|
|
</button>
|
|
<Link to="/register" style={{
|
|
display: 'inline-flex', alignItems: 'center', gap: 8, direction: 'ltr',
|
|
background: GOLD, color: NAVY, borderRadius: 10, padding: '12px 26px',
|
|
fontSize: 14, fontWeight: 800, textDecoration: 'none',
|
|
}}>
|
|
<UserPlus size={16} strokeWidth={2.5} />
|
|
<span>{isFa ? 'ثبت نام' : 'Sign up'}</span>
|
|
</Link>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export default function PostDetail() {
|
|
const { lang } = useLang()
|
|
const { id } = useParams<{ id: string }>()
|
|
const post = getPost(id || 'p1')!
|
|
const t = post[lang]
|
|
const isFa = lang === 'fa'
|
|
const { member } = useAuth()
|
|
const navigate = useNavigate()
|
|
const loggedIn = member !== null
|
|
|
|
return (
|
|
<div dir={isFa ? 'rtl' : 'ltr'} style={{ background: '#fff', minHeight: '80vh' }}>
|
|
|
|
{/* ── COVER — full-screen, full-bleed (rounded mask corners) ── */}
|
|
<div
|
|
style={{
|
|
position: 'relative',
|
|
width: '100vw',
|
|
marginInline: 'calc(50% - 50vw)',
|
|
height: 'clamp(420px, 92vh, 1000px)',
|
|
borderRadius: 40,
|
|
overflow: 'hidden',
|
|
}}
|
|
className="max-md:!rounded-3xl"
|
|
>
|
|
<img
|
|
src={post.cover}
|
|
alt={t.title}
|
|
style={{ width: '100%', height: '100%', objectFit: 'cover', display: 'block' }}
|
|
/>
|
|
</div>
|
|
|
|
<div className="max-w-7xl mx-auto px-12 max-md:px-5" style={{ paddingTop: 28, paddingBottom: 72 }}>
|
|
|
|
{/* ── BODY GRID: article (main) + floating related ── */}
|
|
<div style={{ display: 'grid', gridTemplateColumns: '1fr 300px', gap: 40 }} className="max-lg:!grid-cols-1 max-lg:!gap-8">
|
|
|
|
{/* MAIN */}
|
|
<article style={{ minWidth: 0 }}>
|
|
{/* date badge */}
|
|
<div style={{ display: 'inline-flex', alignItems: 'center', gap: 6, fontSize: 12, color: 'rgba(7,29,73,0.55)', fontWeight: 600, marginBottom: 12 }}>
|
|
<Calendar size={14} color={GOLD} /> {t.date}
|
|
</div>
|
|
{/* title */}
|
|
<h1 style={{ fontSize: 'clamp(22px, 2.6vw, 34px)', fontWeight: 900, color: NAVY, lineHeight: 1.5, letterSpacing: '-0.5px', margin: '0 0 12px' }}>
|
|
{t.title}
|
|
</h1>
|
|
{/* category */}
|
|
<div style={{ display: 'inline-flex', alignItems: 'center', gap: 7, fontSize: 13, fontWeight: 700, color: GOLD, marginBottom: 26 }}>
|
|
<Tag size={15} /> {post.category[lang]}
|
|
</div>
|
|
|
|
{/* body — full when logged in, gated preview (blurred + gate) when logged out */}
|
|
{loggedIn ? (
|
|
<>
|
|
{/* body blocks — consecutive paragraphs grouped so they reveal sequentially */}
|
|
{(() => {
|
|
const out: React.ReactNode[] = []
|
|
let paraRun: string[] = []
|
|
const flush = (key: string) => {
|
|
if (!paraRun.length) return
|
|
out.push(
|
|
<MagicTextGroup
|
|
key={key}
|
|
paragraphs={paraRun}
|
|
dir={isFa ? 'rtl' : 'ltr'}
|
|
style={{ fontSize: 15.5, lineHeight: 2.2, color: NAVY, margin: '0 0 18px', fontWeight: 500 }}
|
|
/>,
|
|
)
|
|
paraRun = []
|
|
}
|
|
post.body.forEach((b, i) => {
|
|
if (b.type === 'p') { paraRun.push(b[lang]); return }
|
|
flush(`g${i}`)
|
|
if (b.type === 'h') {
|
|
out.push(<h2 key={i} style={{ fontSize: 'clamp(17px,1.9vw,22px)', fontWeight: 900, color: NAVY, margin: '30px 0 14px', lineHeight: 1.5 }}>{b[lang]}</h2>)
|
|
} else if (b.type === 'chart') {
|
|
out.push(
|
|
<motion.div key={i} initial={{ opacity: 0, y: 18 }} whileInView={{ opacity: 1, y: 0 }} viewport={{ once: true, amount: 0.15 }} transition={{ duration: 0.5, ease: EASE }} style={{ margin: '26px 0' }}>
|
|
{b.chart === 'pareto' ? <ParetoChart /> : <BarsChart />}
|
|
</motion.div>,
|
|
)
|
|
} else if (b.type === 'figure') {
|
|
out.push(
|
|
<figure key={i} style={{ margin: '26px 0' }}>
|
|
<div style={{ borderRadius: 14, overflow: 'hidden', border: '1px solid rgba(205,158,83,0.3)' }}><img src={b.img} alt={b[lang]} style={{ width: '100%', display: 'block' }} /></div>
|
|
<figcaption style={{ fontSize: 12.5, color: 'rgba(7,29,73,0.6)', marginTop: 10, textAlign: 'center', fontWeight: 600 }}>{b[lang]}</figcaption>
|
|
</figure>,
|
|
)
|
|
}
|
|
})
|
|
flush('g-end')
|
|
return out
|
|
})()}
|
|
|
|
{/* ── COMMENTS (دیدگاه) ── */}
|
|
<section style={{ marginTop: 46 }}>
|
|
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 24, gap: 12, flexWrap: 'wrap' }}>
|
|
<div style={{ display: 'inline-flex', alignItems: 'center', gap: 8 }}>
|
|
<h2 style={{ fontSize: 20, fontWeight: 900, color: NAVY, margin: 0 }}>{isFa ? 'دیدگاه' : 'Comments'}</h2>
|
|
<MessageCircle size={20} color={GOLD} />
|
|
</div>
|
|
<button style={{ display: 'inline-flex', alignItems: 'center', gap: 7, background: 'transparent', color: NAVY, border: `1.5px solid ${GOLD}`, borderRadius: 999, padding: '8px 18px', fontFamily: 'inherit', fontSize: 13, fontWeight: 800, cursor: 'pointer' }}>
|
|
<Plus size={15} strokeWidth={2.5} color={GOLD} />
|
|
{isFa ? 'ثبت دیدگاه' : 'Add comment'}
|
|
</button>
|
|
</div>
|
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
|
|
{post.comments.map((c) => <CommentCard key={c.id} c={c} lang={lang} />)}
|
|
</div>
|
|
</section>
|
|
</>
|
|
) : (
|
|
(() => {
|
|
const paras: string[] = []
|
|
post.body.forEach((b) => { if (b.type === 'p') paras.push(b[lang]) })
|
|
const teaser = paras.join(' ')
|
|
return (
|
|
<>
|
|
{/* logged-out: at most 5 lines, faded out at the bottom, then the gate */}
|
|
{teaser && (
|
|
<p style={{
|
|
fontSize: 15.5, lineHeight: 2.2, color: NAVY, margin: '0 0 12px', textAlign: 'justify', fontWeight: 500,
|
|
display: '-webkit-box', WebkitLineClamp: 5, WebkitBoxOrient: 'vertical', overflow: 'hidden',
|
|
WebkitMaskImage: 'linear-gradient(to bottom, #000 56%, transparent 100%)',
|
|
maskImage: 'linear-gradient(to bottom, #000 56%, transparent 100%)',
|
|
}}>{teaser}</p>
|
|
)}
|
|
<MembershipGate lang={lang} onPreviewLogin={() => navigate('/login')} />
|
|
</>
|
|
)
|
|
})()
|
|
)}
|
|
</article>
|
|
|
|
{/* FLOATING RELATED */}
|
|
<RelatedSidebar related={post.related} lang={lang} />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|