Vitron-Front/app/components/store/StoreForm.tsx
2026-04-29 01:44:16 +03:30

839 lines
25 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

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 { useState, useRef, useEffect } from "react";
import { useNavigate } from "@remix-run/react";
import { Loader2, Save, Store } 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 { 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;
}
interface StoreFormProps {
mode: "create" | "edit";
storeId?: string;
}
export default function StoreForm({ mode, storeId }: StoreFormProps) {
const navigate = useNavigate();
const { toast } = useToast();
const fileInputRef = useRef<HTMLInputElement>(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 } = useStoreOwnership(
mode === "edit" ? storeId : undefined,
mode
);
// Form state
const [selectedImage, setSelectedImage] = useState<File | null>(null);
const [imagePreviewUrl, setImagePreviewUrl] = useState<string | null>(null);
const [isDrawerOpen, setIsDrawerOpen] = useState(false);
const [hasChanges, setHasChanges] = useState(false);
// Loading modal states
const [createSellerOpen, setCreateSellerOpen] = useState(false);
const [imageUploadOpen, setImageUploadOpen] = useState(false);
const [formData, setFormData] = useState<FormData>({
username: "",
storeName: "",
storeDescription: "",
selectedCategory: "",
selectedState: "",
selectedCity: "",
instagramId: "",
storeLogo: "",
});
// Validation errors state
const [validationErrors, setValidationErrors] = useState<ValidationErrors>({
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 || "",
};
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 || "") ||
selectedImage !== null; // Include image changes
setHasChanges(hasFormChanges);
}
}, [mode, formData, sellerData, selectedImage]);
// 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]);
// Show loading while checking ownership or loading seller data (edit mode)
if (mode === "edit" && (isOwnershipLoading || isSellerLoading)) {
return (
<div className="flex items-center justify-center min-h-[500px]">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
</div>
);
}
// 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 = (event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0];
if (file) {
if (imagePreviewUrl) {
URL.revokeObjectURL(imagePreviewUrl);
}
const newImageUrl = URL.createObjectURL(file);
setSelectedImage(file);
setImagePreviewUrl(newImageUrl);
}
};
// 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
}
}
const updateData = {
storeName: formData.storeName,
storeDescription: formData.storeDescription || null,
instagramId: formData.instagramId || null,
storeLogo: logoUrl || 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 <ProfilePagesHeader title="ایجاد فروشگاه" />;
} else {
return (
<HeaderWithSupport title="ویرایش اطلاعات فروشگاه" onBack={handleBack} />
);
}
};
// Render logo section based on mode
const renderLogoSection = () => {
// Show the current image or selected image preview
const currentImageUrl = imagePreviewUrl || formData.storeLogo;
return (
<>
<p className="text-R12 text-center text-BLACK2">
{mode === "create"
? "عکس پروفایل فروشگاه خود را انتخاب کنید:"
: "لوگو فروشگاه:"}
</p>
<ImageUpload
imagePreviewUrl={currentImageUrl}
onUploadClick={handleUploadClick}
onImageSelect={handleImageSelect}
/>
</>
);
};
// Render description field based on mode
const renderDescriptionField = () => {
if (mode === "create") {
return (
<FormField
label="توضیحات فروشگاه"
required
error={validationErrors.storeDescription}
>
<AppTextArea
inputTitle=""
inputValue={formData.storeDescription}
inputPlaceholder="در مورد فروشگاه خود توضیح دهید..."
inputOnChange={(e) =>
handleInputChange("storeDescription", e.target.value)
}
/>
</FormField>
);
} else {
return (
<div className="flex flex-col gap-2">
<p className="text-R14">توضیحات فروشگاه</p>
<Textarea
placeholder="توضیحات کوتاهی از فروشگاه خود بنویسید..."
value={formData.storeDescription}
onChange={(e) =>
handleInputChange("storeDescription", e.target.value)
}
className="min-h-[100px] resize-none"
dir="rtl"
/>
</div>
);
}
};
// Render submit button
const renderSubmitButton = () => {
if (mode === "create") {
return (
<Button
disabled={
!isValidForm ||
createSellerMutation.isPending ||
imageUploadMutation.isPending
}
className="w-full"
size="xl"
variant="dark"
onClick={handleSubmit}
>
{imageUploadMutation.isPending ? (
<>
<Loader2 className="animate-spin" />
در حال آپلود تصویر...
</>
) : createSellerMutation.isPending ? (
<>
<Loader2 className="animate-spin" />
در حال ثبت...
</>
) : (
"ایجاد فروشگاه"
)}
</Button>
);
} else {
return (
<div className="sticky bottom-0 bg-WHITE border-t border-WHITE2 p-4">
<Button
onClick={handleSubmit}
disabled={
!hasChanges ||
updateSellerMutation.isPending ||
imageUploadMutation.isPending ||
!formData.storeName.trim()
}
className="w-full"
size="lg"
variant={hasChanges ? "dark" : "outline"}
>
{imageUploadMutation.isPending ? (
<>
<Loader2 className="w-4 h-4 animate-spin mr-2" />
در حال آپلود تصویر...
</>
) : updateSellerMutation.isPending ? (
<>
<Loader2 className="w-4 h-4 animate-spin mr-2" />
در حال ذخیره...
</>
) : (
<>
<Save className="w-4 h-4 mr-2" />
{hasChanges ? "ذخیره تغییرات" : "بدون تغییر"}
</>
)}
</Button>
</div>
);
}
};
const containerClass =
mode === "create"
? "flex flex-col gap-0 w-full pb-20"
: "flex flex-col bg-WHITE";
const contentClass =
mode === "create"
? "flex flex-col gap-6 w-full px-4 pt-6"
: "flex flex-col gap-6 p-4 bg-WHITE";
return (
<div className={containerClass}>
{renderHeader()}
<div className={contentClass}>
{renderLogoSection()}
{/* Username Input - Only for create mode */}
{mode === "create" && (
<FormField
label="نام کاربری"
required
error={validationErrors.username}
>
<AppInput
inputIcon={Store}
inputTitle=""
inputValue={formData.username}
inputPlaceholder="username"
inputDir="ltr"
inputOnChange={(e) =>
handleInputChange("username", e.target.value)
}
/>
</FormField>
)}
{/* Store Name Input */}
{mode === "create" ? (
<FormField
label="نام فروشگاه"
required
error={validationErrors.storeName}
>
<AppInput
inputIcon={Store}
inputTitle=""
inputValue={formData.storeName}
inputPlaceholder="نام فروشگاه خود را وارد کنید"
inputDir="rtl"
inputOnChange={(e) =>
handleInputChange("storeName", e.target.value)
}
/>
</FormField>
) : (
<AppInput
inputTitle="نام فروشگاه"
inputValue={formData.storeName}
inputPlaceholder="نام فروشگاه خود را وارد کنید"
inputOnChange={(e) =>
handleInputChange("storeName", e.target.value)
}
inputDir="rtl"
/>
)}
{/* Store Description */}
{renderDescriptionField()}
{/* Instagram ID */}
{mode === "create" ? (
<FormField
label="آیدی اینستاگرام"
required
error={validationErrors.instagramId}
>
<AppInput
inputTitle=""
inputValue={formData.instagramId}
inputPlaceholder="instagram id"
inputDir="ltr"
inputOnChange={(e) =>
handleInputChange("instagramId", e.target.value)
}
/>
</FormField>
) : (
<AppInput
inputTitle="آیدی اینستاگرام"
inputValue={formData.instagramId}
inputPlaceholder="@username"
inputOnChange={(e) =>
handleInputChange("instagramId", e.target.value)
}
inputDir="ltr"
/>
)}
{/* Category Selection - Only for create mode */}
{mode === "create" && (
<FormField label="دسته‌بندی" required>
<AppSelectBox
items={categoryOptions}
value={formData.selectedCategory}
setValue={(value) => {
const newValue =
typeof value === "function"
? value(formData.selectedCategory)
: value;
handleInputChange("selectedCategory", newValue);
}}
placeholder="انتخاب دسته‌بندی"
loading={categoriesLoading}
haveSearchbar={true}
width="w-full"
/>
</FormField>
)}
{/* State and City Selection - Only for create mode */}
{mode === "create" && (
<div className="flex gap-2 w-full">
<FormField label="استان" required>
<AppSelectBox
items={stateOptions}
value={formData.selectedState}
setValue={(value) => {
const newValue =
typeof value === "function"
? value(formData.selectedState)
: value;
handleStateChange(newValue);
}}
placeholder="انتخاب استان"
loading={stateCityLoading}
haveSearchbar={true}
width="w-full"
/>
</FormField>
<FormField label="شهر" required>
<AppSelectBox
items={cityOptions}
value={formData.selectedCity}
setValue={(value) => {
const newValue =
typeof value === "function"
? value(formData.selectedCity)
: value;
handleInputChange("selectedCity", newValue);
}}
placeholder={
formData.selectedState
? "انتخاب شهر"
: "ابتدا استان را انتخاب کنید"
}
loading={false}
haveSearchbar={true}
width="w-full"
disabled={!formData.selectedState}
/>
</FormField>
</div>
)}
{/* Store Status Display - Only for edit mode */}
{mode === "edit" && sellerData && (
<div className="p-4 bg-WHITE rounded-lg border">
<div className="flex w-full items-center justify-between">
<div className="flex flex-col gap-1 w-full">
<div className="flex w-full gap-1 items-center">
<p className="text-R12">وضعیت فروشگاه</p>
<span
className={`px-2 py-1 rounded-full text-xs ${
sellerData?.isVerified
? "bg-green-100 text-green-800"
: "bg-yellow-100 text-yellow-800"
}`}
>
{sellerData?.isVerified ? "فعال" : "در انتظار تایید"}
</span>
</div>
<p className="text-R10 text-BLACK2">
{sellerData?.isVerified
? "فروشگاه شما فعال است و برای مخاطبان نمایش داده می‌شود"
: "فروشگاه شما در انتظار تایید است"}
</p>
</div>
</div>
</div>
)}
{/* Submit Button */}
{renderSubmitButton()}
</div>
{/* Image Upload Drawer - Available for both modes */}
<ImageUploadDrawer
isOpen={isDrawerOpen}
onOpenChange={setIsDrawerOpen}
onCameraSelect={handleCameraSelect}
onGallerySelect={handleGallerySelect}
/>
{/* Loading Modals */}
<LoadingModal
open={createSellerOpen}
setOpen={setCreateSellerOpen}
title="در حال بررسی..."
/>
<LoadingModal
open={imageUploadOpen}
setOpen={setImageUploadOpen}
title="در حال آپلود تصویر..."
/>
{/* Hidden file input - Available for both modes */}
<input
ref={fileInputRef}
type="file"
accept="image/*"
onChange={handleImageSelect}
className="hidden"
/>
</div>
);
}