mne-nuzhno-veb-prilozhenie-d/index.html

327 lines
15 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!doctype html>
<html lang="ru">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Конструктор презентаций</title>
<style>
* { margin: 0; box-sizing: border-box; }
body { font-family: -apple-system, "Segoe UI", Roboto, sans-serif; background: #0f1218; color: #f4f4f5; }
.container { max-width: 1200px; margin: 0 auto; padding: 20px; }
header { display: flex; justify-content: space-between; align-items: center; padding: 20px 0; border-bottom: 1px solid #27272a; margin-bottom: 30px; }
h1 { font-size: 28px; }
button { background: #00b8cc; color: #0f1218; border: none; border-radius: 8px; padding: 10px 20px; cursor: pointer; font-size: 14px; font-weight: 600; }
button:hover { background: #00d9e8; }
button.secondary { background: #27272a; color: #f4f4f5; }
button.danger { background: #ef4444; color: white; }
.grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 20px; }
.card { background: #18181b; border-radius: 12px; padding: 20px; cursor: pointer; transition: transform 0.2s; }
.card:hover { transform: translateY(-4px); background: #27272a; }
.card h3 { margin-bottom: 10px; font-size: 18px; }
.card p { color: #a1a1aa; font-size: 14px; }
.card-actions { margin-top: 15px; display: flex; gap: 8px; }
.card-actions button { padding: 6px 12px; font-size: 12px; }
.modal { display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.8); z-index: 100; }
.modal.on { display: flex; justify-content: center; align-items: center; }
.modal-content { background: #18181b; border-radius: 12px; padding: 30px; width: 90%; max-width: 800px; max-height: 90vh; overflow-y: auto; }
.modal-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; }
.modal-header h2 { font-size: 24px; }
input[type="text"], textarea { width: 100%; padding: 12px; background: #27272a; border: 1px solid #3f3f46; border-radius: 8px; color: #f4f4f5; font-size: 14px; margin-bottom: 15px; }
textarea { min-height: 100px; resize: vertical; }
.slide-editor { background: #27272a; border-radius: 8px; padding: 20px; margin-bottom: 15px; }
.slide-editor h4 { margin-bottom: 10px; color: #00b8cc; }
.actions { display: flex; gap: 10px; margin-top: 20px; }
.empty { text-align: center; padding: 60px 20px; color: #a1a1aa; }
.preview-slide { background: #0f1218; border: 1px solid #3f3f46; border-radius: 8px; padding: 30px; margin-bottom: 20px; }
.preview-slide h1 { font-size: 32px; margin-bottom: 20px; color: #f4f4f5; }
.preview-slide li { font-size: 18px; line-height: 1.6; margin-bottom: 8px; list-style: none; padding-left: 20px; position: relative; }
.preview-slide li::before { content: ""; position: absolute; left: 0; top: 8px; width: 8px; height: 8px; border-radius: 2px; background: #00b8cc; }
.nav { display: flex; justify-content: space-between; margin-top: 20px; }
.upload-zone { border: 2px dashed #3f3f46; border-radius: 12px; padding: 40px; text-align: center; margin-bottom: 20px; cursor: pointer; transition: all 0.2s; }
.upload-zone:hover { border-color: #00b8cc; background: #1f1f23; }
.upload-zone.dragover { border-color: #00b8cc; background: #1f1f23; }
.upload-zone p { color: #a1a1aa; margin-bottom: 10px; }
.tabs { display: flex; gap: 10px; margin-bottom: 20px; }
.tab { padding: 10px 20px; background: #27272a; border-radius: 8px; cursor: pointer; }
.tab.active { background: #00b8cc; color: #0f1218; }
.loading { text-align: center; padding: 40px; color: #a1a1aa; }
.spinner { display: inline-block; width: 30px; height: 30px; border: 3px solid #27272a; border-top-color: #00b8cc; border-radius: 50%; animation: spin 1s linear infinite; }
@keyframes spin { to { transform: rotate(360deg); } }
</style>
</head>
<body>
<div class="container">
<header>
<h1>📊 Конструктор презентаций</h1>
<button onclick="showCreateModal()">+ Новая презентация</button>
</header>
<div id="presentations" class="grid"></div>
</div>
<!-- Модальное окно создания -->
<div id="createModal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h2>Новая презентация</h2>
<button class="secondary" onclick="closeModal('createModal')"></button>
</div>
<div class="tabs">
<div class="tab active" onclick="switchTab('manual')">Вручную</div>
<div class="tab" onclick="switchTab('auto')">Из документа (ИИ)</div>
</div>
<div id="manualTab">
<input type="text" id="newTitle" placeholder="Название презентации">
<input type="text" id="newAuthor" placeholder="Автор (необязательно)">
<div class="actions">
<button onclick="createPresentation()">Создать</button>
<button class="secondary" onclick="closeModal('createModal')">Отмена</button>
</div>
</div>
<div id="autoTab" style="display: none;">
<div class="upload-zone" id="uploadZone" onclick="document.getElementById('fileInput').click()">
<p>📎 Перетащите файл сюда или кликните для выбора</p>
<p style="font-size: 12px;">Поддерживаются: .xlsx, .docx, .pdf, .txt</p>
<input type="file" id="fileInput" accept=".xlsx,.xls,.docx,.pdf,.txt,.csv" style="display: none;" onchange="handleFile(this.files[0])">
</div>
<div id="uploadStatus" class="loading" style="display: none;">
<div class="spinner"></div>
<p>Анализирую документ и создаю презентацию...</p>
</div>
<div id="fileInfo" style="margin-bottom: 15px; color: #a1a1aa; display: none;"></div>
<div class="actions">
<button onclick="createFromDocument()">🤖 Создать презентацию из файла</button>
<button class="secondary" onclick="closeModal('createModal')">Отмена</button>
</div>
</div>
</div>
</div>
<!-- Модальное окно редактора -->
<div id="editorModal" class="modal">
<div class="modal-content" style="max-width: 1000px;">
<div class="modal-header">
<h2 id="editTitle">Редактирование</h2>
<button class="secondary" onclick="closeModal('editorModal')"></button>
</div>
<input type="text" id="presTitle" placeholder="Название">
<div id="slidesContainer"></div>
<button class="secondary" onclick="addSlide()">+ Добавить слайд</button>
<div class="actions">
<button onclick="savePresentation()">Сохранить</button>
<button class="secondary" onclick="showPreview()">Предпросмотр</button>
<button class="danger" onclick="deletePresentation()">Удалить</button>
</div>
</div>
</div>
<!-- Модальное окно предпросмотра -->
<div id="previewModal" class="modal">
<div class="modal-content" style="max-width: 900px;">
<div class="modal-header">
<h2 id="previewTitle">Предпросмотр</h2>
<button class="secondary" onclick="closeModal('previewModal')"></button>
</div>
<div id="previewContainer"></div>
<div class="nav">
<button class="secondary" onclick="prevSlide()">← Назад</button>
<span id="slideNum" style="color: #a1a1aa; align-self: center;"></span>
<button class="secondary" onclick="nextSlide()">Вперёд →</button>
</div>
</div>
</div>
<script>
let presentations = [];
let currentPres = null;
let previewIndex = 0;
let uploadedFile = null;
async function load() {
presentations = await fetch("/api/presentations").then(r => r.json());
render();
}
function render() {
const container = document.getElementById("presentations");
if (presentations.length === 0) {
container.innerHTML = '<div class="empty"><h2>Нет презентаций</h2><p>Создайте первую презентацию!</p></div>';
return;
}
container.innerHTML = presentations.map(p => `
<div class="card" onclick="openEditor(${p.id})">
<h3>${escapeHtml(p.title)}</h3>
<p>${p.author || "Без автора"}${p.slides ? p.slides.length : 0} слайд(ов)</p>
<div class="card-actions">
<button onclick="event.stopPropagation(); openEditor(${p.id})">Открыть</button>
</div>
</div>
`).join("");
}
function switchTab(tab) {
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
event.target.classList.add('active');
document.getElementById('manualTab').style.display = tab === 'manual' ? 'block' : 'none';
document.getElementById('autoTab').style.display = tab === 'auto' ? 'block' : 'none';
}
function showCreateModal() {
document.getElementById("newTitle").value = "";
document.getElementById("newAuthor").value = "";
uploadedFile = null;
document.getElementById("fileInfo").style.display = "none";
document.getElementById("uploadStatus").style.display = "none";
document.getElementById("createModal").classList.add("on");
}
async function createPresentation() {
const title = document.getElementById("newTitle").value.trim();
if (!title) return alert("Введите название");
await fetch("/api/presentations", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ title, author: document.getElementById("newAuthor").value.trim() })
});
closeModal("createModal");
load();
}
function handleFile(file) {
if (!file) return;
uploadedFile = file;
document.getElementById("fileInfo").style.display = "block";
document.getElementById("fileInfo").textContent = "📄 " + file.name + " (" + Math.round(file.size / 1024) + " КБ)";
}
// Drag-and-drop
const uploadZone = document.getElementById("uploadZone");
if (uploadZone) {
uploadZone.addEventListener("dragover", (e) => {
e.preventDefault();
uploadZone.classList.add("dragover");
});
uploadZone.addEventListener("dragleave", () => {
uploadZone.classList.remove("dragover");
});
uploadZone.addEventListener("drop", (e) => {
e.preventDefault();
uploadZone.classList.remove("dragover");
const file = e.dataTransfer.files[0];
if (file) handleFile(file);
});
}
async function createFromDocument() {
if (!uploadedFile) return alert("Выберите файл");
document.getElementById("uploadStatus").style.display = "block";
const formData = new FormData();
formData.append("file", uploadedFile);
try {
const resp = await fetch("/api/create-from-file", {
method: "POST",
body: formData
});
const result = await resp.json();
document.getElementById("uploadStatus").style.display = "none";
if (result.ok) {
alert("Презентация создана! " + result.slides + " слайд(ов)");
closeModal("createModal");
load();
} else {
alert("Ошибка: " + (result.error || "Не удалось создать презентацию"));
}
} catch (e) {
document.getElementById("uploadStatus").style.display = "none";
alert("Ошибка загрузки: " + e.message);
}
}
async function openEditor(id) {
currentPres = await fetch("/api/presentations/" + id).then(r => r.json());
document.getElementById("presTitle").value = currentPres.title;
document.getElementById("editTitle").textContent = currentPres.title;
renderSlides();
document.getElementById("editorModal").classList.add("on");
}
function renderSlides() {
const container = document.getElementById("slidesContainer");
container.innerHTML = currentPres.slides.map((s, i) => `
<div class="slide-editor">
<h4>Слайд ${i + 1}</h4>
<input type="text" value="${escapeHtml(s.title)}" onchange="updateSlide(${i}, 'title', this.value)" placeholder="Заголовок слайда">
<textarea onchange="updateSlide(${i}, 'bullets', this.value)" placeholder="Тезисы (каждый с новой строки)">${(s.bullets || []).join("\n")}</textarea>
</div>
`).join("");
}
function updateSlide(index, field, value) {
if (field === "bullets") {
currentPres.slides[index][field] = value.split("\n").filter(l => l.trim());
} else {
currentPres.slides[index][field] = value;
}
}
function addSlide() {
currentPres.slides.push({ title: "Новый слайд", bullets: ["Тезис"] });
renderSlides();
}
async function savePresentation() {
currentPres.title = document.getElementById("presTitle").value;
await fetch("/api/presentations/" + currentPres.id, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(currentPres)
});
alert("Сохранено!");
load();
}
async function deletePresentation() {
if (!confirm("Удалить презентацию?")) return;
await fetch("/api/presentations/" + currentPres.id, { method: "DELETE" });
closeModal("editorModal");
load();
}
function showPreview() {
previewIndex = 0;
document.getElementById("previewTitle").textContent = currentPres.title;
renderPreview();
document.getElementById("previewModal").classList.add("on");
}
function renderPreview() {
const container = document.getElementById("previewContainer");
const slide = currentPres.slides[previewIndex];
container.innerHTML = `
<div class="preview-slide">
<h1>${escapeHtml(slide.title)}</h1>
<ul>${(slide.bullets || []).map(b => "<li>" + escapeHtml(b) + "</li>").join("")}</ul>
</div>
`;
document.getElementById("slideNum").textContent = (previewIndex + 1) + " / " + currentPres.slides.length;
}
function prevSlide() { if (previewIndex > 0) { previewIndex--; renderPreview(); } }
function nextSlide() { if (previewIndex < currentPres.slides.length - 1) { previewIndex++; renderPreview(); } }
function closeModal(id) { document.getElementById(id).classList.remove("on"); }
function escapeHtml(text) {
if (!text) return "";
return String(text).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
}
load();
</script>
</body>
</html>