init f0ckm
This commit is contained in:
322
views/admin/halls.html
Normal file
322
views/admin/halls.html
Normal file
@@ -0,0 +1,322 @@
|
||||
@include(snippets/header)
|
||||
<div class="pagewrapper">
|
||||
<div id="main">
|
||||
<style>
|
||||
.hm-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
height: 28px;
|
||||
padding: 0 12px;
|
||||
border-radius: 3px;
|
||||
font-size: 0.85em;
|
||||
font-family: var(--font);
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
text-decoration: none;
|
||||
box-sizing: border-box;
|
||||
line-height: 1;
|
||||
}
|
||||
</style>
|
||||
<div class="container" style="padding-top: 20px;">
|
||||
<h1>{{ t('hall.manager_title') }}</h1>
|
||||
<p style="color: #aaa; margin-bottom: 20px;">{{ t('hall.manager_desc') }}</p>
|
||||
|
||||
<div style="display:flex;gap:10px;align-items:center;flex-wrap:wrap;margin-bottom:20px;">
|
||||
<input type="text" id="new-hall-name" placeholder="{{ t('hall.new_hall_placeholder') }}" style="flex:1;min-width:180px;background:var(--badge-bg);border:1px solid rgba(255,255,255,0.15);color:var(--white);padding:0 10px;height:28px;border-radius:3px;font-family:var(--font);font-size:0.9em;">
|
||||
<select id="new-hall-rating" style="background:var(--badge-bg);border:1px solid rgba(255,255,255,0.15);color:var(--white);padding:0 10px;height:28px;border-radius:3px;font-family:var(--font);font-size:0.9em;">
|
||||
<option value="sfw">🟢 SFW</option>
|
||||
<option value="nsfw">🔴 NSFW</option>
|
||||
<option value="nsfl">💀 NSFL</option>
|
||||
</select>
|
||||
<span id="new-hall-slug-preview" style="font-size:0.78em;color:#555;min-width:80px;"></span>
|
||||
<button id="btn-create-hall" class="hm-btn" style="background:rgba(255,255,255,0.1);color:var(--white);border:1px solid rgba(255,255,255,0.2);font-weight:bold;">{{ t('hall.create_hall_btn') }}</button>
|
||||
<span id="create-hall-status" style="font-size:0.8em;color:#888;"></span>
|
||||
</div>
|
||||
<hr>
|
||||
|
||||
<div id="hall-manager-grid" style="display: grid; grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); gap: 20px; margin-top: 20px;">
|
||||
@each(hallsList as h)
|
||||
<div class="hall-manager-card" id="hall-card-{{ h.slug }}" data-slug="{{ h.slug }}">
|
||||
<div class="hall-manager-image" style="position:relative; height: 140px; overflow:hidden; background:#111; cursor:pointer;" title="{{ t('hall.click_upload_hint') }}">
|
||||
<img src="/hall_image/{{ h.slug }}" alt="{!! h.name !!}" style="width:100%;height:100%;object-fit:cover;opacity:0.8;" id="hall-img-{{ h.slug }}">
|
||||
<span style="position:absolute;inset:0;display:flex;align-items:center;justify-content:center;font-size:1.1em;font-weight:700;color:#fff;text-transform:uppercase;letter-spacing:0.05em;text-shadow:0 0 10px rgba(0,0,0,0.9),0 1px 3px rgba(0,0,0,0.8);pointer-events:none;">{!! h.name !!}</span>
|
||||
@if(h.custom_image)
|
||||
<button class="btn-del-img" title="Remove custom image" style="position:absolute;top:6px;right:6px;width:24px;height:24px;display:flex;align-items:center;justify-content:center;background:rgba(0,0,0,0.7);color:#fff;border:1px solid rgba(255,255,255,0.3);border-radius:50%;cursor:pointer;font-size:0.75em;line-height:1;z-index:2;padding:0;">✕</button>
|
||||
@endif
|
||||
<input type="file" class="hall-img-upload" accept="image/*" style="display:none;">
|
||||
</div>
|
||||
<div style="padding: 12px;">
|
||||
<div style="margin-bottom: 8px;">
|
||||
<label style="font-size:0.8em;color:#888;display:block;">{{ t('common.name') }}</label>
|
||||
<input type="text" class="hall-name-input" value="{!! h.name !!}" style="width:100%;box-sizing:border-box;background:var(--badge-bg);border:1px solid rgba(255,255,255,0.15);color:var(--white);padding:5px 8px;border-radius:3px;font-family:var(--font);">
|
||||
</div>
|
||||
<div style="margin-bottom: 8px;">
|
||||
<label style="font-size:0.8em;color:#888;display:block;">{{ t('hall.slug') }}</label>
|
||||
<input type="text" class="hall-slug-input" value="{!! h.slug !!}" style="width:100%;box-sizing:border-box;background:var(--badge-bg);border:1px solid rgba(255,255,255,0.15);color:var(--white);padding:5px 8px;border-radius:3px;font-family:var(--font);font-size:0.9em;color:#aaa;">
|
||||
</div>
|
||||
<div style="margin-bottom: 8px;">
|
||||
<label style="font-size:0.8em;color:#888;display:block;">{{ t('common.description') }}</label>
|
||||
<textarea class="hall-desc-input" style="width:100%;box-sizing:border-box;background:var(--badge-bg);border:1px solid rgba(255,255,255,0.15);color:var(--white);padding:5px 8px;border-radius:3px;font-family:var(--font);resize:vertical;min-height:60px;">{!! h.description || '' !!}</textarea>
|
||||
</div>
|
||||
<div style="margin-bottom: 8px;">
|
||||
<label style="font-size:0.8em;color:#888;display:block;">{{ t('hall.rating') }}</label>
|
||||
<select class="hall-rating-input" style="width:100%;box-sizing:border-box;background:var(--badge-bg);border:1px solid rgba(255,255,255,0.15);color:var(--white);padding:5px 8px;border-radius:3px;font-family:var(--font);">
|
||||
<option value="sfw" @if(h.rating === 'sfw' || !h.rating) selected @endif>🟢 SFW</option>
|
||||
<option value="nsfw" @if(h.rating === 'nsfw') selected @endif>🔴 NSFW</option>
|
||||
<option value="nsfl" @if(h.rating === 'nsfl') selected @endif>💀 NSFL</option>
|
||||
</select>
|
||||
</div>
|
||||
<div style="display:flex;gap:8px;flex-wrap:wrap;align-items:center;">
|
||||
<button class="btn-save-hall hm-btn" style="background:var(--accent);color:#000;border:none;font-weight:bold;">{{ t('common.save') }}</button>
|
||||
<a href="/h/{{ h.slug }}" class="hall-view-link hm-btn" style="background:rgba(255,255,255,0.05);color:#aaa;border:1px solid rgba(255,255,255,0.1);">{{ t('common.view') }} →</a>
|
||||
<span style="margin-left:4px;font-size:0.8em;color:#666;">{{ t('hall.posts', { count: h.item_count || 0 }) }}</span>
|
||||
<button class="btn-delete-hall hm-btn" style="margin-left:auto;background:rgba(200,0,0,0.25);color:#f55;border:1px solid rgba(200,0,0,0.4);">🗑 {{ t('common.delete') }}</button>
|
||||
</div>
|
||||
<div class="hall-status" style="margin-top:6px;font-size:0.8em;color:#888;min-height:1.2em;"></div>
|
||||
</div>
|
||||
</div>
|
||||
@endeach
|
||||
</div>
|
||||
|
||||
@if(!hallsList || !hallsList.length)
|
||||
<p style="color:#888;text-align:center;padding:40px 0;">{{ t('hall.no_halls') }}</p>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function() {
|
||||
var grid = document.getElementById('hall-manager-grid');
|
||||
var i18n = window.f0ckI18n || {};
|
||||
|
||||
// ── Card wiring ─────────────────────────────────────────────────────────
|
||||
function wireCard(card) {
|
||||
var slug = card.dataset.slug;
|
||||
var status = card.querySelector('.hall-status');
|
||||
var nameInput = card.querySelector('.hall-name-input');
|
||||
var slugInput = card.querySelector('.hall-slug-input');
|
||||
var descInput = card.querySelector('.hall-desc-input');
|
||||
var ratingInput = card.querySelector('.hall-rating-input');
|
||||
var imgEl = card.querySelector('.hall-manager-image img');
|
||||
var imgContainer = card.querySelector('.hall-manager-image');
|
||||
var fileInput = card.querySelector('.hall-img-upload');
|
||||
var viewLink = card.querySelector('.hall-view-link');
|
||||
|
||||
function setStatus(msg, color) {
|
||||
status.textContent = msg;
|
||||
status.style.color = color || '#aaa';
|
||||
}
|
||||
|
||||
// Live name overlay
|
||||
var nameOverlay = imgContainer.querySelector('span');
|
||||
nameInput.addEventListener('input', function() {
|
||||
if (nameOverlay) nameOverlay.textContent = nameInput.value;
|
||||
});
|
||||
|
||||
// Auto-sanitize slug on blur
|
||||
slugInput.addEventListener('blur', function() {
|
||||
slugInput.value = slugInput.value.trim().toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
|
||||
});
|
||||
|
||||
// Click image → open file picker
|
||||
imgContainer.addEventListener('click', function(e) {
|
||||
if (e.target.classList.contains('btn-del-img')) return;
|
||||
fileInput.click();
|
||||
});
|
||||
|
||||
// Save name + slug + description
|
||||
card.querySelector('.btn-save-hall').addEventListener('click', async function() {
|
||||
setStatus(i18n.hall_saving || 'Saving...');
|
||||
var newSlug = slugInput.value.trim().toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
|
||||
if (!newSlug) { setStatus("✗ " + (i18n.hall_slug_empty_error || 'Slug cannot be empty'), '#f55'); return; }
|
||||
try {
|
||||
var r = await fetch('/api/v2/admin/halls/' + encodeURIComponent(slug), {
|
||||
method: 'PATCH',
|
||||
headers: { 'Content-Type': 'application/json', 'x-csrf-token': (window.f0ckSession && window.f0ckSession.csrf_token) || '' },
|
||||
body: JSON.stringify({ name: nameInput.value, slug: newSlug, description: descInput.value, rating: ratingInput.value })
|
||||
});
|
||||
var d = await r.json();
|
||||
if (d.success) {
|
||||
// If slug changed, update all references in the DOM
|
||||
if (newSlug !== slug) {
|
||||
slug = newSlug; // update closure variable — all future fetches use new slug
|
||||
card.dataset.slug = newSlug;
|
||||
card.id = 'hall-card-' + newSlug;
|
||||
imgEl.src = '/hall_image/' + newSlug + '?v=' + Date.now();
|
||||
if (viewLink) viewLink.href = '/h/' + newSlug;
|
||||
slugInput.value = newSlug;
|
||||
}
|
||||
setStatus("✓ " + (i18n.hall_saved || 'Saved'), 'var(--accent)');
|
||||
} else {
|
||||
setStatus('✗ ' + d.msg, '#f55');
|
||||
}
|
||||
} catch (e) { setStatus("✗ " + (i18n.hall_error || 'Error'), '#f55'); }
|
||||
});
|
||||
|
||||
// Upload image
|
||||
fileInput.addEventListener('change', async function(e) {
|
||||
var file = e.target.files[0];
|
||||
if (!file) return;
|
||||
setStatus(i18n.uploading || 'Uploading...');
|
||||
var fd = new FormData();
|
||||
fd.append('file', file);
|
||||
try {
|
||||
var r = await fetch('/api/v2/admin/halls/' + encodeURIComponent(slug) + '/image', {
|
||||
method: 'POST',
|
||||
headers: { 'x-csrf-token': (window.f0ckSession && window.f0ckSession.csrf_token) || '' },
|
||||
body: fd
|
||||
});
|
||||
var d = await r.json();
|
||||
if (d.success) {
|
||||
imgEl.src = '/hall_image/' + slug + '?v=' + Date.now();
|
||||
setStatus("✓ " + (i18n.hall_image_uploaded || 'Image uploaded'), 'var(--accent)');
|
||||
if (!imgContainer.querySelector('.btn-del-img')) {
|
||||
var xBtn = document.createElement('button');
|
||||
xBtn.className = 'btn-del-img';
|
||||
xBtn.title = i18n.hall_click_upload_hint || 'Click to upload';
|
||||
xBtn.textContent = '✕';
|
||||
xBtn.style.cssText = 'position:absolute;top:6px;right:6px;width:24px;height:24px;display:flex;align-items:center;justify-content:center;background:rgba(0,0,0,0.7);color:#fff;border:1px solid rgba(255,255,255,0.3);border-radius:50%;cursor:pointer;font-size:0.75em;line-height:1;z-index:2;padding:0;';
|
||||
imgContainer.appendChild(xBtn);
|
||||
wireDelBtn(xBtn);
|
||||
}
|
||||
} else {
|
||||
setStatus('✗ ' + d.msg, '#f55');
|
||||
}
|
||||
} catch (e) { setStatus("✗ " + (i18n.hall_error || 'Error'), '#f55'); }
|
||||
e.target.value = '';
|
||||
});
|
||||
|
||||
function wireDelBtn(btn) {
|
||||
btn.addEventListener('click', async function(e) {
|
||||
e.stopPropagation();
|
||||
setStatus(i18n.hall_removing || 'Removing...');
|
||||
try {
|
||||
var r = await fetch('/api/v2/admin/halls/' + encodeURIComponent(slug) + '/image', {
|
||||
method: 'DELETE',
|
||||
headers: { 'x-csrf-token': (window.f0ckSession && window.f0ckSession.csrf_token) || '' }
|
||||
});
|
||||
var d = await r.json();
|
||||
if (d.success) {
|
||||
imgEl.src = '/hall_image/' + slug + '?v=' + Date.now();
|
||||
btn.remove();
|
||||
setStatus("✓ " + (i18n.hall_image_removed || 'Image removed'), 'var(--accent)');
|
||||
} else { setStatus('✗ ' + d.msg, '#f55'); }
|
||||
} catch (e) { setStatus("✗ " + (i18n.hall_error || 'Error'), '#f55'); }
|
||||
});
|
||||
}
|
||||
|
||||
var existingDelBtn = imgContainer.querySelector('.btn-del-img');
|
||||
if (existingDelBtn) wireDelBtn(existingDelBtn);
|
||||
|
||||
// Delete hall
|
||||
card.querySelector('.btn-delete-hall').addEventListener('click', async function() {
|
||||
var confirmMsg = (i18n.hall_delete_confirm || 'Really delete hall "{slug}"?').replace('{slug}', slug);
|
||||
if (!confirm(confirmMsg)) return;
|
||||
setStatus(i18n.hall_deleting || 'Deleting...');
|
||||
try {
|
||||
var r = await fetch('/api/v2/admin/halls/' + encodeURIComponent(slug), {
|
||||
method: 'DELETE',
|
||||
headers: { 'x-csrf-token': (window.f0ckSession && window.f0ckSession.csrf_token) || '' }
|
||||
});
|
||||
var d = await r.json();
|
||||
if (d.success) {
|
||||
card.style.transition = 'opacity 0.3s';
|
||||
card.style.opacity = '0';
|
||||
setTimeout(function() { card.remove(); }, 300);
|
||||
} else { setStatus('✗ ' + d.msg, '#f55'); }
|
||||
} catch (e) { setStatus('✗ Error', '#f55'); }
|
||||
});
|
||||
}
|
||||
|
||||
// Wire all existing cards on load
|
||||
document.querySelectorAll('.hall-manager-card').forEach(wireCard);
|
||||
|
||||
// ── Create Hall ──────────────────────────────────────────────────────────
|
||||
var newHallName = document.getElementById('new-hall-name');
|
||||
var newHallRating = document.getElementById('new-hall-rating');
|
||||
var slugPreview = document.getElementById('new-hall-slug-preview');
|
||||
var createBtn = document.getElementById('btn-create-hall');
|
||||
var createStatus = document.getElementById('create-hall-status');
|
||||
|
||||
function toSlug(s) {
|
||||
return s.trim().toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
|
||||
}
|
||||
|
||||
newHallName.addEventListener('input', function() {
|
||||
var slug = toSlug(newHallName.value);
|
||||
slugPreview.textContent = slug ? '/' + slug : '';
|
||||
});
|
||||
|
||||
function buildCard(name, slug, rating) {
|
||||
var ratingOpts = ['sfw','nsfw','nsfl'].map(function(r) {
|
||||
return '<option value="' + r + '"' + (r === rating ? ' selected' : '') + '>' + (r === 'sfw' ? '🟢 SFW' : r === 'nsfw' ? '🔴 NSFW' : '💀 NSFL') + '</option>';
|
||||
}).join('');
|
||||
var div = document.createElement('div');
|
||||
div.className = 'hall-manager-card';
|
||||
div.id = 'hall-card-' + slug;
|
||||
div.dataset.slug = slug;
|
||||
div.innerHTML =
|
||||
'<div class="hall-manager-image" style="position:relative;height:140px;overflow:hidden;background:#111;cursor:pointer;" title="Click to upload a custom image">' +
|
||||
'<img src="/hall_image/' + slug + '" alt="' + name + '" style="width:100%;height:100%;object-fit:cover;opacity:0.8;">' +
|
||||
'<span style="position:absolute;inset:0;display:flex;align-items:center;justify-content:center;font-size:1.1em;font-weight:700;color:#fff;text-transform:uppercase;letter-spacing:0.05em;text-shadow:0 0 10px rgba(0,0,0,0.9),0 1px 3px rgba(0,0,0,0.8);pointer-events:none;">' + name + '</span>' +
|
||||
'<input type="file" class="hall-img-upload" accept="image/*" style="display:none;">' +
|
||||
'</div>' +
|
||||
'<div style="padding:12px;">' +
|
||||
'<div style="margin-bottom:8px;"><label style="font-size:0.8em;color:#888;display:block;">' + (i18n.common_name || 'Name') + '</label>' +
|
||||
'<input type="text" class="hall-name-input" value="' + name + '" style="width:100%;box-sizing:border-box;background:var(--badge-bg);border:1px solid rgba(255,255,255,0.15);color:var(--white);padding:5px 8px;border-radius:3px;font-family:var(--font);"></div>' +
|
||||
'<div style="margin-bottom:8px;"><label style="font-size:0.8em;color:#888;display:block;">' + (i18n.hall_slug || 'Slug') + '</label>' +
|
||||
'<input type="text" class="hall-slug-input" value="' + slug + '" style="width:100%;box-sizing:border-box;background:var(--badge-bg);border:1px solid rgba(255,255,255,0.15);color:var(--white);padding:5px 8px;border-radius:3px;font-family:var(--font);font-size:0.9em;color:#aaa;"></div>' +
|
||||
'<div style="margin-bottom:8px;"><label style="font-size:0.8em;color:#888;display:block;">' + (i18n.common_description || 'Description') + '</label>' +
|
||||
'<textarea class="hall-desc-input" style="width:100%;box-sizing:border-box;background:var(--badge-bg);border:1px solid rgba(255,255,255,0.15);color:var(--white);padding:5px 8px;border-radius:3px;font-family:var(--font);resize:vertical;min-height:60px;"></textarea></div>' +
|
||||
'<div style="margin-bottom:8px;"><label style="font-size:0.8em;color:#888;display:block;">' + (i18n.hall_rating || 'Rating') + '</label>' +
|
||||
'<select class="hall-rating-input" style="width:100%;box-sizing:border-box;background:var(--badge-bg);border:1px solid rgba(255,255,255,0.15);color:var(--white);padding:5px 8px;border-radius:3px;font-family:var(--font);">' + ratingOpts + '</select></div>' +
|
||||
'<div style="display:flex;gap:8px;flex-wrap:wrap;align-items:center;">' +
|
||||
'<button class="btn-save-hall hm-btn" style="background:var(--accent);color:#000;border:none;font-weight:bold;">' + (i18n.common_save || 'Save') + '</button>' +
|
||||
'<a href="/h/' + slug + '" class="hall-view-link hm-btn" style="background:rgba(255,255,255,0.05);color:#aaa;border:1px solid rgba(255,255,255,0.1);">' + (i18n.common_view || 'View') + ' →</a>' +
|
||||
'<span style="margin-left:4px;font-size:0.8em;color:#666;">' + (i18n.hall_posts || '{count} posts').replace('{count}', '0') + '</span>' +
|
||||
'<button class="btn-delete-hall hm-btn" style="margin-left:auto;background:rgba(200,0,0,0.25);color:#f55;border:1px solid rgba(200,0,0,0.4);">🗑 ' + (i18n.common_delete || 'Delete') + '</button>' +
|
||||
'</div>' +
|
||||
'<div class="hall-status" style="margin-top:6px;font-size:0.8em;color:#888;min-height:1.2em;"></div>' +
|
||||
'</div>';
|
||||
return div;
|
||||
}
|
||||
|
||||
async function createHall() {
|
||||
var name = newHallName.value.trim();
|
||||
if (!name) { createStatus.textContent = i18n.hall_enter_name_error || 'Enter a name'; return; }
|
||||
var slug = toSlug(name);
|
||||
var rating = newHallRating ? newHallRating.value : 'sfw';
|
||||
createStatus.textContent = i18n.hall_creating || 'Creating...';
|
||||
createStatus.style.color = '#888';
|
||||
try {
|
||||
var r = await fetch('/api/v2/admin/halls', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', 'x-csrf-token': (window.f0ckSession && window.f0ckSession.csrf_token) || '' },
|
||||
body: JSON.stringify({ name: name, slug: slug, rating: rating })
|
||||
});
|
||||
var d = await r.json();
|
||||
if (d.success) {
|
||||
createStatus.textContent = "✓ " + (i18n.hall_saved || 'Saved') + "!";
|
||||
createStatus.style.color = 'var(--accent)';
|
||||
var card = buildCard(name, slug, rating);
|
||||
grid.appendChild(card);
|
||||
wireCard(card);
|
||||
// Scroll new card into view
|
||||
card.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||
// Reset form
|
||||
newHallName.value = '';
|
||||
slugPreview.textContent = '';
|
||||
setTimeout(function() { createStatus.textContent = ''; }, 2000);
|
||||
} else {
|
||||
createStatus.textContent = '✗ ' + d.msg;
|
||||
createStatus.style.color = '#f55';
|
||||
}
|
||||
} catch (e) { createStatus.textContent = "✗ " + (i18n.hall_error || 'Error'); createStatus.style.color = '#f55'; }
|
||||
}
|
||||
|
||||
createBtn.addEventListener('click', createHall);
|
||||
newHallName.addEventListener('keydown', function(e) { if (e.key === 'Enter') createHall(); });
|
||||
})();
|
||||
</script>
|
||||
</div>
|
||||
</div>
|
||||
@include(snippets/footer)
|
||||
Reference in New Issue
Block a user