Files

307 lines
16 KiB
HTML

<!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 }}">&#8249;</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 }}">&#8250;</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>