fix: 1C response json parsing in bridge, checkup page cutouts and doctor assets
@ -140,7 +140,15 @@ class BridgeHandler(BaseHTTPRequestHandler):
|
|||||||
if err:
|
if err:
|
||||||
self.send_json(502, {"code": "1c_error", "message": err})
|
self.send_json(502, {"code": "1c_error", "message": err})
|
||||||
else:
|
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
|
return
|
||||||
|
|
||||||
if path == "/doctors":
|
if path == "/doctors":
|
||||||
@ -154,7 +162,15 @@ class BridgeHandler(BaseHTTPRequestHandler):
|
|||||||
self.send_json(502, {"code": "1c_error", "message": err})
|
self.send_json(502, {"code": "1c_error", "message": err})
|
||||||
else:
|
else:
|
||||||
doctors = data or []
|
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:
|
for d in doctors:
|
||||||
|
if isinstance(d, dict):
|
||||||
try:
|
try:
|
||||||
d["kids_from"] = int(d.get("kids_from", 0))
|
d["kids_from"] = int(d.get("kids_from", 0))
|
||||||
except (ValueError, TypeError):
|
except (ValueError, TypeError):
|
||||||
@ -211,8 +227,15 @@ class BridgeHandler(BaseHTTPRequestHandler):
|
|||||||
self.send_json(502, {"code": "1c_error", "message": err})
|
self.send_json(502, {"code": "1c_error", "message": err})
|
||||||
else:
|
else:
|
||||||
services = data or []
|
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:
|
for s in services:
|
||||||
|
if isinstance(s, dict):
|
||||||
try:
|
try:
|
||||||
s["duration_min"] = int(s.get("duration_min", 30))
|
s["duration_min"] = int(s.get("duration_min", 30))
|
||||||
except (ValueError, TypeError):
|
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 = {
|
const CHECKUP_MEDIA = {
|
||||||
'zdorovaya-devoch-a': { photo: ckGirl, icon: icoFemale, hero: checkupHeroGirl },
|
'zdorovaya-devoch-a': { photo: ckGirl, icon: icoFemale, hero: checkupHeroGirl },
|
||||||
'proverit-svoe-zhenskoe-zdorove-': { photo: ckWoman, icon: icoCheckup, cutout: cutWomanHealth },
|
'proverit-svoe-zhenskoe-zdorove-': { photo: ckWoman, icon: icoCheckup, cutout: cutWomanHealth, cutoutRatio: 0.943 },
|
||||||
'planirovannie-beremennosti': { photo: ckPregnancy, icon: icoPregnant, cutout: cutPlanning },
|
'planirovannie-beremennosti': { photo: ckPregnancy, icon: icoPregnant, cutout: cutPlanning, cutoutRatio: 1.056 },
|
||||||
'profilaktika-raka-sheyki-matki': { photo: ckCervix, icon: icoRibbon, cutout: cutCervix, cutoutCenter: true },
|
'profilaktika-raka-sheyki-matki': { photo: ckCervix, icon: icoRibbon, cutout: cutCervix, cutoutRatio: 1.452 },
|
||||||
// серо-синий фон шапки (#7886a9) — давление и родители
|
// серо-синий фон шапки (#7886a9) — давление и родители
|
||||||
'davlenie-v-poryadke': { photo: ckPressure, icon: icoTonometer, cutout: cutPressure, heroBg: '#7886a9' },
|
'davlenie-v-poryadke': { photo: ckPressure, icon: icoTonometer, cutout: cutPressure, cutoutRatio: 1.374, heroBg: '#7886a9' },
|
||||||
'zdorovye-roditeli': { photo: ckParents, icon: icoFamily, cutout: cutParents, heroBg: '#7886a9' },
|
'zdorovye-roditeli': { photo: ckParents, icon: icoFamily, cutout: cutParents, cutoutRatio: 1.202, heroBg: '#7886a9' },
|
||||||
'mne-god': { photo: ckYear, icon: icoBaby },
|
'mne-god': { photo: ckYear, icon: icoBaby },
|
||||||
'chek-ap-ya-v-sad': { photo: ckKindergarten, icon: icoPlayground },
|
'chek-ap-ya-v-sad': { photo: ckKindergarten, icon: icoPlayground },
|
||||||
pervoklashka: { photo: ckSchool, icon: icoSchool },
|
pervoklashka: { photo: ckSchool, icon: icoSchool },
|
||||||
@ -112,11 +112,11 @@ const CHECKUP_MEDIA = {
|
|||||||
detskiy: { photo: ckMini, icon: icoStethoscope },
|
detskiy: { photo: ckMini, icon: icoStethoscope },
|
||||||
'zdorovyy-zhkt': { photo: ckGut, icon: icoStomach },
|
'zdorovyy-zhkt': { photo: ckGut, icon: icoStomach },
|
||||||
// программ ниже в макете нет — берём близкие по смыслу фото макета
|
// программ ниже в макете нет — берём близкие по смыслу фото макета
|
||||||
'vedenie-beremennosti-1-trimestr': { photo: ckPregnancy, icon: icoPregnant, cutout: cutTrimester1 },
|
'vedenie-beremennosti-1-trimestr': { photo: ckPregnancy, icon: icoPregnant, cutout: cutTrimester1, cutoutRatio: 1.048 },
|
||||||
'vedenie-beremennosti-2-trim': { photo: ckPregnancy, icon: icoPregnant, cutout: cutTrimester2 },
|
'vedenie-beremennosti-2-trim': { photo: ckPregnancy, icon: icoPregnant, cutout: cutTrimester2, cutoutRatio: 1.215 },
|
||||||
'vedenie-beremennosti-3-trim': { photo: ckPregnancy, icon: icoPregnant, cutout: cutTrimester3 },
|
'vedenie-beremennosti-3-trim': { photo: ckPregnancy, icon: icoPregnant, cutout: cutTrimester3, cutoutRatio: 1.191 },
|
||||||
'poslerodovoy-chek-ap-cherez-mesyats': { photo: ckWoman, icon: icoFemale, cutout: cutPostpartum },
|
'poslerodovoy-chek-ap-cherez-mesyats': { photo: ckWoman, icon: icoFemale, cutout: cutPostpartum, cutoutRatio: 1.007 },
|
||||||
'zdorovaya-zhenshchina-40': { photo: ckWoman, icon: icoFemale, cutout: cutWoman40 },
|
'zdorovaya-zhenshchina-40': { photo: ckWoman, icon: icoFemale, cutout: cutWoman40, cutoutRatio: 1.054 },
|
||||||
'chek-ap-ya-rodilsya': { photo: ckYear, icon: icoBaby },
|
'chek-ap-ya-rodilsya': { photo: ckYear, icon: icoBaby },
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -133,8 +133,8 @@ export function checkupMedia(checkup) {
|
|||||||
cutout: m.cutout || '',
|
cutout: m.cutout || '',
|
||||||
// свой цвет фона шапки, если в макете он не коралловый
|
// свой цвет фона шапки, если в макете он не коралловый
|
||||||
heroBg: m.heroBg || '',
|
heroBg: m.heroBg || '',
|
||||||
// главное в кадре по центру — при нехватке места фигура обрезается с двух сторон, а не только слева
|
// ширина/высота файла фигуры: по ней шапка понимает, поместится ли фигура справа от текста
|
||||||
cutoutCenter: Boolean(m.cutoutCenter),
|
cutoutRatio: m.cutoutRatio || 0,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -443,53 +443,55 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* ---------- вырезанная фигура в шапке ----------
|
/* ---------- вырезанная фигура в шапке ----------
|
||||||
Фигура срезана сверху и снизу, поэтому всегда на всю высоту шапки (срез совпадает с краями)
|
Фигура срезана сверху и снизу, поэтому всегда на всю высоту шапки и целиком — без обрезки
|
||||||
и не выходит за правую линию сетки. Если снимок шире места справа от текста, лишнее уходит
|
и растворения. Справа может выходить за сетку к краю экрана, как в макете. Если справа от текста
|
||||||
влево под плавное растворение — без прямого среза. */
|
места не хватает, фигура встаёт сразу за текстом и уходит правым краем за экран, но на текст не
|
||||||
|
налезает. Ширину фигуры при текущей высоте даёт --cutout-ratio (ширина/высота файла). */
|
||||||
.checkup-hero__row--cutout .checkup-hero__copy {
|
.checkup-hero__row--cutout .checkup-hero__copy {
|
||||||
max-width: 520px;
|
max-width: 480px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.checkup-hero__image--cutout {
|
.checkup-hero__image--cutout {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
left: 548px;
|
left: 504px;
|
||||||
right: 0;
|
/* правый край — край экрана, на широких экранах не дальше 220px за сеткой */
|
||||||
|
right: max(calc((100% - 100vw) / 2), -220px);
|
||||||
width: auto;
|
width: auto;
|
||||||
height: auto;
|
height: auto;
|
||||||
overflow: hidden;
|
overflow: visible;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
-webkit-mask-image: linear-gradient(to right, transparent 0, #000 72px);
|
container-type: size;
|
||||||
mask-image: linear-gradient(to right, transparent 0, #000 72px);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.checkup-hero__image--cutout img {
|
.checkup-hero__image--cutout img {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
|
left: max(0px, calc(100% - var(--cutout-ratio, 1.2) * 100cqh));
|
||||||
width: auto;
|
width: auto;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
max-width: none;
|
max-width: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* главное в кадре по центру (лента у «Профилактики рака шейки матки») — центрируем, а не к линии */
|
/* планшет: места справа от текста мало — фигура целиком у правого края экрана, а часть под текстом
|
||||||
.checkup-hero__image--center img {
|
бледнеет, чтобы текст читался */
|
||||||
right: auto;
|
|
||||||
left: 50%;
|
|
||||||
transform: translateX(-50%);
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 1023px) {
|
@media (max-width: 1023px) {
|
||||||
.checkup-hero__row--cutout .checkup-hero__copy {
|
.checkup-hero__row--cutout .checkup-hero__copy {
|
||||||
max-width: 55%;
|
max-width: 55%;
|
||||||
}
|
}
|
||||||
.checkup-hero__image--cutout {
|
.checkup-hero__image--cutout {
|
||||||
left: 56%;
|
left: calc(-1 * var(--gutter));
|
||||||
right: 0;
|
right: calc(-1 * var(--gutter));
|
||||||
width: auto;
|
width: auto;
|
||||||
height: 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;
|
height: auto;
|
||||||
}
|
}
|
||||||
.checkup-hero__image--cutout img {
|
.checkup-hero__image--cutout img {
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
object-position: 70% bottom;
|
||||||
opacity: 0.5;
|
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>;
|
if (!checkup.data || checkup.data.slug !== slug) return <p className="state checkup-loading">Загружаем…</p>;
|
||||||
|
|
||||||
const c = checkup.data;
|
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 items = c.items?.length ? c.items.map(cleanText) : checklistItems(c.blocks);
|
||||||
const intro = introText(c.blocks);
|
const intro = introText(c.blocks);
|
||||||
const lead = cleanText(c.lead);
|
const lead = cleanText(c.lead);
|
||||||
@ -64,7 +64,10 @@ export default function CheckupPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{cutout ? (
|
{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="" />
|
<img src={cutout} alt="" />
|
||||||
</div>
|
</div>
|
||||||
) : hero ? (
|
) : hero ? (
|
||||||
|
|||||||