Initial commit

This commit is contained in:
2025-10-04 18:22:24 +03:00
commit 2852c2c054
291 changed files with 38109 additions and 0 deletions

293
src/pages/product/page.tsx Normal file
View File

@@ -0,0 +1,293 @@
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 { useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import { colors } from "ThemeConstants";
import { Product } from "utils/types/appTypes";
import BackButton from "../menu/components/BackButton";
import Extra from "./components/Extra";
import ExtraGroups from "./components/ExtraGroups";
import ProductFooter from "./components/ProductFooter";
import Variants from "./components/Variants";
import styles from "./product.module.css";
export default function ProductDetailPage() {
const { isRTL } = useAppSelector((state) => state.locale);
const { t } = useTranslation();
const [quantity, setQuantity] = useState(1);
const product = JSON.parse(
localStorage.getItem("product") || "null"
) as Product;
// State for variant selections
const [selectedVariants, setSelectedVariants] = useState<
Record<number, string>
>({});
// State for selected extras
const [selectedExtras, setSelectedExtras] = useState<string[]>([]);
// State for selected extras by group
const [selectedExtrasByGroup, setSelectedExtrasByGroup] = useState<
Record<number, 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 getFinalSelectedVariantId = () => {
if (!product?.variants || Object.keys(selectedVariants).length === 0)
return "";
// Find the variant that matches all current selections
const matchingVariant = product.variants.find((variant) => {
return Object.entries(selectedVariants).every(([level, value]) => {
const levelNum = parseInt(level);
return variant.options[levelNum]?.value === value;
});
});
// Convert to string only if defined, otherwise return empty string
return matchingVariant?.id?.toString() || "";
};
const getExtras = () => {
const finalSelectedVariantId = getFinalSelectedVariantId();
if (finalSelectedVariantId) {
const variant = product?.variants?.find(
(variant) => variant.id === Number(finalSelectedVariantId)
);
if (variant?.extras?.length && variant.extras.length > 0) {
return variant.extras;
}
}
return product?.extras;
};
// 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();
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",
}}
>
<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>
<div className={styles.productDetailContainer}>
<div
style={{
display: "grid",
gridTemplateColumns: "1fr", // isMobile ? "1fr" : "1fr 1fr",
gap: 5,
}}
>
<Space direction="vertical" size="middle" style={{ width: "100%" }}>
<div
style={{
display: "flex",
flexDirection: "row",
justifyContent: "space-between",
}}
>
<div
style={{
display: "flex",
flexDirection: "column",
justifyContent: "space-between",
gap: "0.5rem",
marginTop: 16,
}}
>
<ProText strong style={{ fontSize: "1.25rem" }}>
{isRTL ? product?.nameOther : product?.name}{" "}
</ProText>
{product?.description && (
<ProText
type="secondary"
style={{ fontSize: "1rem", lineHeight: "1.25rem" }}
>
{isRTL ? product?.descriptionAR : product?.description}
</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 />
</div>
</div>
<div
style={{
display: "flex",
flexDirection: "column",
justifyContent: "end",
}}
>
<ActionsButtons
quantity={quantity}
setQuantity={(quantity) => setQuantity(quantity)}
max={100}
min={1}
/>
</div>
</div>
{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>
<ProductFooter
product={product}
isValid={isValid}
variantId={getFinalSelectedVariantId()}
selectedExtras={selectedExtras}
selectedGroups={Object.values(selectedExtrasByGroup).flat()}
quantity={quantity}
/>
</div>
</div>
);
}