Add articles list and detail screens

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>
This commit is contained in:
2026-04-19 18:48:08 +05:00
parent 31b861e452
commit de96895e9c
5 changed files with 358 additions and 12 deletions
+234
View File
@@ -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>
);
}
+9 -8
View File
@@ -118,14 +118,15 @@ export function HomeCardsScreen({ nav }) {
<SectionHeader title="Статьи врачей" action="Все" onAction={() => nav.push('articles')} />
<div style={{ display: 'flex', gap: 12, overflowX: 'auto', padding: '0 20px 8px' }} className="noscroll">
{articles.map((a, i) => (
<button key={i} className="press card" style={{
<button key={a.id} onClick={() => nav.push('article:' + a.id)} className="press card" style={{
flexShrink: 0, width: 220, padding: 0, overflow: 'hidden', textAlign: 'left',
}}>
<div style={{
height: 90, background: ['#F5EDDF','#E3F4F2','#FDF8E6','#FCF1F0'][i % 4],
display: 'flex', alignItems: 'flex-end', padding: 12,
height: 90, background: a.hero || ['#F5EDDF','#E3F4F2','#FDF8E6','#FCF1F0'][i % 4],
display: 'flex', alignItems: 'flex-end', justifyContent: 'space-between', padding: 12,
}}>
<span className="chip chip-warm" style={{ background: 'rgba(255,255,255,0.85)' }}>{a.tag}</span>
<span style={{ fontSize: 28, lineHeight: 1 }}>{a.emoji}</span>
</div>
<div style={{ padding: 12 }}>
<div style={{ fontSize: 13, fontWeight: 700, color: 'var(--c-fg-1)', lineHeight: 1.3, marginBottom: 6, minHeight: 34 }}>{a.title}</div>
@@ -273,12 +274,12 @@ export function HomeFeedScreen({ nav }) {
</div>
<div style={{ padding: '18px 20px 8px' }}>
<SectionHeader title="Полезно прочитать" pad="0 0 0 0" />
<SectionHeader title="Полезно прочитать" action="Все" onAction={() => nav.push('articles')} pad="0 0 0 0" />
<div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
{articles.slice(0,3).map((a,i)=>(
<button key={i} className="press card" style={{ display: 'flex', gap: 12, alignItems: 'center', padding: 14, textAlign: 'left' }}>
<div style={{ width: 56, height: 56, borderRadius: 12, background: ['#F5EDDF','#E3F4F2','#FDF8E6'][i%3], flexShrink: 0, display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: 24 }}>
{['📖','👂','🤱'][i%3]}
{articles.slice(0,3).map((a)=>(
<button key={a.id} onClick={() => nav.push('article:' + a.id)} className="press card" style={{ display: 'flex', gap: 12, alignItems: 'center', padding: 14, textAlign: 'left' }}>
<div style={{ width: 56, height: 56, borderRadius: 12, background: a.hero, flexShrink: 0, display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: 28 }}>
{a.emoji}
</div>
<div style={{ flex: 1 }}>
<div style={{ fontSize: 11, color: 'var(--c-primary-dark)', fontWeight: 700, textTransform: 'uppercase', letterSpacing: .5, marginBottom: 3 }}>{a.tag}</div>