feat: add WebP migration UI and API endpoint

- Add /api/admin/migrate-images endpoint to convert JPEG/PNG to WebP
- Add WebPMigration component in admin gallery showing stats
- Upload endpoint already converts new images to WebP
- Migration saves ~30-50% storage space per image
This commit is contained in:
2026-07-01 21:47:16 -05:00
parent d0be23551a
commit ee68dc2897
6 changed files with 916 additions and 0 deletions
+56
View File
@@ -145,6 +145,61 @@ function Empty({ icon, title, desc }: any) {
);
}
/* ─── WebP Migration ─── */
function WebPMigration({ onToast, onRefresh }: { onToast: (msg: string, type: string) => void; onRefresh: () => void }) {
const [migrating, setMigrating] = useState(false);
const [stats, setStats] = useState<{ totalImages: number; totalSizeMB: number } | null>(null);
useEffect(() => {
fetch('/api/admin/migrate-images')
.then(res => res.ok ? res.json() : null)
.then(data => data && setStats({ totalImages: data.totalImages, totalSizeMB: data.totalSizeMB }))
.catch(() => {});
}, []);
const runMigration = async () => {
setMigrating(true);
try {
const res = await fetch('/api/admin/migrate-images', { method: 'POST' });
const data = await res.json();
if (res.ok) {
onToast(`Converted ${data.converted} images, saved ${data.totalSavedMB} MB`, 'success');
onRefresh();
} else {
onToast(data.error || 'Migration failed', 'error');
}
} catch {
onToast('Migration failed', 'error');
}
setMigrating(false);
};
return (
<div style={{
marginBottom: '16px',
padding: '12px 16px',
background: '#fff',
borderRadius: '12px',
border: `1px solid ${C.border}`,
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
flexWrap: 'wrap',
gap: '12px',
}}>
<div>
<div style={{ fontWeight: 600, color: C.navy, fontSize: '14px' }}>🔄 WebP Migration</div>
<div style={{ fontSize: '12px', color: C.textLight }}>
{stats ? `${stats.totalImages} images · ${stats.totalSizeMB} MB total` : 'Checking...'}
</div>
</div>
<Btn onClick={runMigration} disabled={migrating} size="sm">
{migrating ? 'Migrating...' : 'Convert to WebP'}
</Btn>
</div>
);
}
function SecHead({ title, count, action }: any) {
return (
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '16px', flexWrap: 'wrap', gap: '10px' }}>
@@ -2462,6 +2517,7 @@ export default function AdminPage() {
case 'gallery': return (
<div>
<SecHead title="🖼️ Image Gallery" count={images.length} />
<WebPMigration onToast={showToast} onRefresh={loadImages} />
<ImageGallery images={images} onDelete={deleteImage} />
</div>
);