diff --git a/.github/workflows/build-assets.yml b/.github/workflows/build-assets.yml index 0d0d5bd..e59e5ab 100644 --- a/.github/workflows/build-assets.yml +++ b/.github/workflows/build-assets.yml @@ -2,14 +2,14 @@ name: Build Assets on: push: - branches: [main, develop] + branches: [develop] paths: - 'assets/src/**' - 'templates/**' - 'tailwind.config.js' - 'scripts/generate-icons.php' pull_request: - branches: [main, develop] + branches: [main] paths: - 'assets/src/**' - 'templates/**' diff --git a/.gitignore b/.gitignore index f2dd7d0..1476252 100644 --- a/.gitignore +++ b/.gitignore @@ -51,3 +51,4 @@ error_log tailwindcss-linux-x64 esbuild uploads/.import_history.json +frontend/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 2ad2c7a..8d771e4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,58 @@ All notable changes to BigDump are documented in this file. > **Note**: BigDump was originally created by Alexey Ozerov in 2003. Version 2.x is a complete MVC refactoring by w3spi5 (2025). +## [2.27] - 2025-01-13 - Celebration UI & Import Completion + +### Added in 2.27 + +- **Celebration Effects on Import Completion**: Visual feedback for successful imports + - Canvas-based fireworks with particle trails, glow effects, and color gradients + - 3D rotating confetti with realistic physics (air resistance, terminal velocity) + - Confetti cannons firing from screen sides at intervals + - Fireworks display for 20 seconds, confetti falls indefinitely + - Performance optimized with `requestAnimationFrame` + +- **Animated Favicon**: Visual indicator during active import + - Canvas-generated bouncing arrow animation + - Starts on SSE connection, stops on completion/error + - Restores original favicon when stopped + +- **Skeleton Loaders**: Better loading state UX + - Horizontal shimmer animation for unknown statistics + - Replaces static "?" placeholders during import start + - Smooth gradient animation with dark mode support + +### Fixed in 2.27 + +- **99.5% Progress Bug**: Import no longer stuck at 99.5% + - Forces 100% display when `data.finished` is true + - Updates all percentage displays (progress bar, stat boxes, table) + - Adds green glow animation class on completion + +- **SSE Completion Redirect**: Success message now displays in-page + - No longer redirects to error page after session cleanup + - Shows "Import terminé !" with statistics grid + - French localization for success screen labels + +### Changed in 2.27 + +- **Success Screen Styling**: Enhanced dark mode support + - New `.stat-card-success` class with gradient backgrounds + - Glowing borders and hover effects in dark mode + - French labels: "Requêtes", "Lignes", "Traité", "Durée" + +- **Progress Bar Completion**: Animated green glow effect + - `.progress-bar-complete` class with shimmer gradient + - Pulsing box-shadow animation + - Dark mode optimized glow intensity + +- **CI Workflow**: Fixed build-assets workflow for protected branches + - Auto-commit now only runs on `develop` branch + - PR to `main` triggers build verification only + - Prevents conflict with branch protection rules + +--- + ## [2.26] - 2025-01-13 - Clean URLs & Bug Fixes ### Added in 2.26 diff --git a/README.md b/README.md index a5d5d9d..3f45216 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ -# BigDump 2.26 - Staggered MySQL Dump Importer +# BigDump 2.27 - Staggered MySQL Dump Importer [![PHP Version](https://img.shields.io/badge/php-8.1+-yellow.svg)](https://php.net/) [![License](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE) -[![Package Version](https://img.shields.io/badge/version-2.26-blue.svg)](https://php.net/) +[![Package Version](https://img.shields.io/badge/version-2.27-blue.svg)](https://php.net/) [![Build Assets](https://img.shields.io/badge/build-GitHub_Actions-2088FF.svg)](https://github.com/w3spi5/bigdump/actions) [![PHAR](https://img.shields.io/badge/PHAR-single--file-purple.svg)](https://github.com/w3spi5/bigdump/releases) diff --git a/assets/css/bigdump.css b/assets/css/bigdump.css index b1a9d21..1dfea87 100644 --- a/assets/css/bigdump.css +++ b/assets/css/bigdump.css @@ -773,6 +773,120 @@ display: flex; } +/* ======================================== + SKELETON LOADER + ======================================== */ + +.skeleton { + display: inline-block; + height: 1em; + min-width: 3em; + background: linear-gradient( + 90deg, + rgba(156, 163, 175, 0.3) 25%, + rgba(156, 163, 175, 0.5) 50%, + rgba(156, 163, 175, 0.3) 75% + ); + background-size: 200% 100%; + animation: skeleton-shimmer 1.5s ease-in-out infinite; + border-radius: 0.25rem; + vertical-align: middle; +} + +[data-theme="dark"] .skeleton { + background: linear-gradient( + 90deg, + rgba(75, 85, 99, 0.4) 25%, + rgba(75, 85, 99, 0.7) 50%, + rgba(75, 85, 99, 0.4) 75% + ); + background-size: 200% 100%; +} + +@keyframes skeleton-shimmer { + 0% { background-position: 200% 0; } + 100% { background-position: -200% 0; } +} + +/* ======================================== + PROGRESS BAR COMPLETION ANIMATION + ======================================== */ + +.progress-bar-complete { + background: linear-gradient(90deg, #22c55e, #16a34a, #4ade80, #22c55e) !important; + background-size: 300% 100% !important; + animation: progress-complete-glow 2s ease-in-out infinite !important; + box-shadow: 0 0 20px rgba(34, 197, 94, 0.6), + 0 0 40px rgba(34, 197, 94, 0.4), + inset 0 0 10px rgba(255, 255, 255, 0.2) !important; +} + +@keyframes progress-complete-glow { + 0%, 100% { + background-position: 0% 50%; + box-shadow: 0 0 20px rgba(34, 197, 94, 0.6), + 0 0 40px rgba(34, 197, 94, 0.4), + inset 0 0 10px rgba(255, 255, 255, 0.2); + } + 50% { + background-position: 100% 50%; + box-shadow: 0 0 30px rgba(34, 197, 94, 0.8), + 0 0 60px rgba(34, 197, 94, 0.5), + inset 0 0 15px rgba(255, 255, 255, 0.3); + } +} + +/* Dark mode adjustment for completion glow */ +.dark .progress-bar-complete { + box-shadow: 0 0 25px rgba(74, 222, 128, 0.7), + 0 0 50px rgba(74, 222, 128, 0.5), + inset 0 0 10px rgba(255, 255, 255, 0.15) !important; +} + +/* Success message fade-in animation */ +.animate-fade-in { + animation: fade-in-up 0.5s ease-out forwards; +} + +@keyframes fade-in-up { + 0% { + opacity: 0; + transform: translateY(20px); + } + 100% { + opacity: 1; + transform: translateY(0); + } +} + +/* Success stat cards with enhanced dark mode */ +.stat-card-success { + background: linear-gradient(135deg, #dcfce7 0%, #bbf7d0 100%); + border-radius: 0.75rem; + padding: 1rem; + border: 1px solid rgba(34, 197, 94, 0.2); + transition: transform 0.2s ease, box-shadow 0.2s ease; +} + +.stat-card-success:hover { + transform: translateY(-2px); + box-shadow: 0 4px 12px rgba(34, 197, 94, 0.15); +} + +.dark .stat-card-success { + background: linear-gradient(135deg, rgba(34, 197, 94, 0.15) 0%, rgba(22, 163, 74, 0.25) 100%); + border: 1px solid rgba(74, 222, 128, 0.3); + box-shadow: 0 0 20px rgba(34, 197, 94, 0.1), + inset 0 1px 0 rgba(255, 255, 255, 0.05); +} + +.dark .stat-card-success:hover { + box-shadow: 0 0 25px rgba(34, 197, 94, 0.2), + 0 4px 15px rgba(0, 0, 0, 0.3), + inset 0 1px 0 rgba(255, 255, 255, 0.08); + border-color: rgba(74, 222, 128, 0.5); +} + /* ======================================== FILE UPLOAD ENHANCEMENTS ======================================== */ diff --git a/assets/js/celebration.js b/assets/js/celebration.js new file mode 100644 index 0000000..5d80a74 --- /dev/null +++ b/assets/js/celebration.js @@ -0,0 +1,466 @@ +/** + * BigDump - Celebration Effects Module (Enhanced) + * Creates realistic fireworks and confetti animations on import completion. + * Features: particle trails, glow effects, 3D confetti rotation, air resistance. + */ +(function() { + 'use strict'; + + var canvas = null; + var ctx = null; + var animationId = null; + var particles = []; + var confetti = []; + var trails = []; + var isRunning = false; + var startTime = 0; + var fireworksDuration = 20000; // 20 seconds of fireworks + var confettiForever = true; // Confetti falls indefinitely + + // Vibrant colors for fireworks + var fireworkPalettes = [ + ['#FF6B6B', '#FF8E8E', '#FFB4B4'], // Red + ['#4ECDC4', '#7EDCD6', '#A8EBE7'], // Teal + ['#45B7D1', '#6FC9DE', '#99DBEB'], // Blue + ['#FFD93D', '#FFE469', '#FFEF9A'], // Yellow + ['#6BCB77', '#8DD896', '#AFE5B5'], // Green + ['#FF8C00', '#FFA533', '#FFBE66'], // Orange + ['#DA70D6', '#E494E0', '#EEB8EA'], // Orchid + ['#9B59B6', '#B07CC6', '#C59FD6'], // Purple + ['#E74C3C', '#ED7669', '#F3A096'], // Coral + ['#3498DB', '#5DADE2', '#85C1E9'] // Sky blue + ]; + + var confettiColors = [ + '#FF6B6B', '#4ECDC4', '#45B7D1', '#FFD93D', '#6BCB77', + '#FF8C00', '#DA70D6', '#9B59B6', '#E74C3C', '#3498DB', + '#F39C12', '#1ABC9C', '#E91E63', '#00BCD4', '#8BC34A' + ]; + + /** + * Enhanced Particle class with trails and glow + */ + function Particle(x, y, color, velocity, options) { + options = options || {}; + this.x = x; + this.y = y; + this.color = color; + this.velocity = velocity; + this.gravity = options.gravity || 0.06; + this.decay = options.decay || 0.015; + this.alpha = 1; + this.size = options.size || 3; + this.hasTrail = options.hasTrail !== false; + this.glowSize = options.glowSize || this.size * 2; + this.friction = options.friction || 0.98; + this.trail = []; + this.maxTrailLength = options.trailLength || 5; + } + + Particle.prototype.update = function() { + // Store trail position + if (this.hasTrail && this.alpha > 0.3) { + this.trail.push({ x: this.x, y: this.y, alpha: this.alpha }); + if (this.trail.length > this.maxTrailLength) { + this.trail.shift(); + } + } + + this.velocity.x *= this.friction; + this.velocity.y *= this.friction; + this.velocity.y += this.gravity; + this.x += this.velocity.x; + this.y += this.velocity.y; + this.alpha -= this.decay; + return this.alpha > 0; + }; + + Particle.prototype.draw = function(ctx) { + // Draw trail + if (this.hasTrail && this.trail.length > 0) { + for (var i = 0; i < this.trail.length; i++) { + var t = this.trail[i]; + var trailAlpha = (i / this.trail.length) * this.alpha * 0.5; + var trailSize = this.size * (0.3 + (i / this.trail.length) * 0.5); + ctx.save(); + ctx.globalAlpha = trailAlpha; + ctx.beginPath(); + ctx.arc(t.x, t.y, trailSize, 0, Math.PI * 2); + ctx.fillStyle = this.color; + ctx.fill(); + ctx.restore(); + } + } + + // Draw glow + ctx.save(); + ctx.globalAlpha = this.alpha * 0.4; + ctx.beginPath(); + ctx.arc(this.x, this.y, this.glowSize, 0, Math.PI * 2); + var gradient = ctx.createRadialGradient(this.x, this.y, 0, this.x, this.y, this.glowSize); + gradient.addColorStop(0, this.color); + gradient.addColorStop(1, 'transparent'); + ctx.fillStyle = gradient; + ctx.fill(); + ctx.restore(); + + // Draw core + ctx.save(); + ctx.globalAlpha = this.alpha; + ctx.beginPath(); + ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2); + ctx.fillStyle = this.color; + ctx.fill(); + + // Bright center + ctx.beginPath(); + ctx.arc(this.x, this.y, this.size * 0.5, 0, Math.PI * 2); + ctx.fillStyle = '#FFFFFF'; + ctx.globalAlpha = this.alpha * 0.8; + ctx.fill(); + ctx.restore(); + }; + + /** + * Enhanced Confetti with 3D rotation and air resistance + */ + function ConfettiPiece(x, y, color) { + this.x = x; + this.y = y; + this.color = color; + this.width = Math.random() * 12 + 6; + this.height = Math.random() * 8 + 4; + this.velocity = { + x: (Math.random() - 0.5) * 8, + y: Math.random() * 2 + 1 + }; + // 3D rotation angles + this.rotationX = Math.random() * 360; + this.rotationY = Math.random() * 360; + this.rotationZ = Math.random() * 360; + this.rotationSpeedX = (Math.random() - 0.5) * 8; + this.rotationSpeedY = (Math.random() - 0.5) * 12; + this.rotationSpeedZ = (Math.random() - 0.5) * 6; + // Oscillation (swaying) + this.oscillationSpeed = Math.random() * 0.08 + 0.03; + this.oscillationDistance = Math.random() * 60 + 30; + this.initialX = x; + this.time = Math.random() * 100; + // Air resistance + this.drag = 0.98 + Math.random() * 0.015; + this.terminalVelocity = 4 + Math.random() * 2; + } + + ConfettiPiece.prototype.update = function(canvasHeight) { + this.time += this.oscillationSpeed; + + // Horizontal oscillation (swaying in wind) + this.x = this.initialX + Math.sin(this.time) * this.oscillationDistance; + this.initialX += this.velocity.x * 0.1; // Drift + + // Apply drag + this.velocity.x *= this.drag; + this.velocity.y += 0.08; // Gravity + + // Terminal velocity + if (this.velocity.y > this.terminalVelocity) { + this.velocity.y = this.terminalVelocity; + } + + this.y += this.velocity.y; + + // Tumbling rotation + this.rotationX += this.rotationSpeedX; + this.rotationY += this.rotationSpeedY; + this.rotationZ += this.rotationSpeedZ; + + // Slow down rotation over time + this.rotationSpeedX *= 0.999; + this.rotationSpeedY *= 0.999; + + return this.y < canvasHeight + 50; + }; + + ConfettiPiece.prototype.draw = function(ctx) { + ctx.save(); + ctx.translate(this.x, this.y); + + // Apply 3D-like rotation (fake perspective) + var scaleX = Math.cos(this.rotationY * Math.PI / 180); + var scaleY = Math.cos(this.rotationX * Math.PI / 180); + ctx.rotate(this.rotationZ * Math.PI / 180); + ctx.scale(scaleX, scaleY); + + // Draw confetti piece with slight shadow + ctx.shadowColor = 'rgba(0,0,0,0.2)'; + ctx.shadowBlur = 2; + ctx.shadowOffsetY = 1; + + ctx.fillStyle = this.color; + ctx.fillRect(-this.width / 2, -this.height / 2, this.width, this.height); + + // Add shine effect + ctx.fillStyle = 'rgba(255,255,255,0.3)'; + ctx.fillRect(-this.width / 2, -this.height / 2, this.width * 0.4, this.height); + + ctx.restore(); + }; + + /** + * Create an enhanced firework explosion + */ + function createFirework(x, y) { + var palette = fireworkPalettes[Math.floor(Math.random() * fireworkPalettes.length)]; + var particleCount = 60 + Math.floor(Math.random() * 40); + var burstSpeed = 4 + Math.random() * 3; + + // Main explosion particles + for (var i = 0; i < particleCount; i++) { + var angle = (Math.PI * 2 / particleCount) * i + (Math.random() - 0.5) * 0.2; + var speed = burstSpeed * (0.6 + Math.random() * 0.8); + var colorIndex = Math.floor(Math.random() * palette.length); + var velocity = { + x: Math.cos(angle) * speed, + y: Math.sin(angle) * speed + }; + particles.push(new Particle(x, y, palette[colorIndex], velocity, { + gravity: 0.05, + decay: 0.010 + Math.random() * 0.008, + size: 2 + Math.random() * 2, + trailLength: 4, + glowSize: 6 + })); + } + + // Inner ring (brighter, faster decay) + var innerCount = 20 + Math.floor(Math.random() * 15); + for (var j = 0; j < innerCount; j++) { + var innerAngle = Math.random() * Math.PI * 2; + var innerSpeed = burstSpeed * 1.3 * Math.random(); + var innerVelocity = { + x: Math.cos(innerAngle) * innerSpeed, + y: Math.sin(innerAngle) * innerSpeed + }; + particles.push(new Particle(x, y, '#FFFFFF', innerVelocity, { + gravity: 0.04, + decay: 0.025, + size: 1.5, + trailLength: 3, + glowSize: 4, + hasTrail: true + })); + } + + // Sparkle particles (twinkle effect) + var sparkleCount = 15 + Math.floor(Math.random() * 10); + for (var k = 0; k < sparkleCount; k++) { + var sparkAngle = Math.random() * Math.PI * 2; + var sparkSpeed = burstSpeed * 0.3 + Math.random() * burstSpeed * 0.8; + var sparkVelocity = { + x: Math.cos(sparkAngle) * sparkSpeed, + y: Math.sin(sparkAngle) * sparkSpeed + }; + particles.push(new Particle(x, y, '#FFFACD', sparkVelocity, { + gravity: 0.08, + decay: 0.03 + Math.random() * 0.02, + size: 1, + glowSize: 3, + hasTrail: false + })); + } + } + + /** + * Create confetti burst from top + */ + function createConfettiBurst(count) { + if (!canvas) return; + + for (var i = 0; i < count; i++) { + var x = Math.random() * canvas.width; + var color = confettiColors[Math.floor(Math.random() * confettiColors.length)]; + confetti.push(new ConfettiPiece(x, -20 - Math.random() * 50, color)); + } + } + + /** + * Create confetti cannon from sides + */ + function createConfettiCannon(fromLeft) { + if (!canvas) return; + + var startX = fromLeft ? -10 : canvas.width + 10; + var angle = fromLeft ? -Math.PI / 4 : -Math.PI * 3 / 4; + + for (var i = 0; i < 30; i++) { + var spread = (Math.random() - 0.5) * 0.5; + var speed = 8 + Math.random() * 6; + var color = confettiColors[Math.floor(Math.random() * confettiColors.length)]; + var piece = new ConfettiPiece(startX, canvas.height * 0.7, color); + piece.velocity.x = Math.cos(angle + spread) * speed; + piece.velocity.y = Math.sin(angle + spread) * speed; + piece.initialX = startX; + confetti.push(piece); + } + } + + /** + * Initialize canvas + */ + function init() { + if (canvas) return; + + canvas = document.createElement('canvas'); + canvas.id = 'celebration-canvas'; + canvas.style.cssText = 'position: fixed; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none; z-index: 9999;'; + document.body.appendChild(canvas); + + ctx = canvas.getContext('2d'); + resizeCanvas(); + + window.addEventListener('resize', resizeCanvas); + } + + /** + * Resize canvas to match window + */ + function resizeCanvas() { + if (!canvas) return; + canvas.width = window.innerWidth; + canvas.height = window.innerHeight; + } + + /** + * Animation loop + */ + function animate() { + if (!isRunning) return; + + var elapsed = Date.now() - startTime; + + // Clear canvas with slight fade for trail effect + ctx.fillStyle = 'rgba(0, 0, 0, 0.1)'; + ctx.fillRect(0, 0, canvas.width, canvas.height); + ctx.clearRect(0, 0, canvas.width, canvas.height); + + // Update and draw particles + particles = particles.filter(function(p) { + var alive = p.update(); + if (alive) p.draw(ctx); + return alive; + }); + + // Update and draw confetti + confetti = confetti.filter(function(c) { + var alive = c.update(canvas.height); + if (alive) c.draw(ctx); + return alive; + }); + + // Create new fireworks periodically for 20 seconds + if (elapsed < fireworksDuration) { + // More frequent fireworks at start, gradually decreasing + var fireworkChance = elapsed < 5000 ? 0.08 : (elapsed < 10000 ? 0.05 : 0.03); + if (Math.random() < fireworkChance) { + var x = Math.random() * canvas.width * 0.7 + canvas.width * 0.15; + var y = Math.random() * canvas.height * 0.4 + canvas.height * 0.08; + createFirework(x, y); + } + } + + // Add confetti continuously forever (or until stopped) + if (confettiForever && Math.random() < 0.15) { + createConfettiBurst(1); + } + + // Confetti cannons at specific intervals during first 15 seconds + if (elapsed < 15000) { + var cannonInterval = 2000; // Every 2 seconds + var cannonWindow = 30; + if (elapsed % cannonInterval < cannonWindow && Math.random() < 0.5) { + createConfettiCannon(Math.random() < 0.5); + } + } + + // Continue animation as long as running (confetti forever!) + if (isRunning) { + animationId = requestAnimationFrame(animate); + } else { + cleanup(); + } + } + + /** + * Clean up canvas and reset state + */ + function cleanup() { + isRunning = false; + if (animationId) { + cancelAnimationFrame(animationId); + animationId = null; + } + if (canvas && canvas.parentNode) { + canvas.parentNode.removeChild(canvas); + } + canvas = null; + ctx = null; + particles = []; + confetti = []; + trails = []; + window.removeEventListener('resize', resizeCanvas); + } + + /** + * Start the celebration effects + */ + function start() { + if (isRunning) return; + + init(); + isRunning = true; + startTime = Date.now(); + particles = []; + confetti = []; + trails = []; + + // Initial burst of fireworks (staggered) + var fireworkPositions = [ + { x: 0.3, y: 0.25, delay: 0 }, + { x: 0.7, y: 0.2, delay: 150 }, + { x: 0.5, y: 0.15, delay: 300 }, + { x: 0.25, y: 0.3, delay: 450 }, + { x: 0.75, y: 0.28, delay: 600 } + ]; + + fireworkPositions.forEach(function(pos) { + setTimeout(function() { + if (!isRunning || !canvas) return; + createFirework(canvas.width * pos.x, canvas.height * pos.y); + }, pos.delay); + }); + + // Initial confetti burst + createConfettiBurst(80); + + animate(); + console.log('Celebration started!'); + } + + /** + * Stop the celebration effects immediately + */ + function stop() { + if (!isRunning) return; + cleanup(); + console.log('Celebration stopped'); + } + + // Expose API + window.BigDump = window.BigDump || {}; + window.BigDump.celebration = { + start: start, + stop: stop, + isRunning: function() { return isRunning; } + }; + +})(); diff --git a/assets/js/favicon-animator.js b/assets/js/favicon-animator.js new file mode 100644 index 0000000..7a03b6c --- /dev/null +++ b/assets/js/favicon-animator.js @@ -0,0 +1,145 @@ +/** + * BigDump - Animated Favicon Module + * Creates a bouncing arrow animation during import process. + * Uses Canvas API to dynamically generate favicon frames. + */ +(function() { + 'use strict'; + + var canvas = null; + var ctx = null; + var animationId = null; + var originalFavicon = null; + var linkElement = null; + var frame = 0; + var isAnimating = false; + + // Animation settings + var SIZE = 32; // Favicon size + var ARROW_COLOR = '#3B82F6'; // Blue-500 + var BG_COLOR = '#1E3A5F'; // Dark blue background + var BOUNCE_HEIGHT = 6; // Pixels to bounce + var FRAME_RATE = 60; // ms between frames + + /** + * Initialize canvas and get original favicon + */ + function init() { + if (canvas) return; + + canvas = document.createElement('canvas'); + canvas.width = SIZE; + canvas.height = SIZE; + ctx = canvas.getContext('2d'); + + // Find and store original favicon + linkElement = document.querySelector('link[rel="icon"][type="image/png"]') || + document.querySelector('link[rel="icon"]'); + + if (linkElement) { + originalFavicon = linkElement.href; + } + } + + /** + * Draw a frame of the bouncing arrow animation + * @param {number} offset - Vertical bounce offset + */ + function drawFrame(offset) { + // Clear canvas + ctx.clearRect(0, 0, SIZE, SIZE); + + // Draw circular background + ctx.beginPath(); + ctx.arc(SIZE / 2, SIZE / 2, SIZE / 2 - 1, 0, Math.PI * 2); + ctx.fillStyle = BG_COLOR; + ctx.fill(); + + // Calculate arrow position with bounce + var centerX = SIZE / 2; + var centerY = SIZE / 2 + offset; + + // Draw downward arrow + ctx.beginPath(); + ctx.strokeStyle = ARROW_COLOR; + ctx.fillStyle = ARROW_COLOR; + ctx.lineWidth = 3; + ctx.lineCap = 'round'; + ctx.lineJoin = 'round'; + + // Arrow shaft + ctx.moveTo(centerX, centerY - 8); + ctx.lineTo(centerX, centerY + 4); + ctx.stroke(); + + // Arrow head (triangle) + ctx.beginPath(); + ctx.moveTo(centerX - 6, centerY); + ctx.lineTo(centerX, centerY + 8); + ctx.lineTo(centerX + 6, centerY); + ctx.closePath(); + ctx.fill(); + + // Update favicon + if (linkElement) { + linkElement.href = canvas.toDataURL('image/png'); + } + } + + /** + * Animation loop using eased bounce + */ + function animate() { + if (!isAnimating) return; + + // Eased bounce: sin wave for smooth up/down motion + var bounce = Math.sin(frame * 0.15) * BOUNCE_HEIGHT; + drawFrame(bounce); + + frame++; + animationId = setTimeout(animate, FRAME_RATE); + } + + /** + * Start the favicon animation + */ + function start() { + if (isAnimating) return; + + init(); + isAnimating = true; + frame = 0; + animate(); + console.log('Favicon animation started'); + } + + /** + * Stop the animation and restore original favicon + */ + function stop() { + if (!isAnimating) return; + + isAnimating = false; + + if (animationId) { + clearTimeout(animationId); + animationId = null; + } + + // Restore original favicon + if (linkElement && originalFavicon) { + linkElement.href = originalFavicon; + } + + console.log('Favicon animation stopped'); + } + + // Expose API + window.BigDump = window.BigDump || {}; + window.BigDump.faviconAnimator = { + start: start, + stop: stop, + isAnimating: function() { return isAnimating; } + }; + +})(); diff --git a/assets/src/css/tailwind.css b/assets/src/css/tailwind.css index 3234c81..44ac9de 100644 --- a/assets/src/css/tailwind.css +++ b/assets/src/css/tailwind.css @@ -817,6 +817,120 @@ display: flex; } +/* ======================================== + SKELETON LOADER + ======================================== */ + +.skeleton { + display: inline-block; + height: 1em; + min-width: 3em; + background: linear-gradient( + 90deg, + rgba(156, 163, 175, 0.3) 25%, + rgba(156, 163, 175, 0.5) 50%, + rgba(156, 163, 175, 0.3) 75% + ); + background-size: 200% 100%; + animation: skeleton-shimmer 1.5s ease-in-out infinite; + border-radius: 0.25rem; + vertical-align: middle; +} + +[data-theme="dark"] .skeleton { + background: linear-gradient( + 90deg, + rgba(75, 85, 99, 0.4) 25%, + rgba(75, 85, 99, 0.7) 50%, + rgba(75, 85, 99, 0.4) 75% + ); + background-size: 200% 100%; +} + +@keyframes skeleton-shimmer { + 0% { background-position: 200% 0; } + 100% { background-position: -200% 0; } +} + +/* ======================================== + PROGRESS BAR COMPLETION ANIMATION + ======================================== */ + +.progress-bar-complete { + background: linear-gradient(90deg, #22c55e, #16a34a, #4ade80, #22c55e) !important; + background-size: 300% 100% !important; + animation: progress-complete-glow 2s ease-in-out infinite !important; + box-shadow: 0 0 20px rgba(34, 197, 94, 0.6), + 0 0 40px rgba(34, 197, 94, 0.4), + inset 0 0 10px rgba(255, 255, 255, 0.2) !important; +} + +@keyframes progress-complete-glow { + 0%, 100% { + background-position: 0% 50%; + box-shadow: 0 0 20px rgba(34, 197, 94, 0.6), + 0 0 40px rgba(34, 197, 94, 0.4), + inset 0 0 10px rgba(255, 255, 255, 0.2); + } + 50% { + background-position: 100% 50%; + box-shadow: 0 0 30px rgba(34, 197, 94, 0.8), + 0 0 60px rgba(34, 197, 94, 0.5), + inset 0 0 15px rgba(255, 255, 255, 0.3); + } +} + +/* Dark mode adjustment for completion glow */ +.dark .progress-bar-complete { + box-shadow: 0 0 25px rgba(74, 222, 128, 0.7), + 0 0 50px rgba(74, 222, 128, 0.5), + inset 0 0 10px rgba(255, 255, 255, 0.15) !important; +} + +/* Success message fade-in animation */ +.animate-fade-in { + animation: fade-in-up 0.5s ease-out forwards; +} + +@keyframes fade-in-up { + 0% { + opacity: 0; + transform: translateY(20px); + } + 100% { + opacity: 1; + transform: translateY(0); + } +} + +/* Success stat cards with enhanced dark mode */ +.stat-card-success { + background: linear-gradient(135deg, #dcfce7 0%, #bbf7d0 100%); + border-radius: 0.75rem; + padding: 1rem; + border: 1px solid rgba(34, 197, 94, 0.2); + transition: transform 0.2s ease, box-shadow 0.2s ease; +} + +.stat-card-success:hover { + transform: translateY(-2px); + box-shadow: 0 4px 12px rgba(34, 197, 94, 0.15); +} + +.dark .stat-card-success { + background: linear-gradient(135deg, rgba(34, 197, 94, 0.15) 0%, rgba(22, 163, 74, 0.25) 100%); + border: 1px solid rgba(74, 222, 128, 0.3); + box-shadow: 0 0 20px rgba(34, 197, 94, 0.1), + inset 0 1px 0 rgba(255, 255, 255, 0.05); +} + +.dark .stat-card-success:hover { + box-shadow: 0 0 25px rgba(34, 197, 94, 0.2), + 0 4px 15px rgba(0, 0, 0, 0.3), + inset 0 1px 0 rgba(255, 255, 255, 0.08); + border-color: rgba(74, 222, 128, 0.5); +} + /* ======================================== FILE UPLOAD ENHANCEMENTS ======================================== */ diff --git a/assets/src/js/celebration.js b/assets/src/js/celebration.js new file mode 100644 index 0000000..5d80a74 --- /dev/null +++ b/assets/src/js/celebration.js @@ -0,0 +1,466 @@ +/** + * BigDump - Celebration Effects Module (Enhanced) + * Creates realistic fireworks and confetti animations on import completion. + * Features: particle trails, glow effects, 3D confetti rotation, air resistance. + */ +(function() { + 'use strict'; + + var canvas = null; + var ctx = null; + var animationId = null; + var particles = []; + var confetti = []; + var trails = []; + var isRunning = false; + var startTime = 0; + var fireworksDuration = 20000; // 20 seconds of fireworks + var confettiForever = true; // Confetti falls indefinitely + + // Vibrant colors for fireworks + var fireworkPalettes = [ + ['#FF6B6B', '#FF8E8E', '#FFB4B4'], // Red + ['#4ECDC4', '#7EDCD6', '#A8EBE7'], // Teal + ['#45B7D1', '#6FC9DE', '#99DBEB'], // Blue + ['#FFD93D', '#FFE469', '#FFEF9A'], // Yellow + ['#6BCB77', '#8DD896', '#AFE5B5'], // Green + ['#FF8C00', '#FFA533', '#FFBE66'], // Orange + ['#DA70D6', '#E494E0', '#EEB8EA'], // Orchid + ['#9B59B6', '#B07CC6', '#C59FD6'], // Purple + ['#E74C3C', '#ED7669', '#F3A096'], // Coral + ['#3498DB', '#5DADE2', '#85C1E9'] // Sky blue + ]; + + var confettiColors = [ + '#FF6B6B', '#4ECDC4', '#45B7D1', '#FFD93D', '#6BCB77', + '#FF8C00', '#DA70D6', '#9B59B6', '#E74C3C', '#3498DB', + '#F39C12', '#1ABC9C', '#E91E63', '#00BCD4', '#8BC34A' + ]; + + /** + * Enhanced Particle class with trails and glow + */ + function Particle(x, y, color, velocity, options) { + options = options || {}; + this.x = x; + this.y = y; + this.color = color; + this.velocity = velocity; + this.gravity = options.gravity || 0.06; + this.decay = options.decay || 0.015; + this.alpha = 1; + this.size = options.size || 3; + this.hasTrail = options.hasTrail !== false; + this.glowSize = options.glowSize || this.size * 2; + this.friction = options.friction || 0.98; + this.trail = []; + this.maxTrailLength = options.trailLength || 5; + } + + Particle.prototype.update = function() { + // Store trail position + if (this.hasTrail && this.alpha > 0.3) { + this.trail.push({ x: this.x, y: this.y, alpha: this.alpha }); + if (this.trail.length > this.maxTrailLength) { + this.trail.shift(); + } + } + + this.velocity.x *= this.friction; + this.velocity.y *= this.friction; + this.velocity.y += this.gravity; + this.x += this.velocity.x; + this.y += this.velocity.y; + this.alpha -= this.decay; + return this.alpha > 0; + }; + + Particle.prototype.draw = function(ctx) { + // Draw trail + if (this.hasTrail && this.trail.length > 0) { + for (var i = 0; i < this.trail.length; i++) { + var t = this.trail[i]; + var trailAlpha = (i / this.trail.length) * this.alpha * 0.5; + var trailSize = this.size * (0.3 + (i / this.trail.length) * 0.5); + ctx.save(); + ctx.globalAlpha = trailAlpha; + ctx.beginPath(); + ctx.arc(t.x, t.y, trailSize, 0, Math.PI * 2); + ctx.fillStyle = this.color; + ctx.fill(); + ctx.restore(); + } + } + + // Draw glow + ctx.save(); + ctx.globalAlpha = this.alpha * 0.4; + ctx.beginPath(); + ctx.arc(this.x, this.y, this.glowSize, 0, Math.PI * 2); + var gradient = ctx.createRadialGradient(this.x, this.y, 0, this.x, this.y, this.glowSize); + gradient.addColorStop(0, this.color); + gradient.addColorStop(1, 'transparent'); + ctx.fillStyle = gradient; + ctx.fill(); + ctx.restore(); + + // Draw core + ctx.save(); + ctx.globalAlpha = this.alpha; + ctx.beginPath(); + ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2); + ctx.fillStyle = this.color; + ctx.fill(); + + // Bright center + ctx.beginPath(); + ctx.arc(this.x, this.y, this.size * 0.5, 0, Math.PI * 2); + ctx.fillStyle = '#FFFFFF'; + ctx.globalAlpha = this.alpha * 0.8; + ctx.fill(); + ctx.restore(); + }; + + /** + * Enhanced Confetti with 3D rotation and air resistance + */ + function ConfettiPiece(x, y, color) { + this.x = x; + this.y = y; + this.color = color; + this.width = Math.random() * 12 + 6; + this.height = Math.random() * 8 + 4; + this.velocity = { + x: (Math.random() - 0.5) * 8, + y: Math.random() * 2 + 1 + }; + // 3D rotation angles + this.rotationX = Math.random() * 360; + this.rotationY = Math.random() * 360; + this.rotationZ = Math.random() * 360; + this.rotationSpeedX = (Math.random() - 0.5) * 8; + this.rotationSpeedY = (Math.random() - 0.5) * 12; + this.rotationSpeedZ = (Math.random() - 0.5) * 6; + // Oscillation (swaying) + this.oscillationSpeed = Math.random() * 0.08 + 0.03; + this.oscillationDistance = Math.random() * 60 + 30; + this.initialX = x; + this.time = Math.random() * 100; + // Air resistance + this.drag = 0.98 + Math.random() * 0.015; + this.terminalVelocity = 4 + Math.random() * 2; + } + + ConfettiPiece.prototype.update = function(canvasHeight) { + this.time += this.oscillationSpeed; + + // Horizontal oscillation (swaying in wind) + this.x = this.initialX + Math.sin(this.time) * this.oscillationDistance; + this.initialX += this.velocity.x * 0.1; // Drift + + // Apply drag + this.velocity.x *= this.drag; + this.velocity.y += 0.08; // Gravity + + // Terminal velocity + if (this.velocity.y > this.terminalVelocity) { + this.velocity.y = this.terminalVelocity; + } + + this.y += this.velocity.y; + + // Tumbling rotation + this.rotationX += this.rotationSpeedX; + this.rotationY += this.rotationSpeedY; + this.rotationZ += this.rotationSpeedZ; + + // Slow down rotation over time + this.rotationSpeedX *= 0.999; + this.rotationSpeedY *= 0.999; + + return this.y < canvasHeight + 50; + }; + + ConfettiPiece.prototype.draw = function(ctx) { + ctx.save(); + ctx.translate(this.x, this.y); + + // Apply 3D-like rotation (fake perspective) + var scaleX = Math.cos(this.rotationY * Math.PI / 180); + var scaleY = Math.cos(this.rotationX * Math.PI / 180); + ctx.rotate(this.rotationZ * Math.PI / 180); + ctx.scale(scaleX, scaleY); + + // Draw confetti piece with slight shadow + ctx.shadowColor = 'rgba(0,0,0,0.2)'; + ctx.shadowBlur = 2; + ctx.shadowOffsetY = 1; + + ctx.fillStyle = this.color; + ctx.fillRect(-this.width / 2, -this.height / 2, this.width, this.height); + + // Add shine effect + ctx.fillStyle = 'rgba(255,255,255,0.3)'; + ctx.fillRect(-this.width / 2, -this.height / 2, this.width * 0.4, this.height); + + ctx.restore(); + }; + + /** + * Create an enhanced firework explosion + */ + function createFirework(x, y) { + var palette = fireworkPalettes[Math.floor(Math.random() * fireworkPalettes.length)]; + var particleCount = 60 + Math.floor(Math.random() * 40); + var burstSpeed = 4 + Math.random() * 3; + + // Main explosion particles + for (var i = 0; i < particleCount; i++) { + var angle = (Math.PI * 2 / particleCount) * i + (Math.random() - 0.5) * 0.2; + var speed = burstSpeed * (0.6 + Math.random() * 0.8); + var colorIndex = Math.floor(Math.random() * palette.length); + var velocity = { + x: Math.cos(angle) * speed, + y: Math.sin(angle) * speed + }; + particles.push(new Particle(x, y, palette[colorIndex], velocity, { + gravity: 0.05, + decay: 0.010 + Math.random() * 0.008, + size: 2 + Math.random() * 2, + trailLength: 4, + glowSize: 6 + })); + } + + // Inner ring (brighter, faster decay) + var innerCount = 20 + Math.floor(Math.random() * 15); + for (var j = 0; j < innerCount; j++) { + var innerAngle = Math.random() * Math.PI * 2; + var innerSpeed = burstSpeed * 1.3 * Math.random(); + var innerVelocity = { + x: Math.cos(innerAngle) * innerSpeed, + y: Math.sin(innerAngle) * innerSpeed + }; + particles.push(new Particle(x, y, '#FFFFFF', innerVelocity, { + gravity: 0.04, + decay: 0.025, + size: 1.5, + trailLength: 3, + glowSize: 4, + hasTrail: true + })); + } + + // Sparkle particles (twinkle effect) + var sparkleCount = 15 + Math.floor(Math.random() * 10); + for (var k = 0; k < sparkleCount; k++) { + var sparkAngle = Math.random() * Math.PI * 2; + var sparkSpeed = burstSpeed * 0.3 + Math.random() * burstSpeed * 0.8; + var sparkVelocity = { + x: Math.cos(sparkAngle) * sparkSpeed, + y: Math.sin(sparkAngle) * sparkSpeed + }; + particles.push(new Particle(x, y, '#FFFACD', sparkVelocity, { + gravity: 0.08, + decay: 0.03 + Math.random() * 0.02, + size: 1, + glowSize: 3, + hasTrail: false + })); + } + } + + /** + * Create confetti burst from top + */ + function createConfettiBurst(count) { + if (!canvas) return; + + for (var i = 0; i < count; i++) { + var x = Math.random() * canvas.width; + var color = confettiColors[Math.floor(Math.random() * confettiColors.length)]; + confetti.push(new ConfettiPiece(x, -20 - Math.random() * 50, color)); + } + } + + /** + * Create confetti cannon from sides + */ + function createConfettiCannon(fromLeft) { + if (!canvas) return; + + var startX = fromLeft ? -10 : canvas.width + 10; + var angle = fromLeft ? -Math.PI / 4 : -Math.PI * 3 / 4; + + for (var i = 0; i < 30; i++) { + var spread = (Math.random() - 0.5) * 0.5; + var speed = 8 + Math.random() * 6; + var color = confettiColors[Math.floor(Math.random() * confettiColors.length)]; + var piece = new ConfettiPiece(startX, canvas.height * 0.7, color); + piece.velocity.x = Math.cos(angle + spread) * speed; + piece.velocity.y = Math.sin(angle + spread) * speed; + piece.initialX = startX; + confetti.push(piece); + } + } + + /** + * Initialize canvas + */ + function init() { + if (canvas) return; + + canvas = document.createElement('canvas'); + canvas.id = 'celebration-canvas'; + canvas.style.cssText = 'position: fixed; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none; z-index: 9999;'; + document.body.appendChild(canvas); + + ctx = canvas.getContext('2d'); + resizeCanvas(); + + window.addEventListener('resize', resizeCanvas); + } + + /** + * Resize canvas to match window + */ + function resizeCanvas() { + if (!canvas) return; + canvas.width = window.innerWidth; + canvas.height = window.innerHeight; + } + + /** + * Animation loop + */ + function animate() { + if (!isRunning) return; + + var elapsed = Date.now() - startTime; + + // Clear canvas with slight fade for trail effect + ctx.fillStyle = 'rgba(0, 0, 0, 0.1)'; + ctx.fillRect(0, 0, canvas.width, canvas.height); + ctx.clearRect(0, 0, canvas.width, canvas.height); + + // Update and draw particles + particles = particles.filter(function(p) { + var alive = p.update(); + if (alive) p.draw(ctx); + return alive; + }); + + // Update and draw confetti + confetti = confetti.filter(function(c) { + var alive = c.update(canvas.height); + if (alive) c.draw(ctx); + return alive; + }); + + // Create new fireworks periodically for 20 seconds + if (elapsed < fireworksDuration) { + // More frequent fireworks at start, gradually decreasing + var fireworkChance = elapsed < 5000 ? 0.08 : (elapsed < 10000 ? 0.05 : 0.03); + if (Math.random() < fireworkChance) { + var x = Math.random() * canvas.width * 0.7 + canvas.width * 0.15; + var y = Math.random() * canvas.height * 0.4 + canvas.height * 0.08; + createFirework(x, y); + } + } + + // Add confetti continuously forever (or until stopped) + if (confettiForever && Math.random() < 0.15) { + createConfettiBurst(1); + } + + // Confetti cannons at specific intervals during first 15 seconds + if (elapsed < 15000) { + var cannonInterval = 2000; // Every 2 seconds + var cannonWindow = 30; + if (elapsed % cannonInterval < cannonWindow && Math.random() < 0.5) { + createConfettiCannon(Math.random() < 0.5); + } + } + + // Continue animation as long as running (confetti forever!) + if (isRunning) { + animationId = requestAnimationFrame(animate); + } else { + cleanup(); + } + } + + /** + * Clean up canvas and reset state + */ + function cleanup() { + isRunning = false; + if (animationId) { + cancelAnimationFrame(animationId); + animationId = null; + } + if (canvas && canvas.parentNode) { + canvas.parentNode.removeChild(canvas); + } + canvas = null; + ctx = null; + particles = []; + confetti = []; + trails = []; + window.removeEventListener('resize', resizeCanvas); + } + + /** + * Start the celebration effects + */ + function start() { + if (isRunning) return; + + init(); + isRunning = true; + startTime = Date.now(); + particles = []; + confetti = []; + trails = []; + + // Initial burst of fireworks (staggered) + var fireworkPositions = [ + { x: 0.3, y: 0.25, delay: 0 }, + { x: 0.7, y: 0.2, delay: 150 }, + { x: 0.5, y: 0.15, delay: 300 }, + { x: 0.25, y: 0.3, delay: 450 }, + { x: 0.75, y: 0.28, delay: 600 } + ]; + + fireworkPositions.forEach(function(pos) { + setTimeout(function() { + if (!isRunning || !canvas) return; + createFirework(canvas.width * pos.x, canvas.height * pos.y); + }, pos.delay); + }); + + // Initial confetti burst + createConfettiBurst(80); + + animate(); + console.log('Celebration started!'); + } + + /** + * Stop the celebration effects immediately + */ + function stop() { + if (!isRunning) return; + cleanup(); + console.log('Celebration stopped'); + } + + // Expose API + window.BigDump = window.BigDump || {}; + window.BigDump.celebration = { + start: start, + stop: stop, + isRunning: function() { return isRunning; } + }; + +})(); diff --git a/assets/src/js/favicon-animator.js b/assets/src/js/favicon-animator.js new file mode 100644 index 0000000..7a03b6c --- /dev/null +++ b/assets/src/js/favicon-animator.js @@ -0,0 +1,145 @@ +/** + * BigDump - Animated Favicon Module + * Creates a bouncing arrow animation during import process. + * Uses Canvas API to dynamically generate favicon frames. + */ +(function() { + 'use strict'; + + var canvas = null; + var ctx = null; + var animationId = null; + var originalFavicon = null; + var linkElement = null; + var frame = 0; + var isAnimating = false; + + // Animation settings + var SIZE = 32; // Favicon size + var ARROW_COLOR = '#3B82F6'; // Blue-500 + var BG_COLOR = '#1E3A5F'; // Dark blue background + var BOUNCE_HEIGHT = 6; // Pixels to bounce + var FRAME_RATE = 60; // ms between frames + + /** + * Initialize canvas and get original favicon + */ + function init() { + if (canvas) return; + + canvas = document.createElement('canvas'); + canvas.width = SIZE; + canvas.height = SIZE; + ctx = canvas.getContext('2d'); + + // Find and store original favicon + linkElement = document.querySelector('link[rel="icon"][type="image/png"]') || + document.querySelector('link[rel="icon"]'); + + if (linkElement) { + originalFavicon = linkElement.href; + } + } + + /** + * Draw a frame of the bouncing arrow animation + * @param {number} offset - Vertical bounce offset + */ + function drawFrame(offset) { + // Clear canvas + ctx.clearRect(0, 0, SIZE, SIZE); + + // Draw circular background + ctx.beginPath(); + ctx.arc(SIZE / 2, SIZE / 2, SIZE / 2 - 1, 0, Math.PI * 2); + ctx.fillStyle = BG_COLOR; + ctx.fill(); + + // Calculate arrow position with bounce + var centerX = SIZE / 2; + var centerY = SIZE / 2 + offset; + + // Draw downward arrow + ctx.beginPath(); + ctx.strokeStyle = ARROW_COLOR; + ctx.fillStyle = ARROW_COLOR; + ctx.lineWidth = 3; + ctx.lineCap = 'round'; + ctx.lineJoin = 'round'; + + // Arrow shaft + ctx.moveTo(centerX, centerY - 8); + ctx.lineTo(centerX, centerY + 4); + ctx.stroke(); + + // Arrow head (triangle) + ctx.beginPath(); + ctx.moveTo(centerX - 6, centerY); + ctx.lineTo(centerX, centerY + 8); + ctx.lineTo(centerX + 6, centerY); + ctx.closePath(); + ctx.fill(); + + // Update favicon + if (linkElement) { + linkElement.href = canvas.toDataURL('image/png'); + } + } + + /** + * Animation loop using eased bounce + */ + function animate() { + if (!isAnimating) return; + + // Eased bounce: sin wave for smooth up/down motion + var bounce = Math.sin(frame * 0.15) * BOUNCE_HEIGHT; + drawFrame(bounce); + + frame++; + animationId = setTimeout(animate, FRAME_RATE); + } + + /** + * Start the favicon animation + */ + function start() { + if (isAnimating) return; + + init(); + isAnimating = true; + frame = 0; + animate(); + console.log('Favicon animation started'); + } + + /** + * Stop the animation and restore original favicon + */ + function stop() { + if (!isAnimating) return; + + isAnimating = false; + + if (animationId) { + clearTimeout(animationId); + animationId = null; + } + + // Restore original favicon + if (linkElement && originalFavicon) { + linkElement.href = originalFavicon; + } + + console.log('Favicon animation stopped'); + } + + // Expose API + window.BigDump = window.BigDump || {}; + window.BigDump.faviconAnimator = { + start: start, + stop: stop, + isAnimating: function() { return isAnimating; } + }; + +})(); diff --git a/src/Core/Application.php b/src/Core/Application.php index d2be40a..f1ca042 100644 --- a/src/Core/Application.php +++ b/src/Core/Application.php @@ -23,7 +23,7 @@ class Application /** * Application version. */ - public const VERSION = '2.26'; + public const VERSION = '2.27'; /** * Configuration instance. diff --git a/src/Services/AjaxService.php b/src/Services/AjaxService.php index 3d84edb..b2cee2b 100644 --- a/src/Services/AjaxService.php +++ b/src/Services/AjaxService.php @@ -164,55 +164,63 @@ function displayErrorInPage(message, stats) { * @param {object} stats Statistics object */ function displaySuccessInPage(stats) { + // Start celebration effects (fireworks + confetti) + if (window.BigDump && window.BigDump.celebration) { + window.BigDump.celebration.start(); + } + var queriesDone = stats && stats.queries_done ? stats.queries_done.toLocaleString() : '0'; var linesDone = stats && stats.lines_done ? stats.lines_done.toLocaleString() : '0'; var bytesDone = stats && stats.bytes_done ? formatBytes(stats.bytes_done) : '0 B'; var elapsed = document.getElementById('elapsedTime'); var elapsedTime = elapsed ? elapsed.textContent : '00:00:00'; - // Create success HTML with Tailwind classes - var successHtml = '