feat: add branches feature, security audit, and multi-branch support
This commit is contained in:
+71
-19
@@ -35,25 +35,37 @@
|
||||
</div>
|
||||
|
||||
<div class="modal-overlay" id="photoFormModal" onclick="if(event.target===this)closePhotoForm()">
|
||||
<div class="edit-modal">
|
||||
<div class="edit-modal" style="max-width:480px">
|
||||
<h3 id="photoFormTitle">Добавить фото</h3>
|
||||
<div>
|
||||
<label>Выбрать файл (jpg/png и т.п.)</label>
|
||||
<input type="file" id="photoFile" accept="image/*">
|
||||
</div>
|
||||
<div id="photoFormPreview" class="photo-preview" style="display:none"><img id="photoPreviewImg" alt=""></div>
|
||||
<div>
|
||||
<label>Подпись</label>
|
||||
<textarea id="photoCaption" placeholder="Например: выступление, тренировка..."></textarea>
|
||||
</div>
|
||||
<div>
|
||||
<label>Дата съёмки</label>
|
||||
<input type="date" id="photoTakenAt">
|
||||
</div>
|
||||
<div class="actions">
|
||||
<button class="cancel" onclick="closePhotoForm()">Отмена</button>
|
||||
<button class="save" onclick="savePhotoForm()">Сохранить</button>
|
||||
</div>
|
||||
<form id="photoForm" class="settings-stack" style="gap:12px">
|
||||
<div class="settings-row">
|
||||
<div class="settings-field" style="flex:1">
|
||||
<label>Выбрать файл (jpg/png и т.п.) <span style="color:#ef4444">*</span></label>
|
||||
<input type="file" id="photoFile" accept="image/*">
|
||||
</div>
|
||||
</div>
|
||||
<div class="settings-row">
|
||||
<div class="settings-field" style="flex:1">
|
||||
<div id="photoFormPreview" class="photo-preview" style="display:none"><img id="photoPreviewImg" alt=""></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="settings-row">
|
||||
<div class="settings-field" style="flex:1">
|
||||
<label>Подпись</label>
|
||||
<textarea id="photoCaption" class="settings-input" placeholder="Например: выступление, тренировка..."></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="settings-row">
|
||||
<div class="settings-field" style="flex:1">
|
||||
<label>Дата съёмки</label>
|
||||
<input type="date" id="photoTakenAt" class="settings-input">
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-foot" style="justify-content:flex-end;gap:8px">
|
||||
<button type="button" class="btn-primary ghost" onclick="closePhotoForm()">Отмена</button>
|
||||
<button type="submit" class="btn-primary">Сохранить</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -64,6 +76,8 @@
|
||||
// --- Groups ---
|
||||
const DAYS = ['Вс', 'Пн', 'Вт', 'Ср', 'Чт', 'Пт', 'Сб'];
|
||||
let branchesCache = [];
|
||||
let allGroups = [];
|
||||
let currentCover = null;
|
||||
|
||||
async function loadBranchesCache() {
|
||||
const res = await fetch(`${API}/api/branches`, { headers: hdr() });
|
||||
@@ -79,6 +93,7 @@ async function loadGroups() {
|
||||
await loadBranchesCache();
|
||||
const res = await fetch(`${API}/api/groups`, { headers: hdr() });
|
||||
const groups = await res.json();
|
||||
allGroups = groups;
|
||||
const list = document.getElementById('groupList');
|
||||
if (!groups.length) { list.innerHTML = '<div style="color:var(--muted);text-align:center;padding:32px">Нет групп</div>'; return; }
|
||||
list.innerHTML = `<div class="group-grid">` + groups.map(g => {
|
||||
@@ -173,6 +188,8 @@ let editingPhotoId = null;
|
||||
async function openPhotos(groupId, groupName) {
|
||||
currentGroupId = groupId;
|
||||
currentGroupName = groupName;
|
||||
const g = allGroups.find(x => String(x.id) === String(groupId));
|
||||
currentCover = g ? g.cover_path : null;
|
||||
photoPage = 1;
|
||||
editingPhotoId = null;
|
||||
document.getElementById('photosTitle').textContent = `Фото — ${groupName}`;
|
||||
@@ -207,12 +224,14 @@ function fmtPhotoDate(d) { return d ? d.split('-').reverse().join('.') : ''; }
|
||||
function photoHTML(p) {
|
||||
const taken = p.taken_at ? `Дата: ${fmtPhotoDate(p.taken_at)}` : '';
|
||||
const uploaded = `Загружено: ${new Date(p.created_at).toLocaleDateString('ru')}`;
|
||||
const isCover = currentCover && p.photo_path === currentCover;
|
||||
return `
|
||||
<div class="photo-item">
|
||||
<div class="photo-item" data-photo-id="${p.id}" data-photo-path="${esc(p.photo_path)}">
|
||||
<img src="${API}${p.photo_path}" onclick="showImg('${API}${p.photo_path}')" alt="">
|
||||
<div class="p-cap">${esc(p.caption || '')}</div>
|
||||
<div class="p-date">${taken ? taken + ' · ' : ''}${uploaded}</div>
|
||||
<div class="p-actions">
|
||||
<button class="p-cover${isCover ? ' active' : ''}" data-photo-id="${p.id}" data-photo-path="${esc(p.photo_path)}" title="${isCover ? 'Текущая обложка' : 'Сделать обложкой'}" ${isCover ? 'disabled' : ''}>🖼️</button>
|
||||
<button class="p-edit" onclick="openEditPhoto(${p.id})" title="Редактировать">✎</button>
|
||||
<button class="p-del" onclick="deletePhoto(${p.id})" title="Удалить">×</button>
|
||||
</div>
|
||||
@@ -335,6 +354,10 @@ async function exportGroupUrls(groupId, groupName) {
|
||||
|
||||
document.getElementById('newGroup').addEventListener('keydown', e => { if (e.key === 'Enter') addGroup(); });
|
||||
document.getElementById('photoFile').addEventListener('change', previewPhotoFile);
|
||||
document.getElementById('photoForm').addEventListener('submit', e => {
|
||||
e.preventDefault();
|
||||
savePhotoForm();
|
||||
});
|
||||
|
||||
// Event delegation for group cover clicks
|
||||
document.getElementById('groupList').addEventListener('click', e => {
|
||||
@@ -346,6 +369,35 @@ document.getElementById('groupList').addEventListener('click', e => {
|
||||
}
|
||||
});
|
||||
|
||||
// Event delegation for photo actions in modal
|
||||
document.getElementById('photoGrid').addEventListener('click', async e => {
|
||||
const coverBtn = e.target.closest('.p-cover');
|
||||
if (coverBtn) {
|
||||
const photoId = parseInt(coverBtn.dataset.photoId, 10);
|
||||
const photoPath = coverBtn.dataset.photoPath;
|
||||
if (!isNaN(photoId)) {
|
||||
try {
|
||||
const res = await fetch(`${API}/api/groups/${currentGroupId}/photos/${photoId}/cover`, {
|
||||
method: 'PUT',
|
||||
headers: hdrJson(),
|
||||
});
|
||||
if (res.ok) {
|
||||
const g = await res.json();
|
||||
currentCover = g.cover_path || null;
|
||||
showToast('Обложка обновлена');
|
||||
loadGroupPhotos();
|
||||
loadGroups();
|
||||
} else {
|
||||
const err = await res.json();
|
||||
alert(err.error || 'Ошибка');
|
||||
}
|
||||
} catch (err) {
|
||||
alert('Ошибка сети: ' + err.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
async function populateNewGroupBranch() {
|
||||
const res = await fetch(`${API}/api/branches`, { headers: hdr() });
|
||||
if (res.ok) {
|
||||
|
||||
Reference in New Issue
Block a user