Bundles feature WIP + fix request-access products cap (20 -> 200)
- app/main.py: raise products_raw[:20] cap to [:200] in /api/request-access and /api/request-more-access - was silently truncating access requests with more than 20 selected products - accumulated bundles/product-page WIP (config, models, runtime, templates) Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
+235
-215
@@ -41,10 +41,6 @@
|
||||
<link rel="icon" type="image/png" sizes="192x192" href="/static/favicon_192.png" />
|
||||
<link rel="apple-touch-icon" sizes="192x192" href="/static/favicon_192.png" />
|
||||
<link rel="manifest" href="/static/manifest.json" />
|
||||
<style>
|
||||
body { background: #070f1c; overflow: hidden; height: 100vh; }
|
||||
@media (max-width: 820px) { body { overflow: auto; height: auto; } }
|
||||
</style>
|
||||
<!-- Yandex.Metrika counter -->
|
||||
<script type="text/javascript">
|
||||
(function(m,e,t,r,i,k,a){
|
||||
@@ -57,60 +53,251 @@
|
||||
</script>
|
||||
<noscript><div><img src="https://mc.yandex.ru/watch/109119977" style="position:absolute; left:-9999px;" alt="" /></div></noscript>
|
||||
<!-- /Yandex.Metrika counter -->
|
||||
<style>
|
||||
:root{
|
||||
--lg2-bg:#eef1f5; --lg2-panel:#ffffff; --lg2-raised:#ffffff;
|
||||
--lg2-line:#d7dee6; --lg2-line-soft:#e3e8ee;
|
||||
--lg2-fg:#16202c; --lg2-fg-dim:#5c6b7c; --lg2-fg-faint:#93a1b0;
|
||||
--lg2-accent:#c96c14; --lg2-accent-dim:rgba(201,108,20,.10); --lg2-accent-ink:#2a1400;
|
||||
--lg2-bad:#d3453f; --lg2-bad-dim:rgba(211,69,63,.08);
|
||||
--lg2-info:#1f6fb2; --lg2-info-dim:rgba(31,111,178,.08);
|
||||
--lg2-good:#1f9d63;
|
||||
}
|
||||
html{ background: var(--lg2-bg); }
|
||||
body.login-page{
|
||||
margin:0;min-height:100vh;background:var(--lg2-bg);color:var(--lg2-fg);
|
||||
font-family:"Ubuntu","IBM Plex Sans",system-ui,sans-serif;overflow-x:hidden;
|
||||
}
|
||||
.lg2-wrap{display:grid;grid-template-columns:1.15fr 420px;min-height:100vh}
|
||||
@media (max-width:980px){ .lg2-wrap{grid-template-columns:minmax(0,1fr)} }
|
||||
|
||||
.lg2-pitch{
|
||||
position:relative;min-width:0;padding:56px clamp(28px,5vw,72px) 48px;border-right:1px solid var(--lg2-line);
|
||||
background-image:linear-gradient(var(--lg2-line-soft) 1px, transparent 1px),linear-gradient(90deg, var(--lg2-line-soft) 1px, transparent 1px);
|
||||
background-size:34px 34px;display:flex;flex-direction:column;
|
||||
}
|
||||
@media (max-width:980px){ .lg2-pitch{border-right:none;border-bottom:1px solid var(--lg2-line)} }
|
||||
.lg2-brand{display:flex;flex-wrap:wrap;align-items:center;gap:12px 16px;margin-bottom:40px}
|
||||
.lg2-brand img{height:59px;display:block}
|
||||
.lg2-brand-mark{font:700 11px/1 "Ubuntu Mono",monospace;letter-spacing:.14em;color:var(--lg2-fg-faint)}
|
||||
@media (max-width:520px){
|
||||
.lg2-brand{flex-direction:column;align-items:flex-start;gap:10px}
|
||||
.lg2-brand img{height:40px}
|
||||
}
|
||||
.lg2-eyebrow{font:700 11px/1 "Ubuntu Mono",monospace;letter-spacing:.18em;text-transform:uppercase;color:var(--lg2-accent);margin:0 0 16px}
|
||||
.lg2-pitch h1{font:800 clamp(30px,3.6vw,46px)/1.06 "Ubuntu",sans-serif;letter-spacing:-.01em;margin:0 0 18px;text-wrap:balance;max-width:16ch;color:var(--lg2-fg);overflow-wrap:break-word;hyphens:auto}
|
||||
.lg2-pitch, .lg2-pitch *{ overflow-wrap: break-word; }
|
||||
.lg2-pitch p.lead{font:400 15px/1.65 "Ubuntu",sans-serif;color:var(--lg2-fg-dim);max-width:52ch;margin:0 0 32px}
|
||||
|
||||
.lg2-feat-row{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:1px;background:var(--lg2-line);border:1px solid var(--lg2-line);border-radius:10px;overflow:hidden;margin-bottom:40px}
|
||||
@media (max-width:720px){ .lg2-feat-row{grid-template-columns:repeat(2,minmax(0,1fr))} }
|
||||
@media (max-width:420px){ .lg2-feat-row{grid-template-columns:minmax(0,1fr)} }
|
||||
.lg2-feat{position:relative;min-width:0;background:var(--lg2-panel);padding:18px 16px 16px;border-top:2px solid var(--lg2-accent)}
|
||||
.lg2-feat-idx{font:700 10px/1 "Ubuntu Mono",monospace;letter-spacing:.08em;color:var(--lg2-fg-faint);display:block;margin-bottom:12px}
|
||||
.lg2-feat-icon{width:26px;height:26px;margin-bottom:14px;color:var(--lg2-accent);display:block}
|
||||
.lg2-feat-icon svg{width:100%;height:100%;display:block}
|
||||
.lg2-feat-label{font:600 13px/1.35 "Ubuntu",sans-serif;color:var(--lg2-fg);margin-bottom:6px}
|
||||
.lg2-feat-desc{font:400 11.5px/1.55 "Ubuntu",sans-serif;color:var(--lg2-fg-dim)}
|
||||
|
||||
.lg2-distrib-btn{
|
||||
align-self:flex-start;margin:0 0 44px;padding:11px 22px;border:1px solid var(--lg2-line);border-radius:9px;
|
||||
background:transparent;color:var(--lg2-fg-dim);font:600 12.5px/1 "Ubuntu",sans-serif;letter-spacing:.02em;cursor:pointer;
|
||||
}
|
||||
.lg2-distrib-btn:hover{border-color:var(--lg2-accent);color:var(--lg2-fg)}
|
||||
|
||||
.lg2-wall-head{display:flex;align-items:baseline;gap:10px;margin-bottom:14px}
|
||||
.lg2-wall-head h2{font:700 11px/1 "Ubuntu Mono",monospace;letter-spacing:.14em;text-transform:uppercase;color:var(--lg2-fg-dim);margin:0}
|
||||
.lg2-wall-head::after{content:"";flex:1;height:1px;background:var(--lg2-line)}
|
||||
.lg2-wall-count{font:600 11px/1 "Ubuntu Mono",monospace;color:var(--lg2-fg-faint)}
|
||||
.lg2-logo-wall{display:grid;grid-template-columns:repeat(auto-fill,minmax(84px,1fr));gap:10px;max-width:760px}
|
||||
.lg2-wall-tile{aspect-ratio:1;background:var(--lg2-panel);border:1px solid var(--lg2-line);border-radius:9px;display:flex;align-items:center;justify-content:center;padding:8px}
|
||||
a.lg2-wall-tile{text-decoration:none;cursor:pointer}
|
||||
.lg2-wall-tile img{width:100%;height:100%;object-fit:contain;filter:grayscale(1) opacity(.75)}
|
||||
.lg2-wall-tile:hover img{filter:none}
|
||||
.lg2-wall-tile:hover{border-color:var(--lg2-accent)}
|
||||
|
||||
.lg2-pitch-footer{margin-top:auto;padding-top:36px;font:400 11px/1 "Ubuntu Mono",monospace;color:var(--lg2-fg-faint)}
|
||||
.lg2-pitch-footer a{color:inherit;text-decoration:none}
|
||||
.lg2-pitch-footer a:hover{color:var(--lg2-fg-dim)}
|
||||
|
||||
.lg2-auth{display:flex;flex-direction:column;justify-content:center;min-width:0;padding:48px 40px;background:var(--lg2-bg);
|
||||
position:sticky;top:0;height:100vh;box-sizing:border-box}
|
||||
@media (max-width:980px){ .lg2-auth{position:static;height:auto} }
|
||||
.lg2-auth-card{width:100%;max-width:320px;margin:0 auto}
|
||||
.lg2-auth-title{font:700 22px/1.2 "Ubuntu",sans-serif;letter-spacing:-.01em;margin:0 0 4px;color:var(--lg2-fg)}
|
||||
.lg2-auth-sub{font:400 12px/1.4 "Ubuntu Mono",monospace;color:var(--lg2-fg-faint);letter-spacing:.02em;margin:0 0 18px;text-transform:uppercase}
|
||||
|
||||
.lg2-prompt{font:400 12.5px/1 "Ubuntu Mono",monospace;color:var(--lg2-fg-dim);margin:0 0 24px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
||||
.lg2-prompt-user{color:var(--lg2-good)}
|
||||
.lg2-prompt-path{color:var(--lg2-accent)}
|
||||
.lg2-caret{display:inline-block;width:7px;height:13px;background:var(--lg2-accent);vertical-align:-2px;margin-left:3px;animation:lg2-blink 1.1s step-end infinite}
|
||||
@keyframes lg2-blink{50%{opacity:0}}
|
||||
@media (prefers-reduced-motion: reduce){ .lg2-caret{animation:none} }
|
||||
|
||||
.lg2-notice, .lg2-error{
|
||||
border-radius:9px;padding:10px 13px;font:500 12.5px/1.5 "Ubuntu",sans-serif;margin-bottom:18px;
|
||||
}
|
||||
.lg2-notice{background:var(--lg2-info-dim);border:1px solid rgba(127,199,255,.35);color:var(--lg2-info)}
|
||||
.lg2-error{background:var(--lg2-bad-dim);border:1px solid rgba(255,107,107,.35);color:var(--lg2-bad)}
|
||||
|
||||
.lg2-field{display:flex;flex-direction:column;gap:7px;margin-bottom:16px}
|
||||
.lg2-field label{font:700 10.5px/1 "Ubuntu Mono",monospace;letter-spacing:.09em;text-transform:uppercase;color:var(--lg2-fg-faint)}
|
||||
.lg2-field input{background:var(--lg2-panel);border:1px solid var(--lg2-line);border-radius:9px;padding:11px 13px;color:var(--lg2-fg);font:400 14px/1 "Ubuntu",sans-serif;outline:none;width:100%;box-sizing:border-box}
|
||||
.lg2-field input::placeholder{color:var(--lg2-fg-faint)}
|
||||
.lg2-field input:focus{border-color:var(--lg2-accent)}
|
||||
|
||||
.lg2-submit{width:100%;margin-top:6px;padding:12px 16px;border:none;border-radius:9px;cursor:pointer;background:var(--lg2-accent);color:var(--lg2-accent-ink);font:700 13.5px/1 "Ubuntu",sans-serif;letter-spacing:.02em}
|
||||
.lg2-submit:hover{filter:brightness(1.06)}
|
||||
.lg2-request-btn{width:100%;margin-top:12px;padding:10px 16px;border-radius:9px;border:1px solid var(--lg2-line);background:transparent;color:var(--lg2-fg-dim);font:600 12.5px/1 "Ubuntu",sans-serif;cursor:pointer}
|
||||
.lg2-request-btn:hover{border-color:var(--lg2-accent);color:var(--lg2-fg)}
|
||||
.lg2-auth-foot{margin-top:40px;text-align:center}
|
||||
.lg2-auth-foot a{color:var(--lg2-fg-faint);font-size:11px;text-decoration:none}
|
||||
.lg2-auth-foot a:hover{color:var(--lg2-fg-dim)}
|
||||
|
||||
/* Re-skin the shared access-modal component to match the new panel palette
|
||||
(it's used by "Запросить доступ" here and on the dashboard alike, so we
|
||||
override on top of style.css instead of editing it there). */
|
||||
.access-modal-overlay{ background: rgba(6,9,14,.82) !important; }
|
||||
.access-modal{
|
||||
background: var(--lg2-panel) !important; border: 1px solid var(--lg2-line) !important;
|
||||
box-shadow: 0 24px 80px rgba(0,0,0,.45) !important;
|
||||
}
|
||||
.access-modal-title{ color: var(--lg2-fg) !important; font-family:"Ubuntu",sans-serif; }
|
||||
.access-modal-sub{ color: var(--lg2-fg-faint) !important; }
|
||||
.access-field label{ color: var(--lg2-fg-dim) !important; font-family:"Ubuntu",sans-serif; }
|
||||
.access-field .req{ color: var(--lg2-accent) !important; }
|
||||
.access-field input[type=text],
|
||||
.access-field input[type=email],
|
||||
.access-field input[type=tel],
|
||||
.access-textarea{
|
||||
background: var(--lg2-raised) !important; border: 1px solid var(--lg2-line) !important;
|
||||
color: var(--lg2-fg) !important; font-family:"Ubuntu",sans-serif;
|
||||
}
|
||||
.access-field input[type=text]::placeholder,
|
||||
.access-field input[type=email]::placeholder,
|
||||
.access-field input[type=tel]::placeholder,
|
||||
.access-textarea::placeholder{ color: var(--lg2-fg-faint) !important; }
|
||||
.access-field input[type=text]:focus,
|
||||
.access-field input[type=email]:focus,
|
||||
.access-field input[type=tel]:focus,
|
||||
.access-textarea:focus{
|
||||
border-color: var(--lg2-accent) !important; box-shadow: 0 0 0 3px var(--lg2-accent-dim) !important;
|
||||
}
|
||||
.access-products-wrap{ background: var(--lg2-raised) !important; border: 1px solid var(--lg2-line) !important; }
|
||||
.access-products-loading{ color: var(--lg2-fg-faint) !important; }
|
||||
.access-products-cat{ color: var(--lg2-fg-faint) !important; }
|
||||
.access-product-item{ color: var(--lg2-fg-dim) !important; font-family:"Ubuntu",sans-serif; }
|
||||
.access-product-item:hover span{ color: var(--lg2-fg) !important; }
|
||||
.access-product-item input[type=checkbox]{ accent-color: var(--lg2-accent); }
|
||||
.access-modal-error{
|
||||
background: var(--lg2-bad-dim) !important; border: 1px solid rgba(255,107,107,.35) !important;
|
||||
color: var(--lg2-bad) !important;
|
||||
}
|
||||
.access-btn-cancel{
|
||||
background: transparent !important; border: 1px solid var(--lg2-line) !important; color: var(--lg2-fg-dim) !important;
|
||||
font-family:"Ubuntu",sans-serif;
|
||||
}
|
||||
.access-btn-cancel:hover{ border-color: var(--lg2-fg-faint) !important; color: var(--lg2-fg) !important; background: transparent !important; }
|
||||
.access-btn-submit{
|
||||
background: var(--lg2-accent) !important; color: var(--lg2-accent-ink) !important; box-shadow: none !important;
|
||||
font-family:"Ubuntu",sans-serif; font-weight: 700 !important;
|
||||
}
|
||||
.access-btn-submit:hover:not(:disabled){ opacity: 1 !important; filter: brightness(1.06); }
|
||||
.access-consent-label span{ color: var(--lg2-fg-dim) !important; font-family:"Ubuntu",sans-serif; }
|
||||
.access-consent-link{ color: var(--lg2-accent) !important; }
|
||||
.am-success-title{ color: var(--lg2-fg) !important; }
|
||||
.am-success-sub{ color: var(--lg2-fg-dim) !important; }
|
||||
.am-success-icon{ color: var(--lg2-good) !important; }
|
||||
|
||||
@media (max-width:820px){
|
||||
.lg2-pitch{padding:40px 22px 34px}
|
||||
.lg2-auth{padding:36px 22px}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="login-wrap">
|
||||
<aside class="login-left">
|
||||
<div class="login-left-glow login-left-glow-top"></div>
|
||||
<div class="login-left-glow login-left-glow-bottom"></div>
|
||||
<div class="login-left-inner">
|
||||
<a href="#" onclick="window.open(location.hostname==='stand.mont.ru'?'https://www.mont.ru':'https://4mont.ru','_blank');return false;"><img src="/static/logo.png?v=4" alt="MONT" class="login-corner-logo" /></a>
|
||||
<h1 class="login-left-title">Инфраструктурный<br>полигон MONT</h1>
|
||||
<p class="login-left-desc">Платформа для демонстрации и пилотного тестирования российского ПО. Партнеры MONT и их заказчики получают браузерный доступ к рабочим стендам с отечественными ОС, платформами виртуализации, СРК и другими решениями — без установки и настройки.</p>
|
||||
<ul class="login-features">
|
||||
<li class="login-feature">
|
||||
<span class="login-feature-icon">🖥</span>
|
||||
<span>Доступ к рабочим столам ОС</span>
|
||||
</li>
|
||||
<li class="login-feature">
|
||||
<span class="login-feature-icon">🌐</span>
|
||||
<span>Веб-интерфейсы сервисов</span>
|
||||
</li>
|
||||
<li class="login-feature">
|
||||
<span class="login-feature-icon">⚡</span>
|
||||
<span>Доступ в один клик</span>
|
||||
</li>
|
||||
<li class="login-feature">
|
||||
<span class="login-feature-icon">🔒</span>
|
||||
<span>Защищённый контур</span>
|
||||
</li>
|
||||
</ul>
|
||||
<button type="button" class="login-distrib-btn" onclick="window.open(location.hostname==='stand.mont.ru'?'https://maps.mont.ru':'https://maps.4mont.ru','_blank')">Продукты нашей дистрибуции</button>
|
||||
<body class="login-page">
|
||||
<div class="lg2-wrap">
|
||||
<aside class="lg2-pitch">
|
||||
<div class="lg2-brand">
|
||||
<a href="#" onclick="window.open(location.hostname==='stand.mont.ru'?'https://www.mont.ru':'https://4mont.ru','_blank');return false;">
|
||||
<img src="/static/logo-color.png?v=1" alt="MONT" />
|
||||
</a>
|
||||
<span class="lg2-brand-mark">Инфраструктурный полигон</span>
|
||||
</div>
|
||||
|
||||
<p class="lg2-eyebrow">Демо и пилоты российского ПО</p>
|
||||
<h1>Полигон импортонезависимой инфраструктуры</h1>
|
||||
<p class="lead">Платформа для демонстрации и пилотного тестирования российского ПО. Партнёры MONT
|
||||
и их заказчики получают браузерный доступ к рабочим стендам с отечественными ОС, платформами
|
||||
виртуализации, СРК и другими решениями — без установки и настройки.</p>
|
||||
|
||||
<div class="lg2-feat-row">
|
||||
<div class="lg2-feat">
|
||||
<span class="lg2-feat-idx">01</span>
|
||||
<span class="lg2-feat-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="4" width="18" height="12" rx="1.5"/><path d="M8 20h8M12 16v4"/></svg></span>
|
||||
<div class="lg2-feat-label">Доступ к рабочим столам ОС</div>
|
||||
<div class="lg2-feat-desc">Полный рабочий стол Astra, RED OS, Uncom и других ОС прямо в браузере, без RDP-клиента.</div>
|
||||
</div>
|
||||
<div class="lg2-feat">
|
||||
<span class="lg2-feat-idx">02</span>
|
||||
<span class="lg2-feat-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="9"/><path d="M3 12h18M12 3c2.5 2.6 3.8 5.7 3.8 9s-1.3 6.4-3.8 9c-2.5-2.6-3.8-5.7-3.8-9s1.3-6.4 3.8-9z"/></svg></span>
|
||||
<div class="lg2-feat-label">Веб-интерфейсы сервисов</div>
|
||||
<div class="lg2-feat-desc">Админ-панели и порталы продуктов открываются как обычная вкладка браузера.</div>
|
||||
</div>
|
||||
<div class="lg2-feat">
|
||||
<span class="lg2-feat-idx">03</span>
|
||||
<span class="lg2-feat-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M13 2 4 14h7l-1 8 9-12h-7l1-8z"/></svg></span>
|
||||
<div class="lg2-feat-label">Доступ в один клик</div>
|
||||
<div class="lg2-feat-desc">Стенд поднимается автоматически — ничего разворачивать и настраивать не нужно.</div>
|
||||
</div>
|
||||
<div class="lg2-feat">
|
||||
<span class="lg2-feat-idx">04</span>
|
||||
<span class="lg2-feat-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><rect x="4" y="11" width="16" height="9" rx="1.5"/><path d="M8 11V7a4 4 0 0 1 8 0v4"/></svg></span>
|
||||
<div class="lg2-feat-label">Защищённый контур</div>
|
||||
<div class="lg2-feat-desc">Изолированная сеть с резервированием и регулярными бэкапами всех стендов.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="button" class="lg2-distrib-btn" onclick="window.open(location.hostname==='stand.mont.ru'?'https://maps.mont.ru':'https://maps.4mont.ru','_blank')">Продукты нашей дистрибуции</button>
|
||||
|
||||
{% set wall_services = public_services|rejectattr('slug', 'equalto', 'communigate-pro-user')|list %}
|
||||
{% if wall_services %}
|
||||
<div class="lg2-wall-head" id="products"><h2>Что на полигоне</h2><span class="lg2-wall-count">{{ wall_services|length }} продуктов</span></div>
|
||||
<div class="lg2-logo-wall">
|
||||
{% for service in wall_services %}
|
||||
{% set wall_name = 'CommuniGate Pro' if service.slug == 'communigate-pro-admin' else service.name %}
|
||||
<a class="lg2-wall-tile" href="/product/{{ service.slug }}" title="{{ wall_name }} — подробнее">
|
||||
<img src="{{ service.icon_path or '/static/service-placeholder.svg' }}" alt="{{ wall_name }}" />
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="lg2-pitch-footer">MONT PROVING GROUND</div>
|
||||
</aside>
|
||||
<main class="login-right">
|
||||
<div class="login-right-inner">
|
||||
<div class="login-form-title">Вход в систему</div>
|
||||
<div class="login-form-subtitle">Инфраструктурный полигон</div>
|
||||
{% if session_notice %}<div class="session-notice lp-session-notice">{{ session_notice }}</div>{% endif %}
|
||||
{% if login_error %}<div class="auth-error lp-auth-error">{{ login_error }}</div>{% endif %}
|
||||
<form method="post" action="/login" class="login-form">
|
||||
|
||||
<main class="lg2-auth">
|
||||
<div class="lg2-auth-card">
|
||||
<div class="lg2-auth-title">Вход в систему</div>
|
||||
<div class="lg2-auth-sub">Инфраструктурный полигон</div>
|
||||
<div class="lg2-prompt"><span class="lg2-prompt-user">mont@polygon</span><span class="lg2-prompt-path">:~/access</span>$ ./login.sh<span class="lg2-caret"></span></div>
|
||||
{% if session_notice %}<div class="lg2-notice">{{ session_notice }}</div>{% endif %}
|
||||
{% if login_error %}<div class="lg2-error">{{ login_error }}</div>{% endif %}
|
||||
<form method="post" action="/login">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}" />
|
||||
<div class="login-field">
|
||||
<div class="lg2-field">
|
||||
<label>Логин</label>
|
||||
<input type="text" name="username" placeholder="Введите логин" required autocomplete="username" />
|
||||
</div>
|
||||
<div class="login-field">
|
||||
<div class="lg2-field">
|
||||
<label>Пароль</label>
|
||||
<input type="password" name="password" placeholder="Введите пароль" required autocomplete="current-password" />
|
||||
</div>
|
||||
<button type="submit" class="login-submit">Войти</button>
|
||||
<button type="submit" class="lg2-submit">Войти</button>
|
||||
</form>
|
||||
<button type="button" class="login-request-btn" id="btn-request-access" data-open-access-modal="1">Запросить доступ</button>
|
||||
<button type="button" class="lg2-request-btn" id="btn-request-access" data-open-access-modal="1">Запросить доступ</button>
|
||||
<div class="lg2-auth-foot"><a href="mailto:ruslan@ipcom.su">Made by Galyaviev</a></div>
|
||||
</div>
|
||||
<footer class="login-footer">
|
||||
<a href="mailto:ruslan@ipcom.su" class="login-footer-link">Made by Galyaviev</a>
|
||||
</footer>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
@@ -356,179 +543,12 @@
|
||||
})();
|
||||
</script>
|
||||
|
||||
<!-- Contact Ruslan Modal -->
|
||||
<div id="contact-modal" class="access-modal-overlay" style="display:none" aria-modal="true" role="dialog">
|
||||
<div class="access-modal">
|
||||
<div class="access-modal-header">
|
||||
<div class="access-modal-title">Связаться с Русланом</div>
|
||||
|
||||
</div>
|
||||
<div class="access-modal-body" id="cm-body">
|
||||
<div class="access-field">
|
||||
<label>Ваше имя <span class="req">*</span></label>
|
||||
<input id="cm-name" type="text" placeholder="Иван Иванов" />
|
||||
</div>
|
||||
<div class="access-field">
|
||||
<label>Email <span class="req">*</span></label>
|
||||
<input id="cm-email" type="email" placeholder="ivan@company.ru" />
|
||||
</div>
|
||||
<div class="access-field">
|
||||
<label>Телефон <span class="req">*</span></label>
|
||||
<input id="cm-phone" type="tel" placeholder="+7 (999) 000-00-00" />
|
||||
</div>
|
||||
<div class="access-field">
|
||||
<label>Сообщение <span class="req">*</span></label>
|
||||
<textarea id="cm-text" class="access-textarea" placeholder="Ваш вопрос или предложение..." rows="4"></textarea>
|
||||
</div>
|
||||
<div class="access-consent-field">
|
||||
<label class="access-consent-label">
|
||||
<input type="checkbox" id="cm-consent" />
|
||||
<span>Согласен на <a href="/privacy" target="_blank" class="access-consent-link">обработку персональных данных</a></span>
|
||||
</label>
|
||||
</div>
|
||||
<div id="cm-error" class="access-modal-error" style="display:none"></div>
|
||||
</div>
|
||||
<div class="access-modal-footer" id="cm-footer">
|
||||
<button type="button" class="access-btn-cancel" id="cm-cancel">Отмена</button>
|
||||
<button type="button" class="access-btn-submit" id="cm-submit">Отправить</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function() {
|
||||
const overlay = document.getElementById('contact-modal');
|
||||
const btnCancel = document.getElementById('cm-cancel');
|
||||
const btnSubmit = document.getElementById('cm-submit');
|
||||
const errEl = document.getElementById('cm-error');
|
||||
|
||||
function resetContactForm() {
|
||||
if (!document.getElementById('cm-name')) {
|
||||
document.getElementById('cm-body').innerHTML = `
|
||||
<div class="access-field"><label>Ваше имя <span class="req">*</span></label><input id="cm-name" type="text" placeholder="Иван Иванов" /></div>
|
||||
<div class="access-field"><label>Email <span class="req">*</span></label><input id="cm-email" type="email" placeholder="ivan@company.ru" /></div>
|
||||
<div class="access-field"><label>Телефон <span class="req">*</span></label><input id="cm-phone" type="tel" placeholder="+7 (999) 000-00-00" /></div>
|
||||
<div class="access-field"><label>Сообщение <span class="req">*</span></label><textarea id="cm-text" class="access-textarea" placeholder="Ваш вопрос или предложение..." rows="4"></textarea></div>
|
||||
<div class="access-consent-field"><label class="access-consent-label"><input type="checkbox" id="cm-consent" /><span>Согласен на <a href="/privacy" target="_blank" class="access-consent-link">обработку персональных данных</a></span></label></div>
|
||||
<div id="cm-error" class="access-modal-error" style="display:none"></div>`;
|
||||
document.getElementById('cm-footer').innerHTML = `<button type="button" class="access-btn-cancel" id="cm-cancel">Отмена</button><button type="button" class="access-btn-submit" id="cm-submit">Отправить</button>`;
|
||||
document.getElementById('cm-cancel').addEventListener('click', closeContact);
|
||||
document.getElementById('cm-submit').addEventListener('click', submitContact);
|
||||
document.querySelectorAll('#cm-name,#cm-email,#cm-phone,#cm-text').forEach(function(el){ el.addEventListener('input', function(){ el.classList.remove('am-invalid'); }); });
|
||||
} else {
|
||||
['cm-name','cm-email','cm-phone','cm-text'].forEach(function(id){ var el=document.getElementById(id); if(el){el.value='';el.classList.remove('am-invalid');} });
|
||||
var err=document.getElementById('cm-error'); if(err) err.style.display='none';
|
||||
var btn=document.getElementById('cm-submit'); if(btn){btn.disabled=false;btn.textContent='Отправить';}
|
||||
}
|
||||
}
|
||||
|
||||
function openContact() {
|
||||
resetContactForm();
|
||||
overlay.style.display = 'flex';
|
||||
document.body.style.overflow = 'hidden';
|
||||
}
|
||||
|
||||
function closeContact() {
|
||||
overlay.style.display = 'none';
|
||||
document.body.style.overflow = '';
|
||||
errEl.style.display = 'none';
|
||||
document.querySelectorAll('#contact-modal .am-invalid').forEach(el => el.classList.remove('am-invalid'));
|
||||
}
|
||||
window._closeContactModal = closeContact;
|
||||
|
||||
async function submitContact() {
|
||||
const nameEl = document.getElementById('cm-name');
|
||||
const emailEl = document.getElementById('cm-email');
|
||||
const phoneEl = document.getElementById('cm-phone');
|
||||
const textEl = document.getElementById('cm-text');
|
||||
const submitBtn = document.getElementById('cm-submit');
|
||||
const errorEl = document.getElementById('cm-error');
|
||||
|
||||
const name = nameEl ? nameEl.value.trim() : '';
|
||||
const email = emailEl ? emailEl.value.trim() : '';
|
||||
const phone = phoneEl ? phoneEl.value.trim() : '';
|
||||
const text = textEl ? textEl.value.trim() : '';
|
||||
|
||||
const emailRe = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
const phoneRe = /^[\+\d][\d\s\-\(\)]{6,18}$/;
|
||||
|
||||
const consentEl = document.getElementById('cm-consent');
|
||||
const fields = [
|
||||
{ el: nameEl, check: () => !!name, msg: 'Введите имя' },
|
||||
{ el: emailEl, check: () => emailRe.test(email), msg: 'Введите корректный email' },
|
||||
{ el: phoneEl, check: () => phoneRe.test(phone), msg: 'Введите корректный номер телефона' },
|
||||
{ el: textEl, check: () => !!text, msg: 'Введите сообщение' },
|
||||
];
|
||||
|
||||
const errors = [];
|
||||
fields.forEach(f => {
|
||||
if (f.el && !f.check()) { f.el.classList.add('am-invalid'); errors.push(f.msg); }
|
||||
else if (f.el) f.el.classList.remove('am-invalid');
|
||||
});
|
||||
if (!consentEl || !consentEl.checked) {
|
||||
errors.push('Необходимо согласие на обработку персональных данных');
|
||||
const cf = document.querySelector('#contact-modal .access-consent-field');
|
||||
if (cf) cf.classList.add('am-invalid-consent');
|
||||
} else {
|
||||
const cf = document.querySelector('#contact-modal .access-consent-field');
|
||||
if (cf) cf.classList.remove('am-invalid-consent');
|
||||
}
|
||||
|
||||
if (errors.length) {
|
||||
if (errorEl) { errorEl.textContent = errors.join(' • '); errorEl.style.display = 'block'; }
|
||||
return;
|
||||
}
|
||||
|
||||
if (submitBtn) { submitBtn.disabled = true; submitBtn.textContent = 'Отправка...'; }
|
||||
if (errorEl) errorEl.style.display = 'none';
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/contact', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({name, email, phone, text}),
|
||||
});
|
||||
if (!res.ok) {
|
||||
const d = await res.json().catch(() => ({}));
|
||||
throw new Error(d.detail || 'Ошибка отправки');
|
||||
}
|
||||
document.getElementById('cm-body').innerHTML =
|
||||
'<div class="am-success-msg">' +
|
||||
'<div class="am-success-icon">✓</div>' +
|
||||
'<div class="am-success-title">Отправлено</div>' +
|
||||
'<div class="am-success-sub">Постараюсь ответить в ближайшее время</div>' +
|
||||
'</div>';
|
||||
document.getElementById('cm-footer').innerHTML =
|
||||
'<button type="button" class="access-btn-cancel" onclick="window._closeContactModal()">Закрыть</button>';
|
||||
} catch(e) {
|
||||
const eb = document.getElementById('cm-error');
|
||||
if (eb) { eb.textContent = e.message || 'Ошибка отправки, попробуйте позже'; eb.style.display = 'block'; }
|
||||
const sb = document.getElementById('cm-submit');
|
||||
if (sb) { sb.disabled = false; sb.textContent = 'Отправить'; }
|
||||
}
|
||||
}
|
||||
|
||||
document.querySelectorAll('#cm-name,#cm-email,#cm-phone,#cm-text').forEach(el => {
|
||||
el.addEventListener('input', () => el.classList.remove('am-invalid'));
|
||||
});
|
||||
|
||||
document.getElementById('btn-contact-ruslan').addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
openContact();
|
||||
});
|
||||
btnCancel.addEventListener('click', closeContact);
|
||||
btnSubmit.addEventListener('click', submitContact);
|
||||
overlay.addEventListener('click', function(e) { if (e.target === overlay) closeContact(); });
|
||||
document.addEventListener('keydown', function(e) { if (e.key === 'Escape' && overlay.style.display !== 'none') closeContact(); });
|
||||
})();
|
||||
</script>
|
||||
|
||||
<!-- Cookie Banner -->
|
||||
<div id="cookie-banner" style="display:none;position:fixed;bottom:0;left:0;right:0;z-index:99999;background:rgba(7,15,28,0.97);border-top:1px solid rgba(255,255,255,0.1);backdrop-filter:blur(8px);padding:14px 24px;align-items:center;justify-content:space-between;gap:16px;flex-wrap:wrap;">
|
||||
<p style="margin:0;font-size:0.85rem;color:#c8d8ea;line-height:1.5;flex:1;min-width:200px;">
|
||||
Мы используем файлы cookie, чтобы сделать работу с сайтом удобнее. Нажмите «Принять», чтобы согласиться с использованием файлов cookie в соответствии с <a href="https://www.mont.ru/ru-ru/confidential" target="_blank" style="color:#5b9bd5;text-decoration:underline;">Политикой конфиденциальности</a>.
|
||||
Мы используем файлы cookie, чтобы сделать работу с сайтом удобнее. Нажмите «Принять», чтобы согласиться с использованием файлов cookie в соответствии с <a href="https://www.mont.ru/ru-ru/confidential" target="_blank" style="color:#ff9d3d;text-decoration:underline;">Политикой конфиденциальности</a>.
|
||||
</p>
|
||||
<button onclick="document.getElementById('cookie-banner').style.display='none';localStorage.setItem('cookie_accepted','1');" style="flex-shrink:0;padding:8px 22px;background:linear-gradient(135deg,#1a5db5,#2d8cf0);color:#fff;border:none;border-radius:8px;font-size:0.88rem;font-weight:600;cursor:pointer;white-space:nowrap;">Принять</button>
|
||||
<button onclick="document.getElementById('cookie-banner').style.display='none';localStorage.setItem('cookie_accepted','1');" style="flex-shrink:0;padding:8px 22px;background:linear-gradient(135deg,#ffb15c,#e88420);color:#1a0f00;border:none;border-radius:8px;font-size:0.88rem;font-weight:700;cursor:pointer;white-space:nowrap;">Принять</button>
|
||||
</div>
|
||||
<script>
|
||||
if (!localStorage.getItem('cookie_accepted')) {
|
||||
|
||||
Reference in New Issue
Block a user