- Updated import paths for various components to align with new directory structure. - Changed imports from 'row-actions', 'confirm-dialog', 'image-preview-button', and 'file-upload' to their respective new locations in 'data-display', 'dialogs', and 'inputs'. - Adjusted imports in multiple pages including purchase, restock, transaction, category, customer, product, raw-material, supplier, roles, and settings. - Ensured all relevant components are imported from their new locations to maintain functionality.
239 lines
7.1 KiB
TypeScript
239 lines
7.1 KiB
TypeScript
import { FileImage, Upload, X } from 'lucide-react';
|
|
import { useEffect, useRef, useState, useId } from 'react';
|
|
import {
|
|
Attachment,
|
|
AttachmentAction,
|
|
AttachmentActions,
|
|
AttachmentContent,
|
|
AttachmentDescription,
|
|
AttachmentMedia,
|
|
AttachmentTitle,
|
|
AttachmentTrigger,
|
|
} from '@/components/ui/attachment';
|
|
import { uploadFile, UploadError } from '@/lib/upload';
|
|
|
|
type FileUploadProps = {
|
|
value: string | null;
|
|
onChange: (key: string | null) => void;
|
|
folder?: string;
|
|
accept?: string;
|
|
maxSize?: number;
|
|
onUploadingChange?: (uploading: boolean) => void;
|
|
existingUrl?: string | null;
|
|
onFileMeta?: (meta: { size: number; type: string } | null) => void;
|
|
};
|
|
|
|
function formatFileSize(bytes: number): string {
|
|
if (bytes < 1024) {
|
|
return `${bytes} B`;
|
|
}
|
|
|
|
if (bytes < 1024 * 1024) {
|
|
return `${(bytes / 1024).toFixed(1)} KB`;
|
|
}
|
|
|
|
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
}
|
|
|
|
function acceptToLabels(accept: string): string[] {
|
|
const mimeMap: Record<string, string> = {
|
|
'image/jpeg': 'JPG',
|
|
'image/png': 'PNG',
|
|
'image/webp': 'WebP',
|
|
'image/gif': 'GIF',
|
|
'image/svg+xml': 'SVG',
|
|
'application/pdf': 'PDF',
|
|
'video/mp4': 'MP4',
|
|
'application/zip': 'ZIP',
|
|
};
|
|
|
|
return accept
|
|
.split(',')
|
|
.map(
|
|
(mime) =>
|
|
mimeMap[mime.trim()] ||
|
|
mime.trim().split('/').pop()?.toUpperCase() ||
|
|
'File',
|
|
)
|
|
.filter((v, i, a) => a.indexOf(v) === i);
|
|
}
|
|
|
|
function formatMaxSize(bytes: number): string {
|
|
if (bytes < 1024 * 1024) {
|
|
return `${(bytes / 1024).toFixed(0)}KB`;
|
|
}
|
|
|
|
return `${(bytes / (1024 * 1024)).toFixed(0)}MB`;
|
|
}
|
|
|
|
export function FileUpload({
|
|
value,
|
|
onChange,
|
|
folder,
|
|
accept = 'image/jpeg,image/png,image/webp,image/gif',
|
|
maxSize = 10 * 1024 * 1024,
|
|
onUploadingChange,
|
|
existingUrl,
|
|
onFileMeta,
|
|
}: FileUploadProps) {
|
|
const inputRef = useRef<HTMLInputElement>(null);
|
|
const uploadId = useId();
|
|
const [uploading, setUploading] = useState(false);
|
|
const [error, setError] = useState<string | null>(null);
|
|
const [fileName, setFileName] = useState<string | null>(null);
|
|
const [fileSize, setFileSize] = useState<number | null>(null);
|
|
const [preview, setPreview] = useState<string | null>(null);
|
|
|
|
const onUploadingChangeRef = useRef(onUploadingChange);
|
|
onUploadingChangeRef.current = onUploadingChange;
|
|
|
|
useEffect(() => {
|
|
onUploadingChangeRef.current?.(uploading);
|
|
}, [uploading]);
|
|
|
|
async function handleFileChange(e: React.ChangeEvent<HTMLInputElement>) {
|
|
const file = e.target.files?.[0];
|
|
|
|
if (!file) {
|
|
return;
|
|
}
|
|
|
|
setError(null);
|
|
setUploading(true);
|
|
setFileName(file.name);
|
|
setFileSize(file.size);
|
|
|
|
const objectUrl = URL.createObjectURL(file);
|
|
setPreview(objectUrl);
|
|
|
|
try {
|
|
const key = await uploadFile(file, folder);
|
|
onChange(key);
|
|
onFileMeta?.({ size: file.size, type: file.type });
|
|
} catch (err) {
|
|
const message =
|
|
err instanceof UploadError
|
|
? err.message
|
|
: 'Gagal mengunggah file.';
|
|
setError(message);
|
|
setPreview(null);
|
|
setFileName(null);
|
|
setFileSize(null);
|
|
onFileMeta?.(null);
|
|
} finally {
|
|
setUploading(false);
|
|
|
|
if (inputRef.current) {
|
|
inputRef.current.value = '';
|
|
}
|
|
}
|
|
}
|
|
|
|
function handleRemove(e: React.MouseEvent) {
|
|
e.stopPropagation();
|
|
onChange(null);
|
|
setFileName(null);
|
|
setFileSize(null);
|
|
setPreview(null);
|
|
setError(null);
|
|
onFileMeta?.(null);
|
|
|
|
if (inputRef.current) {
|
|
inputRef.current.value = '';
|
|
}
|
|
}
|
|
|
|
const state = uploading
|
|
? 'uploading'
|
|
: error
|
|
? 'error'
|
|
: value
|
|
? 'done'
|
|
: 'idle';
|
|
|
|
return (
|
|
<>
|
|
<input
|
|
ref={inputRef}
|
|
type="file"
|
|
accept={accept}
|
|
onChange={handleFileChange}
|
|
className="hidden"
|
|
id={uploadId}
|
|
/>
|
|
|
|
<Attachment
|
|
state={state}
|
|
orientation="horizontal"
|
|
className="w-full"
|
|
>
|
|
<AttachmentTrigger
|
|
onClick={() => inputRef.current?.click()}
|
|
aria-label={
|
|
value ? 'Ganti file' : 'Pilih file untuk diunggah'
|
|
}
|
|
/>
|
|
|
|
<AttachmentMedia
|
|
variant={preview || existingUrl ? 'image' : 'icon'}
|
|
>
|
|
{preview ? (
|
|
<img src={preview} alt={fileName ?? 'Preview'} />
|
|
) : existingUrl && value ? (
|
|
<img src={existingUrl} alt="Bukti" />
|
|
) : uploading ? (
|
|
<Upload className="animate-pulse" />
|
|
) : (
|
|
<FileImage />
|
|
)}
|
|
</AttachmentMedia>
|
|
|
|
<AttachmentContent>
|
|
{value ? (
|
|
<>
|
|
<AttachmentTitle>{fileName}</AttachmentTitle>
|
|
<AttachmentDescription>
|
|
{fileSize
|
|
? formatFileSize(fileSize)
|
|
: 'Terupload'}
|
|
</AttachmentDescription>
|
|
</>
|
|
) : uploading ? (
|
|
<>
|
|
<AttachmentTitle>Mengunggah...</AttachmentTitle>
|
|
<AttachmentDescription>
|
|
Memproses file
|
|
</AttachmentDescription>
|
|
</>
|
|
) : error ? (
|
|
<>
|
|
<AttachmentTitle>Gagal</AttachmentTitle>
|
|
<AttachmentDescription>
|
|
{error}
|
|
</AttachmentDescription>
|
|
</>
|
|
) : (
|
|
<>
|
|
<AttachmentTitle>Unggah File</AttachmentTitle>
|
|
<AttachmentDescription>
|
|
{`${acceptToLabels(accept).join(', ')} · Maks ${formatMaxSize(maxSize)}`}
|
|
</AttachmentDescription>
|
|
</>
|
|
)}
|
|
</AttachmentContent>
|
|
|
|
{value && (
|
|
<AttachmentActions>
|
|
<AttachmentAction
|
|
aria-label="Hapus file"
|
|
onClick={handleRemove}
|
|
>
|
|
<X />
|
|
</AttachmentAction>
|
|
</AttachmentActions>
|
|
)}
|
|
</Attachment>
|
|
</>
|
|
);
|
|
}
|