import { useState, useEffect } from "react"; import SectionCard from "./SectionCard"; import { getAjaxUrl, getNonce } from "../../utils/api"; import { getAdminConfig } from "../../utils/helpers"; export default function ImageSizesSection() { const config = getAdminConfig(); const [sizes, setSizes] = useState(config.image_sizes || []); const [loading, setLoading] = useState(false); const [formData, setFormData] = useState({ name: "", width: "", height: "", crop: false, }); const [message, setMessage] = useState({ type: null, text: "" }); // Keep the dimensions inside the documented range as they are typed, matching // how the other numeric fields in this dashboard behave. The browser also // blocks submission via min/max, and the server re-checks. const clampSize = (raw, max) => { if (raw === "") return ""; const value = parseInt(raw, 10); if (Number.isNaN(value)) return ""; return Math.min(max, Math.max(1, value)); }; const handleAdd = async (e) => { e.preventDefault(); setMessage({ type: null, text: "" }); if (!formData.name.trim() || !formData.width || !formData.height) { setMessage({ type: "error", text: "Please fill all fields." }); return; } // Validate reserved WordPress names const reserved = ["thumbnail", "medium", "medium_large", "medium-large", "large"]; const sanitizedName = formData.name.trim().toLowerCase().replace(/[^a-z0-9_-]/g, "-"); if (reserved.includes(sanitizedName)) { setMessage({ type: "error", text: "This name is reserved by WordPress. Please use a different name." }); return; } // Check for duplicate names if (sizes.some(size => size.name === sanitizedName)) { setMessage({ type: "error", text: "A size with this name already exists. Please use a different name." }); return; } // Validate maximum dimensions if (parseInt(formData.width) > 1920) { setMessage({ type: "error", text: "Width must not exceed 1920 pixels." }); return; } if (parseInt(formData.height) > 3000) { setMessage({ type: "error", text: "Height must not exceed 3000 pixels." }); return; } setLoading(true); try { const fd = new FormData(); fd.append("action", "ai_image_add_custom_size"); fd.append("nonce", getNonce()); fd.append("name", formData.name.trim()); fd.append("width", formData.width); fd.append("height", formData.height); fd.append("crop", formData.crop ? "1" : "0"); const res = await fetch(getAjaxUrl(), { method: "POST", body: fd }); const data = await res.json(); if (data.success) { setMessage({ type: "success", text: data.data?.message || "Size added." }); setSizes(data.data?.sizes || []); setFormData({ name: "", width: "", height: "", crop: false }); } else { setMessage({ type: "error", text: data.data?.message || "Failed to add size." }); } } catch (error) { setMessage({ type: "error", text: error?.message || "Failed to add size." }); } finally { setLoading(false); } }; const handleDelete = async (name) => { if (!confirm(`Delete image size "${name}"?`)) return; setLoading(true); setMessage({ type: null, text: "" }); try { const fd = new FormData(); fd.append("action", "ai_image_delete_custom_size"); fd.append("nonce", getNonce()); fd.append("name", name); const res = await fetch(getAjaxUrl(), { method: "POST", body: fd }); const data = await res.json(); if (data.success) { setMessage({ type: "success", text: data.data?.message || "Size deleted." }); setSizes(data.data?.sizes || []); } else { setMessage({ type: "error", text: data.data?.message || "Failed to delete size." }); } } catch (error) { setMessage({ type: "error", text: error?.message || "Failed to delete size." }); } finally { setLoading(false); } }; // Auto-hide success message after 3 seconds useEffect(() => { if (message.type === "success") { const timer = setTimeout(() => { setMessage({ type: null, text: "" }); }, 3000); return () => clearTimeout(timer); } }, [message]); return (
setFormData((prev) => ({ ...prev, name: e.target.value }))} placeholder="custom-size" disabled={loading} className="block w-full rounded-md border border-gray-200 px-3 py-2 text-sm outline-none focus:border-gray-400 focus:ring-0 disabled:opacity-50 transition-colors" />
setFormData((prev) => ({ ...prev, width: clampSize(e.target.value, 1920) }))} placeholder="1200" disabled={loading} className="block w-full rounded-md border border-gray-200 px-3 py-2 text-sm outline-none focus:border-gray-400 focus:ring-0 disabled:opacity-50 transition-colors" />
setFormData((prev) => ({ ...prev, height: clampSize(e.target.value, 3000) }))} placeholder="900" disabled={loading} className="block w-full rounded-md border border-gray-200 px-3 py-2 text-sm outline-none focus:border-gray-400 focus:ring-0 disabled:opacity-50 transition-colors" />
{message.text && ( {message.text} )}

Name: Lowercase letters, numbers, hyphens, or underscores. Avoid reserved names (thumbnail, medium, large).

Width: Max 1920px. Height: Max 3000px.

Crop: Yes = crop to exact dimensions. No = resize proportionally (may be smaller).

{sizes.length === 0 ? ( ) : ( sizes.map((size) => ( )) )}
Label Name Width Height Crop Actions
No image sizes found.
{size.label} {size.name} {size.width || "—"} {size.height || "—"} {size.crop ? "Yes" : "No"} {size.source === "custom" ? ( ) : ( WordPress )}
); }