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:
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user