349 lines
12 KiB
TypeScript
349 lines
12 KiB
TypeScript
import ActionsButtons from "components/ActionsButtons/ActionsButtons";
|
|
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 } = 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(() => {
|
|
if (!menuData?.products || !productId) return null;
|
|
|
|
// Find the product with matching IDs
|
|
return menuData.products.find(
|
|
(p: Product) => p.id.toString() === productId,
|
|
);
|
|
}, [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,
|
|
});
|
|
}
|
|
}
|
|
|
|
// console.log("Variant levels:", levels);
|
|
// console.log("Selected variants:", selectedVariants);
|
|
|
|
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: "80vh",
|
|
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 ? 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
|
|
strong
|
|
style={{ fontSize: isDesktop ? "1.5rem" : "1.25rem" }}
|
|
>
|
|
{isRTL ? product?.nameOther : product?.name}
|
|
</ProText>
|
|
<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 className={styles.quantitySection}>
|
|
<ActionsButtons
|
|
quantity={quantity}
|
|
setQuantity={(quantity) => setQuantity(quantity)}
|
|
max={100}
|
|
min={1}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{isDesktop && (
|
|
<ProductFooter
|
|
product={product}
|
|
isValid={isValid}
|
|
selectedVariant={getFinalSelectedVariant()}
|
|
selectedExtras={selectedExtras}
|
|
selectedGroups={selectedExtrasByGroup}
|
|
quantity={quantity}
|
|
onClose={onClose}
|
|
/>
|
|
)}
|
|
</div>
|
|
|
|
{/* 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}
|
|
/>
|
|
)}
|
|
|
|
{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}
|
|
/>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|