diff --git a/app/components/store/StoreForm.tsx b/app/components/store/StoreForm.tsx index 2785f44..79739c1 100644 --- a/app/components/store/StoreForm.tsx +++ b/app/components/store/StoreForm.tsx @@ -1,6 +1,6 @@ import { useState, useRef, useEffect } from "react"; import { useNavigate } from "@remix-run/react"; -import { Loader2, Save, Store } from "lucide-react"; +import { Loader2, Save, Store, Upload } from "lucide-react"; import ProfilePagesHeader from "../ProfilePagesHeader"; import HeaderWithSupport from "../HeaderWithSupport"; import AppInput from "../AppInput"; @@ -80,8 +80,26 @@ interface FormData { 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; @@ -109,6 +127,13 @@ export default function StoreForm({ mode, storeId }: StoreFormProps) { 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); @@ -122,6 +147,9 @@ export default function StoreForm({ mode, storeId }: StoreFormProps) { selectedCity: "", instagramId: "", storeLogo: "", + bannerType: "color", + bannerColor: "", + bannerImage: "", }); // Validation errors state @@ -157,6 +185,9 @@ export default function StoreForm({ mode, storeId }: StoreFormProps) { selectedCity: "", // Not available in SellerUpdate instagramId: sellerData.instagramId || "", storeLogo: sellerData.storeLogo || "", + bannerType: sellerData.bannerType || "color", + bannerColor: sellerData.bannerColor || "", + bannerImage: sellerData.bannerImage || "", }; setFormData(initialData); } @@ -170,11 +201,15 @@ export default function StoreForm({ mode, storeId }: StoreFormProps) { formData.storeDescription !== (sellerData.storeDescription || "") || formData.instagramId !== (sellerData.instagramId || "") || formData.storeLogo !== (sellerData.storeLogo || "") || - selectedImage !== null; // Include image changes + 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]); + }, [mode, formData, sellerData, selectedImage, selectedBannerImage]); // Redirect if user already has a store (create mode only) useEffect(() => { @@ -196,6 +231,15 @@ export default function StoreForm({ mode, storeId }: StoreFormProps) { }; }, [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 ( @@ -311,6 +355,24 @@ export default function StoreForm({ mode, storeId }: StoreFormProps) { } }; + // 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); @@ -380,11 +442,38 @@ export default function StoreForm({ mode, storeId }: StoreFormProps) { } } + // 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({ @@ -509,6 +598,116 @@ export default function StoreForm({ mode, storeId }: StoreFormProps) { ); }; + // 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") { @@ -630,6 +829,9 @@ export default function StoreForm({ mode, storeId }: StoreFormProps) {
{renderLogoSection()} + {/* Banner editor (edit mode) */} + {mode === "edit" && renderBannerSection()} + {/* Username Input - Only for create mode */} {mode === "create" && ( { storeDescription?: string | null; storeLogo?: string | null; instagramId?: string | null; + bannerType?: string | null; + bannerColor?: string | null; + bannerImage?: string | null; }; }) => { if (!token) throw new Error("Authentication required"); diff --git a/app/routes/seller.$sellerId._index.tsx b/app/routes/seller.$sellerId._index.tsx index 99c6b47..a65ecb8 100644 --- a/app/routes/seller.$sellerId._index.tsx +++ b/app/routes/seller.$sellerId._index.tsx @@ -21,6 +21,7 @@ import { BadgeCheck, ArrowRight, MessageCircle, + Search, } from "lucide-react"; import { useState, useEffect } from "react"; import { DrawerContent, Drawer } from "../components/ui/drawer"; @@ -33,7 +34,8 @@ import UiProvider from "../components/UiProvider"; import { useRootData } from "~/hooks/use-root-data"; import { LoginRequiredDialog } from "~/components/LoginRequiredDialog"; import SellerLogo from "../components/SellerLogo"; -import SearchTopBar from "~/components/SearchTopBar"; +import AppInput from "~/components/AppInput"; +import FilterDrawer from "~/components/FilterDrawer"; interface LoaderData { seller: SellerType; @@ -78,7 +80,7 @@ export async function loader({ params }: LoaderFunctionArgs) { export default function Seller() { const { sellerId } = useParams(); const { seller } = useLoaderData(); - const [searchParams] = useSearchParams(); + const [searchParams, setSearchParams] = useSearchParams(); const { data: isFollowed, isLoading: isFollowStatusLoading } = useSellerFollowStatus(seller?.id || ""); const { mutate: followSeller, isPending: isFollowPending } = @@ -123,6 +125,17 @@ export default function Seller() { setSearchValue(searchParams.get("q") || ""); }, [searchParams]); + // Apply the in-header product search to the URL (drives useGetSellerProducts) + const handleProductSearch = () => { + const params = new URLSearchParams(searchParams); + if (searchValue.trim()) { + params.set("q", searchValue); + } else { + params.delete("q"); + } + setSearchParams(params, { replace: true }); + }; + const handleFollowToggle = () => { if (!user) { setIsLoginDialogOpen(true); @@ -185,6 +198,18 @@ export default function Seller() { const handleBack = () => { safeGoBack(navigate); }; + + // Customizable banner: image, solid color, or the default gradient fallback. + const bannerIsImage = seller?.bannerType === "image" && !!seller?.bannerImage; + const bannerIsColor = seller?.bannerType === "color" && !!seller?.bannerColor; + const bannerBgClass = + bannerIsImage || bannerIsColor + ? "" + : "bg-gradient-to-br from-VITROWN_BLUE to-[#14213d]"; + const bannerStyle = bannerIsColor + ? { backgroundColor: seller?.bannerColor as string } + : undefined; + return ( {/* Desktop brand header (cover + overlapping logo + stats + actions) */}
-
+
+ {bannerIsImage ? ( + + ) : null}
@@ -284,10 +319,20 @@ export default function Seller() {
- {/* Mobile brand header (cover + overlapping logo + stats + actions) */} + {/* Mobile brand header (compact: ~chrome small so products fill the screen) */}
{/* Cover */} -
+
+ {bannerIsImage ? ( + + ) : null}
-

+

{seller?.storeName} {seller?.isVerified ? ( ) : null}

- {seller?.username ? ( -

- @{seller.username} -

- ) : null} - {/* Stats */} -
-
- - {seller?.productCount ?? 0} - - محصول -
-
- - {seller?.followerCount ?? 0} - - دنبال‌کننده -
-
- - {seller?.productReviewsCount ? seller?.reviewAverage : "—"} - {seller?.productReviewsCount ? ( - - ) : null} - - امتیاز -
+ {/* Handle + rating inline (followers/product count removed) */} +
+ {seller?.username ? ( +

+ @{seller.username} +

+ ) : null} + {seller?.productReviewsCount ? ( + + + {seller?.reviewAverage} + + ) : null}
{/* Description */} {seller?.storeDescription ? ( -
+
) : null} - {/* Actions */} -
+ {/* Actions: follow + search + filter together */} +
- +
+ { + if (e.key === "Enter") handleProductSearch(); + }} + className="bg-WHITE2 focus:border-blue-500 !h-10" + inputValue={searchValue} + inputPlaceholder="جستجو در محصولات..." + inputOnChange={(e) => setSearchValue(e.target.value)} + /> + +
-
- {/* Search Bar (mobile) */} -
- -
+
{/* seller products */} diff --git a/app/routes/sellers.tsx b/app/routes/sellers.tsx index 8d69cb7..9fdfa6d 100644 --- a/app/routes/sellers.tsx +++ b/app/routes/sellers.tsx @@ -101,16 +101,6 @@ export default function Sellers() {
)} - {seller.sellerName ? ( - <> -
-
- - {seller.sellerName} - -
- - ) : null} ))}
diff --git a/src/api/types/models/index.ts b/src/api/types/models/index.ts index a5ec8f7..cfa488f 100644 --- a/src/api/types/models/index.ts +++ b/src/api/types/models/index.ts @@ -4457,17 +4457,35 @@ export interface Seller { */ category?: string | null; /** - * + * * @type {number} * @memberof Seller */ state?: number | null; /** - * + * * @type {number} * @memberof Seller */ city?: number | null; + /** + * Banner type: "color" or "image". + * @type {string} + * @memberof Seller + */ + bannerType?: string | null; + /** + * Hex banner color when bannerType is "color". + * @type {string} + * @memberof Seller + */ + bannerColor?: string | null; + /** + * Banner image/GIF URL when bannerType is "image". + * @type {string} + * @memberof Seller + */ + bannerImage?: string | null; } @@ -5584,14 +5602,32 @@ export interface SellerUpdate { */ storeLogo?: string | null; /** - * + * * @type {string} * @memberof SellerUpdate */ instagramId?: string | null; + /** + * Banner type: "color" or "image". + * @type {string} + * @memberof SellerUpdate + */ + bannerType?: string | null; + /** + * Hex banner color when bannerType is "color". + * @type {string} + * @memberof SellerUpdate + */ + bannerColor?: string | null; + /** + * Banner image/GIF URL when bannerType is "image". + * @type {string} + * @memberof SellerUpdate + */ + bannerImage?: string | null; } /** - * + * * @export * @interface SellerVariantAttributeKV */