refactor uploader

This commit is contained in:
hossein taromi 2025-08-12 13:48:49 +03:30
parent 9bf5d63d1b
commit 8b685c8668
3 changed files with 46 additions and 59 deletions

View File

@ -1,4 +1,4 @@
import React, { useState, useRef, useCallback } from 'react'; import React, { useState, useRef, useCallback, useEffect } from 'react';
import { Upload, X, Image, File, AlertCircle, CheckCircle } from 'lucide-react'; import { Upload, X, Image, File, AlertCircle, CheckCircle } from 'lucide-react';
import { Button } from './Button'; import { Button } from './Button';
@ -18,7 +18,7 @@ interface FileUploaderProps {
onUpload: (file: File) => Promise<{ id: string; url: string }>; onUpload: (file: File) => Promise<{ id: string; url: string }>;
onRemove?: (fileId: string) => void; onRemove?: (fileId: string) => void;
acceptedTypes?: string[]; acceptedTypes?: string[];
maxFileSize?: number; // در بایت maxFileSize?: number;
maxFiles?: number; maxFiles?: number;
label?: string; label?: string;
description?: string; description?: string;
@ -28,13 +28,14 @@ interface FileUploaderProps {
mode?: 'single' | 'multi'; mode?: 'single' | 'multi';
onUploadStart?: () => void; onUploadStart?: () => void;
onUploadComplete?: () => void; onUploadComplete?: () => void;
initialFiles?: Array<Partial<UploadedFile> & { id: string; url?: string }>;
} }
export const FileUploader: React.FC<FileUploaderProps> = ({ export const FileUploader: React.FC<FileUploaderProps> = ({
onUpload, onUpload,
onRemove, onRemove,
acceptedTypes = ['image/*', 'video/*'], acceptedTypes = ['image/*', 'video/*'],
maxFileSize = 10 * 1024 * 1024, // 10MB maxFileSize = 10 * 1024 * 1024,
maxFiles = 10, maxFiles = 10,
label = "فایل‌ها", label = "فایل‌ها",
description = "تصاویر و ویدیوها را اینجا بکشید یا کلیک کنید", description = "تصاویر و ویدیوها را اینجا بکشید یا کلیک کنید",
@ -44,6 +45,7 @@ export const FileUploader: React.FC<FileUploaderProps> = ({
mode = 'multi', mode = 'multi',
onUploadStart, onUploadStart,
onUploadComplete, onUploadComplete,
initialFiles = [],
}) => { }) => {
const [files, setFiles] = useState<UploadedFile[]>([]); const [files, setFiles] = useState<UploadedFile[]>([]);
const [isDragOver, setIsDragOver] = useState(false); const [isDragOver, setIsDragOver] = useState(false);
@ -59,6 +61,22 @@ export const FileUploader: React.FC<FileUploaderProps> = ({
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}; };
useEffect(() => {
if (initialFiles && initialFiles.length > 0) {
const normalized: UploadedFile[] = initialFiles.map((f) => ({
id: f.id,
name: f.name || (f.url ? f.url.split('/').pop() || 'file' : 'file'),
size: typeof f.size === 'number' ? f.size : 0,
type: f.type || 'image/*',
url: f.url,
preview: f.preview,
progress: 100,
status: 'completed',
}));
setFiles(mode === 'single' ? [normalized[0]] : normalized);
}
}, [initialFiles, mode]);
const validateFile = (file: File) => { const validateFile = (file: File) => {
if (maxFileSize && file.size > maxFileSize) { if (maxFileSize && file.size > maxFileSize) {
return `حجم فایل نباید بیشتر از ${formatFileSize(maxFileSize)} باشد`; return `حجم فایل نباید بیشتر از ${formatFileSize(maxFileSize)} باشد`;
@ -202,7 +220,6 @@ export const FileUploader: React.FC<FileUploaderProps> = ({
</label> </label>
)} )}
{/* Upload Area - only show in multi mode or single mode without uploaded files */}
{showUploadArea && ( {showUploadArea && (
<div <div
className={` className={`
@ -247,7 +264,6 @@ export const FileUploader: React.FC<FileUploaderProps> = ({
</p> </p>
)} )}
{/* Files List */}
{files.length > 0 && ( {files.length > 0 && (
<div className="space-y-3"> <div className="space-y-3">
<h4 className="text-sm font-medium text-gray-700 dark:text-gray-300"> <h4 className="text-sm font-medium text-gray-700 dark:text-gray-300">
@ -259,11 +275,10 @@ export const FileUploader: React.FC<FileUploaderProps> = ({
key={file.id} key={file.id}
className="flex items-center gap-3 p-3 bg-gray-50 dark:bg-gray-700 rounded-lg" className="flex items-center gap-3 p-3 bg-gray-50 dark:bg-gray-700 rounded-lg"
> >
{/* File Icon/Preview */}
<div className="flex-shrink-0"> <div className="flex-shrink-0">
{file.preview ? ( {(file.preview || file.url) ? (
<img <img
src={file.preview} src={(file.preview || file.url) as string}
alt={file.name} alt={file.name}
className="w-10 h-10 object-cover rounded" className="w-10 h-10 object-cover rounded"
/> />
@ -278,7 +293,6 @@ export const FileUploader: React.FC<FileUploaderProps> = ({
)} )}
</div> </div>
{/* File Info */}
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<p className="text-sm font-medium text-gray-900 dark:text-gray-100 truncate"> <p className="text-sm font-medium text-gray-900 dark:text-gray-100 truncate">
{file.name} {file.name}
@ -287,7 +301,6 @@ export const FileUploader: React.FC<FileUploaderProps> = ({
{formatFileSize(file.size)} {formatFileSize(file.size)}
</p> </p>
{/* Progress Bar */}
{file.status === 'uploading' && ( {file.status === 'uploading' && (
<div className="mt-1"> <div className="mt-1">
<div className="w-full bg-gray-200 dark:bg-gray-600 rounded-full h-1.5"> <div className="w-full bg-gray-200 dark:bg-gray-600 rounded-full h-1.5">
@ -300,7 +313,6 @@ export const FileUploader: React.FC<FileUploaderProps> = ({
</div> </div>
)} )}
{/* Error Message */}
{file.status === 'error' && file.error && ( {file.status === 'error' && file.error && (
<p className="text-xs text-red-600 dark:text-red-400 mt-1 flex items-center gap-1"> <p className="text-xs text-red-600 dark:text-red-400 mt-1 flex items-center gap-1">
<AlertCircle className="h-3 w-3" /> <AlertCircle className="h-3 w-3" />
@ -309,7 +321,6 @@ export const FileUploader: React.FC<FileUploaderProps> = ({
)} )}
</div> </div>
{/* Status & Actions */}
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
{file.status === 'completed' && ( {file.status === 'completed' && (
<CheckCircle className="h-5 w-5 text-green-500" /> <CheckCircle className="h-5 w-5 text-green-500" />

View File

@ -38,19 +38,22 @@ const CategoryFormPage = () => {
useEffect(() => { useEffect(() => {
if (category && isEdit) { if (category && isEdit) {
const fileId = (category as any).file?.id ?? category.file_id;
const fileUrl = (category as any).file?.url || '';
setFormData({ setFormData({
name: category.name || '', name: category.name || '',
description: category.description || '', description: category.description || '',
parent_id: category.parent_id || null, parent_id: (category as any).parent_id || null,
file_id: category.file_id || undefined, file_id: fileId || undefined,
}); });
// Set uploaded image if exists if (fileId && fileUrl) {
if (category.file_id) { setUploadedImage({ id: String(fileId), url: fileUrl });
setUploadedImage({ } else if (fileId) {
id: category.file_id.toString(), setUploadedImage({ id: String(fileId), url: '' });
url: '' // We don't have URL from category, just ID } else {
}); setUploadedImage(null);
} }
} }
}, [category, isEdit]); }, [category, isEdit]);
@ -138,7 +141,6 @@ const CategoryFormPage = () => {
backButton={backButton} backButton={backButton}
/> />
{/* Form */}
<div className="card p-4 sm:p-6"> <div className="card p-4 sm:p-6">
<form onSubmit={handleSubmit} className="space-y-4 sm:space-y-6"> <form onSubmit={handleSubmit} className="space-y-4 sm:space-y-6">
<div> <div>
@ -170,17 +172,17 @@ const CategoryFormPage = () => {
onUpload={handleFileUpload} onUpload={handleFileUpload}
onRemove={handleFileRemove} onRemove={handleFileRemove}
acceptedTypes={['image/*']} acceptedTypes={['image/*']}
maxFileSize={5 * 1024 * 1024} // 5MB maxFileSize={5 * 1024 * 1024}
maxFiles={1} maxFiles={1}
mode="single" mode="single"
label="تصویر دسته‌بندی" label="تصویر دسته‌بندی"
description="تصویر دسته‌بندی را انتخاب کنید (حداکثر 5MB)" description="تصویر دسته‌بندی را انتخاب کنید (حداکثر 5MB)"
onUploadStart={() => setIsUploading(true)} onUploadStart={() => setIsUploading(true)}
onUploadComplete={() => setIsUploading(false)} onUploadComplete={() => setIsUploading(false)}
initialFiles={uploadedImage ? [{ id: uploadedImage.id, url: uploadedImage.url }] : []}
/> />
</div> </div>
{/* Actions */}
<div className="flex flex-col space-y-3 sm:flex-row sm:justify-end sm:space-y-0 sm:space-x-3 sm:space-x-reverse pt-4 border-t border-gray-200 dark:border-gray-700"> <div className="flex flex-col space-y-3 sm:flex-row sm:justify-end sm:space-y-0 sm:space-x-3 sm:space-x-reverse pt-4 border-t border-gray-200 dark:border-gray-700">
<Button <Button
type="button" type="button"

View File

@ -4,13 +4,13 @@ import { useForm, useFieldArray } from 'react-hook-form';
import { yupResolver } from '@hookform/resolvers/yup'; import { yupResolver } from '@hookform/resolvers/yup';
import * as yup from 'yup'; import * as yup from 'yup';
import { useProductOption, useCreateProductOption, useUpdateProductOption } from '../core/_hooks'; import { useProductOption, useCreateProductOption, useUpdateProductOption } from '../core/_hooks';
import { ProductOptionFormData, Maintenance, Option } from '../core/_models'; import { ProductOptionFormData } from '../core/_models';
import { Button } from "@/components/ui/Button"; import { Button } from "@/components/ui/Button";
import { Input } from "@/components/ui/Input"; import { Input } from "@/components/ui/Input";
import { LoadingSpinner } from "@/components/ui/LoadingSpinner"; import { LoadingSpinner } from "@/components/ui/LoadingSpinner";
import { FileUploader } from "@/components/ui/FileUploader"; import { FileUploader } from "@/components/ui/FileUploader";
import { useFileUpload, useFileDelete } from "@/hooks/useFileUpload"; import { useFileUpload, useFileDelete } from "@/hooks/useFileUpload";
import { ArrowRight, Settings, Plus, Trash2 } from "lucide-react"; import { ArrowRight, Plus, Trash2 } from "lucide-react";
import { FormHeader, PageContainer, SectionTitle, Label } from '../../../components/ui/Typography'; import { FormHeader, PageContainer, SectionTitle, Label } from '../../../components/ui/Typography';
const maintenanceSchema = yup.object({ const maintenanceSchema = yup.object({
@ -77,8 +77,6 @@ const ProductOptionFormPage = () => {
name: "options" name: "options"
}); });
const formValues = watch();
useEffect(() => { useEffect(() => {
if (isEdit && productOption) { if (isEdit && productOption) {
setValue('title', productOption.title, { shouldValidate: true }); setValue('title', productOption.title, { shouldValidate: true });
@ -86,12 +84,8 @@ const ProductOptionFormPage = () => {
setValue('maintenance', productOption.maintenance, { shouldValidate: true }); setValue('maintenance', productOption.maintenance, { shouldValidate: true });
setValue('options', productOption.options, { shouldValidate: true }); setValue('options', productOption.options, { shouldValidate: true });
// Set uploaded image if exists
if (productOption.maintenance.image) { if (productOption.maintenance.image) {
setUploadedImage({ setUploadedImage({ id: productOption.maintenance.image, url: productOption.maintenance.image });
id: productOption.maintenance.image,
url: productOption.maintenance.image
});
} }
} }
}, [isEdit, productOption, setValue]); }, [isEdit, productOption, setValue]);
@ -180,14 +174,12 @@ const ProductOptionFormPage = () => {
<form onSubmit={handleSubmit(onSubmit)} className="p-6 space-y-6"> <form onSubmit={handleSubmit(onSubmit)} className="p-6 space-y-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-6"> <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<Input <Input
label="عنوان" label="عنوان"
{...register('title')} {...register('title')}
error={errors.title?.message} error={errors.title?.message}
placeholder="عنوان گزینه محصول را وارد کنید" placeholder="عنوان گزینه محصول را وارد کنید"
/> />
</div>
<div> <div>
<Label htmlFor="description">توضیحات</Label> <Label htmlFor="description">توضیحات</Label>
<textarea <textarea
@ -246,26 +238,8 @@ const ProductOptionFormPage = () => {
error={errors.maintenance?.image?.message} error={errors.maintenance?.image?.message}
onUploadStart={() => setIsUploading(true)} onUploadStart={() => setIsUploading(true)}
onUploadComplete={() => setIsUploading(false)} onUploadComplete={() => setIsUploading(false)}
initialFiles={uploadedImage ? [{ id: uploadedImage.id, url: uploadedImage.url }] : []}
/> />
{watch('maintenance.image') && (
<div className="mt-3 flex items-center gap-3">
<img
src={watch('maintenance.image') as string}
alt="preview"
className="w-32 h-20 object-cover rounded border cursor-zoom-in"
/>
<Button
type="button"
variant="secondary"
onClick={() => {
setUploadedImage(null);
setValue('maintenance.image', '', { shouldDirty: true, shouldValidate: true });
}}
>
حذف تصویر فعلی
</Button>
</div>
)}
</div> </div>
</div> </div>
</div> </div>