"use client"; import { useActionState, useEffect, useState, type ReactNode } from "react"; import type { FormState } from "@/app/actions/types"; import { UndoIcon, UploadIcon } from "./icons"; import { PlateBody } from "./settings-plate"; import { Alert, Button, buttonClass } from "./ui"; /** * Uploads a picture (a repository's image, an organization's avatar) with a preview of the chosen * file before it's saved, and offers to remove the one there is. */ export function ImageUploadForm({ action, resetAction, hasImage, noun, resetLabel, description, renderImage, }: { action: (prev: FormState, formData: FormData) => Promise; resetAction: () => Promise; hasImage: boolean; /** "Image" or "Avatar", for the buttons. */ noun: string; resetLabel: string; description: ReactNode; /** The picture as it's shown, or with `src` (the chosen file) instead. */ renderImage: (src?: string) => ReactNode; }) { const [preview, setPreview] = useState(null); const [state, formAction, pending] = useActionState(async (prev: FormState, formData: FormData) => { const result = await action(prev, formData); if (result?.success) setPreview(null); return result; }, undefined); useEffect(() => () => { if (preview) URL.revokeObjectURL(preview); }, [preview]); return (
{renderImage(preview ?? undefined)}

{description} PNG, JPEG, GIF or WebP, up to 1 MB.

{/* The file input stays a real input; its label is drawn as MetalUI's standard cap. */} {preview && ( )}
{hasImage && !preview && (
)}
{state?.error && {state.error}} {state?.success && !preview && {state.success}}
); }