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:
@@ -69,6 +69,11 @@ const SCREEN_OPTIONS = [
|
|||||||
{ id: 'telemed', lb: 'Телемед' },
|
{ id: 'telemed', lb: 'Телемед' },
|
||||||
{ id: 'medcard', lb: 'Медкарта' },
|
{ id: 'medcard', lb: 'Медкарта' },
|
||||||
{ id: 'notifications', lb: 'Уведомления' },
|
{ id: 'notifications', lb: 'Уведомления' },
|
||||||
|
{ id: 'articles', lb: 'Статьи врачей' },
|
||||||
|
{ id: 'article:otitis-kids', lb: 'Статья: отит у ребёнка' },
|
||||||
|
{ id: 'article:septoplasty-recovery', lb: 'Статья: восстановление после септопластики' },
|
||||||
|
{ id: 'article:throat-pregnancy', lb: 'Статья: горло при беременности' },
|
||||||
|
{ id: 'article:hearing-check', lb: 'Статья: когда проверить слух' },
|
||||||
];
|
];
|
||||||
|
|
||||||
function applyTheme(tw) {
|
function applyTheme(tw) {
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
ChatTabScreen, ProfileTabScreen, QRScreen,
|
ChatTabScreen, ProfileTabScreen, QRScreen,
|
||||||
TelemedScreen, MedcardScreen, NotificationsScreen,
|
TelemedScreen, MedcardScreen, NotificationsScreen,
|
||||||
} from './screens/screens-misc.jsx';
|
} from './screens/screens-misc.jsx';
|
||||||
|
import { ArticlesScreen, ArticleDetailScreen } from './screens/screens-articles.jsx';
|
||||||
|
|
||||||
function renderScreen(screenId, nav, ctx) {
|
function renderScreen(screenId, nav, ctx) {
|
||||||
const parts = screenId.split(':');
|
const parts = screenId.split(':');
|
||||||
@@ -40,6 +41,8 @@ function renderScreen(screenId, nav, ctx) {
|
|||||||
case 'telemed': return <TelemedScreen nav={nav} />;
|
case 'telemed': return <TelemedScreen nav={nav} />;
|
||||||
case 'medcard': return <MedcardScreen nav={nav} />;
|
case 'medcard': return <MedcardScreen nav={nav} />;
|
||||||
case 'notifications': return <NotificationsScreen nav={nav} />;
|
case 'notifications': return <NotificationsScreen nav={nav} />;
|
||||||
|
case 'articles': return <ArticlesScreen nav={nav} />;
|
||||||
|
case 'article': return <ArticleDetailScreen nav={nav} articleId={parts[1]} />;
|
||||||
default: return <div style={{padding: 40, textAlign: 'center'}}>Экран не найден: {screenId}</div>;
|
default: return <div style={{padding: 40, textAlign: 'center'}}>Экран не найден: {screenId}</div>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+107
-4
@@ -60,10 +60,113 @@ export const CLINIC_DATA = {
|
|||||||
{ id: 'r5', name: 'Посев на микрофлору', date: '20 апр 2026',doctor: 'syndaev', status: 'pending', kind: 'lab' },
|
{ id: 'r5', name: 'Посев на микрофлору', date: '20 апр 2026',doctor: 'syndaev', status: 'pending', kind: 'lab' },
|
||||||
],
|
],
|
||||||
articles: [
|
articles: [
|
||||||
{ tag: 'Дети', title: 'Как понять, что у ребёнка отит', mins: 4, author: 'Макарова Л.Г.' },
|
{
|
||||||
{ tag: 'Операции', title: 'Восстановление после септопластики', mins: 6, author: 'Синдяев А.В.' },
|
id: 'otitis-kids',
|
||||||
{ tag: 'Беременность', title: 'Безопасно лечим горло при беременности', mins: 5, author: 'Лобанова И.Ю.' },
|
tag: 'Дети',
|
||||||
{ tag: 'Слух', title: 'Когда пора проверить слух', mins: 3, author: 'Семерикова Н.А.' },
|
title: 'Как понять, что у ребёнка отит',
|
||||||
|
mins: 4,
|
||||||
|
author: 'Макарова Л.Г.',
|
||||||
|
doctorId: 'makarova',
|
||||||
|
hero: '#F5EDDF',
|
||||||
|
emoji: '👶',
|
||||||
|
date: '12 апр 2026',
|
||||||
|
lede: 'Ребёнок капризничает и трёт ушко? Рассказываем, когда это повод срочно показаться врачу, а когда можно подождать.',
|
||||||
|
body: [
|
||||||
|
{ type: 'p', text: 'Отит — одна из самых частых причин обращения к педиатру у детей до 5 лет. Пик заболеваемости приходится на осенне-зимний сезон: ОРВИ даёт осложнение на ухо, и слизистая слуховой трубы воспаляется следом за носоглоткой.' },
|
||||||
|
{ type: 'h', text: 'На что обращать внимание' },
|
||||||
|
{ type: 'ul', items: [
|
||||||
|
'Ребёнок дёргает или трёт ушко',
|
||||||
|
'Беспокойный сон, плач при кормлении',
|
||||||
|
'Температура выше 38°C без видимой причины',
|
||||||
|
'Снижение реакции на тихие звуки, просит повторить',
|
||||||
|
'Выделения из слухового прохода',
|
||||||
|
] },
|
||||||
|
{ type: 'h', text: 'Когда нужен врач в тот же день' },
|
||||||
|
{ type: 'p', text: 'Если ребёнку меньше 2 лет, боль длится более суток, температура выше 38,5°C или появились гнойные выделения — не откладывайте визит. При двустороннем остром отите антибиотик нужен почти всегда.' },
|
||||||
|
{ type: 'callout', tone: 'danger', title: 'Чего не делать до осмотра', text: 'Не закапывайте борный спирт и масла, не грейте ухо компрессами, не давайте анальгин. Это может навредить и смазать картину для врача.' },
|
||||||
|
{ type: 'p', text: 'Если есть сомнения — позвоните в клинику или напишите в чат. Детский ЛОР поможет оценить ситуацию и подскажет, нужно ли везти ребёнка на приём сегодня.' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'septoplasty-recovery',
|
||||||
|
tag: 'Операции',
|
||||||
|
title: 'Восстановление после септопластики',
|
||||||
|
mins: 6,
|
||||||
|
author: 'Синдяев А.В.',
|
||||||
|
doctorId: 'syndaev',
|
||||||
|
hero: '#E3F4F2',
|
||||||
|
emoji: '🩺',
|
||||||
|
date: '8 апр 2026',
|
||||||
|
lede: 'Что происходит после операции по исправлению носовой перегородки и как ускорить восстановление.',
|
||||||
|
body: [
|
||||||
|
{ type: 'p', text: 'Септопластика — операция короткая, но восстановление идёт постепенно. Большая часть отёка уходит за первые 2 недели, окончательная форма носа формируется к 3-му месяцу.' },
|
||||||
|
{ type: 'h', text: 'Первая неделя' },
|
||||||
|
{ type: 'p', text: 'Сразу после операции в нос устанавливают силиконовые сплинты или тампоны — дышать приходится ртом. Это временно: сплинты снимают на 3–5 день. В этот период важен постельный режим, холод на переносицу первые сутки, обезболивание по схеме.' },
|
||||||
|
{ type: 'h', text: 'Со 2-й недели — промывания' },
|
||||||
|
{ type: 'ul', items: [
|
||||||
|
'Солевой раствор 4–6 раз в день',
|
||||||
|
'Мягкое удаление корочек у ЛОР-врача',
|
||||||
|
'Контрольная эндоскопия на 10–14 день',
|
||||||
|
] },
|
||||||
|
{ type: 'callout', tone: 'warn', title: 'Ограничения на месяц', text: 'Нельзя: баня, сауна, бассейн, спорт, авиаперелёты, алкоголь, острая и горячая пища. Сморкаться — только поочерёдно каждой ноздрёй, без усилия.' },
|
||||||
|
{ type: 'h', text: 'Когда срочно к врачу' },
|
||||||
|
{ type: 'p', text: 'Сильная боль, не снимающаяся анальгетиками, температура выше 38,5°C, обильное кровотечение, резкое нарастание отёка с покраснением кожи — повод связаться с хирургом в тот же день. В приложении есть чат с вашим врачом.' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'throat-pregnancy',
|
||||||
|
tag: 'Беременность',
|
||||||
|
title: 'Безопасно лечим горло при беременности',
|
||||||
|
mins: 5,
|
||||||
|
author: 'Лобанова И.Ю.',
|
||||||
|
doctorId: 'lobanova',
|
||||||
|
hero: '#FDF8E6',
|
||||||
|
emoji: '🤱',
|
||||||
|
date: '2 апр 2026',
|
||||||
|
lede: 'Большинство привычных препаратов при беременности запрещены. Разбираем, что точно безопасно, а что — только по назначению врача.',
|
||||||
|
body: [
|
||||||
|
{ type: 'p', text: 'При беременности иммунитет естественно снижается, и горло болит чаще обычного. Главная задача — не навредить малышу лечением: многие антисептики и анальгетики проникают через плаценту.' },
|
||||||
|
{ type: 'h', text: 'Безопасно на любом сроке' },
|
||||||
|
{ type: 'ul', items: [
|
||||||
|
'Полоскание солевым раствором (1 ч. л. соли на стакан тёплой воды)',
|
||||||
|
'Полоскание отваром ромашки',
|
||||||
|
'Обильное тёплое питьё, мёд (если нет аллергии)',
|
||||||
|
'Ингаляции физраствором через небулайзер',
|
||||||
|
] },
|
||||||
|
{ type: 'h', text: 'Можно по назначению врача' },
|
||||||
|
{ type: 'p', text: 'Пастилки Лизобакт и Фарингосепт, спрей Тантум Верде (со 2-го триместра), Мирамистин — применяют после оценки пользы и рисков. Антибиотики — только по строгим показаниям и только группы пенициллинов или цефалоспоринов.' },
|
||||||
|
{ type: 'callout', tone: 'danger', title: 'Категорически нельзя', text: 'Спиртсодержащие антисептики, аспирин, большинство леденцов с ментолом, прогревания, горчичники. Народная «смазка горла люголем» — только после осмотра врача.' },
|
||||||
|
{ type: 'h', text: 'Когда срочно к ЛОРу' },
|
||||||
|
{ type: 'p', text: 'Температура выше 38°C более 2 дней, гнойные налёты на миндалинах, трудно открывать рот, асимметрия горла или сильная боль с одной стороны — немедленно на приём. Возможен паратонзиллярный абсцесс, который требует вскрытия.' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'hearing-check',
|
||||||
|
tag: 'Слух',
|
||||||
|
title: 'Когда пора проверить слух',
|
||||||
|
mins: 3,
|
||||||
|
author: 'Семерикова Н.А.',
|
||||||
|
doctorId: 'semerikova',
|
||||||
|
hero: '#FCF1F0',
|
||||||
|
emoji: '👂',
|
||||||
|
date: '28 мар 2026',
|
||||||
|
lede: 'Снижение слуха часто развивается медленно, и человек замечает проблему последним. На что обратить внимание, чтобы не упустить момент.',
|
||||||
|
body: [
|
||||||
|
{ type: 'p', text: 'В отличие от зрения, слух редко падает резко — исключение только травма или острая сенсоневральная тугоухость, когда помощь нужна в первые 72 часа. Обычно снижение копится годами и становится заметным, когда теряется уже 20–30%.' },
|
||||||
|
{ type: 'h', text: 'Бытовые признаки' },
|
||||||
|
{ type: 'ul', items: [
|
||||||
|
'Часто просите повторить сказанное',
|
||||||
|
'Увеличиваете громкость ТВ больше, чем раньше',
|
||||||
|
'Плохо разбираете речь в шумном кафе',
|
||||||
|
'Появился звон или шум в ушах',
|
||||||
|
'Близкие говорят, что вы «кричите», когда говорите нормально',
|
||||||
|
] },
|
||||||
|
{ type: 'h', text: 'Группы риска' },
|
||||||
|
{ type: 'p', text: 'Люди после 50, музыканты, рабочие шумных производств, любители наушников на высокой громкости, перенёсшие черепно-мозговую травму или ототоксичные препараты (некоторые антибиотики и химиотерапия).' },
|
||||||
|
{ type: 'callout', tone: 'info', title: 'Быстрая проверка', text: 'В приложении есть тест слуха на 3 минуты — покажет ориентир. Если результат пограничный или хуже нормы — запишитесь на аудиометрию у сурдолога для точного диагноза.' },
|
||||||
|
{ type: 'p', text: 'Ранняя коррекция (слуховой аппарат или лечение причины) останавливает дальнейшее снижение и сохраняет речевую зону коры мозга активной — это критично для качества жизни в пожилом возрасте.' },
|
||||||
|
],
|
||||||
|
},
|
||||||
],
|
],
|
||||||
recovery: {
|
recovery: {
|
||||||
op: 'Септопластика',
|
op: 'Септопластика',
|
||||||
|
|||||||
@@ -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')} />
|
<SectionHeader title="Статьи врачей" action="Все" onAction={() => nav.push('articles')} />
|
||||||
<div style={{ display: 'flex', gap: 12, overflowX: 'auto', padding: '0 20px 8px' }} className="noscroll">
|
<div style={{ display: 'flex', gap: 12, overflowX: 'auto', padding: '0 20px 8px' }} className="noscroll">
|
||||||
{articles.map((a, i) => (
|
{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',
|
flexShrink: 0, width: 220, padding: 0, overflow: 'hidden', textAlign: 'left',
|
||||||
}}>
|
}}>
|
||||||
<div style={{
|
<div style={{
|
||||||
height: 90, background: ['#F5EDDF','#E3F4F2','#FDF8E6','#FCF1F0'][i % 4],
|
height: 90, background: a.hero || ['#F5EDDF','#E3F4F2','#FDF8E6','#FCF1F0'][i % 4],
|
||||||
display: 'flex', alignItems: 'flex-end', padding: 12,
|
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 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>
|
||||||
<div style={{ padding: 12 }}>
|
<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>
|
<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>
|
||||||
|
|
||||||
<div style={{ padding: '18px 20px 8px' }}>
|
<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 }}>
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
|
||||||
{articles.slice(0,3).map((a,i)=>(
|
{articles.slice(0,3).map((a)=>(
|
||||||
<button key={i} className="press card" style={{ display: 'flex', gap: 12, alignItems: 'center', padding: 14, textAlign: 'left' }}>
|
<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: ['#F5EDDF','#E3F4F2','#FDF8E6'][i%3], flexShrink: 0, display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: 24 }}>
|
<div style={{ width: 56, height: 56, borderRadius: 12, background: a.hero, flexShrink: 0, display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: 28 }}>
|
||||||
{['📖','👂','🤱'][i%3]}
|
{a.emoji}
|
||||||
</div>
|
</div>
|
||||||
<div style={{ flex: 1 }}>
|
<div style={{ flex: 1 }}>
|
||||||
<div style={{ fontSize: 11, color: 'var(--c-primary-dark)', fontWeight: 700, textTransform: 'uppercase', letterSpacing: .5, marginBottom: 3 }}>{a.tag}</div>
|
<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