- useMyStores + team hooks (invite by phone / list / remove) in use-seller-hooks. - useStoreOwnership rewritten to allow staff (not just the owner) via my-stores; exposes `role` so callers can gate owner-only UI, and no longer redirects members away from a store they help manage. - Store settings: owner-only items (financial, shipping, edit store, instagram sync, team) hidden from staff; new "مدیریت اعضای تیم" entry (owner-only). - New /store/:storeId/team page: invite by phone + members list with pending/ active status + remove (owner-only, redirects staff). MyLayout keeps store mode on the team route. - StoreForm edit is owner-only (redirects staff). Profile "enter my store" now routes owners to their store and staff to the store they manage. - Chat already split earlier: seller inbox vs personal buyer inbox. Depends on the seller-team backend; ships on this branch, not main. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1053 lines
33 KiB
TypeScript
1053 lines
33 KiB
TypeScript
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<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, 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<File | null>(null);
|
||
const [imagePreviewUrl, setImagePreviewUrl] = useState<string | null>(null);
|
||
const [isDrawerOpen, setIsDrawerOpen] = useState(false);
|
||
const [hasChanges, setHasChanges] = useState(false);
|
||
|
||
// Banner state (edit mode)
|
||
const bannerFileInputRef = useRef<HTMLInputElement>(null);
|
||
const [selectedBannerImage, setSelectedBannerImage] = useState<File | null>(
|
||
null
|
||
);
|
||
const [bannerPreviewUrl, setBannerPreviewUrl] = useState<string | null>(null);
|
||
|
||
// 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: "",
|
||
bannerType: "color",
|
||
bannerColor: "",
|
||
bannerImage: "",
|
||
});
|
||
|
||
// 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 || "",
|
||
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 (
|
||
<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 = async (
|
||
event: React.ChangeEvent<HTMLInputElement>
|
||
) => {
|
||
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<HTMLInputElement>
|
||
) => {
|
||
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 <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 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 (
|
||
<div className="flex flex-col gap-3">
|
||
<p className="text-R14">بنر فروشگاه</p>
|
||
|
||
{/* Live preview */}
|
||
<div className="relative h-28 w-full rounded-[14px] overflow-hidden border border-WHITE2 bg-WHITE3">
|
||
{formData.bannerType === "image" && previewImage ? (
|
||
<img
|
||
src={previewImage}
|
||
alt="بنر فروشگاه"
|
||
className="w-full h-full object-cover"
|
||
/>
|
||
) : (
|
||
<div
|
||
className="w-full h-full"
|
||
style={{ backgroundColor: previewColor }}
|
||
/>
|
||
)}
|
||
</div>
|
||
|
||
{/* Type toggle */}
|
||
<div className="flex gap-2">
|
||
<button
|
||
type="button"
|
||
onClick={() => handleInputChange("bannerType", "color")}
|
||
className={`flex-1 h-10 rounded-[10px] border text-R12 ${
|
||
formData.bannerType === "color"
|
||
? "bg-BLACK text-WHITE border-BLACK"
|
||
: "bg-WHITE2 text-BLACK border-WHITE3"
|
||
}`}
|
||
>
|
||
رنگ
|
||
</button>
|
||
<button
|
||
type="button"
|
||
onClick={() => handleInputChange("bannerType", "image")}
|
||
className={`flex-1 h-10 rounded-[10px] border text-R12 ${
|
||
formData.bannerType === "image"
|
||
? "bg-BLACK text-WHITE border-BLACK"
|
||
: "bg-WHITE2 text-BLACK border-WHITE3"
|
||
}`}
|
||
>
|
||
تصویر
|
||
</button>
|
||
</div>
|
||
|
||
{formData.bannerType === "color" ? (
|
||
<div className="flex flex-wrap items-center gap-2">
|
||
{BANNER_PRESETS.map((c) => (
|
||
<button
|
||
key={c}
|
||
type="button"
|
||
onClick={() => handleInputChange("bannerColor", c)}
|
||
aria-label={c}
|
||
className={`w-8 h-8 rounded-full border-2 transition-all ${
|
||
(formData.bannerColor || "").toLowerCase() === c.toLowerCase()
|
||
? "border-BLACK scale-110"
|
||
: "border-WHITE2"
|
||
}`}
|
||
style={{ backgroundColor: c }}
|
||
/>
|
||
))}
|
||
{/* Custom color */}
|
||
<label
|
||
className="w-8 h-8 rounded-full border-2 border-dashed border-GRAY3 overflow-hidden flex items-center justify-center cursor-pointer"
|
||
aria-label="رنگ دلخواه"
|
||
>
|
||
<input
|
||
type="color"
|
||
value={previewColor}
|
||
onChange={(e) =>
|
||
handleInputChange("bannerColor", e.target.value)
|
||
}
|
||
className="w-10 h-10 cursor-pointer border-0 bg-transparent p-0"
|
||
/>
|
||
</label>
|
||
</div>
|
||
) : (
|
||
<div className="flex flex-col gap-2">
|
||
<button
|
||
type="button"
|
||
onClick={() => bannerFileInputRef.current?.click()}
|
||
className="w-full h-11 rounded-[10px] border-2 border-dashed border-GRAY3 flex items-center justify-center gap-2 text-GRAY text-R12 hover:border-primary hover:text-primary transition-colors"
|
||
>
|
||
<Upload size={18} />
|
||
{previewImage ? "تغییر تصویر بنر" : "آپلود تصویر بنر"}
|
||
</button>
|
||
<p className="text-R10 text-BLACK2 leading-5">
|
||
اندازه پیشنهادی: ۱۶۰۰×۵۰۰ پیکسل. بنر تمامعرض نمایش داده میشود و
|
||
فقط ارتفاع آن برش میخورد؛ بخشهای مهم (متن/لوگو) را وسط قرار دهید.
|
||
فرمت JPG/PNG/WebP یا GIF متحرک.
|
||
</p>
|
||
</div>
|
||
)}
|
||
|
||
<input
|
||
ref={bannerFileInputRef}
|
||
type="file"
|
||
accept="image/*,image/gif"
|
||
onChange={handleBannerSelect}
|
||
className="hidden"
|
||
/>
|
||
</div>
|
||
);
|
||
};
|
||
|
||
// 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()}
|
||
|
||
{/* Banner editor (edit mode) */}
|
||
{mode === "edit" && renderBannerSection()}
|
||
|
||
{/* 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>
|
||
);
|
||
}
|