Vitron-Front/app/routes/store.add.manual.$storeId.tsx
2026-04-29 01:44:16 +03:30

976 lines
31 KiB
TypeScript
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import type { MetaFunction } from "@remix-run/node";
import { X, Loader } from "lucide-react";
import HeaderWithSupport from "../components/HeaderWithSupport";
import { useSearchParams } from "react-router-dom";
import { safeGoBack } from "~/utils/helpers";
import { useRef, useState, useEffect } from "react";
import { Button } from "../components/ui/button";
import { ImageUploadDrawer } from "~/components/store/ImageUploadDrawer";
import { ImageEditor } from "~/components/store/ImageEditor";
import { ImageUploadSection } from "~/components/store/ImageUploadSection";
import { ProductDetailsForm } from "~/components/store/ProductDetailsForm";
import { ProductVariantsSection } from "~/components/store/ProductVariantsSection";
import { PricingForm } from "~/components/store/PricingForm";
import {
useCreateFullProduct,
useProductCategories,
useProductDetails,
useUpdateProduct,
} from "~/requestHandler/use-product-hooks";
import { useImageUpload } from "~/requestHandler/use-media-hooks";
import AppSelectBox from "~/components/AppSelectBox";
import { FormField } from "~/components/ui/FormField";
import {
useGetInstagramPostDetails,
useGenerateProductFromPost,
} from "~/requestHandler/use-instagram-hooks";
import {
FullProductCreate,
ProductVariant as ApiProductVariant,
ProductAttributeItem,
MediaUploadImageUsageEnum,
} from "../../src/api/types";
import { Dialog, DialogContent, DialogOverlay } from "../components/ui/dialog";
import { useNavigate, useParams } from "@remix-run/react";
import { useStoreOwnership } from "../hooks/useStoreOwnership";
export const meta: MetaFunction = () => {
return [
{ title: "افزودن محصول - ویترون" },
{
name: "description",
content:
"افزودن محصول جدید به فروشگاه شما در ویترون. آپلود تصاویر، تعیین قیمت، ویژگی‌ها و تنظیمات محصول.",
},
{
name: "keywords",
content: "افزودن محصول، جدید محصول، فروشگاه، قیمت گذاری، ویترون",
},
{ name: "robots", content: "noindex, follow" },
{ property: "og:title", content: "افزودن محصول - ویترون" },
{
property: "og:description",
content:
"افزودن محصول جدید به فروشگاه شما در ویترون. آپلود تصاویر، تعیین قیمت، ویژگی‌ها و تنظیمات محصول.",
},
{ property: "og:type", content: "website" },
{ name: "twitter:card", content: "summary" },
{ name: "twitter:title", content: "افزودن محصول - ویترون" },
{
name: "twitter:description",
content:
"افزودن محصول جدید به فروشگاه شما در ویترون. آپلود تصاویر، تعیین قیمت، ویژگی‌ها و تنظیمات محصول.",
},
];
};
// Local interface for the component's variant structure
interface ProductVariant {
color: string;
size: string;
quantity: number;
}
export default function AddProduct() {
const [searchParams] = useSearchParams();
const navigate = useNavigate();
const { storeId } = useParams();
// Get URL parameters
const postId = searchParams.get("id");
const isAI = searchParams.get("ai") === "true";
const editProductId = searchParams.get("edit");
// Check store ownership - this will redirect if user doesn't own the store
const { isLoading: isOwnershipLoading } = useStoreOwnership(storeId);
// Step management
const [currentStep, setCurrentStep] = useState(1);
// Step 1 - Product Info
const [productTitle, setProductTitle] = useState("");
const [productDescription, setProductDescription] = useState("");
const [selectedCategory, setSelectedCategory] = useState("");
// Step 2 - Pricing
const [productPrice, setProductPrice] = useState("");
const [finalPrice, setFinalPrice] = useState("");
// Loading states
const [isSubmittingProduct, setIsSubmittingProduct] = useState(false);
const [imageUploadProgress, setImageUploadProgress] = useState({
current: 0,
total: 0,
isUploading: false,
});
// API hooks
const createProductMutation = useCreateFullProduct();
const updateProductMutation = useUpdateProduct();
const imageUploadMutation = useImageUpload();
// Product details for editing
const { data: existingProduct } = useProductDetails(editProductId || "");
// Instagram post hooks - only fetch if we have postId and AI is false
const { data: instagramPostData, isLoading: isLoadingPostData } =
useGetInstagramPostDetails(postId || "", {
enabled: !!postId && !isAI,
});
// Generate product hook - for AI generation
const {
data: aiGeneratedData,
isLoading: isAiGenerating,
error: aiGenerationError,
} = useGenerateProductFromPost(postId || "", {
enabled: !!postId && isAI,
});
// Fetch categories
const { data: categoriesData, isLoading: categoriesLoading } =
useProductCategories();
// Transform categories data for AppSelectBox
const categoryOptions = (categoriesData || []).map((cat) => ({
value: cat.id || "",
label: cat.name,
}));
// Track if form has been initialized and previous AI state
const [formInitialized, setFormInitialized] = useState(false);
const [previousIsAI, setPreviousIsAI] = useState<boolean | null>(null);
// Determine if we're in edit mode
const isEditMode = !!editProductId;
// Effect to fill form with existing product data for editing
useEffect(() => {
if (existingProduct && isEditMode && !formInitialized) {
setProductTitle(existingProduct.title || "");
setProductDescription(existingProduct.description || "");
setSelectedCategory(existingProduct.category || "");
setProductPrice(
existingProduct.basePrice
? parseInt(existingProduct.basePrice).toString()
: ""
);
// Set final price if available from first variant
if (existingProduct.variants && existingProduct.variants.length > 0) {
const firstVariant = existingProduct.variants[0];
const discountPrice = firstVariant.discountPrice
? parseInt(firstVariant.discountPrice).toString()
: "";
const regularPrice = firstVariant.price
? parseInt(firstVariant.price).toString()
: "";
setFinalPrice(discountPrice || regularPrice);
}
// Set images from existing product
if (existingProduct.imageUrls && existingProduct.imageUrls.length > 0) {
const existingImages = existingProduct.imageUrls.map((url, index) => ({
file: new File([], `existing-image-${index + 1}.jpg`, {
type: "image/jpeg",
}),
url: url,
isEdited: false,
originalUrl: url,
uploadedUrl: undefined,
}));
setSelectedImages(existingImages);
}
// Set variants from existing product
if (existingProduct.variants && existingProduct.variants.length > 0) {
const existingVariants = existingProduct.variants
.filter((variant) => variant.stock && variant.stock > 0) // Only include variants with stock
.map((variant) => ({
color:
variant.resolvedAttributes?.COLOR ||
variant.attributes?.COLOR ||
"unselected",
size:
variant.resolvedAttributes?.SIZE ||
variant.attributes?.SIZE ||
"unselected",
quantity: variant.stock || 0,
}));
// If no variants with stock, create a default variant
if (existingVariants.length === 0) {
existingVariants.push({
color: "unselected",
size: "unselected",
quantity: 1,
});
}
setProductVariants(existingVariants);
} else {
// If no variants exist, create a default variant
setProductVariants([
{
color: "unselected",
size: "unselected",
quantity: 1,
},
]);
}
setFormInitialized(true);
}
}, [existingProduct, isEditMode, formInitialized]);
// Effect to fill form when AI generation is complete
useEffect(() => {
if (aiGeneratedData && isAI && !formInitialized && !isEditMode) {
setProductTitle(aiGeneratedData.product.title || "");
setProductDescription(aiGeneratedData.product.description || "");
setSelectedImages(
aiGeneratedData.product.imageUrls.map((url) => ({
file: new File([], url),
url: url,
isEdited: false,
originalUrl: url,
uploadedUrl: undefined,
}))
);
// Set pricing
if (aiGeneratedData.product.basePrice) {
setProductPrice(
parseInt(aiGeneratedData.product.basePrice.toString()).toString()
);
}
// Set final price from first variant if available
if (
aiGeneratedData.product.variants &&
aiGeneratedData.product.variants.length > 0
) {
const firstVariant = aiGeneratedData.product.variants[0];
const variantPrice = firstVariant.price
? parseInt(firstVariant.price.toString()).toString()
: "";
const basePrice = aiGeneratedData.product.basePrice
? parseInt(aiGeneratedData.product.basePrice.toString()).toString()
: "";
setFinalPrice(variantPrice || basePrice);
}
// Set category - we need to find the category ID from the category name
if (aiGeneratedData.product.category && categoriesData) {
// Don't set category if it's UNSELECTED or similar values
const categoryName = aiGeneratedData.product.category;
if (
categoryName.toLowerCase() !== "unselected" &&
categoryName.toLowerCase() !== "none"
) {
const categoryMatch = categoriesData.find(
(cat) => cat.name === categoryName
);
if (categoryMatch && categoryMatch.id) {
setSelectedCategory(categoryMatch.id);
}
}
}
// Set variants - convert from API format to component format
if (
aiGeneratedData.product.variants &&
aiGeneratedData.product.variants.length > 0
) {
const convertedVariants = aiGeneratedData.product.variants.map(
(variant) => ({
color:
variant.attributes?.COLOR?.toLowerCase() === "unselected"
? "unselected"
: variant.attributes?.COLOR || "unselected",
size:
variant.attributes?.SIZE?.toLowerCase() === "unselected"
? "unselected"
: variant.attributes?.SIZE || "unselected",
quantity: variant.stock || 0,
})
);
setProductVariants(convertedVariants);
} else {
// Default variant if none provided
setProductVariants([
{
color: "unselected",
size: "unselected",
quantity: 1,
},
]);
}
setFormInitialized(true);
}
}, [aiGeneratedData, isAI, formInitialized, isEditMode, categoriesData]);
// Effect to set category when categoriesData loads after AI generation
useEffect(() => {
if (
aiGeneratedData &&
isAI &&
formInitialized &&
!isEditMode &&
categoriesData &&
!selectedCategory &&
aiGeneratedData.product.category
) {
const categoryName = aiGeneratedData.product.category;
// Don't set category if it's UNSELECTED or similar values
if (
categoryName.toLowerCase() !== "unselected" &&
categoryName.toLowerCase() !== "none"
) {
const categoryMatch = categoriesData.find(
(cat) => cat.name === categoryName
);
if (categoryMatch && categoryMatch.id) {
setSelectedCategory(categoryMatch.id);
}
}
}
}, [
categoriesData,
aiGeneratedData,
isAI,
formInitialized,
isEditMode,
selectedCategory,
]);
// Effect to handle initial data loading from Instagram post (only for manual mode)
useEffect(() => {
if (instagramPostData && !isAI && !formInitialized && !isEditMode) {
// If AI is false and we have post data, populate initial values
setProductTitle(instagramPostData.caption?.split("\n")[0] || "");
setProductDescription(instagramPostData.caption || "");
// Convert Instagram URLs to File-like objects
if (
instagramPostData.localMedia &&
instagramPostData.localMedia.length > 0
) {
const imageFiles = instagramPostData.localMedia.map(
(url: string, index: number) => {
// Create a dummy File object for Instagram URLs
const dummyFile = new File([], `instagram-image-${index + 1}.jpg`, {
type: "image/jpeg",
});
return {
file: dummyFile,
url: url,
isEdited: false,
originalUrl: url,
uploadedUrl: undefined,
};
}
);
setSelectedImages(imageFiles);
}
setFormInitialized(true);
}
}, [instagramPostData, isAI, formInitialized, isEditMode]);
// Reset form only when actually switching between AI and manual modes
useEffect(() => {
// Only reset if we're switching from one mode to another (not on initial load)
if (previousIsAI !== null && previousIsAI !== isAI && postId) {
setFormInitialized(false);
setProductTitle("");
setProductDescription("");
setSelectedImages([]);
setSelectedCategory("");
setProductVariants([]);
setProductPrice("");
setFinalPrice("");
}
setPreviousIsAI(isAI);
}, [isAI, postId, previousIsAI]);
const [isDrawerOpen, setIsDrawerOpen] = useState(false);
const [isImageEditorOpen, setIsImageEditorOpen] = useState(false);
const [currentEditingIndex, setCurrentEditingIndex] = useState<number | null>(
null
);
const fileInputRef = useRef<HTMLInputElement>(null);
const [selectedImages, setSelectedImages] = useState<
Array<{
file: File;
url: string;
isEdited: boolean;
originalUrl?: string;
uploadedUrl?: string;
}>
>([]);
const [productVariants, setProductVariants] = useState<ProductVariant[]>([]);
// Function to update variant quantity
const updateVariantQuantity = (index: number, newQuantity: number) => {
setProductVariants((prev) =>
prev.map((variant, i) =>
i === index ? { ...variant, quantity: newQuantity } : variant
)
);
};
// Validation functions
const isStep1Valid = () => {
return (
selectedImages.length > 0 &&
productTitle.trim() !== "" &&
productDescription.trim() !== "" &&
selectedCategory !== "" &&
productVariants.length > 0
);
};
const isStep2Valid = () => {
return productPrice.trim() !== "";
};
// Helper function to determine which images need to be uploaded
const getImagesToUpload = () => {
return selectedImages.filter((img) => img.isEdited || !img.originalUrl);
};
// Helper function to get final image URLs
const getFinalImageUrls = async (): Promise<string[]> => {
const imagesToUpload = getImagesToUpload();
const totalImages = imagesToUpload.length;
setImageUploadProgress({
current: 0,
total: totalImages,
isUploading: true,
});
const finalUrls: string[] = [];
// Process each image
for (let i = 0; i < selectedImages.length; i++) {
const image = selectedImages[i];
if (image.uploadedUrl) {
// If image was already uploaded before, use that URL
finalUrls.push(image.uploadedUrl);
} else if (image.isEdited || !image.originalUrl) {
// Upload edited or new images
setImageUploadProgress({
current: finalUrls.length + 1,
total: totalImages,
isUploading: true,
});
const result = await imageUploadMutation.mutateAsync({
file: image.file,
usageType: MediaUploadImageUsageEnum.Product,
});
const uploadedUrl = result.originalUrl || "";
finalUrls.push(uploadedUrl);
// Store the uploaded URL for future use
setSelectedImages((prev) =>
prev.map((img, index) =>
index === i ? { ...img, uploadedUrl } : img
)
);
} else {
// Use original URL for unedited images
finalUrls.push(image.originalUrl);
}
}
setImageUploadProgress({ current: 0, total: 0, isUploading: false });
return finalUrls;
};
// Helper function to prepare product attributes
const prepareProductAttributes = (
variants: ProductVariant[]
): ProductAttributeItem[] => {
const attributes: ProductAttributeItem[] = [];
// Add colors (excluding unselected)
const colors = [
...new Set(
variants.map((v) => v.color).filter((c) => c !== "unselected")
),
];
if (colors.length > 0) {
colors.forEach((color) => {
attributes.push({
attribute: "COLOR",
value: color, // Use raw color value instead of persian translation
});
});
}
// Add sizes (excluding unselected)
const sizes = [
...new Set(variants.map((v) => v.size).filter((s) => s !== "unselected")),
];
if (sizes.length > 0) {
sizes.forEach((size) => {
attributes.push({
attribute: "SIZE",
value: size,
});
});
}
return attributes;
};
// Helper function to prepare product variants
const prepareProductVariants = (
variants: ProductVariant[]
): ApiProductVariant[] => {
return variants.map((variant) => {
// Create variant name based on color and size
let variantName = "";
if (variant.color === "unselected" && variant.size === "unselected") {
variantName = "Default";
} else if (variant.color === "unselected") {
variantName = variant.size;
} else if (variant.size === "unselected") {
variantName = variant.color;
} else {
variantName = `${variant.color}-${variant.size}`;
}
// Prepare attributes object
const attributes: Record<string, string> = {};
if (variant.color !== "unselected") {
attributes.COLOR = variant.color;
}
if (variant.size !== "unselected" && variant.size !== "") {
attributes.SIZE = variant.size;
}
return {
variantName: variantName,
stock: variant.quantity,
price: productPrice || "0",
discountPrice: finalPrice || productPrice || "0",
attributes: attributes,
};
});
};
// Main product submission handler
const handleProductSubmission = async () => {
try {
setIsSubmittingProduct(true);
// Get final image URLs (only upload edited/new images)
const imageUrls = await getFinalImageUrls();
if (isEditMode && editProductId) {
// Update existing product
const updateData = {
title: productTitle,
description: productDescription,
basePrice: productPrice || "",
imageUrls: imageUrls.filter(Boolean), // Remove empty URLs
category: selectedCategory,
attributes: prepareProductAttributes(productVariants),
variants: prepareProductVariants(productVariants),
};
await updateProductMutation.mutateAsync({
productId: editProductId,
productData: updateData,
});
navigate(`/store/${storeId}/product/${editProductId}`);
} else {
// Create new product
const productData: FullProductCreate = {
title: productTitle,
description: productDescription,
basePrice: productPrice || "",
imageUrls: imageUrls.filter(Boolean), // Remove empty URLs
category: selectedCategory,
attributes: prepareProductAttributes(productVariants),
variants: prepareProductVariants(productVariants),
instagramPostId: postId ? parseInt(postId) : undefined,
};
const product = await createProductMutation.mutateAsync(productData);
navigate(`/store/${product.seller?.username}`);
}
} catch (error) {
console.error(
`Failed to ${isEditMode ? "update" : "create"} product:`,
error
);
} finally {
setIsSubmittingProduct(false);
}
};
const handleContinue = () => {
if (currentStep === 1 && isStep1Valid()) {
setCurrentStep(2);
} else if (currentStep === 2 && isStep2Valid()) {
handleProductSubmission();
}
};
const handleBack = () => {
if (currentStep === 2) {
setCurrentStep(1);
} else {
safeGoBack(navigate);
}
};
// Handle drawer actions
const handleUploadClick = () => {
setIsDrawerOpen(true);
};
const handleCameraSelect = () => {
setIsDrawerOpen(false);
if (fileInputRef.current) {
fileInputRef.current.setAttribute("capture", "environment");
fileInputRef.current.click();
}
};
const handleGallerySelect = () => {
setIsDrawerOpen(false);
if (fileInputRef.current) {
fileInputRef.current.removeAttribute("capture");
fileInputRef.current.click();
}
};
const handleRemoveImage = (indexToRemove: number) => {
setSelectedImages((prev) => {
const imageToRemove = prev[indexToRemove];
const newImages = prev.filter((_, index) => index !== indexToRemove);
if (imageToRemove) {
setTimeout(() => {
URL.revokeObjectURL(imageToRemove.url);
}, 1000);
}
return newImages;
});
};
const handleScaleImage = (index: number) => {
setCurrentEditingIndex(index);
setIsImageEditorOpen(true);
};
const handleImageEditorSave = (editedImageBlob: Blob) => {
if (currentEditingIndex === null) return;
// Clean up old URL
const oldImage = selectedImages[currentEditingIndex];
if (oldImage) {
URL.revokeObjectURL(oldImage.url);
}
// Create new file and URL
const newFile = new File(
[editedImageBlob],
oldImage?.file.name || "edited-image.jpg",
{
type: editedImageBlob.type,
}
);
const newUrl = URL.createObjectURL(editedImageBlob);
// Update the image in state
setSelectedImages((prev) =>
prev.map((image, index) =>
index === currentEditingIndex
? {
file: newFile,
url: newUrl,
isEdited: true,
originalUrl: oldImage?.originalUrl || oldImage?.url,
uploadedUrl: undefined, // Clear uploaded URL since image is edited
}
: image
)
);
setCurrentEditingIndex(null);
};
// Handle image selection
const handleImageSelect = (event: React.ChangeEvent<HTMLInputElement>) => {
const files = event.target.files;
if (files && files.length > 0) {
// Convert FileList to Array and process all files
const newImages = Array.from(files).map((file) => ({
file,
url: URL.createObjectURL(file),
isEdited: false,
uploadedUrl: undefined,
}));
setSelectedImages((prev) => [...prev, ...newImages]);
// Reset the input value so the same files can be selected again if needed
if (fileInputRef.current) {
fileInputRef.current.value = "";
}
}
};
// Handle pricing changes
const handlePriceChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setProductPrice(e.target.value.replace(/[^0-9]/g, ""));
};
const onFinalPriceChange = (finalPrice: string) => {
setFinalPrice(finalPrice);
};
// Show loading while checking ownership
if (isOwnershipLoading) {
return (
<div className="flex items-center justify-center">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
</div>
);
}
return (
<div className="flex flex-col gap-0 w-full">
<HeaderWithSupport
title={
isEditMode
? "ویرایش محصول"
: postId && isAI
? "ایجاد محصول با هوش مصنوعی"
: postId && !isAI
? "ایجاد محصول از پست اینستاگرام"
: "افزودن محصول جدید"
}
onBack={handleBack}
/>
{/* Show form only if not in AI generation mode or AI generation is complete */}
{(!postId || !isAI || !isAiGenerating) && (
<>
{currentStep === 1 && (
<>
{/* Image Upload Section */}
<ImageUploadSection
selectedImages={selectedImages}
onUploadClick={handleUploadClick}
onRemoveImage={handleRemoveImage}
onScaleImage={handleScaleImage}
onImageSelect={handleImageSelect}
fileInputRef={fileInputRef}
/>
{/* Product Details Form */}
<ProductDetailsForm
productTitle={productTitle}
productDescription={productDescription}
onTitleChange={(e) => setProductTitle(e.target.value)}
onDescriptionChange={(e) =>
setProductDescription(e.target.value)
}
/>
{/* Category Selection */}
<div className="px-4 mt-4">
<FormField label="دسته‌بندی" required>
<AppSelectBox
items={categoryOptions}
value={selectedCategory}
setValue={setSelectedCategory}
placeholder="انتخاب دسته‌بندی"
loading={categoriesLoading}
haveSearchbar={true}
width="w-full"
/>
</FormField>
</div>
{/* Product Variants Section */}
<ProductVariantsSection
setProductVariants={setProductVariants}
productVariants={productVariants}
onUpdateVariantQuantity={updateVariantQuantity}
/>
</>
)}
{currentStep === 2 && (
<PricingForm
productPrice={productPrice}
finalPrice={finalPrice}
onPriceChange={handlePriceChange}
onFinalPriceChange={onFinalPriceChange}
/>
)}
{/* Action Buttons */}
<div className="flex flex-col gap-3 px-4 pb-20 mt-2">
<Button
className="w-full"
size="xl"
variant="dark"
onClick={handleContinue}
disabled={
isSubmittingProduct ||
imageUploadProgress.isUploading ||
(currentStep === 1 ? !isStep1Valid() : !isStep2Valid())
}
>
{currentStep === 1
? "ادامه"
: isEditMode
? "به‌روزرسانی محصول"
: "ثبت محصول"}
</Button>
{/* Validation Messages */}
{currentStep === 1 && !isStep1Valid() && (
<div className="text-xs text-red-500 text-center space-y-1">
{selectedImages.length === 0 && (
<div>حداقل یک تصویر اضافه کنید</div>
)}
{!productTitle.trim() && <div>عنوان محصول را وارد کنید</div>}
{!productDescription.trim() && (
<div>توضیحات محصول را وارد کنید</div>
)}
{!selectedCategory && (
<div>دستهبندی محصول را انتخاب کنید</div>
)}
{productVariants.length === 0 && (
<div>حداقل یک ویژگی اضافه کنید</div>
)}
</div>
)}
{currentStep === 2 && !isStep2Valid() && (
<div className="text-xs text-red-500 text-center">
لطفاً قیمت محصول را وارد کنید
</div>
)}
</div>
</>
)}
{/* Show a message when AI is generating */}
{postId && isAI && isAiGenerating && (
<div className="flex flex-col items-center justify-center p-8 mt-20">
<Loader className="animate-spin mb-4" size={48} />
<h2 className="text-lg font-bold mb-2">
در حال ایجاد محصول با هوش مصنوعی
</h2>
<p className="text-BLACK2 text-center">
لطفا صبر کنید تا محصول شما از روی پست اینستاگرام ایجاد شود...
</p>
</div>
)}
{postId && isAI && aiGenerationError && (
<div className="flex flex-col items-center justify-center p-8 mt-20">
<div className="text-red-500 mb-4">
<X size={48} />
</div>
<h2 className="text-lg font-bold mb-2 text-red-600">
خطا در ایجاد محصول
</h2>
<p className="text-BLACK2 text-center mb-4">
متأسفانه نتوانستیم محصول را از روی پست اینستاگرام شما ایجاد کنیم
</p>
<Button
onClick={() => safeGoBack(navigate)}
variant="outline"
className="mb-2"
>
بازگشت
</Button>
</div>
)}
{/* Image Editor */}
<ImageEditor
isOpen={isImageEditorOpen}
onClose={() => setIsImageEditorOpen(false)}
imageUrl={
currentEditingIndex !== null
? selectedImages[currentEditingIndex]?.url || ""
: ""
}
onSave={handleImageEditorSave}
/>
{/* Image Upload Drawer */}
<ImageUploadDrawer
isOpen={isDrawerOpen}
onOpenChange={setIsDrawerOpen}
onCameraSelect={handleCameraSelect}
onGallerySelect={handleGallerySelect}
/>
{/* Loading Modals */}
<LoadingModal
open={imageUploadProgress.isUploading}
setOpen={() => {}}
title={`در حال آپلود ${imageUploadProgress.current}/${imageUploadProgress.total} عکس...`}
/>
<LoadingModal
open={isSubmittingProduct}
setOpen={() => {}}
title={
isEditMode ? "در حال به‌روزرسانی محصول..." : "در حال ایجاد محصول..."
}
/>
<LoadingModal
open={isLoadingPostData}
setOpen={() => {}}
title="در حال بارگذاری اطلاعات پست..."
/>
</div>
);
}
interface LoadingModalProps {
open: boolean;
setOpen: (open: boolean) => void;
title?: string;
description?: string;
}
export const LoadingModal = ({
open,
setOpen,
title = "در حال بارگذاری...",
description,
}: LoadingModalProps) => {
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogOverlay className="bg-BLACK/50" />
<DialogContent className="p-6 bg-WHITE rounded-[15px] w-[calc(100%-40px)] max-w-sm">
<div className="flex flex-col items-center justify-center gap-4 py-4">
<Loader className="animate-spin" />
<p className="text-B14 font-bold text-center">لطفا منتظر بمانید...</p>
<p className="text-R12 text-center">{title}</p>
{description && <p className="text-R12 text-center">{description}</p>}
</div>
</DialogContent>
</Dialog>
);
};