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