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 */}
+
+
+
Клиника УГН
+
+
+
+
+
{new Date().getFullYear() - 2014}
+
лет опыта
+
+
+
+
+
+ {/* Articles */}
+
+
nav.push('articles')} />
+
+ {articles.map(a => (
+
+ ))}
+
+
+
+ );
+}
+
+// ─────────────────────────────────────────────────────────────
+// Search screen
+// ─────────────────────────────────────────────────────────────
+function SearchSection({ title, children }) {
+ return (
+
+ );
+}
+
+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 (
+
+
+
+
+
+
+
+
+ {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 && (
+
+ )}
+
+
+ );
+}