Initial scaffold: Клиника УГН mobile prototype

Ported HTML/CSS/JS design bundle from Claude Design to Vite + React.
20 screens (home 3 variants, booking flow, doctors, appointments,
results + audiogram, recovery, audio test, chat, profile, QR, telemed,
medcard, notifications). Tweaks panel with iOS/Android frames,
layout/accent/font switchers.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-19 18:16:59 +05:00
commit 95c9889b5d
18 changed files with 4542 additions and 0 deletions
+294
View File
@@ -0,0 +1,294 @@
import React from 'react';
import { I } from '../icons.jsx';
import { CLINIC_DATA } from '../data.js';
import { Avatar, AppointmentCard, SectionHeader } from '../components.jsx';
export function HomeCardsScreen({ nav }) {
const { doctors, appointments, specializations, clinic, articles } = CLINIC_DATA;
const upcoming = appointments.find(a => a.status === 'upcoming');
const upDoc = upcoming && doctors.find(d => d.id === upcoming.doctor);
return (
<div style={{ paddingBottom: 100 }}>
<div style={{ padding: '8px 20px 16px', background: 'linear-gradient(180deg,#F5EDDF 0%,transparent 100%)' }}>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 20 }}>
<div>
<div className="sub" style={{ marginBottom: 2 }}>Добрый день,</div>
<div style={{ fontSize: 22, fontWeight: 700, color: 'var(--c-fg-1)' }}>Анна Сергеевна</div>
</div>
<div style={{ display: 'flex', gap: 8 }}>
<button onClick={() => nav.push('notifications')} className="press" style={{ width: 42, height: 42, borderRadius: 999, background: '#fff', border: '1px solid var(--c-border)', display: 'flex', alignItems: 'center', justifyContent: 'center', position: 'relative' }}>
<I.bell size={20} style={{ color: 'var(--c-fg-1)' }} />
<span style={{ position: 'absolute', top: 7, right: 9, width: 8, height: 8, borderRadius: 999, background: 'var(--c-accent)' }} />
</button>
<button onClick={() => nav.push('qr')} className="press" style={{ width: 42, height: 42, borderRadius: 999, background: '#fff', border: '1px solid var(--c-border)', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<I.qr size={20} style={{ color: 'var(--c-fg-1)' }} />
</button>
</div>
</div>
<button onClick={() => nav.push('booking-specs')} className="press" style={{
width: '100%', textAlign: 'left',
background: 'var(--c-primary-darker)', color: '#fff',
borderRadius: 20, padding: 18,
display: 'flex', alignItems: 'center', gap: 14,
boxShadow: '0 10px 30px rgba(22,107,99,.25)',
}}>
<div style={{ width: 48, height: 48, borderRadius: 14, background: 'rgba(255,255,255,0.15)', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<I.plus size={26} />
</div>
<div style={{ flex: 1 }}>
<div style={{ fontSize: 16, fontWeight: 700, marginBottom: 2 }}>Записаться на приём</div>
<div style={{ fontSize: 13, opacity: .8 }}>К ЛОР-врачу · сегодня или завтра</div>
</div>
<I.arrow size={22} />
</button>
</div>
{upcoming && upDoc && (
<div style={{ padding: '16px 20px 8px' }}>
<SectionHeader title="Ближайший приём" pad="0 0 0 0" action="Все" onAction={() => nav.set('appts')} />
<AppointmentCard appt={upcoming} doctor={upDoc} addr={clinic.addresses.find(a => a.id === upcoming.address)} onClick={() => nav.push('appt:' + upcoming.id)} />
</div>
)}
<div style={{ padding: '16px 20px 8px' }}>
<SectionHeader title="Специализации" pad="0 0 0 0" />
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3,1fr)', gap: 10 }}>
{specializations.map(s => {
const icons = { ear: I.ear, hear: I.hearing, leaf: I.heart, mic: I.mic, baby: I.profile, scalpel: I.stetho };
const Ic = icons[s.icon];
return (
<button key={s.id} onClick={() => nav.push('doctors')} className="press card" style={{ padding: 14, display: 'flex', flexDirection: 'column', alignItems: 'flex-start', gap: 10 }}>
<div style={{ width: 36, height: 36, borderRadius: 10, background: 'var(--c-primary-100)', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<Ic size={20} style={{ color: 'var(--c-primary-darker)' }} />
</div>
<div>
<div style={{ fontSize: 13, fontWeight: 700, color: 'var(--c-fg-1)', marginBottom: 1 }}>{s.label}</div>
<div className="sub" style={{ fontSize: 11 }}>{s.count} врачей</div>
</div>
</button>
);
})}
</div>
</div>
<div style={{ padding: '16px 20px 8px' }}>
<SectionHeader title="Быстрые действия" pad="0 0 0 0" />
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(2,1fr)', gap: 10 }}>
<button onClick={() => nav.push('telemed')} className="press card" style={{ padding: 14, display: 'flex', gap: 12, alignItems: 'center', textAlign: 'left' }}>
<div style={{ width: 40, height: 40, borderRadius: 10, background: 'var(--c-accent-50)', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<I.video size={20} style={{ color: 'var(--c-accent)' }} />
</div>
<div>
<div style={{ fontSize: 13, fontWeight: 700 }}>Телемедицина</div>
<div className="sub" style={{ fontSize: 11 }}>Видео с врачом</div>
</div>
</button>
<button onClick={() => nav.push('audiotest')} className="press card" style={{ padding: 14, display: 'flex', gap: 12, alignItems: 'center', textAlign: 'left' }}>
<div style={{ width: 40, height: 40, borderRadius: 10, background: 'var(--c-primary-100)', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<I.hearing size={20} style={{ color: 'var(--c-primary-darker)' }} />
</div>
<div>
<div style={{ fontSize: 13, fontWeight: 700 }}>Тест слуха</div>
<div className="sub" style={{ fontSize: 11 }}>За 3 минуты</div>
</div>
</button>
<button onClick={() => nav.push('results')} className="press card" style={{ padding: 14, display: 'flex', gap: 12, alignItems: 'center', textAlign: 'left' }}>
<div style={{ width: 40, height: 40, borderRadius: 10, background: 'var(--c-warm-100)', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<I.doc size={20} style={{ color: 'var(--c-warm-text)' }} />
</div>
<div>
<div style={{ fontSize: 13, fontWeight: 700 }}>Анализы</div>
<div className="sub" style={{ fontSize: 11 }}>5 результатов</div>
</div>
</button>
<button onClick={() => nav.push('recovery')} className="press card" style={{ padding: 14, display: 'flex', gap: 12, alignItems: 'center', textAlign: 'left' }}>
<div style={{ width: 40, height: 40, borderRadius: 10, background: 'var(--c-success-50)', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<I.shield size={20} style={{ color: 'var(--c-success)' }} />
</div>
<div>
<div style={{ fontSize: 13, fontWeight: 700 }}>Восстановление</div>
<div className="sub" style={{ fontSize: 11 }}>День 6 из 14</div>
</div>
</button>
</div>
</div>
<div style={{ padding: '16px 0 8px' }}>
<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={{
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,
}}>
<span className="chip chip-warm" style={{ background: 'rgba(255,255,255,0.85)' }}>{a.tag}</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>
<div className="sub" style={{ fontSize: 11 }}>{a.author} · {a.mins} мин</div>
</div>
</button>
))}
</div>
</div>
</div>
);
}
export function HomeListScreen({ nav }) {
const { doctors, appointments, clinic } = CLINIC_DATA;
const upcoming = appointments.find(a => a.status === 'upcoming');
const upDoc = upcoming && doctors.find(d => d.id === upcoming.doctor);
const items = [
{ id: 'book', icon: I.plus, title: 'Записаться на приём', sub: 'ЛОР, сурдолог, фониатр', tint: 'var(--c-primary-darker)', bg: 'var(--c-primary-100)', cta: true, go: 'booking-specs' },
{ id: 'appt', icon: I.calendar, title: 'Мои приёмы', sub: `${appointments.filter(a=>a.status==='upcoming').length} предстоящих · ${appointments.filter(a=>a.status==='past').length} прошедших`, go: 'appts' },
{ id: 'card', icon: I.file, title: 'Медицинская карта', sub: 'История, результаты, заключения', go: 'medcard' },
{ id: 'results', icon: I.doc, title: 'Анализы и обследования', sub: '5 результатов · 1 в работе', go: 'results' },
{ id: 'telemed', icon: I.video, title: 'Видеоконсультация', sub: 'Онлайн с ЛОР-врачом', go: 'telemed' },
{ id: 'audio', icon: I.hearing, title: 'Тест слуха', sub: 'Аудиограмма за 3 минуты', go: 'audiotest' },
{ id: 'recovery', icon: I.shield, title: 'Восстановление', sub: 'Септопластика · день 6', go: 'recovery' },
{ id: 'chat', icon: I.chat, title: 'Чат с врачом', sub: '2 непрочитанных', badge: 2, go: 'chat' },
];
return (
<div style={{ paddingBottom: 100 }}>
<div style={{ padding: '8px 20px 16px' }}>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 16 }}>
<div>
<div className="sub">Здравствуйте,</div>
<div style={{ fontSize: 20, fontWeight: 700 }}>Анна Сергеевна</div>
</div>
<button onClick={() => nav.push('notifications')} className="press" style={{ width: 40, height: 40, borderRadius: 999, background: '#fff', border: '1px solid var(--c-border)', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<I.bell size={18} />
</button>
</div>
{upcoming && upDoc && (
<div style={{ marginBottom: 18 }}>
<AppointmentCard appt={upcoming} doctor={upDoc} addr={clinic.addresses.find(a=>a.id===upcoming.address)} onClick={() => nav.push('appt:' + upcoming.id)} compact />
</div>
)}
</div>
<div className="card" style={{ margin: '0 16px', padding: 0, overflow: 'hidden' }}>
{items.map((it, i) => (
<React.Fragment key={it.id}>
<button onClick={() => nav.push(it.go)} className="press" style={{
width: '100%', display: 'flex', alignItems: 'center', gap: 14,
padding: '14px 16px', textAlign: 'left',
background: it.cta ? 'var(--c-primary-50)' : 'transparent',
}}>
<div style={{
width: 40, height: 40, borderRadius: 10,
background: it.bg || 'var(--c-primary-100)',
display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0,
}}>
{React.createElement(it.icon, { size: 20, style: { color: it.tint || 'var(--c-primary-darker)' } })}
</div>
<div style={{ flex: 1, minWidth: 0 }}>
<div style={{ fontSize: 15, fontWeight: 700, color: 'var(--c-fg-1)', marginBottom: 2 }}>{it.title}</div>
<div className="sub" style={{ fontSize: 12 }}>{it.sub}</div>
</div>
{it.badge && <span style={{ background: 'var(--c-accent)', color: '#fff', fontSize: 11, fontWeight: 700, padding: '2px 7px', borderRadius: 999 }}>{it.badge}</span>}
<I.chev size={16} style={{ color: 'var(--c-fg-4)' }} />
</button>
{i < items.length - 1 && <div style={{ height: 1, background: 'var(--c-divider)', marginLeft: 70 }} />}
</React.Fragment>
))}
</div>
</div>
);
}
export function HomeFeedScreen({ nav }) {
const { doctors, appointments, clinic, articles, recovery } = CLINIC_DATA;
const upcoming = appointments.find(a => a.status === 'upcoming');
const upDoc = upcoming && doctors.find(d => d.id === upcoming.doctor);
return (
<div style={{ paddingBottom: 100 }}>
<div style={{ padding: '8px 20px 12px' }}>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<div>
<div className="sub">18 апреля, суббота</div>
<div style={{ fontSize: 24, fontWeight: 700 }}>Как Ваше самочувствие?</div>
</div>
<button onClick={() => nav.push('profile')} className="press">
<Avatar init="АС" size={42} />
</button>
</div>
</div>
<div style={{ padding: '0 16px' }}>
<button onClick={() => nav.push('recovery')} className="press card" style={{
width: '100%', textAlign: 'left', padding: 18, marginBottom: 14,
background: 'linear-gradient(135deg,#E3F4F2,#F2FAF9)',
border: '1px solid var(--c-primary-200)',
}}>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 12 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
<div style={{ width: 36, height: 36, borderRadius: 10, background: 'var(--c-primary-darker)', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<I.shield size={18} style={{ color: '#fff' }} />
</div>
<div>
<div style={{ fontWeight: 700, fontSize: 14 }}>Восстановление</div>
<div className="sub" style={{ fontSize: 12 }}>{recovery.op}</div>
</div>
</div>
<span className="chip">День {recovery.dayNow} из {recovery.totalDays}</span>
</div>
<div style={{ height: 6, background: 'rgba(255,255,255,0.7)', borderRadius: 999, overflow: 'hidden', marginBottom: 12 }}>
<div style={{ width: `${recovery.dayNow/recovery.totalDays*100}%`, height: '100%', background: 'var(--c-primary-darker)', borderRadius: 999 }} />
</div>
<div style={{ fontSize: 13, color: 'var(--c-fg-2)' }}>
Сегодня: <strong>осмотр хирурга, снятие корочек</strong>
</div>
</button>
<div className="card" style={{ padding: 14, marginBottom: 14, display: 'flex', gap: 12, alignItems: 'center' }}>
<div style={{ width: 40, height: 40, borderRadius: 10, background: 'var(--c-warning-50)', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<I.pill size={20} style={{ color: 'var(--c-warning)' }} />
</div>
<div style={{ flex: 1 }}>
<div style={{ fontSize: 14, fontWeight: 700 }}>Амоксиклав 625 мг</div>
<div className="sub" style={{ fontSize: 12 }}>Принять в 20:00 через 2 часа</div>
</div>
<button className="btn-s" style={{ padding: '8px 12px', fontSize: 13 }}>Принял</button>
</div>
{upcoming && upDoc && (
<>
<SectionHeader title="Ближайший приём" pad="4px 4px 8px" />
<AppointmentCard appt={upcoming} doctor={upDoc} addr={clinic.addresses.find(a=>a.id===upcoming.address)} onClick={() => nav.push('appt:' + upcoming.id)} />
</>
)}
<button onClick={() => nav.push('booking-specs')} className="press" style={{
width: '100%', marginTop: 14, padding: '14px 18px', borderRadius: 16,
background: 'var(--c-accent)', color: '#fff', fontWeight: 700, fontSize: 15,
display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 8,
}}>
<I.plus size={20} /> Записаться на приём
</button>
</div>
<div style={{ padding: '18px 20px 8px' }}>
<SectionHeader title="Полезно прочитать" 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]}
</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>
<div style={{ fontSize: 14, fontWeight: 700, lineHeight: 1.3, marginBottom: 3 }}>{a.title}</div>
<div className="sub" style={{ fontSize: 12 }}>{a.author} · {a.mins} мин</div>
</div>
</button>
))}
</div>
</div>
</div>
);
}