Sprint 6: Таймлайн X home variant for chronic patients
Fourth home variant alongside Карточки / Лента / Таймлайн, labeled "Таймлайн X" in Tweaks "Главный экран". Designed for patients with chronic conditions (not post-op). data.js: new `chronic` object — condition, stage, doctor, metrics (days since last flare-up, compliance %, flare-ups this year), daily + scheduled tasks, lifestyle recommendations, and 5-visit observation history. HomeTimelineXScreen sections: - Health status hero with soft gradient primary-100 → warm-100, success-tinted stable-state chip (green dot), three narrow-font metrics - Today's tasks card: daily habits with streak + 🔥, scheduled procedures with dates - Ask-a-question promotion: AI card (teal gradient) + doctor card (with avatar) side by side - Vertical observation timeline: 5 past visits with doctor initial, color-coded by type (diagnosis/procedure/therapy/flareup/checkup) - Upcoming appointment, lifestyle recommendations carousel with emoji, book CTA, article carousel Softened hero from dark teal gradient with white text to light primary-100 → warm-100 gradient with dark text — stable-state signal, not clinical verdict. Following Sprint 5 convention: matching entry in src/docs.js under key `home:timelineX` with title, category, goal, tasks, rationale. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -202,6 +202,242 @@ export function HomeListScreen({ nav }) {
|
||||
);
|
||||
}
|
||||
|
||||
export function HomeTimelineXScreen({ nav }) {
|
||||
const { doctors, appointments, clinic, articles, chronic } = CLINIC_DATA;
|
||||
const upcoming = appointments.find(a => a.status === 'upcoming');
|
||||
const upDoc = upcoming && doctors.find(d => d.id === upcoming.doctor);
|
||||
const myDoctor = doctors.find(d => d.id === chronic.doctorId);
|
||||
|
||||
const typeColors = {
|
||||
diagnosis: { bg: 'var(--c-primary-100)', fg: 'var(--c-primary-darker)' },
|
||||
procedure: { bg: 'var(--c-primary-100)', fg: 'var(--c-primary-darker)' },
|
||||
therapy: { bg: 'var(--c-warm-100)', fg: 'var(--c-warm-text)' },
|
||||
flareup: { bg: 'var(--c-accent-50)', fg: 'var(--c-accent)' },
|
||||
checkup: { bg: 'var(--c-success-50)', fg: 'var(--c-success)' },
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ paddingBottom: 100 }}>
|
||||
{/* Greeting header */}
|
||||
<div style={{ padding: '8px 20px 14px' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
<div>
|
||||
<div className="sub">20 апреля, понедельник</div>
|
||||
<div style={{ fontSize: 22, fontWeight: 700 }}>Здравствуйте, Анна</div>
|
||||
</div>
|
||||
<button onClick={() => nav.push('profile')} className="press">
|
||||
<Avatar init="АС" size={42} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Health status hero */}
|
||||
<div style={{ padding: '0 16px 16px' }}>
|
||||
<div className="card" style={{
|
||||
padding: 18,
|
||||
background: 'linear-gradient(135deg, var(--c-primary-100), var(--c-warm-100))',
|
||||
border: '1px solid var(--c-primary-200)',
|
||||
}}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 8 }}>
|
||||
<span style={{ fontSize: 10, textTransform: 'uppercase', letterSpacing: .8, color: 'var(--c-primary-darker)', fontWeight: 700 }}>Ваше состояние</span>
|
||||
<span style={{
|
||||
padding: '4px 10px', borderRadius: 999,
|
||||
background: 'rgba(255,255,255,0.75)', color: 'var(--c-success)',
|
||||
fontSize: 11, fontWeight: 700, display: 'inline-flex', alignItems: 'center', gap: 5,
|
||||
}}>
|
||||
<span style={{ width: 6, height: 6, borderRadius: 999, background: 'var(--c-success)' }} />
|
||||
{chronic.stage}
|
||||
</span>
|
||||
</div>
|
||||
<div style={{ fontSize: 19, fontWeight: 700, color: 'var(--c-fg-1)', marginBottom: 4 }}>{chronic.condition}</div>
|
||||
<div style={{ fontSize: 12, color: 'var(--c-fg-3)', marginBottom: 16 }}>
|
||||
Наблюдение с {chronic.diagnosed} · {myDoctor.name.split(' ').slice(0, 2).join(' ')}
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: 8, paddingTop: 14, borderTop: '1px solid rgba(255,255,255,0.6)' }}>
|
||||
<div>
|
||||
<div style={{ fontFamily: 'var(--font-narrow)', fontSize: 26, fontWeight: 700, lineHeight: 1, color: 'var(--c-primary-darker)' }}>{chronic.daysSinceLastFlareup}</div>
|
||||
<div style={{ fontSize: 10, color: 'var(--c-fg-3)', marginTop: 3 }}>дней без обострений</div>
|
||||
</div>
|
||||
<div>
|
||||
<div style={{ fontFamily: 'var(--font-narrow)', fontSize: 26, fontWeight: 700, lineHeight: 1, color: 'var(--c-primary-darker)' }}>{chronic.complianceScore}%</div>
|
||||
<div style={{ fontSize: 10, color: 'var(--c-fg-3)', marginTop: 3 }}>комплаенс</div>
|
||||
</div>
|
||||
<div>
|
||||
<div style={{ fontFamily: 'var(--font-narrow)', fontSize: 26, fontWeight: 700, lineHeight: 1, color: 'var(--c-primary-darker)' }}>{chronic.flareupsThisYear}</div>
|
||||
<div style={{ fontSize: 10, color: 'var(--c-fg-3)', marginTop: 3 }}>обострение в году</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Current tasks */}
|
||||
<div style={{ padding: '0 20px 18px' }}>
|
||||
<SectionHeader title="Задачи сегодня" pad="0 0 8px 0" action="История" onAction={() => nav.push('medcard')} />
|
||||
<div className="card" style={{ padding: 0 }}>
|
||||
{chronic.currentTasks.map((t, i, a) => {
|
||||
const isDaily = t.type === 'daily';
|
||||
return (
|
||||
<React.Fragment key={t.id}>
|
||||
<div style={{ display: 'flex', gap: 12, alignItems: 'center', padding: '12px 14px' }}>
|
||||
{isDaily ? (
|
||||
<div style={{
|
||||
width: 28, height: 28, borderRadius: 999, flexShrink: 0,
|
||||
background: t.done ? 'var(--c-primary-darker)' : '#fff',
|
||||
border: t.done ? 0 : '2px solid var(--c-border-strong)',
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
}}>
|
||||
{t.done && <I.check size={16} style={{ color: '#fff' }} sw={3} />}
|
||||
</div>
|
||||
) : (
|
||||
<div style={{
|
||||
width: 28, height: 28, borderRadius: 8, flexShrink: 0,
|
||||
background: 'var(--c-primary-100)',
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
}}>
|
||||
<I.calendar size={14} style={{ color: 'var(--c-primary-darker)' }} />
|
||||
</div>
|
||||
)}
|
||||
<div style={{ flex: 1, minWidth: 0 }}>
|
||||
<div style={{
|
||||
fontSize: 14, fontWeight: 600,
|
||||
color: isDaily && t.done ? 'var(--c-fg-3)' : 'var(--c-fg-1)',
|
||||
textDecoration: isDaily && t.done ? 'line-through' : 'none',
|
||||
}}>{t.text}</div>
|
||||
<div className="sub" style={{ fontSize: 11 }}>
|
||||
{isDaily ? (t.streak > 0 ? `Серия: ${t.streak} дней` : 'Ежедневно') : `До ${t.nextDate}`}
|
||||
</div>
|
||||
</div>
|
||||
{isDaily && t.streak > 0 && <span style={{ fontSize: 18 }}>🔥</span>}
|
||||
{!isDaily && <I.chev size={15} style={{ color: 'var(--c-fg-4)' }} />}
|
||||
</div>
|
||||
{i < a.length - 1 && <div style={{ height: 1, background: 'var(--c-divider)', marginLeft: 54 }} />}
|
||||
</React.Fragment>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Promotion: ask AI or doctor */}
|
||||
<div style={{ padding: '0 20px 18px' }}>
|
||||
<SectionHeader title="Есть вопрос?" pad="0 0 8px 0" />
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 10 }}>
|
||||
<button onClick={() => nav.push('chat:ai')} className="press" style={{
|
||||
padding: 14, borderRadius: 16,
|
||||
background: 'linear-gradient(135deg, var(--c-primary-darker), var(--c-primary-dark))',
|
||||
color: '#fff', textAlign: 'left', display: 'flex', flexDirection: 'column', gap: 6,
|
||||
}}>
|
||||
<div style={{ fontSize: 26 }}>✨</div>
|
||||
<div>
|
||||
<div style={{ fontSize: 13, fontWeight: 700, marginBottom: 2 }}>Спросить помощника</div>
|
||||
<div style={{ fontSize: 11, opacity: .85, lineHeight: 1.4 }}>Быстрый ответ круглосуточно</div>
|
||||
</div>
|
||||
</button>
|
||||
<button onClick={() => nav.push('chat:doctor-syndaev')} className="press card" style={{
|
||||
padding: 14, textAlign: 'left', display: 'flex', flexDirection: 'column', gap: 6,
|
||||
}}>
|
||||
<Avatar init={myDoctor.init} size={32} />
|
||||
<div>
|
||||
<div style={{ fontSize: 13, fontWeight: 700, marginBottom: 2 }}>Написать врачу</div>
|
||||
<div className="sub" style={{ fontSize: 11, lineHeight: 1.4 }}>{myDoctor.name.split(' ').slice(0, 2).join(' ')}</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* History timeline */}
|
||||
<div style={{ padding: '0 20px 18px' }}>
|
||||
<SectionHeader title="История наблюдения" pad="0 0 8px 0" action="Вся карта" onAction={() => nav.push('medcard')} />
|
||||
<div className="card" style={{ padding: '8px 0' }}>
|
||||
{chronic.pastVisits.map((v, i, a) => {
|
||||
const d = doctors.find(x => x.id === v.doctorId);
|
||||
const c = typeColors[v.type] || typeColors.checkup;
|
||||
const isLast = i === a.length - 1;
|
||||
return (
|
||||
<div key={v.id} style={{ display: 'flex', gap: 14, padding: '10px 14px', alignItems: 'flex-start', position: 'relative' }}>
|
||||
{!isLast && (
|
||||
<div style={{
|
||||
position: 'absolute', left: 27, top: 32, bottom: -6,
|
||||
width: 2, background: 'var(--c-divider)',
|
||||
}} />
|
||||
)}
|
||||
<div style={{
|
||||
width: 28, height: 28, borderRadius: 999, flexShrink: 0,
|
||||
background: c.bg, color: c.fg,
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
fontSize: 11, fontWeight: 700,
|
||||
position: 'relative', zIndex: 1,
|
||||
}}>
|
||||
{d.init}
|
||||
</div>
|
||||
<div style={{ flex: 1, paddingBottom: 4, minWidth: 0 }}>
|
||||
<div style={{ fontSize: 13, fontWeight: 700, color: 'var(--c-fg-1)', marginBottom: 2, lineHeight: 1.35 }}>{v.title}</div>
|
||||
<div className="sub" style={{ fontSize: 11 }}>{v.date} · {d.name.split(' ').slice(0, 2).join(' ')}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Upcoming appointment */}
|
||||
{upcoming && upDoc && (
|
||||
<div style={{ padding: '0 20px 18px' }}>
|
||||
<SectionHeader title="Ближайший приём" pad="0 0 8px 0" />
|
||||
<AppointmentCard appt={upcoming} doctor={upDoc} addr={clinic.addresses.find(a => a.id === upcoming.address)} onClick={() => nav.push('appt:' + upcoming.id)} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Recommendations */}
|
||||
<div style={{ padding: '0 0 18px' }}>
|
||||
<SectionHeader title="Рекомендации" />
|
||||
<div style={{ display: 'flex', gap: 10, overflowX: 'auto', padding: '0 20px 8px' }} className="noscroll">
|
||||
{chronic.recommendations.map((r, i) => (
|
||||
<div key={i} className="card" style={{
|
||||
flexShrink: 0, width: 170, padding: 14,
|
||||
}}>
|
||||
<div style={{ fontSize: 28, marginBottom: 8, lineHeight: 1 }}>{r.icon}</div>
|
||||
<div style={{ fontSize: 13, fontWeight: 700, marginBottom: 3, lineHeight: 1.3 }}>{r.title}</div>
|
||||
<div className="sub" style={{ fontSize: 11, lineHeight: 1.4 }}>{r.sub}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Book CTA */}
|
||||
<div style={{ padding: '0 20px 18px' }}>
|
||||
<button onClick={() => nav.push('booking-specs')} className="btn-p block">
|
||||
<I.plus size={18} /> Записаться на осмотр
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Articles */}
|
||||
<div style={{ padding: '0 0 8px' }}>
|
||||
<SectionHeader title="Полезное чтение" action="Все" onAction={() => nav.push('articles')} />
|
||||
<div style={{ display: 'flex', gap: 12, overflowX: 'auto', padding: '0 20px 8px' }} className="noscroll">
|
||||
{articles.slice(0, 3).map(a => (
|
||||
<button key={a.id} onClick={() => nav.push('article:' + a.id)} className="press card" style={{
|
||||
flexShrink: 0, width: 200, padding: 0, overflow: 'hidden', textAlign: 'left',
|
||||
}}>
|
||||
<div style={{
|
||||
height: 80, background: a.hero,
|
||||
display: 'flex', alignItems: 'flex-end', justifyContent: 'space-between', padding: 10,
|
||||
}}>
|
||||
<span className="chip chip-warm" style={{ background: 'rgba(255,255,255,0.85)' }}>{a.tag}</span>
|
||||
<span style={{ fontSize: 24, lineHeight: 1 }}>{a.emoji}</span>
|
||||
</div>
|
||||
<div style={{ padding: 12 }}>
|
||||
<div style={{ fontSize: 13, fontWeight: 700, lineHeight: 1.3, marginBottom: 6, minHeight: 34 }}>{a.title}</div>
|
||||
<div className="sub" style={{ fontSize: 11 }}>{a.mins} мин</div>
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function HomeFeedScreen({ nav }) {
|
||||
const { doctors, appointments, clinic, articles, recovery } = CLINIC_DATA;
|
||||
const upcoming = appointments.find(a => a.status === 'upcoming');
|
||||
|
||||
Reference in New Issue
Block a user