604 lines
37 KiB
HTML
604 lines
37 KiB
HTML
<div class="profile-page-wrapper" style="max-width: 800px; margin: 0 auto; width: 100%; padding: 20px 10px; box-sizing: border-box;">
|
|
<div class="profile_head" id="live-profile-preview-box" data-banner-user="{{ user.user || '' }}" style="position: relative; @if(user.banner_file && user_banner_enabled) --author-banner: url('/a/{{ user.banner_file }}'); --author-banner-position: {{ user.banner_position === 'center' ? 'center top' : (user.banner_position || 'center top') }}; --author-banner-size: {{ user.banner_size || 'cover' }}; @endif">
|
|
@if(session && session.id === user.user_id && user_banner_enabled)
|
|
<div id="inline-banner-edit-wrapper" style="display: none; position:absolute; top: 5px; right: 5px; z-index: 10;">
|
|
<button id="inline-banner-edit-btn" onclick="const menu = document.getElementById('banner-edit-menu'); menu.style.display = menu.style.display === 'none' ? 'block' : 'none';" style="background: rgba(0,0,0,0.6); border: 1px solid rgba(255,255,255,0.2); border-radius: 4px; padding: 4px 8px; color: #fff; cursor: pointer; font-size: 0.8em;" title="{{ t('profile.edit_banner', 'Edit Banner') }}">
|
|
<i class="fa-solid fa-panorama"></i>
|
|
</button>
|
|
<div id="banner-edit-menu" style="display: none; position: absolute; right: 0; top: 100%; margin-top: 5px; background: #222; border: 1px solid #444; border-radius: 4px; padding: 5px; min-width: 120px; box-shadow: 0 2px 10px rgba(0,0,0,0.5);">
|
|
@if(user.banner_file)
|
|
<button id="banner-edit-btn" onclick="document.getElementById('banner-edit-menu').style.display='none';" style="display: block; width: 100%; text-align: left; padding: 5px 8px; background: none; border: none; color: #fff; cursor: pointer; font-size: 0.8em;" onmouseover="this.style.background='#333'" onmouseout="this.style.background='none'">Edit Position</button>
|
|
<button id="banner-remove-btn" onclick="document.getElementById('banner-edit-menu').style.display='none';" style="display: block; width: 100%; text-align: left; padding: 5px 8px; background: none; border: none; color: #e74c3c; cursor: pointer; font-size: 0.8em;" onmouseover="this.style.background='#333'" onmouseout="this.style.background='none'">Remove Banner</button>
|
|
<hr id="banner-menu-hr" style="border: none; border-top: 1px solid #444; margin: 5px 0;">
|
|
@endif
|
|
<button onclick="document.getElementById('banner-choose-btn').click(); document.getElementById('banner-edit-menu').style.display='none';" style="display: block; width: 100%; text-align: left; padding: 5px 8px; background: none; border: none; color: #fff; cursor: pointer; font-size: 0.8em;" onmouseover="this.style.background='#333'" onmouseout="this.style.background='none'">Upload New</button>
|
|
</div>
|
|
</div>
|
|
<div style="display:none;">
|
|
<button id="banner-choose-btn"></button>
|
|
<input type="file" id="banner-file-input" accept="image/gif,image/jpeg,image/png,image/webp">
|
|
<span id="banner-filename"></span>
|
|
<div id="banner-upload-status"></div>
|
|
<div id="banner-progress-wrapper"><div id="banner-progress-fill"></div><span id="banner-progress-text"></span></div>
|
|
</div>
|
|
@endif
|
|
<div class="profile_head_avatar">
|
|
<div class="avatar-image-wrapper" style="position: relative; width: 55px; height: 55px; display: block;">
|
|
@if(user.is_ghost)
|
|
<div class="profile-avatar-img avatar-placeholder" style="background: #444; color: #888; display: flex; align-items: center; justify-content: center; font-size: 24px; font-weight: bold; border-radius: 4px;">?</div>
|
|
@elseif(user.avatar_file)
|
|
<img src="/a/{{ user.avatar_file }}" class="profile-avatar-img" />
|
|
@elseif(user.avatar && user.avatar > 0)
|
|
<img src="/t/{{ user.avatar }}.webp" class="profile-avatar-img" />
|
|
@else
|
|
<img src="/a/default.png" class="profile-avatar-img" />
|
|
@endif
|
|
@if(session && session.id === user.user_id)
|
|
<button id="inline-avatar-btn" class="avatar-edit-btn" title="{{ t('profile.edit_avatar', 'Edit Avatar') }}">
|
|
<i class="fa-solid fa-pen" style="font-size: 0.8em;"></i>
|
|
</button>
|
|
<input type="file" id="inline-avatar-file" accept="image/gif,image/jpeg,image/png,image/webp" style="display: none;">
|
|
@endif
|
|
</div>
|
|
@if(session && session.id === user.user_id)
|
|
<button id="edit-profile-btn" class="btn btn-sm btn-outline-primary" onclick="const box = document.getElementById('live-profile-preview-box'); box.classList.toggle('edit-mode-active'); if(box.classList.contains('edit-mode-active')){ this.innerHTML='<i class="fa-solid fa-check"></i>'; this.style.color='#51cf66'; this.style.borderColor='#51cf66'; } else { this.innerHTML='<i class="fa-solid fa-pen"></i>'; this.style.color='var(--accent)'; this.style.borderColor='var(--accent)'; }" style="position: absolute; bottom: 5px; left: 5px; padding: 3px 8px; font-size: 0.85em; border: 1px solid var(--accent); color: var(--accent); background: rgba(0,0,0,0.8); cursor: pointer; border-radius: 4px; z-index: 20; box-shadow: 0 0 5px rgba(0,0,0,0.5);" title="{{ t('profile.edit_btn', 'Edit Profile') }}"><i class="fa-solid fa-pen"></i></button>
|
|
@endif
|
|
</div>
|
|
<div class="layersoffear">
|
|
<div class="profile_head_username">
|
|
<div style="display: flex; justify-content: space-between; align-items: flex-start; gap: 10px;">
|
|
<div style="flex: 1; min-width: 0; word-break: break-word; line-height: 1.2; align-self: flex-start; margin-top: 0; display: flex; align-items: center; gap: 5px;" id="profile-name-container">
|
|
<span id="profile-display-name" @if(user.username_color) style="color: {{ user.username_color }}" @endif>@if(user.admin)⚡ @elseif(user.is_moderator)🛡 @endif{!! user.display_name || user.user !!}@if(user.is_ghost) <span class="badge badge-secondary" style="font-size: 0.5em; vertical-align: middle; background-color: #5bc0de; color: #fff; padding: 2px 5px; border-radius: 3px; margin-left: 5px;">LEGACY</span>@endif @if(user.banned) <span class="badge badge-danger" tooltip="{{ user.ban_duration }}" style="font-size: 0.5em; vertical-align: middle; background-color: #d9534f; color: #fff; padding: 2px 5px; border-radius: 3px; margin-left: 5px;">BANNED</span>@endif</span>@if(user.display_name) <span style="font-size: 0.65em; color: #666; font-weight: 400; margin-left: 5px; letter-spacing: 0.5px;" id="username-bracket">({!! user.user !!})</span>@endif
|
|
@if(session && session.id === user.user_id)
|
|
<button id="inline-name-edit-btn" style="background: transparent; border:none; color: var(--text-muted); cursor: pointer; font-size: 0.7em; padding: 0;" title="Edit Name & Color">
|
|
<i class="fa-solid fa-pen"></i>
|
|
</button>
|
|
@endif
|
|
</div>
|
|
@if(session && session.id === user.user_id)
|
|
<div id="profile-name-edit-container" style="display: none; align-items: center; gap: 5px; background: rgba(0,0,0,0.5); padding: 5px; border-radius: 4px; width: max-content;">
|
|
<input type="text" id="inline-display-name" value="@if(user.display_name){!! user.display_name !!}@endif" maxlength="32" style="background: #222; border: 1px solid #444; color: #fff; padding: 4px; border-radius: 3px; width: 120px; font-size: 0.9em;">
|
|
<input type="color" id="inline-username-color" value="{{ user.username_color || '#ffffff' }}" style="width: 25px; height: 25px; padding: 0; border: 1px solid #444; cursor: pointer; background: none;">
|
|
<button id="inline-name-save-btn" class="btn btn-sm btn-outline-success" style="padding: 2px 5px; font-size: 0.8em;"><i class="fa-solid fa-check"></i></button>
|
|
<button id="inline-name-cancel-btn" class="btn btn-sm btn-outline-danger" style="padding: 2px 5px; font-size: 0.8em;"><i class="fa-solid fa-xmark"></i></button>
|
|
</div>
|
|
@endif
|
|
@if(session)
|
|
<div class="profile-actions" style="margin: 0; flex-shrink: 0;">
|
|
@if(session.id !== user.user_id)
|
|
@if(private_messages)
|
|
<button id="send-dm-btn" class="btn btn-sm btn-outline-info" data-username="{!! user.user !!}" style="padding: 2px 8px; font-size: 0.8em; border: 1px solid var(--accent); color: var(--accent); background: transparent; cursor: pointer;" title="{{ t('profile.message_btn') }}"><i class="fa-solid fa-envelope"></i></button>
|
|
@endif
|
|
@if(session.admin || session.is_moderator)
|
|
@if(session.admin || !user.admin)
|
|
@if(user.banned)
|
|
<button id="unban-user-btn" class="btn btn-sm btn-outline-success" style="padding: 2px 8px; font-size: 0.8em; border: 1px solid #5cb85c; color: #5cb85c; background: transparent; cursor: pointer;" title="{{ t('profile.unban_btn') }}"><i class="fa-solid fa-unlock"></i></button>
|
|
@else
|
|
<button id="ban-user-btn" class="btn btn-sm btn-outline-danger" style="padding: 2px 8px; font-size: 0.8em; border: 1px solid var(--accent); color: var(--accent); background: transparent; cursor: pointer;" title="{{ t('profile.ban_btn') }}"><i class="fa-solid fa-ban"></i></button>
|
|
@endif
|
|
<button id="warn-user-btn" class="btn btn-sm btn-outline-warning" style="padding: 2px 8px; font-size: 0.8em; border: 1px solid #f0ad4e; color: #f0ad4e; background: transparent; cursor: pointer;" title="{{ t('profile.warn_btn') }}"><i class="fa-solid fa-triangle-exclamation"></i></button>
|
|
@endif
|
|
@endif
|
|
@endif
|
|
</div>
|
|
@endif
|
|
</div>
|
|
@if(enable_profile_description && (!user.is_ghost))
|
|
<div class="profile_description" style="position: relative;">
|
|
<div id="profile-desc-display" style="display: block; margin-bottom: 5px; margin-top: 5px;">
|
|
<span class="desc-text" style="word-break: break-word;">@if(user.description){!! user.description !!}@else<em style="color:var(--text-muted);">No description</em>@endif</span>
|
|
@if(session && session.id === user.user_id)
|
|
<button id="inline-desc-edit-btn" style="background: transparent; border:none; color: var(--text-muted); cursor: pointer; font-size: 0.7em; padding: 0; margin-left: 5px; display: inline-block; vertical-align: baseline;" title="Edit Description">
|
|
<i class="fa-solid fa-pen"></i>
|
|
</button>
|
|
@endif
|
|
</div>
|
|
</div>
|
|
@endif
|
|
</div>
|
|
<div class="profile_head_user_stats">
|
|
@if(user.is_ghost)
|
|
<div class="stat-legacy">{{ t('profile.legacy_record') }} <time class="timeago" tooltip="{{ user.timestamp.timefull }}">{{ user.timestamp.timeago }}</time></div>
|
|
@else
|
|
<div class="stat-id">ID: {{ user.user_id || user.id }}</div>
|
|
|
|
<div class="stat-joined" tooltip="{{ user.timestamp.timefull }}" data-iso="{{ user.timestamp.timefull }}">{{ t('profile.age_days', { n: user.age_days }) }}</div>
|
|
@if(!user.is_ghost)
|
|
<div class="stat-comments">{{ t('profile.stat_comments') }} <a href="/user/{!! user.user !!}/comments">{{ count.comments }}</a></div>
|
|
<div class="stat-tags">{{ t('profile.stat_tags') }} <a href="/user/{!! user.user !!}/tags">{{ count.tags }}</a></div>
|
|
@if(!user.is_ghost)
|
|
<div class="stat-halls">{{ t('profile.stat_halls') }} <a href="/user/{!! user.user !!}/halls">{{ count.halls }}</a></div>
|
|
@endif
|
|
@endif
|
|
|
|
@endif
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="user_content_wrapper">
|
|
<div class="user-uploads">
|
|
<div class="uploads-header">
|
|
{{ t('profile.uploads_label') }}: {{ count.f0cks }} <a href="{{ (f0cks.link && f0cks.link.main) ? f0cks.link.main : '#' }}">{{ t('profile.view_all') }}</a>
|
|
</div>
|
|
@if(count.f0cks)
|
|
<div class="posts no-infinite-scroll">
|
|
@each(f0cks.items as item)
|
|
<a href="{{ f0cks.link.main }}{{ item.id }}" class="{{ item.is_pinned ? 'anim-boxshadow ' : '' }}thumb lazy-thumb {{ item.is_pinned ? 'is-pinned' : '' }}" data-file="{{ item.dest }}" data-mime="{{ item.mime }}" data-user="{!! item.display_name || item.username !!}" data-ext="{{ item.mime.split('/')[1].replace('youtube', 'yt').replace('x-shockwave-flash', 'flash').replace('vnd.adobe.flash.movie', 'flash').toUpperCase() }}" data-mode="{{ item.tag_id == nsfl_tag_id ? 'nsfl' : (item.tag_id == 2 ? 'nsfw' : (item.tag_id == 1 ? 'sfw' : 'null')) }}" data-bg="/t/{{ item.id }}.webp">
|
|
<div class="thumb-indicators">
|
|
@if(item.is_pinned)
|
|
<i class="fa-solid fa-thumbtack pin-indicator anim"></i>
|
|
@endif
|
|
@if(item.is_oc)
|
|
<span class="oc-indicator anim">OC</span>
|
|
@endif
|
|
</div>
|
|
<p></p>
|
|
</a>
|
|
@endeach
|
|
</div>
|
|
@else
|
|
{{ t('profile.no_uploads') }}
|
|
@endif
|
|
</div>
|
|
@if(!user.is_ghost)
|
|
<div class="favs">
|
|
<div class="favs-header">
|
|
{{ t('profile.favs_label') }}: {{ count.favs }} <a href="@if(favs.link && favs.link.main){{ favs.link.main }}@else#@endif">{{ t('profile.view_all') }}</a>
|
|
</div>
|
|
@if(count.favs)
|
|
<div class="posts no-infinite-scroll">
|
|
@each(favs.items as item)
|
|
<a href="{{ favs.link.main }}{{ item.id }}" class="{{ item.is_pinned ? 'anim-boxshadow ' : '' }}thumb lazy-thumb {{ item.is_pinned ? 'is-pinned' : '' }}" data-file="{{ item.dest }}" data-mime="{{ item.mime }}" data-user="{!! item.display_name || item.username !!}" data-ext="{{ item.mime.split('/')[1].replace('youtube', 'yt').replace('x-shockwave-flash', 'flash').replace('vnd.adobe.flash.movie', 'flash').toUpperCase() }}" data-mode="{{ item.tag_id == nsfl_tag_id ? 'nsfl' : (item.tag_id == 2 ? 'nsfw' : (item.tag_id == 1 ? 'sfw' : 'null')) }}" data-bg="/t/{{ item.id }}.webp">
|
|
<div class="thumb-indicators">
|
|
@if(item.is_pinned)
|
|
<i class="fa-solid fa-thumbtack pin-indicator anim"></i>
|
|
@endif
|
|
@if(item.is_oc)
|
|
<span class="oc-indicator anim">OC</span>
|
|
@endif
|
|
</div>
|
|
<p></p>
|
|
</a>
|
|
@endeach
|
|
</div>
|
|
@else
|
|
{{ t('profile.no_favs') }}
|
|
@endif
|
|
</div>
|
|
@endif
|
|
</div>
|
|
|
|
@if(session)
|
|
@if(session.id !== user.user_id)
|
|
@if(!user.is_ghost)
|
|
@if(session.admin || session.is_moderator)
|
|
<!-- Ban Modal -->
|
|
<div id="ban-modal" class="modal" style="display: none; position: fixed; z-index: 10001; left: 0; top: 0; width: 100%; height: 100%; overflow: auto; background-color: rgba(0,0,0,0.8);">
|
|
<div class="modal-content" style="background-color: var(--bg); margin: 15% auto; padding: 20px; border: 1px solid var(--accent); width: 400px; border-radius: 8px;">
|
|
<h2 style="color: var(--accent); margin-top: 0;">{{ t('profile.ban_modal_title') }}</h2>
|
|
<form id="ban-form">
|
|
<div style="margin-bottom: 15px;">
|
|
<label style="display: block; margin-bottom: 5px;">{{ t('profile.ban_modal_reason') }}</label>
|
|
<input type="text" id="ban-reason" style="width: 100%; padding: 8px; background: #222; border: 1px solid #444; color: #fff;" required>
|
|
</div>
|
|
<div style="margin-bottom: 20px;">
|
|
<label style="display: block; margin-bottom: 5px;">{{ t('profile.ban_modal_duration') }}</label>
|
|
<select id="ban-duration" style="width: 100%; padding: 8px; background: #222; border: 1px solid #444; color: #fff;">
|
|
<option value="1">{{ t('profile.ban_1h') }}</option>
|
|
<option value="24">{{ t('profile.ban_1d') }}</option>
|
|
<option value="168">{{ t('profile.ban_1w') }}</option>
|
|
<option value="720">{{ t('profile.ban_1m') }}</option>
|
|
<option value="permanent">{{ t('profile.ban_permanent') }}</option>
|
|
</select>
|
|
</div>
|
|
<div style="display: flex; justify-content: flex-end; gap: 10px;">
|
|
<button type="button" id="ban-modal-close" style="padding: 8px 15px; background: #444; border: none; color: #fff; cursor: pointer;">{{ t('profile.ban_modal_cancel') }}</button>
|
|
<button type="submit" style="padding: 8px 15px; background: var(--accent); border: none; color: var(--bg); font-weight: bold; cursor: pointer;">{{ t('profile.ban_modal_confirm') }}</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Warn Modal -->
|
|
<div id="warn-modal" class="modal" style="display: none; position: fixed; z-index: 10001; left: 0; top: 0; width: 100%; height: 100%; overflow: auto; background-color: rgba(0,0,0,0.8);">
|
|
<div class="modal-content" style="background-color: var(--bg); margin: 15% auto; padding: 20px; border: 1px solid #f0ad4e; width: 400px; border-radius: 8px;">
|
|
<h2 style="color: #f0ad4e; margin-top: 0;">{{ t('profile.warn_modal_title') }}</h2>
|
|
<form id="warn-form">
|
|
<div style="margin-bottom: 15px;">
|
|
<label style="display: block; margin-bottom: 5px;">{{ t('profile.warn_modal_reason') }}</label>
|
|
<textarea id="warn-reason" style="width: 100%; padding: 8px; background: #222; border: 1px solid #444; color: #fff; height: 100px;" required></textarea>
|
|
<p style="font-size: 0.8em; color: #aaa; margin-top: 5px;">{{ t('profile.warn_modal_hint') }}</p>
|
|
</div>
|
|
<div style="display: flex; justify-content: flex-end; gap: 10px;">
|
|
<button type="button" id="warn-modal-close" style="padding: 8px 15px; background: #444; border: none; color: #fff; cursor: pointer;">{{ t('profile.warn_modal_cancel') }}</button>
|
|
<button type="submit" style="padding: 8px 15px; background: #f0ad4e; border: none; color: #000; font-weight: bold; cursor: pointer;">{{ t('profile.warn_modal_submit') }}</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
(function () {
|
|
const banBtn = document.getElementById('ban-user-btn');
|
|
const unbanBtn = document.getElementById('unban-user-btn');
|
|
const warnBtn = document.getElementById('warn-user-btn');
|
|
const banModal = document.getElementById('ban-modal');
|
|
const warnModal = document.getElementById('warn-modal');
|
|
const banClose = document.getElementById('ban-modal-close');
|
|
const warnClose = document.getElementById('warn-modal-close');
|
|
const banForm = document.getElementById('ban-form');
|
|
const warnForm = document.getElementById('warn-form');
|
|
const userId = '{{ user.user_id || user.id }}';
|
|
|
|
if (banBtn) {
|
|
banBtn.onclick = () => {
|
|
// Filter duration options for moderators
|
|
const isAdmin = {{ session.admin ? 'true' : 'false' }};
|
|
const durationSelect = document.getElementById('ban-duration');
|
|
if (durationSelect && !isAdmin) {
|
|
Array.from(durationSelect.options).forEach(opt => {
|
|
const val = opt.value;
|
|
if (val === 'permanent' || parseInt(val) > 48) {
|
|
opt.style.display = 'none';
|
|
opt.disabled = true;
|
|
}
|
|
});
|
|
// Ensure a valid option is selected
|
|
if (durationSelect.value === 'permanent' || parseInt(durationSelect.value) > 48) {
|
|
durationSelect.value = "1";
|
|
}
|
|
}
|
|
banModal.style.display = 'block';
|
|
};
|
|
}
|
|
if (warnBtn) {
|
|
warnBtn.onclick = () => warnModal.style.display = 'block';
|
|
}
|
|
|
|
if (unbanBtn) {
|
|
unbanBtn.onclick = async () => {
|
|
if (!confirm('{{ t('profile.confirm_unban') }}')) return;
|
|
try {
|
|
const res = await fetch('/api/v2/admin/unban', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'X-CSRF-Token': window.f0ckSession?.csrf_token
|
|
},
|
|
body: JSON.stringify({ user_id: userId })
|
|
});
|
|
const data = await res.json();
|
|
if (data.success) {
|
|
if (window.showFlash) window.showFlash('{{ t('profile.unban_success') }}', 'success');
|
|
else alert('{{ t('profile.unban_success') }}');
|
|
location.reload();
|
|
} else {
|
|
alert('Error: ' + data.msg);
|
|
}
|
|
} catch (err) {
|
|
alert('Failed to unban user: ' + err.message);
|
|
}
|
|
};
|
|
}
|
|
|
|
if (banClose) {
|
|
banClose.onclick = () => banModal.style.display = 'none';
|
|
}
|
|
if (warnClose) {
|
|
warnClose.onclick = () => warnModal.style.display = 'none';
|
|
}
|
|
|
|
window.onclick = (event) => {
|
|
if (event.target == banModal) {
|
|
banModal.style.display = 'none';
|
|
}
|
|
if (event.target == warnModal) {
|
|
warnModal.style.display = 'none';
|
|
}
|
|
};
|
|
|
|
if (banForm) {
|
|
banForm.onsubmit = async (e) => {
|
|
e.preventDefault();
|
|
const reason = document.getElementById('ban-reason').value;
|
|
const duration = document.getElementById('ban-duration').value;
|
|
|
|
try {
|
|
const res = await fetch('/api/v2/admin/ban', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'X-CSRF-Token': window.f0ckSession?.csrf_token
|
|
},
|
|
body: JSON.stringify({ user_id: userId, reason, duration })
|
|
});
|
|
const data = await res.json();
|
|
if (data.success) {
|
|
if (window.showFlash) window.showFlash('{{ t('profile.ban_success') }}', 'success');
|
|
else alert('{{ t('profile.ban_success') }}');
|
|
location.reload();
|
|
} else {
|
|
alert('Error: ' + data.msg);
|
|
}
|
|
} catch (err) {
|
|
alert('Failed to ban user: ' + err.message);
|
|
}
|
|
};
|
|
}
|
|
|
|
if (warnForm) {
|
|
warnForm.onsubmit = async (e) => {
|
|
e.preventDefault();
|
|
const reason = document.getElementById('warn-reason').value;
|
|
const submitBtn = warnForm.querySelector('button[type="submit"]');
|
|
|
|
try {
|
|
submitBtn.disabled = true;
|
|
submitBtn.innerText = '{{ t('profile.warning_issuing') }}';
|
|
|
|
const payload = new URLSearchParams();
|
|
payload.append('user_id', userId);
|
|
payload.append('reason', reason);
|
|
|
|
const res = await fetch('/api/v2/mod/warnings/issue', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/x-www-form-urlencoded',
|
|
'X-CSRF-Token': window.f0ckSession?.csrf_token
|
|
},
|
|
body: payload
|
|
});
|
|
const data = await res.json();
|
|
if (data.success) {
|
|
document.getElementById('warn-reason').value = '';
|
|
warnModal.style.display = 'none';
|
|
if (window.showFlash) window.showFlash('{{ t('profile.warning_success') }}', 'success');
|
|
} else {
|
|
alert('Error: ' + data.msg);
|
|
}
|
|
} catch (err) {
|
|
alert('Failed to issue warning: ' + err.message);
|
|
} finally {
|
|
submitBtn.disabled = false;
|
|
submitBtn.innerText = '{{ t('profile.warning_issue_btn') }}';
|
|
}
|
|
};
|
|
}
|
|
})();
|
|
</script>
|
|
@endif
|
|
@endif
|
|
@endif
|
|
|
|
@if(session && session.id === user.user_id)
|
|
<div id="profile-desc-edit-container" class="modal-overlay" style="display: none;">
|
|
<div class="modal-content" style="max-width: 600px; width: 90%; text-align: left;">
|
|
<h3 style="margin-top: 0; margin-bottom: 15px;">Edit Description</h3>
|
|
<textarea id="inline-desc-input" style="width: 100%; background: #222; border: 1px solid #444; color: #fff; padding: 10px; border-radius: 4px; box-sizing: border-box; min-height: 120px; resize: vertical; font-family: inherit;">@if(user.description){!! user.description !!}@endif</textarea>
|
|
<div class="modal-actions" style="display: flex; justify-content: flex-end; gap: 10px; margin-top: 15px;">
|
|
<button id="inline-desc-cancel-btn" class="btn btn-secondary">Cancel</button>
|
|
<button id="inline-desc-save-btn" class="btn btn-primary">Save</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
@endif
|
|
|
|
@if(session && session.id === user.user_id)
|
|
<style>
|
|
#live-profile-preview-box:not(.edit-mode-active) .avatar-edit-btn,
|
|
#live-profile-preview-box:not(.edit-mode-active) #inline-banner-edit-wrapper,
|
|
#live-profile-preview-box:not(.edit-mode-active) #inline-name-edit-btn,
|
|
#live-profile-preview-box:not(.edit-mode-active) #inline-desc-edit-btn {
|
|
display: none !important;
|
|
}
|
|
#live-profile-preview-box.edit-mode-active .avatar-edit-btn {
|
|
opacity: 0;
|
|
}
|
|
#live-profile-preview-box.edit-mode-active .avatar-image-wrapper:hover .avatar-edit-btn {
|
|
opacity: 1;
|
|
}
|
|
</style>
|
|
<script src="/s/js/settings.js"></script>
|
|
<script>
|
|
(function() {
|
|
|
|
|
|
const avatarEditBtn = document.getElementById('inline-avatar-btn');
|
|
const avatarFileInput = document.getElementById('inline-avatar-file');
|
|
if (avatarEditBtn && avatarFileInput) {
|
|
avatarEditBtn.addEventListener('click', () => {
|
|
avatarFileInput.click();
|
|
});
|
|
avatarFileInput.addEventListener('change', async (e) => {
|
|
const file = e.target.files[0];
|
|
if (!file) return;
|
|
|
|
avatarEditBtn.innerHTML = '<i class="fa-solid fa-spinner fa-spin" style="font-size: 0.7em;"></i>';
|
|
avatarEditBtn.disabled = true;
|
|
|
|
try {
|
|
const formData = new FormData();
|
|
formData.append('file', file);
|
|
const res = await fetch('/api/v2/settings/uploadAvatar', {
|
|
method: 'POST',
|
|
headers: { 'X-CSRF-Token': window.f0ckSession?.csrf_token },
|
|
body: formData
|
|
});
|
|
const data = await res.json();
|
|
if (data.success) {
|
|
let objectUrl = '/a/' + (data.avatar_file || data.file) + '?t=' + Date.now();
|
|
let wrapper = document.querySelector('.avatar-image-wrapper');
|
|
if (wrapper) {
|
|
let placeholder = wrapper.querySelector('.avatar-placeholder');
|
|
if (placeholder) {
|
|
let img = document.createElement('img');
|
|
img.className = 'profile-avatar-img';
|
|
img.src = objectUrl;
|
|
wrapper.insertBefore(img, placeholder);
|
|
placeholder.remove();
|
|
} else {
|
|
let img = wrapper.querySelector('.profile-avatar-img');
|
|
if (img) img.src = objectUrl;
|
|
}
|
|
}
|
|
|
|
document.querySelectorAll('.nav-avatar-img').forEach(img => img.src = objectUrl);
|
|
document.querySelectorAll('.comment-avatar img, .sidebar-avatar, .poll-voter-avatar, .rank-avatar').forEach(img => {
|
|
// Only replace if it matches the user's old avatar file, or if they didn't have one, just match if it links to their profile
|
|
const oldFile = '{{ user.avatar_file }}';
|
|
const oldId = '{{ user.avatar }}';
|
|
const isOldImage = (oldFile && img.src.includes('/a/' + oldFile)) || (oldId && img.src.includes('/t/' + oldId + '.webp'));
|
|
const link = img.closest('a');
|
|
const isUserLink = link && link.href.includes('/user/{{ user.user }}');
|
|
|
|
if (isOldImage || isUserLink) {
|
|
img.src = objectUrl;
|
|
}
|
|
});
|
|
avatarEditBtn.innerHTML = '<i class="fa-solid fa-pen" style="font-size: 0.7em;"></i>';
|
|
avatarEditBtn.disabled = false;
|
|
} else {
|
|
alert(data.msg || 'Failed to upload avatar');
|
|
avatarEditBtn.innerHTML = '<i class="fa-solid fa-pen" style="font-size: 0.7em;"></i>';
|
|
avatarEditBtn.disabled = false;
|
|
}
|
|
} catch (err) {
|
|
alert('Network error');
|
|
avatarEditBtn.innerHTML = '<i class="fa-solid fa-pen" style="font-size: 0.7em;"></i>';
|
|
avatarEditBtn.disabled = false;
|
|
}
|
|
});
|
|
}
|
|
|
|
const nameEditBtn = document.getElementById('inline-name-edit-btn');
|
|
const nameContainer = document.getElementById('profile-name-container');
|
|
const nameEditContainer = document.getElementById('profile-name-edit-container');
|
|
const nameCancelBtn = document.getElementById('inline-name-cancel-btn');
|
|
const nameSaveBtn = document.getElementById('inline-name-save-btn');
|
|
const inlineDisplayName = document.getElementById('inline-display-name');
|
|
const inlineUsernameColor = document.getElementById('inline-username-color');
|
|
|
|
if (nameEditBtn && nameEditContainer) {
|
|
nameEditBtn.addEventListener('click', () => {
|
|
nameContainer.style.display = 'none';
|
|
nameEditContainer.style.display = 'flex';
|
|
});
|
|
nameCancelBtn.addEventListener('click', () => {
|
|
nameContainer.style.display = 'flex';
|
|
nameEditContainer.style.display = 'none';
|
|
});
|
|
nameSaveBtn.addEventListener('click', async () => {
|
|
nameSaveBtn.disabled = true;
|
|
const displayName = inlineDisplayName.value;
|
|
const color = inlineUsernameColor.value;
|
|
|
|
try {
|
|
let dPayload = {}; dPayload["display_name"] = displayName;
|
|
await fetch('/api/v2/settings/display_name', {
|
|
method: 'PUT',
|
|
headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': window.f0ckSession?.csrf_token },
|
|
body: JSON.stringify(dPayload)
|
|
});
|
|
let cPayload = {}; cPayload["color"] = color;
|
|
await fetch('/api/v2/settings/username_color', {
|
|
method: 'PUT',
|
|
headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': window.f0ckSession?.csrf_token },
|
|
body: JSON.stringify(cPayload)
|
|
});
|
|
nameSaveBtn.disabled = false;
|
|
nameContainer.style.display = 'flex';
|
|
nameEditContainer.style.display = 'none';
|
|
|
|
const updateNameNode = (el, text) => {
|
|
if (!el) return;
|
|
for (let i = el.childNodes.length - 1; i >= 0; i--) {
|
|
let node = el.childNodes[i];
|
|
if (node.nodeType === Node.TEXT_NODE && node.nodeValue.trim().length > 0) {
|
|
const match = node.nodeValue.match(new RegExp('^(\\s*)(.*)'));
|
|
node.nodeValue = (match ? match[1] : '') + text;
|
|
break;
|
|
}
|
|
}
|
|
};
|
|
const targetName = displayName ? displayName : '{{ user.user }}';
|
|
const profileSpan = document.getElementById('profile-display-name');
|
|
if (profileSpan) {
|
|
profileSpan.style.color = color;
|
|
updateNameNode(profileSpan, targetName);
|
|
}
|
|
const navSpan = document.getElementById('nav-display-name');
|
|
if (navSpan) {
|
|
navSpan.style.color = color;
|
|
updateNameNode(navSpan, targetName);
|
|
}
|
|
const bracket = document.getElementById('username-bracket');
|
|
if (bracket) {
|
|
bracket.style.display = displayName ? 'inline' : 'none';
|
|
}
|
|
} catch (err) {
|
|
alert('Error saving name/color');
|
|
nameSaveBtn.disabled = false;
|
|
}
|
|
});
|
|
}
|
|
|
|
const descEditBtn = document.getElementById('inline-desc-edit-btn');
|
|
const descDisplay = document.getElementById('profile-desc-display');
|
|
const descEditContainer = document.getElementById('profile-desc-edit-container');
|
|
const descCancelBtn = document.getElementById('inline-desc-cancel-btn');
|
|
const descSaveBtn = document.getElementById('inline-desc-save-btn');
|
|
const inlineDescInput = document.getElementById('inline-desc-input');
|
|
|
|
if (descEditBtn && descEditContainer) {
|
|
descEditBtn.addEventListener('click', () => {
|
|
descEditContainer.style.display = 'flex';
|
|
document.body.classList.add('modal-open');
|
|
});
|
|
descCancelBtn.addEventListener('click', () => {
|
|
descEditContainer.style.display = 'none';
|
|
document.body.classList.remove('modal-open');
|
|
});
|
|
descSaveBtn.addEventListener('click', async () => {
|
|
descSaveBtn.disabled = true;
|
|
const description = inlineDescInput.value;
|
|
try {
|
|
let p = {}; p["description"] = description;
|
|
const res = await fetch('/api/v2/settings/description', {
|
|
method: 'PUT',
|
|
headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': window.f0ckSession?.csrf_token },
|
|
body: JSON.stringify(p)
|
|
});
|
|
const data = await res.json();
|
|
if (data.success) {
|
|
descSaveBtn.disabled = false;
|
|
descEditContainer.style.display = 'none';
|
|
document.body.classList.remove('modal-open');
|
|
const descTextDiv = descDisplay.querySelector('.desc-text');
|
|
if (description.trim() === '') {
|
|
descTextDiv.innerHTML = '<em style="color:var(--text-muted);">No description</em>';
|
|
} else {
|
|
const tempDiv = document.createElement('div');
|
|
tempDiv.textContent = description;
|
|
descTextDiv.innerHTML = tempDiv.innerHTML.replace(new RegExp('\\\\n', 'g'), '<br>');
|
|
}
|
|
} else {
|
|
alert(data.msg || 'Failed to update description');
|
|
descSaveBtn.disabled = false;
|
|
}
|
|
} catch (err) {
|
|
alert('Error saving description');
|
|
descSaveBtn.disabled = false;
|
|
}
|
|
});
|
|
}
|
|
})();
|
|
</script>
|
|
@endif
|
|
@endif
|
|
</div>
|
|
|
|
<div class="pagination-container-fluid" @if(typeof hidePagination !=='undefined' && hidePagination) style="display: none;" @endif>
|
|
<div class="pagination-wrapper bottom-pagination fixed-pagination">
|
|
@include(snippets/pagination)
|
|
</div>
|
|
</div> |