menu: UI enhancements in desktop sizes
- add product dialog - enhance product page layout for desktop size
This commit is contained in:
@@ -1,10 +1,13 @@
|
||||
import { Badge, Card, Grid } from "antd";
|
||||
import { Badge, Card } from "antd";
|
||||
import ArabicPrice from "components/ArabicPrice";
|
||||
import ImageWithFallback from "components/ImageWithFallback";
|
||||
import { ItemDescriptionIcons } from "components/ItemDescriptionIcons/ItemDescriptionIcons";
|
||||
import ProText from "components/ProText";
|
||||
import ProTitle from "components/ProTitle";
|
||||
import useBreakPoint from "hooks/useBreakPoint";
|
||||
import { AddToCartButton } from "pages/menu/components/AddToCartButton/AddToCartButton.tsx";
|
||||
import { ProductPreviewDialog } from "pages/menu/components/ProductPreviewDialog";
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useAppSelector } from "redux/hooks";
|
||||
@@ -23,18 +26,34 @@ interface MenuListProps {
|
||||
categoryRefs: React.RefObject<{ [key: number]: HTMLDivElement | null }>;
|
||||
}
|
||||
|
||||
const { useBreakpoint } = Grid;
|
||||
|
||||
export function MenuList({ data, categoryRefs }: MenuListProps) {
|
||||
const { isRTL } = useAppSelector((state) => state.locale);
|
||||
const products = data?.products;
|
||||
const { xs, md } = useBreakpoint();
|
||||
const { isMobile, isTablet, isDesktop } = useBreakPoint();
|
||||
const { items } = useAppSelector((state) => state.order);
|
||||
const restaurantName = localStorage.getItem("restaurantName");
|
||||
const navigate = useNavigate();
|
||||
const { t } = useTranslation();
|
||||
const { themeName } = useAppSelector((state) => state.theme);
|
||||
|
||||
// Dialog state
|
||||
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
||||
|
||||
// Handle product click - open dialog on desktop, navigate on mobile/tablet
|
||||
const handleProductClick = (item: Product) => {
|
||||
localStorage.setItem("product", JSON.stringify(item));
|
||||
if (isDesktop) {
|
||||
setIsDialogOpen(true);
|
||||
} else {
|
||||
navigate(`/${restaurantName}/product/${item.id}`);
|
||||
}
|
||||
};
|
||||
|
||||
// Handle dialog close
|
||||
const handleDialogClose = () => {
|
||||
setIsDialogOpen(false);
|
||||
};
|
||||
|
||||
// Show error state if data exists but has no products
|
||||
if (data && (!data.products || data.products.length === 0)) {
|
||||
return (
|
||||
@@ -104,10 +123,7 @@ export function MenuList({ data, categoryRefs }: MenuListProps) {
|
||||
<div
|
||||
key={item.id}
|
||||
className={styles.productLink}
|
||||
onClick={() => {
|
||||
localStorage.setItem("product", JSON.stringify(item));
|
||||
navigate(`/${restaurantName}/product/${item.id}`);
|
||||
}}
|
||||
onClick={() => handleProductClick(item)}
|
||||
>
|
||||
<Card
|
||||
key={item.id}
|
||||
@@ -126,8 +142,8 @@ export function MenuList({ data, categoryRefs }: MenuListProps) {
|
||||
? "16px 16px 8px 16px"
|
||||
: "16px 16px 24px 16px",
|
||||
overflow: "hide",
|
||||
boxShadow: "none",
|
||||
},
|
||||
boxShadow: "none",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<div
|
||||
@@ -136,7 +152,7 @@ export function MenuList({ data, categoryRefs }: MenuListProps) {
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
height: "100%",
|
||||
gap: xs ? 10 : 16,
|
||||
gap: isMobile ? 10 : 16,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
@@ -225,9 +241,9 @@ export function MenuList({ data, categoryRefs }: MenuListProps) {
|
||||
fallbackSrc="/default.png"
|
||||
alt={item.name}
|
||||
className={`${styles.popularMenuItemImage} ${
|
||||
xs
|
||||
isMobile
|
||||
? styles.popularMenuItemImageMobile
|
||||
: md
|
||||
: isTablet
|
||||
? styles.popularMenuItemImageTablet
|
||||
: styles.popularMenuItemImageDesktop
|
||||
}`}
|
||||
@@ -267,6 +283,9 @@ export function MenuList({ data, categoryRefs }: MenuListProps) {
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Product Preview Dialog for Desktop */}
|
||||
<ProductPreviewDialog isOpen={isDialogOpen} onClose={handleDialogClose} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
.productModal{
|
||||
width: 80vw;
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import { Modal } from "antd";
|
||||
import ProductDetailPage from "pages/product/page";
|
||||
import styles from "./ProductPreviewDialog.module.css";
|
||||
|
||||
interface ProductPreviewDialogProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export function ProductPreviewDialog({
|
||||
isOpen,
|
||||
onClose,
|
||||
}: ProductPreviewDialogProps) {
|
||||
// const { isRTL } = useAppSelector((state) => state.locale);
|
||||
// const { themeName } = useAppSelector((state) => state.theme);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
open={isOpen}
|
||||
onCancel={onClose}
|
||||
footer={null}
|
||||
width="auto"
|
||||
centered
|
||||
className={styles.productModal}
|
||||
height={600}
|
||||
style={{
|
||||
width: "80vw",
|
||||
minWidth: "80vw",
|
||||
}}
|
||||
>
|
||||
<ProductDetailPage />
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
1
src/pages/menu/components/ProductPreviewDialog/index.ts
Normal file
1
src/pages/menu/components/ProductPreviewDialog/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { ProductPreviewDialog } from "./ProductPreviewDialog";
|
||||
@@ -4,7 +4,7 @@ import ProText from "components/ProText";
|
||||
import { Dispatch, SetStateAction } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useAppSelector } from "redux/hooks";
|
||||
import { Extra4, TheExtrasGroup } from "utils/types/appTypes";
|
||||
import { TheExtrasGroup } from "utils/types/appTypes";
|
||||
import styles from "../product.module.css";
|
||||
|
||||
export default function ExtraGroups({
|
||||
@@ -89,7 +89,7 @@ export default function ExtraGroups({
|
||||
|
||||
<div className={styles.productContainer}>
|
||||
<ProCheckboxGroups
|
||||
options={group.extras.map((extra: Extra4) => ({
|
||||
options={group.extras.map((extra: any) => ({
|
||||
value: extra.id.toString(),
|
||||
label: isRTL ? extra.name : extra.nameAR,
|
||||
price: `+${extra.price}`,
|
||||
@@ -99,7 +99,7 @@ export default function ExtraGroups({
|
||||
// Check if the new selection would exceed the limit
|
||||
if (values.length > group.limit) {
|
||||
message.error(
|
||||
`You can only select up to ${group.limit} option${group.limit > 1 ? "s" : ""} from this group.`
|
||||
`You can only select up to ${group.limit} option${group.limit > 1 ? "s" : ""} from this group.`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1,20 +1,18 @@
|
||||
import { ShoppingCartOutlined } from "@ant-design/icons";
|
||||
import { Button, Grid, message, Row } from "antd";
|
||||
import { BottomSheet } from "pages/cart/components/specialRequest/BottomSheet.tsx";
|
||||
import { Button, message, Row } from "antd";
|
||||
import {
|
||||
addItem,
|
||||
selectCart,
|
||||
updateSpecialRequest,
|
||||
} from "features/order/orderSlice";
|
||||
import { useState } from "react";
|
||||
import useBreakPoint from "hooks/useBreakPoint";
|
||||
import { BottomSheet } from "pages/cart/components/specialRequest/BottomSheet.tsx";
|
||||
import { useMemo, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import { useAppDispatch, useAppSelector } from "redux/hooks";
|
||||
import { colors, ProBlack2 } from "ThemeConstants";
|
||||
import { Product } from "utils/types/appTypes";
|
||||
|
||||
const { useBreakpoint } = Grid;
|
||||
|
||||
export default function ProductFooter({
|
||||
product,
|
||||
isValid = true,
|
||||
@@ -30,14 +28,25 @@ export default function ProductFooter({
|
||||
selectedGroups: string[];
|
||||
quantity: number;
|
||||
}) {
|
||||
const { id } = useParams();
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useAppDispatch();
|
||||
const { themeName } = useAppSelector((state) => state.theme);
|
||||
const { specialRequest } = useAppSelector(selectCart);
|
||||
const [isSpecialRequestOpen, setIsSpecialRequestOpen] = useState(false);
|
||||
const { xs } = useBreakpoint();
|
||||
const navigate = useNavigate();
|
||||
const { isMobile, isDesktop } = useBreakPoint();
|
||||
|
||||
// Check if product has any customization options
|
||||
const hasCustomizationOptions = useMemo(() => {
|
||||
const hasVariants = product?.variants?.length > 0;
|
||||
const hasExtras = product.extras.length > 0;
|
||||
const hasExtraGroups = product?.theExtrasGroups?.length > 0;
|
||||
|
||||
return hasVariants || hasExtras || hasExtraGroups;
|
||||
}, [
|
||||
product?.variants?.length,
|
||||
product.extras.length,
|
||||
product?.theExtrasGroups?.length,
|
||||
]);
|
||||
|
||||
const handleAddToCart = () => {
|
||||
if (!isValid) {
|
||||
@@ -75,17 +84,27 @@ export default function ProductFooter({
|
||||
setIsSpecialRequestOpen(false);
|
||||
};
|
||||
|
||||
//
|
||||
return (
|
||||
<>
|
||||
<Row
|
||||
style={{
|
||||
width: "100%",
|
||||
padding: "16px 16px 0",
|
||||
position: "fixed",
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
...(!isDesktop
|
||||
? {
|
||||
position: "fixed",
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
width: "100%",
|
||||
backgroundColor: themeName === "light" ? "white" : ProBlack2,
|
||||
}
|
||||
: {
|
||||
position: "absolute",
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
width: hasCustomizationOptions ? "50%" : "100%",
|
||||
}),
|
||||
height: "10vh",
|
||||
backgroundColor: themeName === "light" ? "white" : ProBlack2,
|
||||
}}
|
||||
>
|
||||
<div style={{ width: "100%" }}>
|
||||
@@ -103,8 +122,8 @@ export default function ProductFooter({
|
||||
disabled={!isValid}
|
||||
style={{
|
||||
flex: 1,
|
||||
height: xs ? "48px" : "48px",
|
||||
fontSize: xs ? "1rem" : "16px",
|
||||
height: isMobile ? "48px" : "48px",
|
||||
fontSize: isMobile ? "1rem" : "16px",
|
||||
transition: "all 0.3s ease",
|
||||
width: "100%",
|
||||
borderRadius: 888,
|
||||
@@ -116,12 +135,12 @@ export default function ProductFooter({
|
||||
cursor: isValid ? "pointer" : "not-allowed",
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
if (!xs && isValid) {
|
||||
if (!isMobile && isValid) {
|
||||
e.currentTarget.style.transform = "translateY(-2px)";
|
||||
}
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
if (!xs) {
|
||||
if (!isMobile) {
|
||||
e.currentTarget.style.transform = "translateY(0)";
|
||||
}
|
||||
}}
|
||||
@@ -132,12 +151,14 @@ export default function ProductFooter({
|
||||
</div>
|
||||
</Row>
|
||||
|
||||
<BottomSheet
|
||||
isOpen={isSpecialRequestOpen}
|
||||
onClose={handleSpecialRequestClose}
|
||||
initialValue={specialRequest}
|
||||
onSave={handleSpecialRequestSave}
|
||||
/>
|
||||
{!isDesktop && (
|
||||
<BottomSheet
|
||||
isOpen={isSpecialRequestOpen}
|
||||
onClose={handleSpecialRequestClose}
|
||||
initialValue={specialRequest}
|
||||
onSave={handleSpecialRequestSave}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Divider } from "antd";
|
||||
import ProRatioGroups from "components/ProRatioGroups/ProRatioGroups";
|
||||
import ProText from "components/ProText";
|
||||
import useBreakPoint from "hooks/useBreakPoint";
|
||||
import { Dispatch, SetStateAction, useMemo } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useAppSelector } from "redux/hooks";
|
||||
@@ -18,6 +19,7 @@ export default function Variants({
|
||||
}) {
|
||||
const { isRTL } = useAppSelector((state) => state.locale);
|
||||
const { t } = useTranslation();
|
||||
const { isDesktop } = useBreakPoint();
|
||||
|
||||
// Determine variant levels based on options array length
|
||||
const variantLevels = useMemo(() => {
|
||||
@@ -108,7 +110,7 @@ export default function Variants({
|
||||
<>
|
||||
{variantsList?.length > 0 && variantLevels.length > 0 && (
|
||||
<>
|
||||
<Divider style={{ margin: "0" }} />
|
||||
{!isDesktop && <Divider style={{ margin: "0" }} />}
|
||||
<div>
|
||||
<div
|
||||
style={{
|
||||
|
||||
@@ -7,8 +7,8 @@ import { default_image } from "utils/constants";
|
||||
// import PageTransition from "components/PageTransition/PageTransition";
|
||||
import { Space } from "antd";
|
||||
import ArabicPrice from "components/ArabicPrice";
|
||||
import { useMemo, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import useBreakPoint from "hooks/useBreakPoint";
|
||||
import { useCallback, useMemo, useState } from "react";
|
||||
import { colors } from "ThemeConstants";
|
||||
import { Product } from "utils/types/appTypes";
|
||||
import BackButton from "../menu/components/BackButton";
|
||||
@@ -20,11 +20,11 @@ import styles from "./product.module.css";
|
||||
|
||||
export default function ProductDetailPage() {
|
||||
const { isRTL } = useAppSelector((state) => state.locale);
|
||||
const { t } = useTranslation();
|
||||
const { isDesktop } = useBreakPoint();
|
||||
const [quantity, setQuantity] = useState(1);
|
||||
|
||||
const product = JSON.parse(
|
||||
localStorage.getItem("product") || "null"
|
||||
localStorage.getItem("product") || "null",
|
||||
) as Product;
|
||||
|
||||
// State for variant selections
|
||||
@@ -45,7 +45,7 @@ export default function ProductDetailPage() {
|
||||
if (!product?.variants || product.variants.length === 0) return [];
|
||||
|
||||
const maxOptionsLength = Math.max(
|
||||
...product.variants.map((v) => v.options.length)
|
||||
...product.variants.map((v) => v.options.length),
|
||||
);
|
||||
const levels: Array<{
|
||||
level: number;
|
||||
@@ -64,7 +64,7 @@ export default function ProductDetailPage() {
|
||||
product.variants
|
||||
.filter((v) => v.options[i]?.option === optionKey)
|
||||
.map((v) => v.options[i]?.value)
|
||||
.filter(Boolean)
|
||||
.filter(Boolean),
|
||||
),
|
||||
];
|
||||
|
||||
@@ -84,7 +84,7 @@ export default function ProductDetailPage() {
|
||||
}, [product?.variants]);
|
||||
|
||||
// Get the final selected variant ID (the variant that matches all current selections)
|
||||
const getFinalSelectedVariantId = () => {
|
||||
const getFinalSelectedVariantId = useCallback(() => {
|
||||
if (!product?.variants || Object.keys(selectedVariants).length === 0)
|
||||
return "";
|
||||
|
||||
@@ -98,26 +98,26 @@ export default function ProductDetailPage() {
|
||||
|
||||
// Convert to string only if defined, otherwise return empty string
|
||||
return matchingVariant?.id?.toString() || "";
|
||||
};
|
||||
}, [product?.variants, selectedVariants]);
|
||||
|
||||
const getExtras = () => {
|
||||
const getExtras = useCallback(() => {
|
||||
const finalSelectedVariantId = getFinalSelectedVariantId();
|
||||
if (finalSelectedVariantId) {
|
||||
const variant = product?.variants?.find(
|
||||
(variant) => variant.id === Number(finalSelectedVariantId)
|
||||
(variant) => variant.id === Number(finalSelectedVariantId),
|
||||
);
|
||||
if (variant?.extras?.length && variant.extras.length > 0) {
|
||||
return variant.extras;
|
||||
}
|
||||
}
|
||||
return product?.extras;
|
||||
};
|
||||
}, [product?.variants, product?.extras, getFinalSelectedVariantId]);
|
||||
|
||||
// Validation function to check if all required selections are made
|
||||
const validateRequiredSelections = () => {
|
||||
// Check if all variant levels are selected
|
||||
const allVariantsSelected = variantLevels.every(
|
||||
(level) => selectedVariants[level.level] !== undefined
|
||||
(level) => selectedVariants[level.level] !== undefined,
|
||||
);
|
||||
|
||||
// Check if all required extra groups are satisfied
|
||||
@@ -128,7 +128,7 @@ export default function ProductDetailPage() {
|
||||
return selectedCount === group.limit;
|
||||
}
|
||||
return true; // Optional groups are always satisfied
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
return allVariantsSelected && allRequiredExtraGroupsSatisfied;
|
||||
@@ -136,6 +136,21 @@ export default function ProductDetailPage() {
|
||||
|
||||
const isValid = validateRequiredSelections();
|
||||
|
||||
// Check if product has any customization options
|
||||
const hasCustomizationOptions = useMemo(() => {
|
||||
const hasVariants =
|
||||
product?.variants?.length > 0 && variantLevels.length > 0;
|
||||
const hasExtras = getExtras().length > 0;
|
||||
const hasExtraGroups = product?.theExtrasGroups?.length > 0;
|
||||
|
||||
return hasVariants || hasExtras || hasExtraGroups;
|
||||
}, [
|
||||
product?.variants,
|
||||
product?.theExtrasGroups,
|
||||
variantLevels.length,
|
||||
getExtras,
|
||||
]);
|
||||
|
||||
if (!product) {
|
||||
return (
|
||||
<div style={{ padding: "40px", textAlign: "center" }}>
|
||||
@@ -152,141 +167,166 @@ export default function ProductDetailPage() {
|
||||
scrollbarWidth: "none",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
zIndex: 999,
|
||||
left: 20,
|
||||
top: 70,
|
||||
backgroundColor: "#FFF",
|
||||
borderRadius: "50%",
|
||||
boxShadow: "0 2px 8px rgba(0, 0, 0, 0.1)",
|
||||
}}
|
||||
>
|
||||
<BackButton />
|
||||
</div>
|
||||
|
||||
<div
|
||||
style={{
|
||||
overflow: "hidden",
|
||||
transition: "all 0.3s ease",
|
||||
}}
|
||||
>
|
||||
<ImageWithFallback
|
||||
src={product?.image}
|
||||
fallbackSrc={default_image}
|
||||
height={240}
|
||||
style={{
|
||||
width: "100vw",
|
||||
objectFit: "cover",
|
||||
transition: "transform 0.3s ease",
|
||||
}}
|
||||
loadingContainerStyle={{ borderRadius: 0, width: "100vw" }}
|
||||
/>
|
||||
</div>
|
||||
{!isDesktop && (
|
||||
<div className={styles.backButtonContainer}>
|
||||
<BackButton />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className={styles.productDetailContainer}>
|
||||
<div
|
||||
style={{
|
||||
display: "grid",
|
||||
gridTemplateColumns: "1fr", // isMobile ? "1fr" : "1fr 1fr",
|
||||
gap: 5,
|
||||
}}
|
||||
className={
|
||||
isDesktop && hasCustomizationOptions
|
||||
? styles.desktopLayout
|
||||
: isDesktop && !hasCustomizationOptions
|
||||
? styles.desktopLayoutFullWidth
|
||||
: styles.mobileLayout
|
||||
}
|
||||
>
|
||||
<Space direction="vertical" size="middle" style={{ width: "100%" }}>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
{/* Left Column - Product Info & Image */}
|
||||
<div className={isDesktop ? styles.leftColumn : styles.fullWidth}>
|
||||
<div className={styles.productImageSection}>
|
||||
<ImageWithFallback
|
||||
key={`product-${product?.id}-${product?.image}`}
|
||||
src={product?.image}
|
||||
fallbackSrc={default_image}
|
||||
height={isDesktop ? 300 : 240}
|
||||
width={"100%"}
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
justifyContent: "space-between",
|
||||
gap: "0.5rem",
|
||||
marginTop: 16,
|
||||
width: "100%",
|
||||
objectFit: "cover",
|
||||
}}
|
||||
>
|
||||
<ProText strong style={{ fontSize: "1.25rem" }}>
|
||||
{isRTL ? product?.nameOther : product?.name}{" "}
|
||||
</ProText>
|
||||
{product?.description && (
|
||||
loadingContainerStyle={{
|
||||
width: "100%",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={styles.productInfoSection}>
|
||||
<div className={styles.productHeader}>
|
||||
<div className={styles.productDetails}>
|
||||
<ProText
|
||||
type="secondary"
|
||||
style={{ fontSize: "1rem", lineHeight: "1.25rem" }}
|
||||
strong
|
||||
style={{ fontSize: isDesktop ? "1.5rem" : "1.25rem" }}
|
||||
>
|
||||
{isRTL ? product?.descriptionAR : product?.description}
|
||||
{isRTL ? product?.nameOther : product?.name}
|
||||
</ProText>
|
||||
)}
|
||||
<ArabicPrice
|
||||
price={product?.price}
|
||||
style={{ fontSize: "1rem", color: colors.primary }}
|
||||
/>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
justifyContent: "start",
|
||||
gap: 20,
|
||||
...(isRTL ? { marginRight: -5 } : {}),
|
||||
}} // Item Description Icons
|
||||
>
|
||||
<ItemDescriptionIcons />
|
||||
<br />
|
||||
{product?.description && (
|
||||
<ProText
|
||||
type="secondary"
|
||||
style={{
|
||||
lineClamp: 1,
|
||||
display: "-webkit-box",
|
||||
WebkitBoxOrient: "vertical",
|
||||
WebkitLineClamp: 1,
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
wordWrap: "break-word",
|
||||
overflowWrap: "break-word",
|
||||
lineHeight: "1.4",
|
||||
maxHeight: "2.8em",
|
||||
fontWeight: "500",
|
||||
letterSpacing: "0.01em",
|
||||
fontSize: "1rem",
|
||||
}}
|
||||
>
|
||||
{isRTL ? product?.descriptionAR : product?.description}
|
||||
</ProText>
|
||||
)}
|
||||
<ArabicPrice
|
||||
price={product?.price}
|
||||
style={{
|
||||
fontSize: isDesktop ? "1.2rem" : "1rem",
|
||||
color: colors.primary,
|
||||
marginTop: "12px",
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
justifyContent: "start",
|
||||
gap: 20,
|
||||
marginTop: "16px",
|
||||
...(isRTL ? { marginRight: -5 } : {}),
|
||||
}}
|
||||
>
|
||||
<ItemDescriptionIcons className={styles.itemDescriptionIcons} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
justifyContent: "end",
|
||||
}}
|
||||
>
|
||||
<ActionsButtons
|
||||
quantity={quantity}
|
||||
setQuantity={(quantity) => setQuantity(quantity)}
|
||||
max={100}
|
||||
min={1}
|
||||
/>
|
||||
|
||||
{isDesktop && (
|
||||
<div className={styles.quantitySection}>
|
||||
<ActionsButtons
|
||||
quantity={quantity}
|
||||
setQuantity={(quantity) => setQuantity(quantity)}
|
||||
max={100}
|
||||
min={1}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{product?.variants?.length > 0 && variantLevels.length > 0 && (
|
||||
<Variants
|
||||
selectedVariants={selectedVariants}
|
||||
setSelectedVariants={setSelectedVariants}
|
||||
variantsList={product?.variants}
|
||||
/>
|
||||
)}
|
||||
|
||||
{getExtras().length > 0 && (
|
||||
<Extra
|
||||
extrasList={getExtras()}
|
||||
{isDesktop && (
|
||||
<ProductFooter
|
||||
product={product}
|
||||
isValid={isValid}
|
||||
variantId={getFinalSelectedVariantId()}
|
||||
selectedExtras={selectedExtras}
|
||||
setSelectedExtras={setSelectedExtras}
|
||||
selectedGroups={Object.values(selectedExtrasByGroup).flat()}
|
||||
quantity={quantity}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{product.theExtrasGroups.length > 0 && (
|
||||
<ExtraGroups
|
||||
groupsList={product.theExtrasGroups}
|
||||
selectedExtrasByGroup={selectedExtrasByGroup}
|
||||
setSelectedExtrasByGroup={setSelectedExtrasByGroup}
|
||||
/>
|
||||
)}
|
||||
</Space>
|
||||
{/* Right Column - Variants, Extras, and Extra Groups */}
|
||||
{hasCustomizationOptions && (
|
||||
<div className={isDesktop ? styles.rightColumn : styles.fullWidth}>
|
||||
<Space
|
||||
direction="vertical"
|
||||
size="middle"
|
||||
style={{ width: "100%", padding: "0 1rem" }}
|
||||
>
|
||||
{product?.variants?.length > 0 && variantLevels.length > 0 && (
|
||||
<Variants
|
||||
selectedVariants={selectedVariants}
|
||||
setSelectedVariants={setSelectedVariants}
|
||||
variantsList={product?.variants}
|
||||
/>
|
||||
)}
|
||||
|
||||
{getExtras().length > 0 && (
|
||||
<Extra
|
||||
extrasList={getExtras()}
|
||||
selectedExtras={selectedExtras}
|
||||
setSelectedExtras={setSelectedExtras}
|
||||
/>
|
||||
)}
|
||||
|
||||
{product.theExtrasGroups.length > 0 && (
|
||||
<ExtraGroups
|
||||
groupsList={product.theExtrasGroups}
|
||||
selectedExtrasByGroup={selectedExtrasByGroup}
|
||||
setSelectedExtrasByGroup={setSelectedExtrasByGroup}
|
||||
/>
|
||||
)}
|
||||
</Space>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<ProductFooter
|
||||
product={product}
|
||||
isValid={isValid}
|
||||
variantId={getFinalSelectedVariantId()}
|
||||
selectedExtras={selectedExtras}
|
||||
selectedGroups={Object.values(selectedExtrasByGroup).flat()}
|
||||
quantity={quantity}
|
||||
/>
|
||||
{!isDesktop && (
|
||||
<ProductFooter
|
||||
product={product}
|
||||
isValid={isValid}
|
||||
variantId={getFinalSelectedVariantId()}
|
||||
selectedExtras={selectedExtras}
|
||||
selectedGroups={Object.values(selectedExtrasByGroup).flat()}
|
||||
quantity={quantity}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
.productDetailContainer {
|
||||
overflow: auto;
|
||||
scrollbar-width: none;
|
||||
padding: 0 16px !important;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.productContainer :global(.ant-radio-wrapper:last-child) {
|
||||
@@ -152,8 +152,135 @@
|
||||
color: var(--primary2);
|
||||
}
|
||||
|
||||
/* Desktop Layout Styles */
|
||||
.desktopLayout {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 32px;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 24px 0;
|
||||
}
|
||||
|
||||
.desktopLayoutFullWidth {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
gap: 32px;
|
||||
padding: 24px 0 0 0;
|
||||
}
|
||||
|
||||
.mobileLayout {
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.leftColumn {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.rightColumn {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
padding: 16px;
|
||||
background: rgba(0, 0, 0, 0.01);
|
||||
border-radius: 12px;
|
||||
border: 1px solid rgba(0, 0, 0, 0.06);
|
||||
overflow: auto;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
.fullWidth {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.productImageSection {
|
||||
position: relative;
|
||||
border-radius: 0px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
|
||||
transition:
|
||||
transform 0.3s ease,
|
||||
box-shadow 0.3s ease;
|
||||
}
|
||||
|
||||
.productImageSection:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.productInfoSection {
|
||||
padding: 0 1rem;
|
||||
}
|
||||
|
||||
.productHeader {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
gap: 16px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.productDetails {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.quantitySection {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
position: relative;
|
||||
top: 5px
|
||||
}
|
||||
|
||||
/* Dark mode desktop styles */
|
||||
:global(.darkApp) .productImageSection {
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
:global(.darkApp) .productImageSection:hover {
|
||||
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
:global(.darkApp) .rightColumn {
|
||||
background: rgba(255, 255, 255, 0.02);
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
|
||||
/* Mobile responsiveness */
|
||||
@media (max-width: 768px) {
|
||||
.desktopLayout {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 16px;
|
||||
padding: 16px 0;
|
||||
}
|
||||
|
||||
.desktopLayoutFullWidth {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 16px;
|
||||
padding: 16px 0;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.rightColumn {
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
border: none;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.productHeader {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.quantitySection {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.descriptionSection {
|
||||
padding: 16px 16px 0 16px;
|
||||
}
|
||||
@@ -167,3 +294,17 @@
|
||||
line-height: 1.6;
|
||||
}
|
||||
}
|
||||
|
||||
.backButtonContainer {
|
||||
position: absolute;
|
||||
z-index: 999;
|
||||
left: 20px;
|
||||
top: 70px;
|
||||
background-color: #fff;
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
:global(.darkApp) .itemDescriptionIcons path {
|
||||
fill: #ffffff !important;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user