menu: on click on prduct open details as BS, also add redirect button to detials page if user click on redirect button

This commit is contained in:
2025-12-23 01:36:46 +03:00
parent fc1a75bc4b
commit 5c27303695
16 changed files with 224 additions and 78 deletions

View File

@@ -253,6 +253,8 @@ export function ProBottomSheet({
border: `1px solid ${themeName === "dark" ? "#424242" : "transparent"}`,
transition: "all 0.3s ease",
cursor: "pointer",
width: 30,
height: 30,
};
const titleStyle: React.CSSProperties = {

View File

@@ -6,6 +6,8 @@ import { useAppSelector } from "redux/hooks";
import { ProBlack2 } from "ThemeConstants";
import ProTitle from "../ProTitle";
import useBreakPoint from "hooks/useBreakPoint";
import NextIcon from "components/Icons/NextIcon";
const { Text } = Typography;
interface ProHeaderProps {
@@ -41,7 +43,6 @@ const ProHeader: FunctionComponent<ProHeaderProps> = ({
top: 0,
display: "flex",
zIndex: 1000,
flexDirection: isRTL ? "row-reverse" : "row",
justifyContent: "space-between",
alignItems: "center", // This centers vertically
backgroundColor: themeName === "light" ? "white" : ProBlack2,
@@ -52,10 +53,9 @@ const ProHeader: FunctionComponent<ProHeaderProps> = ({
>
<Button
type="text"
icon={<BackIcon />}
icon={isRTL ? <NextIcon /> : <BackIcon />}
style={{
flex: 0, // Don't allow this to grow
marginRight: "auto", // Push it to the start
}}
onClick={handleBack}
/>

View File

@@ -1,23 +1,17 @@
import { PlusOutlined } from "@ant-design/icons";
import { Card, Divider, Form, Space, Layout } from "antd";
import { Card, Divider, Space, Layout } from "antd";
import ArabicPrice from "components/ArabicPrice";
import CartActionsButtons from "components/CartActionsButtons/CartActionsButtons.tsx";
import ImageWithFallback from "components/ImageWithFallback";
import ProHeader from "components/ProHeader/ProHeader.tsx";
import ProInputCard from "components/ProInputCard/ProInputCard.tsx";
import ProRatioGroups from "components/ProRatioGroups/ProRatioGroups.tsx";
import ProText from "components/ProText.tsx";
import ProTitle from "components/ProTitle.tsx";
import {
selectCart,
updateCollectionMethod,
} from "features/order/orderSlice.ts";
import { selectCart } from "features/order/orderSlice.ts";
import styles from "pages/cart/cart.module.css";
import YouMightAlsoLike from "pages/cart/components/youMayLike/YouMightAlsoLike.tsx";
import { Link, useParams } from "react-router-dom";
import { colors } from "ThemeConstants.ts";
import OrderSummary from "components/OrderSummary/OrderSummary.tsx";
import { useAppSelector } from "redux/hooks.ts";
import { FormInstance } from "antd";
@@ -25,7 +19,6 @@ import useBreakPoint from "hooks/useBreakPoint.ts";
import CarPlateCard from "pages/cart/components/CarPlateCard.tsx";
import CartFooter from "pages/cart/components/cartFooter/CartFooter.tsx";
import CouponCard from "pages/cart/components/CouponCard.tsx";
import RewardWaiterCard from "pages/cart/components/RewardWaiterCard.tsx";
import SpecialRequestCard from "pages/cart/components/specialRequest/SpecialRequestCard.tsx";
import TableNumberCard from "pages/cart/components/TableNumberCard.tsx";
import TimeEstimateCard from "pages/cart/components/timeEstimate/TimeEstimateCard.tsx";
@@ -41,7 +34,7 @@ export default function CartMobileTabletLayout({
form,
}: CartMobileTabletLayoutProps) {
const { t } = useTranslation();
const { items, collectionMethod, orderType } = useAppSelector(selectCart);
const { items, orderType } = useAppSelector(selectCart);
const { isRTL } = useAppSelector((state) => state.locale);
const { subdomain } = useParams();
const { pickup_type } = useAppSelector((state) => state.order.restaurant);
@@ -52,8 +45,8 @@ export default function CartMobileTabletLayout({
const getMenuItemImageStyle = () => {
if (isMobile) {
return {
width: 90,
height: 80,
width: 115,
height: 96,
};
}
return {
@@ -73,6 +66,10 @@ export default function CartMobileTabletLayout({
size={isMobile ? "middle" : isTablet ? "large" : "large"}
style={{ width: "100%", gap: 16 }}
>
{/* Table Number */}
{(orderType === OrderType.DineIn ||
orderType === OrderType.ToOffice) && <TableNumberCard />}
<div className={`${styles.cartContent} ${getResponsiveClass()}`}>
<div className={styles.cartItems}>
<Card hoverable className={styles.cartItem}>
@@ -170,7 +167,7 @@ export default function CartMobileTabletLayout({
style={{
margin: 0,
lineClamp: 1,
padding: isMobile ? 3 : isTablet ? 8 : 10,
padding: isMobile ? "3px 0" : isTablet ? 8 : 10,
fontSize: isMobile ? 14 : isTablet ? 18 : 20,
display: "-webkit-box",
WebkitBoxOrient: "vertical",
@@ -187,7 +184,7 @@ export default function CartMobileTabletLayout({
: "6.8em",
fontWeight: 500,
letterSpacing: "0.01em",
width: "65%",
width: "55%",
}}
>
{item.description}
@@ -196,7 +193,7 @@ export default function CartMobileTabletLayout({
<div
style={{
position: "absolute",
bottom: index !== items.length - 1 ? 16 : 6,
bottom: index !== items.length - 1 ? 16 : 3,
[isRTL ? "right" : "left"]: 0,
}}
>
@@ -218,7 +215,9 @@ export default function CartMobileTabletLayout({
/>
<div
style={{
marginTop: 10,
position: "absolute",
right: 3,
bottom: 3,
}}
>
<CartActionsButtons item={item} />
@@ -239,8 +238,6 @@ export default function CartMobileTabletLayout({
<SpecialRequestCard />
<CouponCard />
{/* Car Plate*/}
{((orderType === OrderType.Pickup && pickup_type === "car") ||
orderType === OrderType.ScheduledOrder) && <CarPlateCard />}
@@ -250,7 +247,7 @@ export default function CartMobileTabletLayout({
orderType === OrderType.ScheduledOrder) && <TimeEstimateCard />}
{/* Collection Method */}
{orderType === OrderType.Pickup && (
{/* {orderType === OrderType.Pickup && (
<ProInputCard title={t("cart.collectionMethod")}>
<Form.Item
name="collectionMethod"
@@ -282,17 +279,13 @@ export default function CartMobileTabletLayout({
/>
</Form.Item>
</ProInputCard>
)}
)} */}
{/* Reward Your Waiter */}
<RewardWaiterCard />
{/* Table Number */}
{(orderType === OrderType.DineIn ||
orderType === OrderType.ToOffice) && <TableNumberCard />}
{/* <RewardWaiterCard /> */}
{/* Invoice Summary */}
<OrderSummary />
{/* <OrderSummary /> */}
</Space>
</Layout.Content>
<CartFooter form={form} />

View File

@@ -6,9 +6,10 @@
display: flex;
flex-direction: row;
justify-content: flex-start;
overflow-x: hidden;
overflow-x: auto;
padding: 0;
scroll-behavior: smooth;
scrollbar-width: none;
}
:global(.darkApp) .youMightAlsoLikeContainer path {
@@ -56,7 +57,7 @@
outline-offset: 4px;
}
.youMightAlsoLikeContainer {
height: 130px !important;
height: 150px !important;
}
.itemDescriptionIconsContainer {

View File

@@ -191,19 +191,19 @@ export default function YouMightAlsoLike() {
flexDirection: "column",
alignItems: "center",
justifyContent: "space-between",
width: isMobile ? "95px" : isTablet ? "120px" : "140px",
width: isMobile ? "120px" : isTablet ? "120px" : "140px",
position: "relative",
overflow: "hidden",
}}
>
<div
style={{
width: isMobile ? 18 : 24,
height: isMobile ? 18 : 24,
width: isMobile ? 28 : 24,
height: isMobile ? 28 : 24,
borderRadius: "50%",
top: isMobile ? 50 : isTablet ? 60 : 80,
top: isMobile ? 65 : isTablet ? 60 : 80,
position: "absolute",
[isRTL ? "left" : "right"]: isMobile ? 15 : 20,
[isRTL ? "left" : "right"]: isMobile ? 10 : 20,
display: "flex",
flexDirection: "row",
justifyContent: "center",
@@ -234,8 +234,8 @@ export default function YouMightAlsoLike() {
? styles.popularMenuItemImageTablet
: styles.popularMenuItemImageDesktop
}`}
width={isMobile ? 73 : isTablet ? 90 : 110}
height={isMobile ? 73 : isTablet ? 90 : 110}
width={isMobile ? 106 : isTablet ? 90 : 110}
height={isMobile ? 96 : isTablet ? 90 : 110}
fallbackSrc={default_image}
/>

View File

@@ -3,7 +3,7 @@ import InputCard from "components/InputCard";
import OrderSummary from "components/OrderSummary/OrderSummary";
import PaymentMethods from "components/PaymentMethods/PaymentMethods";
import ProHeader from "components/ProHeader/ProHeader";
import { selectCart } from "features/order/orderSlice";
import { selectCart, updateCollectionMethod } from "features/order/orderSlice";
import { useTranslation } from "react-i18next";
import { useAppSelector } from "redux/hooks";
import styles from "../address/address.module.css";
@@ -13,11 +13,16 @@ import CheckoutButton from "./components/CheckoutButton";
import { GiftCard } from "./components/GiftCard";
import PhoneCard from "./components/phoneCard";
import { OrderType } from "./hooks/types";
import RewardWaiterCard from "pages/cart/components/RewardWaiterCard";
import ProInputCard from "components/ProInputCard/ProInputCard";
import ProRatioGroups from "components/ProRatioGroups/ProRatioGroups";
import CouponCard from "pages/cart/components/CouponCard";
export default function CheckoutPage() {
const { t } = useTranslation();
const [form] = Form.useForm();
const { phone, order, orderType } = useAppSelector(selectCart);
const { phone, order, orderType, collectionMethod } =
useAppSelector(selectCart);
const { token } = useAppSelector((state) => state.auth);
return (
<>
@@ -53,8 +58,47 @@ export default function CheckoutPage() {
{/* <RoomDetails />
<OfficeDetails /> */}
{/* <GiftDetails /> */}
<BriefMenu />
{/* <BriefMenu /> */}
<PaymentMethods />
<CouponCard />
{/* Collection Method */}
{orderType === OrderType.Pickup && (
<ProInputCard title={t("cart.collectionMethod")}>
<Form.Item
name="collectionMethod"
required
rules={[
{
required: true,
message: t("cart.pleaseSelectCollectionMethod"),
},
]}
>
<ProRatioGroups
options={[
{ label: t("cart.Cash"), value: "cod", price: "" },
{
label: t("cart.e-payment"),
value: "paymentgateway",
price: "",
},
]}
value={collectionMethod}
onRatioClick={(value) => {
if (value === "cod") {
updateCollectionMethod(value);
} else {
updateCollectionMethod(value);
}
}}
/>
</Form.Item>
</ProInputCard>
)}
{/* Reward Your Waiter */}
<RewardWaiterCard />
<OrderSummary />
</Layout.Content>

View File

@@ -85,10 +85,6 @@ export function AddToCartButton({ item }: { item: Product }) {
message.warning(t("menu.restaurantIsClosed"));
return;
}
if (restaurant && !restaurant.isOpened) {
message.warning(t("menu.restaurantIsClosed"));
return;
}
if (basicCartItem && basicCartItem.uniqueId) {
if (basicCartItem.quantity > 1) {

View File

@@ -1,6 +1,7 @@
import { Button } from "antd";
import BackIcon from "components/Icons/BackIcon";
import NextIcon from "components/Icons/NextIcon";
import { useAppSelector } from "redux/hooks";
interface BackButtonProps {
navigateBack?: boolean; // true = use router.back(), false = just clear state
@@ -10,6 +11,7 @@ export default function BackButton({ navigateBack = true }: BackButtonProps) {
const handleBack = () => {
if (navigateBack) window.history.back();
};
const { isRTL } = useAppSelector((state) => state.locale);
return (
<Button
@@ -23,8 +25,14 @@ export default function BackButton({ navigateBack = true }: BackButtonProps) {
borderRadius: "50%",
}}
icon={
<div style={{ position: "relative", top: 2.5, right: 1 }}>
<BackIcon />
<div
style={{
position: "relative",
top: 2.5,
[isRTL ? "left" : "right"]: 1,
}}
>
{isRTL ? <NextIcon /> : <BackIcon />}
</div>
}
onClick={handleBack}

View File

@@ -6,6 +6,8 @@ import { useAppSelector } from "redux/hooks";
import { Product } from "utils/types/appTypes";
import styles from "./MenuList.module.css";
import ProductCard from "pages/menu/components/MenuList/ProductCard.tsx";
import { ProductBottomSheet } from "pages/product/components/ProductBottomSheet";
import { useState } from "react";
interface MenuListProps {
data:
@@ -20,6 +22,7 @@ interface MenuListProps {
export function MenuList({ data, categoryRefs }: MenuListProps) {
const { isRTL } = useAppSelector((state) => state.locale);
const products = data?.products;
const [isBottomSheetOpen, setIsBottomSheetOpen] = useState(false);
const { t } = useTranslation();
const { themeName } = useAppSelector((state) => state.theme);
@@ -80,12 +83,20 @@ export function MenuList({ data, categoryRefs }: MenuListProps) {
</ProTitle>
<div className={styles.menuItemsGrid}>
{categoryProducts.map((item: Product) => (
<ProductCard item={item} key={item.id} />
<ProductCard
item={item}
key={item.id}
setIsBottomSheetOpen={setIsBottomSheetOpen}
/>
))}
</div>
</div>
);
})}
<ProductBottomSheet
isOpen={isBottomSheetOpen}
onClose={() => setIsBottomSheetOpen(false)}
/>
</div>
</>
);

View File

@@ -11,24 +11,25 @@ import { useAppSelector } from "redux/hooks.ts";
import { ProductPreviewDialog } from "pages/menu/components/ProductPreviewDialog";
import { useState } from "react";
import { AddToCartButton } from "../AddToCartButton/AddToCartButton";
import { useNavigate, useParams } from "react-router-dom";
import { useTranslation } from "react-i18next";
type Props = {
item: Product;
setIsBottomSheetOpen: (isOpen: boolean) => void;
};
export default function ProductCard({ item }: Props) {
export default function ProductCard({ item, setIsBottomSheetOpen }: Props) {
const { isRTL } = useAppSelector((state) => state.locale);
const { isMobile, isTablet, isDesktop } = useBreakPoint();
const { items } = useAppSelector((state) => state.order);
const [isDialogOpen, setIsDialogOpen] = useState(false);
const navigate = useNavigate();
const { subdomain } = useParams();
// const navigate = useNavigate();
// const { subdomain } = useParams();
const { t } = useTranslation();
// Handle dialog close
const handleDialogClose = () => {
setIsDialogOpen(false);
setIsBottomSheetOpen(false);
};
// Handle product click - open dialog on desktop, navigate on mobile/tablet
@@ -36,9 +37,11 @@ export default function ProductCard({ item }: Props) {
localStorage.setItem("productId", item.id.toString());
if (isDesktop) {
setIsDialogOpen(true);
} else {
navigate(`/${subdomain}/product/${item.id}`);
}
setIsBottomSheetOpen(true);
// else {
// navigate(`/${subdomain}/product/${item.id}`);
// }
};
return (

View File

@@ -45,10 +45,9 @@
gap: 5px;
}
.closeButton {
background: #DD41434d;
color: #DD4143;
background: #dd41434d;
color: #dd4143;
width: 62px !important;
height: 20px !important;
border: none;
@@ -503,11 +502,6 @@
color: #fff !important;
}
.searchButtonContainer {
right: 20px;
border-radius: 50%;
}
.searchButton {
width: 32px !important;
height: 32px !important;
@@ -575,19 +569,24 @@
color: #666;
}
.searchButtonContainer {
right: 20px;
border-radius: 50%;
}
/* RTL support for back button */
.backButtonContainer[dir="rtl"] {
:global(.ant-app-rtl) .backButtonContainer {
left: auto;
right: 20px;
}
/* RTL support for search button */
.searchButtonContainer[dir="rtl"] {
right: auto;
:global(.ant-app-rtl) .searchButtonContainer {
left: 20px;
right: auto;
}
:global(.rtl) .dineInIcon {
:global(.ant-app-rtl) .dineInIcon {
margin-bottom: 1px;
}

View File

@@ -165,7 +165,7 @@ function MenuPage() {
</div>
<div className={`${styles.pageContainer}`}>
<Space direction="vertical" style={{ width: "100%" }}>
<Space orientation="vertical" style={{ width: "100%" }}>
<div>
{restaurant?.loyalty_stamps &&
restaurant?.is_loyalty_enabled && <LoyaltyCard />}

View File

@@ -0,0 +1,68 @@
import { ProBottomSheet } from "components/ProBottomSheet/ProBottomSheet.tsx";
import ProductDetailPage from "../page";
import { Button } from "antd";
import { UploadOutlined } from "@ant-design/icons";
import { useAppSelector } from "redux/hooks";
import { useNavigate, useParams } from "react-router-dom";
interface ProductBottomSheetProps {
isOpen: boolean;
onClose: () => void;
}
export function ProductBottomSheet({
isOpen,
onClose,
}: ProductBottomSheetProps) {
const { themeName } = useAppSelector((state) => state.theme);
const { isRTL } = useAppSelector((state) => state.locale);
const { subdomain } = useParams();
const closeButtonStyle: React.CSSProperties = {
color: themeName === "dark" ? "#ffffff" : "#4D5154",
display: "flex",
alignItems: "center",
justifyContent: "center",
padding: 8,
borderRadius: "50%",
backgroundColor: themeName === "dark" ? "rgba(54, 54, 54, 0.8)" : "#EDEEEE",
border: `1px solid ${themeName === "dark" ? "#424242" : "transparent"}`,
transition: "all 0.3s ease",
cursor: "pointer",
width: 30,
height: 30,
position: "absolute",
top: 40,
[isRTL ? "right" : "left"]: 16,
zIndex: 1000,
};
const navigate = useNavigate();
const productId = localStorage.getItem("productId");
const redirectToProductPage = () => {
if (productId) {
navigate(`/${subdomain}/product/${productId}`);
onClose();
}
};
return (
<ProBottomSheet
isOpen={isOpen}
onClose={onClose}
showCloseButton={true}
initialSnap={1}
height={"90vh"}
snapPoints={["90vh"]}
contentStyle={{ padding: 0 }}
>
<Button
type="text"
icon={<UploadOutlined />}
onClick={redirectToProductPage}
style={closeButtonStyle}
/>
<ProductDetailPage onClose={onClose} />
</ProBottomSheet>
);
}

View File

@@ -55,6 +55,9 @@ export default function ProductFooter({
product.extras.length,
product?.theExtrasGroups?.length,
]);
const isBottomSheetView = useMemo(() => {
return window.location.href.includes("menu");
}, []);
const handleAddToCart = () => {
if (restaurant && !restaurant.isOpened) {
@@ -114,7 +117,7 @@ export default function ProductFooter({
}),
);
// Navigate back to menu - scroll position will be restored automatically
if (!isDesktop) window.history.back();
if (!isDesktop && !isBottomSheetView ) window.history.back();
else {
onClose?.();
}

View File

@@ -25,7 +25,6 @@ export default function ProductDetailPage({
onClose?: () => void;
}) {
const { productId } = useParams();
const { isRTL } = useAppSelector((state) => state.locale);
const { restaurant } = useAppSelector((state) => state.order);
const { isDesktop, isTablet } = useBreakPoint();
@@ -33,6 +32,14 @@ export default function ProductDetailPage({
const [viewportHeight, setViewportHeight] = useState<number>(
typeof window !== "undefined" ? window.innerHeight : 0,
);
const isBottomSheetView = useMemo(() => {
return window.location.href.includes("menu");
}, []);
const currenctProductId = useMemo(() => {
// in case the prduct viewing from the bottom sheet, the product id is not passed in the url
return productId || localStorage.getItem("productId");
}, [localStorage.getItem("productId")]);
// Update viewport height when browser UI changes (mobile address bar, etc.)
useEffect(() => {
@@ -55,7 +62,10 @@ export default function ProductDetailPage({
window.removeEventListener("resize", updateViewportHeight);
window.removeEventListener("orientationchange", updateViewportHeight);
if (window.visualViewport) {
window.visualViewport.removeEventListener("resize", updateViewportHeight);
window.visualViewport.removeEventListener(
"resize",
updateViewportHeight,
);
}
};
}, []);
@@ -71,14 +81,15 @@ export default function ProductDetailPage({
// 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))
if (!menuData?.products || (!currenctProductId && !productIdLocalStorage))
return null;
// Find the product with matching IDs
return menuData.products.find(
(p: Product) => p.id.toString() === (productId || productIdLocalStorage),
(p: Product) =>
p.id.toString() === (currenctProductId || productIdLocalStorage),
);
}, [menuData, productId]);
}, [menuData, currenctProductId]);
// State for variant selections
const [selectedVariants, setSelectedVariants] = useState<
@@ -205,14 +216,15 @@ export default function ProductDetailPage({
return (
<div
style={{
height: viewportHeight > 0
? `${viewportHeight - 195}px`
: "calc(100dvh - 195px)",
height:
viewportHeight > 0
? `${viewportHeight - 195}px`
: "calc(100dvh - 195px)",
overflow: "auto",
scrollbarWidth: "none",
}}
>
{!isDesktop && (
{!isDesktop && !isBottomSheetView && (
<div className={styles.backButtonContainer}>
<BackButton />
</div>
@@ -371,6 +383,7 @@ export default function ProductDetailPage({
selectedGroups={selectedExtrasByGroup}
quantity={quantity}
setQuantity={(quantity: number) => setQuantity(quantity)}
onClose={onClose}
/>
)}
</div>

View File

@@ -315,6 +315,11 @@
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
:global(.ant-app-rtl) .backButtonContainer {
left: auto;
right: 20px;
}
:global(.darkApp) .itemDescriptionIcons path {
fill: #ffffff !important;
}