Browse Source
Enriched the 4 articles in data.js with id, doctorId, hero color, emoji, date, lede, and structured body blocks (paragraphs, headings, bullet lists, tone-variant callouts). New screens: - ArticlesScreen — tag-filtered list with hero-card layout - ArticleDetailScreen — hero block, meta row, lede, body renderer (p/h/ul/callout), author card linking to doctor profile, CTA to book the author, and "Related" footer Home card and feed sections now link to detail and list screens. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>main
5 changed files with 358 additions and 12 deletions
@ -0,0 +1,234 @@
|
||||
import React, { useState } from 'react'; |
||||
import { I } from '../icons.jsx'; |
||||
import { CLINIC_DATA } from '../data.js'; |
||||
import { Avatar, ScreenHeader } from '../components.jsx'; |
||||
|
||||
function ArticleHero({ article, big = false }) { |
||||
return ( |
||||
<div style={{ |
||||
background: article.hero, |
||||
height: big ? 180 : 100, |
||||
borderRadius: big ? 0 : 14, |
||||
padding: big ? '18px 20px' : 14, |
||||
display: 'flex', |
||||
flexDirection: 'column', |
||||
justifyContent: 'space-between', |
||||
position: 'relative', |
||||
overflow: 'hidden', |
||||
}}> |
||||
<span className="chip chip-warm" style={{ alignSelf: 'flex-start', background: 'rgba(255,255,255,0.85)' }}>{article.tag}</span> |
||||
<div style={{ |
||||
position: 'absolute', |
||||
right: big ? 18 : 12, |
||||
bottom: big ? 14 : 8, |
||||
fontSize: big ? 72 : 36, |
||||
opacity: 0.9, |
||||
lineHeight: 1, |
||||
}}>{article.emoji}</div> |
||||
</div> |
||||
); |
||||
} |
||||
|
||||
function renderBlock(b, i) { |
||||
if (b.type === 'p') { |
||||
return <p key={i} style={{ margin: '0 0 14px', fontSize: 15, lineHeight: 1.6, color: 'var(--c-fg-2)' }}>{b.text}</p>; |
||||
} |
||||
if (b.type === 'h') { |
||||
return <h3 key={i} style={{ margin: '22px 0 10px', fontSize: 17, fontWeight: 700, color: 'var(--c-fg-1)', lineHeight: 1.3 }}>{b.text}</h3>; |
||||
} |
||||
if (b.type === 'ul') { |
||||
return ( |
||||
<ul key={i} style={{ margin: '0 0 16px', paddingLeft: 0, listStyle: 'none' }}> |
||||
{b.items.map((it, j) => ( |
||||
<li key={j} style={{ display: 'flex', gap: 10, alignItems: 'flex-start', padding: '4px 0', fontSize: 15, lineHeight: 1.5, color: 'var(--c-fg-2)' }}> |
||||
<span style={{ width: 6, height: 6, borderRadius: 999, background: 'var(--c-primary-darker)', flexShrink: 0, marginTop: 9 }} /> |
||||
<span>{it}</span> |
||||
</li> |
||||
))} |
||||
</ul> |
||||
); |
||||
} |
||||
if (b.type === 'callout') { |
||||
const tones = { |
||||
danger: { bg: 'var(--c-accent-50)', border: 'var(--c-accent)', icon: I.shield, color: 'var(--c-accent)' }, |
||||
info: { bg: 'var(--c-primary-50)', border: 'var(--c-primary-darker)', icon: I.stetho, color: 'var(--c-primary-darker)' }, |
||||
warn: { bg: 'var(--c-warning-50)', border: 'var(--c-warning)', icon: I.bell, color: 'var(--c-warning)' }, |
||||
}; |
||||
const t = tones[b.tone] || tones.info; |
||||
const IconC = t.icon; |
||||
return ( |
||||
<div key={i} style={{ |
||||
background: t.bg, borderLeft: `3px solid ${t.border}`, borderRadius: 12, |
||||
padding: 14, margin: '6px 0 18px', display: 'flex', gap: 12, alignItems: 'flex-start', |
||||
}}> |
||||
<IconC size={20} style={{ color: t.color, flexShrink: 0, marginTop: 1 }} /> |
||||
<div> |
||||
{b.title && <div style={{ fontSize: 14, fontWeight: 700, color: 'var(--c-fg-1)', marginBottom: 4 }}>{b.title}</div>} |
||||
<div style={{ fontSize: 14, color: 'var(--c-fg-2)', lineHeight: 1.5 }}>{b.text}</div> |
||||
</div> |
||||
</div> |
||||
); |
||||
} |
||||
return null; |
||||
} |
||||
|
||||
export function ArticlesScreen({ nav }) { |
||||
const { articles } = CLINIC_DATA; |
||||
const tags = ['Все', ...Array.from(new Set(articles.map(a => a.tag)))]; |
||||
const [tag, setTag] = useState('Все'); |
||||
const filtered = tag === 'Все' ? articles : articles.filter(a => a.tag === tag); |
||||
return ( |
||||
<div style={{ paddingBottom: 40 }}> |
||||
<ScreenHeader title="Статьи врачей" onBack={() => nav.pop()} rightIcon={I.search} /> |
||||
<div className="pills" style={{ marginBottom: 14 }}> |
||||
{tags.map(t => ( |
||||
<button key={t} onClick={() => setTag(t)} className={'pill' + (tag === t ? ' on' : '')}>{t}</button> |
||||
))} |
||||
</div> |
||||
<div style={{ padding: '0 16px', display: 'flex', flexDirection: 'column', gap: 12 }}> |
||||
{filtered.map(a => ( |
||||
<button key={a.id} onClick={() => nav.push('article:' + a.id)} className="press card" style={{ |
||||
padding: 0, overflow: 'hidden', textAlign: 'left', |
||||
}}> |
||||
<ArticleHero article={a} /> |
||||
<div style={{ padding: 14 }}> |
||||
<div style={{ fontSize: 15, fontWeight: 700, color: 'var(--c-fg-1)', lineHeight: 1.3, marginBottom: 6 }}>{a.title}</div> |
||||
<div className="sub" style={{ fontSize: 12, marginBottom: 8 }}>{a.lede}</div> |
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, fontSize: 12, color: 'var(--c-fg-3)' }}> |
||||
<span style={{ fontWeight: 700 }}>{a.author}</span> |
||||
<span className="dot" /> |
||||
<span>{a.mins} мин чтения</span> |
||||
</div> |
||||
</div> |
||||
</button> |
||||
))} |
||||
</div> |
||||
</div> |
||||
); |
||||
} |
||||
|
||||
export function ArticleDetailScreen({ nav, articleId }) { |
||||
const a = CLINIC_DATA.articles.find(x => x.id === articleId) || CLINIC_DATA.articles[0]; |
||||
const doctor = CLINIC_DATA.doctors.find(d => d.id === a.doctorId); |
||||
const related = CLINIC_DATA.articles.filter(x => x.id !== a.id).slice(0, 2); |
||||
const [bookmarked, setBookmarked] = useState(false); |
||||
|
||||
return ( |
||||
<div style={{ paddingBottom: 40 }}> |
||||
{/* Floating back button over hero */} |
||||
<div style={{ |
||||
position: 'sticky', top: 0, zIndex: 10, |
||||
padding: '12px 16px', display: 'flex', justifyContent: 'space-between', |
||||
}}> |
||||
<button onClick={() => nav.pop()} className="press" style={{ |
||||
width: 38, height: 38, borderRadius: 999, background: 'rgba(255,255,255,0.92)', |
||||
backdropFilter: 'blur(10px)', |
||||
display: 'flex', alignItems: 'center', justifyContent: 'center', |
||||
boxShadow: 'var(--sh-sm)', |
||||
}}> |
||||
<I.chevL size={20} style={{ color: 'var(--c-fg-1)' }} /> |
||||
</button> |
||||
<button onClick={() => setBookmarked(b => !b)} className="press" style={{ |
||||
width: 38, height: 38, borderRadius: 999, background: 'rgba(255,255,255,0.92)', |
||||
backdropFilter: 'blur(10px)', |
||||
display: 'flex', alignItems: 'center', justifyContent: 'center', |
||||
boxShadow: 'var(--sh-sm)', |
||||
}}> |
||||
<I.heart size={18} style={{ color: bookmarked ? 'var(--c-accent)' : 'var(--c-fg-1)' }} |
||||
fill={bookmarked ? 'var(--c-accent)' : 'none'} /> |
||||
</button> |
||||
</div> |
||||
|
||||
{/* Hero pulled up to fill under status bar area */} |
||||
<div style={{ marginTop: -60, paddingTop: 60, background: a.hero, position: 'relative' }}> |
||||
<div style={{ |
||||
padding: '0 20px 24px', minHeight: 140, |
||||
display: 'flex', flexDirection: 'column', justifyContent: 'flex-end', |
||||
position: 'relative', |
||||
}}> |
||||
<span className="chip chip-warm" style={{ alignSelf: 'flex-start', background: 'rgba(255,255,255,0.85)', marginBottom: 12 }}>{a.tag}</span> |
||||
<h1 style={{ margin: 0, fontSize: 24, fontWeight: 700, color: 'var(--c-fg-1)', lineHeight: 1.2, maxWidth: '72%' }}>{a.title}</h1> |
||||
<div style={{ |
||||
position: 'absolute', right: 12, bottom: 8, |
||||
fontSize: 82, opacity: 0.85, lineHeight: 1, |
||||
}}>{a.emoji}</div> |
||||
</div> |
||||
</div> |
||||
|
||||
{/* Meta row */} |
||||
<div style={{ padding: '16px 20px 8px', display: 'flex', alignItems: 'center', gap: 12 }}> |
||||
<Avatar init={doctor.init} size={40} /> |
||||
<div style={{ flex: 1 }}> |
||||
<div style={{ fontSize: 14, fontWeight: 700 }}>{doctor.name.split(' ').slice(0, 2).join(' ')}</div> |
||||
<div className="sub" style={{ fontSize: 12 }}>{a.date} · {a.mins} мин чтения</div> |
||||
</div> |
||||
<button className="press" style={{ |
||||
width: 36, height: 36, borderRadius: 999, background: 'var(--c-bg)', |
||||
display: 'flex', alignItems: 'center', justifyContent: 'center', border: '1px solid var(--c-border)', |
||||
}}> |
||||
<I.arrow size={16} style={{ color: 'var(--c-fg-2)' }} /> |
||||
</button> |
||||
</div> |
||||
|
||||
<div className="divider" style={{ margin: '8px 20px' }} /> |
||||
|
||||
{/* Lede */} |
||||
<div style={{ padding: '8px 20px 0' }}> |
||||
<p style={{ margin: '0 0 18px', fontSize: 16, fontWeight: 500, color: 'var(--c-fg-1)', lineHeight: 1.5 }}>{a.lede}</p> |
||||
</div> |
||||
|
||||
{/* Body */} |
||||
<div style={{ padding: '0 20px' }}> |
||||
{a.body.map(renderBlock)} |
||||
</div> |
||||
|
||||
{/* Author card */} |
||||
<div style={{ padding: '16px 20px 6px' }}> |
||||
<div className="card" style={{ |
||||
padding: 16, display: 'flex', gap: 14, alignItems: 'center', |
||||
background: 'linear-gradient(135deg, var(--c-primary-100), var(--c-primary-50))', border: 0, |
||||
}}> |
||||
<Avatar init={doctor.init} size={52} /> |
||||
<div style={{ flex: 1 }}> |
||||
<div className="sub" style={{ fontSize: 11, marginBottom: 2 }}>Автор статьи</div> |
||||
<div style={{ fontSize: 15, fontWeight: 700, marginBottom: 2 }}>{doctor.name.split(' ').slice(0, 2).join(' ')}</div> |
||||
<div className="sub" style={{ fontSize: 12 }}>{doctor.spec} · {doctor.exp} лет</div> |
||||
</div> |
||||
<button onClick={() => nav.push('doctor:' + doctor.id)} className="btn-s" style={{ padding: '8px 14px', fontSize: 13 }}>Профиль</button> |
||||
</div> |
||||
</div> |
||||
|
||||
{/* CTA */} |
||||
<div style={{ padding: '16px 20px 6px' }}> |
||||
<button onClick={() => nav.push('booking-time:' + doctor.id)} className="btn-p block"> |
||||
<I.calendar size={18} /> Записаться к автору |
||||
</button> |
||||
</div> |
||||
|
||||
{/* Related */} |
||||
{related.length > 0 && ( |
||||
<div style={{ padding: '22px 20px 0' }}> |
||||
<h2 className="h-sec" style={{ marginBottom: 12 }}>Ещё по теме</h2> |
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}> |
||||
{related.map(r => ( |
||||
<button key={r.id} onClick={() => { nav.pop(); nav.push('article:' + r.id); }} className="press card" style={{ |
||||
display: 'flex', gap: 12, alignItems: 'center', padding: 12, textAlign: 'left', |
||||
}}> |
||||
<div style={{ |
||||
width: 56, height: 56, borderRadius: 12, background: r.hero, flexShrink: 0, |
||||
display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: 26, |
||||
}}>{r.emoji}</div> |
||||
<div style={{ flex: 1, minWidth: 0 }}> |
||||
<div style={{ fontSize: 11, color: 'var(--c-primary-dark)', fontWeight: 700, textTransform: 'uppercase', letterSpacing: .5, marginBottom: 3 }}>{r.tag}</div> |
||||
<div style={{ fontSize: 14, fontWeight: 700, lineHeight: 1.3, marginBottom: 3 }}>{r.title}</div> |
||||
<div className="sub" style={{ fontSize: 12 }}>{r.mins} мин</div> |
||||
</div> |
||||
<I.chev size={16} style={{ color: 'var(--c-fg-4)' }} /> |
||||
</button> |
||||
))} |
||||
</div> |
||||
</div> |
||||
)} |
||||
</div> |
||||
); |
||||
} |
||||
Loading…
Reference in new issue