feat(seller): customizable profile banner + seller page polish
- Banner editor in store edit: color swatches + custom color picker, or image/GIF upload with a 1600x500 dimension hint. Renders the chosen banner (image cover / solid color) on the public seller page, falling back to the default gradient. - Sellers list: drop the desktop name overlay (match mobile, image-only tiles). - Seller page mobile header: chat moved top-left under the banner, search+filter beside the follow button, followers/product counts removed (rating kept), compacted so products fill more of the screen, circular logo, thinner action row. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
bb10907463
commit
660e01c3c3
@ -1,6 +1,6 @@
|
|||||||
import { useState, useRef, useEffect } from "react";
|
import { useState, useRef, useEffect } from "react";
|
||||||
import { useNavigate } from "@remix-run/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 ProfilePagesHeader from "../ProfilePagesHeader";
|
||||||
import HeaderWithSupport from "../HeaderWithSupport";
|
import HeaderWithSupport from "../HeaderWithSupport";
|
||||||
import AppInput from "../AppInput";
|
import AppInput from "../AppInput";
|
||||||
@ -80,8 +80,26 @@ interface FormData {
|
|||||||
selectedCity: string;
|
selectedCity: string;
|
||||||
instagramId: string;
|
instagramId: string;
|
||||||
storeLogo: 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 {
|
interface StoreFormProps {
|
||||||
mode: "create" | "edit";
|
mode: "create" | "edit";
|
||||||
storeId?: string;
|
storeId?: string;
|
||||||
@ -109,6 +127,13 @@ export default function StoreForm({ mode, storeId }: StoreFormProps) {
|
|||||||
const [isDrawerOpen, setIsDrawerOpen] = useState(false);
|
const [isDrawerOpen, setIsDrawerOpen] = useState(false);
|
||||||
const [hasChanges, setHasChanges] = 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
|
// Loading modal states
|
||||||
const [createSellerOpen, setCreateSellerOpen] = useState(false);
|
const [createSellerOpen, setCreateSellerOpen] = useState(false);
|
||||||
const [imageUploadOpen, setImageUploadOpen] = useState(false);
|
const [imageUploadOpen, setImageUploadOpen] = useState(false);
|
||||||
@ -122,6 +147,9 @@ export default function StoreForm({ mode, storeId }: StoreFormProps) {
|
|||||||
selectedCity: "",
|
selectedCity: "",
|
||||||
instagramId: "",
|
instagramId: "",
|
||||||
storeLogo: "",
|
storeLogo: "",
|
||||||
|
bannerType: "color",
|
||||||
|
bannerColor: "",
|
||||||
|
bannerImage: "",
|
||||||
});
|
});
|
||||||
|
|
||||||
// Validation errors state
|
// Validation errors state
|
||||||
@ -157,6 +185,9 @@ export default function StoreForm({ mode, storeId }: StoreFormProps) {
|
|||||||
selectedCity: "", // Not available in SellerUpdate
|
selectedCity: "", // Not available in SellerUpdate
|
||||||
instagramId: sellerData.instagramId || "",
|
instagramId: sellerData.instagramId || "",
|
||||||
storeLogo: sellerData.storeLogo || "",
|
storeLogo: sellerData.storeLogo || "",
|
||||||
|
bannerType: sellerData.bannerType || "color",
|
||||||
|
bannerColor: sellerData.bannerColor || "",
|
||||||
|
bannerImage: sellerData.bannerImage || "",
|
||||||
};
|
};
|
||||||
setFormData(initialData);
|
setFormData(initialData);
|
||||||
}
|
}
|
||||||
@ -170,11 +201,15 @@ export default function StoreForm({ mode, storeId }: StoreFormProps) {
|
|||||||
formData.storeDescription !== (sellerData.storeDescription || "") ||
|
formData.storeDescription !== (sellerData.storeDescription || "") ||
|
||||||
formData.instagramId !== (sellerData.instagramId || "") ||
|
formData.instagramId !== (sellerData.instagramId || "") ||
|
||||||
formData.storeLogo !== (sellerData.storeLogo || "") ||
|
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);
|
setHasChanges(hasFormChanges);
|
||||||
}
|
}
|
||||||
}, [mode, formData, sellerData, selectedImage]);
|
}, [mode, formData, sellerData, selectedImage, selectedBannerImage]);
|
||||||
|
|
||||||
// Redirect if user already has a store (create mode only)
|
// Redirect if user already has a store (create mode only)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -196,6 +231,15 @@ export default function StoreForm({ mode, storeId }: StoreFormProps) {
|
|||||||
};
|
};
|
||||||
}, [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)
|
// Show loading while checking ownership or loading seller data (edit mode)
|
||||||
if (mode === "edit" && (isOwnershipLoading || isSellerLoading)) {
|
if (mode === "edit" && (isOwnershipLoading || isSellerLoading)) {
|
||||||
return (
|
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<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
|
// Handle drawer actions
|
||||||
const handleUploadClick = () => {
|
const handleUploadClick = () => {
|
||||||
setIsDrawerOpen(true);
|
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 = {
|
const updateData = {
|
||||||
storeName: formData.storeName,
|
storeName: formData.storeName,
|
||||||
storeDescription: formData.storeDescription || null,
|
storeDescription: formData.storeDescription || null,
|
||||||
instagramId: formData.instagramId || null,
|
instagramId: formData.instagramId || null,
|
||||||
storeLogo: logoUrl || null,
|
storeLogo: logoUrl || null,
|
||||||
|
bannerType: formData.bannerType || "color",
|
||||||
|
bannerColor: formData.bannerColor || null,
|
||||||
|
bannerImage: bannerImageUrl || null,
|
||||||
};
|
};
|
||||||
|
|
||||||
updateSellerMutation.mutate({
|
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 (
|
||||||
|
<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
|
// Render description field based on mode
|
||||||
const renderDescriptionField = () => {
|
const renderDescriptionField = () => {
|
||||||
if (mode === "create") {
|
if (mode === "create") {
|
||||||
@ -630,6 +829,9 @@ export default function StoreForm({ mode, storeId }: StoreFormProps) {
|
|||||||
<div className={contentClass}>
|
<div className={contentClass}>
|
||||||
{renderLogoSection()}
|
{renderLogoSection()}
|
||||||
|
|
||||||
|
{/* Banner editor (edit mode) */}
|
||||||
|
{mode === "edit" && renderBannerSection()}
|
||||||
|
|
||||||
{/* Username Input - Only for create mode */}
|
{/* Username Input - Only for create mode */}
|
||||||
{mode === "create" && (
|
{mode === "create" && (
|
||||||
<FormField
|
<FormField
|
||||||
|
|||||||
@ -266,6 +266,9 @@ export const useSellerUpdate = () => {
|
|||||||
storeDescription?: string | null;
|
storeDescription?: string | null;
|
||||||
storeLogo?: string | null;
|
storeLogo?: string | null;
|
||||||
instagramId?: string | null;
|
instagramId?: string | null;
|
||||||
|
bannerType?: string | null;
|
||||||
|
bannerColor?: string | null;
|
||||||
|
bannerImage?: string | null;
|
||||||
};
|
};
|
||||||
}) => {
|
}) => {
|
||||||
if (!token) throw new Error("Authentication required");
|
if (!token) throw new Error("Authentication required");
|
||||||
|
|||||||
@ -21,6 +21,7 @@ import {
|
|||||||
BadgeCheck,
|
BadgeCheck,
|
||||||
ArrowRight,
|
ArrowRight,
|
||||||
MessageCircle,
|
MessageCircle,
|
||||||
|
Search,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { DrawerContent, Drawer } from "../components/ui/drawer";
|
import { DrawerContent, Drawer } from "../components/ui/drawer";
|
||||||
@ -33,7 +34,8 @@ import UiProvider from "../components/UiProvider";
|
|||||||
import { useRootData } from "~/hooks/use-root-data";
|
import { useRootData } from "~/hooks/use-root-data";
|
||||||
import { LoginRequiredDialog } from "~/components/LoginRequiredDialog";
|
import { LoginRequiredDialog } from "~/components/LoginRequiredDialog";
|
||||||
import SellerLogo from "../components/SellerLogo";
|
import SellerLogo from "../components/SellerLogo";
|
||||||
import SearchTopBar from "~/components/SearchTopBar";
|
import AppInput from "~/components/AppInput";
|
||||||
|
import FilterDrawer from "~/components/FilterDrawer";
|
||||||
|
|
||||||
interface LoaderData {
|
interface LoaderData {
|
||||||
seller: SellerType;
|
seller: SellerType;
|
||||||
@ -78,7 +80,7 @@ export async function loader({ params }: LoaderFunctionArgs) {
|
|||||||
export default function Seller() {
|
export default function Seller() {
|
||||||
const { sellerId } = useParams();
|
const { sellerId } = useParams();
|
||||||
const { seller } = useLoaderData<typeof loader>();
|
const { seller } = useLoaderData<typeof loader>();
|
||||||
const [searchParams] = useSearchParams();
|
const [searchParams, setSearchParams] = useSearchParams();
|
||||||
const { data: isFollowed, isLoading: isFollowStatusLoading } =
|
const { data: isFollowed, isLoading: isFollowStatusLoading } =
|
||||||
useSellerFollowStatus(seller?.id || "");
|
useSellerFollowStatus(seller?.id || "");
|
||||||
const { mutate: followSeller, isPending: isFollowPending } =
|
const { mutate: followSeller, isPending: isFollowPending } =
|
||||||
@ -123,6 +125,17 @@ export default function Seller() {
|
|||||||
setSearchValue(searchParams.get("q") || "");
|
setSearchValue(searchParams.get("q") || "");
|
||||||
}, [searchParams]);
|
}, [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 = () => {
|
const handleFollowToggle = () => {
|
||||||
if (!user) {
|
if (!user) {
|
||||||
setIsLoginDialogOpen(true);
|
setIsLoginDialogOpen(true);
|
||||||
@ -185,6 +198,18 @@ export default function Seller() {
|
|||||||
const handleBack = () => {
|
const handleBack = () => {
|
||||||
safeGoBack(navigate);
|
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 (
|
return (
|
||||||
<UiProvider
|
<UiProvider
|
||||||
isLoading={false}
|
isLoading={false}
|
||||||
@ -196,7 +221,17 @@ export default function Seller() {
|
|||||||
<div className="flex flex-col pb-20 lg:max-w-[1100px] lg:mx-auto lg:w-full lg:px-8 lg:pt-8">
|
<div className="flex flex-col pb-20 lg:max-w-[1100px] lg:mx-auto lg:w-full lg:px-8 lg:pt-8">
|
||||||
{/* Desktop brand header (cover + overlapping logo + stats + actions) */}
|
{/* Desktop brand header (cover + overlapping logo + stats + actions) */}
|
||||||
<div className="hidden lg:block">
|
<div className="hidden lg:block">
|
||||||
<div className="relative h-[200px] rounded-[22px] overflow-hidden bg-gradient-to-br from-VITROWN_BLUE to-[#14213d]">
|
<div
|
||||||
|
className={`relative h-[200px] rounded-[22px] overflow-hidden ${bannerBgClass}`}
|
||||||
|
style={bannerStyle}
|
||||||
|
>
|
||||||
|
{bannerIsImage ? (
|
||||||
|
<img
|
||||||
|
src={seller?.bannerImage as string}
|
||||||
|
alt=""
|
||||||
|
className="absolute inset-0 w-full h-full object-cover"
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
<div className="absolute inset-0 bg-gradient-to-t from-black/30 to-transparent" />
|
<div className="absolute inset-0 bg-gradient-to-t from-black/30 to-transparent" />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-start gap-6 px-2 mt-4 relative z-10">
|
<div className="flex items-start gap-6 px-2 mt-4 relative z-10">
|
||||||
@ -284,10 +319,20 @@ export default function Seller() {
|
|||||||
<div className="border-b border-WHITE3 mt-6" />
|
<div className="border-b border-WHITE3 mt-6" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Mobile brand header (cover + overlapping logo + stats + actions) */}
|
{/* Mobile brand header (compact: ~chrome small so products fill the screen) */}
|
||||||
<div className="lg:hidden">
|
<div className="lg:hidden">
|
||||||
{/* Cover */}
|
{/* Cover */}
|
||||||
<div className="relative h-[150px] bg-gradient-to-br from-VITROWN_BLUE to-[#14213d]">
|
<div
|
||||||
|
className={`relative h-[120px] ${bannerBgClass}`}
|
||||||
|
style={bannerStyle}
|
||||||
|
>
|
||||||
|
{bannerIsImage ? (
|
||||||
|
<img
|
||||||
|
src={seller?.bannerImage as string}
|
||||||
|
alt=""
|
||||||
|
className="absolute inset-0 w-full h-full object-cover"
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
<div className="absolute inset-0 bg-gradient-to-t from-black/35 to-transparent" />
|
<div className="absolute inset-0 bg-gradient-to-t from-black/35 to-transparent" />
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@ -301,108 +346,98 @@ export default function Seller() {
|
|||||||
</div>
|
</div>
|
||||||
{/* Head */}
|
{/* Head */}
|
||||||
<div className="px-4 -mt-10 relative z-10">
|
<div className="px-4 -mt-10 relative z-10">
|
||||||
<div className="w-[84px] h-[84px] rounded-[24px] border-4 border-WHITE shadow-md overflow-hidden bg-WHITE3">
|
{/* Logo (start/right) + Chat (end/left, just under the banner) */}
|
||||||
{seller?.storeLogo ? (
|
<div className="flex items-end justify-between">
|
||||||
<img
|
<div className="w-[84px] h-[84px] rounded-full border-4 border-WHITE shadow-md overflow-hidden bg-WHITE3">
|
||||||
src={seller.storeLogo}
|
{seller?.storeLogo ? (
|
||||||
alt={seller?.storeName || "store"}
|
<img
|
||||||
className="w-full h-full object-cover"
|
src={seller.storeLogo}
|
||||||
/>
|
alt={seller?.storeName || "store"}
|
||||||
) : null}
|
className="w-full h-full object-cover"
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleCreateChatThread}
|
||||||
|
disabled={!seller?.id || isCreatingChatThread}
|
||||||
|
aria-label="ارسال پیام"
|
||||||
|
className="w-10 h-10 rounded-full bg-WHITE shadow-md border border-WHITE3 flex items-center justify-center text-BLACK active:scale-95 transition-transform disabled:opacity-60"
|
||||||
|
>
|
||||||
|
<MessageCircle size={20} />
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<h1 className="flex items-center gap-1.5 text-[21px] font-extrabold mt-3">
|
<h1 className="flex items-center gap-1.5 text-[21px] font-extrabold mt-2">
|
||||||
{seller?.storeName}
|
{seller?.storeName}
|
||||||
{seller?.isVerified ? (
|
{seller?.isVerified ? (
|
||||||
<BadgeCheck className="text-BLUE" size={18} />
|
<BadgeCheck className="text-BLUE" size={18} />
|
||||||
) : null}
|
) : null}
|
||||||
</h1>
|
</h1>
|
||||||
{seller?.username ? (
|
{/* Handle + rating inline (followers/product count removed) */}
|
||||||
<p className="text-GRAY text-[13px] mt-0.5" dir="ltr">
|
<div className="flex items-center gap-2 mt-0.5">
|
||||||
@{seller.username}
|
{seller?.username ? (
|
||||||
</p>
|
<p className="text-GRAY text-[13px]" dir="ltr">
|
||||||
) : null}
|
@{seller.username}
|
||||||
{/* Stats */}
|
</p>
|
||||||
<div className="flex gap-8 py-3.5 mt-3 border-y border-WHITE3">
|
) : null}
|
||||||
<div>
|
{seller?.productReviewsCount ? (
|
||||||
<b className="block text-[17px] font-extrabold">
|
<span className="flex items-center gap-1 text-[13px] font-bold">
|
||||||
{seller?.productCount ?? 0}
|
<Star size={13} className="fill-BLACK" />
|
||||||
</b>
|
{seller?.reviewAverage}
|
||||||
<span className="text-GRAY text-[11.5px]">محصول</span>
|
</span>
|
||||||
</div>
|
) : null}
|
||||||
<div>
|
|
||||||
<b className="block text-[17px] font-extrabold">
|
|
||||||
{seller?.followerCount ?? 0}
|
|
||||||
</b>
|
|
||||||
<span className="text-GRAY text-[11.5px]">دنبالکننده</span>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<b className="flex items-center gap-1 text-[17px] font-extrabold">
|
|
||||||
{seller?.productReviewsCount ? seller?.reviewAverage : "—"}
|
|
||||||
{seller?.productReviewsCount ? (
|
|
||||||
<Star size={14} className="fill-BLACK" />
|
|
||||||
) : null}
|
|
||||||
</b>
|
|
||||||
<span className="text-GRAY text-[11.5px]">امتیاز</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
{/* Description */}
|
{/* Description */}
|
||||||
{seller?.storeDescription ? (
|
{seller?.storeDescription ? (
|
||||||
<div className="mt-3">
|
<div className="mt-2">
|
||||||
<ReadMoreText
|
<ReadMoreText
|
||||||
sellerData={seller}
|
sellerData={seller}
|
||||||
text={seller?.storeDescription || ""}
|
text={seller?.storeDescription || ""}
|
||||||
maxLength={150}
|
maxLength={110}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
{/* Actions */}
|
{/* Actions: follow + search + filter together */}
|
||||||
<div className="flex gap-2.5 mt-4">
|
<div className="flex items-center gap-2 mt-3">
|
||||||
<Button
|
<Button
|
||||||
variant="dark"
|
variant="dark"
|
||||||
size="lg"
|
size="lg"
|
||||||
className="flex-1"
|
className="shrink-0 !h-10"
|
||||||
onClick={handleFollowToggle}
|
onClick={handleFollowToggle}
|
||||||
disabled={isFollowMutating || isFollowStatusLoading}
|
disabled={isFollowMutating || isFollowStatusLoading}
|
||||||
>
|
>
|
||||||
{isFollowed ? (
|
{isFollowed ? (
|
||||||
<>
|
<>
|
||||||
<Check size={22} />
|
<Check size={20} />
|
||||||
دنبال شده
|
دنبال شده
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<UserPlus size={22} />
|
<UserPlus size={20} />
|
||||||
دنبال کردن
|
دنبال کردن
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<div className="flex-1 flex items-center gap-2">
|
||||||
variant="outline"
|
<AppInput
|
||||||
size="lg"
|
inputIcon={Search}
|
||||||
className="shrink-0 px-4"
|
inputDir="rtl"
|
||||||
aria-label="ارسال پیام"
|
inputTitle={""}
|
||||||
onClick={() => {
|
onKeyDown={(e) => {
|
||||||
handleCreateChatThread();
|
if (e.key === "Enter") handleProductSearch();
|
||||||
}}
|
}}
|
||||||
disabled={!seller?.id || isCreatingChatThread}
|
className="bg-WHITE2 focus:border-blue-500 !h-10"
|
||||||
>
|
inputValue={searchValue}
|
||||||
<MessageCircle size={22} />
|
inputPlaceholder="جستجو در محصولات..."
|
||||||
</Button>
|
inputOnChange={(e) => setSearchValue(e.target.value)}
|
||||||
|
/>
|
||||||
|
<FilterDrawer minimumPrice={0} maximumPrice={10000000} />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="w-full">
|
<div className="w-full mt-3 lg:mt-0">
|
||||||
{/* Search Bar (mobile) */}
|
|
||||||
<div className="lg:hidden">
|
|
||||||
<SearchTopBar
|
|
||||||
searchValue={searchValue}
|
|
||||||
setSearchValue={setSearchValue}
|
|
||||||
placeholder="جستجو در محصولات فروشگاه..."
|
|
||||||
minimumPrice={0}
|
|
||||||
maximumPrice={10000000}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="hidden lg:block lg:mt-2" />
|
<div className="hidden lg:block lg:mt-2" />
|
||||||
|
|
||||||
{/* seller products */}
|
{/* seller products */}
|
||||||
|
|||||||
@ -101,16 +101,6 @@ export default function Sellers() {
|
|||||||
<Store size={48} className="text-gray-400" />
|
<Store size={48} className="text-gray-400" />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{seller.sellerName ? (
|
|
||||||
<>
|
|
||||||
<div className="hidden lg:block absolute inset-0 bg-gradient-to-t from-black/60 to-transparent" />
|
|
||||||
<div className="hidden lg:flex absolute inset-x-0 bottom-0 p-4 items-end">
|
|
||||||
<span className="text-white font-bold text-[15px] truncate">
|
|
||||||
{seller.sellerName}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
) : null}
|
|
||||||
</Link>
|
</Link>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -4457,17 +4457,35 @@ export interface Seller {
|
|||||||
*/
|
*/
|
||||||
category?: string | null;
|
category?: string | null;
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @type {number}
|
* @type {number}
|
||||||
* @memberof Seller
|
* @memberof Seller
|
||||||
*/
|
*/
|
||||||
state?: number | null;
|
state?: number | null;
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @type {number}
|
* @type {number}
|
||||||
* @memberof Seller
|
* @memberof Seller
|
||||||
*/
|
*/
|
||||||
city?: number | null;
|
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;
|
storeLogo?: string | null;
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @type {string}
|
* @type {string}
|
||||||
* @memberof SellerUpdate
|
* @memberof SellerUpdate
|
||||||
*/
|
*/
|
||||||
instagramId?: string | null;
|
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
|
* @export
|
||||||
* @interface SellerVariantAttributeKV
|
* @interface SellerVariantAttributeKV
|
||||||
*/
|
*/
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user