Adding config bool for userhall image upload
This commit is contained in:
@@ -52,6 +52,7 @@
|
|||||||
"private_messages": true,
|
"private_messages": true,
|
||||||
"halls_enabled": true,
|
"halls_enabled": true,
|
||||||
"userhalls_enabled": true,
|
"userhalls_enabled": true,
|
||||||
|
"enable_userhall_image_upload": true,
|
||||||
"abyss_enabled": true,
|
"abyss_enabled": true,
|
||||||
"meme_creator": true,
|
"meme_creator": true,
|
||||||
|
|
||||||
|
|||||||
@@ -50,6 +50,12 @@ export const handleHallImageUpload = async (req, res) => {
|
|||||||
return sendJson(res, { success: false, msg: 'Unauthorized' }, 403);
|
return sendJson(res, { success: false, msg: 'Unauthorized' }, 403);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CSRF check
|
||||||
|
const token = req.headers['x-csrf-token'] || req.url?.qs?.csrf_token;
|
||||||
|
if (!token || token !== session.csrf_token) {
|
||||||
|
return sendJson(res, { success: false, msg: 'Invalid CSRF token' }, 403);
|
||||||
|
}
|
||||||
|
|
||||||
const hallSlug = req.params && req.params.slug;
|
const hallSlug = req.params && req.params.slug;
|
||||||
if (!hallSlug) return sendJson(res, { success: false, msg: 'Missing hall slug' }, 400);
|
if (!hallSlug) return sendJson(res, { success: false, msg: 'Missing hall slug' }, 400);
|
||||||
|
|
||||||
@@ -118,9 +124,9 @@ export const handleHallImageUpload = async (req, res) => {
|
|||||||
// DELETE /api/v2/admin/halls/:slug/image — remove custom image
|
// DELETE /api/v2/admin/halls/:slug/image — remove custom image
|
||||||
export const handleHallImageDelete = async (req, res) => {
|
export const handleHallImageDelete = async (req, res) => {
|
||||||
const session = await lookupSession(req);
|
const session = await lookupSession(req);
|
||||||
if (!session || (!session.admin && !session.is_moderator)) {
|
if (!session || (!session.admin && !session.is_moderator)) return sendJson(res, { success: false, msg: 'Unauthorized' }, 403);
|
||||||
return sendJson(res, { success: false, msg: 'Unauthorized' }, 403);
|
const token = req.headers['x-csrf-token'] || req.url?.qs?.csrf_token;
|
||||||
}
|
if (!token || token !== session.csrf_token) return sendJson(res, { success: false, msg: 'Invalid CSRF token' }, 403);
|
||||||
|
|
||||||
const hallSlug = req.params && req.params.slug;
|
const hallSlug = req.params && req.params.slug;
|
||||||
if (!hallSlug) return sendJson(res, { success: false, msg: 'Missing hall slug' }, 400);
|
if (!hallSlug) return sendJson(res, { success: false, msg: 'Missing hall slug' }, 400);
|
||||||
@@ -156,9 +162,9 @@ export const handleHallImageDelete = async (req, res) => {
|
|||||||
// DELETE /api/v2/admin/halls/:slug — delete a hall entirely
|
// DELETE /api/v2/admin/halls/:slug — delete a hall entirely
|
||||||
export const handleHallDelete = async (req, res) => {
|
export const handleHallDelete = async (req, res) => {
|
||||||
const session = await lookupSession(req);
|
const session = await lookupSession(req);
|
||||||
if (!session || (!session.admin && !session.is_moderator)) {
|
if (!session || (!session.admin && !session.is_moderator)) return sendJson(res, { success: false, msg: 'Unauthorized' }, 403);
|
||||||
return sendJson(res, { success: false, msg: 'Unauthorized' }, 403);
|
const token = req.headers['x-csrf-token'] || req.url?.qs?.csrf_token;
|
||||||
}
|
if (!token || token !== session.csrf_token) return sendJson(res, { success: false, msg: 'Invalid CSRF token' }, 403);
|
||||||
|
|
||||||
const hallSlug = req.params && req.params.slug;
|
const hallSlug = req.params && req.params.slug;
|
||||||
if (!hallSlug) return sendJson(res, { success: false, msg: 'Missing hall slug' }, 400);
|
if (!hallSlug) return sendJson(res, { success: false, msg: 'Missing hall slug' }, 400);
|
||||||
@@ -176,9 +182,9 @@ export const handleHallDelete = async (req, res) => {
|
|||||||
// PATCH /api/v2/admin/halls/:slug — update name/description/slug
|
// PATCH /api/v2/admin/halls/:slug — update name/description/slug
|
||||||
export const handleHallUpdate = async (req, res) => {
|
export const handleHallUpdate = async (req, res) => {
|
||||||
const session = await lookupSession(req);
|
const session = await lookupSession(req);
|
||||||
if (!session || (!session.admin && !session.is_moderator)) {
|
if (!session || (!session.admin && !session.is_moderator)) return sendJson(res, { success: false, msg: 'Unauthorized' }, 403);
|
||||||
return sendJson(res, { success: false, msg: 'Unauthorized' }, 403);
|
const token = req.headers['x-csrf-token'] || req.url?.qs?.csrf_token;
|
||||||
}
|
if (!token || token !== session.csrf_token) return sendJson(res, { success: false, msg: 'Invalid CSRF token' }, 403);
|
||||||
|
|
||||||
const hallSlug = req.params && req.params.slug;
|
const hallSlug = req.params && req.params.slug;
|
||||||
if (!hallSlug) return sendJson(res, { success: false, msg: 'Missing slug' }, 400);
|
if (!hallSlug) return sendJson(res, { success: false, msg: 'Missing slug' }, 400);
|
||||||
@@ -263,9 +269,10 @@ export const handleHallUpdate = async (req, res) => {
|
|||||||
|
|
||||||
// POST /api/v2/admin/halls — create a new hall
|
// POST /api/v2/admin/halls — create a new hall
|
||||||
export const handleHallCreate = async (req, res) => {
|
export const handleHallCreate = async (req, res) => {
|
||||||
const session = await lookupSession(req);
|
// CSRF check
|
||||||
if (!session || (!session.admin && !session.is_moderator)) {
|
const token = req.headers['x-csrf-token'] || req.url?.qs?.csrf_token;
|
||||||
return sendJson(res, { success: false, msg: 'Unauthorized' }, 403);
|
if (!token || token !== session.csrf_token) {
|
||||||
|
return sendJson(res, { success: false, msg: 'Invalid CSRF token' }, 403);
|
||||||
}
|
}
|
||||||
|
|
||||||
let body = {};
|
let body = {};
|
||||||
|
|||||||
@@ -443,7 +443,7 @@ process.on('uncaughtException', err => {
|
|||||||
// Hall manager routes are handled by bypass middleware with their own session auth
|
// Hall manager routes are handled by bypass middleware with their own session auth
|
||||||
if (cfg.websrv.halls_enabled !== false && req.url.pathname.match(/^\/api\/v2\/admin\/halls(\/|$)/)) return;
|
if (cfg.websrv.halls_enabled !== false && req.url.pathname.match(/^\/api\/v2\/admin\/halls(\/|$)/)) return;
|
||||||
// User hall image upload is handled by bypass middleware below
|
// User hall image upload is handled by bypass middleware below
|
||||||
if (cfg.websrv.userhalls_enabled !== false && req.url.pathname.match(/^\/api\/v2\/me\/halls\/[^/]+\/image$/)) return;
|
if (cfg.websrv.userhalls_enabled !== false && cfg.websrv.enable_userhall_image_upload !== false && req.url.pathname.match(/^\/api\/v2\/me\/halls\/[^/]+\/image$/)) return;
|
||||||
if (!validateCsrf(req, res)) return;
|
if (!validateCsrf(req, res)) return;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -544,7 +544,7 @@ process.on('uncaughtException', err => {
|
|||||||
|
|
||||||
// Bypass middleware for user hall image uploads (multipart — raw body needed)
|
// Bypass middleware for user hall image uploads (multipart — raw body needed)
|
||||||
app.use(async (req, res) => {
|
app.use(async (req, res) => {
|
||||||
if (cfg.websrv.userhalls_enabled === false) return;
|
if (cfg.websrv.userhalls_enabled === false || cfg.websrv.enable_userhall_image_upload === false) return;
|
||||||
const userHallImgMatch = req.url.pathname.match(/^\/api\/v2\/me\/halls\/([^/]+)\/image$/);
|
const userHallImgMatch = req.url.pathname.match(/^\/api\/v2\/me\/halls\/([^/]+)\/image$/);
|
||||||
if (userHallImgMatch && req.method === 'POST') {
|
if (userHallImgMatch && req.method === 'POST') {
|
||||||
console.error('[BOOT] [USER_HALL BYPASS] Image upload:', req.url.pathname);
|
console.error('[BOOT] [USER_HALL BYPASS] Image upload:', req.url.pathname);
|
||||||
@@ -733,6 +733,7 @@ process.on('uncaughtException', err => {
|
|||||||
get halls() { return getHalls(); },
|
get halls() { return getHalls(); },
|
||||||
halls_enabled: cfg.websrv.halls_enabled !== false,
|
halls_enabled: cfg.websrv.halls_enabled !== false,
|
||||||
userhalls_enabled: cfg.websrv.userhalls_enabled !== false,
|
userhalls_enabled: cfg.websrv.userhalls_enabled !== false,
|
||||||
|
enable_userhall_image_upload: cfg.websrv.enable_userhall_image_upload !== false,
|
||||||
abyss_enabled: cfg.websrv.abyss_enabled !== false,
|
abyss_enabled: cfg.websrv.abyss_enabled !== false,
|
||||||
smtp_enabled: !!(cfg.smtp && cfg.smtp.enabled && cfg.smtp.mail_reset_password),
|
smtp_enabled: !!(cfg.smtp && cfg.smtp.enabled && cfg.smtp.mail_reset_password),
|
||||||
show_background_cfg: cfg.websrv.background !== false,
|
show_background_cfg: cfg.websrv.background !== false,
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ export const handleUserHallImageUpload = async (req, res, slug) => {
|
|||||||
if (!session) return sendJson(res, { success: false, msg: 'Unauthorized' }, 403);
|
if (!session) return sendJson(res, { success: false, msg: 'Unauthorized' }, 403);
|
||||||
|
|
||||||
// CSRF check
|
// CSRF check
|
||||||
const token = req.headers['x-csrf-token'];
|
const token = req.headers['x-csrf-token'] || req.url?.qs?.csrf_token;
|
||||||
if (!token || token !== session.csrf_token) {
|
if (!token || token !== session.csrf_token) {
|
||||||
return sendJson(res, { success: false, msg: 'Invalid CSRF token' }, 403);
|
return sendJson(res, { success: false, msg: 'Invalid CSRF token' }, 403);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -481,9 +481,16 @@
|
|||||||
hall_name_empty: "{{ t('hall.enter_name_error') }}",
|
hall_name_empty: "{{ t('hall.enter_name_error') }}",
|
||||||
hall_enter_name_error: "{{ t('hall.enter_name_error') }}",
|
hall_enter_name_error: "{{ t('hall.enter_name_error') }}",
|
||||||
hall_slug_empty_error: "{{ t('hall.slug_empty_error') }}",
|
hall_slug_empty_error: "{{ t('hall.slug_empty_error') }}",
|
||||||
|
hall_delete_confirm: "{{ t('hall.delete_confirm') }}",
|
||||||
hall_image_uploaded: "{{ t('hall.image_uploaded') }}",
|
hall_image_uploaded: "{{ t('hall.image_uploaded') }}",
|
||||||
hall_image_removed: "{{ t('hall.image_removed') }}",
|
hall_image_removed: "{{ t('hall.image_removed') }}",
|
||||||
hall_click_upload_hint: "{{ t('hall.click_upload_hint') }}",
|
hall_click_upload_hint: "{{ t('hall.click_upload_hint') }}",
|
||||||
|
common_save: "{{ t('common.save') }}",
|
||||||
|
common_delete: "{{ t('common.delete') }}",
|
||||||
|
common_view: "{{ t('common.view') }}",
|
||||||
|
common_name: "{{ t('common.name') }}",
|
||||||
|
common_description: "{{ t('common.description') }}",
|
||||||
|
common_private: "{{ t('common.private') }}",
|
||||||
// notifications
|
// notifications
|
||||||
notif_upload_approved: "{{ t('notifications.upload_approved_short') }}",
|
notif_upload_approved: "{{ t('notifications.upload_approved_short') }}",
|
||||||
notif_upload_pending: "{{ t('notifications.upload_pending_short') }}",
|
notif_upload_pending: "{{ t('notifications.upload_pending_short') }}",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
@each(hallsList as hall)
|
@each(hallsList as hall)
|
||||||
<div class="hall-manager-card" id="uh-card-{{ hall.slug }}" data-slug="{{ hall.slug }}" data-owner-id="{{ ownerUser.id }}">
|
<div class="hall-manager-card" id="uh-card-{{ hall.slug }}" data-slug="{{ hall.slug }}" data-owner-id="{{ ownerUser.id }}">
|
||||||
<div class="hall-manager-image" style="position:relative;height:140px;overflow:hidden;background:#111;cursor:pointer;" title="Click to upload a custom image">
|
<div class="hall-manager-image" style="position:relative;height:140px;overflow:hidden;background:#111;cursor:pointer;" title="{{ enable_userhall_image_upload ? 'Click to upload a custom image' : 'View hall' }}">
|
||||||
@if(hall.user_id)
|
@if(hall.user_id)
|
||||||
<img src="/user_hall_image/{{ hall.user_id }}/{{ hall.slug }}" alt="{!! hall.name !!}" style="width:100%;height:100%;object-fit:cover;opacity:0.8;">
|
<img src="/user_hall_image/{{ hall.user_id }}/{{ hall.slug }}" alt="{!! hall.name !!}" style="width:100%;height:100%;object-fit:cover;opacity:0.8;">
|
||||||
@else
|
@else
|
||||||
@@ -14,11 +14,13 @@
|
|||||||
@endif
|
@endif
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
@if(isOwner || (session && session.admin))
|
@if(enable_userhall_image_upload)
|
||||||
@if(hall.custom_image)
|
@if(isOwner || (session && session.admin))
|
||||||
<button class="uh-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>
|
@if(hall.custom_image)
|
||||||
|
<button class="uh-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="uh-img-upload" accept="image/*" style="display:none;">
|
||||||
@endif
|
@endif
|
||||||
<input type="file" class="uh-img-upload" accept="image/*" style="display:none;">
|
|
||||||
@endif
|
@endif
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -42,7 +44,7 @@
|
|||||||
<button class="uh-btn-save hm-btn" style="background:var(--accent);color:#000;border:none;font-weight:bold;">{{ t('common.save') }}</button>
|
<button class="uh-btn-save hm-btn" style="background:var(--accent);color:#000;border:none;font-weight:bold;">{{ t('common.save') }}</button>
|
||||||
<a href="/user/{{ ownerUser.user }}/hall/{{ hall.slug }}" class="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>
|
<a href="/user/{{ ownerUser.user }}/hall/{{ hall.slug }}" class="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;">
|
<span style="margin-left:4px;font-size:0.8em;color:#666;">
|
||||||
{{ t('hall.posts', { count: hall.total_items }) }}
|
{{ t('hall.posts').replace('{count}', hall.total_items) }}
|
||||||
</span>
|
</span>
|
||||||
<button class="uh-btn-delete 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>
|
<button class="uh-btn-delete 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>
|
||||||
@@ -58,7 +60,7 @@
|
|||||||
@endif
|
@endif
|
||||||
<a href="/user/{{ ownerUser.user }}/hall/{{ hall.slug }}" class="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>
|
<a href="/user/{{ ownerUser.user }}/hall/{{ hall.slug }}" class="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:6px;font-size:0.8em;color:#666;">
|
<span style="margin-left:6px;font-size:0.8em;color:#666;">
|
||||||
{{ t('hall.posts', { count: hall.total_items }) }}
|
{{ t('hall.posts').replace('{count}', hall.total_items) }}
|
||||||
</span>
|
</span>
|
||||||
@endif
|
@endif
|
||||||
<div class="uh-card-status" style="margin-top:6px;font-size:0.8em;color:#888;min-height:1.2em;"></div>
|
<div class="uh-card-status" style="margin-top:6px;font-size:0.8em;color:#888;min-height:1.2em;"></div>
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
</style>
|
</style>
|
||||||
<div class="container" style="padding-top: 20px;">
|
<div class="container" style="padding-top: 20px;">
|
||||||
<div style="display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:10px;margin-bottom:16px;">
|
<div style="display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:10px;margin-bottom:16px;">
|
||||||
<h3 style="margin:0;">{{ t('hall.user_halls_title', { user: ownerUser.user }) }}</h3>
|
<h3 style="margin:0;">{{ t('hall.user_halls_title').replace('{user}', ownerUser.user) }}</h3>
|
||||||
@if(isOwner || (session && session.admin))
|
@if(isOwner || (session && session.admin))
|
||||||
<div style="display:flex;gap:10px;align-items:center;flex-wrap:wrap;">
|
<div style="display:flex;gap:10px;align-items:center;flex-wrap:wrap;">
|
||||||
<input type="text" id="uh-new-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;">
|
<input type="text" id="uh-new-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;">
|
||||||
@@ -25,6 +25,13 @@
|
|||||||
</div>
|
</div>
|
||||||
@endif
|
@endif
|
||||||
</div>
|
</div>
|
||||||
|
<div id="uh-config" style="display:none;"
|
||||||
|
data-is-admin="{{ (session && session.admin) ? 'true' : 'false' }}"
|
||||||
|
data-owner-id="{{ ownerUser.id }}"
|
||||||
|
data-owner-user="{{ ownerUser.user }}"
|
||||||
|
data-is-owner="{{ isOwner ? 'true' : 'false' }}"
|
||||||
|
data-enable-upload="{{ enable_userhall_image_upload ? 'true' : 'false' }}"
|
||||||
|
data-i18n-hall-posts="{{ t('hall.posts') }}"></div>
|
||||||
|
|
||||||
<div id="hall-manager-grid" style="display: grid; grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); gap: 20px; margin-top: 20px;">
|
<div id="hall-manager-grid" style="display: grid; grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); gap: 20px; margin-top: 20px;">
|
||||||
@include(user-hall-cards)
|
@include(user-hall-cards)
|
||||||
@@ -50,7 +57,7 @@
|
|||||||
@if(isOwner || (session && session.admin))
|
@if(isOwner || (session && session.admin))
|
||||||
<script>
|
<script>
|
||||||
(function() {
|
(function() {
|
||||||
var csrf = (window.f0ckSession && window.f0ckSession.csrf_token) || '';
|
function getCsrf() { return (window.f0ckSession && window.f0ckSession.csrf_token) || ''; }
|
||||||
var i18n = window.f0ckI18n || {};
|
var i18n = window.f0ckI18n || {};
|
||||||
var grid = document.getElementById('hall-manager-grid');
|
var grid = document.getElementById('hall-manager-grid');
|
||||||
|
|
||||||
@@ -59,25 +66,30 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ── Create Hall ─────────────────────────────────────────────────────────────
|
// ── Create Hall ─────────────────────────────────────────────────────────────
|
||||||
var newNameInput = document.getElementById('uh-new-name');
|
// ── Config from data attributes ────────────────────────────────────────────
|
||||||
var createStatus = document.getElementById('uh-create-status');
|
var cfgEl = document.getElementById('uh-config');
|
||||||
|
var isAdmin = cfgEl.dataset.isAdmin === 'true';
|
||||||
var isAdmin = @if(session && session.admin) true @else false @endif;
|
var ownerUserId = cfgEl.dataset.ownerId;
|
||||||
var ownerUserId = '{{ ownerUser.id }}';
|
var ownerUser = cfgEl.dataset.ownerUser;
|
||||||
var isOwner = @if(isOwner) true @else false @endif;
|
var isOwner = cfgEl.dataset.isOwner === 'true';
|
||||||
|
var enableUpload = cfgEl.dataset.enableUpload === 'true';
|
||||||
|
var i18n_posts = cfgEl.dataset.i18nHallPosts;
|
||||||
|
|
||||||
// If admin is managing another user, append user_id query param
|
// If admin is managing another user, append user_id query param
|
||||||
var qs = (isAdmin && !isOwner) ? '?user_id=' + ownerUserId : '';
|
var qs = (isAdmin && !isOwner) ? '?user_id=' + ownerUserId : '';
|
||||||
|
|
||||||
|
var newNameInput = document.getElementById('uh-new-name');
|
||||||
|
var createStatus = document.getElementById('uh-create-status');
|
||||||
|
|
||||||
async function createHall() {
|
async function createHall() {
|
||||||
var name = newNameInput.value.trim();
|
var name = newNameInput.value.trim();
|
||||||
if (!name) { createStatus.textContent = "{{ t('hall.enter_name_error') }}"; createStatus.style.color = '#f55'; return; }
|
if (!name) { createStatus.textContent = i18n.hall_enter_name_error || "Name is required"; createStatus.style.color = '#f55'; return; }
|
||||||
var slug = toSlug(name);
|
var slug = toSlug(name);
|
||||||
createStatus.textContent = i18n.hall_creating || "{{ t('hall.creating') }}"; createStatus.style.color = '#888';
|
createStatus.textContent = i18n.hall_creating || "Creating…"; createStatus.style.color = '#888';
|
||||||
try {
|
try {
|
||||||
var r = await fetch('/api/v2/me/halls' + qs, {
|
var r = await fetch('/api/v2/me/halls' + qs, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json', 'x-csrf-token': csrf },
|
headers: { 'Content-Type': 'application/json', 'x-csrf-token': getCsrf() },
|
||||||
body: JSON.stringify({ name: name, slug: slug })
|
body: JSON.stringify({ name: name, slug: slug })
|
||||||
});
|
});
|
||||||
var d = await r.json();
|
var d = await r.json();
|
||||||
@@ -104,30 +116,33 @@
|
|||||||
div.className = 'hall-manager-card';
|
div.className = 'hall-manager-card';
|
||||||
div.id = 'uh-card-' + slug;
|
div.id = 'uh-card-' + slug;
|
||||||
div.dataset.slug = slug;
|
div.dataset.slug = slug;
|
||||||
var ownerUser = '{{ ownerUser.user }}';
|
|
||||||
var ownerUserId = '{{ ownerUser.id }}';
|
|
||||||
div.dataset.ownerId = ownerUserId;
|
div.dataset.ownerId = ownerUserId;
|
||||||
div.innerHTML =
|
var imgHtml =
|
||||||
'<div class="hall-manager-image" style="position:relative;height:140px;overflow:hidden;background:#111;cursor:pointer;" title="{{ t('hall.click_upload_hint') }}">' +
|
'<div class="hall-manager-image" style="position:relative;height:140px;overflow:hidden;background:#111;cursor:pointer;" title="' + (enableUpload ? (i18n.hall_click_upload_hint || 'Click to upload custom image') : (i18n.hall_view || 'View hall')) + '">' +
|
||||||
'<img src="/user_hall_image/' + ownerUserId + '/' + slug + '" alt="' + name + '" style="width:100%;height:100%;object-fit:cover;opacity:0.8;">' +
|
'<img src="/user_hall_image/' + ownerUserId + '/' + 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>' +
|
'<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="uh-img-upload" accept="image/*" style="display:none;">' +
|
|
||||||
'</div>' +
|
if (enableUpload) {
|
||||||
|
imgHtml += '<input type="file" class="uh-img-upload" accept="image/*" style="display:none;">';
|
||||||
|
}
|
||||||
|
imgHtml += '</div>';
|
||||||
|
|
||||||
|
div.innerHTML = imgHtml +
|
||||||
'<div style="padding:12px;">' +
|
'<div style="padding:12px;">' +
|
||||||
'<div style="margin-bottom:8px;"><label style="font-size:0.8em;color:#888;display:block;">{{ t('common.name') }}</label>' +
|
'<div style="margin-bottom:8px;"><label style="font-size:0.8em;color:#888;display:block;">' + (i18n.common_name || 'Name') + '</label>' +
|
||||||
'<input type="text" class="uh-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>' +
|
'<input type="text" class="uh-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;">{{ t('common.description') }}</label>' +
|
'<div style="margin-bottom:8px;"><label style="font-size:0.8em;color:#888;display:block;">' + (i18n.common_description || 'Description') + '</label>' +
|
||||||
'<textarea class="uh-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:50px;"></textarea></div>' +
|
'<textarea class="uh-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:50px;"></textarea></div>' +
|
||||||
'<div style="display:flex;gap:8px;flex-wrap:wrap;align-items:center;margin-bottom:4px;">' +
|
'<div style="display:flex;gap:8px;flex-wrap:wrap;align-items:center;margin-bottom:4px;">' +
|
||||||
'<label style="display:flex;align-items:center;gap:5px;cursor:pointer;color:#aaa;font-size:0.85em;"><input type="checkbox" class="uh-private-toggle"> {{ t('common.private') }}</label>' +
|
'<label style="display:flex;align-items:center;gap:5px;cursor:pointer;color:#aaa;font-size:0.85em;"><input type="checkbox" class="uh-private-toggle"> ' + (i18n.common_private || 'Private') + '</label>' +
|
||||||
'</div>' +
|
'</div>' +
|
||||||
'<div style="display:flex;gap:8px;flex-wrap:wrap;align-items:center;">' +
|
'<div style="display:flex;gap:8px;flex-wrap:wrap;align-items:center;">' +
|
||||||
'<button class="uh-btn-save hm-btn" style="background:var(--accent);color:#000;border:none;font-weight:bold;">{{ t('common.save') }}</button>' +
|
'<button class="uh-btn-save hm-btn" style="background:var(--accent);color:#000;border:none;font-weight:bold;">' + (i18n.common_save || 'Save') + '</button>' +
|
||||||
'<a href="/user/' + ownerUser + '/hall/' + slug + '" class="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>' +
|
'<a href="/user/' + ownerUser + '/hall/' + slug + '" class="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;">' +
|
'<span style="margin-left:4px;font-size:0.8em;color:#666;">' +
|
||||||
"{{ t('hall.posts') }}".replace('{count}', (hall && hall.total_items || 0)) +
|
i18n_posts.replace('{count}', (hall && hall.total_items || 0)) +
|
||||||
'</span>' +
|
'</span>' +
|
||||||
'<button class="uh-btn-delete 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>' +
|
'<button class="uh-btn-delete 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>' +
|
||||||
'<div class="uh-card-status" style="margin-top:6px;font-size:0.8em;color:#888;min-height:1.2em;"></div>' +
|
'<div class="uh-card-status" style="margin-top:6px;font-size:0.8em;color:#888;min-height:1.2em;"></div>' +
|
||||||
'</div>';
|
'</div>';
|
||||||
@@ -157,12 +172,20 @@
|
|||||||
nameInput.addEventListener('input', function() { nameOverlay.textContent = nameInput.value; });
|
nameInput.addEventListener('input', function() { nameOverlay.textContent = nameInput.value; });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Click image → open file picker
|
// Click image → open file picker or go to hall
|
||||||
if (imgContainer && fileInput) {
|
if (imgContainer) {
|
||||||
imgContainer.addEventListener('click', function(e) {
|
if (enableUpload) {
|
||||||
if (e.target.classList.contains('uh-btn-del-img')) return;
|
imgContainer.title = i18n.hall_click_upload_hint || 'Click to upload custom image';
|
||||||
fileInput.click();
|
imgContainer.addEventListener('click', function(e) {
|
||||||
});
|
if (e.target.classList.contains('uh-btn-del-img')) return;
|
||||||
|
if (fileInput) fileInput.click();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
imgContainer.title = i18n.hall_view || 'View hall';
|
||||||
|
imgContainer.addEventListener('click', function() {
|
||||||
|
window.location.href = '/user/' + ownerUser.toLowerCase() + '/hall/' + slug;
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Upload image
|
// Upload image
|
||||||
@@ -176,7 +199,7 @@
|
|||||||
try {
|
try {
|
||||||
var r = await fetch('/api/v2/me/halls/' + encodeURIComponent(slug) + '/image' + qs, {
|
var r = await fetch('/api/v2/me/halls/' + encodeURIComponent(slug) + '/image' + qs, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'x-csrf-token': csrf },
|
headers: { 'x-csrf-token': getCsrf() },
|
||||||
body: fd
|
body: fd
|
||||||
});
|
});
|
||||||
var d = await r.json();
|
var d = await r.json();
|
||||||
@@ -209,7 +232,7 @@
|
|||||||
try {
|
try {
|
||||||
var r = await fetch('/api/v2/me/halls/' + encodeURIComponent(slug) + '/image' + qs, {
|
var r = await fetch('/api/v2/me/halls/' + encodeURIComponent(slug) + '/image' + qs, {
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
headers: { 'x-csrf-token': csrf }
|
headers: { 'x-csrf-token': getCsrf() }
|
||||||
});
|
});
|
||||||
var d = await r.json();
|
var d = await r.json();
|
||||||
if (d.success) {
|
if (d.success) {
|
||||||
@@ -228,7 +251,7 @@
|
|||||||
privToggle.addEventListener('change', function() {
|
privToggle.addEventListener('change', function() {
|
||||||
fetch('/api/v2/me/halls/' + encodeURIComponent(slug) + qs, {
|
fetch('/api/v2/me/halls/' + encodeURIComponent(slug) + qs, {
|
||||||
method: 'PATCH',
|
method: 'PATCH',
|
||||||
headers: { 'Content-Type': 'application/json', 'x-csrf-token': csrf },
|
headers: { 'Content-Type': 'application/json', 'x-csrf-token': getCsrf() },
|
||||||
body: JSON.stringify({ is_private: privToggle.checked })
|
body: JSON.stringify({ is_private: privToggle.checked })
|
||||||
}).then(function(r) { return r.json(); }).then(function(d) {
|
}).then(function(r) { return r.json(); }).then(function(d) {
|
||||||
if (d.success) setStatus(privToggle.checked ? '🔒 Private' : '🌐 Public', 'var(--accent)');
|
if (d.success) setStatus(privToggle.checked ? '🔒 Private' : '🌐 Public', 'var(--accent)');
|
||||||
@@ -249,7 +272,7 @@
|
|||||||
try {
|
try {
|
||||||
var r = await fetch('/api/v2/me/halls/' + encodeURIComponent(slug) + qs, {
|
var r = await fetch('/api/v2/me/halls/' + encodeURIComponent(slug) + qs, {
|
||||||
method: 'PATCH',
|
method: 'PATCH',
|
||||||
headers: { 'Content-Type': 'application/json', 'x-csrf-token': csrf },
|
headers: { 'Content-Type': 'application/json', 'x-csrf-token': getCsrf() },
|
||||||
body: JSON.stringify({ name: newName, description: newDesc || null, is_private: isPrivate })
|
body: JSON.stringify({ name: newName, description: newDesc || null, is_private: isPrivate })
|
||||||
});
|
});
|
||||||
var d = await r.json();
|
var d = await r.json();
|
||||||
@@ -265,12 +288,12 @@
|
|||||||
var delBtn = card.querySelector('.uh-btn-delete');
|
var delBtn = card.querySelector('.uh-btn-delete');
|
||||||
if (delBtn) {
|
if (delBtn) {
|
||||||
delBtn.addEventListener('click', async function() {
|
delBtn.addEventListener('click', async function() {
|
||||||
if (!confirm("{{ t('hall.delete_confirm') }}")) return;
|
if (!confirm(i18n.hall_delete_confirm || 'Are you sure you want to delete this hall?')) return;
|
||||||
setStatus("{{ t('hall.deleting') }}");
|
setStatus(i18n.hall_deleting || 'Deleting…');
|
||||||
try {
|
try {
|
||||||
var r = await fetch('/api/v2/me/halls/' + encodeURIComponent(slug) + qs, {
|
var r = await fetch('/api/v2/me/halls/' + encodeURIComponent(slug) + qs, {
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
headers: { 'x-csrf-token': csrf }
|
headers: { 'x-csrf-token': getCsrf() }
|
||||||
});
|
});
|
||||||
var d = await r.json();
|
var d = await r.json();
|
||||||
if (d.success) {
|
if (d.success) {
|
||||||
|
|||||||
Reference in New Issue
Block a user