Files
web-menu-react-version-/src/pages/product/page.tsx

349 lines
12 KiB
TypeScript

import ImageWithFallback from "components/ImageWithFallback";
import { ItemDescriptionIcons } from "components/ItemDescriptionIcons/ItemDescriptionIcons";
import ProText from "components/ProText";
import { useAppSelector } from "redux/hooks";
import { default_image } from "utils/constants";
// import PageTransition from "components/PageTransition/PageTransition";
import { Space } from "antd";
import ArabicPrice from "components/ArabicPrice";
import useBreakPoint from "hooks/useBreakPoint";
import ExtraGroupsContainer from "pages/product/components/ExtraGroupsContainer.tsx";
import { useCallback, useMemo, useState } from "react";
import { useParams } from "react-router-dom";
import { useGetMenuQuery } from "redux/api/others.ts";
import { colors } from "ThemeConstants";
import { Extra, Product } from "utils/types/appTypes";
import BackButton from "../menu/components/BackButton";
import ExtraComponent from "./components/Extra";
import ProductFooter from "./components/ProductFooter";
import Variants from "./components/Variants";
import styles from "./product.module.css";
export default function ProductDetailPage({
onClose,
}: {
onClose?: () => void;
}) {
const { productId } = useParams();
const { isRTL } = useAppSelector((state) => state.locale);
const { restaurant } = useAppSelector((state) => state.order);
const { isDesktop, isTablet } = useBreakPoint();
const [quantity, setQuantity] = useState(1);
// Get menu data
const { data: menuData } = useGetMenuQuery(restaurant?.restautantId, {
skip: !restaurant?.restautantId,
});
// Find product from menu data
const product: Product = useMemo(() => {
// we still need to support the old way of passing the product id in local storage
// because in desktop we open the product dialog from the menu list
// and the product id is not passed in the url
const productIdLocalStorage = localStorage.getItem("productId");
if (!menuData?.products || (!productId && !productIdLocalStorage))
return null;
// Find the product with matching IDs
return menuData.products.find(
(p: Product) => p.id.toString() === (productId || productIdLocalStorage),
);
}, [menuData, productId]);
// State for variant selections
const [selectedVariants, setSelectedVariants] = useState<
Record<number, string>
>({});
// State for selected extras
const [selectedExtras, setSelectedExtras] = useState<Extra[]>([]);
// State for selected extras by group
const [selectedExtrasByGroup, setSelectedExtrasByGroup] = useState<
Record<number, Array<string>>
>([]);
// Determine variant levels based on options array length
const variantLevels = useMemo(() => {
if (!product?.variants || product.variants.length === 0) return [];
const maxOptionsLength = Math.max(
...product.variants.map((v) => v.options.length),
);
const levels: Array<{
level: number;
optionKey: string;
optionKeyAR: string;
availableValues: string[];
}> = [];
for (let i = 0; i < maxOptionsLength; i++) {
const optionKey = product.variants[0]?.options[i]?.option || "";
const optionKeyAR = product.variants[0]?.optionsAR?.[i]?.option || "";
if (optionKey) {
const availableValues = [
...new Set(
product.variants
.filter((v) => v.options[i]?.option === optionKey)
.map((v) => v.options[i]?.value)
.filter(Boolean),
),
];
levels.push({
level: i, // Use the actual array index as level number
optionKey,
optionKeyAR,
availableValues,
});
}
}
return levels;
}, [product?.variants]);
// Get the final selected variant ID (the variant that matches all current selections)
const getFinalSelectedVariant = useCallback(() => {
if (!product?.variants || Object.keys(selectedVariants).length === 0)
return undefined;
// Find the variant that matches all current selections
// Convert to string only if defined, otherwise return empty string
return product.variants.find((variant) => {
return Object.entries(selectedVariants).every(([level, value]) => {
const levelNum = parseInt(level);
return variant.options[levelNum]?.value === value;
});
});
}, [product?.variants, selectedVariants]);
const getExtras = useCallback(() => {
const finalSelectedVariant = getFinalSelectedVariant();
if (!finalSelectedVariant) return [];
const selectedVariant = product?.variants?.find(
(variant) => variant.id === finalSelectedVariant.id,
);
return selectedVariant?.extras || [];
}, [product?.variants, getFinalSelectedVariant]);
// 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,
);
// Check if all required extra groups are satisfied
const allRequiredExtraGroupsSatisfied = product?.theExtrasGroups.every(
(group) => {
if (group.force_limit_selection === 1) {
const selectedCount = selectedExtrasByGroup[group.id]?.length || 0;
return selectedCount === group.limit;
}
return true; // Optional groups are always satisfied
},
);
return allVariantsSelected && allRequiredExtraGroupsSatisfied;
};
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" }}>
<ProText type="secondary">Product not found</ProText>
</div>
);
}
return (
<div
style={{
height: "calc(100vh - 195px)",
overflow: "auto",
scrollbarWidth: "none",
}}
>
{!isDesktop && (
<div className={styles.backButtonContainer}>
<BackButton />
</div>
)}
<div className={styles.productDetailContainer}>
<div
className={
isDesktop && hasCustomizationOptions
? styles.desktopLayout
: isDesktop && !hasCustomizationOptions
? styles.desktopLayoutFullWidth
: styles.mobileLayout
}
>
{/* 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 || isTablet ? 300 : 240}
width={"100%"}
style={{
width: "100%",
objectFit: "cover",
}}
loadingContainerStyle={{
width: "100%",
}}
/>
</div>
<div className={styles.productInfoSection}>
<div className={styles.productHeader}>
<div className={styles.productDetails}>
<ProText
style={{
fontWeight: 400,
fontStyle: "Regular",
fontSize: "14px",
lineHeight: "140%",
letterSpacing: "0%",
}}
>
{isRTL ? product?.nameOther : product?.name}
</ProText>
{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",
maxHeight: "2.8em",
fontWeight: 400,
fontStyle: "Regular",
fontSize: "12px",
lineHeight: "140%",
letterSpacing: "0%",
marginTop: 10,
}}
>
{isRTL ? product?.descriptionAR : product?.description}
</ProText>
)}
<div
style={{
display: "flex",
flexDirection: "row",
justifyContent: "start",
gap: 20,
marginTop: "16px",
...(isRTL ? { marginRight: -5 } : {}),
}}
>
<ItemDescriptionIcons
className={styles.itemDescriptionIcons}
/>
</div>
<ArabicPrice
price={product?.price}
style={{
fontSize: isDesktop ? "1.2rem" : "1rem",
color: colors.primary,
marginTop: "12px",
}}
/>
</div>
</div>
</div>
{isDesktop && (
<ProductFooter
product={product}
isValid={isValid}
selectedVariant={getFinalSelectedVariant()}
selectedExtras={selectedExtras}
selectedGroups={selectedExtrasByGroup}
quantity={quantity}
onClose={onClose}
setQuantity={(quantity: number) => setQuantity(quantity)}
/>
)}
</div>
{/* Right Column - Variants, Extras, and Extra Groups */}
{hasCustomizationOptions && (
<div className={isDesktop ? styles.rightColumn : styles.fullWidth}>
<Space
orientation="vertical"
size="middle"
style={{ width: "100%", padding: "0 1rem" }}
>
{product?.variants?.length > 0 && variantLevels.length > 0 && (
<Variants
selectedVariants={selectedVariants}
setSelectedVariants={setSelectedVariants}
variantsList={product?.variants}
/>
)}
{product.theExtrasGroups.length > 0 && (
<ExtraGroupsContainer
groupsList={product.theExtrasGroups}
selectedExtrasByGroup={selectedExtrasByGroup}
setSelectedExtrasByGroup={setSelectedExtrasByGroup}
/>
)}
{getExtras()?.length > 0 && (
<ExtraComponent
extrasList={getExtras()}
selectedExtras={selectedExtras}
setSelectedExtras={setSelectedExtras}
/>
)}
</Space>
</div>
)}
</div>
{!isDesktop && (
<ProductFooter
product={product}
isValid={isValid}
selectedVariant={getFinalSelectedVariant()}
selectedExtras={selectedExtras}
selectedGroups={selectedExtrasByGroup}
quantity={quantity}
setQuantity={(quantity: number) => setQuantity(quantity)}
/>
)}
</div>
</div>
);
}