fix: 1C response json parsing in bridge, checkup page cutouts and doctor assets
@ -140,7 +140,15 @@ class BridgeHandler(BaseHTTPRequestHandler):
|
||||
if err:
|
||||
self.send_json(502, {"code": "1c_error", "message": err})
|
||||
else:
|
||||
self.send_json(200, data or [])
|
||||
slots = data or []
|
||||
while isinstance(slots, str):
|
||||
try:
|
||||
slots = json.loads(slots)
|
||||
except Exception:
|
||||
break
|
||||
if not isinstance(slots, list):
|
||||
slots = []
|
||||
self.send_json(200, slots)
|
||||
return
|
||||
|
||||
if path == "/doctors":
|
||||
@ -154,7 +162,15 @@ class BridgeHandler(BaseHTTPRequestHandler):
|
||||
self.send_json(502, {"code": "1c_error", "message": err})
|
||||
else:
|
||||
doctors = data or []
|
||||
while isinstance(doctors, str):
|
||||
try:
|
||||
doctors = json.loads(doctors)
|
||||
except Exception:
|
||||
break
|
||||
if not isinstance(doctors, list):
|
||||
doctors = []
|
||||
for d in doctors:
|
||||
if isinstance(d, dict):
|
||||
try:
|
||||
d["kids_from"] = int(d.get("kids_from", 0))
|
||||
except (ValueError, TypeError):
|
||||
@ -211,8 +227,15 @@ class BridgeHandler(BaseHTTPRequestHandler):
|
||||
self.send_json(502, {"code": "1c_error", "message": err})
|
||||
else:
|
||||
services = data or []
|
||||
# Guarantee types for Go unmarshaling
|
||||
while isinstance(services, str):
|
||||
try:
|
||||
services = json.loads(services)
|
||||
except Exception:
|
||||
break
|
||||
if not isinstance(services, list):
|
||||
services = []
|
||||
for s in services:
|
||||
if isinstance(s, dict):
|
||||
try:
|
||||
s["duration_min"] = int(s.get("duration_min", 30))
|
||||
except (ValueError, TypeError):
|
||||
|
||||
BIN
web/public/assets/doctors/abakarova-patimat-alievna.webp
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
web/public/assets/doctors/abdullaev-magomed-abdullaevich.webp
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
web/public/assets/doctors/abdurakhmanova-zaliyat-abdulaevna.webp
Normal file
|
After Width: | Height: | Size: 8.6 KiB |
BIN
web/public/assets/doctors/afanaseva-elena-fyedorovna.webp
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
web/public/assets/doctors/akhmedova-maira-gazalievna.webp
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
web/public/assets/doctors/akhmedova-milana-nimatullakhovna.webp
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
web/public/assets/doctors/akhmetkhanova-saida-abdulaevna.webp
Normal file
|
After Width: | Height: | Size: 8.8 KiB |
BIN
web/public/assets/doctors/alieva-elvira-alievna.webp
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
web/public/assets/doctors/alilova-patimat-mirzaevna.webp
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
web/public/assets/doctors/azmedkhanovu-patimat-bagomedovnu.webp
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
web/public/assets/doctors/bakhishov-rinat-tagirovich.webp
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
web/public/assets/doctors/biarslanova-liana-biarslanovna.webp
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
web/public/assets/doctors/chergesova-zaira-muratovna.webp
Normal file
|
After Width: | Height: | Size: 8.8 KiB |
BIN
web/public/assets/doctors/eldarkhanova-pakhu-mutaevna.webp
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
web/public/assets/doctors/esetov-murad-azedinovich.webp
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
web/public/assets/doctors/gadzhimuradova-rasiyat-gadzhievna.webp
Normal file
|
After Width: | Height: | Size: 8.0 KiB |
BIN
web/public/assets/doctors/gasanova-galimat-shikhovna.webp
Normal file
|
After Width: | Height: | Size: 9.0 KiB |
BIN
web/public/assets/doctors/gasanova-shakhrizat-magomedovna.webp
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
web/public/assets/doctors/gaydarova-arapat-eldarovna.webp
Normal file
|
After Width: | Height: | Size: 9.0 KiB |
BIN
web/public/assets/doctors/gimbatova-zalina-muradovna.webp
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
web/public/assets/doctors/idrisova-aida-shamilevna-.webp
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
web/public/assets/doctors/idrisova-zarema-zurkanayevna.webp
Normal file
|
After Width: | Height: | Size: 9.5 KiB |
BIN
web/public/assets/doctors/inkova-ayshat-inkavovna.webp
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
web/public/assets/doctors/irzaeva-sevil-vagifovna.webp
Normal file
|
After Width: | Height: | Size: 9.3 KiB |
BIN
web/public/assets/doctors/isaeva-zarema-kaybullakhovna.webp
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
web/public/assets/doctors/ismailova-naizat-tashbekovna.webp
Normal file
|
After Width: | Height: | Size: 9.8 KiB |
BIN
web/public/assets/doctors/khizrieva-zaira-sayputdinovna.webp
Normal file
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 10 KiB |
BIN
web/public/assets/doctors/magdieva-zarema-khulatdaevna.webp
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
web/public/assets/doctors/magomedova-sabina-yaverovna.webp
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
web/public/assets/doctors/magomedova-svetlana-seferbekovna-.webp
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
web/public/assets/doctors/maksudov-albert-akhmedovich.webp
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
web/public/assets/doctors/medzhidova-asiyat-muminovna.webp
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
web/public/assets/doctors/mikailov-magomed-shamilovich.webp
Normal file
|
After Width: | Height: | Size: 9.7 KiB |
BIN
web/public/assets/doctors/mingbolatov.webp
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
web/public/assets/doctors/muftalieva-dzhuleta-byunyaminovna.webp
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
web/public/assets/doctors/mutaeva-irina-vitalevna.webp
Normal file
|
After Width: | Height: | Size: 9.0 KiB |
BIN
web/public/assets/doctors/nurmagomedova-rekuyat-mamedovna-.webp
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
web/public/assets/doctors/omarova-aminat-magomedovna.webp
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
web/public/assets/doctors/omarova-maysarat-askhabovna.webp
Normal file
|
After Width: | Height: | Size: 8.9 KiB |
BIN
web/public/assets/doctors/rasulova-dzhamilyat-akhmedovna.webp
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
web/public/assets/doctors/saaduev-saadu-khabibovich.webp
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
web/public/assets/doctors/savina-iya-yurevna.webp
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
web/public/assets/doctors/saygidnorova-marin-muradovna.webp
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
web/public/assets/doctors/shugaev-eldar-edmanovich.webp
Normal file
|
After Width: | Height: | Size: 9.8 KiB |
BIN
web/public/assets/doctors/taibov-murad-tayibovich.webp
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
web/public/assets/doctors/temirova-gyulbakhar-tadzhirovna.webp
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
web/public/assets/doctors/tumalaeva-naida-ganipaevna.webp
Normal file
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 16 KiB |
@ -99,12 +99,12 @@ export function directionImage(service) {
|
||||
|
||||
const CHECKUP_MEDIA = {
|
||||
'zdorovaya-devoch-a': { photo: ckGirl, icon: icoFemale, hero: checkupHeroGirl },
|
||||
'proverit-svoe-zhenskoe-zdorove-': { photo: ckWoman, icon: icoCheckup, cutout: cutWomanHealth },
|
||||
'planirovannie-beremennosti': { photo: ckPregnancy, icon: icoPregnant, cutout: cutPlanning },
|
||||
'profilaktika-raka-sheyki-matki': { photo: ckCervix, icon: icoRibbon, cutout: cutCervix, cutoutCenter: true },
|
||||
'proverit-svoe-zhenskoe-zdorove-': { photo: ckWoman, icon: icoCheckup, cutout: cutWomanHealth, cutoutRatio: 0.943 },
|
||||
'planirovannie-beremennosti': { photo: ckPregnancy, icon: icoPregnant, cutout: cutPlanning, cutoutRatio: 1.056 },
|
||||
'profilaktika-raka-sheyki-matki': { photo: ckCervix, icon: icoRibbon, cutout: cutCervix, cutoutRatio: 1.452 },
|
||||
// серо-синий фон шапки (#7886a9) — давление и родители
|
||||
'davlenie-v-poryadke': { photo: ckPressure, icon: icoTonometer, cutout: cutPressure, heroBg: '#7886a9' },
|
||||
'zdorovye-roditeli': { photo: ckParents, icon: icoFamily, cutout: cutParents, heroBg: '#7886a9' },
|
||||
'davlenie-v-poryadke': { photo: ckPressure, icon: icoTonometer, cutout: cutPressure, cutoutRatio: 1.374, heroBg: '#7886a9' },
|
||||
'zdorovye-roditeli': { photo: ckParents, icon: icoFamily, cutout: cutParents, cutoutRatio: 1.202, heroBg: '#7886a9' },
|
||||
'mne-god': { photo: ckYear, icon: icoBaby },
|
||||
'chek-ap-ya-v-sad': { photo: ckKindergarten, icon: icoPlayground },
|
||||
pervoklashka: { photo: ckSchool, icon: icoSchool },
|
||||
@ -112,11 +112,11 @@ const CHECKUP_MEDIA = {
|
||||
detskiy: { photo: ckMini, icon: icoStethoscope },
|
||||
'zdorovyy-zhkt': { photo: ckGut, icon: icoStomach },
|
||||
// программ ниже в макете нет — берём близкие по смыслу фото макета
|
||||
'vedenie-beremennosti-1-trimestr': { photo: ckPregnancy, icon: icoPregnant, cutout: cutTrimester1 },
|
||||
'vedenie-beremennosti-2-trim': { photo: ckPregnancy, icon: icoPregnant, cutout: cutTrimester2 },
|
||||
'vedenie-beremennosti-3-trim': { photo: ckPregnancy, icon: icoPregnant, cutout: cutTrimester3 },
|
||||
'poslerodovoy-chek-ap-cherez-mesyats': { photo: ckWoman, icon: icoFemale, cutout: cutPostpartum },
|
||||
'zdorovaya-zhenshchina-40': { photo: ckWoman, icon: icoFemale, cutout: cutWoman40 },
|
||||
'vedenie-beremennosti-1-trimestr': { photo: ckPregnancy, icon: icoPregnant, cutout: cutTrimester1, cutoutRatio: 1.048 },
|
||||
'vedenie-beremennosti-2-trim': { photo: ckPregnancy, icon: icoPregnant, cutout: cutTrimester2, cutoutRatio: 1.215 },
|
||||
'vedenie-beremennosti-3-trim': { photo: ckPregnancy, icon: icoPregnant, cutout: cutTrimester3, cutoutRatio: 1.191 },
|
||||
'poslerodovoy-chek-ap-cherez-mesyats': { photo: ckWoman, icon: icoFemale, cutout: cutPostpartum, cutoutRatio: 1.007 },
|
||||
'zdorovaya-zhenshchina-40': { photo: ckWoman, icon: icoFemale, cutout: cutWoman40, cutoutRatio: 1.054 },
|
||||
'chek-ap-ya-rodilsya': { photo: ckYear, icon: icoBaby },
|
||||
};
|
||||
|
||||
@ -133,8 +133,8 @@ export function checkupMedia(checkup) {
|
||||
cutout: m.cutout || '',
|
||||
// свой цвет фона шапки, если в макете он не коралловый
|
||||
heroBg: m.heroBg || '',
|
||||
// главное в кадре по центру — при нехватке места фигура обрезается с двух сторон, а не только слева
|
||||
cutoutCenter: Boolean(m.cutoutCenter),
|
||||
// ширина/высота файла фигуры: по ней шапка понимает, поместится ли фигура справа от текста
|
||||
cutoutRatio: m.cutoutRatio || 0,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -443,53 +443,55 @@
|
||||
}
|
||||
|
||||
/* ---------- вырезанная фигура в шапке ----------
|
||||
Фигура срезана сверху и снизу, поэтому всегда на всю высоту шапки (срез совпадает с краями)
|
||||
и не выходит за правую линию сетки. Если снимок шире места справа от текста, лишнее уходит
|
||||
влево под плавное растворение — без прямого среза. */
|
||||
Фигура срезана сверху и снизу, поэтому всегда на всю высоту шапки и целиком — без обрезки
|
||||
и растворения. Справа может выходить за сетку к краю экрана, как в макете. Если справа от текста
|
||||
места не хватает, фигура встаёт сразу за текстом и уходит правым краем за экран, но на текст не
|
||||
налезает. Ширину фигуры при текущей высоте даёт --cutout-ratio (ширина/высота файла). */
|
||||
.checkup-hero__row--cutout .checkup-hero__copy {
|
||||
max-width: 520px;
|
||||
max-width: 480px;
|
||||
}
|
||||
|
||||
.checkup-hero__image--cutout {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 548px;
|
||||
right: 0;
|
||||
left: 504px;
|
||||
/* правый край — край экрана, на широких экранах не дальше 220px за сеткой */
|
||||
right: max(calc((100% - 100vw) / 2), -220px);
|
||||
width: auto;
|
||||
height: auto;
|
||||
overflow: hidden;
|
||||
overflow: visible;
|
||||
pointer-events: none;
|
||||
-webkit-mask-image: linear-gradient(to right, transparent 0, #000 72px);
|
||||
mask-image: linear-gradient(to right, transparent 0, #000 72px);
|
||||
container-type: size;
|
||||
}
|
||||
|
||||
.checkup-hero__image--cutout img {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: max(0px, calc(100% - var(--cutout-ratio, 1.2) * 100cqh));
|
||||
width: auto;
|
||||
height: 100%;
|
||||
max-width: none;
|
||||
}
|
||||
|
||||
/* главное в кадре по центру (лента у «Профилактики рака шейки матки») — центрируем, а не к линии */
|
||||
.checkup-hero__image--center img {
|
||||
right: auto;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
/* планшет: места справа от текста мало — фигура целиком у правого края экрана, а часть под текстом
|
||||
бледнеет, чтобы текст читался */
|
||||
@media (max-width: 1023px) {
|
||||
.checkup-hero__row--cutout .checkup-hero__copy {
|
||||
max-width: 55%;
|
||||
}
|
||||
.checkup-hero__image--cutout {
|
||||
left: 56%;
|
||||
right: 0;
|
||||
left: calc(-1 * var(--gutter));
|
||||
right: calc(-1 * var(--gutter));
|
||||
width: auto;
|
||||
height: auto;
|
||||
}
|
||||
.checkup-hero__image--cutout img {
|
||||
left: auto;
|
||||
right: 0;
|
||||
-webkit-mask-image: linear-gradient(to right, rgba(0, 0, 0, 0.18) 35%, #000 70%);
|
||||
mask-image: linear-gradient(to right, rgba(0, 0, 0, 0.18) 35%, #000 70%);
|
||||
}
|
||||
}
|
||||
|
||||
/* телефон: фигура уходит за текст и приглушена, чтобы текст читался */
|
||||
@ -506,7 +508,13 @@
|
||||
height: auto;
|
||||
}
|
||||
.checkup-hero__image--cutout img {
|
||||
left: 0;
|
||||
width: 100%;
|
||||
object-fit: cover;
|
||||
object-position: 70% bottom;
|
||||
opacity: 0.5;
|
||||
-webkit-mask-image: none;
|
||||
mask-image: none;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -27,7 +27,7 @@ export default function CheckupPage() {
|
||||
if (!checkup.data || checkup.data.slug !== slug) return <p className="state checkup-loading">Загружаем…</p>;
|
||||
|
||||
const c = checkup.data;
|
||||
const { hero, photo, cutout, heroBg, cutoutCenter } = checkupMedia(c);
|
||||
const { hero, photo, cutout, heroBg, cutoutRatio } = checkupMedia(c);
|
||||
const items = c.items?.length ? c.items.map(cleanText) : checklistItems(c.blocks);
|
||||
const intro = introText(c.blocks);
|
||||
const lead = cleanText(c.lead);
|
||||
@ -64,7 +64,10 @@ export default function CheckupPage() {
|
||||
</div>
|
||||
</div>
|
||||
{cutout ? (
|
||||
<div className={`checkup-hero__image checkup-hero__image--cutout${cutoutCenter ? ' checkup-hero__image--center' : ''}`}>
|
||||
<div
|
||||
className="checkup-hero__image checkup-hero__image--cutout"
|
||||
style={cutoutRatio ? { '--cutout-ratio': cutoutRatio } : undefined}
|
||||
>
|
||||
<img src={cutout} alt="" />
|
||||
</div>
|
||||
) : hero ? (
|
||||
|
||||