search: apply same menu list updated on search page and add cart button

This commit is contained in:
2025-10-11 11:53:56 +03:00
parent c3f3c9bd49
commit c4d8c3f883
6 changed files with 254 additions and 310 deletions

View File

@@ -325,3 +325,8 @@ label {
.cart-button .ant-badge-count {
left: 0px !important;
}
.product-link-search .ant-card-body {
padding: 16px 16px 8px 16px !important;
box-shadow: none !important;
}

View File

@@ -678,19 +678,22 @@
display: flex;
height: 100%;
width: 100%;
}
.productLink .ant-card {
height: 100%;
display: flex;
flex-direction: column;
justify-content: space-between;
}
.productLink .ant-card .ant-card-body {
flex: 1;
display: flex;
flex-direction: column;
justify-content: space-between;
}
}

View File

@@ -115,20 +115,19 @@ export function MenuList({ data, categoryRefs }: MenuListProps) {
borderRadius: 8,
overflow: "hide",
width: "100%",
boxShadow: "none",
}}
styles={{
body: {
display: "flex",
flexDirection: "column",
justifyContent: "space-between",
borderRadius: 8,
padding: item.description
? "16px 16px 8px 16px"
: "16px 16px 24px 16px",
overflow: "hide",
boxShadow:
"0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -2px rgba(0, 0, 0, 0.05)",
},
boxShadow: "none",
},
}}
>
<div

View File

@@ -1,276 +0,0 @@
import { PlusOutlined } from "@ant-design/icons";
import { Badge, Button, Card, Grid } from "antd";
import ArabicPrice from "components/ArabicPrice";
import HeartIcon from "components/Icons/HeartIcon";
import ImageWithFallback from "components/ImageWithFallback";
import { ItemDescriptionIcons } from "components/ItemDescriptionIcons/ItemDescriptionIcons";
import ProText from "components/ProText";
import { addItem, selectCartItems } from "features/order/orderSlice";
import { useCallback } from "react";
import { useTranslation } from "react-i18next";
import { Link, useParams } from "react-router-dom";
import { useAppDispatch, useAppSelector } from "redux/hooks";
import { colors } from "ThemeConstants";
import { Product } from "utils/types/appTypes";
import styles from "../MenuList/MenuList.module.css";
const { useBreakpoint } = Grid;
interface ProductCardProps {
products: Product[];
}
export function ProductCard({ products }: ProductCardProps) {
const { id } = useParams();
const { isRTL, locale } = useAppSelector((state) => state.locale);
const { sm, md } = useBreakpoint();
const isMobile = !sm;
const isTablet = sm && !md;
const { themeName } = useAppSelector((state) => state.theme);
const dispatch = useAppDispatch();
const cartItems = useAppSelector(selectCartItems);
const getItemQuantity = (id: number | string) => {
const item = cartItems.find((i) => i.id === id);
return item ? item.quantity : 0;
};
const { t } = useTranslation();
// Memoized handlers for better performance
const handleQuickAdd = useCallback(
(item: Product) => {
dispatch(
addItem({
item: {
id: item.id,
name: item.name,
price: item.price,
image: item.image,
description: item.description,
variant: "None",
extras: [],
extrasgroup: [],
},
quantity: 1,
})
);
},
[dispatch]
);
const getMenuItemCardStyle = useCallback(
(itemDescription: string) => {
if (isMobile) {
return {
height: itemDescription?.length > 0 ? 160 : 130,
padding: "12px 12px 12px 16px",
};
} else if (isTablet) {
return {
height: 160,
padding: "16px",
};
} else {
return {
height: 180,
padding: "20px",
};
}
},
[isMobile, isTablet]
);
const getMenuItemImageStyle = useCallback(() => {
if (isMobile) {
return {
width: 90,
height: 95,
};
} else if (isTablet) {
return {
width: 120,
height: 120,
};
} else {
return {
width: 140,
height: 140,
};
}
}, [isMobile, isTablet]);
return (
<>
{products?.map((item) => (
<Link
to={`/${id}/product/${item.id}`}
key={item.id}
className={styles.productLink}
>
<Card
key={item.id}
styles={{
body: {
...getMenuItemCardStyle(item.description),
display: "flex",
flexDirection: "column",
justifyContent: "space-between",
borderRadius: 16,
},
}}
>
<div
style={{
display: "flex",
alignItems: "center",
justifyContent: "space-between",
height: "100%",
gap: isMobile ? 10 : 16,
}}
>
<div
style={{
lineHeight: 1,
height: "100%",
flex: 1,
}}
>
<ProText
style={{
margin: 0,
marginBottom: isMobile ? 8 : isTablet ? 16 : 20,
display: "inline-block",
fontSize: isMobile ? "1rem" : isTablet ? 16 : 18,
fontWeight: 600,
letterSpacing: "-0.01em",
lineHeight: 1.2,
}}
>
{locale == "ar" ? item.nameOther : item.name}
</ProText>
<ProText
type="secondary"
className={styles.itemDescription}
style={{
display: "-webkit-box",
WebkitBoxOrient: "vertical",
WebkitLineClamp: isMobile ? 2 : 4,
overflow: "hidden",
textOverflow: "ellipsis",
wordWrap: "break-word",
overflowWrap: "break-word",
lineHeight: "1.5rem",
maxHeight: isMobile ? "3em" : "6.6em",
fontSize: isMobile ? "1rem" : isTablet ? 16 : 18,
letterSpacing: "0.01em",
}}
>
{item.description}
</ProText>
</div>
<div
style={{
display: "flex",
flexDirection: "row",
justifyContent: "start",
gap: isMobile ? 16 : 24,
position: "absolute",
bottom: isMobile ? 16 : 20,
[isRTL ? "right" : "left"]: isMobile ? 16 : 20,
}}
>
<ArabicPrice
price={item.price}
strong
style={{
fontSize: isMobile ? "1rem" : isTablet ? 18 : 22,
fontWeight: 700,
color: themeName === "dark" ? "#FFC600" : "#1a1a1a",
}}
/>
<ItemDescriptionIcons className={styles.itemDescriptionIcons} />
</div>
<div style={{ position: "relative" }}>
<ImageWithFallback
src={item.image_small || ""}
alt={item.name}
fallbackSrc="/default.png"
className={`${styles.popularMenuItemImage} ${
isMobile
? styles.popularMenuItemImageMobile
: isTablet
? styles.popularMenuItemImageTablet
: styles.popularMenuItemImageDesktop
}`}
{...getMenuItemImageStyle()}
/>
<Button
className={styles.heartButton}
icon={<HeartIcon />}
style={{ width: 24, height: 24 }}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
console.log("heart");
}}
/>
<Button
shape="round"
title="add"
iconPosition="start"
icon={
<PlusOutlined
title="add"
style={{
position: "relative",
top: "-1px",
}}
/>
}
size={isMobile ? "small" : "middle"}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
handleQuickAdd(item);
}}
style={{
position: "absolute",
bottom: -10,
[isRTL ? "right" : "left"]: isMobile ? "5%" : "15%",
zIndex: 1,
width: isMobile ? 82 : isTablet ? 90 : 100,
height: isMobile ? 32 : isTablet ? 40 : 44,
fontSize: isMobile ? "1rem" : isTablet ? 16 : 18,
fontWeight: 600,
border: 0,
color: themeName === "light" ? "#333333" : "#FFFFFF",
boxShadow:
themeName === "light"
? "0 2px 0 rgba(0,0,0,0.02)"
: "0 2px 0 #6b6b6b",
}}
>
{t("common.add")}
</Button>
{/* Cart quantity badge - shows current quantity in cart */}
{getItemQuantity(item.id) > 0 && (
<Badge
count={getItemQuantity(item.id)}
className={
styles.cartBadge +
" " +
(isRTL ? styles.cartBadgeRTL : styles.cartBadgeLTR)
}
style={{
backgroundColor: colors.primary,
}}
title={`${getItemQuantity(item.id)} in cart`}
/>
)}
</div>
</div>
</Card>
</Link>
))}
</>
);
}

View File

@@ -0,0 +1,207 @@
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 useBreakPoint from "hooks/useBreakPoint";
import { AddToCartButton } from "pages/menu/components/AddToCartButton/AddToCartButton.tsx";
import { useTranslation } from "react-i18next";
import { useNavigate } from "react-router-dom";
import { useAppSelector } from "redux/hooks";
import { colors } from "ThemeConstants";
import { Product } from "utils/types/appTypes";
import styles from "../MenuList/MenuList.module.css";
interface MenuListProps {
products: Product[];
}
export function SearchMenu({ products }: MenuListProps) {
const { isRTL } = useAppSelector((state) => state.locale);
const { isMobile, isTablet } = useBreakPoint();
const { items } = useAppSelector((state) => state.order);
const restaurantName = localStorage.getItem("restaurantName");
const navigate = useNavigate();
const { t } = useTranslation();
const { themeName } = useAppSelector((state) => state.theme);
// Show error state if data exists but has no products
if (products && (!products || products.length === 0)) {
return (
<div
className={styles.menuSections}
style={{ padding: "40px", textAlign: "center" }}
>
<ProText type="secondary">{t("menu.noMenuItemsAvailable")}</ProText>
</div>
);
}
return (
<>
<div className={styles.menuSections}>
<div className={styles.menuItemsGrid}>
{products.map((item: Product) => (
<div
key={item.id}
className={styles.productLink + " product-link-search"}
onClick={() => {
localStorage.setItem("product", JSON.stringify(item));
navigate(`/${restaurantName}/product/${item.id}`);
}}
>
<Card
key={item.id}
style={{
borderRadius: 8,
overflow: "hide",
width: "100%",
}}
styles={{
body: {
display: "flex",
flexDirection: "column",
justifyContent: "space-between",
borderRadius: 8,
padding: item.description
? "16px 16px 8px 16px"
: "16px 16px 24px 16px",
overflow: "hide",
boxShadow:
"0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -2px rgba(0, 0, 0, 0.05)",
},
}}
>
<div
style={{
display: "flex",
alignItems: "center",
justifyContent: "space-between",
height: "100%",
gap: isMobile ? 10 : 16,
}}
>
<div
style={{
display: "flex",
flexDirection: "column",
justifyContent: "space-between",
gap: "0.5rem",
}}
>
<ProText
style={{
margin: 0,
display: "inline-block",
fontSize: "1rem",
fontWeight: 600,
letterSpacing: "-0.01em",
lineHeight: 1.2,
color: themeName === "dark" ? "#fff" : "#000044",
}}
>
{isRTL ? item.name : item.nameOther}
</ProText>
{item.description && (
<ProText
type="secondary"
className={styles.itemDescription}
style={{
display: "-webkit-box",
WebkitBoxOrient: "vertical",
WebkitLineClamp: 2,
overflow: "hidden",
textOverflow: "ellipsis",
wordWrap: "break-word",
overflowWrap: "break-word",
lineHeight: "1.5rem",
maxHeight: "3em",
fontSize: "1rem",
letterSpacing: "0.01em",
}}
>
{item.description}
</ProText>
)}
<div>
{item.original_price !== item.price && (
<ArabicPrice
price={item.original_price}
strong
style={{
fontSize: "1rem",
fontWeight: 700,
color: colors.primary,
textDecoration: "line-through",
marginRight: isRTL ? 0 : 10,
marginLeft: isRTL ? 10 : 0,
}}
/>
)}
<ArabicPrice
price={item.price}
strong
style={{
fontSize: "1rem",
fontWeight: 700,
color: colors.primary,
}}
/>
</div>
<div
style={{
position: "relative",
...(isRTL ? { right: -5 } : {}),
}}
>
<ItemDescriptionIcons
className={styles.itemDescriptionIcons}
/>
</div>
</div>
<div style={{ position: "relative" }}>
<ImageWithFallback
src={item.image_small || "/default.png"}
fallbackSrc="/default.png"
alt={item.name}
className={`${styles.popularMenuItemImage} ${
isMobile
? styles.popularMenuItemImageMobile
: isTablet
? styles.popularMenuItemImageTablet
: styles.popularMenuItemImageDesktop
}`}
width={90}
height={90}
/>
<AddToCartButton item={item} />
{items.find((i) => i.id === item.id) && (
<Badge
count={items.find((i) => i.id === item.id)?.quantity}
className={
styles.cartBadge +
" " +
(isRTL ? styles.cartBadgeRTL : styles.cartBadgeLTR)
}
style={{
backgroundColor: colors.primary,
}}
title={`${
items.find((i) => i.id === item.id)?.quantity
} in cart`}
/>
)}
</div>
</div>
</Card>
</div>
))}
</div>
</div>
</>
);
}

View File

@@ -4,13 +4,15 @@ import SearchIcon from "components/Icons/SearchIcon";
import LoadingSpinner from "components/LoadingSpinner";
import ProHeader from "components/ProHeader/ProHeader";
import ProText from "components/ProText";
import useBreakPoint from "hooks/useBreakPoint";
import { CartButton } from "pages/menu/components/CartButton/CartButton";
import { SearchMenu } from "pages/menu/components/SearchMenu/SearchMenu";
import { useCallback, useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { Link, useParams, useSearchParams } from "react-router-dom";
import { useGetMenuQuery } from "redux/api/others";
import { useAppSelector } from "redux/hooks";
import { Product } from "utils/types/appTypes";
import { ProductCard } from "../menu/components/ProductCard/ProductCard";
import styles from "./search.module.css";
export default function SearchPage() {
@@ -22,6 +24,7 @@ export default function SearchPage() {
const [searchResults, setSearchResults] = useState<Product[]>([]);
const [isSearching, setIsSearching] = useState(false);
const restaurantID = localStorage.getItem("restaurantID");
const { isDesktop } = useBreakPoint();
const { data: menuData } = useGetMenuQuery(restaurantID as string, {
skip: !restaurantID,
@@ -52,7 +55,7 @@ export default function SearchPage() {
setIsSearching(false);
}, 300);
},
[menuData]
[menuData],
);
// Handle input change and update search params
@@ -67,7 +70,7 @@ export default function SearchPage() {
setSearchParams({});
}
},
[setSearchParams]
[setSearchParams],
);
// Debounced search effect
@@ -93,7 +96,6 @@ export default function SearchPage() {
<div
style={{
height: "82vh",
display: "flex",
flexDirection: "column",
gap: 16,
@@ -152,7 +154,7 @@ export default function SearchPage() {
<LoadingSpinner size="large" spinning={true} showText={true} />
</div>
) : searchQuery && searchResults?.length > 0 ? (
<ProductCard products={searchResults} />
<SearchMenu products={searchResults} />
) : searchQuery && searchResults?.length === 0 && !isSearching ? (
<div className={styles.noResults}>
<ProText className={styles.noResultsText}>
@@ -169,29 +171,33 @@ export default function SearchPage() {
) : null}
</div>
</div>
<Row
style={{
width: "100%",
padding: "16px 16px 0",
position: "fixed",
bottom: 0,
left: 0,
backgroundColor: themeName === "light" ? "white" : ProBlack2,
boxShadow: "0px -1px 3px rgba(0, 0, 0, 0.1)",
height: "10vh",
zIndex: 999,
}}
>
<Link to={`${id}/cart`} style={{ width: "100%" }}>
<Button
type="primary"
shape="round"
style={{ width: "100%", height: 48, marginBottom: 16 }}
>
{t("menu.cart")}
</Button>
</Link>
</Row>
{!isDesktop ? (
<Row
style={{
width: "100%",
padding: "16px 16px 0",
position: "fixed",
bottom: 0,
left: 0,
backgroundColor: themeName === "light" ? "white" : ProBlack2,
boxShadow: "0px -1px 3px rgba(0, 0, 0, 0.1)",
height: "10vh",
zIndex: 999,
}}
>
<Link to={`/${id}/cart`} style={{ width: "100%" }}>
<Button
type="primary"
shape="round"
style={{ width: "100%", height: 48, marginBottom: 16 }}
>
{t("menu.cart")}
</Button>
</Link>
</Row>
) : (
<CartButton />
)}
</>
);
}