Продолжение: Модальные окна и дополнительные утилиты
12. Модальные окна
HTML структура модальных окон:
<div class ="modal" id ="callbackModal" >
<div class ="modal-overlay" > </div >
<div class ="modal-container" >
<button class ="modal-close" >
<i class ="fas fa-times" > </i >
</button >
<div class ="modal-content" >
<div class ="modal-header" >
<div class ="modal-icon" >
<i class ="fas fa-phone-alt" > </i >
</div >
<h3 > Заказать обратный звонок</h3 >
<p > Оставьте свой номер, и мы перезвоним вам в течение 15 минут</p >
</div >
<form class ="modal-form" id ="callbackForm" >
<div class ="form-group" >
<label for ="callback-name" > Ваше имя</label >
<input
type ="text"
id ="callback-name"
name ="name"
placeholder ="Введите ваше имя"
required
>
</div >
<div class ="form-group" >
<label for ="callback-phone" > Номер телефона</label >
<input
type ="tel"
id ="callback-phone"
name ="phone"
placeholder ="+7 (___) ___-__-__"
required
>
</div >
<div class ="form-group" >
<label for ="callback-time" > Удобное время для звонка</label >
<select id ="callback-time" name ="time" >
<option value ="" > Выберите время</option >
<option value ="9-12" > 9:00 - 12:00</option >
<option value ="12-15" > 12:00 - 15:00</option >
<option value ="15-18" > 15:00 - 18:00</option >
<option value ="18-21" > 18:00 - 21:00</option >
</select >
</div >
<div class ="form-checkbox" >
<input type ="checkbox" id ="callback-agreement" required >
<label for ="callback-agreement" >
Я согласен на обработку <a href ="privacy.html" target ="_blank" > персональных данных</a >
</label >
</div >
<button type ="submit" class ="btn btn-primary btn-block" >
<span > Заказать звонок</span >
<i class ="fas fa-arrow-right" > </i >
</button >
</form >
<div class ="modal-footer" >
<p > Или позвоните нам прямо сейчас:</p >
<a href ="tel:+79001234567" class ="modal-phone" >
<i class ="fas fa-phone-alt" > </i >
+7 (900) 123-45-67
</a >
</div >
</div >
</div >
</div >
<div class ="modal modal-large" id ="calculatorModal" >
<div class ="modal-overlay" > </div >
<div class ="modal-container" >
<button class ="modal-close" >
<i class ="fas fa-times" > </i >
</button >
<div class ="modal-content" >
<div class ="modal-header" >
<div class ="modal-icon" >
<i class ="fas fa-calculator" > </i >
</div >
<h3 > Рассчитать стоимость ремонта</h3 >
<p > Заполните форму, и мы рассчитаем примерную стоимость вашего ремонта</p >
</div >
<form class ="modal-form calculator-form" id ="calculatorForm" >
<div class ="calculator-step active" data-step ="1" >
<h4 class ="step-title" > Шаг 1 из 4: Тип помещения</h4 >
<div class ="calculator-options" >
<label class ="calculator-option" >
<input type ="radio" name ="room-type" value ="apartment" required >
<div class ="option-card" >
<i class ="fas fa-home" > </i >
<span > Квартира</span >
</div >
</label >
<label class ="calculator-option" >
<input type ="radio" name ="room-type" value ="house" >
<div class ="option-card" >
<i class ="fas fa-building" > </i >
<span > Дом</span >
</div >
</label >
<label class ="calculator-option" >
<input type ="radio" name ="room-type" value ="office" >
<div class ="option-card" >
<i class ="fas fa-briefcase" > </i >
<span > Офис</span >
</div >
</label >
<label class ="calculator-option" >
<input type ="radio" name ="room-type" value ="commercial" >
<div class ="option-card" >
<i class ="fas fa-store" > </i >
<span > Коммерция</span >
</div >
</label >
</div >
</div >
<div class ="calculator-step" data-step ="2" >
<h4 class ="step-title" > Шаг 2 из 4: Площадь помещения</h4 >
<div class ="form-group" >
<label for ="calc-area" > Площадь (м²)</label >
<input
type ="number"
id ="calc-area"
name ="area"
placeholder ="Введите площадь"
min ="10"
max ="1000"
required
>
<div class ="range-slider" >
<input
type ="range"
id ="calc-area-range"
min ="10"
max ="200"
value ="50"
>
<div class ="range-labels" >
<span > 10 м²</span >
<span > 200 м²</span >
</div >
</div >
</div >
<div class ="calculator-options" >
<label class ="calculator-option" >
<input type ="radio" name ="rooms" value ="1" >
<div class ="option-card small" >
<span > 1 комната</span >
</div >
</label >
<label class ="calculator-option" >
<input type ="radio" name ="rooms" value ="2" >
<div class ="option-card small" >
<span > 2 комнаты</span >
</div >
</label >
<label class ="calculator-option" >
<input type ="radio" name ="rooms" value ="3" >
<div class ="option-card small" >
<span > 3 комнаты</span >
</div >
</label >
<label class ="calculator-option" >
<input type ="radio" name ="rooms" value ="4+" >
<div class ="option-card small" >
<span > 4+ комнаты</span >
</div >
</label >
</div >
</div >
<div class ="calculator-step" data-step ="3" >
<h4 class ="step-title" > Шаг 3 из 4: Тип ремонта</h4 >
<div class ="calculator-options vertical" >
<label class ="calculator-option" >
<input type ="radio" name ="repair-type" value ="cosmetic" required >
<div class ="option-card detailed" >
<div class ="option-header" >
<i class ="fas fa-paint-roller" > </i >
<div >
<strong > Косметический ремонт</strong >
<span class ="option-price" > от 2 500 ₽/м²</span >
</div >
</div >
<ul class ="option-features" >
<li > <i class ="fas fa-check" > </i > Поклейка обоев</li >
<li > <i class ="fas fa-check" > </i > Покраска стен</li >
<li > <i class ="fas fa-check" > </i > Замена напольного покрытия</li >
</ul >
</div >
</label >
<label class ="calculator-option" >
<input type ="radio" name ="repair-type" value ="major" >
<div class ="option-card detailed" >
<div class ="option-header" >
<i class ="fas fa-hammer" > </i >
<div >
<strong > Капитальный ремонт</strong >
<span class ="option-price" > от 4 500 ₽/м²</span >
</div >
</div >
<ul class ="option-features" >
<li > <i class ="fas fa-check" > </i > Демонтаж и возведение стен</li >
<li > <i class ="fas fa-check" > </i > Замена электрики и сантехники</li >
<li > <i class ="fas fa-check" > </i > Полная отделка</li >
</ul >
</div >
</label >
<label class ="calculator-option" >
<input type ="radio" name ="repair-type" value ="designer" >
<div class ="option-card detailed" >
<div class ="option-header" >
<i class ="fas fa-magic" > </i >
<div >
<strong > Дизайнерский ремонт</strong >
<span class ="option-price" > от 7 000 ₽/м²</span >
</div >
</div >
<ul class ="option-features" >
<li > <i class ="fas fa-check" > </i > Индивидуальный дизайн-проект</li >
<li > <i class ="fas fa-check" > </i > Премиум материалы</li >
<li > <i class ="fas fa-check" > </i > Эксклюзивные решения</li >
</ul >
</div >
</label >
</div >
</div >
<div class ="calculator-step" data-step ="4" >
<h4 class ="step-title" > Шаг 4 из 4: Ваши контакты</h4 >
<div class ="calculator-result" >
<div class ="result-card" >
<div class ="result-icon" >
<i class ="fas fa-check-circle" > </i >
</div >
<h5 > Предварительная стоимость</h5 >
<div class ="result-price" >
<span class ="price-value" id ="calculatedPrice" > 0</span >
<span class ="price-currency" > ₽</span >
</div >
<p class ="result-note" >
Точная стоимость будет рассчитана после выезда специалиста
</p >
</div >
</div >
<div class ="form-row" >
<div class ="form-group" >
<label for ="calc-name" > Ваше имя</label >
<input
type ="text"
id ="calc-name"
name ="name"
placeholder ="Введите имя"
required
>
</div >
<div class ="form-group" >
<label for ="calc-phone" > Телефон</label >
<input
type ="tel"
id ="calc-phone"
name ="phone"
placeholder ="+7 (___) ___-__-__"
required
>
</div >
</div >
<div class ="form-group" >
<label for ="calc-email" > Email (необязательно)</label >
<input
type ="email"
id ="calc-email"
name ="email"
placeholder ="your@email.com"
>
</div >
<div class ="form-checkbox" >
<input type ="checkbox" id ="calc-agreement" required >
<label for ="calc-agreement" >
Я согласен на обработку персональных данных
</label >
</div >
</div >
<div class ="calculator-navigation" >
<button type ="button" class ="btn btn-outline" id ="prevStep" style ="display: none;" >
<i class ="fas fa-arrow-left" > </i >
Назад
</button >
<div class ="calculator-progress" >
<div class ="progress-bar" >
<div class ="progress-fill" id ="progressFill" > </div >
</div >
<span class ="progress-text" > Шаг <span id ="currentStep" > 1</span > из 4</span >
</div >
<button type ="button" class ="btn btn-primary" id ="nextStep" >
Далее
<i class ="fas fa-arrow-right" > </i >
</button >
<button type ="submit" class ="btn btn-primary" id ="submitCalculator" style ="display: none;" >
Получить расчет
<i class ="fas fa-check" > </i >
</button >
</div >
</form >
</div >
</div >
</div >
<div class ="modal" id ="consultationModal" >
<div class ="modal-overlay" > </div >
<div class ="modal-container" >
<button class ="modal-close" >
<i class ="fas fa-times" > </i >
</button >
<div class ="modal-content" >
<div class ="modal-header" >
<div class ="modal-icon" >
<i class ="fas fa-comments" > </i >
</div >
<h3 > Бесплатная консультация</h3 >
<p > Задайте ваш вопрос, и наш специалист свяжется с вами</p >
</div >
<form class ="modal-form" id ="consultationForm" >
<div class ="form-group" >
<label for ="consult-name" > Ваше имя</label >
<input
type ="text"
id ="consult-name"
name ="name"
placeholder ="Введите ваше имя"
required
>
</div >
<div class ="form-group" >
<label for ="consult-phone" > Номер телефона</label >
<input
type ="tel"
id ="consult-phone"
name ="phone"
placeholder ="+7 (___) ___-__-__"
required
>
</div >
<div class ="form-group" >
<label for ="consult-question" > Ваш вопрос</label >
<textarea
id ="consult-question"
name ="question"
rows ="4"
placeholder ="Опишите, что вас интересует..."
required
> </textarea >
</div >
<div class ="form-checkbox" >
<input type ="checkbox" id ="consult-agreement" required >
<label for ="consult-agreement" >
Я согласен на обработку персональных данных
</label >
</div >
<button type ="submit" class ="btn btn-primary btn-block" >
<span > Отправить вопрос</span >
<i class ="fas fa-paper-plane" > </i >
</button >
</form >
</div >
</div >
</div >
<div class ="modal modal-success" id ="successModal" >
<div class ="modal-overlay" > </div >
<div class ="modal-container" >
<button class ="modal-close" >
<i class ="fas fa-times" > </i >
</button >
<div class ="modal-content" >
<div class ="success-animation" >
<div class ="success-checkmark" >
<div class ="check-icon" >
<span class ="icon-line line-tip" > </span >
<span class ="icon-line line-long" > </span >
<div class ="icon-circle" > </div >
<div class ="icon-fix" > </div >
</div >
</div >
</div >
<h3 > Спасибо за обращение!</h3 >
<p > Ваша заявка успешно отправлена. Мы свяжемся с вами в ближайшее время.</p >
<button class ="btn btn-primary" onclick ="closeModal('successModal')" >
Отлично
</button >
</div >
</div >
</div >
<div class ="modal modal-image" id ="imageModal" >
<div class ="modal-overlay" > </div >
<div class ="modal-container" >
<button class ="modal-close" >
<i class ="fas fa-times" > </i >
</button >
<button class ="modal-nav modal-prev" >
<i class ="fas fa-chevron-left" > </i >
</button >
<button class ="modal-nav modal-next" >
<i class ="fas fa-chevron-right" > </i >
</button >
<div class ="modal-image-content" >
<img src ="" alt ="" id ="modalImage" >
<div class ="modal-image-caption" id ="modalCaption" > </div >
</div >
</div >
</div >
CSS стили для модальных окон:
.modal {
position : fixed;
top : 0 ;
left : 0 ;
width : 100% ;
height : 100% ;
z-index : 9999 ;
display : none;
align-items : center;
justify-content : center;
padding : 20px ;
}
.modal .active {
display : flex;
}
.modal-overlay {
position : absolute;
top : 0 ;
left : 0 ;
width : 100% ;
height : 100% ;
background : rgba (0 , 0 , 0 , 0.7 );
backdrop-filter : blur (5px );
animation : fadeIn 0.3s ease;
}
.modal-container {
position : relative;
background : white;
border-radius : 20px ;
max-width : 500px ;
width : 100% ;
max-height : 90vh ;
overflow-y : auto;
box-shadow : 0 20px 60px rgba (0 , 0 , 0 , 0.3 );
animation : slideUp 0.4s ease;
z-index : 1 ;
}
.modal-large .modal-container {
max-width : 800px ;
}
.modal-close {
position : absolute;
top : 20px ;
right : 20px ;
width : 40px ;
height : 40px ;
background : #f8f9fa ;
border : none;
border-radius : 50% ;
cursor : pointer;
display : flex;
align-items : center;
justify-content : center;
font-size : 18px ;
color : var (--text-color);
transition : all 0.3s ease;
z-index : 10 ;
}
.modal-close :hover {
background : var (--primary-color);
color : white;
transform : rotate (90deg );
}
.modal-content {
padding : 40px ;
}
.modal-header {
text-align : center;
margin-bottom : 30px ;
}
.modal-icon {
width : 80px ;
height : 80px ;
background : linear-gradient (135deg , var (--primary-color), var (--secondary-color));
border-radius : 50% ;
display : flex;
align-items : center;
justify-content : center;
margin : 0 auto 20px ;
font-size : 32px ;
color : white;
}
.modal-header h3 {
font-size : 28px ;
font-weight : 700 ;
color : var (--text-color);
margin-bottom : 10px ;
}
.modal-header p {
font-size : 15px ;
color : #6c757d ;
line-height : 1.6 ;
}
.modal-form {
display : flex;
flex-direction : column;
gap : 20px ;
}
.form-group {
display : flex;
flex-direction : column;
}
.form-group label {
font-size : 14px ;
font-weight : 600 ;
color : var (--text-color);
margin-bottom : 8px ;
}
.form-group input ,
.form-group select ,
.form-group textarea {
padding : 12px 16px ;
border : 2px solid #e9ecef ;
border-radius : 10px ;
font-size : 15px ;
transition : all 0.3s ease;
font-family : inherit;
}
.form-group input :focus ,
.form-group select :focus ,
.form-group textarea :focus {
outline : none;
border-color : var (--primary-color);
box-shadow : 0 0 0 3px rgba (255 , 107 , 107 , 0.1 );
}
.form-group textarea {
resize : vertical;
min-height : 100px ;
}
.form-row {
display : grid;
grid-template-columns : 1 fr 1 fr;
gap : 15px ;
}
.form-checkbox {
display : flex;
align-items : flex-start;
gap : 10px ;
}
.form-checkbox input [type="checkbox" ] {
width : 20px ;
height : 20px ;
margin-top : 2px ;
cursor : pointer;
accent-color : var (--primary-color);
}
.form-checkbox label {
font-size : 13px ;
color : #6c757d ;
line-height : 1.5 ;
cursor : pointer;
}
.form-checkbox label a {
color : var (--primary-color);
text-decoration : underline;
}
.modal-footer {
margin-top : 30px ;
padding-top : 20px ;
border-top : 1px solid #e9ecef ;
text-align : center;
}
.modal-footer p {
font-size : 14px ;
color : #6c757d ;
margin-bottom : 10px ;
}
.modal-phone {
display : inline-flex;
align-items : center;
gap : 10px ;
font-size : 20px ;
font-weight : 700 ;
color : var (--primary-color);
text-decoration : none;
transition : all 0.3s ease;
}
.modal-phone :hover {
transform : scale (1.05 );
}
.calculator-step {
display : none;
}
.calculator-step .active {
display : block;
animation : fadeIn 0.3s ease;
}
.step-title {
font-size : 18px ;
font-weight : 600 ;
color : var (--text-color);
margin-bottom : 25px ;
text-align : center;
}
.calculator-options {
display : grid;
grid-template-columns : repeat (2 , 1 fr);
gap : 15px ;
}
.calculator-options .vertical {
grid-template-columns : 1 fr;
}
.calculator-option {
cursor : pointer;
}
.calculator-option input [type="radio" ] {
display : none;
}
.option-card {
padding : 25px ;
background : #f8f9fa ;
border : 2px solid #e9ecef ;
border-radius : 12px ;
text-align : center;
transition : all 0.3s ease;
cursor : pointer;
}
.option-card i {
font-size : 32px ;
color : var (--primary-color);
margin-bottom : 10px ;
display : block;
}
.option-card span {
font-size : 15px ;
font-weight : 600 ;
color : var (--text-color);
}
.option-card .small {
padding : 15px ;
}
.option-card .small span {
font-size : 14px ;
}
.calculator-option input :checked + .option-card {
background : linear-gradient (135deg , var (--primary-color), var (--secondary-color));
border-color : transparent;
color : white;
transform : scale (1.05 );
}
.calculator-option input :checked + .option-card i ,
.calculator-option input :checked + .option-card span {
color : white;
}
.option-card :hover {
border-color : var (--primary-color);
transform : translateY (-3px );
}
.option-card .detailed {
text-align : left;
padding : 20px ;
}
.option-header {
display : flex;
align-items : center;
gap : 15px ;
margin-bottom : 15px ;
}
.option-header i {
font-size : 28px ;
margin : 0 ;
}
.option-header strong {
display : block;
font-size : 16px ;
margin-bottom : 5px ;
}
.option-price {
font-size : 14px ;
color : var (--primary-color);
font-weight : 600 ;
}
.calculator-option input :checked + .option-card .detailed .option-price {
color : white;
}
.option-features {
list-style : none;
padding : 0 ;
margin : 0 ;
}
.option-features li {
display : flex;
align-items : center;
gap : 10px ;
font-size : 13px ;
color : #6c757d ;
margin-bottom : 8px ;
}
.option-features i {
font-size : 12px ;
color : var (--primary-color);
margin : 0 ;
}
.calculator-option input :checked + .option-card .detailed .option-features li ,
.calculator-option input :checked + .option-card .detailed .option-features i {
color : white;
}
.range-slider {
margin-top : 15px ;
}
.range-slider input [type="range" ] {
width : 100% ;
height : 6px ;
background : #e9ecef ;
border-radius : 3px ;
outline : none;
-webkit-appearance : none;
}
.range-slider input [type="range" ] ::-webkit-slider-thumb {
-webkit-appearance : none;
appearance : none;
width : 20px ;
height : 20px ;
background : var (--primary-color);
border-radius : 50% ;
cursor : pointer;
transition : all 0.3s ease;
}
.range-slider input [type="range" ] ::-webkit-slider-thumb:hover {
transform : scale (1.2 );
}
.range-slider input [type="range" ] ::-moz-range-thumb {
width : 20px ;
height : 20px ;
background : var (--primary-color);
border-radius : 50% ;
cursor : pointer;
border : none;
}
.range-labels {
display : flex;
justify-content : space-between;
margin-top : 8px ;
font-size : 12px ;
color : #6c757d ;
}
.calculator-result {
margin-bottom : 30px ;
}
.result-card {
background : linear-gradient (135deg , var (--primary-color), var (--secondary-color));
padding : 30px ;
border-radius : 15px ;
text-align : center;
color : white;
}
.result-icon {
width : 60px ;
height : 60px ;
background : rgba (255 , 255 , 255 , 0.2 );
border-radius : 50% ;
display : flex;
align-items : center;
justify-content : center;
margin : 0 auto 15px ;
font-size : 28px ;
}
.result-card h5 {
font-size : 16px ;
font-weight : 600 ;
margin-bottom : 15px ;
opacity : 0.9 ;
}
.result-price {
display : flex;
align-items : center;
justify-content : center;
gap : 5px ;
margin-bottom : 15px ;
}
.price-value {
font-size : 48px ;
font-weight : 700 ;
line-height : 1 ;
}
.price-currency {
font-size : 24px ;
font-weight : 600 ;
}
.result-note {
font-size : 13px ;
opacity : 0.8 ;
margin : 0 ;
}
.calculator-navigation {
display : flex;
align-items : center;
gap : 15px ;
margin-top : 30px ;
padding-top : 20px ;
border-top : 1px solid #e9ecef ;
}
.calculator-progress {
flex : 1 ;
text-align : center;
}
.progress-bar {
height : 6px ;
background : #e9ecef ;
border-radius : 3px ;
overflow : hidden;
margin-bottom : 8px ;
}
.progress-fill {
height : 100% ;
background : linear-gradient (90deg , var (--primary-color), var (--secondary-color));
width : 25% ;
transition : width 0.3s ease;
}
.progress-text {
font-size : 13px ;
color : #6c757d ;
}
.calculator-navigation .btn {
padding : 12px 24px ;
white-space : nowrap;
}
.modal-success .modal-content {
text-align : center;
padding : 50px 40px ;
}
.success-animation {
margin-bottom : 30px ;
}
.success-checkmark {
width : 80px ;
height : 80px ;
margin : 0 auto;
position : relative;
}
.check-icon {
width : 80px ;
height : 80px ;
position : relative;
border-radius : 50% ;
box-sizing : content-box;
border : 4px solid #4CAF50 ;
}
.icon-line {
height : 5px ;
background-color : #4CAF50 ;
display : block;
border-radius : 2px ;
position : absolute;
z-index : 10 ;
}
.icon-line .line-tip {
top : 46px ;
left : 14px ;
width : 25px ;
transform : rotate (45deg );
animation : checkTip 0.75s ;
}
.icon-line .line-long {
top : 38px ;
right : 8px ;
width : 47px ;
transform : rotate (-45deg );
animation : checkLong 0.75s ;
}
.icon-circle {
top : -4px ;
left : -4px ;
z-index : 10 ;
width : 80px ;
height : 80px ;
border-radius : 50% ;
position : absolute;
box-sizing : content-box;
border : 4px solid rgba (76 , 175 , 80 , 0.2 );
}
.icon-fix {
top : 8px ;
width : 5px ;
left : 26px ;
z-index : 1 ;
height : 85px ;
position : absolute;
transform : rotate (-45deg );
background-color : white;
}
.modal-success h3 {
font-size : 28px ;
font-weight : 700 ;
color : var (--text-color);
margin-bottom : 15px ;
}
.modal-success p {
font-size : 15px ;
color : #6c757d ;
margin-bottom : 30px ;
line-height : 1.6 ;
}
.modal-image .modal-container {
max-width : 90vw ;
max-height : 90vh ;
background : transparent;
box-shadow : none;
}
.modal-image-content {
position : relative;
}
.modal-image-content img {
width : 100% ;
height : auto;
max-height : 80vh ;
object-fit : contain;
border-radius : 10px ;
}
.modal-image-caption {
background : rgba (0 , 0 , 0 , 0.8 );
color : white;
padding : 15px 20px ;
border-radius : 0 0 10px 10px ;
text-align : center;
font-size : 14px ;
}
.modal-nav {
position : absolute;
top : 50% ;
transform : translateY (-50% );
width : 50px ;
height : 50px ;
background : rgba (255 , 255 , 255 , 0.9 );
border : none;
border-radius : 50% ;
cursor : pointer;
display : flex;
align-items : center;
justify-content : center;
font-size : 20px ;
color : var (--text-color);
transition : all 0.3s ease;
z-index : 10 ;
}
.modal-nav :hover {
background : white;
transform : translateY (-50% ) scale (1.1 );
}
.modal-prev {
left : 20px ;
}
.modal-next {
right : 20px ;
}
@keyframes fadeIn {
from {
opacity : 0 ;
}
to {
opacity : 1 ;
}
}
@keyframes slideUp {
from {
opacity : 0 ;
transform : translateY (30px );
}
to {
opacity : 1 ;
transform : translateY (0 );
}
}
@keyframes checkTip {
0% {
width : 0 ;
left : 1px ;
top : 19px ;
}
54% {
width : 0 ;
left : 1px ;
top : 19px ;
}
70% {
width : 50px ;
left : -8px ;
top : 37px ;
}
84% {
width : 17px ;
left : 21px ;
top : 48px ;
}
100% {
width : 25px ;
left : 14px ;
top : 46px ;
}
}
@keyframes checkLong {
0% {
width : 0 ;
right : 46px ;
top : 54px ;
}
65% {
width : 0 ;
right : 46px ;
top : 54px ;
}
84% {
width : 55px ;
right : 0 ;
top : 35px ;
}
100% {
width : 47px ;
right : 8px ;
top : 38px ;
}
}
@media (max-width : 768px ) {
.modal-container {
max-width : 100% ;
border-radius : 20px 20px 0 0 ;
max-height : 95vh ;
}
.modal-content {
padding : 30px 20px ;
}
.modal-header h3 {
font-size : 24px ;
}
.modal-icon {
width : 60px ;
height : 60px ;
font-size : 24px ;
}
.calculator-options {
grid-template-columns : 1 fr;
}
.form-row {
grid-template-columns : 1 fr;
}
.calculator-navigation {
flex-wrap : wrap;
}
.calculator-navigation .btn {
flex : 1 ;
min-width : 120px ;
}
.calculator-progress {
order : -1 ;
width : 100% ;
margin-bottom : 15px ;
}
.price-value {
font-size : 36px ;
}
.modal-nav {
width : 40px ;
height : 40px ;
font-size : 16px ;
}
.modal-prev {
left : 10px ;
}
.modal-next {
right : 10px ;
}
}
@media (max-width : 480px ) {
.modal {
padding : 0 ;
}
.modal-container {
border-radius : 0 ;
max-height : 100vh ;
}
.option-card {
padding : 20px 15px ;
}
.option-card i {
font-size : 24px ;
}
}
JavaScript для модальных окон:
class ModalManager {
constructor () {
this .modals = document .querySelectorAll ('.modal' );
this .init ();
}
init () {
this .modals .forEach (modal => {
const overlay = modal.querySelector ('.modal-overlay' );
const closeBtn = modal.querySelector ('.modal-close' );
overlay?.addEventListener ('click' , () => this .close (modal));
closeBtn?.addEventListener ('click' , () => this .close (modal));
});
document .addEventListener ('keydown' , (e ) => {
if (e.key === 'Escape' ) {
this .closeAll ();
}
});
document .querySelectorAll ('[data-modal]' ).forEach (trigger => {
trigger.addEventListener ('click' , (e ) => {
e.preventDefault ();
const modalId = trigger.dataset .modal ;
this .open (modalId);
});
});
}
open (modalId ) {
const modal = document .getElementById (modalId + 'Modal' );
if (modal) {
modal.classList .add ('active' );
document .body .style .overflow = 'hidden' ;
}
}
close (modal ) {
if (typeof modal === 'string' ) {
modal = document .getElementById (modal + 'Modal' );
}
if (modal) {
modal.classList .remove ('active' );
document .body .style .overflow = '' ;
}
}
closeAll () {
this .modals .forEach (modal => this .close (modal));
}
}
function openModal (modalId ) {
window .modalManager .open (modalId);
}
function closeModal (modalId ) {
window .modalManager .close (modalId);
}
class Calculator {
constructor () {
this .form = document .getElementById ('calculatorForm' );
this .currentStep = 1 ;
this .totalSteps = 4 ;
this .formData = {};
this .prices = {
cosmetic : 2500 ,
major : 4500 ,
designer : 7000
};
if (this .form ) {
this .init ();
}
}
init () {
const nextBtn = document .getElementById ('nextStep' );
const prevBtn = document .getElementById ('prevStep' );
const submitBtn = document .getElementById ('submitCalculator' );
nextBtn?.addEventListener ('click' , () => this .nextStep ());
prevBtn?.addEventListener ('click' , () => this .prevStep ());
const areaInput = document .getElementById ('calc-area' );
const areaRange = document .getElementById ('calc-area-range' );
areaInput?.addEventListener ('input' , (e ) => {
areaRange.value = e.target .value ;
});
areaRange?.addEventListener ('input' , (e ) => {
areaInput.value = e.target .value ;
});
this .form .addEventListener ('submit' , (e ) => this .handleSubmit (e));
}
nextStep () {
if (this .validateStep (this .currentStep )) {
this .saveStepData (this .currentStep );
if (this .currentStep < this .totalSteps ) {
this .currentStep ++;
this .updateStep ();
if (this .currentStep === this .totalSteps ) {
this .calculatePrice ();
}
}
}
}
prevStep () {
if (this .currentStep > 1 ) {
this .currentStep --;
this .updateStep ();
}
}
updateStep () {
document .querySelectorAll ('.calculator-step' ).forEach (step => {
step.classList .remove ('active' );
});
const currentStepEl = document .querySelector (`[data-step="${this .currentStep} "]` );
currentStepEl?.classList .add ('active' );
const progress = (this .currentStep / this .totalSteps ) * 100 ;
document .getElementById ('progressFill' ).style .width = progress + '%' ;
document .getElementById ('currentStep' ).textContent = this .currentStep ;
const prevBtn = document .getElementById ('prevStep' );
const nextBtn = document .getElementById ('nextStep' );
const submitBtn = document .getElementById ('submitCalculator' );
prevBtn.style .display = this .currentStep === 1 ? 'none' : 'flex' ;
nextBtn.style .display = this .currentStep === this .totalSteps ? 'none' : 'flex' ;
submitBtn.style .display = this .currentStep === this .totalSteps ? 'flex' : 'none' ;
}
validateStep (step ) {
const stepEl = document .querySelector (`[data-step="${step} "]` );
const requiredInputs = stepEl.querySelectorAll ('[required]' );
let isValid = true ;
requiredInputs.forEach (input => {
if (input.type === 'radio' ) {
const radioGroup = stepEl.querySelectorAll (`[name="${input.name} "]` );
const isChecked = Array .from (radioGroup).some (radio => radio.checked );
if (!isChecked) {
isValid = false ;
this .showError ('Пожалуйста, выберите один из вариантов' );
}
} else if (!input.value .trim ()) {
isValid = false ;
input.style .borderColor = '#dc3545' ;
setTimeout (() => {
input.style .borderColor = '' ;
}, 2000 );
}
});
return isValid;
}
saveStepData (step ) {
const stepEl = document .querySelector (`[data-step="${step} "]` );
const inputs = stepEl.querySelectorAll ('input, select, textarea' );
inputs.forEach (input => {
if (input.type === 'radio' ) {
if (input.checked ) {
this .formData [input.name ] = input.value ;
}
} else {
this .formData [input.name ] = input.value ;
}
});
}
calculatePrice () {
const area = parseFloat (this .formData .area ) || 50 ;
const repairType = this .formData ['repair-type' ] || 'cosmetic' ;
const pricePerSqm = this .prices [repairType] || 2500 ;
const totalPrice = area * pricePerSqm;
this .animateValue ('calculatedPrice' , 0 , totalPrice, 1000 );
}
animateValue (id, start, end, duration ) {
const element = document .getElementById (id);
const range = end - start;
const increment = range / (duration / 16 );
let current = start;
const timer = setInterval (() => {
current += increment;
if (current >= end) {
current = end;
clearInterval (timer);
}
element.textContent = Math .floor (current).toLocaleString ('ru-RU' );
}, 16 );
}
async handleSubmit (e ) {
e.preventDefault ();
if (!this .validateStep (this .currentStep )) {
return ;
}
this .saveStepData (this .currentStep );
const submitBtn = document .getElementById ('submitCalculator' );
const originalText = submitBtn.innerHTML ;
submitBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Отправка...' ;
submitBtn.disabled = true ;
try {
await this .sendData (this .formData );
closeModal ('calculator' );
openModal ('success' );
this .form .reset ();
this .currentStep = 1 ;
this .formData = {};
this .updateStep ();
} catch (error) {
this .showError ('Произошла ошибка при отправке. Попробуйте еще раз.' );
} finally {
submitBtn.innerHTML = originalText;
submitBtn.disabled = false ;
}
}
async sendData (data ) {
return new Promise ((resolve ) => {
setTimeout (() => {
console .log ('Отправленные данные:' , data);
resolve ();
}, 1500 );
});
}
showError (message ) {
alert (message);
}
}
class FormHandler {
constructor () {
this .forms = {
callback : document .getElementById ('callbackForm' ),
consultation : document .getElementById ('consultationForm' )
};
this .init ();
}
init () {
Object .entries (this .forms ).forEach (([name, form] ) => {
if (form) {
form.addEventListener ('submit' , (e ) => this .handleSubmit (e, name));
}
});
this .initPhoneMask ();
}
initPhoneMask () {
const phoneInputs = document .querySelectorAll ('input[type="tel"]' );
phoneInputs.forEach (input => {
input.addEventListener ('input' , (e ) => {
let value = e.target .value .replace (/\D/g , '' );
if (value.length > 0 ) {
if (value[0 ] === '7' ) {
value = value.substring (1 );
}
let formatted = '+7' ;
if (value.length > 0 ) {
formatted += ' (' + value.substring (0 , 3 );
}
if (value.length >= 4 ) {
formatted += ') ' + value.substring (3 , 6 );
}
if (value.length >= 7 ) {
formatted += '-' + value.substring (6 , 8 );
}
if (value.length >= 9 ) {
formatted += '-' + value.substring (8 , 10 );
}
e.target .value = formatted;
}
});
});
}
async handleSubmit (e, formName ) {
e.preventDefault ();
const form = e.target ;
const submitBtn = form.querySelector ('button[type="submit"]' );
const originalText = submitBtn.innerHTML ;
submitBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Отправка...' ;
submitBtn.disabled = true ;
const formData = new FormData (form);
const data = Object .fromEntries (formData);
try {
await this .sendForm (data, formName);
const modalId = form.closest ('.modal' ).id .replace ('Modal' , '' );
closeModal (modalId);
openModal ('success' );
form.reset ();
} catch (error) {
alert ('Произошла ошибка при отправке. Попробуйте еще раз.' );
} finally {
submitBtn.innerHTML = originalText;
submitBtn.disabled = false ;
}
}
async sendForm (data, formName ) {
return new Promise ((resolve ) => {
setTimeout (() => {
console .log (`Форма ${formName} :` , data);
resolve ();
}, 1500 );
});
}
}
class ImageGallery {
constructor () {
this .modal = document .getElementById ('imageModal' );
this .modalImage = document .getElementById ('modalImage' );
this .modalCaption = document .getElementById ('modalCaption' );
this .images = [];
this .currentIndex = 0 ;
if (this .modal ) {
this .init ();
}
}
init () {
document .querySelectorAll ('[data-gallery]' ).forEach ((img, index ) => {
this .images .push ({
src : img.src ,
alt : img.alt ,
caption : img.dataset .caption || img.alt
});
img.style .cursor = 'pointer' ;
img.addEventListener ('click' , () => this .open (index));
});
const prevBtn = this .modal .querySelector ('.modal-prev' );
const nextBtn = this .modal .querySelector ('.modal-next' );
prevBtn?.addEventListener ('click' , () => this .prev ());
nextBtn?.addEventListener ('click' , () => this .next ());
document .addEventListener ('keydown' , (e ) => {
if (this .modal .classList .contains ('active' )) {
if (e.key === 'ArrowLeft' ) this .prev ();
if (e.key === 'ArrowRight' ) this .next ();
}
});
}
open (index ) {
this .currentIndex = index;
this .updateImage ();
this .modal .classList .add ('active' );
document .body .style .overflow = 'hidden' ;
}
updateImage () {
const image = this .images [this .currentIndex ];
this .modalImage .src = image.src ;
this .modalImage .alt = image.alt ;
this .modalCaption .textContent = image.caption ;
}
next () {
this .currentIndex = (this .currentIndex + 1 ) % this .images .length ;
this .updateImage ();
}
prev () {
this .currentIndex = (this .currentIndex - 1 + this .images .length ) % this .images .length ;
this .updateImage ();
}
}
document .addEventListener ('DOMContentLoaded' , () => {
window .modalManager = new ModalManager ();
new Calculator ();
new FormHandler ();
new ImageGallery ();
});