refactor uploader
This commit is contained in:
parent
9bf5d63d1b
commit
8b685c8668
|
|
@ -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" />
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Reference in New Issue