Files
diplom-frontend/app/components/ui/icon-upload-field.tsx
2025-05-21 08:52:33 +03:00

166 lines
6.5 KiB
TypeScript

import { ImagePlus, RotateCcw, XCircle } from "lucide-react";
import React, { useCallback } from "react";
import type { ControllerRenderProps, FieldError } from "react-hook-form";
import { Button } from "./button";
// Props for our custom field component
interface IconUploadFieldProps {
field: ControllerRenderProps<any, string>; // field.value can be File | string (URL) | null | undefined
error?: FieldError;
accept?: string;
previewContainerClassName?: string;
defaultPreview?: string; // Visual fallback URL if field.value is initially undefined
formDefaultValue?: string | null | undefined; // The actual RHF default value for this field
}
export function IconUploadField({
field,
error,
accept = "image/*",
previewContainerClassName = "w-24 h-24 rounded-full",
defaultPreview,
formDefaultValue, // New prop
}: IconUploadFieldProps) {
const [previewUrl, setPreviewUrl] = React.useState<string | null>(null);
const fileInputRef = React.useRef<HTMLInputElement>(null);
React.useEffect(() => {
let objectUrlToRevoke: string | null = null;
if (field.value instanceof File) {
objectUrlToRevoke = URL.createObjectURL(field.value);
setPreviewUrl(objectUrlToRevoke);
} else if (typeof field.value === "string" && field.value) {
setPreviewUrl(field.value);
} else if (field.value === null) {
setPreviewUrl(null);
} else if (field.value === undefined && defaultPreview) {
// Show visual default prop if field value is undefined
setPreviewUrl(defaultPreview);
} else {
setPreviewUrl(null);
}
return () => {
if (objectUrlToRevoke) {
URL.revokeObjectURL(objectUrlToRevoke);
}
};
}, [field.value, defaultPreview]);
const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0];
field.onChange(file || undefined);
if (event.target) {
event.target.value = "";
}
};
const handleRemoveImage = useCallback(
(e: React.MouseEvent<HTMLButtonElement>) => {
e.preventDefault();
field.onChange(null); // Set RHF value to null
if (fileInputRef.current) {
fileInputRef.current.value = ""; // Clear the native file input
}
},
[field],
);
const handleResetToDefault = useCallback(
(e: React.MouseEvent<HTMLButtonElement>) => {
e.preventDefault();
// The button's visibility logic ensures formDefaultValue is relevant.
// This will set RHF value to string (URL), null, or undefined, as per formDefaultValue.
field.onChange(undefined);
if (fileInputRef.current) {
fileInputRef.current.value = ""; // Clear the native file input
}
},
[field, formDefaultValue],
);
const triggerFileInput = useCallback(() => {
fileInputRef.current?.click();
}, []);
// Visibility for the "Remove" button: show if there's a File or a URL string in field.value
const showRemoveButton =
(!!field.value && (field.value instanceof File || typeof field.value === "string")) ||
(formDefaultValue && field.value === undefined);
// Visibility for the "Reset to Default" button
const canReset = typeof formDefaultValue !== "undefined"; // Was a formDefaultValue prop provided?
// Is current value different from the formDefaultValue?
// (A File object is always different from a URL string/null/undefined default)
const isDifferentFromDefault =
(field.value instanceof File || (field.value !== formDefaultValue && field.value !== undefined)) &&
formDefaultValue;
const showResetButton = canReset && isDifferentFromDefault;
return (
<div className="flex items-center space-x-4">
<div
ref={field.ref}
className={`relative ${previewContainerClassName} border-2 ${
error ? "border-destructive" : "border-dashed border-muted-foreground"
} flex items-center justify-center cursor-pointer hover:border-primary transition-colors overflow-hidden`}
onClick={triggerFileInput}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
triggerFileInput();
}
}}
role="button"
tabIndex={0}
aria-label={previewUrl ? "Change icon" : "Upload icon"}
aria-invalid={!!error}
aria-describedby={error ? `${field.name}-error` : undefined}
>
{previewUrl ? (
<img src={previewUrl} alt="Icon preview" className="w-full h-full object-cover" />
) : (
<ImagePlus className={`w-10 h-10 ${error ? "text-destructive" : "text-muted-foreground"}`} />
)}
<input
type="file"
ref={fileInputRef}
className="hidden"
accept={accept}
onChange={handleFileChange}
onBlur={field.onBlur}
name={field.name}
/>
</div>
<div className="flex flex-col space-y-2">
{/* Optional: A separate button to trigger file input, if needed. */}
{/* <Button type="button" variant="outline" size="sm" onClick={triggerFileInput}>
{previewUrl ? "Change Icon" : "Upload Icon"}
</Button> */}
{showRemoveButton && (
<Button type="button" variant="destructive" size="sm" onClick={handleRemoveImage}>
<XCircle className="mr-2 h-4 w-4" />
Remove
</Button>
)}
{showResetButton && (
<Button
type="button"
variant="outline" // Choose a suitable variant
size="sm"
onClick={handleResetToDefault}
>
<RotateCcw className="mr-2 h-4 w-4" />
Reset to Default
</Button>
)}
</div>
</div>
);
}