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 { 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" />
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue