Vitron-Front/app/components/product/ProductActions.tsx
Arda Samadi 692d385d09 feat(product): mobile redesign — brand below image, floating back, circular colors
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>
2026-06-27 15:06:54 +03:30

323 lines
11 KiB
TypeScript
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 {
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}
/>
</>
);
};