PHP Classes

File: templates/user_profile.twig

Recommend this page to a friend!
  Packages of Matthew Asham   Binkterm PHP   templates/user_profile.twig   Download  
File: templates/user_profile.twig
Role: Auxiliary data
Content type: text/plain
Description: Auxiliary data
Class: Binkterm PHP
Bulletin board system based on the Web
Author: By
Last change:
Date: 5 days ago
Size: 27,335 bytes
 

Contents

Class file image Download
{% extends "base.twig" %} {% block title %}{{ t('ui.user_profile.title_with_username', {'username': profile_username}, 'common') }} - {{ parent() }}{% endblock %} {% block content %} <div class="d-flex justify-content-between align-items-center mb-4"> <h2> <i class="fas fa-user"></i> {{ t('ui.user_profile.title_with_username', {'username': profile_username}, 'common') }} </h2> {% if is_own_profile %} <a href="/profile" class="btn btn-sm btn-fidonet"> <i class="fas fa-edit"></i> {{ t('ui.user_profile.edit_my_profile', {}, 'common') }} </a> {% endif %} </div> <div class="row"> <div class="col-lg-8"> <div class="card"> <div class="card-header"> <h5 class="mb-0">{{ t('ui.user_profile.user_information', {}, 'common') }}</h5> </div> <div class="card-body"> <dl class="row"> <dt class="col-sm-3">{{ t('ui.user_profile.username', {}, 'common') }}:</dt> <dd class="col-sm-9">{{ profile_username }}</dd> {% if can_view_sensitive %} <dt class="col-sm-3">{{ t('ui.user_profile.real_name', {}, 'common') }}:</dt> <dd class="col-sm-9">{{ profile_real_name|default(t('ui.user_profile.not_specified', {}, 'common')) }}</dd> {% endif %} {% if profile_location %} <dt class="col-sm-3">{{ t('ui.user_profile.location', {}, 'common') }}:</dt> <dd class="col-sm-9"> <i class="fas fa-map-marker-alt me-1"></i> {{ profile_location }} </dd> {% endif %} {% if profile_fidonet_address and can_view_sensitive %} <dt class="col-sm-3">{{ t('ui.user_profile.fidonet_address', {}, 'common') }}:</dt> <dd class="col-sm-9"> <code>{{ profile_fidonet_address }}</code> </dd> {% endif %} <dt class="col-sm-3">{{ t('ui.user_profile.member_since', {}, 'common') }}:</dt> <dd class="col-sm-9">{{ profile_created_at|date('F j, Y') }}</dd> {% if profile_last_login and can_view_sensitive %} <dt class="col-sm-3">{{ t('ui.user_profile.last_seen', {}, 'common') }}:</dt> <dd class="col-sm-9">{{ profile_last_login|date('F j, Y g:i A') }}</dd> {% endif %} <dt class="col-sm-3">{{ t('ui.user_profile.role', {}, 'common') }}:</dt> <dd class="col-sm-9"> {% if profile_is_admin %} <span class="badge bg-danger">{{ t('ui.user_profile.role_administrator', {}, 'common') }}</span> {% else %} <span class="badge bg-primary">{{ t('ui.user_profile.role_user', {}, 'common') }}</span> {% endif %} </dd> </dl> </div> </div> <div class="card mt-3"> <div class="card-header"> <h5 class="mb-0">{{ t('ui.user_profile.about_me', {}, 'common') }}</h5> </div> <div class="card-body"> {% if profile_about_me_html %} <div class="about-me-rendered">{{ profile_about_me_html|raw }}</div> {% else %} <p class="text-muted mb-0"><em>{{ t('ui.user_profile.about_me_empty', {}, 'common') }}</em></p> {% endif %} </div> </div> </div> <div class="col-lg-4"> {% if credits_enabled and can_view_sensitive %} <div class="card"> <div class="card-header"> <h6 class="mb-0">{{ t('ui.user_profile.credits', {}, 'common') }}</h6> </div> <div class="card-body"> <div class="text-center"> <h3 class="mb-0">{{ credits_symbol }}{{ credit_balance }}</h3> <small class="text-muted">{{ t('ui.user_profile.current_balance', {}, 'common') }}</small> </div> </div> </div> {% endif %} {% if credits_enabled and not is_own_profile %} <div class="card mt-3"> <div class="card-header"> <h6 class="mb-0">{{ t('ui.user_profile.send_credits', {}, 'common') }}</h6> </div> <div class="card-body"> <div id="sendCreditsAlert"></div> <form id="sendCreditsForm"> <div class="mb-3"> <label for="creditAmount" class="form-label">{{ t('ui.user_profile.amount', {}, 'common') }}</label> <input type="number" class="form-control" id="creditAmount" name="amount" min="1" max="200" required> <small class="text-muted">{{ t('ui.user_profile.send_credits_limit_fee', {'fee_percent': (transfer_fee_percent * 100)|round(1)}, 'common') }}</small> </div> <div class="mb-3"> <label for="creditMessage" class="form-label">{{ t('ui.user_profile.message_optional', {}, 'common') }}</label> <input type="text" class="form-control" id="creditMessage" name="message" maxlength="100" placeholder="{{ t('ui.user_profile.message_optional_placeholder', {}, 'common') }}"> </div> <button type="submit" class="btn btn-success w-100"> <i class="fas fa-paper-plane me-2"></i>{{ t('ui.user_profile.send_credits', {}, 'common') }} </button> </form> <div class="mt-2"> <small class="text-muted"> <i class="fas fa-info-circle me-1"></i> {{ t('ui.user_profile.send_credits_fee_note', {'fee_percent': (transfer_fee_percent * 100)|round(1)}, 'common') }} </small> </div> </div> </div> {% endif %} {% if can_view_sensitive %} <div class="card {% if credits_enabled %}mt-3{% endif %}"> <div class="card-header"> <h6 class="mb-0">{{ t('ui.user_profile.activity_summary', {}, 'common') }}</h6> </div> <div class="card-body"> <dl class="row mb-0"> <dt class="col-8">{{ t('ui.user_profile.netmail_sent', {}, 'common') }}:</dt> <dd class="col-4"><span id="netmailCount">-</span></dd> <dt class="col-8">{{ t('ui.user_profile.echomail_posted', {}, 'common') }}:</dt> <dd class="col-4"><span id="echomailCount">-</span></dd> <dt class="col-8">{{ t('ui.user_profile.total_messages', {}, 'common') }}:</dt> <dd class="col-4"><strong><span id="totalCount">-</span></strong></dd> </dl> </div> </div> {% else %} <div class="card"> <div class="card-body text-center text-muted"> <i class="fas fa-lock fa-2x mb-2"></i> <p class="mb-0">{{ t('ui.user_profile.additional_info_private', {}, 'common') }}</p> </div> </div> {% endif %} </div> </div> {% if viewer_is_admin %} <div class="row mt-4"> <div class="col-12"> <div class="card"> <div class="card-header d-flex justify-content-between align-items-center"> <ul class="nav nav-tabs card-header-tabs" id="adminTabList" role="tablist"> <li class="nav-item" role="presentation"> <button class="nav-link active" id="tab-activity" data-bs-toggle="tab" data-bs-target="#pane-activity" type="button" role="tab"> <i class="fas fa-list-alt me-1"></i> {{ t('ui.user_profile.tab.activity_log', {}, 'common') }} </button> </li> <li class="nav-item" role="presentation"> <button class="nav-link" id="tab-transactions" data-bs-toggle="tab" data-bs-target="#pane-transactions" type="button" role="tab"> <i class="fas fa-receipt me-1"></i> {{ t('ui.user_profile.tab.transaction_history', {}, 'common') }} </button> </li> </ul> <span class="badge bg-secondary ms-2">{{ t('ui.user_profile.admin_view', {}, 'common') }}</span> </div> <div class="tab-content"> {# ?? Transaction History ????????????????????????????????????? #} <div class="tab-pane fade" id="pane-transactions" role="tabpanel"> {% if credits_enabled %} {% if transactions|length > 0 %} <div class="table-responsive"> <table class="table table-hover align-middle mb-0"> <thead class="table-light"> <tr> <th style="width: 15%">{{ t('ui.user_profile.date', {}, 'common') }}</th> <th style="width: 15%">{{ t('ui.user_profile.type', {}, 'common') }}</th> <th>{{ t('ui.user_profile.description', {}, 'common') }}</th> <th style="width: 12%" class="text-end">{{ t('ui.user_profile.amount', {}, 'common') }}</th> <th style="width: 12%" class="text-end">{{ t('ui.user_profile.balance', {}, 'common') }}</th> </tr> </thead> <tbody id="transactionTableBody"> {% for tx in transactions %} <tr data-transaction-id="{{ tx.id }}"> <td> <small>{{ tx.created_at|date('m/d/Y') }}</small><br> <small class="text-muted">{{ tx.created_at|date('g:i A') }}</small> </td> <td> <span class="badge bg-{% if tx.transaction_type == 'daily_login' %}info{% elseif tx.transaction_type == 'system_reward' %}success{% elseif tx.transaction_type == 'payment' %}primary{% elseif tx.transaction_type == 'admin_adjustment' %}warning{% else %}secondary{% endif %}"> {% if tx.transaction_type == 'daily_login' %} {{ t('ui.user_profile.transaction_type.daily_login', {}, 'common') }} {% elseif tx.transaction_type == 'system_reward' %} {{ t('ui.user_profile.transaction_type.system_reward', {}, 'common') }} {% elseif tx.transaction_type == 'payment' %} {{ t('ui.user_profile.transaction_type.payment', {}, 'common') }} {% elseif tx.transaction_type == 'admin_adjustment' %} {{ t('ui.user_profile.transaction_type.admin_adjustment', {}, 'common') }} {% else %} {{ t('ui.user_profile.transaction_type.unknown', {}, 'common') }} {% endif %} </span> </td> <td>{{ tx.description }}</td> <td class="text-end {% if tx.amount > 0 %}text-success{% else %}text-danger{% endif %}"> <strong>{{ tx.amount > 0 ? '+' : '' }}{{ credits_symbol }}{{ tx.amount }}</strong> </td> <td class="text-end">{{ credits_symbol }}{{ tx.balance_after }}</td> </tr> {% endfor %} </tbody> </table> </div> <div class="card-footer bg-light" id="loadMoreSection"> <button type="button" class="btn btn-sm btn-outline-secondary w-100" id="loadMoreBtn" onclick="loadMoreTransactions()"> <i class="fas fa-chevron-down me-2"></i> {{ t('ui.user_profile.load_more_transactions', {}, 'common') }} </button> </div> {% else %} <div class="p-3 text-center text-muted"> <i class="fas fa-receipt fa-2x mb-2"></i> <p class="mb-0">{{ t('ui.user_profile.no_transactions_yet', {}, 'common') }}</p> </div> {% endif %} {% else %} <div class="p-3 text-center text-muted"> <i class="fas fa-ban fa-2x mb-2"></i> <p class="mb-0">{{ t('errors.credits.feature_disabled', {}, 'errors') }}</p> </div> {% endif %} </div> {# ?? Activity Log ???????????????????????????????????????????? #} <div class="tab-pane fade show active" id="pane-activity" role="tabpanel"> {% if activity_log|length > 0 %} <div class="table-responsive"> <table class="table table-hover align-middle mb-0"> <thead class="table-light"> <tr> <th style="width: 15%">{{ t('ui.user_profile.activity_log.date', {}, 'common') }}</th> <th style="width: 13%">{{ t('ui.user_profile.activity_log.category', {}, 'common') }}</th> <th style="width: 20%">{{ t('ui.user_profile.activity_log.activity', {}, 'common') }}</th> <th>{{ t('ui.user_profile.activity_log.detail', {}, 'common') }}</th> </tr> </thead> <tbody id="activityTableBody"> {% for entry in activity_log %} <tr> <td> <small class="activity-date" data-utc="{{ entry.created_at }}"></small> </td> <td> <span class="badge bg-{% if entry.category == 'message' %}primary{% elseif entry.category == 'file' %}success{% elseif entry.category == 'door' %}info{% elseif entry.category == 'chat' %}warning{% elseif entry.category == 'auth' %}danger{% else %}secondary{% endif %}"> {{ entry.category }} </span> </td> <td><small>{{ entry.activity }}</small></td> <td><small class="text-muted">{{ entry.object_name|default('') }}</small></td> </tr> {% endfor %} </tbody> </table> </div> <div class="card-footer bg-light" id="loadMoreActivitySection"> <button type="button" class="btn btn-sm btn-outline-secondary w-100" id="loadMoreActivityBtn" onclick="loadMoreActivity()"> <i class="fas fa-chevron-down me-2"></i> {{ t('ui.user_profile.activity_log.load_more', {}, 'common') }} </button> </div> {% else %} <div class="p-3 text-center text-muted"> <i class="fas fa-list-alt fa-2x mb-2"></i> <p class="mb-0">{{ t('ui.user_profile.activity_log.no_activity', {}, 'common') }}</p> </div> {% endif %} </div> </div>{# end tab-content #} </div> </div> </div> {% endif %} {% endblock %} {% block scripts %} <script> let currentOffset = 10; // We already loaded 10 transactions let activityOffset = 25; // We already loaded 25 activity entries document.addEventListener('DOMContentLoaded', function() { document.querySelectorAll('.activity-date[data-utc]').forEach(function(el) { el.textContent = formatFullDate(el.getAttribute('data-utc')); }); }); function uiT(key, fallback, params = {}) { if (window.t) { return window.t(key, params, fallback); } return fallback; } $(document).ready(function() { {% if can_view_sensitive %} loadActivityStats(); {% endif %} {% if credits_enabled and not is_own_profile %} $('#sendCreditsForm').on('submit', function(e) { e.preventDefault(); sendCredits(); }); {% endif %} }); function loadActivityStats() { $.get('/api/user/stats/{{ profile_user_id }}') .done(function(data) { $('#netmailCount').text(data.netmail_count || 0); $('#echomailCount').text(data.echomail_count || 0); $('#totalCount').text((data.netmail_count || 0) + (data.echomail_count || 0)); }) .fail(function() { $('#netmailCount, #echomailCount, #totalCount').text('?'); }); } function sendCredits() { const amount = parseInt($('#creditAmount').val()); const message = $('#creditMessage').val(); if (amount < 1 || amount > 200) { showSendCreditsAlert(uiT('errors.credits.send.invalid_amount', 'Amount must be between 1 and 200 credits'), 'danger'); return; } const btn = $('#sendCreditsForm button[type=submit]'); const originalHtml = btn.html(); btn.prop('disabled', true).html(`<i class="fas fa-spinner fa-spin me-2"></i>${uiT('ui.user_profile.sending', 'Sending...')}`); $.ajax({ url: '/api/credits/send', method: 'POST', contentType: 'application/json', data: JSON.stringify({ recipient_id: {{ profile_user_id }}, amount: amount, message: message }), success: function(response) { if (response.success) { const fallbackMessage = uiT( 'ui.user_profile.send_credits_success', 'Successfully sent {{ credits_symbol }}{amount} to {{ profile_username }}! (Fee: {{ credits_symbol }}{fee}, They received: {{ credits_symbol }}{received})', { symbol: '{{ credits_symbol }}', username: '{{ profile_username|e('js') }}', amount: amount, fee: response.fee, received: response.amount_received } ); const successMessage = window.getApiMessage ? window.getApiMessage(response, fallbackMessage) : fallbackMessage; showSendCreditsAlert(successMessage, 'success'); $('#sendCreditsForm')[0].reset(); } else { const err = window.getApiErrorMessage ? window.getApiErrorMessage(response, uiT('errors.credits.send.failed', 'Failed to send credits')) : uiT('errors.credits.send.failed', 'Failed to send credits'); showSendCreditsAlert(err, 'danger'); } }, error: function(xhr) { const error = window.getApiErrorMessage ? window.getApiErrorMessage(xhr.responseJSON, uiT('errors.credits.send.failed', 'Failed to send credits')) : uiT('errors.credits.send.failed', 'Failed to send credits'); showSendCreditsAlert(error, 'danger'); }, complete: function() { btn.prop('disabled', false).html(originalHtml); } }); } function showSendCreditsAlert(message, type) { const alertHtml = ` <div class="alert alert-${type} alert-dismissible fade show" role="alert"> ${escapeHtml(message)} <button type="button" class="btn-close" data-bs-dismiss="alert"></button> </div> `; $('#sendCreditsAlert').html(alertHtml); if (type === 'success') { setTimeout(function() { $('#sendCreditsAlert').fadeOut(function() { $(this).html('').show(); }); }, 5000); } } function loadMoreTransactions() { const btn = $('#loadMoreBtn'); const originalHtml = btn.html(); btn.prop('disabled', true).html(`<i class="fas fa-spinner fa-spin me-2"></i>${uiT('ui.common.loading', 'Loading...')}`); $.get('/api/user/transactions/{{ profile_user_id }}?offset=' + currentOffset + '&limit=10') .done(function(data) { if (data.transactions && data.transactions.length > 0) { const tbody = $('#transactionTableBody'); data.transactions.forEach(function(tx) { const fullDate = typeof formatFullDate === 'function' ? formatFullDate(tx.created_at) : tx.created_at; let badgeClass = 'secondary'; if (tx.transaction_type === 'daily_login') badgeClass = 'info'; else if (tx.transaction_type === 'system_reward') badgeClass = 'success'; else if (tx.transaction_type === 'payment') badgeClass = 'primary'; else if (tx.transaction_type === 'admin_adjustment') badgeClass = 'warning'; const typeLabel = getTransactionTypeLabel(tx.transaction_type); const amountClass = tx.amount > 0 ? 'text-success' : 'text-danger'; const amountSign = tx.amount > 0 ? '+' : ''; const row = `<tr data-transaction-id="${tx.id}"> <td> <small>${escapeHtml(fullDate)}</small> </td> <td> <span class="badge bg-${badgeClass}">${typeLabel}</span> </td> <td>${escapeHtml(tx.description)}</td> <td class="text-end ${amountClass}"> <strong>${amountSign}{{ credits_symbol }}${tx.amount}</strong> </td> <td class="text-end">{{ credits_symbol }}${tx.balance_after}</td> </tr>`; tbody.append(row); }); currentOffset += data.transactions.length; // Hide button if we loaded less than requested (no more transactions) if (data.transactions.length < 10) { $('#loadMoreSection').html(`<div class="text-center text-muted p-2"><small>${uiT('ui.user_profile.no_more_transactions', 'No more transactions')}</small></div>`); } } else { $('#loadMoreSection').html(`<div class="text-center text-muted p-2"><small>${uiT('ui.user_profile.no_more_transactions', 'No more transactions')}</small></div>`); } }) .fail(function() { alert(uiT('ui.user_profile.load_transactions_failed', 'Failed to load transactions')); }) .always(function() { btn.prop('disabled', false).html(originalHtml); }); } function getTransactionTypeLabel(type) { const map = { daily_login: uiT('ui.user_profile.transaction_type.daily_login', 'Daily Login'), system_reward: uiT('ui.user_profile.transaction_type.system_reward', 'System Reward'), payment: uiT('ui.user_profile.transaction_type.payment', 'Payment'), admin_adjustment: uiT('ui.user_profile.transaction_type.admin_adjustment', 'Admin Adjustment') }; return map[type] || uiT('ui.user_profile.transaction_type.unknown', 'Unknown'); } function getCategoryBadgeClass(category) { const map = { message: 'primary', file: 'success', door: 'info', chat: 'warning', auth: 'danger' }; return map[category] || 'secondary'; } function loadMoreActivity() { const btn = $('#loadMoreActivityBtn'); const originalHtml = btn.html(); btn.prop('disabled', true).html(`<i class="fas fa-spinner fa-spin me-2"></i>${uiT('ui.common.loading', 'Loading...')}`); $.get(`/api/user/activity/{{ profile_user_id }}?offset=${activityOffset}&limit=25`) .done(function(data) { if (data.activity && data.activity.length > 0) { const tbody = $('#activityTableBody'); data.activity.forEach(function(entry) { const badgeClass = getCategoryBadgeClass(entry.category); const row = `<tr> <td> <small>${escapeHtml(formatFullDate(entry.created_at))}</small> </td> <td><span class="badge bg-${badgeClass}">${escapeHtml(entry.category)}</span></td> <td><small>${escapeHtml(entry.activity)}</small></td> <td><small class="text-muted">${escapeHtml(entry.object_name || '')}</small></td> </tr>`; tbody.append(row); }); activityOffset += data.activity.length; if (data.activity.length < 25) { $('#loadMoreActivitySection').html(`<div class="text-center text-muted p-2"><small>${uiT('ui.user_profile.activity_log.no_more', 'No more activity')}</small></div>`); } } else { $('#loadMoreActivitySection').html(`<div class="text-center text-muted p-2"><small>${uiT('ui.user_profile.activity_log.no_more', 'No more activity')}</small></div>`); } }) .fail(function() { alert(uiT('ui.user_profile.activity_log.load_failed', 'Failed to load activity log')); }) .always(function() { btn.prop('disabled', false).html(originalHtml); }); } </script> {% endblock %}