Per design feedback, restructure the mobile product page (desktop layout unchanged except color swatches): - remove the top seller bar; the seller/brand (logo + name + ارسال پیام) now sits in a row below the gallery. - add a floating circular back button over the image (safe-area aware). - move the rating out of the action row into a prominent body row, shown only when the average rating is > 0. - color swatches are now circles with a selection ring (shared with desktop for consistency) instead of squares. - make the افزودن به سبد خرید button full-width/taller with a compact price beside it. - the action row keeps save / share / add-to-collection / try-on. - the red discount icon is kept as-is. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
323 lines
11 KiB
TypeScript
323 lines
11 KiB
TypeScript
import {
|
||
BookmarkMinus,
|
||
Bookmark,
|
||
MessageSquare,
|
||
Send,
|
||
Share2,
|
||
X,
|
||
Loader2,
|
||
Copy,
|
||
Layers,
|
||
Sparkles,
|
||
} from "lucide-react";
|
||
import { useState, useEffect } from "react";
|
||
import {
|
||
Drawer,
|
||
DrawerContent,
|
||
DrawerHeader,
|
||
DrawerTitle,
|
||
} from "~/components/ui/drawer";
|
||
import { Skeleton } from "~/components/ui/skeleton";
|
||
import {
|
||
useServiceBookmarkProduct,
|
||
useServiceDeleteBookmark,
|
||
useServiceCheckBookmark,
|
||
} from "~/utils/RequestHandler";
|
||
import { useToast } from "~/hooks/use-toast";
|
||
import { useRootData } from "~/hooks/use-root-data";
|
||
import { LoginRequiredDialog } from "../LoginRequiredDialog";
|
||
import { AddToSetDrawer } from "../sets";
|
||
import AIPromoteModal from "../AIPromoteModal";
|
||
|
||
export const ProductActions = ({ productId }: { productId: string }) => {
|
||
const [isShareDrawerOpen, setIsShareDrawerOpen] = useState(false);
|
||
const [isLoginModalOpen, setIsLoginModalOpen] = useState(false);
|
||
const [isAddToSetDrawerOpen, setIsAddToSetDrawerOpen] = useState(false);
|
||
const [isAITryOnModalOpen, setIsAITryOnModalOpen] = useState(false);
|
||
const [showAIFeatureTooltip, setShowAIFeatureTooltip] = useState(false);
|
||
|
||
// Show AI feature tooltip on first visit
|
||
useEffect(() => {
|
||
const hasSeenAIFeature = localStorage.getItem("hasSeenAITryOnFeature");
|
||
if (!hasSeenAIFeature) {
|
||
setShowAIFeatureTooltip(true);
|
||
}
|
||
}, []);
|
||
|
||
const handleDismissAITooltip = () => {
|
||
setShowAIFeatureTooltip(false);
|
||
localStorage.setItem("hasSeenAITryOnFeature", "true");
|
||
};
|
||
|
||
const handleOpenAIModal = () => {
|
||
handleDismissAITooltip();
|
||
setIsAITryOnModalOpen(true);
|
||
};
|
||
const [loginDialogMessage, setLoginDialogMessage] = useState(
|
||
"برای افزودن محصول به علاقهمندیها ابتدا باید وارد حساب کاربری خود شوید."
|
||
);
|
||
const { toast } = useToast();
|
||
const { user } = useRootData();
|
||
|
||
const { data: bookmarkData, isLoading: isCheckingBookmark } =
|
||
useServiceCheckBookmark(productId || "");
|
||
const { mutate: bookmarkProduct, isPending: isBookmarking } =
|
||
useServiceBookmarkProduct();
|
||
const { mutate: deleteBookmark, isPending: isDeletingBookmark } =
|
||
useServiceDeleteBookmark();
|
||
|
||
const isBookmarked = bookmarkData?.bookmarked === true;
|
||
|
||
const handleBookmark = () => {
|
||
// Check if user is logged in
|
||
if (!user?.access_token) {
|
||
setIsLoginModalOpen(true);
|
||
return;
|
||
}
|
||
|
||
if (isBookmarked) {
|
||
deleteBookmark(productId, {
|
||
onSuccess: () => {
|
||
toast({
|
||
title: "محصول از علاقهمندیها حذف شد",
|
||
variant: "default",
|
||
duration: 3000,
|
||
});
|
||
},
|
||
onError: () => {
|
||
toast({
|
||
title: "خطا",
|
||
description: "عملیات با خطا مواجه شد. لطفا دوباره تلاش کنید.",
|
||
variant: "destructive",
|
||
duration: 3000,
|
||
});
|
||
},
|
||
});
|
||
} else {
|
||
bookmarkProduct(productId, {
|
||
onSuccess: () => {
|
||
toast({
|
||
title: "محصول به علاقهمندیها اضافه شد",
|
||
variant: "default",
|
||
duration: 3000,
|
||
});
|
||
},
|
||
onError: () => {
|
||
toast({
|
||
title: "خطا",
|
||
description: "عملیات با خطا مواجه شد. لطفا دوباره تلاش کنید.",
|
||
variant: "destructive",
|
||
duration: 3000,
|
||
});
|
||
},
|
||
});
|
||
}
|
||
};
|
||
|
||
const handleAddToSet = () => {
|
||
// Check if user is logged in
|
||
if (!user?.access_token) {
|
||
setLoginDialogMessage(
|
||
"برای افزودن محصول به ست ابتدا باید وارد حساب کاربری خود شوید."
|
||
);
|
||
setIsLoginModalOpen(true);
|
||
return;
|
||
}
|
||
setIsAddToSetDrawerOpen(true);
|
||
};
|
||
|
||
const handleShare = (platform: string) => {
|
||
const url = window.location.href;
|
||
let shareUrl = "";
|
||
|
||
if (platform === "telegram") {
|
||
shareUrl = `https://t.me/share/url?url=${encodeURIComponent(url)}`;
|
||
} else if (platform === "whatsapp") {
|
||
shareUrl = `https://api.whatsapp.com/send?text=${encodeURIComponent(url)}`;
|
||
} else if (platform === "copy") {
|
||
navigator.clipboard.writeText(url);
|
||
toast({
|
||
title: "لینک کپی شد",
|
||
description: "لینک محصول در کلیپبورد کپی شد.",
|
||
variant: "default",
|
||
duration: 2000,
|
||
});
|
||
setIsShareDrawerOpen(false);
|
||
return;
|
||
}
|
||
|
||
if (shareUrl) {
|
||
window.open(shareUrl, "_blank");
|
||
setIsShareDrawerOpen(false);
|
||
}
|
||
};
|
||
|
||
const actionBtn =
|
||
"flex items-center justify-center cursor-pointer transition-colors lg:w-10 lg:h-10 lg:rounded-full lg:border lg:border-WHITE3 lg:hover:bg-WHITE2";
|
||
|
||
return (
|
||
<>
|
||
<div className="flex w-full mt-4 px-4 lg:mt-0 lg:px-0">
|
||
<div className="flex gap-8 lg:gap-2.5">
|
||
<button
|
||
type="button"
|
||
onClick={handleBookmark}
|
||
aria-label="ذخیره"
|
||
className={actionBtn}
|
||
>
|
||
{isCheckingBookmark ? (
|
||
<Skeleton className="h-6 w-6 rounded-full" />
|
||
) : isBookmarked ? (
|
||
isDeletingBookmark ? (
|
||
<Loader2 className="h-5 w-5 animate-spin" />
|
||
) : (
|
||
<Bookmark size={20} className="text-PRIMARY fill-current" />
|
||
)
|
||
) : isBookmarking ? (
|
||
<Loader2 className="h-5 w-5 animate-spin" />
|
||
) : (
|
||
<BookmarkMinus size={20} />
|
||
)}
|
||
</button>
|
||
<button
|
||
type="button"
|
||
onClick={() => setIsShareDrawerOpen(true)}
|
||
aria-label="اشتراکگذاری"
|
||
className={actionBtn}
|
||
>
|
||
<Share2 size={20} />
|
||
</button>
|
||
<button
|
||
type="button"
|
||
onClick={handleAddToSet}
|
||
aria-label="افزودن به ست"
|
||
className={actionBtn}
|
||
>
|
||
<Layers size={20} />
|
||
</button>
|
||
<div className="relative">
|
||
<button
|
||
type="button"
|
||
onClick={handleOpenAIModal}
|
||
aria-label="پرو با هوش مصنوعی"
|
||
className={actionBtn}
|
||
>
|
||
<Sparkles size={20} className="animate-pulse text-PRIMARY" />
|
||
</button>
|
||
{showAIFeatureTooltip && (
|
||
<div className="absolute bottom-full right-0 mb-2 z-50 animate-bounce">
|
||
<div className="relative bg-gradient-to-r from-purple-600 to-pink-500 text-WHITE px-3 py-2 rounded-lg shadow-xl whitespace-nowrap border border-white/20">
|
||
<button
|
||
onClick={handleDismissAITooltip}
|
||
className="absolute -top-1 -right-1 bg-WHITE text-black rounded-full w-4 h-4 flex items-center justify-center text-xs font-bold shadow-md"
|
||
>
|
||
×
|
||
</button>
|
||
<div className="flex items-center gap-2">
|
||
<Sparkles size={14} />
|
||
<span className="text-R12 font-bold">
|
||
پرو با هوش مصنوعی!
|
||
</span>
|
||
</div>
|
||
<p className="text-R10 opacity-90 mt-0.5">
|
||
این محصول رو تو تن خودت ببین
|
||
</p>
|
||
{/* Arrow */}
|
||
<div className="absolute top-full right-1 w-0 h-0 border-l-[6px] border-l-transparent border-r-[6px] border-r-transparent border-t-[6px] border-t-pink-500" />
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<Drawer open={isShareDrawerOpen} onOpenChange={setIsShareDrawerOpen}>
|
||
<DrawerContent>
|
||
<DrawerHeader className="flex flex-col">
|
||
<div className="flex relative items-center justify-center w-full">
|
||
<X
|
||
className="absolute right-0 cursor-pointer hover:bg-BLACK/5 rounded-full p-1 transition-colors"
|
||
size={28}
|
||
onClick={() => setIsShareDrawerOpen(false)}
|
||
/>
|
||
<div className="flex flex-col items-center gap-2">
|
||
<DrawerTitle className="text-B16 font-bold">
|
||
اشتراکگذاری محصول
|
||
</DrawerTitle>
|
||
</div>
|
||
</div>
|
||
</DrawerHeader>
|
||
<div className="px-4 pb-4 flex flex-col gap-3">
|
||
<p className="text-R12 text-GRAY text-center mb-2">
|
||
این محصول را با دوستان خود به اشتراک بگذارید
|
||
</p>
|
||
|
||
<button
|
||
className="flex items-center gap-4 p-4 rounded-xl bg-[#0088cc]/10 hover:bg-[#0088cc]/20 transition-colors"
|
||
onClick={() => handleShare("telegram")}
|
||
>
|
||
<div className="w-12 h-12 rounded-full bg-[#0088cc] flex items-center justify-center">
|
||
<Send size={20} className="text-WHITE" />
|
||
</div>
|
||
<div className="flex flex-col items-start flex-1">
|
||
<span className="text-B14 font-bold text-BLACK">تلگرام</span>
|
||
<span className="text-R12 text-GRAY">
|
||
ارسال به مخاطبین تلگرام
|
||
</span>
|
||
</div>
|
||
</button>
|
||
|
||
<button
|
||
className="flex items-center gap-4 p-4 rounded-xl bg-[#25D366]/10 hover:bg-[#25D366]/20 transition-colors"
|
||
onClick={() => handleShare("whatsapp")}
|
||
>
|
||
<div className="w-12 h-12 rounded-full bg-[#25D366] flex items-center justify-center">
|
||
<MessageSquare size={20} className="text-WHITE" />
|
||
</div>
|
||
<div className="flex flex-col items-start flex-1">
|
||
<span className="text-B14 font-bold text-BLACK">واتساپ</span>
|
||
<span className="text-R12 text-GRAY">
|
||
ارسال به مخاطبین واتساپ
|
||
</span>
|
||
</div>
|
||
</button>
|
||
|
||
<button
|
||
className="flex items-center gap-4 p-4 rounded-xl bg-BLACK/5 hover:bg-BLACK/10 transition-colors"
|
||
onClick={() => handleShare("copy")}
|
||
>
|
||
<div className="w-12 h-12 rounded-full bg-BLACK flex items-center justify-center">
|
||
<Copy size={20} className="text-WHITE" />
|
||
</div>
|
||
<div className="flex flex-col items-start flex-1">
|
||
<span className="text-B14 font-bold text-BLACK">کپی لینک</span>
|
||
<span className="text-R12 text-GRAY">
|
||
کپی لینک در کلیپبورد
|
||
</span>
|
||
</div>
|
||
</button>
|
||
</div>
|
||
</DrawerContent>
|
||
</Drawer>
|
||
|
||
<LoginRequiredDialog
|
||
loginTitle={loginDialogMessage}
|
||
isOpen={isLoginModalOpen}
|
||
onOpenChange={setIsLoginModalOpen}
|
||
/>
|
||
|
||
<AddToSetDrawer
|
||
isOpen={isAddToSetDrawerOpen}
|
||
onOpenChange={setIsAddToSetDrawerOpen}
|
||
productId={productId}
|
||
/>
|
||
|
||
<AIPromoteModal
|
||
isOpen={isAITryOnModalOpen}
|
||
onClose={() => setIsAITryOnModalOpen(false)}
|
||
productId={productId}
|
||
/>
|
||
</>
|
||
);
|
||
};
|