import { useState, useRef, useEffect } from "react"; import { useNavigate } from "@remix-run/react"; import { Loader2, Save, Store, Upload } from "lucide-react"; import ProfilePagesHeader from "../ProfilePagesHeader"; import HeaderWithSupport from "../HeaderWithSupport"; import AppInput from "../AppInput"; import AppTextArea from "../AppTextArea"; import AppSelectBox from "../AppSelectBox"; import { Button } from "../ui/button"; import { FormField } from "../ui/FormField"; import { ImageUpload } from "./ImageUpload"; import { convertHeicToJpeg } from "~/utils/convert-heic"; import { ImageUploadDrawer } from "./ImageUploadDrawer"; import { Textarea } from "../ui/textarea"; import { useToast } from "../../hooks/use-toast"; import { useCreateSellerApplication, useSellerCategories, useStateCityData, useSellerUpdate, useSellerData, } from "../../requestHandler/use-seller-hooks"; import { useImageUpload } from "../../requestHandler/use-media-hooks"; import { SellerApplication } from "../../../src/api/types/models"; import { validateUsername, validateInstagramId, validateStoreName, validateStoreDescription, } from "../../utils/validation"; import { useServiceGetProfile } from "../../requestHandler/use-profile-hooks"; import { LoadingModal } from "../../routes/store.add.manual.$storeId"; import { useStoreOwnership } from "../../hooks/useStoreOwnership"; // Types interface CategoryData { id: string; name: string; } interface CityData { id: number; name: string; } interface StateData { id: number; name: string; cities: CityData[]; } interface CategoryOption { value: string; label: string; } interface StateOption { value: string; label: string; } interface CityOption { value: string; label: string; } interface ValidationErrors { username: string; storeName: string; storeDescription: string; instagramId: string; } interface FormData { username: string; storeName: string; storeDescription: string; selectedCategory: string; selectedState: string; selectedCity: string; instagramId: string; storeLogo: string; bannerType: string; bannerColor: string; bannerImage: string; } // Preset banner colors offered alongside the custom color picker. const BANNER_PRESETS = [ "#1d2b4d", "#0f172a", "#111827", "#374151", "#7c3aed", "#be123c", "#0e7490", "#15803d", "#b45309", "#db2777", ]; const DEFAULT_BANNER_COLOR = "#1d2b4d"; interface StoreFormProps { mode: "create" | "edit"; storeId?: string; } export default function StoreForm({ mode, storeId }: StoreFormProps) { const navigate = useNavigate(); const { toast } = useToast(); const fileInputRef = useRef(null); // Conditional data fetching for edit mode const { data: sellerData, isLoading: isSellerLoading } = useSellerData( mode === "edit" ? storeId : undefined ); // Check store ownership for edit mode const { isLoading: isOwnershipLoading, role } = useStoreOwnership( mode === "edit" ? storeId : undefined, mode ); // Editing store info is owner-only; send staff back to the dashboard. useEffect(() => { if (mode === "edit" && !isOwnershipLoading && role && role !== "owner") { navigate(`/store/${storeId}`); } }, [mode, isOwnershipLoading, role, storeId, navigate]); // Form state const [selectedImage, setSelectedImage] = useState(null); const [imagePreviewUrl, setImagePreviewUrl] = useState(null); const [isDrawerOpen, setIsDrawerOpen] = useState(false); const [hasChanges, setHasChanges] = useState(false); // Banner state (edit mode) const bannerFileInputRef = useRef(null); const [selectedBannerImage, setSelectedBannerImage] = useState( null ); const [bannerPreviewUrl, setBannerPreviewUrl] = useState(null); // Loading modal states const [createSellerOpen, setCreateSellerOpen] = useState(false); const [imageUploadOpen, setImageUploadOpen] = useState(false); const [formData, setFormData] = useState({ username: "", storeName: "", storeDescription: "", selectedCategory: "", selectedState: "", selectedCity: "", instagramId: "", storeLogo: "", bannerType: "color", bannerColor: "", bannerImage: "", }); // Validation errors state const [validationErrors, setValidationErrors] = useState({ username: "", storeName: "", storeDescription: "", instagramId: "", }); // Mutations and queries const createSellerMutation = useCreateSellerApplication(); const updateSellerMutation = useSellerUpdate(); const imageUploadMutation = useImageUpload(); // Fetch categories and state/city data const { data: categoriesData, isLoading: categoriesLoading } = useSellerCategories(); const { data: stateCityData, isLoading: stateCityLoading } = useStateCityData(); const { data: profileData } = useServiceGetProfile(); // Initialize form with seller data for edit mode useEffect(() => { if (mode === "edit" && sellerData) { const initialData = { username: sellerData.username || "", storeName: sellerData.storeName || "", storeDescription: sellerData.storeDescription || "", selectedCategory: "", // Not available in SellerUpdate selectedState: "", // Not available in SellerUpdate selectedCity: "", // Not available in SellerUpdate instagramId: sellerData.instagramId || "", storeLogo: sellerData.storeLogo || "", bannerType: sellerData.bannerType || "color", bannerColor: sellerData.bannerColor || "", bannerImage: sellerData.bannerImage || "", }; setFormData(initialData); } }, [mode, sellerData]); // Track changes for edit mode useEffect(() => { if (mode === "edit" && sellerData) { const hasFormChanges = formData.storeName !== (sellerData.storeName || "") || formData.storeDescription !== (sellerData.storeDescription || "") || formData.instagramId !== (sellerData.instagramId || "") || formData.storeLogo !== (sellerData.storeLogo || "") || formData.bannerType !== (sellerData.bannerType || "color") || formData.bannerColor !== (sellerData.bannerColor || "") || formData.bannerImage !== (sellerData.bannerImage || "") || selectedImage !== null || // Include image changes selectedBannerImage !== null; setHasChanges(hasFormChanges); } }, [mode, formData, sellerData, selectedImage, selectedBannerImage]); // Redirect if user already has a store (create mode only) useEffect(() => { if ( mode === "create" && profileData?.sellerUsername && profileData.sellerUsername !== "" ) { navigate(`/store/${profileData.sellerUsername}`); } }, [mode, profileData, navigate]); // Clean up image preview URL on component unmount useEffect(() => { return () => { if (imagePreviewUrl) { URL.revokeObjectURL(imagePreviewUrl); } }; }, [imagePreviewUrl]); // Clean up banner preview URL useEffect(() => { return () => { if (bannerPreviewUrl) { URL.revokeObjectURL(bannerPreviewUrl); } }; }, [bannerPreviewUrl]); // Show loading while checking ownership or loading seller data (edit mode) if (mode === "edit" && (isOwnershipLoading || isSellerLoading)) { return (
); } // Transform data for AppSelectBox const categoryOptions: CategoryOption[] = (categoriesData as CategoryData[])?.map((cat) => ({ value: cat.id || "", label: cat.name, })) || []; const stateOptions: StateOption[] = (stateCityData as StateData[])?.map((state) => ({ value: state.id?.toString() || "", label: state.name, })) || []; const selectedStateData = (stateCityData as StateData[])?.find( (state) => state.id?.toString() === formData.selectedState ); const cityOptions: CityOption[] = selectedStateData?.cities?.map((city) => ({ value: city.id?.toString() || "", label: city.name, })) || []; // Validate individual field const validateField = ( name: keyof ValidationErrors, value: string ): boolean => { let error = ""; switch (name) { case "username": error = validateUsername(value) || ""; break; case "storeName": error = validateStoreName(value) || ""; break; case "storeDescription": error = validateStoreDescription(value) || ""; break; case "instagramId": error = validateInstagramId(value) || ""; break; } setValidationErrors((prev) => ({ ...prev, [name]: error, })); return !error; }; // Handle form input changes const handleInputChange = (name: keyof FormData, value: string) => { // Sanitize Instagram ID input - only allow valid characters if (name === "instagramId") { // Remove @ symbol if present and only allow alphanumeric, dots, and underscores value = value.replace(/^@/, "").replace(/[^a-zA-Z0-9._]/g, ""); } setFormData((prev) => ({ ...prev, [name]: value, })); // Validate field on change if it's a validatable field if (name in validationErrors) { validateField(name as keyof ValidationErrors, value); } }; // Form validation const isValidForm = mode === "create" ? formData.username.trim() !== "" && formData.storeName.trim() !== "" && formData.storeDescription.trim() !== "" && formData.selectedCategory !== "" && formData.selectedState !== "" && formData.selectedCity !== "" && formData.instagramId.trim() !== "" && !validationErrors.username && !validationErrors.storeName && !validationErrors.storeDescription && !validationErrors.instagramId : formData.storeName.trim() !== "" && !validationErrors.storeName && !validationErrors.storeDescription && !validationErrors.instagramId; // Handle image selection const handleImageSelect = async ( event: React.ChangeEvent ) => { const picked = event.target.files?.[0]; if (picked) { // iPhone logos are often HEIC, which most browsers can't preview; convert. const file = await convertHeicToJpeg(picked); if (imagePreviewUrl) { URL.revokeObjectURL(imagePreviewUrl); } const newImageUrl = URL.createObjectURL(file); setSelectedImage(file); setImagePreviewUrl(newImageUrl); } }; // Handle banner image selection (from gallery; GIFs preserved) const handleBannerSelect = async ( event: React.ChangeEvent ) => { const picked = event.target.files?.[0]; if (picked) { // convertHeicToJpeg only touches HEIC; GIF/PNG/JPG pass through untouched. const file = await convertHeicToJpeg(picked); if (bannerPreviewUrl) { URL.revokeObjectURL(bannerPreviewUrl); } const newUrl = URL.createObjectURL(file); setSelectedBannerImage(file); setBannerPreviewUrl(newUrl); setFormData((prev) => ({ ...prev, bannerType: "image" })); } }; // 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(); } }; // Handle state change - reset city when state changes const handleStateChange = (value: string) => { setFormData((prev) => ({ ...prev, selectedState: value, selectedCity: "", })); }; const handleBack = () => { if (mode === "create") { navigate(-1); } else { navigate(`/store/${storeId}/setting`); } }; // Handle form submission const handleSubmit = async () => { if (mode === "edit") { // Edit mode submission if (!hasChanges) return; let logoUrl = formData.storeLogo; // Keep existing logo by default // If user selected a new image, upload it first if (selectedImage) { try { setImageUploadOpen(true); const imageResponse = await imageUploadMutation.mutateAsync({ file: selectedImage, usageType: "logo", }); logoUrl = imageResponse.originalUrl || formData.storeLogo; // Use new image or fallback to existing setImageUploadOpen(false); } catch (error) { console.error("Error uploading image:", error); setImageUploadOpen(false); toast({ title: "خطا در آپلود تصویر", description: "متاسفانه خطایی در آپلود تصویر رخ داد. لطفا مجددا تلاش کنید.", variant: "destructive", }); return; // Stop submission if image upload fails } } // Upload a newly selected banner image first (GIFs preserved by the API). let bannerImageUrl = formData.bannerImage; if (selectedBannerImage) { try { setImageUploadOpen(true); const bannerResponse = await imageUploadMutation.mutateAsync({ file: selectedBannerImage, usageType: "banner", }); bannerImageUrl = bannerResponse.originalUrl || formData.bannerImage; setImageUploadOpen(false); } catch (error) { console.error("Error uploading banner:", error); setImageUploadOpen(false); toast({ title: "خطا در آپلود بنر", description: "متاسفانه خطایی در آپلود بنر رخ داد. لطفا مجددا تلاش کنید.", variant: "destructive", }); return; } } const updateData = { storeName: formData.storeName, storeDescription: formData.storeDescription || null, instagramId: formData.instagramId || null, storeLogo: logoUrl || null, bannerType: formData.bannerType || "color", bannerColor: formData.bannerColor || null, bannerImage: bannerImageUrl || null, }; updateSellerMutation.mutate({ username: storeId!, data: updateData, }); return; } // Create mode submission const usernameValid = validateField("username", formData.username); const storeNameValid = validateField("storeName", formData.storeName); const storeDescriptionValid = validateField( "storeDescription", formData.storeDescription ); const instagramIdValid = validateField("instagramId", formData.instagramId); if ( !isValidForm || !usernameValid || !storeNameValid || !storeDescriptionValid || !instagramIdValid ) { toast({ description: "لطفا خطاهای فرم را تصحیح کنید.", variant: "destructive", }); return; } let imageUrl: string | null = null; // If user selected an image, upload it first if (selectedImage) { try { setImageUploadOpen(true); const imageResponse = await imageUploadMutation.mutateAsync({ file: selectedImage, usageType: "logo", }); imageUrl = imageResponse.originalUrl || null; setImageUploadOpen(false); } catch (error) { console.error("Error uploading image:", error); setImageUploadOpen(false); toast({ title: "خطا در آپلود تصویر", description: "متاسفانه خطایی در آپلود تصویر رخ داد. لطفا مجددا تلاش کنید.", variant: "destructive", }); return; } } const applicationData: SellerApplication = { username: formData.username, storeName: formData.storeName, storeDescription: formData.storeDescription, storeLogo: imageUrl, instagramId: formData.instagramId, category: formData.selectedCategory, state: parseInt(formData.selectedState), city: parseInt(formData.selectedCity), }; setCreateSellerOpen(true); try { await createSellerMutation.mutateAsync(applicationData); setCreateSellerOpen(false); toast({ title: "فروشگاه ایجاد شد", description: "فروشگاه شما با موفقیت ایجاد شد.", }); // Small delay to ensure profile is updated before navigation setTimeout(() => { navigate(`/store/${applicationData.username}`); }, 500); } catch (error) { console.error("Error creating seller application:", error); setCreateSellerOpen(false); toast({ title: "خطا در ثبت درخواست", description: "متاسفانه خطایی در ثبت درخواست رخ داد. لطفا مجددا تلاش کنید.", variant: "destructive", }); } }; // Render header based on mode const renderHeader = () => { if (mode === "create") { return ; } else { return ( ); } }; // Render logo section based on mode const renderLogoSection = () => { // Show the current image or selected image preview const currentImageUrl = imagePreviewUrl || formData.storeLogo; return ( <>

{mode === "create" ? "عکس پروفایل فروشگاه خود را انتخاب کنید:" : "لوگو فروشگاه:"}

); }; // Render banner editor (edit mode only): color swatches/picker or image/GIF const renderBannerSection = () => { const previewImage = bannerPreviewUrl || formData.bannerImage; const previewColor = formData.bannerColor || DEFAULT_BANNER_COLOR; return (

بنر فروشگاه

{/* Live preview */}
{formData.bannerType === "image" && previewImage ? ( بنر فروشگاه ) : (
)}
{/* Type toggle */}
{formData.bannerType === "color" ? (
{BANNER_PRESETS.map((c) => (
) : (

اندازه پیشنهادی: ۱۶۰۰×۵۰۰ پیکسل. بنر تمام‌عرض نمایش داده می‌شود و فقط ارتفاع آن برش می‌خورد؛ بخش‌های مهم (متن/لوگو) را وسط قرار دهید. فرمت JPG/PNG/WebP یا GIF متحرک.

)}
); }; // Render description field based on mode const renderDescriptionField = () => { if (mode === "create") { return ( handleInputChange("storeDescription", e.target.value) } /> ); } else { return (

توضیحات فروشگاه