<!--КАЛЬКУЛЯТОР СТОИМОСТИ ТЕННИСИТА-->
<div id="tc-app" class="tc-app t-text">
<label class="tc-label">Вид необходимых работ</label>
<div class="tc-segmented">
<button class="tc-seg-btn active" data-mode="repair" type="button">Ремонт</button>
<button class="tc-seg-btn" data-mode="construction" type="button">Строительство</button>
</div>
<form class="tc-form" id="tc-form">
<div class="tc-field">
<label class="tc-label">Материал</label>
<div class="tc-products" id="tc-products"></div>
</div>
<div class="tc-field">
<label class="tc-label">Площадь корта</label>
<div class="tc-input-row">
<div class="tc-input-row-section">
<input type="number" id="tc-area" class="tc-input" value="648" min="1"> <span class="tc-suffix">м²</span> </div>
<button type="button" class="tc-chip" id="tc-preset">Стандарт: 18×36 м = 648 м²</button>
</div>
</div>
<div class="tc-field tc-conditional" id="tc-thickness-field" hidden>
<label class="tc-label">Толщина слоя после укатки</label>
<div class="tc-slider-wrap">
<input type="range" id="tc-thickness" class="tc-slider" min="30" max="70" value="45" step="5">
<div class="tc-slider-fill"></div>
<output class="tc-slider-val" for="tc-thickness">45 мм</output>
</div>
<p class="tc-hint" id="tc-thickness-hint">Неуплотнённый: <strong>70 мм</strong> → укатка: <strong>50 мм</strong></p>
<p class="tc-hint"> (45 мм - стандартный слой укладки)</p>
</div>
<div class="tc-field">
<label class="tc-label">Фасовка <span class="tc-tooltip-wrapper">
<span class="tc-tooltip-icon">?</span>
<div class="tc-tooltip-content"> <strong>Мешок 40 кг (ЛМТ СТРОЙ)</strong>
<br> • Меньше мешков — быстрее укладка
<br> • Оптимальный вес для профессиональной бригады
<br> • Компактное хранение
<br> • На одном поддоне помещается больше тоннажа
<br> • Классическая фасовка 40 кг гарантирует сохранность фракции и проверена десятилетиями практики.
<br> • Оптимальная нагрузка для равномерного рассева и комфортной работы бригады. </div>
</span>
</label>
<div class="tc-toggle">
<button type="button" class="tc-toggle-btn" data-packaging="bags">Мешки 40 кг</button>
<button type="button" class="tc-toggle-btn active" data-packaging="bulk">Тонны</button>
</div>
</div>
<div class="tc-field">
<label class="tc-switch">
<input type="checkbox" id="tc-superior" class="tc-switch-input"> <span class="tc-switch-track"><span class="tc-switch-thumb"></span></span> <span class="tc-switch-text">
Добавить финишный слой <strong>Extra</strong>
<small>Для профессионального покрытия</small>
</span> </label>
</div>
</form>
<section class="tc-results" id="tc-results">
<div class="tc-results-head">
<h3 class="tc-results-title" id="tc-results-title">Результаты</h3>
<button class="tc-reset" id="tc-reset" type="button">↺ Сброс</button>
</div>
<div class="tc-cards" id="tc-main-cards"></div>
<div class="tc-superior-block" id="tc-superior-block" hidden> <span class="tc-superior-badge">Финишный слой</span>
<div class="tc-cards" id="tc-superior-cards"></div>
</div>
<div class="tc-total" id="tc-total">
<div> <span class="tc-total-label">Итого</span> <span class="tc-total-note">включая все материалы</span> </div> <span class="tc-total-amount" id="tc-total-amount">0 ₽</span> </div> <a href="#popup:calc-form" class="tc-cta" id="tc-cta">Оформить заказ</a>
<div class="tc-opt-message" id="tc-opt-message" hidden>
<p><strong>Специальные условия при покупке от 50 тонн!</strong></p>
<p>Купив такой объем Вы можете стать нашим дилером и получить дополнительную скидку.</p>
</div>
</section>
<div class="tc-empty" id="tc-empty" hidden>
<p>Выберите материал и укажите площадь</p>
</div>
</div>
<style>
.tc-app {
--tc-yellow: #f9a825;
--tc-yellow-hover: #e6991b;
--tc-green: #333333;
--tc-bg: #F5F5F7;
--tc-card: #FFFFFF;
--tc-text: #000000;
--tc-text-secondary: #000;
--tc-border: #E0E0E0;
--tc-shadow: 0 4px 24px rgba(0, 0, 0, 0.08);
--tc-shadow-hover: 0 8px 32px rgba(0, 0, 0, 0.12);
--tc-radius: 16px;
--tc-radius-sm: 12px;
background: var(--tc-bg);
border-radius: var(--tc-radius);
padding: 24px;
margin: 0 auto;
color: var(--tc-text);
box-sizing: border-box;
background-color: #ecf1f7;
box-shadow: 0 0 10px 0 rgb(0 0 0 / 25%);
}
.tc-app *, .tc-app *::before, .tc-app *::after {
box-sizing: inherit;
margin: 0;
padding: 0;
}
.tc-segmented {
display: flex;
background: #fff;
padding: 2px;
border-radius: var(--tc-radius-sm);
margin-bottom: 24px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
position: relative;
width: 50%;
}
.tc-segmented:before {
content: "";
width: 50%;
height: 100%;
position: absolute;
left: 0;
top: 0;
background-color: var(--tc-yellow);
border-radius: inherit;
transition: all 0.4s ease-in-out;
}
/*ЦВЕТ ПЛАШЕК ДЛЯ МАТЕРИАЛОВ*/
button.tc-product:nth-child(1) .tc-product-tag {
background-color: #22C55E; /*CLASSIC*/
}
button.tc-product:nth-child(2) .tc-product-tag {
background-color: #ff8562; /*EXPERT*/
}
button.tc-product:nth-child(3) .tc-product-tag {
background-color: #8770F9; /*PREMIUM*/
}
button.tc-product:nth-child(4) .tc-product-tag {
background-color: #FF20FB; /*EXTRA*/
}
.tc-segmented:has(button.tc-seg-btn.active[data-mode="construction"]):before {
left: 50%;
}
.tc-toggle {
display: flex;
gap: 0;
background-color: #fff;
border-radius: 12px;
padding: 2px;
width: 30%;
position: relative;
}
.tc-toggle:before {
content: "";
width: 50%;
height: 100%;
position: absolute;
left: 0;
top: 0;
background-color: var(--tc-yellow);
border-radius: inherit;
transition: all 0.4s ease-in-out;
}
.tc-toggle:has(button.tc-toggle-btn.active[data-packaging="bulk"]):before {
left: 50%;
}
.tc-seg-btn, .tc-toggle-btn {
position: relative;
flex: 1;
padding: 10px 12px;
border: none;
background: transparent;
font-size: 13px;
font-weight: 500;
color: var(--tc-text-secondary);
border-radius: var(--tc-radius-sm);
cursor: pointer;
transition: all 0.2s ease;
z-index: 2;
}
.tc-seg-btn.active, .tc-toggle-btn.active {
color: #000;
font-weight: 600;
}
.tc-toggle-btn.disabled {
opacity: 0.4;
pointer-events: none;
cursor: not-allowed;
}
.tc-form {
margin-bottom: 24px;
}
.tc-field {
margin-bottom: 20px;
}
.tc-label {
display: block;
font-size: 14px;
font-weight: 400;
color: #000;
margin-bottom: 8px;
}
.tc-input-row {
display: flex;
align-items: center;
gap: 20px;
}
.tc-input {
flex: 1;
padding: 14px 16px;
border: 1px solid var(--tc-border);
border-radius: var(--tc-radius-sm);
font-size: 16px;
font-weight: 500;
background: #fff;
transition: border-color 0.2s;
}
.tc-input:focus {
outline: none;
border-color: var(--tc-yellow);
}
.tc-suffix {
color: var(--tc-text-secondary);
font-weight: 500;
margin-left: -30px;
}
.tc-chip {
color: #000;
font-size: 13px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
background: no-repeat;
border: 1px solid #b2b2b2;
border-radius: 12px;
padding: 15px 10px;
}
.tc-hint {
font-size: 12px;
color: var(--tc-text-secondary);
margin-top: 6px;
font-weight: 400;
display: inline-block;
}
.tc-hint strong {
color: var(--tc-text);
}
.tc-slider-wrap {
position: relative;
margin: 8px 0 6px;
height: 24px;
display: flex;
align-items: center;
}
.tc-slider {
width: 100%;
height: 6px;
border-radius: 3px;
background: var(--tc-border);
appearance: none;
cursor: pointer;
position: relative;
z-index: 3;
}
.tc-slider::-webkit-slider-thumb {
appearance: none;
width: 22px;
height: 22px;
border-radius: 50%;
background: var(--tc-yellow);
border: 3px solid #fff;
box-shadow: var(--tc-shadow);
cursor: pointer;
z-index: 4;
}
.tc-slider-fill {
position: absolute;
top: 50%;
left: 0;
height: 6px;
background: var(--tc-yellow);
border-radius: 3px;
transform: translateY(-50%);
z-index: 5;
pointer-events: none;
}
.tc-slider-val {
position: absolute;
right: 0;
top: -26px;
font-size: 14px;
font-weight: 700;
color: var(--tc-green);
background: #fff;
padding: 4px 10px;
border-radius: 8px;
box-shadow: var(--tc-shadow);
}
.tc-products {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
gap: 12px;
}
.tc-product {
padding: 14px;
border: 1px solid var(--tc-border);
border-radius: var(--tc-radius-sm);
background: #fff;
cursor: pointer;
transition: all 0.2s;
text-align: left;
position: relative;
}
.tc-product:hover {
border-color: var(--tc-yellow);
transform: scale(1.03);
box-shadow: var(--tc-shadow);
}
.tc-product.selected {
border-color: var(--tc-yellow);
background: linear-gradient(135deg, #FFF9E6 0%, #fff 100%);
box-shadow: 0 0 0 1px var(--tc-yellow), var(--tc-shadow);
}
.tc-product-tag {
display: inline-block;
padding: 3px 8px;
border-radius: 4px;
font-size: 10px;
font-weight: 700;
text-transform: uppercase;
margin-bottom: 8px;
background: #ff8562;
color: #fff;
position: absolute;
right: 0;
top: 0;
border-radius: 0 12px 0 4px;
}
button.tc-product {
display: flex;
flex-direction: column;
}
.tc-product-name {
font-size: 14px;
font-weight: 700;
margin-bottom: 6px;
line-height: 1.3;
color: #000;
}
.tc-product-desc {
font-size: 11px;
color: var(--tc-text-secondary);
margin-bottom: 10px;
line-height: 1.4;
}
.tc-product-price {
font-size: 13px;
font-weight: 600;
color: var(--tc-green);
margin-top: auto;
}
.tc-product-opt {
font-size: 10px;
color: #22C55E;
font-weight: 700;
margin-left: 4px;
background: #ECFDF5;
padding: 2px 6px;
border-radius: 4px;
}
.tc-switch {
display: flex;
align-items: center;
gap: 20px;
cursor: pointer;
flex-direction: row-reverse;
width: max-content;
}
.tc-switch-input {
display: none;
}
.tc-switch-track {
width: 36px;
height: 50px;
background: var(--tc-border);
border-radius: 3px;
position: relative;
transition: background 0.2s;
flex-shrink: 0;
transform: rotate(-90deg);
}
.tc-switch-thumb {
position: absolute;
top: 0px;
left: 0px;
width: 17px;
height: 50px;
background: #fff;
border-radius: 2px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
transition: transform 0.2s;
}
.tc-switch-input:checked + .tc-switch-track {
background: var(--tc-yellow);
}
.tc-switch-input:checked + .tc-switch-track .tc-switch-thumb {
transform: translateX(20px);
}
.tc-switch-text {
font-size: 14px;
color: var(--tc-text);
}
.tc-switch-text strong {
color: var(--tc-green);
}
.tc-switch-text small {
display: block;
font-size: 12px;
color: var(--tc-text-secondary);
}
.tc-results {
background: var(--tc-card);
border-radius: var(--tc-radius);
padding: 20px;
box-shadow: var(--tc-shadow);
margin-bottom: 24px;
animation: tc-fadeIn 0.25s ease;
display: flex;
flex-wrap: wrap;
gap: 0px;
}
.tc-results>div {
width: 100%;
}
.modal-summary {
background-color: #f3f3f3;
padding: 5px 5px;
margin-top: 12px;
border-radius: 5px;
display: flex;
flex-direction: column;
gap: 5px;
}
.modal-summary>p {
display: flex;
justify-content: space-between;
background-color: #ffffff;
padding: 3px 6px;
}
@keyframes tc-fadeIn {
from {
opacity: 0;
transform: translateY(8px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.tc-results-head {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
padding-bottom: 12px;
border-bottom: 1px solid var(--tc-border);
}
.tc-results-title {
font-size: 16px;
font-weight: 700;
}
.tc-reset {
padding: 6px 12px;
background: var(--tc-bg);
border: none;
border-radius: 8px;
font-size: 12px;
font-weight: 500;
color: var(--tc-text-secondary);
cursor: pointer;
}
.tc-reset:hover {
background: var(--tc-border);
}
.tc-cards {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(130px, 1fr));
gap: 12px;
margin-bottom: 16px;
}
.tc-card {
padding: 14px;
border-radius: var(--tc-radius-sm);
background: var(--tc-bg);
border: 1px solid var(--tc-border);
text-align: center;
position: relative;
}
.tc-card.primary {
background: linear-gradient(135deg, #FFF9E6 0%, #fff 100%);
border-color: var(--tc-yellow);
}
.tc-card.sup {
background: linear-gradient(135deg, rgb(255 133 98 / 38%) 0%, #fff 100%);
border-color: #ff8562;
}
.tc-card.accent {
background: linear-gradient(135deg, #ECFDF5 0%, #fff 100%);
border-color: #22C55E;
}
.tc-card-value {
font-size: 20px;
font-weight: 800;
color: var(--tc-text);
margin-bottom: 4px;
}
.tc-card.primary .tc-card-value, .tc-card.accent .tc-card-value {
color: var(--tc-green);
}
.tc-card-label {
font-size: 11px;
color: var(--tc-text-secondary);
line-height: 1.4;
}
.tc-opt-badge {
position: absolute;
top: 8px;
right: 8px;
padding: 3px 8px;
background: #22C55E;
color: #fff;
font-size: 9px;
font-weight: 700;
border-radius: 4px;
}
.tc-superior-block {
margin: 16px 0;
padding-top: 16px;
border-top: 2px dashed var(--tc-border);
}
.tc-superior-badge {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 4px 10px;
background: linear-gradient(135deg, #ff8562, #f9a825);
color: #fff;
font-size: 11px;
font-weight: 700;
border-radius: 6px;
margin-bottom: 12px;
}
.tc-total {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 20px;
background: var(--tc-green);
border-radius: var(--tc-radius-sm);
margin-bottom: 16px;
color: #fff;
height: 70px;
flex-grow: 1;
flex-basis: 50%;
}
.tc-total-label {
font-size: 14px;
font-weight: 500;
opacity: 1;
}
.tc-total-note {
font-size: 12px;
opacity: 1;
display: block;
}
.tc-total-amount {
font-size: 28px;
font-weight: 800;
}
.tc-cta {
width: 300px;
display: flex;
align-items: center;
justify-content: center;
padding: 10px;
background: var(--tc-yellow);
color: #000 !important;
border: none;
border-radius: var(--tc-radius-sm);
font-size: 16px;
font-weight: 700;
cursor: pointer;
transition: all 0.2s;
text-decoration: none;
text-align: center;
height: 70px;
margin-left: 10px;
}
.tc-cta:hover {
background: var(--tc-yellow-hover);
box-shadow: var(--tc-shadow-hover);
transform: scale(1.03);
}
.tc-empty {
text-align: center;
padding: 40px 20px;
color: var(--tc-text-secondary);
}
[hidden] {
display: none !important;
}
.tc-conditional {
animation: tc-slideDown 0.25s ease;
}
.uc-products-table, .uc-superior-table, .uc-constants-table {
display: none;
}
@keyframes tc-slideDown {
from {
opacity: 0;
transform: translateY(-8px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* Тултип для фасовки */
.tc-tooltip-wrapper {
position: relative;
display: none;
align-items: center;
margin-left: 2px;
vertical-align: middle;
cursor: help;
}
.tc-field:has(button.tc-toggle-btn[data-packaging="bags"].active) .tc-tooltip-wrapper {
display: inline-flex;
}
.tc-tooltip-icon {
width: 16px;
height: 16px;
border-radius: 50%;
background: var(--tc-border);
color: var(--tc-text-secondary);
font-size: 11px;
font-weight: 700;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s;
}
.tc-tooltip-wrapper:hover .tc-tooltip-icon {
background: var(--tc-yellow);
color: #000;
}
.tc-tooltip-content {
position: absolute;
bottom: calc(100% + 8px);
left: 50%;
transform: translateX(-50%) translateY(6px);
width: 300px;
padding: 12px;
background: #fff;
border: 1px solid var(--tc-border);
border-radius: 10px;
box-shadow: 0 6px 20px rgba(0,0,0,0.12);
font-size: 12px;
line-height: 1.5;
color: var(--tc-text);
opacity: 0;
visibility: hidden;
pointer-events: none;
transition: all 0.2s ease;
z-index: 100;
text-align: left;
white-space: normal;
}
.tc-tooltip-content::after {
content: '';
position: absolute;
top: 100%;
left: 50%;
transform: translateX(-50%);
border-width: 6px;
border-style: solid;
border-color: #fff transparent transparent transparent;
}
.tc-tooltip-wrapper:hover .tc-tooltip-content {
opacity: 1;
visibility: visible;
transform: translateX(-50%) translateY(0);
}
@media (max-width: 480px) {
.tc-tooltip-content {
width: 220px;
left: 0;
transform: translateX(-20%) translateY(6px);
}
.tc-tooltip-wrapper:hover .tc-tooltip-content {
transform: translateX(-20%) translateY(0);
}
.tc-tooltip-content::after {
left: 20%;
transform: translateX(0);
}
}
@media (max-width: 480px) {
.tc-app {
padding: 16px;
}
.tc-total {
flex-direction: column;
gap: 8px;
text-align: center;
}
.tc-products, .tc-cards {
grid-template-columns: 1fr;
}
}
@media screen and (max-width:650px) {
.tc-input-row {
flex-direction: column;
align-items: flex-start;
gap: 10px;
margin-bottom: 10px;
}
}
@media screen and (max-width:960px) {
.tc-toggle {
width: 40%;
}
}
@media screen and (max-width:680px) {
.tc-toggle, .tc-segmented, .tc-cta {
width: 100%;
}
label.tc-switch {
width: 100%;
justify-content: left;
}
div#tc-total {
height: auto;
}
a#tc-cta {
margin-left: 0;
}
.tc-card-value {
font-size: 15px;
}
.tc-total-amount {
font-size: 20px;
}
.tc-cta {
font-size: 14px;
}
h3#tc-results-title {
font-size: 12px;
}
.modal-summary.t-text {
font-size: 12px;
}
}
/* Стили для сообщения ОПТ */
.tc-opt-message {
background: linear-gradient(135deg, #ECFDF5 0%, #fff 100%);
border: 1px solid #22C55E;
color: #166534;
padding: 12px 16px;
border-radius: var(--tc-radius-sm);
font-size: 13px;
font-weight: 500;
margin-top: 16px;
text-align: center;
line-height: 1.4;
width: 100%;
box-sizing: border-box;
}
.tc-opt-message p {
margin: 0;
}
.tc-opt-message p:first-child {
margin-bottom: 4px;
}
</style>
<script>
(() => {
'use strict';
const fmt = n => Number(n).toLocaleString('ru-RU');
const ceil = n => Math.ceil(n);
const OPT_DISPLAY_THRESHOLD = 50;
const DEFAULT_CONFIG = {
products: [
{
id: 'classic',
name: 'CLASSIC',
tag: null,
bagPrice: null,
prices: [6000, 5800, 5500],
desc: 'Для массового бюджетного строительства (фракция 0.25–3.5 мм)'
},
{
id: 'optima',
name: 'EXPERT',
tag: 'НОВИНКА!',
tagClass: 'new',
bagPrice: null,
prices: [8500, 8000, 7700],
desc: 'Идеален для ремонта и подготовки к сезону. Улучшенная формула. Легко укатывается (фракция 0.25–3.0 мм)'
},
{
id: 'comfort',
name: 'PREMIUM',
tag: null,
bagPrice: 500,
prices: [11000, 10500, 10000],
desc: 'С повышенным содержанием карьерной глины для профессиональных теннисных кортов (фракция 0.25–3.5 мм)'
},
{
id: 'premium',
name: 'EXTRA',
tag: null,
bagPrice: 800,
prices: [15000, 15000, 15000],
desc: 'Элитный финишный слой для идеального отскока. фракция (0.25–1.0 мм)'
}
],
superior: {
name: 'Extra',
bagPrice: 525,
prices: [15000, 14400, 14400]
},
constants: {
STANDARD_AREA: 648,
BAG_KG: 40,
COMPACTION: 1.4,
WASTE: 1.205
}
};
function parseTildaTable(selector) {
const container = document.querySelector(selector);
if (!container) {
return null;
}
const headerEl = container.querySelector('.t431__data-part1');
const dataEl = container.querySelector('.t431__data-part2');
if (!headerEl || !dataEl) {
return null;
}
const headerText = headerEl.textContent.trim();
const rowsText = dataEl.textContent.trim();
if (!headerText || !rowsText) {
return null;
}
const headers = headerText.split(';').map(header => header.trim());
const rows = rowsText.split(/\r?\n/).map(row => row.trim()).filter(Boolean);
return rows.map(row => {
const values = row.split(';').map(value => value.trim());
const result = {};
headers.forEach((header, index) => {
let value = values[index];
if (value === undefined || value === '' || value === 'null') {
value = null;
} else if (!Number.isNaN(Number(value))) {
value = Number(value);
}
result[header] = value;
});
return result;
});
}
function buildConfig() {
const p = parseTildaTable('.uc-products-table');
const s = parseTildaTable('.uc-superior-table')?.[0];
const c = parseTildaTable('.uc-constants-table')?.[0];
const products = p && p.length ? p.map(prod => ({
id: String(prod.name || '').toLowerCase().replace(/\s+/g, '-').replace(/[^a-zа-яё0-9-]/gi, ''),
name: prod.name,
tag: prod.tag || null,
tagClass: prod.tag ? 'new' : null,
bagPrice: prod.bagPrice,
prices: [
prod['prices(1-20)'] || 0,
prod['prices(20-100)'] || 0,
prod['prices(100)'] || 0
],
desc: prod.desc || ''
})) : DEFAULT_CONFIG.products;
const superior = s ? {
name: s.name || 'Extra',
bagPrice: s.bagPrice,
prices: [
s['prices(1-20)'] || 0,
s['prices(20-100)'] || 0,
s['prices(100)'] || 0
]
} : DEFAULT_CONFIG.superior;
const constants = c ? {
STANDARD_AREA: Number(c.STANDARD_AREA) || 648,
BAG_KG: Number(c.BAG_KG) || 40,
COMPACTION: Number(c.COMPACTION) || 1.4,
WASTE: Number(c.WASTE) || 1.2
} : DEFAULT_CONFIG.constants;
return {
products,
superior,
constants
};
}
function waitForPriceData(timeout = 10000) {
return new Promise(resolve => {
const start = Date.now();
const check = () => {
const app = document.querySelector('#tc-app');
const products = document.querySelector('.uc-products-table .t431__data-part2');
const superior = document.querySelector('.uc-superior-table .t431__data-part2');
const constants = document.querySelector('.uc-constants-table .t431__data-part2');
const isReady = app && products?.textContent.trim() && superior?.textContent.trim() && constants?.textContent.trim();
if (isReady) {
resolve(true);
return;
}
if (Date.now() - start >= timeout) {
resolve(false);
return;
}
setTimeout(check, 50);
};
check();
});
}
let CONFIG = DEFAULT_CONFIG;
const state = {
mode: 'repair',
area: DEFAULT_CONFIG.constants.STANDARD_AREA,
thickness: 45,
productId: DEFAULT_CONFIG.products[0]?.id || 'classic',
packaging: 'bulk',
superior: false
};
const calcTons = () => {
const { mode, area, thickness } = state;
const { STANDARD_AREA, COMPACTION, WASTE } = CONFIG.constants;
if (mode === 'repair') {
return ceil(area / STANDARD_AREA);
}
return ceil(area * (thickness * COMPACTION / 1000) * WASTE);
};
const calcSuperiorTons = () => ceil(state.area / CONFIG.constants.STANDARD_AREA * 2);
const calcBags = tons => ceil(tons / (CONFIG.constants.BAG_KG / 1000));
const getTierInfo = tons => {
if (tons <= 20) {
return { idx: 0, label: '1–20 т' };
}
if (tons <= 100) {
return { idx: 1, label: '20–100 т' };
}
return { idx: 2, label: 'Свыше 100 т' };
};
const calcResult = p => {
const tons = calcTons();
const bags = calcBags(tons);
const tier = getTierInfo(tons);
const pricePerTon = p.prices[tier.idx];
const isBags = state.packaging === 'bags' && p.bagPrice !== null;
const totalCost = isBags ? Math.round(bags * p.bagPrice) : Math.round(tons * pricePerTon);
return {
tons,
bags,
pricePerTon,
tierLabel: tier.label,
isBags,
totalCost,
supportsBags: p.bagPrice !== null
};
};
const calcSuperiorResult = () => {
const tons = calcSuperiorTons();
const bags = calcBags(tons);
const tier = getTierInfo(tons);
const p = CONFIG.superior;
const pricePerTon = p.prices[tier.idx];
const isBags = state.packaging === 'bags' && p.bagPrice !== null;
const totalCost = isBags ? Math.round(bags * p.bagPrice) : Math.round(tons * pricePerTon);
return {
tons,
bags,
pricePerTon,
tierLabel: tier.label,
isBags,
totalCost,
supportsBags: p.bagPrice !== null
};
};
const updateToggleUI = () => {
const product = CONFIG.products.find(p => p.id === state.productId);
const bagsBtn = document.querySelector('[data-packaging="bags"]');
if (!bagsBtn) return;
const disabled = !product?.bagPrice;
bagsBtn.classList.toggle('disabled', disabled);
if (disabled && state.packaging === 'bags') {
state.packaging = 'bulk';
document.querySelectorAll('.tc-toggle-btn').forEach(b => {
b.classList.toggle('active', b.dataset.packaging === 'bulk');
});
}
};
const renderProducts = () => {
const grid = document.querySelector('#tc-products');
if (!grid) return;
const tons = calcTons();
const tierIdx = getTierInfo(tons).idx;
grid.innerHTML = CONFIG.products.map(p => {
const tag = p.tag ? `<span class="tc-product-tag ${p.tagClass}">${p.tag}</span>` : '';
const currentPrice = p.prices[tierIdx];
const optLabel = tons >= OPT_DISPLAY_THRESHOLD ? `<span class="tc-product-opt">ОПТ</span>` : '';
const priceText = p.bagPrice ? `${fmt(currentPrice)} ₽/т ${optLabel} · ${fmt(p.bagPrice)} ₽/мешок` : `${fmt(currentPrice)} ₽/т ${optLabel}`;
return `<button class="tc-product ${p.id === state.productId ? 'selected' : ''}" data-id="${p.id}" type="button">${tag}<div class="tc-product-name">${p.name}</div><div class="tc-product-desc">${p.desc}</div><div class="tc-product-price">${priceText}</div></button>`;
}).join('');
grid.onclick = e => {
const card = e.target.closest('.tc-product');
if (!card) return;
state.productId = card.dataset.id;
grid.querySelectorAll('.tc-product').forEach(c => {
c.classList.toggle('selected', c === card);
});
updateToggleUI();
renderResults();
};
updateToggleUI();
};
const renderResultCards = (res, product, variant) => {
const cards = [];
const isOpt = res.tons >= OPT_DISPLAY_THRESHOLD;
const optBadge = isOpt ? `<span class="tc-opt-badge">ОПТ</span>` : '';
if (res.isBags) {
cards.push(`<div class="tc-card ${variant}"><div class="tc-card-value">${fmt(res.bags)}</div><div class="tc-card-label">Мешков 40 кг</div></div>`);
cards.push(`<div class="tc-card"><div class="tc-card-value">${res.tons} т</div><div class="tc-card-label">Итого тонн</div></div>`);
cards.push(`<div class="tc-card ${variant}" style="position:relative">${optBadge}<div class="tc-card-value">${fmt(product.bagPrice)} ₽</div><div class="tc-card-label">Цена за мешок</div></div>`);
} else {
cards.push(`<div class="tc-card ${variant}"><div class="tc-card-value">${res.tons} т</div><div class="tc-card-label">Необходимо</div></div>`);
cards.push(`<div class="tc-card"><div class="tc-card-value">${res.tierLabel}</div><div class="tc-card-label">Ценовой диапазон</div></div>`);
cards.push(`<div class="tc-card ${variant}" style="position:relative">${optBadge}<div class="tc-card-value">${fmt(res.pricePerTon)} ₽</div><div class="tc-card-label">Цена за тонну</div></div>`);
}
cards.push(`<div class="tc-card accent" style="position:relative">${optBadge}<div class="tc-card-value">${fmt(res.totalCost)} ₽</div><div class="tc-card-label">Итоговая стоимость</div></div>`);
return cards.join('');
};
const renderResults = () => {
const emptyEl = document.querySelector('#tc-empty');
const resultsEl = document.querySelector('#tc-results');
if (!emptyEl || !resultsEl) {
return;
}
if (!state.productId || state.area <= 0) {
emptyEl.hidden = false;
resultsEl.hidden = true;
return;
}
emptyEl.hidden = true;
resultsEl.hidden = false;
const product = CONFIG.products.find(p => p.id === state.productId);
if (!product) return;
const res = calcResult(product);
const sRes = state.superior ? calcSuperiorResult() : null;
const modeLabel = state.mode === 'repair' ? 'Ремонт' : 'Строительство';
const thickLabel = state.mode === 'construction' ? ` · ${state.thickness} мм` : '';
const titleEl = document.querySelector('#tc-results-title');
if (titleEl) {
titleEl.textContent = `${modeLabel}${thickLabel} · ${product.name}${product.tag ? ' ' + product.tag : ''}`;
}
const mainCards = document.querySelector('#tc-main-cards');
if (mainCards) {
mainCards.innerHTML = renderResultCards(res, product, 'primary');
}
const supBlock = document.querySelector('#tc-superior-block');
const supCards = document.querySelector('#tc-superior-cards');
const totalAmount = document.querySelector('#tc-total-amount');
const optMsg = document.querySelector('#tc-opt-message');
if (optMsg) {
optMsg.hidden = res.tons < OPT_DISPLAY_THRESHOLD;
}
if (state.superior && sRes) {
if (supBlock) {
supBlock.hidden = false;
}
if (supCards) {
supCards.innerHTML = renderResultCards(sRes, CONFIG.superior, 'sup');
}
if (totalAmount) {
totalAmount.textContent = `${fmt(res.totalCost + sRes.totalCost)} ₽`;
}
} else {
if (supBlock) {
supBlock.hidden = true;
}
if (totalAmount) {
totalAmount.textContent = `${fmt(res.totalCost)} ₽`;
}
}
};
const populateTildaPopup = e => {
if (!state.productId) {
e.preventDefault();
const toast = document.createElement('div');
toast.textContent = 'Выберите материал';
toast.style.cssText = 'position:fixed;bottom:20px;left:50%;transform:translateX(-50%);background:#EF4444;color:#fff;padding:12px 20px;border-radius:12px;font-weight:500;z-index:10001;box-shadow:0 4px 16px rgba(0,0,0,0.15)';
document.body.appendChild(toast);
setTimeout(() => toast.remove(), 2500);
return;
}
const product = CONFIG.products.find(p => p.id === state.productId);
const res = calcResult(product);
const sRes = state.superior ? calcSuperiorResult() : null;
const modeLabel = state.mode === 'repair' ? 'Ремонт' : 'Строительство';
const thickLabel = state.mode === 'construction' ? ` (${state.thickness} мм)` : '';
const totalCost = res.totalCost + (sRes ? sRes.totalCost : 0);
const formInputs = document.querySelector('.uc-calc-form .t-form__inputsbox');
if (formInputs) {
const setVal = (name, val) => {
const input = formInputs.querySelector(`input[name="${name}"]`);
if (input) {
input.value = val || '';
}
};
setVal('тип работ', modeLabel + thickLabel);
setVal('площадь', fmt(state.area) + ' м²');
setVal('толщина слоя', state.mode === 'construction' ? state.thickness + ' мм' : '');
setVal('материал', product.name + (product.tag ? ' ' + product.tag : ''));
setVal('тип отгрузки', state.packaging === 'bags' ? 'Мешки 40 кг' : 'Тонны');
setVal('ценовой диапазон', res.tierLabel);
setVal('итого мешков', res.bags);
setVal('итого тонн', res.tons + ' т');
setVal('стоимость мешками', fmt(res.isBags ? res.totalCost : Math.round(res.bags * product.bagPrice)) + ' ₽');
setVal('стоимость тонн', fmt(!res.isBags ? res.totalCost : Math.round(res.tons * res.pricePerTon)) + ' ₽');
setVal('финишный слой', state.superior ? 'Да' : '');
if (state.superior && sRes) {
setVal('финишный слой - итого мешков', sRes.bags);
setVal('финишный слой - итого тонн', sRes.tons + ' т');
setVal('финишный слой - стоимость', fmt(sRes.totalCost) + ' ₽');
}
setVal('итоговая стоимость', fmt(totalCost) + ' ₽');
}
const popupDescr = document.querySelector('.uc-calc-form .t702__descr');
let summaryBlock = document.querySelector('.uc-calc-form .modal-summary');
if (!summaryBlock && popupDescr) {
summaryBlock = document.createElement('div');
summaryBlock.className = 'modal-summary t-text';
popupDescr.insertAdjacentElement('afterend', summaryBlock);
}
if (summaryBlock) {
const pkgType = state.packaging === 'bags' ? 'Мешки 40 кг' : 'Тонны';
const mainQty = state.packaging === 'bags' ? fmt(res.bags) + ' шт' : res.tons + ' т';
let html = `<p><span>Тип работ:</span><span>${modeLabel}${thickLabel}</span></p>`;
html += `<p><span>Материал:</span><span>${product.name}${product.tag ? ' ' + product.tag : ''}</span></p>`;
html += `<p><span>Площадь:</span><span>${fmt(state.area)} м²</span></p>`;
html += `<p><span>Фасовка:</span><span>${pkgType}</span></p>`;
html += `<p><span>Объём/Кол-во:</span><span>${mainQty}</span></p>`;
html += `<p><span>Тариф:</span><span>${res.tierLabel} · ${fmt(res.pricePerTon)} ₽/т</span></p>`;
html += `<p><span>Стоимость основного:</span><span>${fmt(res.totalCost)} ₽</span></p>`;
if (state.superior && sRes) {
html += `<p><span>Финишное покрытие:</span><span>${fmt(sRes.totalCost)} ₽</span></p>`;
}
html += `<p><span><strong>Итоговая стоимость:</strong></span><span><strong>${fmt(totalCost)} ₽</strong></span></p>`;
summaryBlock.innerHTML = html;
}
};
let initialized = false;
const init = () => {
if (initialized) return;
initialized = true;
document.querySelectorAll('.tc-seg-btn').forEach(btn => {
btn.onclick = () => {
state.mode = btn.dataset.mode;
document.querySelectorAll('.tc-seg-btn').forEach(b => {
b.classList.toggle('active', b === btn);
});
const thickField = document.querySelector('#tc-thickness-field');
if (thickField) {
thickField.hidden = state.mode !== 'construction';
}
renderProducts();
renderResults();
};
});
const areaInput = document.querySelector('#tc-area');
if (areaInput) {
areaInput.addEventListener('input', () => {
const value = parseFloat(areaInput.value);
state.area = (isNaN(value) || value <= 0) ? 0 : value;
renderProducts();
renderResults();
});
}
document.querySelector('#tc-preset')?.addEventListener('click', () => {
state.area = CONFIG.constants.STANDARD_AREA;
const input = document.querySelector('#tc-area');
if (input) {
input.value = CONFIG.constants.STANDARD_AREA;
}
renderProducts();
renderResults();
});
const slider = document.querySelector('#tc-thickness');
const sliderFill = document.querySelector('.tc-slider-fill');
const sliderVal = document.querySelector('.tc-slider-val');
const updateSlider = () => {
if (!slider) return;
state.thickness = parseInt(slider.value, 10);
if (sliderVal) {
sliderVal.textContent = state.thickness + ' мм';
}
const pct = ((state.thickness - slider.min) / (slider.max - slider.min)) * 100;
if (sliderFill) {
sliderFill.style.width = pct + '%';
}
const hint = document.querySelector('#tc-thickness-hint');
if (hint) {
hint.innerHTML = `Неуплотнённый: <strong>${Math.round(state.thickness * (4 / 3))} мм</strong> → укатка: <strong>${state.thickness} мм</strong>`;
}
renderProducts();
renderResults();
};
if (slider) {
slider.addEventListener('input', updateSlider);
}
updateSlider();
document.querySelectorAll('.tc-toggle-btn').forEach(btn => {
btn.onclick = () => {
const product = CONFIG.products.find(p => p.id === state.productId);
if (btn.dataset.packaging === 'bags' && !product?.bagPrice) {
return;
}
state.packaging = btn.dataset.packaging;
document.querySelectorAll('.tc-toggle-btn').forEach(b => {
b.classList.toggle('active', b === btn);
});
renderResults();
};
});
document.querySelector('#tc-superior')?.addEventListener('change', e => {
state.superior = e.target.checked;
renderResults();
});
document.querySelector('#tc-reset')?.addEventListener('click', () => {
state.superior = false;
const sw = document.querySelector('#tc-superior');
if (sw) {
sw.checked = false;
}
renderResults();
});
document.querySelector('#tc-cta')?.addEventListener('click', populateTildaPopup);
document.querySelectorAll('.tc-toggle-btn').forEach(button => {
button.classList.toggle('active', button.dataset.packaging === state.packaging);
});
renderProducts();
renderResults();
};
async function waitForAppAndInit() {
const dataReady = await waitForPriceData();
CONFIG = buildConfig();
state.productId = CONFIG.products[0]?.id || 'classic';
state.area = CONFIG.constants.STANDARD_AREA;
const areaInput = document.querySelector('#tc-area');
if (areaInput) {
areaInput.value = state.area;
}
init();
}
waitForAppAndInit();
if (typeof ResizeObserver !== 'undefined' && window.parent !== window) {
new ResizeObserver(() => {
window.parent.postMessage({
type: 'terracourts-resize',
height: document.documentElement.scrollHeight + 24
}, '*');
}).observe(document.body);
}
})();
</script> Выберите материал и укажите площадь