svalavuo преди 5 дни
родител
ревизия
e64b58ce08
променени са 5 файла, в които са добавени 839 реда и са изтрити 4 реда
  1. 46 0
      admin/edit.php
  2. 244 0
      admin/upload_image.php
  3. 299 1
      css/wysiwyg.css
  4. 17 0
      database_migrations/create_images_table.sql
  5. 233 3
      js/wysiwyg.js

+ 46 - 0
admin/edit.php

@@ -186,6 +186,52 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
             </form>
         </main>
     </div>
+
+    <!-- Image Gallery Modal -->
+    <div class="image-gallery" id="imageGallery">
+        <div class="gallery-container">
+            <div class="gallery-header">
+                <h3 class="gallery-title">Image Gallery</h3>
+                <button class="gallery-close" onclick="wysiwygEditor.closeImageGallery()">Close</button>
+            </div>
+            
+            <div class="gallery-content">
+                <div class="gallery-tabs">
+                    <button class="gallery-tab active" onclick="wysiwygEditor.showGalleryTab('browse')">Browse Images</button>
+                    <button class="gallery-tab" onclick="wysiwygEditor.showGalleryTab('upload')">Upload New</button>
+                </div>
+                
+                <!-- Browse Tab -->
+                <div class="gallery-browse" id="galleryBrowse">
+                    <div class="gallery-grid" id="galleryGrid">
+                        <!-- Images will be loaded here -->
+                    </div>
+                </div>
+                
+                <!-- Upload Tab -->
+                <div class="gallery-upload" id="galleryUpload">
+                    <div class="upload-area" id="uploadArea">
+                        <div class="upload-icon">+</div>
+                        <div class="upload-text">Click to upload or drag and drop</div>
+                        <div class="upload-hint">Supported formats: JPG, PNG, GIF (Max 5MB)</div>
+                        <input type="file" class="file-input" id="fileInput" multiple accept="image/*">
+                    </div>
+                    <div class="upload-progress" id="uploadProgress">
+                        <div class="progress-bar">
+                            <div class="progress-fill" id="progressFill"></div>
+                        </div>
+                        <div class="upload-status" id="uploadStatus"></div>
+                    </div>
+                </div>
+            </div>
+            
+            <div class="gallery-actions">
+                <button class="gallery-upload-btn" onclick="wysiwygEditor.showGalleryTab('upload')">Upload New Image</button>
+                <button class="gallery-insert" id="galleryInsertBtn" disabled onclick="wysiwygEditor.insertSelectedImage()">Insert Image</button>
+            </div>
+        </div>
+    </div>
+
 <script src="../js/wysiwyg.js"></script>
 </body>
 </html>

+ 244 - 0
admin/upload_image.php

@@ -0,0 +1,244 @@
+<?php
+/**
+ * Image Upload Handler
+ * Handles image uploads for the WYSIWYG editor
+ */
+
+// Start session
+if (session_status() === PHP_SESSION_NONE) {
+    session_start();
+}
+
+require_once '../includes/config.php';
+require_once '../includes/database.php';
+require_once '../includes/auth.php';
+
+// Require authentication
+$auth = new Auth();
+$auth->requireAuth();
+
+// Set headers for JSON response
+header('Content-Type: application/json');
+
+// Create uploads directory if it doesn't exist
+$uploadsDir = '../uploads/images';
+if (!file_exists($uploadsDir)) {
+    mkdir($uploadsDir, 0755, true);
+}
+
+// Handle file upload
+if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['images'])) {
+    $uploadedFiles = [];
+    $errors = [];
+    
+    // Handle multiple files
+    $files = $_FILES['images'];
+    $fileCount = count($files['name']);
+    
+    for ($i = 0; $i < $fileCount; $i++) {
+        if ($files['error'][$i] === UPLOAD_ERR_OK) {
+            $file = [
+                'name' => $files['name'][$i],
+                'type' => $files['type'][$i],
+                'tmp_name' => $files['tmp_name'][$i],
+                'error' => $files['error'][$i],
+                'size' => $files['size'][$i]
+            ];
+            
+            // Validate file
+            $validation = validateImageFile($file);
+            if ($validation['valid']) {
+                // Generate unique filename
+                $extension = pathinfo($file['name'], PATHINFO_EXTENSION);
+                $filename = uniqid() . '.' . $extension;
+                $filepath = $uploadsDir . '/' . $filename;
+                
+                // Move file to uploads directory
+                if (move_uploaded_file($file['tmp_name'], $filepath)) {
+                    // Save to database
+                    $imageId = saveImageToDatabase($filename, $file['name'], $file['size']);
+                    
+                    if ($imageId) {
+                        $uploadedFiles[] = [
+                            'id' => $imageId,
+                            'filename' => $filename,
+                            'original_name' => $file['name'],
+                            'size' => $file['size'],
+                            'url' => '/uploads/images/' . $filename,
+                            'thumbnail_url' => '/uploads/images/' . $filename
+                        ];
+                    } else {
+                        $errors[] = 'Failed to save ' . $file['name'] . ' to database';
+                        unlink($filepath); // Remove uploaded file
+                    }
+                } else {
+                    $errors[] = 'Failed to upload ' . $file['name'];
+                }
+            } else {
+                $errors[] = $validation['error'];
+            }
+        } else {
+            $errors[] = 'Error uploading file: ' . getUploadErrorMessage($files['error'][$i]);
+        }
+    }
+    
+    echo json_encode([
+        'success' => empty($errors),
+        'files' => $uploadedFiles,
+        'errors' => $errors
+    ]);
+    exit;
+}
+
+// Handle GET request to fetch existing images
+if ($_SERVER['REQUEST_METHOD'] === 'GET') {
+    $images = getExistingImages();
+    echo json_encode([
+        'success' => true,
+        'images' => $images
+    ]);
+    exit;
+}
+
+// Handle DELETE request to remove image
+if ($_SERVER['REQUEST_METHOD'] === 'DELETE' && isset($_GET['id'])) {
+    $imageId = (int)$_GET['id'];
+    $success = deleteImage($imageId);
+    echo json_encode([
+        'success' => $success
+    ]);
+    exit;
+}
+
+echo json_encode(['success' => false, 'error' => 'Invalid request']);
+
+/**
+ * Validate uploaded image file
+ */
+function validateImageFile($file) {
+    // Check file size (5MB max)
+    $maxSize = 5 * 1024 * 1024; // 5MB
+    if ($file['size'] > $maxSize) {
+        return ['valid' => false, 'error' => 'File too large. Maximum size is 5MB'];
+    }
+    
+    // Check file type
+    $allowedTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
+    if (!in_array($file['type'], $allowedTypes)) {
+        return ['valid' => false, 'error' => 'Invalid file type. Only JPG, PNG, GIF, and WebP are allowed'];
+    }
+    
+    // Check file extension
+    $allowedExtensions = ['jpg', 'jpeg', 'png', 'gif', 'webp'];
+    $extension = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
+    if (!in_array($extension, $allowedExtensions)) {
+        return ['valid' => false, 'error' => 'Invalid file extension'];
+    }
+    
+    return ['valid' => true];
+}
+
+/**
+ * Save image information to database
+ */
+function saveImageToDatabase($filename, $originalName, $size) {
+    $db = Database::getInstance();
+    
+    try {
+        $db->insert('images', [
+            'filename' => $filename,
+            'original_name' => $originalName,
+            'file_size' => $size,
+            'uploaded_at' => date('Y-m-d H:i:s'),
+            'uploaded_by' => $_SESSION['user_id'] ?? 1
+        ]);
+        
+        return $db->lastInsertId();
+    } catch (Exception $e) {
+        error_log('Failed to save image to database: ' . $e->getMessage());
+        return false;
+    }
+}
+
+/**
+ * Get existing images from database
+ */
+function getExistingImages() {
+    $db = Database::getInstance();
+    
+    try {
+        $images = $db->fetchAll("SELECT * FROM images ORDER BY uploaded_at DESC");
+        
+        $result = [];
+        foreach ($images as $image) {
+            $result[] = [
+                'id' => $image['id'],
+                'filename' => $image['filename'],
+                'original_name' => $image['original_name'],
+                'size' => $image['file_size'],
+                'url' => '/uploads/images/' . $image['filename'],
+                'thumbnail_url' => '/uploads/images/' . $image['filename'],
+                'uploaded_at' => $image['uploaded_at']
+            ];
+        }
+        
+        return $result;
+    } catch (Exception $e) {
+        error_log('Failed to fetch images: ' . $e->getMessage());
+        return [];
+    }
+}
+
+/**
+ * Delete image from database and filesystem
+ */
+function deleteImage($imageId) {
+    $db = Database::getInstance();
+    
+    try {
+        // Get image info
+        $image = $db->fetch("SELECT * FROM images WHERE id = ?", [$imageId]);
+        if (!$image) {
+            return false;
+        }
+        
+        // Delete from database
+        $db->delete('images', 'id = ?', [$imageId]);
+        
+        // Delete file from filesystem
+        $filepath = '../uploads/images/' . $image['filename'];
+        if (file_exists($filepath)) {
+            unlink($filepath);
+        }
+        
+        return true;
+    } catch (Exception $e) {
+        error_log('Failed to delete image: ' . $e->getMessage());
+        return false;
+    }
+}
+
+/**
+ * Get upload error message
+ */
+function getUploadErrorMessage($errorCode) {
+    switch ($errorCode) {
+        case UPLOAD_ERR_INI_SIZE:
+            return 'The uploaded file exceeds the upload_max_filesize directive in php.ini';
+        case UPLOAD_ERR_FORM_SIZE:
+            return 'The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form';
+        case UPLOAD_ERR_PARTIAL:
+            return 'The uploaded file was only partially uploaded';
+        case UPLOAD_ERR_NO_FILE:
+            return 'No file was uploaded';
+        case UPLOAD_ERR_NO_TMP_DIR:
+            return 'Missing a temporary folder';
+        case UPLOAD_ERR_CANT_WRITE:
+            return 'Failed to write file to disk';
+        case UPLOAD_ERR_EXTENSION:
+            return 'A PHP extension stopped the file upload';
+        default:
+            return 'Unknown upload error';
+    }
+}
+?>

+ 299 - 1
css/wysiwyg.css

@@ -94,7 +94,305 @@
     text-align: right;
 }
 
-/* Responsive design */
+/* Image Gallery */
+.image-gallery {
+    display: none;
+    position: fixed;
+    top: 0;
+    left: 0;
+    width: 100%;
+    height: 100%;
+    background: rgba(0, 0, 0, 0.8);
+    z-index: 1000;
+    overflow-y: auto;
+}
+
+.image-gallery.open {
+    display: block;
+}
+
+.gallery-container {
+    max-width: 1200px;
+    margin: 50px auto;
+    background: white;
+    border-radius: 8px;
+    box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
+}
+
+.gallery-header {
+    padding: 20px;
+    border-bottom: 1px solid #ddd;
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+}
+
+.gallery-title {
+    font-size: 18px;
+    font-weight: bold;
+    margin: 0;
+}
+
+.gallery-close {
+    background: #dc3545;
+    color: white;
+    border: none;
+    padding: 8px 16px;
+    border-radius: 4px;
+    cursor: pointer;
+    font-size: 14px;
+}
+
+.gallery-close:hover {
+    background: #c82333;
+}
+
+.gallery-content {
+    padding: 20px;
+}
+
+.gallery-tabs {
+    display: flex;
+    margin-bottom: 20px;
+    border-bottom: 1px solid #ddd;
+}
+
+.gallery-tab {
+    padding: 10px 20px;
+    background: none;
+    border: none;
+    border-bottom: 2px solid transparent;
+    cursor: pointer;
+    font-size: 14px;
+    font-weight: 500;
+}
+
+.gallery-tab.active {
+    border-bottom-color: #007bff;
+    color: #007bff;
+}
+
+.gallery-tab:hover {
+    color: #007bff;
+}
+
+.gallery-upload {
+    display: none;
+    padding: 20px;
+    border: 2px dashed #ddd;
+    border-radius: 4px;
+    text-align: center;
+}
+
+.gallery-upload.active {
+    display: block;
+}
+
+.upload-area {
+    padding: 40px;
+    border: 2px dashed #007bff;
+    border-radius: 4px;
+    background: #f8f9fa;
+    cursor: pointer;
+    transition: all 0.3s ease;
+}
+
+.upload-area:hover {
+    background: #e3f2fd;
+    border-color: #0056b3;
+}
+
+.upload-area.dragover {
+    background: #e3f2fd;
+    border-color: #0056b3;
+}
+
+.upload-icon {
+    font-size: 48px;
+    color: #007bff;
+    margin-bottom: 10px;
+}
+
+.upload-text {
+    font-size: 16px;
+    color: #333;
+    margin-bottom: 10px;
+}
+
+.upload-hint {
+    font-size: 14px;
+    color: #666;
+}
+
+.file-input {
+    display: none;
+}
+
+.gallery-grid {
+    display: grid;
+    grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
+    gap: 15px;
+    max-height: 400px;
+    overflow-y: auto;
+}
+
+.gallery-item {
+    position: relative;
+    border: 1px solid #ddd;
+    border-radius: 4px;
+    overflow: hidden;
+    cursor: pointer;
+    transition: all 0.3s ease;
+}
+
+.gallery-item:hover {
+    border-color: #007bff;
+    box-shadow: 0 2px 8px rgba(0, 123, 255, 0.3);
+}
+
+.gallery-item.selected {
+    border-color: #007bff;
+    box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.5);
+}
+
+.gallery-image {
+    width: 100%;
+    height: 120px;
+    object-fit: cover;
+    display: block;
+}
+
+.gallery-info {
+    padding: 8px;
+    font-size: 12px;
+    color: #666;
+    background: #f8f9fa;
+}
+
+.gallery-name {
+    font-weight: 500;
+    color: #333;
+    white-space: nowrap;
+    overflow: hidden;
+    text-overflow: ellipsis;
+}
+
+.gallery-size {
+    color: #666;
+}
+
+.gallery-actions {
+    padding: 15px;
+    border-top: 1px solid #ddd;
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+}
+
+.gallery-insert {
+    background: #007bff;
+    color: white;
+    border: none;
+    padding: 10px 20px;
+    border-radius: 4px;
+    cursor: pointer;
+    font-size: 14px;
+}
+
+.gallery-insert:hover:not(:disabled) {
+    background: #0056b3;
+}
+
+.gallery-insert:disabled {
+    background: #6c757d;
+    cursor: not-allowed;
+}
+
+.gallery-upload-btn {
+    background: #28a745;
+    color: white;
+    border: none;
+    padding: 10px 20px;
+    border-radius: 4px;
+    cursor: pointer;
+    font-size: 14px;
+}
+
+.gallery-upload-btn:hover {
+    background: #218838;
+}
+
+.upload-progress {
+    margin-top: 15px;
+    display: none;
+}
+
+.upload-progress.active {
+    display: block;
+}
+
+.progress-bar {
+    width: 100%;
+    height: 8px;
+    background: #e9ecef;
+    border-radius: 4px;
+    overflow: hidden;
+}
+
+.progress-fill {
+    height: 100%;
+    background: #007bff;
+    width: 0%;
+    transition: width 0.3s ease;
+}
+
+.upload-status {
+    margin-top: 10px;
+    font-size: 14px;
+    color: #666;
+}
+
+.upload-status.success {
+    color: #28a745;
+}
+
+.upload-status.error {
+    color: #dc3545;
+}
+
+/* Responsive Design */
+@media (max-width: 768px) {
+    .gallery-container {
+        margin: 20px;
+        max-width: none;
+    }
+    
+    .gallery-grid {
+        grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
+        gap: 10px;
+    }
+    
+    .gallery-image {
+        height: 80px;
+    }
+    
+    .gallery-header {
+        flex-direction: column;
+        gap: 10px;
+        text-align: center;
+    }
+    
+    .gallery-actions {
+        flex-direction: column;
+        gap: 10px;
+    }
+    
+    .gallery-insert,
+    .gallery-upload-btn {
+        width: 100%;
+    }
+}
+
 @media (max-width: 768px) {
     .wysiwyg-toolbar {
         padding: 6px;

+ 17 - 0
database_migrations/create_images_table.sql

@@ -0,0 +1,17 @@
+-- Create images table for WYSIWYG editor
+-- This table stores uploaded images for the image gallery
+
+CREATE TABLE IF NOT EXISTS images (
+    id INT AUTO_INCREMENT PRIMARY KEY,
+    filename VARCHAR(255) NOT NULL,
+    original_name VARCHAR(255) NOT NULL,
+    file_size INT NOT NULL,
+    mime_type VARCHAR(100),
+    uploaded_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+    uploaded_by INT,
+    INDEX idx_uploaded_at (uploaded_at),
+    INDEX idx_uploaded_by (uploaded_by)
+);
+
+-- Add foreign key constraint if users table exists
+-- ALTER TABLE images ADD CONSTRAINT fk_images_user FOREIGN KEY (uploaded_by) REFERENCES users(id) ON DELETE SET NULL;

+ 233 - 3
js/wysiwyg.js

@@ -11,8 +11,11 @@ class WYSIWYGEditor {
             ...options
         };
         
+        this.selectedImages = [];
+        
         this.createEditor();
         this.bindEvents();
+        this.setupImageUpload();
     }
     
     createEditor() {
@@ -129,10 +132,237 @@ class WYSIWYGEditor {
     }
     
     insertImage() {
-        const url = prompt('Enter image URL:');
-        if (url) {
-            document.execCommand('insertImage', false, url);
+        this.openImageGallery();
+    }
+    
+    openImageGallery() {
+        this.selectedImages = [];
+        this.loadGalleryImages();
+        document.getElementById('imageGallery').classList.add('open');
+    }
+    
+    closeImageGallery() {
+        document.getElementById('imageGallery').classList.remove('open');
+        this.selectedImages = [];
+        this.updateInsertButton();
+    }
+    
+    showGalleryTab(tab) {
+        // Hide all tabs
+        document.getElementById('galleryBrowse').style.display = 'none';
+        document.getElementById('galleryUpload').style.display = 'none';
+        
+        // Remove active class from all tabs
+        document.querySelectorAll('.gallery-tab').forEach(tab => {
+            tab.classList.remove('active');
+        });
+        
+        // Show selected tab and add active class
+        if (tab === 'browse') {
+            document.getElementById('galleryBrowse').style.display = 'block';
+            document.querySelectorAll('.gallery-tab')[0].classList.add('active');
+        } else if (tab === 'upload') {
+            document.getElementById('galleryUpload').style.display = 'block';
+            document.querySelectorAll('.gallery-tab')[1].classList.add('active');
+        }
+    }
+    
+    async loadGalleryImages() {
+        try {
+            const response = await fetch('upload_image.php');
+            const data = await response.json();
+            
+            if (data.success) {
+                this.renderGalleryImages(data.images);
+            } else {
+                console.error('Failed to load images:', data.error);
+            }
+        } catch (error) {
+            console.error('Error loading images:', error);
+        }
+    }
+    
+    renderGalleryImages(images) {
+        const grid = document.getElementById('galleryGrid');
+        grid.innerHTML = '';
+        
+        images.forEach(image => {
+            const item = document.createElement('div');
+            item.className = 'gallery-item';
+            item.dataset.imageId = image.id;
+            item.dataset.imageUrl = image.url;
+            item.dataset.imageName = image.original_name;
+            
+            item.innerHTML = `
+                <img src="${image.thumbnail_url}" alt="${image.original_name}" class="gallery-image">
+                <div class="gallery-info">
+                    <div class="gallery-name">${image.original_name}</div>
+                    <div class="gallery-size">${this.formatFileSize(image.size)}</div>
+                </div>
+            `;
+            
+            item.addEventListener('click', () => this.selectImage(item));
+            grid.appendChild(item);
+        });
+    }
+    
+    selectImage(item) {
+        const imageId = item.dataset.imageId;
+        
+        if (item.classList.contains('selected')) {
+            item.classList.remove('selected');
+            this.selectedImages = this.selectedImages.filter(id => id !== imageId);
+        } else {
+            item.classList.add('selected');
+            this.selectedImages.push(imageId);
+        }
+        
+        this.updateInsertButton();
+    }
+    
+    updateInsertButton() {
+        const insertBtn = document.getElementById('galleryInsertBtn');
+        insertBtn.disabled = this.selectedImages.length === 0;
+    }
+    
+    insertSelectedImage() {
+        if (this.selectedImages.length === 0) return;
+        
+        const selectedItems = document.querySelectorAll('.gallery-item.selected');
+        const images = [];
+        
+        selectedItems.forEach(item => {
+            images.push({
+                url: item.dataset.imageUrl,
+                name: item.dataset.imageName
+            });
+        });
+        
+        // Insert images into editor
+        images.forEach(image => {
+            const img = document.createElement('img');
+            img.src = image.url;
+            img.alt = image.name;
+            img.style.maxWidth = '100%';
+            img.style.height = 'auto';
+            
+            // Insert at cursor position
+            const selection = window.getSelection();
+            if (selection.rangeCount > 0) {
+                const range = selection.getRangeAt(0);
+                range.insertNode(img);
+                range.collapse(false);
+            } else {
+                this.content.appendChild(img);
+            }
+        });
+        
+        // Update textarea and close gallery
+        this.textarea.value = this.content.innerHTML;
+        this.closeImageGallery();
+    }
+    
+    setupImageUpload() {
+        const uploadArea = document.getElementById('uploadArea');
+        const fileInput = document.getElementById('fileInput');
+        
+        // Click to upload
+        uploadArea.addEventListener('click', () => {
+            fileInput.click();
+        });
+        
+        // File selection
+        fileInput.addEventListener('change', (e) => {
+            this.handleFileUpload(e.target.files);
+        });
+        
+        // Drag and drop
+        uploadArea.addEventListener('dragover', (e) => {
+            e.preventDefault();
+            uploadArea.classList.add('dragover');
+        });
+        
+        uploadArea.addEventListener('dragleave', () => {
+            uploadArea.classList.remove('dragover');
+        });
+        
+        uploadArea.addEventListener('drop', (e) => {
+            e.preventDefault();
+            uploadArea.classList.remove('dragover');
+            this.handleFileUpload(e.dataTransfer.files);
+        });
+    }
+    
+    async handleFileUpload(files) {
+        const formData = new FormData();
+        
+        for (let i = 0; i < files.length; i++) {
+            formData.append('images[]', files[i]);
         }
+        
+        try {
+            this.showUploadProgress();
+            
+            const response = await fetch('upload_image.php', {
+                method: 'POST',
+                body: formData
+            });
+            
+            const data = await response.json();
+            
+            if (data.success) {
+                this.showUploadSuccess('Images uploaded successfully!');
+                // Refresh gallery
+                this.loadGalleryImages();
+                // Switch to browse tab
+                this.showGalleryTab('browse');
+            } else {
+                this.showUploadError('Upload failed: ' + data.errors.join(', '));
+            }
+        } catch (error) {
+            this.showUploadError('Upload failed: ' + error.message);
+        } finally {
+            this.hideUploadProgress();
+        }
+    }
+    
+    showUploadProgress() {
+        document.getElementById('uploadProgress').classList.add('active');
+        document.getElementById('uploadStatus').textContent = 'Uploading...';
+        document.getElementById('progressFill').style.width = '50%';
+    }
+    
+    hideUploadProgress() {
+        document.getElementById('uploadProgress').classList.remove('active');
+        document.getElementById('progressFill').style.width = '0%';
+    }
+    
+    showUploadSuccess(message) {
+        const status = document.getElementById('uploadStatus');
+        status.textContent = message;
+        status.className = 'upload-status success';
+        
+        setTimeout(() => {
+            this.hideUploadProgress();
+        }, 3000);
+    }
+    
+    showUploadError(message) {
+        const status = document.getElementById('uploadStatus');
+        status.textContent = message;
+        status.className = 'upload-status error';
+        
+        setTimeout(() => {
+            this.hideUploadProgress();
+        }, 5000);
+    }
+    
+    formatFileSize(bytes) {
+        if (bytes === 0) return '0 Bytes';
+        const k = 1024;
+        const sizes = ['Bytes', 'KB', 'MB', 'GB'];
+        const i = Math.floor(Math.log(bytes) / Math.log(k));
+        return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
     }
     
     formatHeading(tag) {