Desktop: product detail two-column layout
- ProductDetailView: gallery | info two-column grid on desktop with breadcrumb; gallery sticky; reviews + similar products full-width below. Mobile stack unchanged (only lg: utilities + a moved BottomBar, which is position:fixed so its DOM location doesn't affect mobile) - BottomBar: static inline buy card on desktop (fixed bottom bar on mobile) - ProductInfo: larger title + tightened top spacing at lg - MyLayout: desktop gate now includes /product/* Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
f57fe4e9d6
commit
60d6a2a6e4
@ -34,6 +34,7 @@ const MyLayout = React.memo(({ children }: { children: React.ReactNode }) => {
|
|||||||
const isDesktopReady =
|
const isDesktopReady =
|
||||||
p === "/" ||
|
p === "/" ||
|
||||||
p.startsWith("/search/") ||
|
p.startsWith("/search/") ||
|
||||||
|
p.startsWith("/product/") ||
|
||||||
p.startsWith("/seller/") ||
|
p.startsWith("/seller/") ||
|
||||||
p === "/collections" ||
|
p === "/collections" ||
|
||||||
p.startsWith("/collection/") ||
|
p.startsWith("/collection/") ||
|
||||||
|
|||||||
@ -194,7 +194,7 @@ export const BottomBar = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="max-w-md mx-auto fixed gap-2 bottom-[calc(50px+env(safe-area-inset-bottom))] left-0 right-0 border-t border-inner-border bg-WHITE p-4 z-50 flex flex-row">
|
<div className="max-w-md mx-auto fixed gap-2 bottom-[calc(50px+env(safe-area-inset-bottom))] left-0 right-0 border-t border-inner-border bg-WHITE p-4 z-50 flex flex-row lg:static lg:max-w-none lg:mx-0 lg:border lg:rounded-xl lg:mt-6 lg:z-auto">
|
||||||
<div className="flex w-full items-center justify-between">
|
<div className="flex w-full items-center justify-between">
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<AddToCartButton
|
<AddToCartButton
|
||||||
|
|||||||
@ -13,7 +13,7 @@ import {
|
|||||||
import { ProductDetail as ProductDetailType } from "../../../src/api/types";
|
import { ProductDetail as ProductDetailType } from "../../../src/api/types";
|
||||||
import { Eye, EyeOff } from "lucide-react";
|
import { Eye, EyeOff } from "lucide-react";
|
||||||
import { Button } from "../ui/button";
|
import { Button } from "../ui/button";
|
||||||
import { useNavigate } from "@remix-run/react";
|
import { useNavigate, Link } from "@remix-run/react";
|
||||||
import { useSimilarProductsInfinite } from "~/requestHandler/use-product-hooks";
|
import { useSimilarProductsInfinite } from "~/requestHandler/use-product-hooks";
|
||||||
import MondrianProductList from "../MondrianProductList";
|
import MondrianProductList from "../MondrianProductList";
|
||||||
|
|
||||||
@ -194,8 +194,39 @@ export const ProductDetailView = memo(function ProductDetailView({
|
|||||||
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col pb-20">
|
<div className="flex flex-col pb-20 lg:max-w-[1320px] lg:mx-auto lg:w-full lg:px-8 lg:pb-12">
|
||||||
{!isOwner && <ProductHeader product={product} />}
|
{!isOwner && (
|
||||||
|
<div className="lg:hidden">
|
||||||
|
<ProductHeader product={product} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Desktop breadcrumb */}
|
||||||
|
<nav className="hidden lg:flex items-center gap-2 text-[13px] text-GRAY mt-4 mb-4">
|
||||||
|
<Link to="/" className="hover:text-BLACK">
|
||||||
|
خانه
|
||||||
|
</Link>
|
||||||
|
<span>/</span>
|
||||||
|
{product.seller?.username ? (
|
||||||
|
<>
|
||||||
|
<Link
|
||||||
|
to={`/seller/${product.seller.username}`}
|
||||||
|
className="hover:text-BLACK"
|
||||||
|
>
|
||||||
|
{product.seller.username}
|
||||||
|
</Link>
|
||||||
|
<span>/</span>
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
<span className="text-BLACK truncate max-w-[320px]">
|
||||||
|
{product.title}
|
||||||
|
</span>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
{/* Top section: gallery | info (two columns on desktop) */}
|
||||||
|
<div className="lg:grid lg:grid-cols-2 lg:gap-10 lg:items-start">
|
||||||
|
{/* Gallery */}
|
||||||
|
<div className="lg:rounded-[14px] lg:overflow-hidden lg:shadow-sm lg:sticky lg:top-[88px]">
|
||||||
<ProductImageCarousel
|
<ProductImageCarousel
|
||||||
images={product.imageUrls || []}
|
images={product.imageUrls || []}
|
||||||
productId={product.id}
|
productId={product.id}
|
||||||
@ -208,9 +239,13 @@ export const ProductDetailView = memo(function ProductDetailView({
|
|||||||
isChangeStatusDialogOpen={isChangeStatusDialogOpen}
|
isChangeStatusDialogOpen={isChangeStatusDialogOpen}
|
||||||
setIsChangeStatusDialogOpen={setIsChangeStatusDialogOpen}
|
setIsChangeStatusDialogOpen={setIsChangeStatusDialogOpen}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Info */}
|
||||||
|
<div className="flex flex-col">
|
||||||
{/* Inactive Product Status */}
|
{/* Inactive Product Status */}
|
||||||
{!productActiveStatus && isOwner ? (
|
{!productActiveStatus && isOwner ? (
|
||||||
<div className="flex gap-1 px-4 w-full justify-between items-center mt-2">
|
<div className="flex gap-1 px-4 w-full justify-between items-center mt-2 lg:px-0">
|
||||||
<div className="flex items-center w-full gap-2 p-2 bg-WHITE3 rounded-lg">
|
<div className="flex items-center w-full gap-2 p-2 bg-WHITE3 rounded-lg">
|
||||||
<EyeOff size={16} className="text-BLACK2 font-bold" />
|
<EyeOff size={16} className="text-BLACK2 font-bold" />
|
||||||
<span className="text-B12 font-bold text-BLACK2">
|
<span className="text-B12 font-bold text-BLACK2">
|
||||||
@ -249,7 +284,8 @@ export const ProductDetailView = memo(function ProductDetailView({
|
|||||||
{hasColorVariants && (
|
{hasColorVariants && (
|
||||||
<ColorSelector
|
<ColorSelector
|
||||||
colors={inStockColors.map(
|
colors={inStockColors.map(
|
||||||
(color) => (color.info as unknown as { detail: string })?.detail
|
(color) =>
|
||||||
|
(color.info as unknown as { detail: string })?.detail
|
||||||
)}
|
)}
|
||||||
selectedColor={selectedColor}
|
selectedColor={selectedColor}
|
||||||
setSelectedColor={setSelectedColor}
|
setSelectedColor={setSelectedColor}
|
||||||
@ -265,12 +301,12 @@ export const ProductDetailView = memo(function ProductDetailView({
|
|||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex flex-col gap-4 mt-10 px-4">
|
<div className="flex flex-col gap-4 mt-10 px-4 lg:px-0">
|
||||||
<p className="text-B18 font-bold">موجود نیست</p>
|
<p className="text-B18 font-bold">موجود نیست</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{isOwner && (
|
{isOwner && (
|
||||||
<div className="w-full p-4 flex justify-center items-center">
|
<div className="w-full p-4 flex justify-center items-center lg:justify-start lg:px-0">
|
||||||
<Button
|
<Button
|
||||||
variant="dark"
|
variant="dark"
|
||||||
size="sm"
|
size="sm"
|
||||||
@ -284,16 +320,40 @@ export const ProductDetailView = memo(function ProductDetailView({
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Buy box (fixed bottom bar on mobile, inline on desktop) */}
|
||||||
|
{!isOwner && (
|
||||||
|
<BottomBar
|
||||||
|
currentPrice={currentPrice}
|
||||||
|
currentDiscountPrice={currentDiscountPrice}
|
||||||
|
currentStock={currentStock}
|
||||||
|
product={product}
|
||||||
|
selectedColor={
|
||||||
|
inStockColors.find(
|
||||||
|
(color) =>
|
||||||
|
(color.info as unknown as { detail: string })?.detail ===
|
||||||
|
selectedColor
|
||||||
|
)?.value || null
|
||||||
|
}
|
||||||
|
selectedSize={selectedSize}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Full-width below the fold: contact, reviews, similar */}
|
||||||
{productActiveStatus && (
|
{productActiveStatus && (
|
||||||
<>
|
<>
|
||||||
{!isOwner && <ContactButton product={product} />}
|
{!isOwner && <ContactButton product={product} />}
|
||||||
<ReviewSection productId={product.id || ""} isOwner={isOwner} />
|
<ReviewSection productId={product.id || ""} isOwner={isOwner} />
|
||||||
{/* Similar Products Section */}
|
{/* Similar Products Section */}
|
||||||
{!isOwner && (
|
{!isOwner && (
|
||||||
<section className="w-full gap-2 flex flex-col max-w-[100vw] my-6">
|
<section className="w-full gap-2 flex flex-col max-w-[100vw] my-6 lg:max-w-none">
|
||||||
<div className="w-full px-4">
|
<div className="w-full px-4 lg:px-0">
|
||||||
<p className="text-B16 font-bold">محصولات مشابه</p>
|
<p className="text-B16 lg:text-B24 font-bold">محصولات مشابه</p>
|
||||||
<p className="text-R14">محصولات مرتبط که ممکن است بپسندید</p>
|
<p className="text-R14 text-GRAY">
|
||||||
|
محصولات مرتبط که ممکن است بپسندید
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<MondrianProductList
|
<MondrianProductList
|
||||||
products={
|
products={
|
||||||
@ -323,25 +383,8 @@ export const ProductDetailView = memo(function ProductDetailView({
|
|||||||
/>
|
/>
|
||||||
</section>
|
</section>
|
||||||
)}
|
)}
|
||||||
{/* Bottom bar handles variant selection and pricing */}
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{!isOwner && (
|
|
||||||
<BottomBar
|
|
||||||
currentPrice={currentPrice}
|
|
||||||
currentDiscountPrice={currentDiscountPrice}
|
|
||||||
currentStock={currentStock}
|
|
||||||
product={product}
|
|
||||||
selectedColor={
|
|
||||||
inStockColors.find(
|
|
||||||
(color) =>
|
|
||||||
(color.info as unknown as { detail: string })?.detail ===
|
|
||||||
selectedColor
|
|
||||||
)?.value || null
|
|
||||||
}
|
|
||||||
selectedSize={selectedSize}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@ -10,7 +10,7 @@ export const ProductInfo = ({
|
|||||||
product,
|
product,
|
||||||
productDiscountPercent,
|
productDiscountPercent,
|
||||||
}: ProductInfoProps) => (
|
}: ProductInfoProps) => (
|
||||||
<div className="flex flex-col gap-2 mt-8 px-4">
|
<div className="flex flex-col gap-2 mt-8 px-4 lg:mt-2">
|
||||||
<div className="flex gap-3 items-center">
|
<div className="flex gap-3 items-center">
|
||||||
{productDiscountPercent && productDiscountPercent > 0 ? (
|
{productDiscountPercent && productDiscountPercent > 0 ? (
|
||||||
<div className="flex gap-[2px] items-center">
|
<div className="flex gap-[2px] items-center">
|
||||||
@ -23,7 +23,7 @@ export const ProductInfo = ({
|
|||||||
{productDiscountPercent && productDiscountPercent > 0 ? (
|
{productDiscountPercent && productDiscountPercent > 0 ? (
|
||||||
<div className="w-[6px] h-[6px] rounded-full bg-BLACK" />
|
<div className="w-[6px] h-[6px] rounded-full bg-BLACK" />
|
||||||
) : null}
|
) : null}
|
||||||
<p className="text-B18 font-bold">{product.title}</p>
|
<p className="text-B18 lg:text-[26px] font-bold">{product.title}</p>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-R12 text-GRAY">{product.description}</p>
|
<p className="text-R12 text-GRAY">{product.description}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user