feat: contact form modal with Telegram, enlarge Made by button
This commit is contained in:
+14
-10
@@ -522,16 +522,20 @@
|
|||||||
line-height: 1.1;
|
line-height: 1.1;
|
||||||
z-index: 5;
|
z-index: 5;
|
||||||
}
|
}
|
||||||
.credit .name {
|
#btn-contact-ruslan {
|
||||||
font-family: Caveat, cursive;
|
font-family: Caveat, cursive;
|
||||||
font-size: 14px;
|
font-size: 42px;
|
||||||
color: #1c3f7c;
|
color: #1c3f7c;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0;
|
||||||
|
line-height: 1;
|
||||||
|
transition: opacity .15s ease, transform .15s ease;
|
||||||
}
|
}
|
||||||
.credit a {
|
#btn-contact-ruslan:hover {
|
||||||
font-size: 7px;
|
opacity: .75;
|
||||||
color: #2f5fae;
|
transform: scale(1.04);
|
||||||
text-decoration: none;
|
|
||||||
font-weight: 700;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 980px) {
|
@media (max-width: 980px) {
|
||||||
@@ -539,8 +543,7 @@
|
|||||||
.board { grid-template-columns: 1fr; }
|
.board { grid-template-columns: 1fr; }
|
||||||
.hero { padding: 20px; }
|
.hero { padding: 20px; }
|
||||||
.credit { right: 8px; bottom: 6px; }
|
.credit { right: 8px; bottom: 6px; }
|
||||||
.credit .name { font-size: 8px; }
|
#btn-contact-ruslan { font-size: 28px; }
|
||||||
.credit a { font-size: 6px; }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
@@ -637,8 +640,9 @@
|
|||||||
background: rgba(255, 255, 255, .86);
|
background: rgba(255, 255, 255, .86);
|
||||||
border: 1px solid #dbe6ff;
|
border: 1px solid #dbe6ff;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
padding: 4px 6px;
|
padding: 4px 8px;
|
||||||
}
|
}
|
||||||
|
#btn-contact-ruslan { font-size: 24px; }
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 420px) {
|
@media (max-width: 420px) {
|
||||||
|
|||||||
+114
-2
@@ -61,11 +61,22 @@
|
|||||||
<div id="resultRows" class="rows"></div>
|
<div id="resultRows" class="rows"></div>
|
||||||
</section>
|
</section>
|
||||||
<div class="credit">
|
<div class="credit">
|
||||||
<div class="name">Made by Galyaviev</div>
|
<button type="button" id="btn-contact-ruslan">Made by Galyaviev</button>
|
||||||
<a href="mailto:RGalyaviev@mont.com">RGalyaviev@mont.com</a>
|
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
|
<!-- Contact modal -->
|
||||||
|
<div id="contact-modal" style="display:none;position:fixed;inset:0;background:rgba(0,0,0,.6);z-index:9000;align-items:center;justify-content:center;padding:1rem;">
|
||||||
|
<div style="background:#fff;border-radius:20px;width:100%;max-width:460px;max-height:90vh;overflow-y:auto;box-shadow:0 24px 64px rgba(16,43,95,.22);">
|
||||||
|
<div style="padding:1.5rem 1.75rem .75rem;display:flex;align-items:center;justify-content:space-between;">
|
||||||
|
<div style="font-size:1.15rem;font-weight:800;color:#1a3e79;">Связаться с Русланом</div>
|
||||||
|
<button onclick="window._closeContact()" style="background:none;border:none;font-size:1.4rem;color:#8aa;cursor:pointer;line-height:1;padding:0 4px;">×</button>
|
||||||
|
</div>
|
||||||
|
<div id="cm-body" style="padding:.5rem 1.75rem;display:flex;flex-direction:column;gap:.9rem;"></div>
|
||||||
|
<div id="cm-footer" style="display:flex;justify-content:flex-end;gap:.75rem;padding:.75rem 1.75rem 1.5rem;"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Yandex.Metrika counter -->
|
<!-- Yandex.Metrika counter -->
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
(function(m,e,t,r,i,k,a){
|
(function(m,e,t,r,i,k,a){
|
||||||
@@ -81,5 +92,106 @@
|
|||||||
<!-- /Yandex.Metrika counter -->
|
<!-- /Yandex.Metrika counter -->
|
||||||
|
|
||||||
<script src="{{ url_for('static', filename='js/index.js') }}"></script>
|
<script src="{{ url_for('static', filename='js/index.js') }}"></script>
|
||||||
|
<script>
|
||||||
|
(function() {
|
||||||
|
const overlay = document.getElementById('contact-modal');
|
||||||
|
|
||||||
|
const fieldStyle = 'width:100%;box-sizing:border-box;margin-top:.3rem;padding:.6rem .85rem;' +
|
||||||
|
'background:#f7faff;border:1px solid #c8d8f7;border-radius:9px;color:#1a3060;' +
|
||||||
|
'font-size:.92rem;outline:none;font-family:Manrope,sans-serif;';
|
||||||
|
const labelStyle = 'color:#526079;font-size:.8rem;font-weight:700;letter-spacing:.2px;';
|
||||||
|
|
||||||
|
function buildForm() {
|
||||||
|
document.getElementById('cm-body').innerHTML =
|
||||||
|
'<div><label style="' + labelStyle + '">Ваше имя *</label>' +
|
||||||
|
'<input id="cm-name" type="text" placeholder="Иван Иванов" style="' + fieldStyle + '"/></div>' +
|
||||||
|
'<div><label style="' + labelStyle + '">Email *</label>' +
|
||||||
|
'<input id="cm-email" type="email" placeholder="ivan@company.ru" style="' + fieldStyle + '"/></div>' +
|
||||||
|
'<div><label style="' + labelStyle + '">Телефон *</label>' +
|
||||||
|
'<input id="cm-phone" type="tel" placeholder="+7 (999) 000-00-00" style="' + fieldStyle + '"/></div>' +
|
||||||
|
'<div><label style="' + labelStyle + '">Сообщение *</label>' +
|
||||||
|
'<textarea id="cm-text" rows="4" placeholder="Ваш вопрос или предложение..." style="' + fieldStyle + 'resize:vertical;"></textarea></div>' +
|
||||||
|
'<div id="cm-error" style="display:none;background:#fff0f0;border:1px solid #fcc;border-radius:8px;' +
|
||||||
|
'padding:.5rem .85rem;color:#c0392b;font-size:.85rem;"></div>';
|
||||||
|
|
||||||
|
document.getElementById('cm-footer').innerHTML =
|
||||||
|
'<button onclick="window._closeContact()" style="background:#f0f5ff;border:1px solid #c8d8f7;border-radius:9px;' +
|
||||||
|
'padding:.6rem 1.25rem;color:#526079;cursor:pointer;font-family:Manrope,sans-serif;font-weight:600;">Отмена</button>' +
|
||||||
|
'<button id="cm-submit" style="background:linear-gradient(135deg,#1f4ea3,#3978e0);border:none;border-radius:9px;' +
|
||||||
|
'padding:.6rem 1.5rem;color:#fff;font-weight:700;cursor:pointer;font-family:Manrope,sans-serif;">Отправить</button>';
|
||||||
|
|
||||||
|
document.getElementById('cm-submit').addEventListener('click', submit);
|
||||||
|
}
|
||||||
|
|
||||||
|
function openModal() {
|
||||||
|
buildForm();
|
||||||
|
overlay.style.display = 'flex';
|
||||||
|
document.body.style.overflow = 'hidden';
|
||||||
|
setTimeout(() => { const n = document.getElementById('cm-name'); if (n) n.focus(); }, 80);
|
||||||
|
}
|
||||||
|
|
||||||
|
window._closeContact = function() {
|
||||||
|
overlay.style.display = 'none';
|
||||||
|
document.body.style.overflow = '';
|
||||||
|
};
|
||||||
|
|
||||||
|
async function submit() {
|
||||||
|
const name = document.getElementById('cm-name').value.trim();
|
||||||
|
const email = document.getElementById('cm-email').value.trim();
|
||||||
|
const phone = document.getElementById('cm-phone').value.trim();
|
||||||
|
const text = document.getElementById('cm-text').value.trim();
|
||||||
|
const errEl = document.getElementById('cm-error');
|
||||||
|
const btn = document.getElementById('cm-submit');
|
||||||
|
|
||||||
|
const emailRe = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||||
|
const phoneRe = /^[\+\d][\d\s\-\(\)]{6,18}$/;
|
||||||
|
const errors = [];
|
||||||
|
[[!name, 'cm-name'], [!emailRe.test(email), 'cm-email'],
|
||||||
|
[!phoneRe.test(phone), 'cm-phone'], [!text, 'cm-text']].forEach(([bad, id]) => {
|
||||||
|
document.getElementById(id).style.borderColor = bad ? '#e74c3c' : '#c8d8f7';
|
||||||
|
if (bad) errors.push(id);
|
||||||
|
});
|
||||||
|
if (errors.length) {
|
||||||
|
errEl.textContent = 'Пожалуйста, заполните все поля корректно';
|
||||||
|
errEl.style.display = 'block';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
btn.disabled = true;
|
||||||
|
btn.textContent = 'Отправка...';
|
||||||
|
errEl.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 style="text-align:center;padding:2rem 0">' +
|
||||||
|
'<div style="width:56px;height:56px;border-radius:50%;background:linear-gradient(135deg,#1f4ea3,#3978e0);' +
|
||||||
|
'color:#fff;font-size:1.8rem;display:flex;align-items:center;justify-content:center;margin:0 auto 1rem;">✓</div>' +
|
||||||
|
'<div style="font-size:1.1rem;font-weight:800;color:#1a3e79;">Отправлено!</div>' +
|
||||||
|
'<div style="color:#526079;font-size:.9rem;margin-top:.4rem;">Постараюсь ответить в ближайшее время</div></div>';
|
||||||
|
document.getElementById('cm-footer').innerHTML =
|
||||||
|
'<button onclick="window._closeContact()" style="background:linear-gradient(135deg,#1f4ea3,#3978e0);border:none;' +
|
||||||
|
'border-radius:9px;padding:.6rem 1.5rem;color:#fff;font-weight:700;cursor:pointer;font-family:Manrope,sans-serif;">Закрыть</button>';
|
||||||
|
} catch(e) {
|
||||||
|
errEl.textContent = e.message || 'Ошибка отправки';
|
||||||
|
errEl.style.display = 'block';
|
||||||
|
btn.disabled = false;
|
||||||
|
btn.textContent = 'Отправить';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('btn-contact-ruslan').addEventListener('click', openModal);
|
||||||
|
overlay.addEventListener('click', e => { if (e.target === overlay) window._closeContact(); });
|
||||||
|
document.addEventListener('keydown', e => { if (e.key === 'Escape') window._closeContact(); });
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -805,6 +805,46 @@ def health():
|
|||||||
return {"status": "ok"}
|
return {"status": "ok"}
|
||||||
|
|
||||||
|
|
||||||
|
@bp.post("/api/contact")
|
||||||
|
def contact():
|
||||||
|
import re as _re
|
||||||
|
data = request.get_json(silent=True) or {}
|
||||||
|
name = str(data.get("name", "")).strip()
|
||||||
|
email = str(data.get("email", "")).strip()
|
||||||
|
phone = str(data.get("phone", "")).strip()
|
||||||
|
text = str(data.get("text", "")).strip()
|
||||||
|
|
||||||
|
if not all([name, email, phone, text]):
|
||||||
|
return jsonify({"detail": "Заполните все обязательные поля"}), 422
|
||||||
|
if not _re.match(r"^[^\s@]+@[^\s@]+\.[^\s@]+$", email):
|
||||||
|
return jsonify({"detail": "Некорректный email"}), 422
|
||||||
|
if not _re.match(r"^[\+\d][\d\s\-\(\)]{6,18}$", phone):
|
||||||
|
return jsonify({"detail": "Некорректный номер телефона"}), 422
|
||||||
|
|
||||||
|
divider = "━" * 22
|
||||||
|
msg = (
|
||||||
|
f"🔔 *Сообщение через форму*\n{divider}\n\n"
|
||||||
|
f"👤 *Имя:* {name}\n"
|
||||||
|
f"📧 *Email:* {email}\n"
|
||||||
|
f"📱 *Телефон:* {phone}\n\n"
|
||||||
|
f"💬 *Сообщение:*\n{text}"
|
||||||
|
)
|
||||||
|
payload = json.dumps({
|
||||||
|
"chat_id": TELEGRAM_CHAT_ID,
|
||||||
|
"text": msg,
|
||||||
|
"parse_mode": "Markdown",
|
||||||
|
}).encode()
|
||||||
|
url = f"https://api.telegram.org/bot{TELEGRAM_BOT_TOKEN}/sendMessage"
|
||||||
|
try:
|
||||||
|
req = Request(url, data=payload, headers={"Content-Type": "application/json"})
|
||||||
|
with urlopen(req, timeout=10) as resp:
|
||||||
|
resp.read()
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({"detail": f"Ошибка отправки: {e}"}), 502
|
||||||
|
|
||||||
|
return jsonify({"ok": True})
|
||||||
|
|
||||||
|
|
||||||
@bp.get("/assets/mont-logo")
|
@bp.get("/assets/mont-logo")
|
||||||
def mont_logo():
|
def mont_logo():
|
||||||
return send_from_directory(BASE_DIR, "mont_logo.png")
|
return send_from_directory(BASE_DIR, "mont_logo.png")
|
||||||
|
|||||||
Reference in New Issue
Block a user