feat: agregar panel de admin para cambiar imagen de login y logo con editor de imágenes y recorte
This commit is contained in:
419
profile.php
419
profile.php
@@ -135,6 +135,425 @@ require_once __DIR__ . '/templates/header.php';
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php if ($_SESSION['role'] === 'admin'): ?>
|
||||
<!-- Admin Section -->
|
||||
<hr class="my-4">
|
||||
<h3 class="mt-4 mb-3">Panel de Administrador</h3>
|
||||
|
||||
<div class="row">
|
||||
<!-- Upload Login Image Card -->
|
||||
<div class="col-lg-6 mb-4">
|
||||
<div class="card shadow-sm h-100">
|
||||
<div class="card-header bg-primary text-white">
|
||||
<h5 class="mb-0"><i class="bi bi-image me-2"></i>Cambiar Imagen de Login</h5>
|
||||
</div>
|
||||
<div class="card-body d-flex flex-column">
|
||||
<p class="text-muted mb-3">Actualiza la imagen de fondo de la pantalla de inicio de sesión (Máximo 5MB).</p>
|
||||
|
||||
<!-- Current login image preview -->
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Imagen Actual:</label>
|
||||
<div class="border rounded p-2">
|
||||
<img id="loginCurrentPreview" src="<?php echo site_url('galeria/login.png?t=' . time()); ?>" alt="Login Image" class="img-fluid" style="max-height: 150px; object-fit: cover;">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Upload form -->
|
||||
<form id="loginImageForm" class="mt-auto">
|
||||
<div class="mb-3">
|
||||
<label for="loginImage" class="form-label">Seleccionar nueva imagen:</label>
|
||||
<input type="file" class="form-control" id="loginImage" name="image" accept="image/*" onchange="prepareImageEditor('login')">
|
||||
<small class="form-text text-muted">Formatos: JPG, PNG, GIF, WebP</small>
|
||||
</div>
|
||||
|
||||
<!-- Preview of selected image -->
|
||||
<div id="loginPreviewSection" class="mb-3" style="display: none;">
|
||||
<label class="form-label">Previsualización:</label>
|
||||
<div class="border rounded p-2">
|
||||
<img id="loginPreview" class="img-fluid" style="max-height: 150px; object-fit: cover;">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Editor button -->
|
||||
<button type="button" class="btn btn-secondary w-100 mb-2" id="loginEditorBtn" onclick="openImageEditor('login')" style="display: none;">
|
||||
<i class="bi bi-crop me-2"></i>Ajustar Imagen
|
||||
</button>
|
||||
|
||||
<button type="button" class="btn btn-primary w-100" id="loginUploadBtn" onclick="uploadAdminImage('login')">
|
||||
<i class="bi bi-cloud-upload me-2"></i>Subir Imagen de Login
|
||||
</button>
|
||||
<div id="loginStatus" class="mt-2"></div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Upload Logo Card -->
|
||||
<div class="col-lg-6 mb-4">
|
||||
<div class="card shadow-sm h-100">
|
||||
<div class="card-header bg-success text-white">
|
||||
<h5 class="mb-0"><i class="bi bi-shield-check me-2"></i>Cambiar Logo</h5>
|
||||
</div>
|
||||
<div class="card-body d-flex flex-column">
|
||||
<p class="text-muted mb-3">Actualiza el logo que se muestra en la navegación (Máximo 5MB).</p>
|
||||
|
||||
<!-- Current logo preview -->
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Logo Actual:</label>
|
||||
<div class="border rounded p-2" style="background-color: #f8f9fa;">
|
||||
<img id="logoCurrentPreview" src="<?php echo site_url('assets/images/logo.png?t=' . time()); ?>" alt="Logo" style="max-height: 60px; object-fit: contain;">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Upload form -->
|
||||
<form id="logoImageForm" class="mt-auto">
|
||||
<div class="mb-3">
|
||||
<label for="logoImage" class="form-label">Seleccionar nuevo logo:</label>
|
||||
<input type="file" class="form-control" id="logoImage" name="image" accept="image/*" onchange="prepareImageEditor('logo')">
|
||||
<small class="form-text text-muted">Formatos: JPG, PNG, GIF, WebP</small>
|
||||
</div>
|
||||
|
||||
<!-- Preview of selected image -->
|
||||
<div id="logoPreviewSection" class="mb-3" style="display: none;">
|
||||
<label class="form-label">Previsualización:</label>
|
||||
<div class="border rounded p-2" style="background-color: #f8f9fa;">
|
||||
<img id="logoPreview" style="max-height: 60px; object-fit: contain;">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Editor button -->
|
||||
<button type="button" class="btn btn-secondary w-100 mb-2" id="logoEditorBtn" onclick="openImageEditor('logo')" style="display: none;">
|
||||
<i class="bi bi-crop me-2"></i>Ajustar Logo
|
||||
</button>
|
||||
|
||||
<button type="button" class="btn btn-success w-100" id="logoUploadBtn" onclick="uploadAdminImage('logo')">
|
||||
<i class="bi bi-cloud-upload me-2"></i>Subir Logo
|
||||
</button>
|
||||
<div id="logoStatus" class="mt-2"></div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<!-- JavaScript para upload de imágenes admin -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/cropperjs@1.5.13/dist/cropper.min.js"></script>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/cropperjs@1.5.13/dist/cropper.min.css">
|
||||
|
||||
<style>
|
||||
#imageEditorModal .modal-body {
|
||||
max-height: 70vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
#editorImage {
|
||||
max-width: 100%;
|
||||
max-height: 500px;
|
||||
}
|
||||
|
||||
.editor-controls {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.editor-controls button {
|
||||
flex: 1;
|
||||
min-width: 100px;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="modal fade" id="imageEditorModal" tabindex="-1">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Ajustar Imagen</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div id="editorContainer">
|
||||
<img id="editorImage" src="">
|
||||
</div>
|
||||
<div class="editor-controls mt-3">
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="rotateImage(-90)">
|
||||
<i class="bi bi-arrow-counterclockwise me-1"></i>Girar Izq
|
||||
</button>
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="rotateImage(90)">
|
||||
<i class="bi bi-arrow-clockwise me-1"></i>Girar Der
|
||||
</button>
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="flipImageH()">
|
||||
<i class="bi bi-arrow-left-right me-1"></i>Voltear H
|
||||
</button>
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="flipImageV()">
|
||||
<i class="bi bi-arrow-up-down me-1"></i>Voltear V
|
||||
</button>
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="resetImage()">
|
||||
<i class="bi bi-arrow-counterclockwise me-1"></i>Resetear
|
||||
</button>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Zoom: <span id="zoomValue">100%</span></label>
|
||||
<input type="range" class="form-range" id="zoomSlider" min="100" max="300" value="100" oninput="zoomImage(this.value)">
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancelar</button>
|
||||
<button type="button" class="btn btn-primary" onclick="applyImageEdit()">Aplicar Cambios</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let cropperInstance = null;
|
||||
let currentImageType = null;
|
||||
let editedImageBlobs = {}; // Guardar blobs editados
|
||||
|
||||
// Función para enviar logs al servidor
|
||||
function sendLog(message, details = null) {
|
||||
fetch('<?php echo site_url('log_frontend.php'); ?>', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
type: 'info',
|
||||
message: message,
|
||||
details: details
|
||||
})
|
||||
}).catch(e => console.error('Error sending log:', e));
|
||||
}
|
||||
|
||||
function prepareImageEditor(type) {
|
||||
const fileInputId = type === 'login' ? 'loginImage' : 'logoImage';
|
||||
const previewId = type === 'login' ? 'loginPreview' : 'logoPreview';
|
||||
const previewSectionId = type === 'login' ? 'loginPreviewSection' : 'logoPreviewSection';
|
||||
const editorBtnId = type === 'login' ? 'loginEditorBtn' : 'logoEditorBtn';
|
||||
|
||||
const fileInput = document.getElementById(fileInputId);
|
||||
const previewImg = document.getElementById(previewId);
|
||||
const previewSection = document.getElementById(previewSectionId);
|
||||
const editorBtn = document.getElementById(editorBtnId);
|
||||
const file = fileInput.files[0];
|
||||
|
||||
if (file) {
|
||||
sendLog('Archivo seleccionado para ' + type, {name: file.name, size: file.size});
|
||||
const reader = new FileReader();
|
||||
reader.onload = function(e) {
|
||||
previewImg.src = e.target.result;
|
||||
previewSection.style.display = 'block';
|
||||
editorBtn.style.display = 'block';
|
||||
delete editedImageBlobs[type];
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
} else {
|
||||
previewSection.style.display = 'none';
|
||||
editorBtn.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
function openImageEditor(type) {
|
||||
sendLog('Abriendo editor para tipo: ' + type);
|
||||
currentImageType = type;
|
||||
const previewId = type === 'login' ? 'loginPreview' : 'logoPreview';
|
||||
const previewImg = document.getElementById(previewId);
|
||||
const editorImg = document.getElementById('editorImage');
|
||||
|
||||
editorImg.src = previewImg.src;
|
||||
|
||||
if (cropperInstance) {
|
||||
cropperInstance.destroy();
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
cropperInstance = new Cropper(editorImg, {
|
||||
aspectRatio: type === 'login' ? 16/9 : 1/1,
|
||||
autoCropArea: 0.8,
|
||||
responsive: true,
|
||||
guides: true,
|
||||
highlight: true,
|
||||
cropBoxMovable: true,
|
||||
cropBoxResizable: true,
|
||||
toggleDragModeOnDblclick: true,
|
||||
});
|
||||
|
||||
const modal = new bootstrap.Modal(document.getElementById('imageEditorModal'));
|
||||
modal.show();
|
||||
sendLog('Cropper iniciado para ' + type);
|
||||
}, 100);
|
||||
}
|
||||
|
||||
function rotateImage(degrees) {
|
||||
if (cropperInstance) {
|
||||
cropperInstance.rotate(degrees);
|
||||
}
|
||||
}
|
||||
|
||||
function flipImageH() {
|
||||
if (cropperInstance) {
|
||||
cropperInstance.scaleX(-cropperInstance.getData().scaleX || -1);
|
||||
}
|
||||
}
|
||||
|
||||
function flipImageV() {
|
||||
if (cropperInstance) {
|
||||
cropperInstance.scaleY(-cropperInstance.getData().scaleY || -1);
|
||||
}
|
||||
}
|
||||
|
||||
function resetImage() {
|
||||
if (cropperInstance) {
|
||||
cropperInstance.reset();
|
||||
}
|
||||
}
|
||||
|
||||
function zoomImage(value) {
|
||||
if (cropperInstance) {
|
||||
const zoom = (value - 100) / 100;
|
||||
cropperInstance.setCanvasData({
|
||||
left: 0,
|
||||
top: 0,
|
||||
width: cropperInstance.getCanvasData().width,
|
||||
height: cropperInstance.getCanvasData().height
|
||||
});
|
||||
cropperInstance.zoom(zoom);
|
||||
document.getElementById('zoomValue').textContent = value + '%';
|
||||
}
|
||||
}
|
||||
|
||||
function applyImageEdit() {
|
||||
if (!cropperInstance) {
|
||||
sendLog('ERROR: cropperInstance es null en applyImageEdit');
|
||||
return;
|
||||
}
|
||||
|
||||
sendLog('Aplicando cambios para tipo: ' + currentImageType);
|
||||
|
||||
const canvas = cropperInstance.getCroppedCanvas({
|
||||
maxWidth: 4096,
|
||||
maxHeight: 4096,
|
||||
fillColor: '#fff',
|
||||
imageSmoothingEnabled: true,
|
||||
imageSmoothingQuality: 'high',
|
||||
});
|
||||
|
||||
const previewId = currentImageType === 'login' ? 'loginPreview' : 'logoPreview';
|
||||
const previewImg = document.getElementById(previewId);
|
||||
|
||||
// Actualizar preview
|
||||
const canvasDataUrl = canvas.toDataURL('image/png');
|
||||
previewImg.src = canvasDataUrl;
|
||||
|
||||
// Convertir canvas a blob y guardarlo
|
||||
canvas.toBlob(blob => {
|
||||
editedImageBlobs[currentImageType] = blob;
|
||||
sendLog('Blob guardado para ' + currentImageType, {size: blob.size, type: blob.type});
|
||||
|
||||
const modal = bootstrap.Modal.getInstance(document.getElementById('imageEditorModal'));
|
||||
if (modal) {
|
||||
modal.hide();
|
||||
}
|
||||
}, 'image/png', 0.95);
|
||||
}
|
||||
|
||||
function uploadAdminImage(type) {
|
||||
sendLog('Iniciando upload para tipo: ' + type);
|
||||
|
||||
const statusId = type === 'login' ? 'loginStatus' : 'logoStatus';
|
||||
const previewId = type === 'login' ? 'loginCurrentPreview' : 'logoCurrentPreview';
|
||||
const fileInputId = type === 'login' ? 'loginImage' : 'logoImage';
|
||||
|
||||
let fileToUpload = null;
|
||||
|
||||
// Usar blob editado si existe, sino usar archivo original
|
||||
if (editedImageBlobs[type]) {
|
||||
sendLog('USANDO BLOB EDITADO para ' + type);
|
||||
fileToUpload = new File([editedImageBlobs[type]], 'edited-image.png', { type: 'image/png' });
|
||||
} else {
|
||||
sendLog('USANDO ARCHIVO ORIGINAL para ' + type);
|
||||
const fileInput = document.getElementById(fileInputId);
|
||||
fileToUpload = fileInput.files[0];
|
||||
}
|
||||
|
||||
if (!fileToUpload) {
|
||||
sendLog('ERROR: No hay archivo para subir tipo ' + type);
|
||||
showStatus(statusId, 'Por favor selecciona un archivo.', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
sendLog('Archivo a subir para ' + type, {
|
||||
name: fileToUpload.name,
|
||||
size: fileToUpload.size,
|
||||
type: fileToUpload.type
|
||||
});
|
||||
|
||||
// Validar tamaño
|
||||
if (fileToUpload.size > 5 * 1024 * 1024) {
|
||||
showStatus(statusId, 'El archivo es demasiado grande. Máximo 5MB.', 'danger');
|
||||
return;
|
||||
}
|
||||
|
||||
// Validar tipo
|
||||
if (!fileToUpload.type.startsWith('image/')) {
|
||||
showStatus(statusId, 'El archivo debe ser una imagen.', 'danger');
|
||||
return;
|
||||
}
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('image', fileToUpload);
|
||||
formData.append('type', type);
|
||||
|
||||
const buttonId = type === 'login' ? 'loginUploadBtn' : 'logoUploadBtn';
|
||||
const button = document.getElementById(buttonId);
|
||||
button.disabled = true;
|
||||
button.innerHTML = '<span class="spinner-border spinner-border-sm me-2"></span>Subiendo...';
|
||||
|
||||
fetch('<?php echo site_url('upload_admin_images.php'); ?>', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
sendLog('Respuesta del servidor para ' + type, data);
|
||||
if (data.success) {
|
||||
showStatus(statusId, data.message, 'success');
|
||||
document.getElementById(previewId).src = data.url + '?t=' + Date.now();
|
||||
|
||||
const fileInput = document.getElementById(fileInputId);
|
||||
fileInput.value = '';
|
||||
|
||||
document.getElementById(type === 'login' ? 'loginPreviewSection' : 'logoPreviewSection').style.display = 'none';
|
||||
document.getElementById(type === 'login' ? 'loginEditorBtn' : 'logoEditorBtn').style.display = 'none';
|
||||
|
||||
delete editedImageBlobs[type];
|
||||
|
||||
sendLog('Upload completado exitosamente para ' + type);
|
||||
setTimeout(() => location.reload(), 2000);
|
||||
} else {
|
||||
sendLog('ERROR en respuesta del servidor para ' + type, data);
|
||||
showStatus(statusId, data.message, 'danger');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
sendLog('ERROR en fetch para ' + type, {message: error.message});
|
||||
showStatus(statusId, 'Error al subir la imagen: ' + error.message, 'danger');
|
||||
})
|
||||
.finally(() => {
|
||||
button.disabled = false;
|
||||
button.innerHTML = '<i class="bi bi-cloud-upload me-2"></i>Subir ' + (type === 'login' ? 'Imagen de Login' : 'Logo');
|
||||
});
|
||||
}
|
||||
|
||||
function showStatus(statusId, message, type) {
|
||||
const statusDiv = document.getElementById(statusId);
|
||||
const alertClass = 'alert-' + type;
|
||||
statusDiv.innerHTML = '<div class="alert ' + alertClass + ' mb-0" role="alert">' + message + '</div>';
|
||||
}
|
||||
</script>
|
||||
|
||||
<?php require_once __DIR__ . '/templates/footer.php'; ?>
|
||||
Reference in New Issue
Block a user