menu: UI enhancements in desktop sizes

- add product dialog
- enhance product page layout for desktop size
This commit is contained in:
2025-10-11 20:34:22 +03:00
parent c4d8c3f883
commit cbc64b6e38
13 changed files with 456 additions and 189 deletions

View File

@@ -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>
);