import React, { useState, useCallback, useEffect } from 'react'
import { Box, Container, Typography, Paper, Button, Slider, Stack, Grid, Card, CardContent, LinearProgress, IconButton, Tooltip, useTheme, ToggleButton, ToggleButtonGroup, Select, MenuItem, FormControl } from '@mui/material'
import { useDropzone } from 'react-dropzone'
import imageCompression from 'browser-image-compression'
import { toast } from 'react-toastify'
import { useTranslation } from 'react-i18next'
import CloudUploadIcon from '@mui/icons-material/CloudUpload'
import DownloadIcon from '@mui/icons-material/Download'
import DeleteIcon from '@mui/icons-material/Delete'
import InfoIcon from '@mui/icons-material/Info'
import CompareArrowsIcon from '@mui/icons-material/CompareArrows'
import LightModeIcon from '@mui/icons-material/LightMode'
import DarkModeIcon from '@mui/icons-material/DarkMode'
import DeleteOutlineIcon from '@mui/icons-material/DeleteOutline'
import LanguageIcon from '@mui/icons-material/Language'
import ReactMarkdown from 'react-markdown'
import './i18n'
const MAX_FILE_SIZE = 10 * 1024 * 1024 // 10MB
const ACCEPTED_TYPES = {
'image/jpeg': ['.jpg', '.jpeg'],
'image/png': ['.png'],
'image/gif': ['.gif'],
'image/svg+xml': ['.svg'],
'image/webp': ['.webp']
}
const COMPRESSION_OPTIONS = [
{ value: 0.1, label: '10%' }, // 10% quality = 90% compression
{ value: 0.25, label: '25%' }, // 25% quality = 75% compression
{ value: 0.5, label: '50%' }, // 50% quality = 50% compression
{ value: 0.75, label: '75%' }, // 75% quality = 25% compression
{ value: 0.9, label: '90%' }, // 90% quality = 10% compression
]
// Add a helper component for markdown text
const MarkdownText = ({ children, ...props }) => (
(
),
p: ({ node, ...props }) =>
}}
{...props}
>
{children}
)
function App({ mode, setMode }) {
const { t, i18n } = useTranslation()
const theme = useTheme()
const [files, setFiles] = useState([])
const [compressionLevel, setCompressionLevel] = useState(0.5)
const [isCompressing, setIsCompressing] = useState(false)
const [originalSizes, setOriginalSizes] = useState({})
const [autoDownload, setAutoDownload] = useState(true)
const [originalFiles, setOriginalFiles] = useState([])
const [compressionProgress, setCompressionProgress] = useState({})
const [language, setLanguage] = useState(i18n.language || 'en')
const [isLoading, setIsLoading] = useState(false)
const [compressionStats, setCompressionStats] = useState({
totalSize: 0,
savedSize: 0,
totalFiles: 0
})
const [previewUrls, setPreviewUrls] = useState({})
const [errors, setErrors] = useState({})
const languages = [
{ code: 'en', name: 'English' },
{ code: 'es', name: 'Español' },
{ code: 'fr', name: 'Français' },
{ code: 'hi', name: 'हिंदी' },
{ code: 'zh', name: '中文' },
{ code: 'ja', name: '日本語' },
{ code: 'ar', name: 'العربية' }
]
const handleLanguageChange = (event) => {
const newLang = event.target.value
setLanguage(newLang)
i18n.changeLanguage(newLang)
}
const onDrop = useCallback((acceptedFiles) => {
const validFiles = acceptedFiles.filter(file => file.size <= MAX_FILE_SIZE)
if (validFiles.length !== acceptedFiles.length) {
toast.error('Some files were too large (max 10MB)')
}
setFiles(validFiles)
setOriginalFiles(validFiles)
setErrors({})
// Store original sizes
const sizes = {}
validFiles.forEach(file => {
sizes[file.name] = file.size
})
setOriginalSizes(sizes)
// Create preview URLs
const previews = {}
validFiles.forEach(file => {
previews[file.name] = URL.createObjectURL(file)
})
setPreviewUrls(previews)
}, [])
const { getRootProps, getInputProps, isDragActive } = useDropzone({
onDrop,
accept: ACCEPTED_TYPES,
maxSize: MAX_FILE_SIZE,
multiple: true
})
// Cleanup preview URLs on unmount
useEffect(() => {
return () => {
Object.values(previewUrls).forEach(url => URL.revokeObjectURL(url))
}
}, [previewUrls])
const compressImage = async (file) => {
const quality = 1 - compressionLevel
const options = {
maxSizeMB: 1,
maxWidthOrHeight: 1920,
useWebWorker: true,
quality: quality,
fileType: file.type,
initialQuality: quality,
alwaysKeepResolution: true,
maxIteration: 5,
exifOrientation: -1,
checkOrientation: true,
preserveExif: false,
progressive: true,
chromaSubsampling: '4:2:0',
stripMetadata: true,
optimization: true,
progressiveLoading: true,
interlacing: true,
qualityBasedCompression: true,
sizeBasedCompression: true,
formatBasedCompression: true,
contentBasedCompression: true,
perceptualCompression: true,
losslessCompression: true,
lossyCompression: true,
hybridCompression: true,
adaptiveCompression: true,
dynamicCompression: true,
intelligentCompression: true,
smartCompression: true,
optimalCompression: true,
maximumCompression: true,
minimumCompression: true,
balancedCompression: true,
}
try {
const compressedFile = await imageCompression(file, options)
return compressedFile
} catch (error) {
console.error('Error compressing image:', error)
throw error
}
}
const handleCompressionChange = (_, value) => {
if (value !== null) {
setCompressionLevel(value)
}
}
const handleSliderChange = (_, value) => {
setCompressionLevel(value)
}
const handleCompress = async () => {
if (files.length === 0) {
toast.error('Please select files to compress')
return
}
setIsCompressing(true)
setIsLoading(true)
setCompressionProgress({})
setErrors({})
try {
const compressedFiles = await Promise.all(
originalFiles.map(async (file, index) => {
try {
setCompressionProgress(prev => ({ ...prev, [index]: 0 }))
const compressed = await compressImage(file)
setCompressionProgress(prev => ({ ...prev, [index]: 100 }))
return compressed
} catch (error) {
setErrors(prev => ({
...prev,
[file.name]: 'Failed to compress image. Please try again.'
}))
return null
}
})
)
const validCompressedFiles = compressedFiles.filter(file => file !== null)
if (validCompressedFiles.length === 0) {
toast.error('No files were compressed successfully')
return
}
setFiles(validCompressedFiles)
// Calculate compression stats
const totalOriginalSize = originalFiles.reduce((acc, file) => acc + file.size, 0)
const totalCompressedSize = validCompressedFiles.reduce((acc, file) => acc + file.size, 0)
setCompressionStats({
totalSize: totalOriginalSize,
savedSize: totalOriginalSize - totalCompressedSize,
totalFiles: validCompressedFiles.length
})
// Auto-download if enabled
if (autoDownload) {
validCompressedFiles.forEach((file, index) => {
const url = URL.createObjectURL(file)
const link = document.createElement('a')
const fileName = file.name.replace(/\.[^/.]+$/, '')
const fileExtension = file.name.split('.').pop()
const newFileName = `${fileName}.usingfreecompressor.${fileExtension}`
link.href = url
link.download = newFileName
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
URL.revokeObjectURL(url)
})
}
toast.success(`Successfully compressed ${validCompressedFiles.length} images!`)
} catch (error) {
console.error('Compression error:', error)
toast.error('Error compressing images')
} finally {
setIsCompressing(false)
setIsLoading(false)
setCompressionProgress({})
}
}
// Memoize expensive calculations
const calculateCompressionStats = useCallback((originalFiles, compressedFiles) => {
const totalOriginalSize = originalFiles.reduce((acc, file) => acc + file.size, 0)
const totalCompressedSize = compressedFiles.reduce((acc, file) => acc + file.size, 0)
return {
totalSize: totalOriginalSize,
savedSize: totalOriginalSize - totalCompressedSize,
totalFiles: compressedFiles.length
}
}, [])
// Optimize cleanup
useEffect(() => {
const cleanup = () => {
const urls = [...Object.values(previewUrls)]
urls.forEach(url => {
try {
URL.revokeObjectURL(url)
} catch (error) {
console.warn('Failed to revoke URL:', error)
}
})
}
window.addEventListener('beforeunload', cleanup)
return () => {
cleanup()
window.removeEventListener('beforeunload', cleanup)
}
}, [previewUrls])
const handleDownload = (file, index) => {
const url = URL.createObjectURL(file)
const link = document.createElement('a')
link.href = url
link.download = `compressed_${index + 1}_${file.name}`
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
URL.revokeObjectURL(url)
}
const handleDelete = (index) => {
const file = files[index]
if (previewUrls[file.name]) {
URL.revokeObjectURL(previewUrls[file.name])
}
setFiles(files.filter((_, i) => i !== index))
setOriginalFiles(originalFiles.filter((_, i) => i !== index))
setErrors(prev => {
const newErrors = { ...prev }
delete newErrors[file.name]
return newErrors
})
}
const 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]
}
const calculateReduction = (originalSize, currentSize) => {
return ((originalSize - currentSize) / originalSize * 100).toFixed(1)
}
return (
{/* Header with Theme and Language Controls */}
}
sx={{
bgcolor: mode === 'light' ? 'rgba(239, 68, 68, 0.1)' : 'rgba(59, 130, 246, 0.1)',
color: mode === 'light' ? '#1e293b' : '#fff',
'& .MuiOutlinedInput-notchedOutline': {
border: 'none',
},
'&:hover': {
bgcolor: mode === 'light' ? 'rgba(239, 68, 68, 0.2)' : 'rgba(59, 130, 246, 0.2)',
},
'& .MuiSelect-icon': {
color: mode === 'light' ? '#64748b' : '#94a3b8',
},
minWidth: '120px',
borderRadius: 2,
'& .MuiSelect-select': {
py: 1,
}
}}
>
{languages.map((lang) => (
))}
setMode(mode === 'light' ? 'dark' : 'light')}
sx={{
bgcolor: mode === 'light' ? 'rgba(239, 68, 68, 0.1)' : 'rgba(59, 130, 246, 0.1)',
'&:hover': {
bgcolor: mode === 'light' ? 'rgba(239, 68, 68, 0.2)' : 'rgba(59, 130, 246, 0.2)',
}
}}
>
{mode === 'light' ? : }
{t('title')}
{t('subtitle')}
}
{...getRootProps()}
sx={{
background: mode === 'light'
? 'linear-gradient(135deg, #ef4444 0%, #dc2626 100%)'
: 'linear-gradient(135deg, #3b82f6 0%, #2563eb 100%)',
'&:hover': {
background: mode === 'light'
? 'linear-gradient(135deg, #dc2626 0%, #b91c1c 100%)'
: 'linear-gradient(135deg, #2563eb 0%, #1d4ed8 100%)',
},
px: 6,
py: 2,
borderRadius: 2,
fontSize: '1.1rem'
}}
>
{t('uploadButton')}
{files.length > 0 && (
<>
{t('quickCompression')}
{COMPRESSION_OPTIONS.map((option) => (
{option.label}
))}
{t('fineTuneCompression')}
`${Math.round(value * 100)}%`}
sx={{
mb: 4,
'& .MuiSlider-markLabel': {
color: mode === 'light' ? 'text.secondary' : 'text.primary',
},
'& .MuiSlider-valueLabel': {
bgcolor: mode === 'light' ? 'primary.main' : 'secondary.main',
color: '#fff',
}
}}
/>
>
)}
{files.length > 0 && (
{compressionStats.totalFiles > 0 && (
{t('compressionSummary')}
{t('totalFiles')}
{compressionStats.totalFiles}
{t('totalSizeSaved')}
{formatFileSize(compressionStats.savedSize)}
{t('averageReduction')}
{((compressionStats.savedSize / compressionStats.totalSize) * 100).toFixed(1)}%
)}
0 ? 8 : 12}>
{t('selectedFiles')}
{files.map((file, index) => (
{compressionProgress[index] !== undefined && (
)}
{previewUrls[file.name] && (
)}
{file.name}
{errors[file.name] && (
{errors[file.name]}
)}
{t('original')}: {formatFileSize(originalSizes[file.name])}
{t('compressed')}: {formatFileSize(file.size)}
{calculateReduction(originalSizes[file.name], file.size)}% {t('smaller')}
{compressionProgress[index] === 100 && (
✓
)}
}
onClick={() => handleDownload(file, index)}
fullWidth
sx={{
background: mode === 'light'
? 'linear-gradient(135deg, #ef4444 0%, #dc2626 100%)'
: 'linear-gradient(135deg, #3b82f6 0%, #2563eb 100%)',
boxShadow: mode === 'light'
? '0 4px 12px rgba(239, 68, 68, 0.2)'
: '0 4px 12px rgba(59, 130, 246, 0.2)',
transition: 'all 0.3s ease-in-out',
'&:hover': {
background: mode === 'light'
? 'linear-gradient(135deg, #dc2626 0%, #b91c1c 100%)'
: 'linear-gradient(135deg, #2563eb 0%, #1d4ed8 100%)',
transform: 'translateY(-1px)'
}
}}
>
{t('download')}
}
onClick={() => handleDelete(index)}
fullWidth
sx={{
borderColor: mode === 'light' ? 'rgba(239, 68, 68, 0.5)' : 'rgba(239, 68, 68, 0.5)',
color: mode === 'light' ? '#ef4444' : '#ef4444',
'&:hover': {
borderColor: mode === 'light' ? '#ef4444' : '#ef4444',
background: 'rgba(239, 68, 68, 0.1)',
}
}}
>
{t('delete')}
))}
)}
{/* SEO Content Section */}
{t('howToTitle')}
{[
'compression',
'whyChoose',
'howItWorks',
'safety',
'features',
'usage'
].map((section) => (
{t(`sections.${section}.title`)}
{t(`sections.${section}.description`)}
{t(`sections.${section}.features`, { returnObjects: true }).map((feature, idx) => (
{feature}
))}
))}
{/* Footer with Copyright */}
{t('footer', { year: new Date().getFullYear() })}
)
}
export default App