diff --git a/src/App.jsx b/src/App.jsx index 5a42b12..3aae517 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -48,7 +48,8 @@ const FONT_OPTIONS = [ { id: 'golos', lb: 'Golos', base: '"Golos Text", system-ui, sans-serif', narrow: '"Oswald", sans-serif' }, ]; const SCREEN_OPTIONS = [ - { id: 'home', lb: 'Главная' }, + { id: 'home', lb: 'Главная 1' }, + { id: 'home-v2', lb: 'Главная 2' }, { id: 'doctors', lb: 'Врачи' }, { id: 'doctor:syndaev', lb: 'Карточка врача' }, { id: 'booking-specs', lb: 'Запись: специализация' }, @@ -74,6 +75,9 @@ const SCREEN_OPTIONS = [ { id: 'article:septoplasty-recovery', lb: 'Статья: восстановление после септопластики' }, { id: 'article:throat-pregnancy', lb: 'Статья: горло при беременности' }, { id: 'article:hearing-check', lb: 'Статья: когда проверить слух' }, + { id: 'search', lb: 'Поиск' }, + { id: 'contacts', lb: 'Контакты' }, + { id: 'prices', lb: 'Цены' }, ]; function applyTheme(tw) { diff --git a/src/PhoneApp.jsx b/src/PhoneApp.jsx index f4bee3e..13bdd14 100644 --- a/src/PhoneApp.jsx +++ b/src/PhoneApp.jsx @@ -14,6 +14,7 @@ import { TelemedScreen, MedcardScreen, NotificationsScreen, } from './screens/screens-misc.jsx'; import { ArticlesScreen, ArticleDetailScreen } from './screens/screens-articles.jsx'; +import { HomeV2Screen, SearchScreen, ContactsScreen, PricesScreen } from './screens/screens-v2.jsx'; function renderScreen(screenId, nav, ctx) { const parts = screenId.split(':'); @@ -21,6 +22,7 @@ function renderScreen(screenId, nav, ctx) { const HOME = { cards: HomeCardsScreen, list: HomeListScreen, feed: HomeFeedScreen }[ctx.homeVariant] || HomeCardsScreen; switch (id) { case 'home': return ; + case 'home-v2': return ; case 'doctors': return ; case 'doctor': return ; case 'booking-specs': return ; @@ -43,6 +45,9 @@ function renderScreen(screenId, nav, ctx) { case 'notifications': return ; case 'articles': return ; case 'article': return ; + case 'search': return ; + case 'contacts': return ; + case 'prices': return ; default: return
Экран не найден: {screenId}
; } } @@ -62,7 +67,8 @@ export function PhoneApp({ initialScreen, ctx }) { }), []); const current = stack[stack.length - 1]; - const tabId = TAB_IDS.includes(current.split(':')[0]) ? current.split(':')[0] : null; + const rootId = current.split(':')[0]; + const tabId = rootId === 'home-v2' ? 'home' : (TAB_IDS.includes(rootId) ? rootId : null); const showTabBar = tabId !== null; const modalScreens = ['qr', 'telemed', 'booking-success', 'audiotest']; diff --git a/src/screens/screens-v2.jsx b/src/screens/screens-v2.jsx new file mode 100644 index 0000000..64f0601 --- /dev/null +++ b/src/screens/screens-v2.jsx @@ -0,0 +1,686 @@ +import React, { useMemo, useState } from 'react'; +import { I } from '../icons.jsx'; +import { CLINIC_DATA } from '../data.js'; +import { Avatar, AppointmentCard, SectionHeader, ScreenHeader } from '../components.jsx'; + +// ───────────────────────────────────────────────────────────── +// Symptoms dictionary — maps natural-language complaints to +// suggested services and specialty. +// ───────────────────────────────────────────────────────────── +const SYMPTOMS = [ + { id: 'throat', q: 'Боль в горле', spec: 'ЛОР', suggest: ['Приём ЛОР-врача первичный', 'Промывание миндалин'] }, + { id: 'ear', q: 'Боль в ухе', spec: 'ЛОР', suggest: ['Приём ЛОР-врача первичный', 'Эндоскопия ЛОР-органов'] }, + { id: 'hearing', q: 'Тугоухость, плохо слышу', spec: 'Сурдолог', suggest: ['Аудиометрия', 'Тимпанометрия'] }, + { id: 'tinnitus',q: 'Шум в ушах', spec: 'Сурдолог', suggest: ['Аудиометрия', 'Приём ЛОР-врача первичный'] }, + { id: 'snoring', q: 'Храп', spec: 'ЛОР-хирург', suggest: ['Септопластика', 'Приём ЛОР-врача первичный'] }, + { id: 'nose', q: 'Заложен нос', spec: 'ЛОР', suggest: ['Эндоскопия ЛОР-органов', 'Промывание носа «Кукушка»'] }, + { id: 'allergy', q: 'Аллергия, сенная лихорадка',spec: 'Аллерголог', suggest: ['Приём ЛОР-врача первичный'] }, + { id: 'adenoids',q: 'Аденоиды', spec: 'Детский ЛОР', suggest: ['Эндоскопия ЛОР-органов', 'Аденотомия (эндоскопическая)'] }, +]; + +const SUGGESTED = ['аденоиды', 'тугоухость', 'храп', 'Синдяев', 'сегодня', 'аудиометрия']; + +function matchesQuery(q, ...fields) { + const Q = String(q).trim().toLowerCase(); + if (!Q) return false; + return fields.some(f => f && String(f).toLowerCase().includes(Q)); +} + +// ───────────────────────────────────────────────────────────── +// Home V2 — search-first layout +// ───────────────────────────────────────────────────────────── +export function HomeV2Screen({ nav }) { + const { doctors, appointments, clinic, articles, services } = CLINIC_DATA; + const upcoming = appointments.find(a => a.status === 'upcoming'); + const upDoc = upcoming && doctors.find(d => d.id === upcoming.doctor); + + return ( +
+
+
+
+
Добрый день,
+
Анна Сергеевна
+
+ +
+ + {/* Universal search */} + +
+ + {/* Book CTA */} +
+ +
+ + {/* Quick tiles — Contacts, Prices, Results, Recovery */} +
+
+ + + + +
+
+ + {upcoming && upDoc && ( +
+ nav.set('appts')} /> + a.id === upcoming.address)} onClick={() => nav.push('appt:' + upcoming.id)} /> +
+ )} + + {/* Clinic stats */} +
+
+
Клиника УГН
+
+
+
27
+
врачей
+
+
+
6
+
к.м.н.
+
+
+
{new Date().getFullYear() - 2014}
+
лет опыта
+
+
+
+
+ + {/* Articles */} +
+ nav.push('articles')} /> +
+ {articles.map(a => ( + + ))} +
+
+
+ ); +} + +// ───────────────────────────────────────────────────────────── +// Search screen +// ───────────────────────────────────────────────────────────── +function SearchSection({ title, children }) { + return ( +
+
{title}
+
{children}
+
+ ); +} + +export function SearchScreen({ nav }) { + const [q, setQ] = useState(''); + const { doctors, services, articles, appointments, clinic } = CLINIC_DATA; + + const results = useMemo(() => { + const Q = q.trim(); + if (!Q) return null; + const isDateish = /сегодня|завтра|\b\d{1,2}\s*(?:апр|мар|май|июн)\b|приём|приёмы|визит/i.test(Q); + return { + doctors: doctors.filter(d => matchesQuery(Q, d.name, d.spec)).slice(0, 5), + services: services.filter(s => matchesQuery(Q, s.name, s.cat)).slice(0, 6), + symptoms: SYMPTOMS.filter(s => matchesQuery(Q, s.q, s.spec)).slice(0, 4), + articles: articles.filter(a => matchesQuery(Q, a.title, a.tag, a.lede)).slice(0, 3), + appointments: isDateish ? appointments.filter(a => a.status === 'upcoming').slice(0, 3) : [], + }; + }, [q]); + + const total = results ? (results.doctors.length + results.services.length + results.symptoms.length + results.articles.length + results.appointments.length) : 0; + + return ( +
+
+ +
+ + setQ(e.target.value)} + placeholder="Врач, симптом, услуга, дата…" + style={{ flex: 1, border: 0, outline: 0, fontSize: 15, background: 'transparent' }} + /> + {q && ( + + )} +
+
+ +
+ {!q.trim() && ( + <> +
Популярные запросы
+
+ {SUGGESTED.map(s => ( + + ))} +
+ +
Частые симптомы
+
+ {SYMPTOMS.slice(0, 6).map((s, i, a) => ( + + + {i < a.length - 1 &&
} + + ))} +
+ + )} + + {q.trim() && total === 0 && ( +
+
🔍
+
Ничего не найдено
+
Попробуйте другой запрос или
+
+ )} + + {results && results.symptoms.length > 0 && ( + + {results.symptoms.map(s => ( + + ))} + + )} + + {results && results.doctors.length > 0 && ( + + {results.doctors.map(d => ( + + ))} + + )} + + {results && results.services.length > 0 && ( + +
+ {results.services.map((s, i, a) => ( + + + {i < a.length - 1 &&
} + + ))} +
+ + )} + + {results && results.articles.length > 0 && ( + + {results.articles.map(a => ( + + ))} + + )} + + {results && results.appointments.length > 0 && ( + + {results.appointments.map(appt => { + const d = doctors.find(x => x.id === appt.doctor); + const ad = clinic.addresses.find(x => x.id === appt.address); + return nav.push('appt:' + appt.id)} />; + })} + + )} +
+
+ ); +} + +// ───────────────────────────────────────────────────────────── +// Contacts screen — building + map mock per address +// ───────────────────────────────────────────────────────────── +function BuildingMock({ variant = 'teal', title = 'КЛИНИКА УГН' }) { + const palettes = { + teal: { + sky: 'linear-gradient(180deg, #CFE6E2 0%, #E3F4F2 100%)', + ground: '#B5D1C0', + wall: '#E8DCC5', + roof: '#8B6B3A', + window: '#2B4B4A', + windowLit: '#F7D88B', + sign: '#166B63', + trees: '#5A8A5E', + }, + warm: { + sky: 'linear-gradient(180deg, #F2E5C7 0%, #FDF8E6 100%)', + ground: '#B4A67D', + wall: '#C7E8E4', + roof: '#5C756A', + window: '#2B3B4A', + windowLit: '#FFE6A3', + sign: '#7A6A2E', + trees: '#6B8B4E', + }, + }; + const p = palettes[variant]; + // Deterministic lit-window pattern (not random, for stable rendering) + const lit = [1,1,0,1,1, 1,0,1,0,1, 0,1,1,1,0, 1,1,0,1,1]; + return ( +
+
+
+
+
+
+
+
{title}
+
+
+ {lit.map((l, i) => ( +
+ ))} +
+
+
+
+ ); +} + +function MapMock({ variant = 'teal' }) { + const pinColor = variant === 'teal' ? '#E04E44' : '#166B63'; + return ( +
+ {/* Main streets */} +
+
+
+
+ {/* Park block */} +
+
ПАРК
+ {/* Building footprints */} +
+
+
+ {/* Pin */} +
+ + + + +
+ {/* Scale indicator */} +
100 м
+
+ ); +} + +export function ContactsScreen({ nav }) { + const { clinic } = CLINIC_DATA; + const addresses = clinic.addresses.filter(a => a.id !== 'krasnokamsk'); + const variants = ['teal', 'warm']; + + return ( +
+ nav.pop()} /> + + {/* Phone + hours */} +
+
+
Круглосуточная запись
+
{clinic.phone}
+
+ {clinic.hours} +
+
+ + +
+
+
+ + {/* Addresses */} +
+ {addresses.map((a, i) => { + const variant = variants[i % variants.length]; + return ( +
+ +
+
{a.full}
+
{a.note}
+
+ + Пн–Вс 9:00–21:00 + + + 2 этаж + +
+ +
+ + + +
+
+
+ ); + })} +
+ + {/* Transport tips */} +
+
+
+ +
+
+
Как добраться
+
+ Автобус 30, 40, 74 · остановка «Центральный рынок». Бесплатная парковка во дворе. +
+
+
+
+
+ ); +} + +// ───────────────────────────────────────────────────────────── +// Prices screen — grouped services with search + category filter +// ───────────────────────────────────────────────────────────── +export function PricesScreen({ nav }) { + const { services } = CLINIC_DATA; + const [q, setQ] = useState(''); + const [cat, setCat] = useState('Все'); + const categories = useMemo(() => ['Все', ...Array.from(new Set(services.map(s => s.cat)))], [services]); + + const filtered = useMemo(() => { + return services.filter(s => { + if (cat !== 'Все' && s.cat !== cat) return false; + if (q.trim() && !matchesQuery(q, s.name, s.cat)) return false; + return true; + }); + }, [q, cat, services]); + + const grouped = useMemo(() => { + const m = new Map(); + filtered.forEach(s => { + if (!m.has(s.cat)) m.set(s.cat, []); + m.get(s.cat).push(s); + }); + return [...m.entries()]; + }, [filtered]); + + const minPrice = filtered.length ? Math.min(...filtered.map(s => s.price)) : 0; + const maxPrice = filtered.length ? Math.max(...filtered.map(s => s.price)) : 0; + + return ( +
+ nav.pop()} /> + +
+
+ + setQ(e.target.value)} + placeholder="Найти услугу" + style={{ flex: 1, border: 0, outline: 0, fontSize: 15, background: 'transparent' }} + /> + {q && ( + + )} +
+
+ +
+ {categories.map(c => ( + + ))} +
+ + {/* Price range summary */} + {filtered.length > 0 && ( +
+ Найдено: {filtered.length} + + от {minPrice.toLocaleString('ru')} ₽ + {' '}до {maxPrice.toLocaleString('ru')} ₽ + +
+ )} + +
+ {grouped.length === 0 && ( +
+
📭
+
Ничего не найдено
+
Попробуйте сбросить фильтры
+ +
+ )} + + {grouped.map(([category, items]) => ( +
+
+ {category} + {items.length} +
+
+ {items.map((s, i) => ( + + + {i < items.length - 1 &&
} + + ))} +
+
+ ))} + + {filtered.length > 0 && ( + + )} +
+
+ ); +}