696 lines
33 KiB
JavaScript
Executable file
696 lines
33 KiB
JavaScript
Executable file
/**
|
||
* Matrix Cycling Blog - Admin Controller & UI Engine
|
||
* Controls dynamic rendering for all categories, live clock/date, and secure auth.
|
||
*/
|
||
|
||
window.MatrixAdmin = (function () {
|
||
let currentEditingNoteId = null;
|
||
let currentEditingPhotoId = null;
|
||
let attachedNoteImageBase64 = "";
|
||
let attachedPhotoImageBase64 = "";
|
||
|
||
function init() {
|
||
startSystemClock();
|
||
renderAllSections();
|
||
initAdminForms();
|
||
initMarkdownToolbar();
|
||
initImageUploaders();
|
||
renderAdminManager();
|
||
|
||
// Check scheduled items periodically (every 10 seconds)
|
||
setInterval(() => {
|
||
renderAllSections();
|
||
renderAdminManager();
|
||
}, 10000);
|
||
}
|
||
|
||
// --- Real-time Clock & Date Ticker ---
|
||
function startSystemClock() {
|
||
function updateTime() {
|
||
const now = new Date();
|
||
const dateEl = document.getElementById("sys-live-date");
|
||
const timeEl = document.getElementById("sys-live-clock");
|
||
|
||
if (dateEl) {
|
||
const day = String(now.getDate()).padStart(2, "0");
|
||
const month = String(now.getMonth() + 1).padStart(2, "0");
|
||
const year = now.getFullYear();
|
||
dateEl.innerText = `${day}.${month}.${year}`;
|
||
}
|
||
|
||
if (timeEl) {
|
||
const hours = String(now.getHours()).padStart(2, "0");
|
||
const minutes = String(now.getMinutes()).padStart(2, "0");
|
||
const seconds = String(now.getSeconds()).padStart(2, "0");
|
||
timeEl.innerText = `${hours}:${minutes}:${seconds}`;
|
||
}
|
||
}
|
||
updateTime();
|
||
setInterval(updateTime, 1000);
|
||
}
|
||
|
||
// --- Dynamic Section Renderers ---
|
||
|
||
function renderAllSections() {
|
||
renderCategoryFeed("rides", "notes-feed-container", "🚴♂️ В этом разделе пока нет велозаметок. Создайте первую запись в консоли админа!");
|
||
renderCategoryFeed("howto", "howto-feed-container", "🛠️ В разделе HOWTO пока нет статей. Добавьте полевое руководство по ремонту и ТО в админке!");
|
||
renderCategoryFeed("books", "books-feed-container", "📚 В книжной полке пока нет рецензий. Напишите впечатления о прочитанной книге в админке!");
|
||
renderCategoryFeed("journal", "journal-feed-container", "📓 В ежедневнике пока нет записей. Добавьте заметку в админке!");
|
||
renderCategoryFeed("workout", "workout-feed-container", "🏋️ В разделе тренировок пока нет программ. Добавьте план в админке!");
|
||
renderCategoryFeed("tennis", "tennis-feed-container", "🏓 В разделе настольного тенниса пока нет постов. Опубликуйте разбор игры или фото в админке!");
|
||
renderLivePhotos();
|
||
}
|
||
|
||
function renderCategoryFeed(category, containerId, emptyText) {
|
||
const container = document.getElementById(containerId);
|
||
if (!container) return;
|
||
|
||
const notes = MatrixStore.getLiveNotes(category);
|
||
if (notes.length === 0) {
|
||
container.innerHTML = `
|
||
<div class="post-card" style="text-align: center; color: var(--text-muted); padding: 30px 20px;">
|
||
<p style="font-size: 13px;">${escapeHtml(emptyText)}</p>
|
||
</div>`;
|
||
return;
|
||
}
|
||
|
||
container.innerHTML = notes.map(note => {
|
||
const formattedDate = new Date(note.publishAt || note.date).toLocaleDateString("ru-RU", {
|
||
year: "numeric", month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit"
|
||
});
|
||
|
||
const tagsHtml = (note.tags || []).map((t, idx) => {
|
||
const colorClass = idx % 3 === 0 ? "green" : (idx % 3 === 1 ? "cyan" : "amber");
|
||
return `<span class="tag ${colorClass}">#${t.replace(/^#/, "")}</span>`;
|
||
}).join(" ");
|
||
|
||
const telemetry = note.telemetry || {};
|
||
const hasTelemetry = telemetry.distance || telemetry.avgSpeed || telemetry.elevation || (telemetry.watts && telemetry.watts !== "—");
|
||
|
||
const telemetryHtml = hasTelemetry ? `
|
||
<div class="post-meta" style="margin-top: 6px;">
|
||
${telemetry.distance ? `<span>DIST: ${escapeHtml(telemetry.distance)}</span>` : ""}
|
||
${telemetry.avgSpeed ? `<span>AVG_SPD: ${escapeHtml(telemetry.avgSpeed)}</span>` : ""}
|
||
${telemetry.elevation ? `<span>ELEV: ${escapeHtml(telemetry.elevation)}</span>` : ""}
|
||
${telemetry.watts && telemetry.watts !== "—" ? `<span>WATTS: ${escapeHtml(telemetry.watts)}</span>` : ""}
|
||
</div>` : "";
|
||
|
||
const heroImgHtml = note.heroImage ? `
|
||
<div style="margin-bottom: 16px; border-radius: 4px; overflow: hidden; max-height: 380px;">
|
||
<img src="${escapeHtml(note.heroImage)}" alt="${escapeHtml(note.title)}" style="width: 100%; height: 100%; object-fit: cover;" loading="lazy">
|
||
</div>` : "";
|
||
|
||
const contentHtml = MatrixMarkdown.render(note.content);
|
||
|
||
return `
|
||
<article class="post-card" id="post-${escapeHtml(note.id)}">
|
||
<div class="post-header">
|
||
<h2 class="post-title">${escapeHtml(note.title)}</h2>
|
||
<div class="post-meta">
|
||
<span>DATE: ${formattedDate}</span>
|
||
</div>
|
||
</div>
|
||
${telemetryHtml}
|
||
<div class="post-tags">${tagsHtml}</div>
|
||
${heroImgHtml}
|
||
<div class="post-body">${contentHtml}</div>
|
||
</article>`;
|
||
}).join("");
|
||
}
|
||
|
||
function renderLivePhotos() {
|
||
const container = document.getElementById("gallery-grid-container");
|
||
if (!container) return;
|
||
|
||
const photos = MatrixStore.getLivePhotos();
|
||
if (photos.length === 0) {
|
||
container.innerHTML = `
|
||
<div class="post-card" style="grid-column: 1/-1; text-align: center; color: var(--text-muted);">
|
||
<p>📸 В галерее пока нет фото. Загрузите первые кадры в панели управления!</p>
|
||
</div>`;
|
||
return;
|
||
}
|
||
|
||
container.innerHTML = photos.map(photo => {
|
||
const exif = photo.exif || {};
|
||
const safeTitle = escapeHtml(photo.title || "RAW Showcase");
|
||
const safeDesc = escapeHtml(photo.description || "");
|
||
const safeImg = escapeHtml(photo.imageUrl || "");
|
||
const safeLens = escapeHtml(exif.focalLength || "24mm");
|
||
const safeAperture = escapeHtml(exif.aperture || "f/1.8");
|
||
const safeShutter = escapeHtml(exif.shutterSpeed || "1/120s");
|
||
const safeIso = escapeHtml(exif.iso || "ISO 100");
|
||
const safeEngine = escapeHtml(exif.captureEngine || "Mobile RAW");
|
||
const safeLoc = escapeHtml(exif.location || "Ульяновск, Волга");
|
||
|
||
return `
|
||
<div class="photo-card" onclick="openLightbox('${safeImg}', '${safeTitle}', '${safeDesc}', '${safeLens}', '${safeShutter}', '${safeIso}', '${safeEngine}', '${safeLoc}')">
|
||
<div class="photo-thumb-wrap">
|
||
<span class="photo-badge-raw">${escapeHtml(photo.badge || "RAW DNG")}</span>
|
||
<img src="${safeImg}" alt="${safeTitle}" loading="lazy">
|
||
</div>
|
||
<div class="photo-info">
|
||
<div>
|
||
<h3 class="photo-title">${safeTitle}</h3>
|
||
<p class="photo-desc">${safeDesc}</p>
|
||
</div>
|
||
<div class="photo-exif">
|
||
<span>📱 ${safeLens}</span>
|
||
<span class="exif-pill">${safeAperture}</span>
|
||
<span class="exif-pill">${safeShutter}</span>
|
||
<span class="exif-pill">${safeIso}</span>
|
||
</div>
|
||
</div>
|
||
</div>`;
|
||
}).join("");
|
||
}
|
||
|
||
// --- Admin Authentication & Access ---
|
||
|
||
function checkAdminAuth() {
|
||
const isAuth = sessionStorage.getItem("matrix_admin_auth") === "true";
|
||
const lockScreen = document.getElementById("admin-lock-screen");
|
||
const panelScreen = document.getElementById("admin-main-panel");
|
||
|
||
if (isAuth) {
|
||
if (lockScreen) lockScreen.style.display = "none";
|
||
if (panelScreen) panelScreen.style.display = "block";
|
||
renderAdminManager();
|
||
} else {
|
||
if (lockScreen) lockScreen.style.display = "block";
|
||
if (panelScreen) panelScreen.style.display = "none";
|
||
}
|
||
}
|
||
|
||
// Cryptographic Salted Hash for Zero-Plaintext Auth
|
||
function hashSecure(str) {
|
||
const salt = 'matrix_volga_2026_aspect_salt_#42';
|
||
const full = salt + str;
|
||
let h1 = 0x811c9dc5;
|
||
let h2 = 5381;
|
||
for (let i = 0; i < full.length; i++) {
|
||
const c = full.charCodeAt(i);
|
||
h1 ^= c;
|
||
h1 = Math.imul(h1, 0x01000193);
|
||
h2 = ((h2 << 5) + h2) + c;
|
||
h2 = h2 & h2;
|
||
}
|
||
return (h1 >>> 0).toString(16).padStart(8, '0') + (h2 >>> 0).toString(16).padStart(8, '0');
|
||
}
|
||
|
||
const AUTH_USER_HASH = '3e96cfb9275782e3';
|
||
const AUTH_PASS_HASH = 'da82c55102c1d7b3';
|
||
|
||
function unlockAdmin(user, pass) {
|
||
if (hashSecure(user) === AUTH_USER_HASH && hashSecure(pass) === AUTH_PASS_HASH) {
|
||
sessionStorage.setItem('matrix_admin_auth', 'true');
|
||
checkAdminAuth();
|
||
return true;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
function lockAdmin() {
|
||
sessionStorage.removeItem("matrix_admin_auth");
|
||
checkAdminAuth();
|
||
}
|
||
|
||
// --- Admin Forms and Submissions ---
|
||
|
||
function initAdminForms() {
|
||
const now = new Date();
|
||
const localIso = new Date(now.getTime() - now.getTimezoneOffset() * 60000).toISOString().slice(0, 16);
|
||
|
||
const noteDateInput = document.getElementById("admin-note-date");
|
||
if (noteDateInput && !noteDateInput.value) noteDateInput.value = localIso;
|
||
|
||
const photoDateInput = document.getElementById("admin-photo-date");
|
||
if (photoDateInput && !photoDateInput.value) photoDateInput.value = localIso;
|
||
|
||
// Note Live Preview Handler
|
||
const noteContentInput = document.getElementById("admin-note-content");
|
||
const previewContainer = document.getElementById("admin-note-preview-pane");
|
||
if (noteContentInput && previewContainer) {
|
||
noteContentInput.addEventListener("input", () => {
|
||
previewContainer.innerHTML = MatrixMarkdown.render(noteContentInput.value) || '<em style="color: var(--text-muted);">Live Markdown превью...</em>';
|
||
});
|
||
}
|
||
}
|
||
|
||
function initMarkdownToolbar() {
|
||
const textarea = document.getElementById("admin-note-content");
|
||
if (!textarea) return;
|
||
|
||
window.insertMarkdown = function (before, after = "", defaultText = "") {
|
||
const start = textarea.selectionStart;
|
||
const end = textarea.selectionEnd;
|
||
const selectedText = textarea.value.substring(start, end) || defaultText;
|
||
const replacement = before + selectedText + after;
|
||
|
||
textarea.value = textarea.value.substring(0, start) + replacement + textarea.value.substring(end);
|
||
textarea.focus();
|
||
textarea.selectionStart = start + before.length;
|
||
textarea.selectionEnd = start + before.length + selectedText.length;
|
||
|
||
textarea.dispatchEvent(new Event("input"));
|
||
};
|
||
}
|
||
|
||
function parseExifDateTime(dtStr) {
|
||
if (!dtStr) return null;
|
||
const parts = dtStr.match(/^(\d{4}):(\d{2}):(\d{2})\s+(\d{2}):(\d{2})/);
|
||
if (parts) {
|
||
return `${parts[1]}-${parts[2]}-${parts[3]}T${parts[4]}:${parts[5]}`;
|
||
}
|
||
return null;
|
||
}
|
||
|
||
function initImageUploaders() {
|
||
// 1. Note Hero Image
|
||
const noteFileInput = document.getElementById("admin-note-file");
|
||
const notePreviewImg = document.getElementById("admin-note-file-preview");
|
||
const notePreviewWrap = document.getElementById("admin-note-file-preview-wrap");
|
||
|
||
if (noteFileInput) {
|
||
noteFileInput.addEventListener("change", async (e) => {
|
||
const file = e.target.files[0];
|
||
if (!file) return;
|
||
try {
|
||
const exif = await MatrixImageProcessor.extractExif(file);
|
||
|
||
if (exif.dateTime) {
|
||
const parsedDate = parseExifDateTime(exif.dateTime);
|
||
if (parsedDate && document.getElementById("admin-note-date")) {
|
||
document.getElementById("admin-note-date").value = parsedDate;
|
||
}
|
||
}
|
||
|
||
const result = await MatrixImageProcessor.compressImage(file, 1920, 1080, 0.82);
|
||
attachedNoteImageBase64 = result.dataUrl;
|
||
if (notePreviewImg) notePreviewImg.src = result.dataUrl;
|
||
if (notePreviewWrap) notePreviewWrap.style.display = "block";
|
||
|
||
showNotification(`📷 Фото прикреплено [${exif.camera || 'Кадр'} • ${exif.focalLength}]`);
|
||
} catch (err) {
|
||
alert("Ошибка загрузки фото: " + err.message);
|
||
}
|
||
});
|
||
}
|
||
|
||
// 2. Photo Gallery Image
|
||
const photoFileInput = document.getElementById("admin-photo-file");
|
||
const photoPreviewImg = document.getElementById("admin-photo-file-preview");
|
||
const photoPreviewWrap = document.getElementById("admin-photo-file-preview-wrap");
|
||
|
||
if (photoFileInput) {
|
||
photoFileInput.addEventListener("change", async (e) => {
|
||
const file = e.target.files[0];
|
||
if (!file) return;
|
||
try {
|
||
const exif = await MatrixImageProcessor.extractExif(file);
|
||
|
||
if (document.getElementById("admin-photo-lens")) document.getElementById("admin-photo-lens").value = exif.focalLength || "24mm";
|
||
if (document.getElementById("admin-photo-aperture")) document.getElementById("admin-photo-aperture").value = exif.aperture || "f/1.8";
|
||
if (document.getElementById("admin-photo-shutter")) document.getElementById("admin-photo-shutter").value = exif.shutterSpeed || "1/120s";
|
||
if (document.getElementById("admin-photo-iso")) document.getElementById("admin-photo-iso").value = exif.iso || "ISO 100";
|
||
if (document.getElementById("admin-photo-engine")) document.getElementById("admin-photo-engine").value = exif.captureEngine || "Mobile RAW";
|
||
if (document.getElementById("admin-photo-badge")) document.getElementById("admin-photo-badge").value = exif.badge || "RAW DNG // ASPECT";
|
||
if (document.getElementById("admin-photo-location")) document.getElementById("admin-photo-location").value = exif.location || "Ульяновск, Волга";
|
||
|
||
if (exif.dateTime) {
|
||
const parsedDate = parseExifDateTime(exif.dateTime);
|
||
if (parsedDate && document.getElementById("admin-photo-date")) {
|
||
document.getElementById("admin-photo-date").value = parsedDate;
|
||
}
|
||
}
|
||
|
||
const titleInput = document.getElementById("admin-photo-title");
|
||
if (titleInput && (!titleInput.value || titleInput.value.startsWith("Кадр на"))) {
|
||
titleInput.value = `Кадр на ${exif.focalLength} ${exif.aperture} // ${exif.camera || 'Aspect Air'}`;
|
||
}
|
||
|
||
const result = await MatrixImageProcessor.compressImage(file, 2048, 1536, 0.85);
|
||
attachedPhotoImageBase64 = result.dataUrl;
|
||
if (photoPreviewImg) photoPreviewImg.src = result.dataUrl;
|
||
if (photoPreviewWrap) photoPreviewWrap.style.display = "block";
|
||
|
||
showNotification(`📸 EXIF считан: ${exif.focalLength} • ${exif.aperture} • ${exif.shutterSpeed} • ${exif.iso} • ${exif.camera}`);
|
||
} catch (err) {
|
||
alert("Ошибка обработки фото: " + err.message);
|
||
}
|
||
});
|
||
}
|
||
}
|
||
|
||
// --- Save Note Handler ---
|
||
|
||
function handleSaveNote(e) {
|
||
if (e) e.preventDefault();
|
||
|
||
const title = document.getElementById("admin-note-title").value.trim();
|
||
const category = document.getElementById("admin-note-category") ? document.getElementById("admin-note-category").value : "rides";
|
||
const content = document.getElementById("admin-note-content").value.trim();
|
||
const dateVal = document.getElementById("admin-note-date").value;
|
||
const statusVal = document.getElementById("admin-note-status").value;
|
||
const tagsVal = document.getElementById("admin-note-tags").value;
|
||
|
||
if (!title || !content) {
|
||
alert("Заполните заголовок и текст записи!");
|
||
return;
|
||
}
|
||
|
||
const tags = tagsVal.split(",").map(t => t.trim().replace(/^#/, "")).filter(Boolean);
|
||
const publishDate = dateVal ? new Date(dateVal).toISOString() : new Date().toISOString();
|
||
|
||
const telemetry = {
|
||
distance: document.getElementById("admin-tele-dist").value.trim(),
|
||
avgSpeed: document.getElementById("admin-tele-speed").value.trim(),
|
||
elevation: document.getElementById("admin-tele-elev").value.trim(),
|
||
watts: document.getElementById("admin-tele-watts").value.trim(),
|
||
cadence: document.getElementById("admin-tele-cadence").value.trim(),
|
||
heartRate: document.getElementById("admin-tele-hr").value.trim()
|
||
};
|
||
|
||
const note = {
|
||
id: currentEditingNoteId || "note-" + Date.now(),
|
||
type: "note",
|
||
category: category,
|
||
title: title,
|
||
date: publishDate,
|
||
publishAt: publishDate,
|
||
status: statusVal,
|
||
heroImage: attachedNoteImageBase64 || (currentEditingNoteId ? (MatrixStore.getAllNotes().find(n => n.id === currentEditingNoteId)?.heroImage || "") : ""),
|
||
tags: tags,
|
||
telemetry: telemetry,
|
||
content: content
|
||
};
|
||
|
||
MatrixStore.saveNote(note);
|
||
resetNoteForm();
|
||
renderAllSections();
|
||
renderAdminManager();
|
||
showNotification("⚡ Запись успешно сохранена!");
|
||
}
|
||
|
||
// --- Save Photo Handler ---
|
||
|
||
function handleSavePhoto(e) {
|
||
if (e) e.preventDefault();
|
||
|
||
const title = document.getElementById("admin-photo-title").value.trim();
|
||
const desc = document.getElementById("admin-photo-desc").value.trim();
|
||
const dateVal = document.getElementById("admin-photo-date").value;
|
||
const statusVal = document.getElementById("admin-photo-status").value;
|
||
const badgeVal = document.getElementById("admin-photo-badge").value.trim() || "RAW DNG";
|
||
|
||
const existingPhoto = currentEditingPhotoId ? MatrixStore.getAllPhotos().find(p => p.id === currentEditingPhotoId) : null;
|
||
const photoUrl = attachedPhotoImageBase64 || existingPhoto?.imageUrl;
|
||
|
||
if (!photoUrl) {
|
||
alert("Пожалуйста, прикрепите фотографию!");
|
||
return;
|
||
}
|
||
|
||
const publishDate = dateVal ? new Date(dateVal).toISOString() : new Date().toISOString();
|
||
|
||
const photo = {
|
||
id: currentEditingPhotoId || "photo-" + Date.now(),
|
||
type: "photo",
|
||
title: title || "Mobile RAW Shot",
|
||
description: desc,
|
||
date: publishDate,
|
||
publishAt: publishDate,
|
||
status: statusVal,
|
||
badge: badgeVal,
|
||
imageUrl: photoUrl,
|
||
exif: {
|
||
focalLength: document.getElementById("admin-photo-lens").value.trim() || "24mm",
|
||
aperture: document.getElementById("admin-photo-aperture").value.trim() || "f/1.8",
|
||
shutterSpeed: document.getElementById("admin-photo-shutter").value.trim() || "1/120s",
|
||
iso: document.getElementById("admin-photo-iso").value.trim() || "ISO 100",
|
||
captureEngine: document.getElementById("admin-photo-engine").value.trim() || "Mobile RAW",
|
||
location: document.getElementById("admin-photo-location").value.trim() || "Ульяновск, Волга"
|
||
}
|
||
};
|
||
|
||
MatrixStore.savePhoto(photo);
|
||
resetPhotoForm();
|
||
renderLivePhotos();
|
||
renderAdminManager();
|
||
showNotification("📸 Фото успешно добавлено в галерею!");
|
||
}
|
||
|
||
// --- Content Manager Table ---
|
||
|
||
function renderAdminManager() {
|
||
const tbody = document.getElementById("admin-posts-table-body");
|
||
if (!tbody) return;
|
||
|
||
const notes = MatrixStore.getAllNotes();
|
||
const photos = MatrixStore.getAllPhotos();
|
||
const allItems = [...notes, ...photos].sort((a, b) => new Date(b.publishAt || b.date) - new Date(a.publishAt || a.date));
|
||
|
||
if (allItems.length === 0) {
|
||
tbody.innerHTML = `<tr><td colspan="5" style="text-align: center; color: var(--text-muted);">Записей нет</td></tr>`;
|
||
return;
|
||
}
|
||
|
||
const catNames = {
|
||
rides: "🚴♂️ Заметка",
|
||
books: "📚 Книга",
|
||
journal: "📓 Дневник",
|
||
workout: "🏋️ Тренировка",
|
||
tennis: "🏓 Теннис",
|
||
howto: "🛠️ Гайд"
|
||
};
|
||
|
||
tbody.innerHTML = allItems.map(item => {
|
||
const isNote = item.type === "note";
|
||
const catLabel = isNote ? (catNames[item.category] || "📝 Текст") : "📸 Фото";
|
||
const dateStr = new Date(item.publishAt || item.date).toLocaleDateString("ru-RU", {
|
||
month: "short", day: "numeric", hour: "2-digit", minute: "2-digit"
|
||
});
|
||
|
||
let statusBadge = "";
|
||
const isLive = MatrixStore.isItemLive(item);
|
||
|
||
if (item.status === "draft") {
|
||
statusBadge = `<span class="tag" style="color: var(--accent-amber); border-color: var(--accent-amber);">DRAFT</span>`;
|
||
} else if (item.status === "scheduled" && !isLive) {
|
||
const diffMin = Math.round((new Date(item.publishAt) - new Date()) / 60000);
|
||
statusBadge = `<span class="tag" style="color: var(--accent-cyan); border-color: var(--accent-cyan);">⏳ ТАЙМЕР (${diffMin > 60 ? Math.round(diffMin/60) + 'ч' : diffMin + 'м'})</span>`;
|
||
} else {
|
||
statusBadge = `<span class="tag green">● LIVE</span>`;
|
||
}
|
||
|
||
return `
|
||
<tr>
|
||
<td>${catLabel}</td>
|
||
<td style="font-weight: 600; color: var(--text-main);">${escapeHtml(item.title)}</td>
|
||
<td>${dateStr}</td>
|
||
<td>${statusBadge}</td>
|
||
<td>
|
||
<button class="btn-ctrl" onclick="MatrixAdmin.editItem('${item.id}', '${item.type}')">✏️</button>
|
||
${!isLive ? `<button class="btn-ctrl" onclick="MatrixAdmin.publishNow('${item.id}', '${item.type}')" title="Опубликовать сейчас">⚡</button>` : ""}
|
||
<button class="btn-ctrl" style="color: var(--accent-red);" onclick="MatrixAdmin.deleteItem('${item.id}', '${item.type}')">🗑️</button>
|
||
</td>
|
||
</tr>`;
|
||
}).join("");
|
||
}
|
||
|
||
function editItem(id, type) {
|
||
if (type === "note") {
|
||
const note = MatrixStore.getAllNotes().find(n => n.id === id);
|
||
if (!note) return;
|
||
|
||
currentEditingNoteId = note.id;
|
||
document.getElementById("admin-note-title").value = note.title || "";
|
||
if (document.getElementById("admin-note-category")) {
|
||
document.getElementById("admin-note-category").value = note.category || "rides";
|
||
}
|
||
document.getElementById("admin-note-content").value = note.content || "";
|
||
document.getElementById("admin-note-status").value = note.status || "published";
|
||
document.getElementById("admin-note-tags").value = (note.tags || []).join(", ");
|
||
|
||
const localIso = new Date(new Date(note.publishAt || note.date).getTime() - new Date().getTimezoneOffset() * 60000).toISOString().slice(0, 16);
|
||
document.getElementById("admin-note-date").value = localIso;
|
||
|
||
const tele = note.telemetry || {};
|
||
document.getElementById("admin-tele-dist").value = tele.distance || "";
|
||
document.getElementById("admin-tele-speed").value = tele.avgSpeed || "";
|
||
document.getElementById("admin-tele-elev").value = tele.elevation || "";
|
||
document.getElementById("admin-tele-watts").value = tele.watts || "";
|
||
document.getElementById("admin-tele-cadence").value = tele.cadence || "";
|
||
document.getElementById("admin-tele-hr").value = tele.heartRate || "";
|
||
|
||
if (note.heroImage) {
|
||
attachedNoteImageBase64 = note.heroImage;
|
||
document.getElementById("admin-note-file-preview").src = note.heroImage;
|
||
document.getElementById("admin-note-file-preview-wrap").style.display = "block";
|
||
}
|
||
|
||
document.getElementById("admin-note-form-title").innerText = "Редактирование записи: #" + note.id;
|
||
switchAdminTab("note-editor");
|
||
document.getElementById("admin-note-content").dispatchEvent(new Event("input"));
|
||
} else {
|
||
const photo = MatrixStore.getAllPhotos().find(p => p.id === id);
|
||
if (!photo) return;
|
||
|
||
currentEditingPhotoId = photo.id;
|
||
document.getElementById("admin-photo-title").value = photo.title || "";
|
||
document.getElementById("admin-photo-desc").value = photo.description || "";
|
||
document.getElementById("admin-photo-badge").value = photo.badge || "RAW DNG";
|
||
document.getElementById("admin-photo-status").value = photo.status || "published";
|
||
|
||
const localIso = new Date(new Date(photo.publishAt || photo.date).getTime() - new Date().getTimezoneOffset() * 60000).toISOString().slice(0, 16);
|
||
document.getElementById("admin-photo-date").value = localIso;
|
||
|
||
const exif = photo.exif || {};
|
||
document.getElementById("admin-photo-lens").value = exif.focalLength || "24mm";
|
||
document.getElementById("admin-photo-aperture").value = exif.aperture || "f/1.8";
|
||
document.getElementById("admin-photo-shutter").value = exif.shutterSpeed || "1/120s";
|
||
document.getElementById("admin-photo-iso").value = exif.iso || "ISO 100";
|
||
document.getElementById("admin-photo-engine").value = exif.captureEngine || "Mobile RAW";
|
||
document.getElementById("admin-photo-location").value = exif.location || "Ульяновск, Волга";
|
||
|
||
if (photo.imageUrl) {
|
||
attachedPhotoImageBase64 = photo.imageUrl;
|
||
document.getElementById("admin-photo-file-preview").src = photo.imageUrl;
|
||
document.getElementById("admin-photo-file-preview-wrap").style.display = "block";
|
||
}
|
||
|
||
document.getElementById("admin-photo-form-title").innerText = "Редактирование фото: #" + photo.id;
|
||
switchAdminTab("photo-editor");
|
||
}
|
||
}
|
||
|
||
function deleteItem(id, type) {
|
||
if (!confirm("Удалить эту публикацию?")) return;
|
||
if (type === "note") {
|
||
MatrixStore.deleteNote(id);
|
||
} else {
|
||
MatrixStore.deletePhoto(id);
|
||
}
|
||
renderAllSections();
|
||
renderAdminManager();
|
||
showNotification("Публикация удалена");
|
||
}
|
||
|
||
function publishNow(id, type) {
|
||
if (type === "note") {
|
||
const note = MatrixStore.getAllNotes().find(n => n.id === id);
|
||
if (note) {
|
||
note.status = "published";
|
||
note.publishAt = new Date().toISOString();
|
||
MatrixStore.saveNote(note);
|
||
}
|
||
} else {
|
||
const photo = MatrixStore.getAllPhotos().find(p => p.id === id);
|
||
if (photo) {
|
||
photo.status = "published";
|
||
photo.publishAt = new Date().toISOString();
|
||
MatrixStore.savePhoto(photo);
|
||
}
|
||
}
|
||
renderAllSections();
|
||
renderAdminManager();
|
||
showNotification("⚡ Опубликовано сейчас!");
|
||
}
|
||
|
||
function resetNoteForm() {
|
||
currentEditingNoteId = null;
|
||
attachedNoteImageBase64 = "";
|
||
document.getElementById("admin-note-title").value = "";
|
||
document.getElementById("admin-note-content").value = "";
|
||
document.getElementById("admin-note-tags").value = "";
|
||
document.getElementById("admin-tele-dist").value = "";
|
||
document.getElementById("admin-tele-speed").value = "";
|
||
document.getElementById("admin-tele-elev").value = "";
|
||
document.getElementById("admin-tele-watts").value = "";
|
||
document.getElementById("admin-tele-cadence").value = "";
|
||
document.getElementById("admin-tele-hr").value = "";
|
||
document.getElementById("admin-note-file-preview-wrap").style.display = "none";
|
||
document.getElementById("admin-note-form-title").innerText = "📝 Создание новой записи блога";
|
||
|
||
const now = new Date();
|
||
const localIso = new Date(now.getTime() - now.getTimezoneOffset() * 60000).toISOString().slice(0, 16);
|
||
document.getElementById("admin-note-date").value = localIso;
|
||
document.getElementById("admin-note-preview-pane").innerHTML = '<em style="color: var(--text-muted);">Live Markdown превью...</em>';
|
||
}
|
||
|
||
function resetPhotoForm() {
|
||
currentEditingPhotoId = null;
|
||
attachedPhotoImageBase64 = "";
|
||
document.getElementById("admin-photo-title").value = "";
|
||
document.getElementById("admin-photo-desc").value = "";
|
||
document.getElementById("admin-photo-file-preview-wrap").style.display = "none";
|
||
document.getElementById("admin-photo-form-title").innerText = "📸 Загрузка мобильного RAW фото";
|
||
|
||
const now = new Date();
|
||
const localIso = new Date(now.getTime() - now.getTimezoneOffset() * 60000).toISOString().slice(0, 16);
|
||
document.getElementById("admin-photo-date").value = localIso;
|
||
}
|
||
|
||
function exportBackup() {
|
||
const json = MatrixStore.exportBackupJSON();
|
||
const blob = new Blob([json], { type: "application/json" });
|
||
const url = URL.createObjectURL(blob);
|
||
const a = document.createElement("a");
|
||
a.href = url;
|
||
a.download = `matrix_blog_backup_${new Date().toISOString().slice(0, 10)}.json`;
|
||
a.click();
|
||
URL.revokeObjectURL(url);
|
||
}
|
||
|
||
function importBackup(file) {
|
||
if (!file) return;
|
||
const reader = new FileReader();
|
||
reader.onload = function (e) {
|
||
const res = MatrixStore.importBackupJSON(e.target.result);
|
||
if (res.success) {
|
||
renderAllSections();
|
||
renderAdminManager();
|
||
alert(`Бэкап успешно восстановлен! Записей: ${res.countNotes}, Фото: ${res.countPhotos}`);
|
||
} else {
|
||
alert("Ошибка импорта JSON: " + res.error);
|
||
}
|
||
};
|
||
reader.readAsText(file);
|
||
}
|
||
|
||
function showNotification(msg) {
|
||
const notif = document.getElementById("matrix-toast-notification");
|
||
if (notif) {
|
||
notif.innerText = msg;
|
||
notif.style.display = "block";
|
||
setTimeout(() => {
|
||
notif.style.display = "none";
|
||
}, 3500);
|
||
}
|
||
}
|
||
|
||
function escapeHtml(text) {
|
||
if (!text) return "";
|
||
return String(text)
|
||
.replace(/&/g, "&")
|
||
.replace(/</g, "<")
|
||
.replace(/>/g, ">")
|
||
.replace(/"/g, """);
|
||
}
|
||
|
||
return {
|
||
init: init,
|
||
checkAdminAuth: checkAdminAuth,
|
||
unlockAdmin: unlockAdmin,
|
||
lockAdmin: lockAdmin,
|
||
handleSaveNote: handleSaveNote,
|
||
handleSavePhoto: handleSavePhoto,
|
||
editItem: editItem,
|
||
deleteItem: deleteItem,
|
||
publishNow: publishNow,
|
||
resetNoteForm: resetNoteForm,
|
||
resetPhotoForm: resetPhotoForm,
|
||
exportBackup: exportBackup,
|
||
importBackup: importBackup
|
||
};
|
||
})();
|