feat: news/events pages, pagination, cookie banner, MONT→MONT rename, logo update, admin improvements, API endpoints, dynamic links by domain
This commit is contained in:
+12
-3
@@ -106,11 +106,20 @@
|
||||
<form method="post" class="admin-create">
|
||||
<input type="hidden" name="scope" value="{{ scope }}" />
|
||||
<input type="hidden" name="action" value="create_admin" />
|
||||
<input type="text" name="username" placeholder="Логин нового админа" required />
|
||||
<label><input type="checkbox" name="allow_infra" checked /> Инфраструктура</label>
|
||||
<label><input type="checkbox" name="allow_ib" checked /> ИБ</label>
|
||||
<input type="text" name="username" placeholder="Логин нового пользователя" required />
|
||||
<select name="new_role" id="newRoleSelect" style="padding:8px 10px;border-radius:8px;border:1px solid #ccd8f0;font-family:Manrope,sans-serif;font-size:13px;color:#1a3060;" onchange="toggleScopeFields(this.value)">
|
||||
<option value="admin">Администратор матрицы</option>
|
||||
<option value="news_editor">Редактор новостей</option>
|
||||
</select>
|
||||
<span id="scopeFields"><label><input type="checkbox" name="allow_infra" checked /> Инфраструктура</label>
|
||||
<label><input type="checkbox" name="allow_ib" checked /> ИБ</label></span>
|
||||
<button class="pri" type="submit">Создать</button>
|
||||
</form>
|
||||
<script>
|
||||
function toggleScopeFields(role) {
|
||||
document.getElementById('scopeFields').style.display = role === 'news_editor' ? 'none' : '';
|
||||
}
|
||||
</script>
|
||||
<div class="admin-users-list">
|
||||
{% for user in admin_users %}
|
||||
<form class="admin-user-item" method="post">
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
<!doctype html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>{{ event.title }} — MONT</title>
|
||||
<meta name="description" content="{{ event.body[:160] | striptags | replace('\n',' ') }}" />
|
||||
<meta name="robots" content="index, follow" />
|
||||
<link rel="canonical" href="{{ base_url }}/events/{{ event.slug }}" />
|
||||
<meta property="og:type" content="article" />
|
||||
<meta property="og:url" content="{{ base_url }}/events/{{ event.slug }}" />
|
||||
<meta property="og:title" content="{{ event.title }} — MONT" />
|
||||
{% if event.image %}<meta property="og:image" content="{{ base_url }}/static/{{ event.image }}" />{% endif %}
|
||||
<link rel="icon" type="image/svg+xml" href="/static/favicon.svg" />
|
||||
<link rel="icon" type="image/png" href="/static/favicon.png" />
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Manrope:wght@400;600;700;800&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
:root { --brand: #1f4ea3; --brand2: #3978e0; --bg: #eef4ff; }
|
||||
* { box-sizing: border-box; }
|
||||
body { margin: 0; font-family: Manrope, sans-serif; background: var(--bg); color: #15203b; min-height: 100vh; }
|
||||
.wrap { width: min(1100px, calc(100% - 32px)); margin: 0 auto; padding: 28px 0 60px; }
|
||||
.back-btn {
|
||||
display: inline-flex; align-items: center; gap: 6px;
|
||||
font-size: 13px; font-weight: 700; color: var(--brand);
|
||||
text-decoration: none; margin-bottom: 20px;
|
||||
padding: 8px 16px; border-radius: 999px;
|
||||
background: #fff; border: 1px solid #c8d8f7; transition: .15s ease;
|
||||
}
|
||||
.back-btn:hover { background: #eef4ff; transform: translateX(-2px); }
|
||||
.article-card {
|
||||
background: #fff; border-radius: 20px;
|
||||
border: 1px solid #dae6ff;
|
||||
box-shadow: 0 12px 40px rgba(16,43,95,.1);
|
||||
overflow: hidden;
|
||||
}
|
||||
.article-header { display: flex; gap: 20px; align-items: flex-start; padding: 28px 32px 0; }
|
||||
.article-hero-img { flex-shrink: 0; width: 160px; height: 106px; object-fit: cover; border-radius: 12px; display: block; }
|
||||
.article-header-text { flex: 1; min-width: 0; }
|
||||
.article-body-wrap { padding: 20px 32px 36px; }
|
||||
.event-date-badge {
|
||||
display: inline-block; background: linear-gradient(135deg,#1f4ea3,#3978e0);
|
||||
color: #fff; font-size: 12px; font-weight: 800;
|
||||
padding: 4px 14px; border-radius: 999px; margin-bottom: 10px;
|
||||
}
|
||||
.article-title { font-size: 24px; font-weight: 800; color: #1a3e79; margin: 0; line-height: 1.25; }
|
||||
.article-body { font-size: 15px; color: #2e3f58; line-height: 1.75; }
|
||||
.article-body p { margin: 0 0 16px; }
|
||||
.article-body p:last-child { margin-bottom: 0; }
|
||||
.article-body a { color: var(--brand); font-weight: 600; text-decoration: none; }
|
||||
.article-body a:hover { text-decoration: underline; }
|
||||
.article-body strong, .article-body b { font-weight: 700; color: #1a3e79; }
|
||||
.article-body em, .article-body i { font-style: italic; }
|
||||
.article-body ul, .article-body ol { margin: 0 0 16px 20px; padding: 0; }
|
||||
.article-body li { margin-bottom: 6px; }
|
||||
.article-body h2, .article-body h3, .article-body h4 { font-weight: 800; color: #1a3e79; margin: 20px 0 10px; }
|
||||
.article-body h2 { font-size: 20px; }
|
||||
.article-body h3 { font-size: 17px; }
|
||||
.article-body blockquote { border-left: 3px solid var(--brand); margin: 16px 0; padding: 8px 16px; background: #f0f5ff; border-radius: 0 8px 8px 0; color: #526079; }
|
||||
.article-body table { width: 100%; border-collapse: collapse; margin: 0 0 16px; font-size: 14px; }
|
||||
.article-body th, .article-body td { border: 1px solid #dae6ff; padding: 8px 12px; text-align: left; }
|
||||
.article-body th { background: #eef4ff; font-weight: 700; color: #1a3e79; }
|
||||
.article-contact { margin-top: 28px; padding-top: 20px; border-top: 1px solid #e8f0ff; font-size: 13px; color: #526079; }
|
||||
.article-contact a { color: var(--brand); font-weight: 700; text-decoration: none; }
|
||||
@media (max-width: 640px) {
|
||||
.article-header { flex-direction: column; padding: 20px 20px 0; }
|
||||
.article-hero-img { width: 100%; height: 140px; }
|
||||
.article-body-wrap { padding: 16px 20px 28px; }
|
||||
.article-title { font-size: 20px; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrap">
|
||||
<a href="/events" class="back-btn">← Все мероприятия</a>
|
||||
<article class="article-card">
|
||||
<div class="article-header">
|
||||
{% if event.image %}
|
||||
<img class="article-hero-img" src="/static/{{ event.image }}" alt="{{ event.title }}" />
|
||||
{% endif %}
|
||||
<div class="article-header-text">
|
||||
<span class="event-date-badge">📅 {{ event.event_date[8:10] }}.{{ event.event_date[5:7] }}.{{ event.event_date[:4] }}</span>
|
||||
<h1 class="article-title">{{ event.title }}</h1>
|
||||
</div>
|
||||
</div>
|
||||
<div class="article-body-wrap">
|
||||
<div class="article-body">
|
||||
{% if '<' in event.body %}
|
||||
{{ event.body | safe }}
|
||||
{% else %}
|
||||
{% for paragraph in event.body.split('\n\n') %}
|
||||
<p>{{ paragraph }}</p>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if event.register_url %}
|
||||
<div style="margin-top:24px;">
|
||||
<a href="{{ event.register_url }}" target="_blank" rel="noopener"
|
||||
style="display:inline-block;background:linear-gradient(135deg,#1f4ea3,#3978e0);
|
||||
color:#fff;font-weight:800;font-size:15px;padding:12px 28px;
|
||||
border-radius:12px;text-decoration:none;transition:.15s;"
|
||||
onmouseover="this.style.opacity='.85'" onmouseout="this.style.opacity='1'">
|
||||
Зарегистрироваться →
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="article-contact">
|
||||
По вопросам участия: <a href="https://www.mont.ru/ru-ru/events" target="_blank" rel="noopener">mont.ru/events</a>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,129 @@
|
||||
<!doctype html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Мероприятия MONT</title>
|
||||
<meta name="description" content="Предстоящие мероприятия, вебинары и конференции дистрибьютора MONT." />
|
||||
<meta name="robots" content="index, follow" />
|
||||
<link rel="canonical" href="{{ base_url }}/events" />
|
||||
<link rel="icon" type="image/svg+xml" href="/static/favicon.svg" />
|
||||
<link rel="icon" type="image/png" href="/static/favicon.png" />
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Manrope:wght@400;600;700;800&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
:root { --brand: #1f4ea3; --brand2: #3978e0; --bg: #eef4ff; }
|
||||
* { box-sizing: border-box; }
|
||||
body { margin: 0; font-family: Manrope, sans-serif; background: var(--bg); color: #15203b; min-height: 100vh; }
|
||||
.wrap { width: min(900px, calc(100% - 32px)); margin: 0 auto; padding: 28px 0 60px; }
|
||||
.back-btn {
|
||||
display: inline-flex; align-items: center; gap: 6px;
|
||||
font-size: 13px; font-weight: 700; color: var(--brand);
|
||||
text-decoration: none; margin-bottom: 24px;
|
||||
padding: 8px 16px; border-radius: 999px;
|
||||
background: #fff; border: 1px solid #c8d8f7; transition: .15s;
|
||||
}
|
||||
.back-btn:hover { background: #eef4ff; transform: translateX(-2px); }
|
||||
h1 { font-size: 28px; font-weight: 800; color: #1a3e79; margin: 0 0 24px; }
|
||||
.top3 { margin-bottom: 32px; }
|
||||
.top3-label {
|
||||
font-size: 11px; font-weight: 800; letter-spacing: 1.2px;
|
||||
text-transform: uppercase; color: #7a9bc0; margin-bottom: 14px;
|
||||
}
|
||||
.top3-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px; }
|
||||
.top3-card {
|
||||
background: #fff; border-radius: 16px; border: 1px solid #dae6ff;
|
||||
box-shadow: 0 6px 20px rgba(16,43,95,.08);
|
||||
overflow: hidden; text-decoration: none; color: inherit;
|
||||
transition: .18s ease; display: flex; flex-direction: column;
|
||||
}
|
||||
.top3-card:hover { transform: translateY(-3px); box-shadow: 0 12px 32px rgba(16,43,95,.14); }
|
||||
.top3-card img { width: 100%; height: 160px; object-fit: cover; display: block; }
|
||||
.top3-card .no-img { width: 100%; height: 100px; background: linear-gradient(135deg, #e8f4f0, #d0eee4); display: flex; align-items: center; justify-content: center; }
|
||||
.top3-card .no-img-icon { font-size: 36px; opacity: .5; }
|
||||
.top3-card-body { padding: 14px 16px 18px; flex: 1; display: flex; flex-direction: column; }
|
||||
.event-date-badge {
|
||||
display: inline-block; background: linear-gradient(135deg,#1f4ea3,#3978e0);
|
||||
color: #fff; font-size: 11px; font-weight: 800; letter-spacing: .3px;
|
||||
padding: 3px 10px; border-radius: 999px; margin-bottom: 8px; align-self: flex-start;
|
||||
}
|
||||
.top3-card-title { font-size: 14px; font-weight: 800; color: #1a3e79; line-height: 1.4; flex: 1; margin: 0 0 12px; }
|
||||
.top3-card-excerpt { font-size: 12px; color: #5a7090; line-height: 1.55; margin: 0 0 12px; flex: 1;
|
||||
display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden; }
|
||||
.read-more { font-size: 12px; font-weight: 700; color: var(--brand2); }
|
||||
.divider { border: none; border-top: 2px solid #e0eaff; margin: 0 0 24px; }
|
||||
.rest-label { font-size: 11px; font-weight: 800; letter-spacing: 1.2px; text-transform: uppercase; color: #7a9bc0; margin-bottom: 16px; }
|
||||
.event-row {
|
||||
background: #fff; border-radius: 14px; border: 1px solid #dae6ff;
|
||||
display: flex; gap: 0; overflow: hidden; margin-bottom: 12px;
|
||||
text-decoration: none; color: inherit;
|
||||
transition: .18s ease; box-shadow: 0 4px 14px rgba(16,43,95,.06);
|
||||
}
|
||||
.event-row:hover { transform: translateY(-2px); box-shadow: 0 8px 24px rgba(16,43,95,.12); }
|
||||
.event-row img { width: 120px; height: 90px; object-fit: cover; flex-shrink: 0; }
|
||||
.event-row .no-img-row { width: 80px; flex-shrink: 0; background: linear-gradient(135deg, #e8f4f0, #d0eee4); }
|
||||
.event-row-body { padding: 12px 16px; display: flex; flex-direction: column; justify-content: center; }
|
||||
.event-row-date { font-size: 11px; color: #3978e0; font-weight: 700; margin-bottom: 4px; }
|
||||
.event-row-title { font-size: 14px; font-weight: 800; color: #1a3e79; line-height: 1.35; margin: 0 0 6px; }
|
||||
.event-row-excerpt { font-size: 12px; color: #5a7090; line-height: 1.5; margin: 0;
|
||||
display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
|
||||
.empty { text-align: center; padding: 60px 0; color: #9ab0d0; font-size: 15px; }
|
||||
@media (max-width: 700px) { .top3-grid { grid-template-columns: 1fr; } .event-row img { width: 90px; height: 70px; } }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrap">
|
||||
<a href="/" class="back-btn">← На главную</a>
|
||||
<h1>Мероприятия MONT</h1>
|
||||
|
||||
{% if events %}
|
||||
{% set top3 = events[:3] %}
|
||||
{% set rest = events[3:] %}
|
||||
|
||||
<div class="top3">
|
||||
<div class="top3-label">Ближайшие мероприятия</div>
|
||||
<div class="top3-grid">
|
||||
{% for e in top3 %}
|
||||
<a class="top3-card" href="/events/{{ e.slug }}">
|
||||
{% if e.image %}
|
||||
<img src="/static/{{ e.image }}" alt="{{ e.title }}" />
|
||||
{% else %}
|
||||
<div class="no-img"><span class="no-img-icon">📅</span></div>
|
||||
{% endif %}
|
||||
<div class="top3-card-body">
|
||||
<span class="event-date-badge">{{ e.event_date[8:10] }}.{{ e.event_date[5:7] }}.{{ e.event_date[:4] }}</span>
|
||||
<h2 class="top3-card-title">{{ e.title }}</h2>
|
||||
<p class="top3-card-excerpt">{{ e.body | striptags | truncate(180, true, '...') }}</p>
|
||||
<span class="read-more">Подробнее →</span>
|
||||
</div>
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if rest %}
|
||||
<hr class="divider" />
|
||||
<div class="rest-label">Все мероприятия</div>
|
||||
{% for e in rest %}
|
||||
<a class="event-row" href="/events/{{ e.slug }}">
|
||||
{% if e.image %}
|
||||
<img src="/static/{{ e.image }}" alt="{{ e.title }}" />
|
||||
{% else %}
|
||||
<div class="no-img-row"></div>
|
||||
{% endif %}
|
||||
<div class="event-row-body">
|
||||
<div class="event-row-date">📅 {{ e.event_date[8:10] }}.{{ e.event_date[5:7] }}.{{ e.event_date[:4] }}</div>
|
||||
<div class="event-row-title">{{ e.title }}</div>
|
||||
<p class="event-row-excerpt">{{ e.body | striptags | truncate(160, true, '...') }}</p>
|
||||
</div>
|
||||
</a>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
{% else %}
|
||||
<div class="empty">Актуальных мероприятий нет</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
+138
-35
@@ -3,17 +3,17 @@
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Вендоры МОНТ — матрица продуктов и категорий дистрибьютора MONT</title>
|
||||
<meta name="description" content="Корзина продуктов МОНТ: актуальная матрица вендоров, категорий и продуктовых линеек дистрибьютора MONT. Инфраструктура и информационная безопасность. МОНТ Казань, Москва." />
|
||||
<meta name="keywords" content="вендоры МОНТ, вендоры MONT, дистрибьютор МОНТ, корзина продуктов МОНТ, матрица МОНТ, МОНТ Казань, партнёры МОНТ, ИБ вендоры, инфраструктура ПО, MONT vendor map" />
|
||||
<title>Вендоры MONT — матрица продуктов и категорий дистрибьютора MONT</title>
|
||||
<meta name="description" content="Корзина продуктов MONT: актуальная матрица вендоров, категорий и продуктовых линеек дистрибьютора MONT. Инфраструктура и информационная безопасность. MONT Казань, Москва." />
|
||||
<meta name="keywords" content="вендоры MONT, вендоры MONT, дистрибьютор MONT, корзина продуктов MONT, матрица MONT, MONT Казань, партнёры MONT, ИБ вендоры, инфраструктура ПО, MONT vendor map" />
|
||||
<meta name="robots" content="index, follow" />
|
||||
<link rel="canonical" href="{{ canonical_url }}" />
|
||||
|
||||
<!-- Open Graph -->
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:url" content="{{ canonical_url }}" />
|
||||
<meta property="og:title" content="Вендоры МОНТ — матрица продуктов и категорий" />
|
||||
<meta property="og:description" content="Актуальная матрица вендоров и продуктов дистрибьютора МОНТ. Инфраструктура и информационная безопасность." />
|
||||
<meta property="og:title" content="Вендоры MONT — матрица продуктов и категорий" />
|
||||
<meta property="og:description" content="Актуальная матрица вендоров и продуктов дистрибьютора MONT. Инфраструктура и информационная безопасность." />
|
||||
<meta property="og:image" content="{{ canonical_url }}/static/mont_logo.png" />
|
||||
<meta property="og:locale" content="ru_RU" />
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
{
|
||||
"@context": "https://schema.org",
|
||||
"@type": "Organization",
|
||||
"name": "МОНТ",
|
||||
"name": "MONT",
|
||||
"alternateName": "MONT",
|
||||
"description": "Дистрибьютор программного обеспечения. Корзина продуктов: вендоры, категории, продуктовые линейки в сфере инфраструктуры и информационной безопасности.",
|
||||
"url": "{{ canonical_url }}",
|
||||
@@ -31,6 +31,7 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<link rel="icon" type="image/svg+xml" href="/static/favicon.svg" />
|
||||
<link rel="icon" type="image/png" href="/static/favicon.png" />
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
@@ -41,21 +42,21 @@
|
||||
<main class="wrap">
|
||||
<section class="brand-strip">
|
||||
<div class="brand-logo">
|
||||
<img src="/static/mont_logo.png?v=2" alt="MONT logo" />
|
||||
<a id="logo-link" href="https://4mont.ru"><img src="/static/mont_logo.png?v=3" alt="MONT logo" /></a>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="hero">
|
||||
<div class="hero-layout">
|
||||
<div>
|
||||
<h1>Вендоры в корзине МОНТ</h1>
|
||||
<h1>Вендоры в портфеле MONT</h1>
|
||||
<p>Актуальная матрица вендоров, продуктов и категорий. Выбирайте вендоров или категории, чтобы видеть релевантные продуктовые линейки в Инфраструктуре и ИБ.</p>
|
||||
<div class="mode-switch-row">
|
||||
<div class="mode-switch">
|
||||
<button id="modeInfra" class="mode-btn active" type="button">Инфраструктура</button>
|
||||
<button id="modeIb" class="mode-btn" type="button">ИБ</button>
|
||||
</div>
|
||||
<a href="https://stend.4mont.ru/" target="_blank" rel="noopener" class="polygon-btn">Инфраструктурный полигон МОНТ</a>
|
||||
<a id="polygon-link" href="https://stend.4mont.ru/" target="_blank" rel="noopener" class="polygon-btn">Инфраструктурный полигон MONT</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -63,39 +64,108 @@
|
||||
|
||||
<!-- SSR-контент для поисковых роботов -->
|
||||
<div style="position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;" aria-hidden="true">
|
||||
<h2>Вендоры дистрибьютора МОНТ (MONT)</h2>
|
||||
<p>Матрица вендоров, продуктов и категорий компании МОНТ — ведущего дистрибьютора программного обеспечения в России. Инфраструктура и информационная безопасность.</p>
|
||||
<h2>Вендоры дистрибьютора MONT (MONT)</h2>
|
||||
<p>Матрица вендоров, продуктов и категорий компании MONT — ведущего дистрибьютора программного обеспечения в России. Инфраструктура и информационная безопасность.</p>
|
||||
<ul>{% for v in ssr_vendors %}<li>{{ v }}</li>{% endfor %}</ul>
|
||||
<h2>Категории продуктов МОНТ</h2>
|
||||
<h2>Категории продуктов MONT</h2>
|
||||
<ul>{% for c in ssr_categories %}<li>{{ c }}</li>{% endfor %}</ul>
|
||||
</div>
|
||||
|
||||
<section class="board">
|
||||
<article class="card">
|
||||
<div class="card-header"><h2>Вендоры</h2><span class="count-badge" id="vendorBadge">...</span></div>
|
||||
<input id="vendorSearch" class="search" placeholder="Поиск вендора..." />
|
||||
<div id="vendorList" class="chip-grid"></div>
|
||||
</article>
|
||||
<div class="page-layout">
|
||||
<div class="main-col">
|
||||
<section class="board">
|
||||
<article class="card">
|
||||
<div class="card-header"><h2>Вендоры</h2><span class="count-badge" id="vendorBadge">...</span></div>
|
||||
<input id="vendorSearch" class="search" placeholder="Поиск вендора..." />
|
||||
<div id="vendorList" class="chip-grid"></div>
|
||||
</article>
|
||||
|
||||
<article class="card">
|
||||
<div class="card-header"><h2>Категории</h2><span class="count-badge" id="categoryBadge">...</span></div>
|
||||
<input id="categorySearch" class="search" placeholder="Поиск категории..." />
|
||||
<div id="categoryList" class="chip-grid"></div>
|
||||
</article>
|
||||
</section>
|
||||
<article class="card">
|
||||
<div class="card-header"><h2>Категории</h2><span class="count-badge" id="categoryBadge">...</span></div>
|
||||
<input id="categorySearch" class="search" placeholder="Поиск категории..." />
|
||||
<div id="categoryList" class="chip-grid"></div>
|
||||
</article>
|
||||
</section>
|
||||
|
||||
<div class="footer-bar">
|
||||
<div id="stats">Загрузка...</div>
|
||||
<button class="action" id="clearBtn">Сбросить фильтры</button>
|
||||
<div class="footer-bar">
|
||||
<div id="stats">Загрузка...</div>
|
||||
<button class="action" id="clearBtn">Сбросить фильтры</button>
|
||||
</div>
|
||||
|
||||
<section class="result">
|
||||
<div class="result-head">
|
||||
<h3>Вендоры и продукты (после фильтрации)</h3>
|
||||
</div>
|
||||
<div id="activeFilters" class="active-filters"></div>
|
||||
<div id="resultRows" class="rows"></div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<aside class="news-sidebar">
|
||||
<div class="news-widget">
|
||||
<div class="news-widget-head">
|
||||
<h2>Новости MONT</h2>
|
||||
<div style="display:flex;gap:6px;align-items:center;">
|
||||
{% if is_news_editor %}
|
||||
<a href="/news-admin" class="news-all-link" style="background:linear-gradient(135deg,#1f4ea3,#3978e0);color:#fff;border-color:transparent;">✏️ Редактор</a>
|
||||
{% endif %}
|
||||
<a href="/news" class="news-all-link">Все →</a>
|
||||
</div>
|
||||
</div>
|
||||
{% if latest_news %}
|
||||
<div class="news-list">
|
||||
{% for n in latest_news %}
|
||||
<a class="news-card" href="/news/{{ n.slug }}">
|
||||
<div class="news-card-img-wrap">
|
||||
{% if n.image %}
|
||||
<img class="news-card-img" src="/static/{{ n.image }}" alt="{{ n.title }}" />
|
||||
{% else %}
|
||||
<span class="news-card-no-img">📰</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="news-card-body">
|
||||
<div class="news-card-date">{{ n.created_at[8:10] }}.{{ n.created_at[5:7] }}.{{ n.created_at[:4] }}</div>
|
||||
<h3 class="news-card-title">{{ n.title }}</h3>
|
||||
<span class="news-card-btn">Подробнее →</span>
|
||||
</div>
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<p class="news-empty">Новостей пока нет</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="news-widget" style="margin-top:14px;">
|
||||
<div class="news-widget-head">
|
||||
<h2>Мероприятия MONT</h2>
|
||||
<a href="/events" class="news-all-link">Все →</a>
|
||||
</div>
|
||||
{% if upcoming_events %}
|
||||
<div class="news-list">
|
||||
{% for e in upcoming_events %}
|
||||
<a class="news-card" href="/events/{{ e.slug }}">
|
||||
<div class="news-card-img-wrap">
|
||||
{% if e.image %}
|
||||
<img class="news-card-img" src="/static/{{ e.image }}" alt="{{ e.title }}" />
|
||||
{% else %}
|
||||
<span class="news-card-no-img">📅</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="news-card-body">
|
||||
<div class="news-card-date">{{ e.event_date[8:10] }}.{{ e.event_date[5:7] }}.{{ e.event_date[:4] }}</div>
|
||||
<h3 class="news-card-title">{{ e.title }}</h3>
|
||||
<span class="news-card-btn">Подробнее →</span>
|
||||
</div>
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<p class="news-empty">Актуальных мероприятий нет</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
|
||||
<section class="result">
|
||||
<div class="result-head">
|
||||
<h3>Вендоры и продукты (после фильтрации)</h3>
|
||||
</div>
|
||||
<div id="activeFilters" class="active-filters"></div>
|
||||
<div id="resultRows" class="rows"></div>
|
||||
</section>
|
||||
<div class="credit">
|
||||
<button type="button" id="btn-contact-ruslan">Made by Galyaviev</button>
|
||||
</div>
|
||||
@@ -224,10 +294,43 @@
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById('btn-contact-ruslan').addEventListener('click', openModal);
|
||||
document.getElementById('btn-contact-ruslan').addEventListener('click', () => { window.location.href = 'mailto:ruslan@ipcom.su'; });
|
||||
overlay.addEventListener('click', e => { if (e.target === overlay) window._closeContact(); });
|
||||
document.addEventListener('keydown', e => { if (e.key === 'Escape') window._closeContact(); });
|
||||
})();
|
||||
</script>
|
||||
<script>
|
||||
(function(){
|
||||
var isMont = location.hostname === 'maps.mont.ru';
|
||||
|
||||
var poly = document.getElementById('polygon-link');
|
||||
if (poly) poly.href = isMont ? 'https://stand.mont.ru/' : 'https://stend.4mont.ru/';
|
||||
|
||||
var logo = document.getElementById('logo-link');
|
||||
if (logo) logo.href = isMont ? 'https://www.mont.ru' : 'https://4mont.ru';
|
||||
})();
|
||||
</script>
|
||||
<div id="cookie-banner" style="display:none;position:fixed;bottom:0;left:0;right:0;z-index:9999;background:#1a3060;color:#fff;padding:1rem 1.5rem;box-shadow:0 -2px 16px rgba(0,0,0,.2);align-items:center;justify-content:space-between;gap:1rem;flex-wrap:wrap;">
|
||||
<span style="font-size:.9rem;line-height:1.5;">
|
||||
Мы используем файлы cookie, чтобы сделать работу с сайтом удобнее. Нажмите «Принять», чтобы согласиться с использованием файлов cookie в соответствии с
|
||||
<a href="https://www.mont.ru/ru-ru/confidential" target="_blank" rel="noopener"
|
||||
style="color:#7eb3ff;text-decoration:underline;">политикой конфиденциальности</a>.
|
||||
</span>
|
||||
<button id="cookie-accept" style="background:linear-gradient(135deg,#3978e0,#5a9ef5);border:none;border-radius:9px;
|
||||
padding:.55rem 1.4rem;color:#fff;font-weight:700;font-family:Manrope,sans-serif;cursor:pointer;white-space:nowrap;">
|
||||
Принять
|
||||
</button>
|
||||
</div>
|
||||
<script>
|
||||
(function(){
|
||||
if (localStorage.getItem('cookie_accepted')) return;
|
||||
var b = document.getElementById('cookie-banner');
|
||||
b.style.display = 'flex';
|
||||
document.getElementById('cookie-accept').addEventListener('click', function(){
|
||||
localStorage.setItem('cookie_accepted', '1');
|
||||
b.style.display = 'none';
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -0,0 +1,306 @@
|
||||
<!doctype html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Редактор новостей — MONT</title>
|
||||
<meta name="robots" content="noindex, nofollow" />
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Manrope:wght@400;600;700;800&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
:root { --brand: #1f4ea3; --brand2: #3978e0; --bg: #eef4ff; }
|
||||
* { box-sizing: border-box; }
|
||||
body { margin: 0; font-family: Manrope, sans-serif; background: var(--bg); color: #15203b; min-height: 100vh; }
|
||||
.wrap { width: min(860px, calc(100% - 32px)); margin: 0 auto; padding: 28px 0 60px; }
|
||||
.top { display: flex; align-items: center; justify-content: space-between; margin-bottom: 24px; flex-wrap: wrap; gap: 10px; }
|
||||
.top strong { font-size: 20px; font-weight: 800; color: #1a3e79; }
|
||||
.top small { font-size: 13px; color: #7a9bc0; }
|
||||
.btn { display: inline-flex; align-items: center; gap: 6px; font-size: 13px; font-weight: 700;
|
||||
padding: 8px 18px; border-radius: 10px; border: none; cursor: pointer; font-family: Manrope, sans-serif; transition: .15s; }
|
||||
.btn-primary { background: linear-gradient(135deg, var(--brand), var(--brand2)); color: #fff; }
|
||||
.btn-primary:hover { opacity: .88; }
|
||||
.btn-danger { background: #fff0f0; color: #c0392b; border: 1px solid #fcc; }
|
||||
.btn-danger:hover { background: #ffe0e0; }
|
||||
.btn-logout { background: #f0f5ff; color: #526079; border: 1px solid #c8d8f7; }
|
||||
.alert { padding: 10px 16px; border-radius: 10px; font-size: 13px; font-weight: 600; margin-bottom: 16px; }
|
||||
.alert.ok { background: #edfaf3; color: #1a6b40; border: 1px solid #b3e8cc; }
|
||||
.alert.error { background: #fff0f0; color: #c0392b; border: 1px solid #fcc; }
|
||||
.box { background: #fff; border-radius: 18px; border: 1px solid #dae6ff; box-shadow: 0 8px 28px rgba(16,43,95,.08); padding: 24px 28px; margin-bottom: 20px; }
|
||||
.box h3 { font-size: 16px; font-weight: 800; color: #1a3e79; margin: 0 0 16px; }
|
||||
input[type=text], textarea {
|
||||
width: 100%; padding: 10px 14px; border: 1px solid #cfd9f0; border-radius: 10px;
|
||||
font-size: 14px; font-family: Manrope, sans-serif; color: #1a3060; outline: none; transition: .2s;
|
||||
}
|
||||
input[type=text]:focus, textarea:focus { border-color: #5b91f6; box-shadow: 0 0 0 3px rgba(91,145,246,.14); }
|
||||
textarea { resize: vertical; min-height: 160px; }
|
||||
.field { margin-bottom: 14px; }
|
||||
.field label { display: block; font-size: 12px; font-weight: 700; color: #526079; margin-bottom: 5px; letter-spacing: .2px; }
|
||||
.upload-zone {
|
||||
border: 2px dashed #c8d8f7; border-radius: 12px; padding: 18px;
|
||||
text-align: center; cursor: pointer; transition: .2s; background: #f8fbff;
|
||||
position: relative;
|
||||
}
|
||||
.upload-zone:hover, .upload-zone.dragover { border-color: #5b91f6; background: #eef4ff; }
|
||||
.upload-zone input[type=file] { position: absolute; inset: 0; opacity: 0; cursor: pointer; width: 100%; height: 100%; }
|
||||
.upload-zone .uz-label { font-size: 13px; color: #7a9bc0; font-weight: 600; pointer-events: none; }
|
||||
.upload-zone .uz-label span { color: var(--brand); }
|
||||
.img-preview { max-width: 100%; max-height: 200px; border-radius: 10px; margin-top: 10px; display: none; object-fit: cover; }
|
||||
.current-img { width: 100%; max-height: 160px; object-fit: cover; border-radius: 10px; margin-bottom: 8px; }
|
||||
.news-list { display: flex; flex-direction: column; gap: 12px; }
|
||||
.news-item { border: 1px solid #dae6ff; border-radius: 12px; padding: 14px 18px; background: #f8fbff; }
|
||||
.news-item-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; margin-bottom: 8px; }
|
||||
.news-item-title { font-size: 15px; font-weight: 800; color: #1a3e79; }
|
||||
.news-item-meta { font-size: 12px; color: #8aa0c0; }
|
||||
.news-item-actions { display: flex; gap: 8px; flex-wrap: wrap; align-items: center; }
|
||||
.badge-pub { font-size: 11px; font-weight: 700; padding: 3px 10px; border-radius: 999px; background: #edfaf3; color: #1a6b40; border: 1px solid #b3e8cc; }
|
||||
.badge-unpub { font-size: 11px; font-weight: 700; padding: 3px 10px; border-radius: 999px; background: #fff8e6; color: #7a5500; border: 1px solid #f0d080; }
|
||||
details summary { cursor: pointer; font-size: 13px; font-weight: 700; color: var(--brand); margin-top: 8px; }
|
||||
details .edit-form { margin-top: 12px; padding-top: 12px; border-top: 1px solid #e0eaff; }
|
||||
.pagination { display: flex; justify-content: center; align-items: center; gap: 6px; margin-top: 20px; flex-wrap: wrap; }
|
||||
.pagination a, .pagination span {
|
||||
display: inline-flex; align-items: center; justify-content: center;
|
||||
width: 34px; height: 34px; border-radius: 8px;
|
||||
font-size: 13px; font-weight: 700; text-decoration: none;
|
||||
border: 1px solid #c8d8f7; background: #fff; color: #1f4ea3; transition: .15s;
|
||||
}
|
||||
.pagination a:hover { background: #eef4ff; }
|
||||
.pagination span.current { background: linear-gradient(135deg,#1f4ea3,#3978e0); color: #fff; border-color: transparent; }
|
||||
.pagination span.dots { background: none; border-color: transparent; color: #9ab0d0; }
|
||||
.login-box { max-width: 360px; margin: 60px auto; background: #fff; border-radius: 20px; border: 1px solid #dae6ff; box-shadow: 0 12px 40px rgba(16,43,95,.1); padding: 32px; }
|
||||
.login-box h2 { font-size: 20px; font-weight: 800; color: #1a3e79; margin: 0 0 20px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrap">
|
||||
|
||||
{% if not is_news_editor %}
|
||||
<div class="login-box">
|
||||
<h2>Вход для редактора новостей</h2>
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
{% for cat, msg in messages %}
|
||||
<div class="alert {{ cat }}">{{ msg }}</div>
|
||||
{% endfor %}
|
||||
{% endwith %}
|
||||
<form method="post">
|
||||
<input type="hidden" name="action" value="news_login" />
|
||||
<div class="field"><label>Логин</label><input type="text" name="username" required autofocus /></div>
|
||||
<div class="field"><label>Пароль</label><input type="text" name="password" required /></div>
|
||||
<button class="btn btn-primary" type="submit" style="width:100%;justify-content:center;">Войти</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{% else %}
|
||||
<div class="top">
|
||||
<div>
|
||||
<strong>Редактор новостей MONT</strong><br>
|
||||
<small>{{ admin_login }}</small>
|
||||
</div>
|
||||
<div style="display:flex;gap:8px;">
|
||||
<a href="/" style="text-decoration:none;"><button class="btn btn-logout" type="button">На сайт</button></a>
|
||||
<form method="post" style="margin:0;">
|
||||
<input type="hidden" name="action" value="logout" />
|
||||
<button class="btn btn-logout" type="submit">Выйти</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
{% for cat, msg in messages %}
|
||||
<div class="alert {{ cat }}">{{ msg }}</div>
|
||||
{% endfor %}
|
||||
{% endwith %}
|
||||
|
||||
<div class="box">
|
||||
<h3>Добавить новость</h3>
|
||||
<form method="post" enctype="multipart/form-data">
|
||||
<input type="hidden" name="action" value="create_news" />
|
||||
<div class="field"><label>Заголовок *</label><input type="text" name="title" placeholder="Заголовок новости" required /></div>
|
||||
<div class="field">
|
||||
<label>Картинка (jpg, png, webp)</label>
|
||||
<div class="upload-zone" id="uzCreate">
|
||||
<input type="file" name="image" accept=".jpg,.jpeg,.png,.webp,.gif" onchange="previewImg(this,'prevCreate')" />
|
||||
<div class="uz-label">Перетащите файл, <span>выберите</span> или кликните в «Заголовок» и нажмите Ctrl+V</div>
|
||||
</div>
|
||||
<img id="prevCreate" class="img-preview" alt="" />
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>Текст новости * (разделяйте абзацы пустой строкой)</label>
|
||||
<textarea name="body" placeholder="Текст новости..."></textarea>
|
||||
</div>
|
||||
<button class="btn btn-primary" type="submit">Опубликовать</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="box">
|
||||
<h3>Все новости</h3>
|
||||
{% if all_news %}
|
||||
<div class="news-list">
|
||||
{% for n in all_news %}
|
||||
<div class="news-item">
|
||||
<div class="news-item-head">
|
||||
<div>
|
||||
<div class="news-item-title">{{ n.title }}</div>
|
||||
<div class="news-item-meta">{{ n.created_at[:10] }} · /news/{{ n.slug }}</div>
|
||||
</div>
|
||||
<div class="news-item-actions">
|
||||
<form method="post" style="margin:0;display:flex;align-items:center;gap:6px;">
|
||||
<input type="hidden" name="action" value="toggle_published" />
|
||||
<input type="hidden" name="news_id" value="{{ n.id }}" />
|
||||
<input type="checkbox" name="published" id="tog_{{ n.id }}"
|
||||
{% if n.published %}checked{% endif %}
|
||||
onchange="this.form.submit()"
|
||||
style="width:16px;height:16px;cursor:pointer;accent-color:#1f4ea3;" />
|
||||
<label for="tog_{{ n.id }}" style="font-size:12px;font-weight:700;cursor:pointer;
|
||||
color:{% if n.published %}#1a6b40{% else %}#7a5500{% endif %};">
|
||||
{% if n.published %}Опубликована{% else %}Скрыта{% endif %}
|
||||
</label>
|
||||
</form>
|
||||
<form method="post" onsubmit="return confirm('Удалить новость?')">
|
||||
<input type="hidden" name="action" value="delete_news" />
|
||||
<input type="hidden" name="news_id" value="{{ n.id }}" />
|
||||
<button class="btn btn-danger" type="submit" style="padding:5px 12px;font-size:12px;">Удалить</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<details>
|
||||
<summary>Редактировать</summary>
|
||||
<div class="edit-form">
|
||||
<form method="post" enctype="multipart/form-data">
|
||||
<input type="hidden" name="action" value="update_news" />
|
||||
<input type="hidden" name="news_id" value="{{ n.id }}" />
|
||||
<div class="field"><label>Заголовок</label><input type="text" name="title" value="{{ n.title }}" required /></div>
|
||||
<div class="field">
|
||||
<label>Картинка (оставьте пустым, чтобы не менять)</label>
|
||||
{% if n.image %}
|
||||
<img src="/static/{{ n.image }}" class="current-img" alt="" />
|
||||
{% endif %}
|
||||
<div class="upload-zone">
|
||||
<input type="file" name="image" accept=".jpg,.jpeg,.png,.webp,.gif" onchange="previewImg(this,'prev_{{ n.id }}')" />
|
||||
<div class="uz-label">Перетащите файл, <span>выберите</span> или кликните в «Заголовок» и нажмите Ctrl+V</div>
|
||||
</div>
|
||||
<img id="prev_{{ n.id }}" class="img-preview" alt="" />
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>Текст</label>
|
||||
<textarea name="body">{{ n.body if n.body is defined else '' }}</textarea>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>Дата публикации (ДД.ММ.ГГГГ ЧЧ:ММ)</label>
|
||||
<input type="text" name="created_at_edit"
|
||||
value="{{ n.created_at[8:10] }}.{{ n.created_at[5:7] }}.{{ n.created_at[:4] }} {{ n.created_at[11:16] }}"
|
||||
placeholder="29.05.2026 14:30" style="width:200px;" />
|
||||
</div>
|
||||
<div class="field" style="display:flex;align-items:center;gap:8px;">
|
||||
<input type="checkbox" name="published" id="pub_{{ n.id }}" {% if n.published %}checked{% endif %} style="width:auto;" />
|
||||
<label for="pub_{{ n.id }}" style="margin:0;font-size:13px;">Опубликована</label>
|
||||
</div>
|
||||
<button class="btn btn-primary" type="submit">Сохранить</button>
|
||||
</form>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<p style="color:#8aa0c0;font-size:14px;">Новостей пока нет.</p>
|
||||
{% endif %}
|
||||
|
||||
{% if total_adm_pages > 1 %}
|
||||
<div class="pagination">
|
||||
{% if adm_page > 1 %}
|
||||
<a href="?page={{ adm_page - 1 }}">‹</a>
|
||||
{% endif %}
|
||||
{% for p in range(1, total_adm_pages + 1) %}
|
||||
{% if p == adm_page %}
|
||||
<span class="current">{{ p }}</span>
|
||||
{% elif p == 1 or p == total_adm_pages or (p >= adm_page - 2 and p <= adm_page + 2) %}
|
||||
<a href="?page={{ p }}">{{ p }}</a>
|
||||
{% elif p == adm_page - 3 or p == adm_page + 3 %}
|
||||
<span class="dots">…</span>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% if adm_page < total_adm_pages %}
|
||||
<a href="?page={{ adm_page + 1 }}">›</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
<script>
|
||||
function previewImg(input, previewId) {
|
||||
const prev = document.getElementById(previewId);
|
||||
if (!prev) return;
|
||||
if (input.files && input.files[0]) {
|
||||
const reader = new FileReader();
|
||||
reader.onload = e => { prev.src = e.target.result; prev.style.display = 'block'; };
|
||||
reader.readAsDataURL(input.files[0]);
|
||||
}
|
||||
}
|
||||
|
||||
function applyFileToZone(file, zone) {
|
||||
if (!file || !file.type.startsWith('image/')) return false;
|
||||
const input = zone.querySelector('input[type=file]');
|
||||
if (!input) return false;
|
||||
const dt = new DataTransfer();
|
||||
dt.items.add(file);
|
||||
input.files = dt.files;
|
||||
input.dispatchEvent(new Event('change'));
|
||||
zone.classList.add('dragover');
|
||||
setTimeout(() => zone.classList.remove('dragover'), 600);
|
||||
return true;
|
||||
}
|
||||
|
||||
function getActiveZone() {
|
||||
// Prefer zone inside an open <details>
|
||||
const openDetails = document.querySelector('details[open]');
|
||||
if (openDetails) {
|
||||
const z = openDetails.querySelector('.upload-zone');
|
||||
if (z) return z;
|
||||
}
|
||||
// Fallback: create form zone
|
||||
return document.getElementById('uzCreate');
|
||||
}
|
||||
|
||||
document.querySelectorAll('.upload-zone').forEach(zone => {
|
||||
zone.addEventListener('dragover', e => { e.preventDefault(); zone.classList.add('dragover'); });
|
||||
zone.addEventListener('dragleave', () => zone.classList.remove('dragover'));
|
||||
zone.addEventListener('drop', e => {
|
||||
e.preventDefault(); zone.classList.remove('dragover');
|
||||
const input = zone.querySelector('input[type=file]');
|
||||
if (input && e.dataTransfer.files.length) {
|
||||
input.files = e.dataTransfer.files;
|
||||
input.dispatchEvent(new Event('change'));
|
||||
}
|
||||
});
|
||||
// Mark as active on click/focus
|
||||
zone.addEventListener('mouseenter', () => zone.dataset.lastActive = Date.now());
|
||||
});
|
||||
|
||||
function showToast(msg) {
|
||||
const hint = document.createElement('div');
|
||||
hint.textContent = msg;
|
||||
hint.style.cssText = 'position:fixed;bottom:24px;left:50%;transform:translateX(-50%);background:#1f4ea3;color:#fff;padding:10px 22px;border-radius:10px;font-size:13px;font-weight:700;z-index:9999;box-shadow:0 4px 16px rgba(0,0,0,.2);pointer-events:none;';
|
||||
document.body.appendChild(hint);
|
||||
setTimeout(() => hint.remove(), 2500);
|
||||
}
|
||||
|
||||
// Intercept paste in any field: if clipboard has image — apply to upload zone
|
||||
document.addEventListener('paste', e => {
|
||||
const items = Array.from((e.clipboardData || {}).items || []);
|
||||
showToast('paste: ' + items.map(i=>i.type).join(', ') || 'нет items');
|
||||
const imgItem = items.find(i => i.type.startsWith('image/'));
|
||||
if (!imgItem) return;
|
||||
const file = imgItem.getAsFile();
|
||||
if (!file) { showToast('getAsFile вернул null'); return; }
|
||||
const zone = getActiveZone();
|
||||
if (!zone) { showToast('зона не найдена'); return; }
|
||||
e.preventDefault();
|
||||
applyFileToZone(file, zone);
|
||||
showToast('📋 Картинка вставлена из буфера');
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,252 @@
|
||||
<!doctype html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>{{ article.title }} — MONT</title>
|
||||
<meta name="description" content="{{ article.body[:160] | replace('\n',' ') }}" />
|
||||
<meta name="robots" content="index, follow" />
|
||||
<link rel="canonical" href="{{ base_url }}/news/{{ article.slug }}" />
|
||||
<meta property="og:type" content="article" />
|
||||
<meta property="og:url" content="{{ base_url }}/news/{{ article.slug }}" />
|
||||
<meta property="og:title" content="{{ article.title }} — MONT" />
|
||||
<meta property="og:description" content="{{ article.body[:200] | replace('\n',' ') }}" />
|
||||
{% if article.image %}<meta property="og:image" content="{{ base_url }}/static/{{ article.image }}" />{% endif %}
|
||||
<link rel="icon" type="image/svg+xml" href="/static/favicon.svg" />
|
||||
<link rel="icon" type="image/png" href="/static/favicon.png" />
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Caveat:wght@600;700&family=Manrope:wght@400;600;700;800&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
:root { --brand: #1f4ea3; --brand2: #3978e0; --bg: #eef4ff; --radius: 16px; }
|
||||
* { box-sizing: border-box; }
|
||||
body { margin: 0; font-family: Manrope, sans-serif; background: var(--bg); color: #15203b; min-height: 100vh; }
|
||||
.wrap { width: min(1100px, calc(100% - 32px)); margin: 0 auto; padding: 28px 0 60px; }
|
||||
.back-btn {
|
||||
display: inline-flex; align-items: center; gap: 6px;
|
||||
font-size: 13px; font-weight: 700; color: var(--brand);
|
||||
text-decoration: none; margin-bottom: 20px;
|
||||
padding: 8px 16px; border-radius: 999px;
|
||||
background: #fff; border: 1px solid #c8d8f7; transition: .15s ease;
|
||||
}
|
||||
.back-btn:hover { background: #eef4ff; transform: translateX(-2px); }
|
||||
.article-card {
|
||||
background: #fff; border-radius: 20px;
|
||||
border: 1px solid #dae6ff;
|
||||
box-shadow: 0 12px 40px rgba(16,43,95,.1);
|
||||
overflow: hidden;
|
||||
}
|
||||
.article-header { display: flex; gap: 20px; align-items: flex-start; padding: 28px 32px 0; }
|
||||
.article-hero-img {
|
||||
flex-shrink: 0;
|
||||
width: 160px; height: 106px;
|
||||
object-fit: cover;
|
||||
border-radius: 12px;
|
||||
display: block;
|
||||
}
|
||||
.article-header-text { flex: 1; min-width: 0; }
|
||||
.article-body-wrap { padding: 20px 32px 36px; }
|
||||
.article-date { font-size: 12px; color: #8aa0c0; font-weight: 600; margin-bottom: 8px; }
|
||||
.article-title { font-size: 24px; font-weight: 800; color: #1a3e79; margin: 0; line-height: 1.25; }
|
||||
.article-body { font-size: 15px; color: #2e3f58; line-height: 1.75; }
|
||||
.article-body p { margin: 0 0 16px; }
|
||||
.article-body p:last-child { margin-bottom: 0; }
|
||||
.article-body a { color: var(--brand); font-weight: 600; text-decoration: none; }
|
||||
.article-body a:hover { text-decoration: underline; }
|
||||
.article-body strong, .article-body b { font-weight: 700; color: #1a3e79; }
|
||||
.article-body em, .article-body i { font-style: italic; }
|
||||
.article-body ul, .article-body ol { margin: 0 0 16px 20px; padding: 0; }
|
||||
.article-body li { margin-bottom: 6px; }
|
||||
.article-body h2, .article-body h3, .article-body h4 { font-weight: 800; color: #1a3e79; margin: 20px 0 10px; line-height: 1.3; }
|
||||
.article-body h2 { font-size: 20px; }
|
||||
.article-body h3 { font-size: 17px; }
|
||||
.article-body blockquote { border-left: 3px solid var(--brand); margin: 16px 0; padding: 8px 16px; background: #f0f5ff; border-radius: 0 8px 8px 0; color: #526079; }
|
||||
.article-body table { width: 100%; border-collapse: collapse; margin: 0 0 16px; font-size: 14px; }
|
||||
.article-body th, .article-body td { border: 1px solid #dae6ff; padding: 8px 12px; text-align: left; }
|
||||
.article-body th { background: #eef4ff; font-weight: 700; color: #1a3e79; }
|
||||
.article-contact {
|
||||
margin-top: 28px; padding-top: 20px;
|
||||
border-top: 1px solid #e8f0ff;
|
||||
font-size: 13px; color: #526079;
|
||||
}
|
||||
.article-contact a { color: var(--brand); font-weight: 700; text-decoration: none; }
|
||||
.article-contact a:hover { text-decoration: underline; }
|
||||
.credit {
|
||||
text-align: center; margin-top: 24px;
|
||||
}
|
||||
.credit button {
|
||||
font-family: 'Caveat', cursive; font-size: 18px;
|
||||
color: #1c3f7c; background: none; border: none;
|
||||
cursor: pointer; padding: 0;
|
||||
transition: opacity .15s;
|
||||
}
|
||||
.credit button:hover { opacity: .7; }
|
||||
.edit-btn {
|
||||
display: inline-flex; align-items: center; gap: 6px;
|
||||
font-size: 13px; font-weight: 700; color: #1f4ea3;
|
||||
text-decoration: none; padding: 7px 16px;
|
||||
border-radius: 999px; background: #eef4ff;
|
||||
border: 1px solid #c8d8f7; transition: .15s;
|
||||
margin-bottom: 16px; float: right;
|
||||
}
|
||||
.edit-btn:hover { background: #dceeff; }
|
||||
@media (max-width: 640px) {
|
||||
.article-header { flex-direction: column; padding: 20px 20px 0; }
|
||||
.article-hero-img { width: 100%; height: 140px; }
|
||||
.article-body-wrap { padding: 16px 20px 28px; }
|
||||
.article-title { font-size: 20px; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrap">
|
||||
<a href="/" class="back-btn">← На главную</a>
|
||||
{% if is_news_editor %}
|
||||
<a href="/news-admin" class="edit-btn">✏ Редактировать</a>
|
||||
<div style="clear:both;"></div>
|
||||
{% endif %}
|
||||
<article class="article-card">
|
||||
<div class="article-header">
|
||||
{% if article.image %}
|
||||
<img class="article-hero-img" src="/static/{{ article.image }}" alt="{{ article.title }}" />
|
||||
{% endif %}
|
||||
<div class="article-header-text">
|
||||
<div class="article-date">{{ article.created_at[8:10] }}.{{ article.created_at[5:7] }}.{{ article.created_at[:4] }}</div>
|
||||
<h1 class="article-title">{{ article.title }}</h1>
|
||||
</div>
|
||||
</div>
|
||||
<div class="article-body-wrap">
|
||||
<div class="article-body">
|
||||
{% if '<' in article.body %}
|
||||
{{ article.body | safe }}
|
||||
{% else %}
|
||||
{% for paragraph in article.body.split('\n\n') %}
|
||||
<p>{{ paragraph }}</p>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="article-contact">
|
||||
По вопросам сотрудничества: <a href="mailto:mshamov@mont.com">mshamov@mont.com</a>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
<div class="credit">
|
||||
<button type="button" id="btn-contact-ruslan">Made by Galyaviev</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 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>
|
||||
|
||||
<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>
|
||||
</html>
|
||||
@@ -0,0 +1,191 @@
|
||||
<!doctype html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Новости MONT</title>
|
||||
<meta name="description" content="Последние новости дистрибьютора MONT — вендоры, продукты, партнёрские программы." />
|
||||
<meta name="robots" content="index, follow" />
|
||||
<link rel="canonical" href="{{ base_url }}/news" />
|
||||
<link rel="icon" type="image/svg+xml" href="/static/favicon.svg" />
|
||||
<link rel="icon" type="image/png" href="/static/favicon.png" />
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Manrope:wght@400;600;700;800&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
:root { --brand: #1f4ea3; --brand2: #3978e0; --bg: #eef4ff; }
|
||||
* { box-sizing: border-box; }
|
||||
body { margin: 0; font-family: Manrope, sans-serif; background: var(--bg); color: #15203b; min-height: 100vh; }
|
||||
.wrap { width: min(900px, calc(100% - 32px)); margin: 0 auto; padding: 28px 0 60px; }
|
||||
.back-btn {
|
||||
display: inline-flex; align-items: center; gap: 6px;
|
||||
font-size: 13px; font-weight: 700; color: var(--brand);
|
||||
text-decoration: none; margin-bottom: 24px;
|
||||
padding: 8px 16px; border-radius: 999px;
|
||||
background: #fff; border: 1px solid #c8d8f7; transition: .15s;
|
||||
}
|
||||
.back-btn:hover { background: #eef4ff; transform: translateX(-2px); }
|
||||
h1 { font-size: 28px; font-weight: 800; color: #1a3e79; margin: 0 0 24px; }
|
||||
|
||||
.top3 { margin-bottom: 32px; }
|
||||
.top3-label {
|
||||
font-size: 11px; font-weight: 800; letter-spacing: 1.2px;
|
||||
text-transform: uppercase; color: #7a9bc0; margin-bottom: 14px;
|
||||
}
|
||||
.top3-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 16px;
|
||||
}
|
||||
.top3-card {
|
||||
background: #fff; border-radius: 16px;
|
||||
border: 1px solid #dae6ff;
|
||||
box-shadow: 0 6px 20px rgba(16,43,95,.08);
|
||||
overflow: hidden; text-decoration: none; color: inherit;
|
||||
transition: .18s ease; display: flex; flex-direction: column;
|
||||
}
|
||||
.top3-card:hover { transform: translateY(-3px); box-shadow: 0 12px 32px rgba(16,43,95,.14); }
|
||||
.top3-card img { width: 100%; height: 160px; object-fit: cover; display: block; }
|
||||
.top3-card .no-img { width: 100%; height: 100px; background: linear-gradient(135deg, #e8f0ff, #d0e2ff); display: flex; align-items: center; justify-content: center; }
|
||||
.top3-card .no-img-icon { font-size: 32px; opacity: .4; }
|
||||
.top3-card-body { padding: 14px 16px 18px; flex: 1; display: flex; flex-direction: column; }
|
||||
.top3-card-date { font-size: 11px; color: #9ab0d0; font-weight: 600; margin-bottom: 6px; }
|
||||
.top3-card-title { font-size: 14px; font-weight: 800; color: #1a3e79; line-height: 1.4; flex: 1; margin: 0 0 12px; }
|
||||
.top3-card-excerpt { font-size: 12px; color: #5a7090; line-height: 1.55; margin: 0 0 12px; flex: 1;
|
||||
display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden; }
|
||||
.read-more { font-size: 12px; font-weight: 700; color: var(--brand2); }
|
||||
|
||||
.divider { border: none; border-top: 2px solid #e0eaff; margin: 0 0 24px; }
|
||||
.rest-label {
|
||||
font-size: 11px; font-weight: 800; letter-spacing: 1.2px;
|
||||
text-transform: uppercase; color: #7a9bc0; margin-bottom: 16px;
|
||||
}
|
||||
.news-row {
|
||||
background: #fff; border-radius: 14px; border: 1px solid #dae6ff;
|
||||
display: flex; gap: 0; overflow: hidden; margin-bottom: 12px;
|
||||
text-decoration: none; color: inherit;
|
||||
transition: .18s ease; box-shadow: 0 4px 14px rgba(16,43,95,.06);
|
||||
}
|
||||
.news-row:hover { transform: translateY(-2px); box-shadow: 0 8px 24px rgba(16,43,95,.12); }
|
||||
.news-row img { width: 120px; height: 90px; object-fit: cover; flex-shrink: 0; }
|
||||
.news-row .no-img-row { width: 80px; flex-shrink: 0; background: linear-gradient(135deg, #e8f0ff, #d0e2ff); }
|
||||
.news-row-body { padding: 12px 16px; display: flex; flex-direction: column; justify-content: center; }
|
||||
.news-row-date { font-size: 11px; color: #9ab0d0; font-weight: 600; margin-bottom: 4px; }
|
||||
.news-row-title { font-size: 14px; font-weight: 800; color: #1a3e79; line-height: 1.35; margin: 0 0 6px; }
|
||||
.news-row-excerpt { font-size: 12px; color: #5a7090; line-height: 1.5; margin: 0;
|
||||
display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
|
||||
|
||||
.empty { text-align: center; padding: 60px 0; color: #9ab0d0; font-size: 15px; }
|
||||
|
||||
.pagination { display: flex; justify-content: center; align-items: center; gap: 6px; margin-top: 28px; flex-wrap: wrap; }
|
||||
.pagination a, .pagination span {
|
||||
display: inline-flex; align-items: center; justify-content: center;
|
||||
width: 36px; height: 36px; border-radius: 10px;
|
||||
font-size: 13px; font-weight: 700; text-decoration: none;
|
||||
border: 1px solid #c8d8f7; background: #fff; color: #1f4ea3; transition: .15s;
|
||||
}
|
||||
.pagination a:hover { background: #eef4ff; }
|
||||
.pagination span.current { background: linear-gradient(135deg,#1f4ea3,#3978e0); color: #fff; border-color: transparent; }
|
||||
.pagination span.dots { background: none; border-color: transparent; color: #9ab0d0; }
|
||||
|
||||
@media (max-width: 700px) {
|
||||
.top3-grid { grid-template-columns: 1fr; }
|
||||
.news-row img { width: 90px; height: 70px; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrap">
|
||||
<a href="/" class="back-btn">← На главную</a>
|
||||
<h1>Новости MONT</h1>
|
||||
|
||||
{% if articles %}
|
||||
|
||||
{% if page == 1 %}
|
||||
{% set top3 = articles[:3] %}
|
||||
{% set rest = articles[3:] %}
|
||||
<div class="top3">
|
||||
<div class="top3-label">Последние новости</div>
|
||||
<div class="top3-grid">
|
||||
{% for a in top3 %}
|
||||
<a class="top3-card" href="/news/{{ a.slug }}">
|
||||
{% if a.image %}
|
||||
<img src="/static/{{ a.image }}" alt="{{ a.title }}" />
|
||||
{% else %}
|
||||
<div class="no-img"><span class="no-img-icon">📰</span></div>
|
||||
{% endif %}
|
||||
<div class="top3-card-body">
|
||||
<div class="top3-card-date">{{ a.created_at[8:10] }}.{{ a.created_at[5:7] }}.{{ a.created_at[:4] }}</div>
|
||||
<h2 class="top3-card-title">{{ a.title }}</h2>
|
||||
<p class="top3-card-excerpt">{{ a.body | striptags | truncate(180, true, '...') }}</p>
|
||||
<span class="read-more">Читать →</span>
|
||||
</div>
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% if rest %}
|
||||
<hr class="divider" />
|
||||
<div class="rest-label">Ещё новости</div>
|
||||
{% for a in rest %}
|
||||
<a class="news-row" href="/news/{{ a.slug }}">
|
||||
{% if a.image %}
|
||||
<img src="/static/{{ a.image }}" alt="{{ a.title }}" />
|
||||
{% else %}
|
||||
<div class="no-img-row"></div>
|
||||
{% endif %}
|
||||
<div class="news-row-body">
|
||||
<div class="news-row-date">{{ a.created_at[8:10] }}.{{ a.created_at[5:7] }}.{{ a.created_at[:4] }}</div>
|
||||
<div class="news-row-title">{{ a.title }}</div>
|
||||
<p class="news-row-excerpt">{{ a.body | striptags | truncate(160, true, '...') }}</p>
|
||||
</div>
|
||||
</a>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
{% else %}
|
||||
<hr class="divider" />
|
||||
<div class="rest-label">Новости — страница {{ page }}</div>
|
||||
{% for a in articles %}
|
||||
<a class="news-row" href="/news/{{ a.slug }}">
|
||||
{% if a.image %}
|
||||
<img src="/static/{{ a.image }}" alt="{{ a.title }}" />
|
||||
{% else %}
|
||||
<div class="no-img-row"></div>
|
||||
{% endif %}
|
||||
<div class="news-row-body">
|
||||
<div class="news-row-date">{{ a.created_at[8:10] }}.{{ a.created_at[5:7] }}.{{ a.created_at[:4] }}</div>
|
||||
<div class="news-row-title">{{ a.title }}</div>
|
||||
<p class="news-row-excerpt">{{ a.body | striptags | truncate(160, true, '...') }}</p>
|
||||
</div>
|
||||
</a>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
{% else %}
|
||||
<div class="empty">Новостей пока нет</div>
|
||||
{% endif %}
|
||||
|
||||
{% if total_pages > 1 %}
|
||||
<div class="pagination">
|
||||
{% if page > 1 %}
|
||||
<a href="?page={{ page - 1 }}">‹</a>
|
||||
{% endif %}
|
||||
{% for p in range(1, total_pages + 1) %}
|
||||
{% if p == page %}
|
||||
<span class="current">{{ p }}</span>
|
||||
{% elif p == 1 or p == total_pages or (p >= page - 2 and p <= page + 2) %}
|
||||
<a href="?page={{ p }}">{{ p }}</a>
|
||||
{% elif p == page - 3 or p == page + 3 %}
|
||||
<span class="dots">…</span>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% if page < total_pages %}
|
||||
<a href="?page={{ page + 1 }}">›</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
+194
-9
@@ -3,15 +3,15 @@
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>{{ vendor.name }} — вендор МОНТ | матрица продуктов MONT</title>
|
||||
<meta name="description" content="{{ vendor.description[:160] if vendor.description else 'Вендор ' + vendor.name + ' в корзине продуктов дистрибьютора МОНТ. Продукты, категории, ссылки.' }}" />
|
||||
<title>{{ vendor.name }} — вендор MONT | матрица продуктов MONT</title>
|
||||
<meta name="description" content="{{ vendor.description[:160] if vendor.description else 'Вендор ' + vendor.name + ' в корзине продуктов дистрибьютора MONT. Продукты, категории, ссылки.' }}" />
|
||||
<meta name="robots" content="index, follow" />
|
||||
<link rel="canonical" href="{{ canonical_url }}" />
|
||||
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:url" content="{{ canonical_url }}" />
|
||||
<meta property="og:title" content="{{ vendor.name }} — вендор МОНТ" />
|
||||
<meta property="og:description" content="{{ vendor.description[:200] if vendor.description else 'Вендор ' + vendor.name + ' в корзине продуктов дистрибьютора МОНТ.' }}" />
|
||||
<meta property="og:title" content="{{ vendor.name }} — вендор MONT" />
|
||||
<meta property="og:description" content="{{ vendor.description[:200] if vendor.description else 'Вендор ' + vendor.name + ' в корзине продуктов дистрибьютора MONT.' }}" />
|
||||
{% if vendor.logo %}<meta property="og:image" content="{{ base_url }}/static/{{ vendor.logo }}" />{% endif %}
|
||||
|
||||
<script type="application/ld+json">
|
||||
@@ -24,12 +24,54 @@
|
||||
{% if vendor.logo %}"logo": "{{ base_url }}/static/{{ vendor.logo }}",{% endif %}
|
||||
"distributor": {
|
||||
"@type": "Organization",
|
||||
"name": "МОНТ",
|
||||
"name": "MONT",
|
||||
"url": "{{ base_url }}"
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<script type="application/ld+json">
|
||||
{
|
||||
"@context": "https://schema.org",
|
||||
"@type": "FAQPage",
|
||||
"mainEntity": [
|
||||
{
|
||||
"@type": "Question",
|
||||
"name": "Где купить {{ vendor.name }} в России?",
|
||||
"acceptedAnswer": {
|
||||
"@type": "Answer",
|
||||
"text": "Продукты {{ vendor.name }} можно приобрести через официального дистрибьютора MONT (MONT). Главный офис — Москва, филиал — Казань; партнёры работают по всей России. Для заказа напишите BDM MONT Максиму Шамову: mshamov@mont.com или на общую почту mont@mont.com."
|
||||
}
|
||||
},
|
||||
{
|
||||
"@type": "Question",
|
||||
"name": "Как стать партнёром {{ vendor.name }} через MONT?",
|
||||
"acceptedAnswer": {
|
||||
"@type": "Answer",
|
||||
"text": "Для получения партнёрского статуса {{ vendor.name }} через дистрибьютора MONT свяжитесь с BDM Максимом Шамовым: mshamov@mont.com или mont@mont.com. MONT является официальным дистрибьютором и помогает партнёрам получить авторизацию, обучение и техническую поддержку по продуктам {{ vendor.name }}."
|
||||
}
|
||||
},
|
||||
{
|
||||
"@type": "Question",
|
||||
"name": "Какие продукты {{ vendor.name }} доступны через MONT?",
|
||||
"acceptedAnswer": {
|
||||
"@type": "Answer",
|
||||
"text": "{% if products %}В корзине MONT представлены следующие продукты {{ vendor.name }}: {{ products | map(attribute='name') | join(', ') }}.{% else %}MONT предлагает широкую линейку продуктов {{ vendor.name }}. Актуальный список доступен на странице вендора в корзине MONT.{% endif %} Для уточнения наличия и цен обращайтесь к дистрибьютору."
|
||||
}
|
||||
},
|
||||
{
|
||||
"@type": "Question",
|
||||
"name": "Есть ли официальный дистрибьютор {{ vendor.name }} в Казани?",
|
||||
"acceptedAnswer": {
|
||||
"@type": "Answer",
|
||||
"text": "Да. MONT имеет главный офис в Москве и филиал в Казани. Партнёры работают во всех крупных городах России. Свяжитесь с BDM MONT Максимом Шамовым: mshamov@mont.com или mont@mont.com."
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
</script>
|
||||
|
||||
<link rel="icon" type="image/svg+xml" href="/static/favicon.svg" />
|
||||
<link rel="icon" type="image/png" href="/static/favicon.png" />
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
@@ -127,6 +169,36 @@
|
||||
}
|
||||
.back-btn:hover { background: #eef4ff; transform: translateX(-2px); }
|
||||
|
||||
.seo-block {
|
||||
background: #fff;
|
||||
border-radius: 16px;
|
||||
border: 1px solid #dae6ff;
|
||||
padding: 24px 28px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
.seo-block h2 { font-size: 17px; font-weight: 800; color: #1a3e79; margin: 0 0 10px; }
|
||||
.seo-block p { font-size: 14px; color: #3a4f6e; line-height: 1.7; margin: 0 0 10px; }
|
||||
.seo-block p:last-child { margin-bottom: 0; }
|
||||
.cities-row { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 12px; }
|
||||
.city-tag {
|
||||
font-size: 12px; font-weight: 600; padding: 4px 12px;
|
||||
border-radius: 999px; background: #f0f6ff; color: #22427a;
|
||||
border: 1px solid #c8dcff;
|
||||
}
|
||||
.faq-list { margin: 0; padding: 0; list-style: none; display: flex; flex-direction: column; gap: 10px; }
|
||||
.faq-item { border: 1px solid #dae6ff; border-radius: 12px; overflow: hidden; }
|
||||
.faq-q {
|
||||
width: 100%; text-align: left; background: #f5f9ff;
|
||||
border: none; padding: 14px 18px; cursor: pointer;
|
||||
font-size: 14px; font-weight: 700; color: #1a3e79;
|
||||
font-family: Manrope, sans-serif; display: flex; justify-content: space-between; align-items: center;
|
||||
}
|
||||
.faq-q:hover { background: #eaf2ff; }
|
||||
.faq-q .arr { transition: transform .2s; font-size: 12px; color: #7a9bd0; }
|
||||
.faq-q.open .arr { transform: rotate(180deg); }
|
||||
.faq-a { display: none; padding: 12px 18px 16px; font-size: 14px; color: #3a4f6e; line-height: 1.7; }
|
||||
.faq-a.open { display: block; }
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.vendor-card { flex-direction: column; padding: 20px; }
|
||||
.vendor-logo-box { width: 100%; height: 100px; }
|
||||
@@ -136,10 +208,10 @@
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrap">
|
||||
<a href="/" class="back-btn">← Все вендоры МОНТ</a>
|
||||
<a href="/" class="back-btn">← Все вендоры MONT</a>
|
||||
|
||||
<nav class="breadcrumb" aria-label="breadcrumb">
|
||||
<a href="/">Корзина МОНТ</a> › {{ vendor.name }}
|
||||
<a href="/">Корзина MONT</a> › {{ vendor.name }}
|
||||
</nav>
|
||||
|
||||
<div class="vendor-card">
|
||||
@@ -157,7 +229,7 @@
|
||||
{% endif %}
|
||||
<div class="vendor-links">
|
||||
{% if vendor.mont_page %}
|
||||
<a class="vlink mont" href="{{ vendor.mont_page }}" target="_blank" rel="noopener">Страница на МОНТ ↗</a>
|
||||
<a class="vlink mont" href="{{ vendor.mont_page }}" target="_blank" rel="noopener">Страница на MONT ↗</a>
|
||||
{% endif %}
|
||||
{% if vendor.website %}
|
||||
<a class="vlink site" href="{{ vendor.website }}" target="_blank" rel="noopener">Официальный сайт ↗</a>
|
||||
@@ -168,7 +240,7 @@
|
||||
|
||||
{% if products %}
|
||||
<div class="products-box">
|
||||
<h2 class="section-title">Продукты {{ vendor.name }} в корзине МОНТ</h2>
|
||||
<h2 class="section-title">Продукты {{ vendor.name }} в корзине MONT</h2>
|
||||
<div class="products-grid">
|
||||
{% for p in products %}
|
||||
{% if p.url %}
|
||||
@@ -189,6 +261,119 @@
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="seo-block">
|
||||
<h2>Официальный дистрибьютор {{ vendor.name }} в России — MONT</h2>
|
||||
<p>
|
||||
MONT (MONT) является официальным дистрибьютором {{ vendor.name }} на территории России.
|
||||
Через MONT доступны лицензии, техническая поддержка и партнёрские программы
|
||||
{{ vendor.name }}. Дистрибьютор работает с юридическими лицами, системными
|
||||
интеграторами и реселлерами по всей стране.
|
||||
</p>
|
||||
<p>
|
||||
Купить {{ vendor.name }} или получить консультацию можно через партнёров MONT в
|
||||
вашем регионе. Для связи пишите на
|
||||
<a href="mailto:mont@mont.com" style="color:var(--brand);font-weight:600;">mont@mont.com</a>
|
||||
или напрямую BDM MONT Максиму Шамову —
|
||||
<a href="mailto:mshamov@mont.com" style="color:var(--brand);font-weight:600;">mshamov@mont.com</a>.
|
||||
Заявки принимаются из всех городов России.
|
||||
</p>
|
||||
<div class="cities-row">
|
||||
<span class="city-tag">{{ vendor.name }} Москва</span>
|
||||
<span class="city-tag">{{ vendor.name }} Казань</span>
|
||||
<span class="city-tag">{{ vendor.name }} Санкт-Петербург</span>
|
||||
<span class="city-tag">{{ vendor.name }} Екатеринбург</span>
|
||||
<span class="city-tag">{{ vendor.name }} Новосибирск</span>
|
||||
<span class="city-tag">{{ vendor.name }} Краснодар</span>
|
||||
<span class="city-tag">{{ vendor.name }} Нижний Новгород</span>
|
||||
<span class="city-tag">{{ vendor.name }} Ростов-на-Дону</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="seo-block">
|
||||
<h2>Часто задаваемые вопросы о {{ vendor.name }} и MONT</h2>
|
||||
<ul class="faq-list" id="faqList">
|
||||
<li class="faq-item">
|
||||
<button class="faq-q" type="button">
|
||||
Где купить {{ vendor.name }} в России?
|
||||
<span class="arr">▼</span>
|
||||
</button>
|
||||
<div class="faq-a">
|
||||
Продукты {{ vendor.name }} доступны через официального дистрибьютора MONT.
|
||||
Главный офис MONT находится в Москве, филиал — в Казани; партнёры работают
|
||||
в Санкт-Петербурге, Екатеринбурге, Новосибирске, Краснодаре и других городах.
|
||||
Для оформления заказа напишите BDM MONT Максиму Шамову:
|
||||
<a href="mailto:mshamov@mont.com" style="color:var(--brand);font-weight:600;">mshamov@mont.com</a>
|
||||
или на общую почту
|
||||
<a href="mailto:mont@mont.com" style="color:var(--brand);font-weight:600;">mont@mont.com</a>.
|
||||
</div>
|
||||
</li>
|
||||
<li class="faq-item">
|
||||
<button class="faq-q" type="button">
|
||||
Как стать партнёром {{ vendor.name }} через MONT?
|
||||
<span class="arr">▼</span>
|
||||
</button>
|
||||
<div class="faq-a">
|
||||
MONT помогает компаниям получить партнёрский статус {{ vendor.name }}:
|
||||
авторизацию, доступ к обучению и технической поддержке. Для начала
|
||||
сотрудничества свяжитесь с BDM MONT Максимом Шамовым:
|
||||
<a href="mailto:mshamov@mont.com" style="color:var(--brand);font-weight:600;">mshamov@mont.com</a>
|
||||
или напишите на общую почту
|
||||
<a href="mailto:mont@mont.com" style="color:var(--brand);font-weight:600;">mont@mont.com</a> —
|
||||
специалисты подберут подходящий партнёрский уровень и помогут пройти все этапы авторизации.
|
||||
</div>
|
||||
</li>
|
||||
<li class="faq-item">
|
||||
<button class="faq-q" type="button">
|
||||
Какие продукты {{ vendor.name }} представлены в корзине MONT?
|
||||
<span class="arr">▼</span>
|
||||
</button>
|
||||
<div class="faq-a">
|
||||
{% if products %}
|
||||
В корзине MONT представлены следующие продукты {{ vendor.name }}:
|
||||
{{ products | map(attribute='name') | join(', ') }}.
|
||||
Актуальность наличия и цены уточняйте у BDM MONT Максима Шамова:
|
||||
<a href="mailto:mshamov@mont.com" style="color:var(--brand);font-weight:600;">mshamov@mont.com</a>.
|
||||
{% else %}
|
||||
MONT предлагает актуальную линейку продуктов {{ vendor.name }}.
|
||||
Для уточнения наличия и условий поставки напишите BDM MONT Максиму Шамову:
|
||||
<a href="mailto:mshamov@mont.com" style="color:var(--brand);font-weight:600;">mshamov@mont.com</a>.
|
||||
{% endif %}
|
||||
</div>
|
||||
</li>
|
||||
<li class="faq-item">
|
||||
<button class="faq-q" type="button">
|
||||
Есть ли официальный дистрибьютор {{ vendor.name }} в Казани?
|
||||
<span class="arr">▼</span>
|
||||
</button>
|
||||
<div class="faq-a">
|
||||
Да. MONT имеет филиал в Казани и главный офис в Москве — компания является
|
||||
одним из ведущих дистрибьюторов ПО в России. Партнёры MONT работают во
|
||||
всех федеральных округах. Для заказа {{ vendor.name }} в Казани или другом
|
||||
регионе свяжитесь с BDM MONT Максимом Шамовым:
|
||||
<a href="mailto:mshamov@mont.com" style="color:var(--brand);font-weight:600;">mshamov@mont.com</a>
|
||||
или на общую почту
|
||||
<a href="mailto:mont@mont.com" style="color:var(--brand);font-weight:600;">mont@mont.com</a>.
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.getElementById('faqList').addEventListener('click', function(e) {
|
||||
const btn = e.target.closest('.faq-q');
|
||||
if (!btn) return;
|
||||
const isOpen = btn.classList.contains('open');
|
||||
document.querySelectorAll('.faq-q.open').forEach(b => {
|
||||
b.classList.remove('open');
|
||||
b.nextElementSibling.classList.remove('open');
|
||||
});
|
||||
if (!isOpen) {
|
||||
btn.classList.add('open');
|
||||
btn.nextElementSibling.classList.add('open');
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user