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 { Button } from './Button';
@ -18,7 +18,7 @@ interface FileUploaderProps {
onUpload: (file: File) => Promise<{ id: string; url: string }>;
onRemove?: (fileId: string) => void;
acceptedTypes?: string[];
maxFileSize?: number; // در بایت
maxFileSize?: number;
maxFiles?: number;
label?: string;
description?: string;
@ -28,13 +28,14 @@ interface FileUploaderProps {
mode?: 'single' | 'multi';
onUploadStart?: () => void;
onUploadComplete?: () => void;
initialFiles?: Array<Partial<UploadedFile> & { id: string; url?: string }>;
}
export const FileUploader: React.FC<FileUploaderProps> = ({
onUpload,
onRemove,
acceptedTypes = ['image/*', 'video/*'],
maxFileSize = 10 * 1024 * 1024, // 10MB
maxFileSize = 10 * 1024 * 1024,
maxFiles = 10,
label = "فایل‌ها",
description = "تصاویر و ویدیوها را اینجا بکشید یا کلیک کنید",
@ -44,6 +45,7 @@ export const FileUploader: React.FC<FileUploaderProps> = ({
mode = 'multi',
onUploadStart,
onUploadComplete,
initialFiles = [],
}) => {
const [files, setFiles] = useState<UploadedFile[]>([]);
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];
};
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) => {
if (maxFileSize && file.size > maxFileSize) {
return `حجم فایل نباید بیشتر از ${formatFileSize(maxFileSize)} باشد`;
@ -202,7 +220,6 @@ export const FileUploader: React.FC<FileUploaderProps> = ({
</label>
)}
{/* Upload Area - only show in multi mode or single mode without uploaded files */}
{showUploadArea && (
<div
className={`
@ -247,7 +264,6 @@ export const FileUploader: React.FC<FileUploaderProps> = ({
</p>
)}
{/* Files List */}
{files.length > 0 && (
<div className="space-y-3">
<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}
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">
{file.preview ? (
{(file.preview || file.url) ? (
<img
src={file.preview}
src={(file.preview || file.url) as string}
alt={file.name}
className="w-10 h-10 object-cover rounded"
/>
@ -278,7 +293,6 @@ export const FileUploader: React.FC<FileUploaderProps> = ({
)}
</div>
{/* File Info */}
<div className="flex-1 min-w-0">
<p className="text-sm font-medium text-gray-900 dark:text-gray-100 truncate">
{file.name}
@ -287,7 +301,6 @@ export const FileUploader: React.FC<FileUploaderProps> = ({
{formatFileSize(file.size)}
</p>
{/* Progress Bar */}
{file.status === 'uploading' && (
<div className="mt-1">
<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>
)}
{/* Error Message */}
{file.status === 'error' && file.error && (
<p className="text-xs text-red-600 dark:text-red-400 mt-1 flex items-center gap-1">
<AlertCircle className="h-3 w-3" />
@ -309,7 +321,6 @@ export const FileUploader: React.FC<FileUploaderProps> = ({
)}
</div>
{/* Status & Actions */}
<div className="flex items-center gap-2">
{file.status === 'completed' && (
<CheckCircle className="h-5 w-5 text-green-500" />

View File

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

View File

@ -4,13 +4,13 @@ import { useForm, useFieldArray } from 'react-hook-form';
import { yupResolver } from '@hookform/resolvers/yup';
import * as yup from 'yup';
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 { Input } from "@/components/ui/Input";
import { LoadingSpinner } from "@/components/ui/LoadingSpinner";
import { FileUploader } from "@/components/ui/FileUploader";
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';
const maintenanceSchema = yup.object({
@ -77,8 +77,6 @@ const ProductOptionFormPage = () => {
name: "options"
});
const formValues = watch();
useEffect(() => {
if (isEdit && productOption) {
setValue('title', productOption.title, { shouldValidate: true });
@ -86,12 +84,8 @@ const ProductOptionFormPage = () => {
setValue('maintenance', productOption.maintenance, { shouldValidate: true });
setValue('options', productOption.options, { shouldValidate: true });
// Set uploaded image if exists
if (productOption.maintenance.image) {
setUploadedImage({
id: productOption.maintenance.image,
url: productOption.maintenance.image
});
setUploadedImage({ id: productOption.maintenance.image, url: productOption.maintenance.image });
}
}
}, [isEdit, productOption, setValue]);
@ -180,14 +174,12 @@ const ProductOptionFormPage = () => {
<form onSubmit={handleSubmit(onSubmit)} className="p-6 space-y-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<Input
label="عنوان"
{...register('title')}
error={errors.title?.message}
placeholder="عنوان گزینه محصول را وارد کنید"
/>
</div>
<Input
label="عنوان"
{...register('title')}
error={errors.title?.message}
placeholder="عنوان گزینه محصول را وارد کنید"
/>
<div>
<Label htmlFor="description">توضیحات</Label>
<textarea
@ -246,26 +238,8 @@ const ProductOptionFormPage = () => {
error={errors.maintenance?.image?.message}
onUploadStart={() => setIsUploading(true)}
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>