Compare commits
54 Commits
ebe9928091
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| e86966062a | |||
| 3fe0c68526 | |||
| 5cb681c4a8 | |||
| f97b83062c | |||
| 69425580d6 | |||
| 8083e9ec96 | |||
| 4dfa08d26c | |||
| 788b05d6f4 | |||
| 0ce2d320a8 | |||
| 046773cb8b | |||
| e50d2dfd4c | |||
| ab5867b0cb | |||
| 68a12a4796 | |||
| 5d08498f8c | |||
| d0de05cfb0 | |||
| 8a4e6691d4 | |||
| adc98200cb | |||
| 26a16fa8d7 | |||
| 093d4279d9 | |||
| 2c676eb153 | |||
| 6f1f4441f1 | |||
| 7b6fe140ad | |||
| 44e2730428 | |||
| aaef1bc11b | |||
| ba350e877f | |||
| a68480c075 | |||
| ce68b8b978 | |||
| 3a72b8e933 | |||
| 9d061ae8a6 | |||
| a232fdabe2 | |||
| b71330745e | |||
| 4fb03a287e | |||
| b84d0646af | |||
| 179bca3e9f | |||
| e87c3e0783 | |||
| bd556a5ef7 | |||
| 6bf4cb6399 | |||
| a0e558d416 | |||
| 4ed288099c | |||
| 41605781f4 | |||
| a337ee11e2 | |||
| e95411460f | |||
| 5c7e64b17c | |||
| 2c7a8d369a | |||
| eb6ca34162 | |||
| 1d9ae7190e | |||
| a06147dfa4 | |||
| 69bc9d4dee | |||
| 2b0516ad0d | |||
| eed10e8faf | |||
| f988fd3a4e | |||
| 167b26e0b9 | |||
| b0288ebcf6 | |||
| 6271c14eff |
BIN
public/thawani.png
Normal file
BIN
public/thawani.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.2 KiB |
@@ -76,7 +76,8 @@
|
||||
"room": "الغرفة",
|
||||
"office": "المكتب",
|
||||
"booking": "الحجز",
|
||||
"scheduledOrder": "الطلب المجدول"
|
||||
"scheduledOrder": "الطلب المجدول",
|
||||
"rewardsAndLoyalty": "المكافأة واللايفور"
|
||||
},
|
||||
"home": {
|
||||
"title": "العنوان",
|
||||
@@ -91,7 +92,7 @@
|
||||
"scheduledOrder": "طلب مجدول"
|
||||
},
|
||||
"promotion": {
|
||||
"title": "الترويجات",
|
||||
"title": "عرض التفاصيل",
|
||||
"description": "احصل على خصم 10% على طلبك الأول"
|
||||
}
|
||||
},
|
||||
@@ -129,7 +130,7 @@
|
||||
"payDescription": "الدفع",
|
||||
"rating": "التقييم ",
|
||||
"loyaltyPoints": "نقاط الولاء",
|
||||
"loyaltyDescription": "اشترى {{value}} وجبات واحصل على وجبة مجانية",
|
||||
"loyaltyDescription": "اشترى {{value}} وجبات واحصل على وجبة مجانية!",
|
||||
"youMightAlsoLike": "قد تعجبك أيضاً..",
|
||||
"choose1": "اختر 1",
|
||||
"specialRequest": "طلب خاص",
|
||||
@@ -164,7 +165,22 @@
|
||||
"restaurantIsClosed": "المطعم مغلق",
|
||||
"address": "العنوان",
|
||||
"openingTimes": "ساعات العمل",
|
||||
"customizable": "قابل للتخصيص"
|
||||
"customizable": "قابل للتخصيص",
|
||||
"youHaveXEarnedRewardsReadyToRedeem": "🎉 لديك {{rewards}} مكافأة مستحقة للاستخدام!",
|
||||
"justXMorePurchasesToUnlockYourFREEItem": "فقط {{cups}} أكثر للفتح الوجبة المجانية!",
|
||||
"youreJustXCupsAwayFromYourNextReward": "🎉 أنت فقط {{cups}} أكثر للحصول على المكافأة التالية!",
|
||||
"callWaiter": "اتصل بالرادير",
|
||||
"balance": "الرصيد",
|
||||
"closed": "مغلق",
|
||||
"sunday": "الأحد",
|
||||
"monday": "الإثنين",
|
||||
"tuesday": "الثلاثاء",
|
||||
"wednesday": "الأربعاء",
|
||||
"thursday": "الخميس",
|
||||
"friday": "الجمعة",
|
||||
"saturday": "السبت",
|
||||
"selectYourTable": "اختر طاولتك",
|
||||
"calledWaiterSuccess": "تم اتصال الرادير بنجاح"
|
||||
},
|
||||
"cart": {
|
||||
"addSpecialRequestOptional": "إضافة طلب خاص (اختياري)",
|
||||
@@ -192,6 +208,12 @@
|
||||
"cancel": "إلغاء",
|
||||
"success": "تم حذف العنصر من سلة المشتريات"
|
||||
},
|
||||
"clearCartConfirmation": {
|
||||
"title": "مسح السلة",
|
||||
"content": "هل أنت متأكد أنك تريد مسح السلة؟",
|
||||
"confirm": "مسح",
|
||||
"cancel": "إلغاء"
|
||||
},
|
||||
"quantity": "الكمية",
|
||||
"price": "السعر",
|
||||
"perItem": "للقطعة",
|
||||
@@ -265,7 +287,10 @@
|
||||
"am": "ص",
|
||||
"pm": "م",
|
||||
"cannotSelectPastDate": "لا يمكنك اختيار تاريخ سابق. يرجى اختيار اليوم أو تاريخ مستقبلي.",
|
||||
"checkRequiredFields": "يرجى التحقق من الحقول المطلوبة"
|
||||
"checkRequiredFields": "يرجى التحقق من الحقول المطلوبة",
|
||||
"loyalty": "ولاء",
|
||||
"vat": "ضريبة القيمة المضافة ({{value}}%))",
|
||||
"otherTaxes": "{{name}} ({{value}}%)"
|
||||
},
|
||||
"checkout": {
|
||||
"addCarDetails": "إضافة تفاصيل السيارة",
|
||||
@@ -309,7 +334,10 @@
|
||||
"today": "اليوم",
|
||||
"change": "تغيير",
|
||||
"pickup": "استلام",
|
||||
"setPickupTime":"تحديد وقت الاستلام"
|
||||
"setPickupTime": "تحديد وقت الاستلام",
|
||||
"carPlateNumber": "رقم لوحة السيارة",
|
||||
"noItems": "لا يوجد عناصر في السلة",
|
||||
"thawani": "ثواني"
|
||||
},
|
||||
"address": {
|
||||
"title": "العنوان",
|
||||
@@ -361,7 +389,9 @@
|
||||
"gotIt": "فهمت",
|
||||
"howItWorksDescription": "يمكنك إرسال هدية إلى أي شخص عبر التطبيق. يمكنك إرسال هدية إلى أي شخص عبر التطبيق. يمكنك إرسال هدية إلى أي شخص عبر التطبيق. يمكنك إرسال هدية إلى أي شخص عبر التطبيق. يمكنك إرسال هدية إلى أي شخص عبر التطبيق.",
|
||||
"senderEmail": "البريد الإلكتروني المرسل",
|
||||
"save": "حفظ"
|
||||
"save": "حفظ",
|
||||
"pleaseEnterRoomNumber": "يرجى إدخال رقم الغرفة",
|
||||
"pleaseEnterOfficeNumber": "يرجى إدخال رقم المكتب"
|
||||
},
|
||||
"login": {
|
||||
"singup/Login": "الدخول / التسجيل",
|
||||
@@ -526,5 +556,70 @@
|
||||
"color": "اللون",
|
||||
"category": "الفئة",
|
||||
"plateNumber": "رقم السيارة"
|
||||
},
|
||||
"redeem": {
|
||||
"title": "استخدم الهدية",
|
||||
"redeem": "استخدم",
|
||||
"redeemDescription": "استخدم بطاقة الهدية",
|
||||
"redeemButton": "استخدم",
|
||||
"redeemButtonDescription": "استخدم بطاقة الهدية",
|
||||
"showThisCodeAtTheRestaurant": "اعرض هذا الرمز في المطعم",
|
||||
"addGiftDetails": "اضف تفاصيل الهدية",
|
||||
"description": "اضف تفاصيل الهدية بالرسالة، ووقت التوصيل، وتفاصيل المستلم.",
|
||||
"checkout": "الدفع",
|
||||
"yourName": "اسمك",
|
||||
"yourPhone": "رقم هاتفك",
|
||||
"keepMyNameSecret": "الاحتفاظ باسمي مخفياً",
|
||||
"receiverInformation": "تفاصيل المستلم",
|
||||
"costumeAmount": "مبلغ البطاقة",
|
||||
"enterCustomOucherAmount": "أدخل مبلغ البطاقة المخصص",
|
||||
"amount": "المبلغ",
|
||||
"eCardAmount": "مبلغ البطاقة الإلكترونية",
|
||||
"receiverName": "اسم المستلم",
|
||||
"edit": "تعديل",
|
||||
"yourInformation": "تفاصيلك",
|
||||
"minimumAmountShouldBe1OMR": "يجب أن يكون المبلغ الأدنى 1 OMR",
|
||||
"add": "إضافة",
|
||||
"senderNameRequired": "يجب أن يكون اسم المرسل مطلوب",
|
||||
"receiverNameRequired": "يجب أن يكون اسم المستلم مطلوب",
|
||||
"includesFreeItemsInThisOrder": "يشمل العناصر المجانية في هذا الطلب",
|
||||
"redeemGiftedItems": "استخدم الهديات",
|
||||
"pending": "قيد المعالجة",
|
||||
"useThisCodeIfScanningNotPossible": "استخدم هذا الرمز إذا كان من المستحيل المسح",
|
||||
"voucherWillBeAppliedAtCheckout": "سيتم تطبيق القسيمة عند الدفع",
|
||||
"yourGiftCardBalance": "رصيد بطاقة الهدية",
|
||||
"redeemNow": "استخدم الآن",
|
||||
"restaurantLocation": "موقع المطعم",
|
||||
"voucherBalance": "رصيد القسيمة",
|
||||
"getDirections": "الحصول على الاتجاهات",
|
||||
"giftedItems": "العناصر المهدية",
|
||||
"viewAll": "عرض الكل",
|
||||
"voucherCodeCopied": "تم نسخ رمز القسيمة",
|
||||
"copyFailed": "فشل نسخ رمز القسيمة"
|
||||
},
|
||||
"rewardsAndLoyalty": {
|
||||
"title": "المكافأة واللايفور",
|
||||
"description": "المكافأة واللايفور",
|
||||
"rewardsAndLoyalty": "المكافأة واللايفور",
|
||||
"rewardsAndLoyaltyDescription": "المكافأة واللايفور",
|
||||
"rewardsAndLoyaltyButton": "المكافأة واللايفور",
|
||||
"rewardsAndLoyaltyButtonDescription": "المكافأة واللايفور",
|
||||
"completedPurchases": "المشتريات المكتملة",
|
||||
"totalPurchased": "المشتريات الكلية",
|
||||
"saved": "المحفوظ",
|
||||
"almosthere": "قريب جدا!",
|
||||
"youreJustXCupsAwayFromYourNextReward": "أنت فقط {{cups}} أكواب بعيد عن المكافأة التالية!",
|
||||
"youCanRedeemDuringTheCheckout": "يمكنك استخدام المكافأة أثناء الدفع",
|
||||
"youCurrentlyHave": "لديك",
|
||||
"freeItems": "عناصر مجانية",
|
||||
"redeemNow": "استخدم الآن",
|
||||
"yourAvailableRewards": "المكافأات المتاحة",
|
||||
"loyaltyHistory": "سجل الولاء",
|
||||
"loyaltyHistoryDescription": "سجل الولاء",
|
||||
"earnedPoints": "حصل على {{points}} نقطة",
|
||||
"order": "الطلب",
|
||||
"yourOrderFrom": "طلبك من {{restaurantName}}",
|
||||
"earned": "حصل على",
|
||||
"xPoints": "{{points}} نقطة"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,7 +76,8 @@
|
||||
"noMenuItemsAvailable": "No menu items available",
|
||||
"restaurantCover": "Restaurant Cover",
|
||||
"restaurantLogo": "Restaurant Logo",
|
||||
"scheduledOrder": "Scheduled Order"
|
||||
"scheduledOrder": "Scheduled Order",
|
||||
"rewardsAndLoyalty": "Rewards And Loyalty"
|
||||
},
|
||||
"home": {
|
||||
"title": "title",
|
||||
@@ -107,7 +108,7 @@
|
||||
"scheduledOrder": "Scheduled Order"
|
||||
},
|
||||
"promotion": {
|
||||
"title": "Promotions",
|
||||
"title": "Show details",
|
||||
"description": "Get 10% off your first order"
|
||||
}
|
||||
},
|
||||
@@ -143,7 +144,7 @@
|
||||
"close": "Close",
|
||||
"rating": "Rating ",
|
||||
"loyaltyPoints": "Loyalty Points",
|
||||
"loyaltyDescription": "Buy {{value}} meals and get 1 FREE",
|
||||
"loyaltyDescription": "Buy {{value}} meals and get 1 FREE!",
|
||||
"choose1": "Choose 1",
|
||||
"youMightAlsoLike": "You might also like..",
|
||||
"specialRequest": "Special Request",
|
||||
@@ -176,9 +177,29 @@
|
||||
"payDescription": "Pay for your order",
|
||||
"address": "Address",
|
||||
"openingTimes": "Opening Times",
|
||||
"customizable": "Customizable"
|
||||
"customizable": "Customizable",
|
||||
"youHaveXEarnedRewardsReadyToRedeem": "🎉 You have {{rewards}} rewards ready to redeem!",
|
||||
"justXMorePurchasesToUnlockYourFREEItem": "Just {{cups}} more purchases to unlock your FREE item!",
|
||||
"youreJustXCupsAwayFromYourNextReward": "🎉 You're just {{cups}} stamps away from your next reward!",
|
||||
"callWaiter": "Call Waiter",
|
||||
"balance": "Balance",
|
||||
"closed": "Closed",
|
||||
"sunday": "Sunday",
|
||||
"monday": "Monday",
|
||||
"tuesday": "Tuesday",
|
||||
"wednesday": "Wednesday",
|
||||
"thursday": "Thursday",
|
||||
"friday": "Friday",
|
||||
"saturday": "Saturday",
|
||||
"selectYourTable": "Select your table",
|
||||
"calledWaiterSuccess": "Waiter has been called successfully"
|
||||
},
|
||||
"cart": {
|
||||
"remainingToPay": "Remaining to Pay",
|
||||
"remainingVoucherAmount": "Remaining Voucher Amount",
|
||||
"voucherApplied": "Voucher Applied",
|
||||
"giftedItems": "Gifted Items",
|
||||
"voucherBalance": "Voucher Balance",
|
||||
"addSpecialRequestOptional": "Add Special Request (Optional)",
|
||||
"continueToGiftDetails": "Continue to Gift Details",
|
||||
"continueToGiftDetailsDescription": "Please fill in the details of the gift recipient and sender to continue.",
|
||||
@@ -204,6 +225,12 @@
|
||||
"cancel": "Cancel",
|
||||
"success": ",Item removed from cart"
|
||||
},
|
||||
"clearCartConfirmation": {
|
||||
"title": "Clear Cart",
|
||||
"content": "Are you sure you want to clear the cart?",
|
||||
"confirm": "Clear",
|
||||
"cancel": "Cancel"
|
||||
},
|
||||
"quantity": "Quantity",
|
||||
"price": "Price",
|
||||
"perItem": "Per Item",
|
||||
@@ -276,7 +303,11 @@
|
||||
"am": "AM",
|
||||
"pm": "PM",
|
||||
"cannotSelectPastDate": "You cannot select a past date. Please select today or a future date.",
|
||||
"checkRequiredFields": "Please check required fields"
|
||||
"checkRequiredFields": "Please check required fields",
|
||||
"applyYourAvailableRewardsToGetDiscountsOnItemsInYourCart": "Apply your available rewards to get discounts on items in your cart",
|
||||
"loyalty": "Loyalty",
|
||||
"vat": "Vat ({{value}}%)",
|
||||
"otherTaxes": "{{name}} ({{value}}%)"
|
||||
},
|
||||
"checkout": {
|
||||
"addCarDetails": "Add Car Details",
|
||||
@@ -322,7 +353,10 @@
|
||||
"today": "Today",
|
||||
"change": "Change",
|
||||
"pickup": "Pickup",
|
||||
"setPickupTime": "Set Pickup Time"
|
||||
"setPickupTime": "Set Pickup Time",
|
||||
"carPlateNumber": "Car Plate Number",
|
||||
"noItems": "No items in cart",
|
||||
"thawani": "Thawani"
|
||||
},
|
||||
"address": {
|
||||
"title": "Address",
|
||||
@@ -375,7 +409,9 @@
|
||||
"gotIt": "Got It",
|
||||
"howItWorksDescription": "The gifted amount will be credited directly to your friend's wallet in the app. The recipient can use the amount to book a session of their choice within the app. The gifted amount is non-refundable and can only be used for booking sessions.",
|
||||
"senderEmail": "Sender Email",
|
||||
"save": "Save"
|
||||
"save": "Save",
|
||||
"pleaseEnterRoomNumber": "Please enter room number",
|
||||
"pleaseEnterOfficeNumber": "Please enter office number"
|
||||
},
|
||||
"login": {
|
||||
"singup/Login": "Sing up / Login",
|
||||
@@ -539,5 +575,72 @@
|
||||
"color": "Color",
|
||||
"category": "Category",
|
||||
"plateNumber": "Plate Number"
|
||||
},
|
||||
"redeem": {
|
||||
"title": "Redeem Gift",
|
||||
"redeem": "Redeem",
|
||||
"redeemDescription": "Redeem your gift card",
|
||||
"redeemButton": "Redeem",
|
||||
"redeemButtonDescription": "Redeem your gift card",
|
||||
"showThisCodeAtTheRestaurant": "Show this code at the restaurant",
|
||||
"addGiftDetails": "Add Gift Details",
|
||||
"description": "Personalize your gift with a message, delivery timing, and recipient details.",
|
||||
"checkout": "Checkout",
|
||||
"yourName": "Your Name",
|
||||
"yourPhone": "Your Phone",
|
||||
"keepMyNameSecret": "Keep my name secret",
|
||||
"receiverInformation": "Receiver Information",
|
||||
"costumeAmount": "Costume amount",
|
||||
"enterCustomOucherAmount": "Enter custom oucher amount",
|
||||
"amount": "Amount",
|
||||
"eCardAmount": "E-Card Amount",
|
||||
"receiverName": "Receiver Name",
|
||||
"edit": "Edit",
|
||||
"yourInformation": "Your Information",
|
||||
"minimumAmountShouldBe1OMR": "Minimum amount should be 1 OMR",
|
||||
"add": "Add",
|
||||
"senderNameRequired": "Sender name is required",
|
||||
"receiverNameRequired": "Receiver name is required",
|
||||
"includesFreeItemsInThisOrder": "Includes free items in this order",
|
||||
"redeemGiftedItems": "Redeem gifted items",
|
||||
"pending": "Pending",
|
||||
"useThisCodeIfScanningNotPossible": "Use this code if scanning is not possible",
|
||||
"voucherWillBeAppliedAtCheckout": "Voucher will be applied at checkout",
|
||||
"yourGiftCardBalance": "Your gift card balance",
|
||||
"redeemNow": "Redeem Now",
|
||||
"restaurantLocation": "Restaurant Location",
|
||||
"voucherBalance": "Voucher Balance",
|
||||
"getDirections": "Get Directions",
|
||||
"giftedItems": "Gifted Items",
|
||||
"viewAll": "View All",
|
||||
"voucherCodeCopied": "Voucher code copied!",
|
||||
"copyFailed": "Failed to copy voucher code",
|
||||
"hiX": "Hi {{name}}!",
|
||||
"youHaveReceivedAGiftCarFromX": "You have received a gift car from {{name}}!"
|
||||
},
|
||||
"rewardsAndLoyalty": {
|
||||
"title": "Rewards And Loyalty",
|
||||
"description": "Rewards And Loyalty",
|
||||
"rewardsAndLoyalty": "Rewards And Loyalty",
|
||||
"rewardsAndLoyaltyDescription": "Rewards And Loyalty",
|
||||
"rewardsAndLoyaltyButton": "Rewards And Loyalty",
|
||||
"rewardsAndLoyaltyButtonDescription": "Rewards And Loyalty",
|
||||
"completedPurchases": "Completed Purchases",
|
||||
"totalPurchased": "Total Purchased",
|
||||
"saved": "Saved",
|
||||
"almosthere": "Almost here!",
|
||||
"youreJustXCupsAwayFromYourNextReward": "You're just {{cups}} cups away from your next reward!",
|
||||
"youCanRedeemDuringTheCheckout": "You can redeem during the checkout",
|
||||
"youCurrentlyHave": "You currently have",
|
||||
"freeItems": "free items",
|
||||
"redeemNow": "Redeem Now",
|
||||
"yourAvailableRewards": "Your available rewards",
|
||||
"loyaltyHistory": "Loyalty History",
|
||||
"loyaltyHistoryDescription": "Loyalty History",
|
||||
"earnedPoints": "Earned {{points}} points",
|
||||
"order": "Order",
|
||||
"yourOrderFrom": "Your order from {{restaurantName}}",
|
||||
"earned": "Earned",
|
||||
"xPoints": "{{points}} points"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React from "react";
|
||||
import { useAppSelector } from "redux/hooks";
|
||||
import ProText from "../ProText";
|
||||
import { formatPriceUi } from "utils/helpers";
|
||||
|
||||
interface ArabicPriceProps {
|
||||
price: number | string;
|
||||
@@ -25,7 +26,8 @@ const ArabicPrice: React.FC<ArabicPriceProps> = ({
|
||||
const { restaurant } = useAppSelector((state) => state.order);
|
||||
|
||||
// Format the price to ensure it has 2 decimal places
|
||||
const formattedPrice = typeof price === "number" ? price.toFixed(2) : price;
|
||||
const formattedPrice =
|
||||
typeof price === "number" ? formatPriceUi(price, restaurant?.currency_decimals ?? 3) : price;
|
||||
const { textDecoration, ...restStyle } = style;
|
||||
const decorationStyle = textDecoration
|
||||
? ({ textDecoration } as React.CSSProperties)
|
||||
|
||||
@@ -6,7 +6,7 @@ import CancelPopupIcon from "components/Icons/CancelPopupIcon";
|
||||
import NextIcon from "components/Icons/NextIcon";
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useParams } from "react-router-dom";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import { useCancelOrderMutation } from "redux/api/others";
|
||||
import { useAppSelector } from "redux/hooks";
|
||||
import { ProBottomSheet } from "../ProBottomSheet/ProBottomSheet";
|
||||
@@ -18,7 +18,8 @@ export function CancelOrderBottomSheet() {
|
||||
const { t } = useTranslation();
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const { isRTL } = useAppSelector((state) => state.locale);
|
||||
const { orderId } = useParams();
|
||||
const { orderId, subdomain } = useParams();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [cancelOrder] = useCancelOrderMutation();
|
||||
|
||||
@@ -31,6 +32,7 @@ export function CancelOrderBottomSheet() {
|
||||
message.error(res.error.data.message);
|
||||
} else {
|
||||
message.success(res.data.message);
|
||||
navigate(`/${subdomain}`);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -6,6 +6,7 @@ import GiftItemsBtnIcon from "components/Icons/GiftItemsBtnIcon";
|
||||
import GiftBalanceBtnIcon from "components/Icons/GiftBalanceBtnIcon";
|
||||
import GiftItemsAndBalanceBtnIcon from "components/Icons/GiftItemsAndBalanceBtnIcon";
|
||||
import ProText from "components/ProText";
|
||||
import { useParams, useNavigate } from "react-router-dom";
|
||||
|
||||
interface GiftTypeBottomSheetProps {
|
||||
isOpen: boolean;
|
||||
@@ -13,6 +14,12 @@ interface GiftTypeBottomSheetProps {
|
||||
onSave?: () => void;
|
||||
}
|
||||
|
||||
export enum GiftType {
|
||||
Items = "items",
|
||||
Vouchers = "vouchers",
|
||||
ItemsAndVouchers = "itemsAndVouchers",
|
||||
}
|
||||
|
||||
export function GiftTypeBottomSheet({
|
||||
isOpen,
|
||||
onClose,
|
||||
@@ -20,11 +27,15 @@ export function GiftTypeBottomSheet({
|
||||
}: GiftTypeBottomSheetProps) {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const handleSave = (giftType: string) => {
|
||||
dispatch(updateGiftDetails({ giftType: giftType }));
|
||||
const { subdomain } = useParams();
|
||||
const navigate = useNavigate();
|
||||
const handleSave = (giftType: GiftType) => {
|
||||
dispatch(updateGiftDetails({ giftType }));
|
||||
onClose();
|
||||
onSave?.();
|
||||
if (giftType !== GiftType.Vouchers)
|
||||
navigate(`/${subdomain}/menu?orderType=gift`);
|
||||
else navigate(`/${subdomain}/e-gift-cards`);
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
@@ -40,6 +51,7 @@ export function GiftTypeBottomSheet({
|
||||
textAlign: "center",
|
||||
color: "#86858E",
|
||||
};
|
||||
|
||||
return (
|
||||
<ProBottomSheet
|
||||
isOpen={isOpen}
|
||||
@@ -59,19 +71,22 @@ export function GiftTypeBottomSheet({
|
||||
padding: 24,
|
||||
}}
|
||||
>
|
||||
<div style={{ width: 60 }} onClick={() => handleSave("items")}>
|
||||
<div style={{ width: 60 }} onClick={() => handleSave(GiftType.Items)}>
|
||||
<GiftItemsBtnIcon />
|
||||
<br />
|
||||
<ProText style={textStyle}>{t("gift.items")}</ProText>
|
||||
</div>
|
||||
<div style={{ width: 60 }} onClick={() => handleSave("vouchers")}>
|
||||
<div
|
||||
style={{ width: 60 }}
|
||||
onClick={() => handleSave(GiftType.Vouchers)}
|
||||
>
|
||||
<GiftBalanceBtnIcon />
|
||||
<br />
|
||||
<ProText style={textStyle}>{t("gift.balance")}</ProText>
|
||||
</div>
|
||||
<div
|
||||
style={{ width: 60 }}
|
||||
onClick={() => handleSave("itemsAndVouchers")}
|
||||
onClick={() => handleSave(GiftType.ItemsAndVouchers)}
|
||||
>
|
||||
<GiftItemsAndBalanceBtnIcon />
|
||||
<br />
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
import { Status, Wrapper } from "@googlemaps/react-wrapper";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
|
||||
@@ -74,7 +73,7 @@ function MapComponent({
|
||||
});
|
||||
|
||||
setMap(newMap);
|
||||
console.log('Map initialized successfully');
|
||||
console.log("Map initialized successfully");
|
||||
|
||||
// If we have an initial location, add a marker
|
||||
if (initialLocation) {
|
||||
@@ -148,16 +147,14 @@ function MapComponent({
|
||||
geocoder.geocode({ location: { lat, lng } }, (results, status) => {
|
||||
if (status === "OK" && results && results[0]) {
|
||||
const address = results[0].formatted_address;
|
||||
console.log(
|
||||
"Initial marker drag - calling onLocationSelect:",
|
||||
{ lat, lng, address },
|
||||
);
|
||||
console.log("Initial marker drag - calling onLocationSelect:", {
|
||||
lat,
|
||||
lng,
|
||||
address,
|
||||
});
|
||||
onLocationSelect?.(lat, lng, address);
|
||||
} else {
|
||||
console.log(
|
||||
"Geocoding failed on initial marker drag:",
|
||||
status,
|
||||
);
|
||||
console.log("Geocoding failed on initial marker drag:", status);
|
||||
// Fallback: use coordinates as address if geocoding fails
|
||||
const fallbackAddress = `Location: ${lat.toFixed(6)}, ${lng.toFixed(6)}`;
|
||||
console.log(
|
||||
@@ -201,19 +198,29 @@ function MapComponent({
|
||||
|
||||
// Get address from coordinates
|
||||
const geocoder = new google.maps.Geocoder();
|
||||
geocoder.geocode({ location: { lat, lng } }, (results, status) => {
|
||||
geocoder.geocode(
|
||||
{ location: { lat, lng } },
|
||||
(results, status) => {
|
||||
if (status === "OK" && results && results[0]) {
|
||||
const address = results[0].formatted_address;
|
||||
console.log('Marker drag - calling onLocationSelect:', { lat, lng, address });
|
||||
console.log("Marker drag - calling onLocationSelect:", {
|
||||
lat,
|
||||
lng,
|
||||
address,
|
||||
});
|
||||
onLocationSelect?.(lat, lng, address);
|
||||
} else {
|
||||
console.log('Geocoding failed on drag:', status);
|
||||
console.log("Geocoding failed on drag:", status);
|
||||
// Fallback: use coordinates as address if geocoding fails
|
||||
const fallbackAddress = `Location: ${lat.toFixed(6)}, ${lng.toFixed(6)}`;
|
||||
console.log('Using fallback address for drag:', fallbackAddress);
|
||||
console.log(
|
||||
"Using fallback address for drag:",
|
||||
fallbackAddress,
|
||||
);
|
||||
onLocationSelect?.(lat, lng, fallbackAddress);
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -222,13 +229,17 @@ function MapComponent({
|
||||
geocoder.geocode({ location: { lat, lng } }, (results, status) => {
|
||||
if (status === "OK" && results && results[0]) {
|
||||
const address = results[0].formatted_address;
|
||||
console.log('Map click - calling onLocationSelect:', { lat, lng, address });
|
||||
console.log("Map click - calling onLocationSelect:", {
|
||||
lat,
|
||||
lng,
|
||||
address,
|
||||
});
|
||||
onLocationSelect?.(lat, lng, address);
|
||||
} else {
|
||||
console.log('Geocoding failed on click:', status);
|
||||
console.log("Geocoding failed on click:", status);
|
||||
// Fallback: use coordinates as address if geocoding fails
|
||||
const fallbackAddress = `Location: ${lat.toFixed(6)}, ${lng.toFixed(6)}`;
|
||||
console.log('Using fallback address:', fallbackAddress);
|
||||
console.log("Using fallback address:", fallbackAddress);
|
||||
onLocationSelect?.(lat, lng, fallbackAddress);
|
||||
}
|
||||
});
|
||||
@@ -238,6 +249,57 @@ function MapComponent({
|
||||
}
|
||||
}, [map, onLocationSelect, readOnly, initialLocation]);
|
||||
|
||||
// Update map center and marker when initialLocation changes (after map is initialized)
|
||||
useEffect(() => {
|
||||
if (map && initialLocation) {
|
||||
// Update map center
|
||||
map.setCenter({
|
||||
lat: initialLocation.lat,
|
||||
lng: initialLocation.lng,
|
||||
});
|
||||
map.setZoom(15);
|
||||
|
||||
// Update or create marker
|
||||
if (markerRef.current) {
|
||||
// Update existing marker position
|
||||
markerRef.current.setPosition({
|
||||
lat: initialLocation.lat,
|
||||
lng: initialLocation.lng,
|
||||
});
|
||||
} else {
|
||||
// Create new marker if it doesn't exist
|
||||
const newMarker = new google.maps.Marker({
|
||||
position: { lat: initialLocation.lat, lng: initialLocation.lng },
|
||||
map: map,
|
||||
draggable: !readOnly,
|
||||
});
|
||||
markerRef.current = newMarker;
|
||||
|
||||
// Add drag end listener if not readOnly
|
||||
if (!readOnly && onLocationSelect) {
|
||||
newMarker.addListener("dragend", () => {
|
||||
const position = newMarker.getPosition();
|
||||
if (position) {
|
||||
const lat = position.lat();
|
||||
const lng = position.lng();
|
||||
|
||||
const geocoder = new google.maps.Geocoder();
|
||||
geocoder.geocode({ location: { lat, lng } }, (results, status) => {
|
||||
if (status === "OK" && results && results[0]) {
|
||||
const address = results[0].formatted_address;
|
||||
onLocationSelect(lat, lng, address);
|
||||
} else {
|
||||
const fallbackAddress = `Location: ${lat.toFixed(6)}, ${lng.toFixed(6)}`;
|
||||
onLocationSelect(lat, lng, fallbackAddress);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [map, initialLocation, readOnly, onLocationSelect]);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={mapRef}
|
||||
|
||||
@@ -1,22 +1,58 @@
|
||||
import { Button } from "antd";
|
||||
import { Card } from "antd";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { ProBottomSheet } from "../ProBottomSheet/ProBottomSheet";
|
||||
import ProText from "components/ProText";
|
||||
import ProTitle from "components/ProTitle";
|
||||
import { useAppSelector } from "redux/hooks";
|
||||
import { useGetOpeningTimesQuery } from "redux/api/others";
|
||||
import { useMemo } from "react";
|
||||
import TimeIcon from "components/Icons/order/TimeIcon";
|
||||
|
||||
interface OpeningTimesBottomSheetProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
const textStyle: React.CSSProperties = {
|
||||
fontWeight: 400,
|
||||
fontStyle: "Regular",
|
||||
fontSize: 14,
|
||||
lineHeight: "140%",
|
||||
letterSpacing: "0%",
|
||||
marginBottom: 4,
|
||||
// Helper function to format time (HH:mm to 12h format)
|
||||
const formatTime = (time: string | null | undefined): string => {
|
||||
if (!time) return "";
|
||||
|
||||
// If already in 12h format (contains AM/PM), return as is
|
||||
if (time.includes("AM") || time.includes("PM")) {
|
||||
return time;
|
||||
}
|
||||
|
||||
// Parse 24h format (HH:mm)
|
||||
const [hours, minutes] = time.split(":");
|
||||
const hour24 = parseInt(hours, 10);
|
||||
|
||||
if (isNaN(hour24)) return time;
|
||||
|
||||
const hour12 = hour24 % 12 || 12;
|
||||
const ampm = hour24 >= 12 ? "PM" : "AM";
|
||||
return `${hour12}:${minutes} ${ampm}`;
|
||||
};
|
||||
|
||||
// Helper function to get time ranges for a day
|
||||
const getDayTimes = (
|
||||
openingTimes: any,
|
||||
dayIndex: number,
|
||||
): { shift1: string | null; shift2: string | null } => {
|
||||
if (!openingTimes) {
|
||||
return { shift1: null, shift2: null };
|
||||
}
|
||||
|
||||
const from1 = openingTimes[`${dayIndex}_from` as keyof typeof openingTimes];
|
||||
const to1 = openingTimes[`${dayIndex}_to` as keyof typeof openingTimes];
|
||||
const from2 = openingTimes[`2_${dayIndex}_from` as keyof typeof openingTimes];
|
||||
const to2 = openingTimes[`2_${dayIndex}_to` as keyof typeof openingTimes];
|
||||
|
||||
const shift1 =
|
||||
from1 && to1 ? `${formatTime(from1)} - ${formatTime(to1)}` : null;
|
||||
const shift2 =
|
||||
from2 && to2 ? `${formatTime(from2)} - ${formatTime(to2)}` : null;
|
||||
|
||||
return { shift1, shift2 };
|
||||
};
|
||||
|
||||
export function OpeningTimesBottomSheet({
|
||||
@@ -26,6 +62,12 @@ export function OpeningTimesBottomSheet({
|
||||
const { t } = useTranslation();
|
||||
const { isRTL } = useAppSelector((state) => state.locale);
|
||||
const { restaurant } = useAppSelector((state) => state.order);
|
||||
const { data: openingTimes } = useGetOpeningTimesQuery(
|
||||
restaurant?.restautantId,
|
||||
{
|
||||
skip: !restaurant?.restautantId,
|
||||
},
|
||||
);
|
||||
|
||||
const days = [
|
||||
"sunday",
|
||||
@@ -37,7 +79,11 @@ export function OpeningTimesBottomSheet({
|
||||
"saturday",
|
||||
];
|
||||
const todayIndex = new Date().getDay();
|
||||
const todayDay = days[todayIndex];
|
||||
|
||||
// Memoize day times to avoid recalculating on every render
|
||||
const dayTimes = useMemo(() => {
|
||||
return days.map((_, index) => getDayTimes(openingTimes, index));
|
||||
}, [openingTimes]);
|
||||
|
||||
return (
|
||||
<ProBottomSheet
|
||||
@@ -46,170 +92,205 @@ export function OpeningTimesBottomSheet({
|
||||
title={t("menu.openingTimes")}
|
||||
showCloseButton={false}
|
||||
initialSnap={1}
|
||||
height={445}
|
||||
snapPoints={[445]}
|
||||
height={600}
|
||||
snapPoints={[600]}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
padding: 20,
|
||||
padding: "20px 0",
|
||||
gap: 20,
|
||||
maxHeight: "calc(600px - 120px)",
|
||||
overflowY: "auto",
|
||||
}}
|
||||
>
|
||||
{/* Address Section */}
|
||||
<Card
|
||||
bordered={false}
|
||||
style={{
|
||||
backgroundColor: "#f8f9fa",
|
||||
borderRadius: 12,
|
||||
boxShadow: "none",
|
||||
}}
|
||||
bodyStyle={{ padding: "16px" }}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: 8,
|
||||
}}
|
||||
>
|
||||
<ProTitle
|
||||
level={5}
|
||||
style={{
|
||||
marginBottom: 0,
|
||||
fontSize: 16,
|
||||
fontWeight: 600,
|
||||
color: "#333",
|
||||
}}
|
||||
>
|
||||
{t("menu.address")}
|
||||
</ProTitle>
|
||||
<ProText
|
||||
type="secondary"
|
||||
style={{
|
||||
fontSize: 14,
|
||||
lineHeight: "20px",
|
||||
color: "#666",
|
||||
}}
|
||||
>
|
||||
<ProTitle level={5}>{t("menu.address")}</ProTitle>
|
||||
<ProText type="secondary">
|
||||
{isRTL ? restaurant?.addressAR : restaurant?.address}
|
||||
</ProText>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<ProTitle level={5}>{t("menu.openingTimes")}</ProTitle>
|
||||
<div style={{ display: "flex", justifyContent: "space-between" }}>
|
||||
<ProText
|
||||
type="secondary"
|
||||
{/* Opening Times Section */}
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 16 }}>
|
||||
<div
|
||||
style={{
|
||||
...textStyle,
|
||||
fontWeight: todayDay === "sunday" ? 700 : 400,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 8,
|
||||
marginBottom: 4,
|
||||
}}
|
||||
>
|
||||
sunday
|
||||
</ProText>
|
||||
<ProText
|
||||
type="secondary"
|
||||
<TimeIcon color="#333" />
|
||||
<ProTitle
|
||||
level={5}
|
||||
style={{
|
||||
...textStyle,
|
||||
fontWeight: todayDay === "sunday" ? 700 : 400,
|
||||
marginBottom: 0,
|
||||
fontSize: 16,
|
||||
fontWeight: 600,
|
||||
color: "#333",
|
||||
}}
|
||||
>
|
||||
10:00 AM to 10:00 PM
|
||||
{t("menu.openingTimes")}
|
||||
</ProTitle>
|
||||
</div>
|
||||
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: 8,
|
||||
}}
|
||||
>
|
||||
{days.map((day, index) => {
|
||||
const isToday = index === todayIndex;
|
||||
const { shift1, shift2 } = dayTimes[index];
|
||||
const hasShifts = shift1 || shift2;
|
||||
|
||||
return (
|
||||
<Card
|
||||
key={day}
|
||||
bordered
|
||||
style={{
|
||||
borderRadius: 12,
|
||||
borderColor: isToday ? "#1890ff" : "#e8e8e8",
|
||||
borderWidth: isToday ? 2 : 1,
|
||||
backgroundColor: isToday ? "#f0f8ff" : "#ffffff",
|
||||
boxShadow: isToday
|
||||
? "0 2px 8px rgba(24, 144, 255, 0.15)"
|
||||
: "0 1px 2px rgba(0, 0, 0, 0.05)",
|
||||
transition: "all 0.2s ease",
|
||||
}}
|
||||
bodyStyle={{ padding: "14px 16px" }}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
gap: 16,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 12,
|
||||
flex: 1,
|
||||
}}
|
||||
>
|
||||
{isToday && (
|
||||
<div
|
||||
style={{
|
||||
width: 6,
|
||||
height: 6,
|
||||
borderRadius: "50%",
|
||||
backgroundColor: "#1890ff",
|
||||
flexShrink: 0,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<ProText
|
||||
style={{
|
||||
fontSize: 15,
|
||||
fontWeight: isToday ? 600 : 500,
|
||||
color: isToday ? "#1890ff" : "#333",
|
||||
textTransform: "capitalize",
|
||||
}}
|
||||
>
|
||||
{t(`menu.${day}`)}
|
||||
</ProText>
|
||||
</div>
|
||||
<div style={{ display: "flex", justifyContent: "space-between" }}>
|
||||
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: isRTL ? "flex-start" : "flex-end",
|
||||
gap: shift2 ? 4 : 0,
|
||||
flex: 1,
|
||||
}}
|
||||
>
|
||||
{hasShifts ? (
|
||||
<>
|
||||
<ProText
|
||||
style={{
|
||||
fontSize: 14,
|
||||
fontWeight: isToday ? 500 : 400,
|
||||
color: isToday ? "#1890ff" : "#666",
|
||||
lineHeight: "20px",
|
||||
}}
|
||||
>
|
||||
{shift1}
|
||||
</ProText>
|
||||
{shift2 && (
|
||||
<ProText
|
||||
style={{
|
||||
fontSize: 14,
|
||||
fontWeight: isToday ? 500 : 400,
|
||||
color: isToday ? "#1890ff" : "#666",
|
||||
lineHeight: "20px",
|
||||
}}
|
||||
>
|
||||
{shift2}
|
||||
</ProText>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<ProText
|
||||
type="secondary"
|
||||
style={{
|
||||
...textStyle,
|
||||
fontWeight: todayDay === "monday" ? 700 : 400,
|
||||
fontSize: 14,
|
||||
color: "#999",
|
||||
fontStyle: "italic",
|
||||
}}
|
||||
>
|
||||
monday
|
||||
</ProText>
|
||||
<ProText
|
||||
type="secondary"
|
||||
style={{
|
||||
...textStyle,
|
||||
fontWeight: todayDay === "monday" ? 700 : 400,
|
||||
}}
|
||||
>
|
||||
10:00 AM to 10:00 PM
|
||||
</ProText>
|
||||
</div>
|
||||
<div style={{ display: "flex", justifyContent: "space-between"}}>
|
||||
<ProText
|
||||
type="secondary"
|
||||
style={{
|
||||
...textStyle,
|
||||
fontWeight: todayDay === "tuesday" ? 700 : 400,
|
||||
}}
|
||||
>
|
||||
tuesday
|
||||
</ProText>
|
||||
<ProText
|
||||
type="secondary"
|
||||
style={{
|
||||
...textStyle,
|
||||
fontWeight: todayDay === "tuesday" ? 700 : 400,
|
||||
}}
|
||||
>
|
||||
10:00 AM to 10:00 PM
|
||||
</ProText>
|
||||
</div>
|
||||
<div style={{ display: "flex", justifyContent: "space-between" }}>
|
||||
<ProText
|
||||
type="secondary"
|
||||
style={{
|
||||
...textStyle,
|
||||
fontWeight: todayDay === "wednesday" ? 700 : 400,
|
||||
}}
|
||||
>
|
||||
wednesday
|
||||
</ProText>
|
||||
<ProText
|
||||
type="secondary"
|
||||
style={{
|
||||
...textStyle,
|
||||
fontWeight: todayDay === "wednesday" ? 700 : 400,
|
||||
}}
|
||||
>
|
||||
10:00 AM to 10:00 PM
|
||||
</ProText>
|
||||
</div>
|
||||
<div style={{ display: "flex", justifyContent: "space-between" }}>
|
||||
<ProText
|
||||
type="secondary"
|
||||
style={{
|
||||
...textStyle,
|
||||
fontWeight: todayDay === "thursday" ? 700 : 400,
|
||||
}}
|
||||
>
|
||||
thursday
|
||||
</ProText>
|
||||
<ProText
|
||||
type="secondary"
|
||||
style={{
|
||||
...textStyle,
|
||||
fontWeight: todayDay === "thursday" ? 700 : 400,
|
||||
}}
|
||||
>
|
||||
10:00 AM to 10:00 PM
|
||||
</ProText>
|
||||
</div>
|
||||
<div style={{ display: "flex", justifyContent: "space-between" }}>
|
||||
<ProText
|
||||
type="secondary"
|
||||
style={{
|
||||
...textStyle,
|
||||
fontWeight: todayDay === "friday" ? 700 : 400,
|
||||
}}
|
||||
>
|
||||
friday
|
||||
</ProText>
|
||||
<ProText
|
||||
type="secondary"
|
||||
style={{
|
||||
...textStyle,
|
||||
fontWeight: todayDay === "friday" ? 700 : 400,
|
||||
}}
|
||||
>
|
||||
10:00 AM to 10:00 PM
|
||||
</ProText>
|
||||
</div>
|
||||
<div style={{ display: "flex", justifyContent: "space-between" }}>
|
||||
<ProText
|
||||
type="secondary"
|
||||
style={{
|
||||
...textStyle,
|
||||
fontWeight: todayDay === "saturday" ? 700 : 400,
|
||||
}}
|
||||
>
|
||||
saturday
|
||||
</ProText>
|
||||
<ProText
|
||||
type="secondary"
|
||||
style={{
|
||||
...textStyle,
|
||||
fontWeight: todayDay === "saturday" ? 700 : 400,
|
||||
}}
|
||||
>
|
||||
10:00 AM to 10:00 PM
|
||||
{t("menu.closed")}
|
||||
</ProText>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
type="primary"
|
||||
style={{ width: "100%", height: 48 }}
|
||||
onClick={onClose}
|
||||
>
|
||||
{t("menu.close")}
|
||||
</Button>
|
||||
</ProBottomSheet>
|
||||
);
|
||||
}
|
||||
|
||||
63
src/components/Icons/BranchesIcon.tsx
Normal file
63
src/components/Icons/BranchesIcon.tsx
Normal file
@@ -0,0 +1,63 @@
|
||||
interface BranchesIconType {
|
||||
className?: string;
|
||||
onClick?: () => void;
|
||||
}
|
||||
|
||||
const BranchesIcon = ({ className, onClick }: BranchesIconType) => {
|
||||
return (
|
||||
<svg
|
||||
width="15"
|
||||
height="16"
|
||||
viewBox="0 0 15 16"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className={className}
|
||||
onClick={onClick}
|
||||
>
|
||||
<path
|
||||
d="M5.40234 3.18555C5.54014 2.593 6.04569 1.00063 7.49875 1.00063C8.95667 1.00063 9.4607 2.60361 9.59656 3.19141"
|
||||
stroke="#333333"
|
||||
strokeMiterlimit="10"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M12.7224 15H2.27935C1.70007 15 1.23047 14.5305 1.23047 13.9512V4.23958C1.23047 3.66035 1.70007 3.19075 2.27935 3.19075H12.7224C13.3017 3.19075 13.7713 3.66035 13.7713 4.23958V13.9512C13.7713 14.5305 13.3017 15 12.7224 15Z"
|
||||
stroke="#333333"
|
||||
strokeMiterlimit="10"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M10.4466 8.39983C10.7649 8.92011 10.9482 9.53128 10.9482 10.1851V10.6016H4.05078V10.1851C4.05078 8.2877 5.59476 6.74955 7.4995 6.74955C8.09085 6.74955 8.64745 6.8978 9.13388 7.15906"
|
||||
stroke="#333333"
|
||||
strokeMiterlimit="10"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M2.90234 10.6016H12.0989"
|
||||
stroke="#333333"
|
||||
strokeMiterlimit="10"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M7.5 6.75V5.94196"
|
||||
stroke="#333333"
|
||||
strokeMiterlimit="10"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M10.6209 12.0625H4.37929C4.18146 12.0625 4.0038 11.9419 3.93143 11.7585L3.47656 10.605H11.5236L11.0687 11.7585C10.9964 11.9419 10.8187 12.0625 10.6209 12.0625Z"
|
||||
stroke="#333333"
|
||||
strokeMiterlimit="10"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export default BranchesIcon;
|
||||
47
src/components/Icons/CoinsIcon.tsx
Normal file
47
src/components/Icons/CoinsIcon.tsx
Normal file
@@ -0,0 +1,47 @@
|
||||
interface CoinsIconType {
|
||||
className?: string;
|
||||
onClick?: () => void;
|
||||
}
|
||||
|
||||
const CoinsIcon = ({ className, onClick }: CoinsIconType) => {
|
||||
return (
|
||||
<svg
|
||||
width="18"
|
||||
height="18"
|
||||
viewBox="0 0 18 18"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className={className}
|
||||
onClick={onClick}
|
||||
>
|
||||
<path
|
||||
d="M15.8555 9.85547V12.4269C15.8555 13.5412 13.1692 14.9983 9.85547 14.9983C6.54175 14.9983 3.85547 13.5412 3.85547 12.4269V10.284"
|
||||
stroke="#FFC600"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M4.10938 10.5083C4.8508 11.4923 7.14366 12.4154 9.85737 12.4154C13.1711 12.4154 15.8574 11.0388 15.8574 9.85598C15.8574 9.1917 15.0114 8.46398 13.6837 7.95312"
|
||||
stroke="#FFC600"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M13.2852 5.57031V8.14174C13.2852 9.25603 10.5989 10.7132 7.28516 10.7132C3.97144 10.7132 1.28516 9.25603 1.28516 8.14174V5.57031"
|
||||
stroke="#FFC600"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M7.28516 8.12914C10.5989 8.12914 13.2852 6.75257 13.2852 5.56971C13.2852 4.38686 10.5989 3 7.28516 3C3.97144 3 1.28516 4.386 1.28516 5.56971C1.28516 6.75257 3.97144 8.12914 7.28516 8.12914Z"
|
||||
stroke="#FFC600"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export default CoinsIcon;
|
||||
33
src/components/Icons/CopyIcon.tsx
Normal file
33
src/components/Icons/CopyIcon.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
interface CopyIconType {
|
||||
className?: string;
|
||||
onClick?: () => void;
|
||||
}
|
||||
|
||||
const CopyIcon = ({ className, onClick }: CopyIconType) => {
|
||||
return (
|
||||
<svg
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className={className}
|
||||
onClick={onClick}
|
||||
>
|
||||
<path
|
||||
d="M5.83203 8.05453C5.83203 7.46509 6.06619 6.89979 6.48299 6.48299C6.89979 6.06619 7.46509 5.83203 8.05453 5.83203H15.2762C15.5681 5.83203 15.8571 5.88952 16.1267 6.00121C16.3964 6.1129 16.6414 6.27661 16.8477 6.48299C17.0541 6.68936 17.2178 6.93437 17.3295 7.20402C17.4412 7.47366 17.4987 7.76267 17.4987 8.05453V15.2762C17.4987 15.5681 17.4412 15.8571 17.3295 16.1267C17.2178 16.3964 17.0541 16.6414 16.8477 16.8477C16.6414 17.0541 16.3964 17.2178 16.1267 17.3295C15.8571 17.4412 15.5681 17.4987 15.2762 17.4987H8.05453C7.76267 17.4987 7.47366 17.4412 7.20402 17.3295C6.93437 17.2178 6.68936 17.0541 6.48299 16.8477C6.27661 16.6414 6.1129 16.3964 6.00121 16.1267C5.88952 15.8571 5.83203 15.5681 5.83203 15.2762V8.05453Z"
|
||||
stroke="#A4A3AA"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M3.34333 13.9475C3.0875 13.8021 2.87471 13.5916 2.72658 13.3374C2.57846 13.0832 2.50028 12.7942 2.5 12.5V4.16667C2.5 3.25 3.25 2.5 4.16667 2.5H12.5C13.125 2.5 13.465 2.82083 13.75 3.33333"
|
||||
stroke="#A4A3AA"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export default CopyIcon;
|
||||
25
src/components/Icons/CupIcon.tsx
Normal file
25
src/components/Icons/CupIcon.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
interface CupIconType {
|
||||
className?: string;
|
||||
onClick?: () => void;
|
||||
}
|
||||
|
||||
const CupIcon = ({ className, onClick }: CupIconType) => {
|
||||
return (
|
||||
<svg
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className={className}
|
||||
onClick={onClick}
|
||||
>
|
||||
<path
|
||||
d="M4.16797 2.5H16.668C17.11 2.5 17.5339 2.67559 17.8465 2.98816C18.159 3.30072 18.3346 3.72464 18.3346 4.16667V6.66667C18.3346 7.10869 18.159 7.53262 17.8465 7.84518C17.5339 8.15774 17.11 8.33333 16.668 8.33333H15.0013V10.8333C15.0013 11.7174 14.6501 12.5652 14.025 13.1904C13.3999 13.8155 12.552 14.1667 11.668 14.1667H6.66797C5.78391 14.1667 4.93607 13.8155 4.31095 13.1904C3.68582 12.5652 3.33464 11.7174 3.33464 10.8333V3.33333C3.33464 3.11232 3.42243 2.90036 3.57871 2.74408C3.73499 2.5878 3.94695 2.5 4.16797 2.5ZM15.0013 4.16667V6.66667H16.668V4.16667H15.0013ZM1.66797 15.8333H16.668V17.5H1.66797V15.8333Z"
|
||||
fill="#FFB700"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export default CupIcon;
|
||||
25
src/components/Icons/DirectionsIcon.tsx
Normal file
25
src/components/Icons/DirectionsIcon.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
interface DirectionsIconType {
|
||||
className?: string;
|
||||
onClick?: () => void;
|
||||
}
|
||||
|
||||
const DirectionsIcon = ({ className, onClick }: DirectionsIconType) => {
|
||||
return (
|
||||
<svg
|
||||
width="18"
|
||||
height="18"
|
||||
viewBox="0 0 18 18"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className={className}
|
||||
onClick={onClick}
|
||||
>
|
||||
<path
|
||||
d="M7.5 9H10.125V9.975C10.125 10.15 10.2 10.2687 10.35 10.3312C10.5 10.3938 10.6375 10.3625 10.7625 10.2375L12.225 8.775C12.375 8.625 12.45 8.45 12.45 8.25C12.45 8.05 12.375 7.875 12.225 7.725L10.7625 6.2625C10.6375 6.1375 10.5 6.10625 10.35 6.16875C10.2 6.23125 10.125 6.35 10.125 6.525V7.5H6.75C6.5375 7.5 6.3595 7.572 6.216 7.716C6.0725 7.86 6.0005 8.038 6 8.25V10.5C6 10.7125 6.072 10.8907 6.216 11.0347C6.36 11.1788 6.538 11.2505 6.75 11.25C6.962 11.2495 7.14025 11.1775 7.28475 11.034C7.42925 10.8905 7.501 10.7125 7.5 10.5V9ZM9 16.5C8.8125 16.5 8.62825 16.4625 8.44725 16.3875C8.26625 16.3125 8.1005 16.2 7.95 16.05L1.95 10.05C1.8 9.9 1.6875 9.73425 1.6125 9.55275C1.5375 9.37125 1.5 9.187 1.5 9C1.5 8.813 1.5375 8.62875 1.6125 8.44725C1.6875 8.26575 1.8 8.1 1.95 7.95L7.95 1.95C8.1 1.8 8.26575 1.6875 8.44725 1.6125C8.62875 1.5375 8.813 1.5 9 1.5C9.187 1.5 9.3715 1.5375 9.5535 1.6125C9.7355 1.6875 9.901 1.8 10.05 1.95L16.05 7.95C16.2 8.1 16.3125 8.26575 16.3875 8.44725C16.4625 8.62875 16.5 8.813 16.5 9C16.5 9.187 16.4625 9.3715 16.3875 9.5535C16.3125 9.7355 16.2 9.901 16.05 10.05L10.05 16.05C9.9 16.2 9.73425 16.3125 9.55275 16.3875C9.37125 16.4625 9.187 16.5 9 16.5ZM6 12L9 15L15 9L9 3L3 9L6 12Z"
|
||||
fill="#E8B400"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export default DirectionsIcon;
|
||||
45
src/components/Icons/GiftIcon.tsx
Normal file
45
src/components/Icons/GiftIcon.tsx
Normal file
@@ -0,0 +1,45 @@
|
||||
interface GiftIconType {
|
||||
className?: string;
|
||||
onClick?: () => void;
|
||||
}
|
||||
|
||||
const GiftIcon = ({ className, onClick }: GiftIconType) => {
|
||||
return (
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className={className}
|
||||
onClick={onClick}
|
||||
>
|
||||
<path
|
||||
d="M13 4.67188H3C2.44772 4.67188 2 5.11959 2 5.67187V13.0052C2 13.5575 2.44772 14.0052 3 14.0052H13C13.5523 14.0052 14 13.5575 14 13.0052V5.67187C14 5.11959 13.5523 4.67188 13 4.67188Z"
|
||||
stroke="#7950E6"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M2 10.6719H14V13.0052C14 13.2704 13.8946 13.5248 13.7071 13.7123C13.5196 13.8999 13.2652 14.0052 13 14.0052H3C2.73478 14.0052 2.48043 13.8999 2.29289 13.7123C2.10536 13.5248 2 13.2704 2 13.0052V10.6719Z"
|
||||
stroke="#7950E6"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M6.33464 4.66927C7.25511 4.66927 8.0013 3.92308 8.0013 3.0026C8.0013 2.08213 7.25511 1.33594 6.33464 1.33594C5.41416 1.33594 4.66797 2.08213 4.66797 3.0026C4.66797 3.92308 5.41416 4.66927 6.33464 4.66927Z"
|
||||
stroke="#7950E6"
|
||||
/>
|
||||
<path
|
||||
d="M9.33333 4.66667C10.0697 4.66667 10.6667 4.06971 10.6667 3.33333C10.6667 2.59695 10.0697 2 9.33333 2C8.59695 2 8 2.59695 8 3.33333C8 4.06971 8.59695 4.66667 9.33333 4.66667Z"
|
||||
stroke="#7950E6"
|
||||
/>
|
||||
<path
|
||||
d="M5.66797 6.67188L8.0013 4.67188L10.3346 6.67188"
|
||||
stroke="#7950E6"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export default GiftIcon;
|
||||
27
src/components/Icons/LoginIcon.tsx
Normal file
27
src/components/Icons/LoginIcon.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
interface LoginIconType {
|
||||
className?: string;
|
||||
onClick?: () => void;
|
||||
}
|
||||
|
||||
const LoginIcon = ({ className, onClick }: LoginIconType) => {
|
||||
return (
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className={className}
|
||||
onClick={onClick}
|
||||
>
|
||||
<path
|
||||
d="M10 2H10.8C11.9201 2 12.4802 2 12.908 2.21799C13.2843 2.40973 13.5903 2.71569 13.782 3.09202C14 3.51984 14 4.0799 14 5.2V10.8C14 11.9201 14 12.4802 13.782 12.908C13.5903 13.2843 13.2843 13.5903 12.908 13.782C12.4802 14 11.9201 14 10.8 14H10M6.66667 4.66667L10 8M10 8L6.66667 11.3333M10 8L2 8"
|
||||
stroke="#333333"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export default LoginIcon;
|
||||
37
src/components/Icons/LoyaltyAndRewardIcon.tsx
Normal file
37
src/components/Icons/LoyaltyAndRewardIcon.tsx
Normal file
@@ -0,0 +1,37 @@
|
||||
interface LoyaltyAndRewardIconType {
|
||||
className?: string;
|
||||
onClick?: () => void;
|
||||
color?: string
|
||||
}
|
||||
|
||||
const LoyaltyAndRewardIcon = ({ className, onClick, color }: LoyaltyAndRewardIconType) => {
|
||||
return (
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className={className}
|
||||
onClick={onClick}
|
||||
>
|
||||
<path
|
||||
d="M1.33203 7.9987C1.33203 5.32736 1.33203 3.99203 2.11336 3.16203C2.8947 2.33203 4.15136 2.33203 6.66536 2.33203H9.33203C11.846 2.33203 13.1034 2.33203 13.884 3.16203C14.6647 3.99203 14.6654 5.32736 14.6654 7.9987C14.6654 10.67 14.6654 12.0054 13.884 12.8354C13.1027 13.6654 11.846 13.6654 9.33203 13.6654H6.66536C4.15136 13.6654 2.89403 13.6654 2.11336 12.8354C1.3327 12.0054 1.33203 10.67 1.33203 7.9987Z"
|
||||
stroke={color || "#333333"}
|
||||
/>
|
||||
<path
|
||||
d="M4.59189 7.22074C5.21789 6.85541 5.76389 7.00274 6.09189 7.23741C6.22655 7.33341 6.29389 7.38141 6.33322 7.38141C6.37322 7.38141 6.43989 7.33341 6.57455 7.23741C6.90255 7.00274 7.44855 6.85541 8.07455 7.22074C8.89589 7.70074 9.08122 9.28341 7.18789 10.6187C6.82655 10.8727 6.64589 11.0001 6.33322 11.0001C6.02055 11.0001 5.83989 10.8734 5.47922 10.6187C3.58522 9.28341 3.76989 7.70074 4.59189 7.22074Z"
|
||||
stroke={color || "#333333"}
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
<path
|
||||
d="M12 11H10"
|
||||
stroke={color || "#333333"}
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export default LoyaltyAndRewardIcon;
|
||||
27
src/components/Icons/MyOrderIcon.tsx
Normal file
27
src/components/Icons/MyOrderIcon.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
interface MyOrderIconType {
|
||||
className?: string;
|
||||
onClick?: () => void;
|
||||
}
|
||||
|
||||
const MyOrderIcon = ({ className, onClick }: MyOrderIconType) => {
|
||||
return (
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className={className}
|
||||
onClick={onClick}
|
||||
>
|
||||
<path
|
||||
d="M10.6656 5.9987V3.9987C10.6656 2.52594 9.4717 1.33203 7.99894 1.33203C6.52618 1.33203 5.33228 2.52594 5.33228 3.9987V5.9987M2.39361 6.90001L1.99361 11.1667C1.87988 12.3798 1.82302 12.9864 2.02431 13.4549C2.20114 13.8665 2.51102 14.2067 2.90429 14.4212C3.35196 14.6654 3.96119 14.6654 5.17964 14.6654H10.8182C12.0367 14.6654 12.6459 14.6654 13.0936 14.4212C13.4869 14.2067 13.7967 13.8665 13.9736 13.4549C14.1749 12.9864 14.118 12.3798 14.0043 11.1667L13.6043 6.90001C13.5082 5.8756 13.4602 5.36339 13.2298 4.97614C13.0269 4.63509 12.7272 4.36211 12.3687 4.19193C11.9616 3.9987 11.4472 3.9987 10.4182 3.9987L5.57964 3.9987C4.55074 3.9987 4.03628 3.9987 3.62922 4.19193C3.27072 4.3621 2.97095 4.63509 2.76805 4.97614C2.53767 5.36339 2.48965 5.87559 2.39361 6.90001Z"
|
||||
stroke="#333333"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export default MyOrderIcon;
|
||||
@@ -17,10 +17,10 @@ const NoteIcon = ({ className, onClick }: NoteIconType) => {
|
||||
<path
|
||||
d="M12 16H12.008M12 8V13M3.23005 7.913L7.91005 3.23C8.06005 3.08 8.26005 3 8.48005 3H15.53C15.74 3 15.95 3.08 16.1 3.23L20.77 7.903C20.92 8.053 21 8.253 21 8.473V15.527C21 15.737 20.92 15.947 20.77 16.097L16.1 20.77C15.95 20.92 15.75 21 15.53 21H8.47005C8.36456 21.0011 8.2599 20.9814 8.16208 20.9419C8.06425 20.9025 7.9752 20.844 7.90005 20.77L3.23005 16.097C3.15602 16.0218 3.09759 15.9328 3.05812 15.835C3.01865 15.7371 2.99891 15.6325 3.00005 15.527V8.473C3.00005 8.263 3.08005 8.053 3.23005 7.903V7.913Z"
|
||||
stroke="#3D3B4A"
|
||||
stroke-width="1.5"
|
||||
stroke-miterlimit="10"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
strokeWidth="1.5"
|
||||
strokeMiterlimit="10"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
51
src/components/Icons/PopularIcon.tsx
Normal file
51
src/components/Icons/PopularIcon.tsx
Normal file
@@ -0,0 +1,51 @@
|
||||
interface PopularIconType {
|
||||
className?: string;
|
||||
onClick?: () => void;
|
||||
}
|
||||
|
||||
const PopularIcon = ({ className, onClick }: PopularIconType) => {
|
||||
return (
|
||||
<svg
|
||||
fill="#FFC600"
|
||||
height="30"
|
||||
width="30"
|
||||
version="1.1"
|
||||
id="Icons"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlnsXlink="http://www.w3.org/1999/xlink"
|
||||
viewBox="0 0 32 32"
|
||||
xmlSpace="preserve"
|
||||
className={className}
|
||||
onClick={onClick}
|
||||
>
|
||||
<g>
|
||||
<path
|
||||
d="M12,17c0.8-4.2,1.9-5.3,6.1-6.1c0.5-0.1,0.8-0.5,0.8-1s-0.3-0.9-0.8-1C13.9,8.1,12.8,7,12,2.8C11.9,2.3,11.5,2,11,2
|
||||
c-0.5,0-0.9,0.3-1,0.8C9.2,7,8.1,8.1,3.9,8.9C3.5,9,3.1,9.4,3.1,9.9s0.3,0.9,0.8,1c4.2,0.8,5.3,1.9,6.1,6.1c0.1,0.5,0.5,0.8,1,0.8
|
||||
S11.9,17.4,12,17z"
|
||||
/>
|
||||
<path
|
||||
d="M22,24c-2.8-0.6-3.4-1.2-4-4c-0.1-0.5-0.5-0.8-1-0.8s-0.9,0.3-1,0.8c-0.6,2.8-1.2,3.4-4,4c-0.5,0.1-0.8,0.5-0.8,1
|
||||
s0.3,0.9,0.8,1c2.8,0.6,3.4,1.2,4,4c0.1,0.5,0.5,0.8,1,0.8s0.9-0.3,1-0.8c0.6-2.8,1.2-3.4,4-4c0.5-0.1,0.8-0.5,0.8-1
|
||||
S22.4,24.1,22,24z"
|
||||
/>
|
||||
<path
|
||||
d="M29.2,14c-2.2-0.4-2.7-0.9-3.1-3.1c-0.1-0.5-0.5-0.8-1-0.8c-0.5,0-0.9,0.3-1,0.8c-0.4,2.2-0.9,2.7-3.1,3.1
|
||||
c-0.5,0.1-0.8,0.5-0.8,1s0.3,0.9,0.8,1c2.2,0.4,2.7,0.9,3.1,3.1c0.1,0.5,0.5,0.8,1,0.8c0.5,0,0.9-0.3,1-0.8
|
||||
c0.4-2.2,0.9-2.7,3.1-3.1c0.5-0.1,0.8-0.5,0.8-1S29.7,14.1,29.2,14z"
|
||||
/>
|
||||
<path
|
||||
d="M5.7,22.3C5.4,22,5,21.9,4.6,22.1c-0.1,0-0.2,0.1-0.3,0.2c-0.1,0.1-0.2,0.2-0.2,0.3C4,22.7,4,22.9,4,23s0,0.3,0.1,0.4
|
||||
c0.1,0.1,0.1,0.2,0.2,0.3c0.1,0.1,0.2,0.2,0.3,0.2C4.7,24,4.9,24,5,24c0.1,0,0.3,0,0.4-0.1s0.2-0.1,0.3-0.2
|
||||
c0.1-0.1,0.2-0.2,0.2-0.3C6,23.3,6,23.1,6,23s0-0.3-0.1-0.4C5.9,22.5,5.8,22.4,5.7,22.3z"
|
||||
/>
|
||||
<path
|
||||
d="M28,7c0.3,0,0.5-0.1,0.7-0.3C28.9,6.5,29,6.3,29,6s-0.1-0.5-0.3-0.7c-0.1-0.1-0.2-0.2-0.3-0.2c-0.2-0.1-0.5-0.1-0.8,0
|
||||
c-0.1,0-0.2,0.1-0.3,0.2C27.1,5.5,27,5.7,27,6c0,0.3,0.1,0.5,0.3,0.7C27.5,6.9,27.7,7,28,7z"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export default PopularIcon;
|
||||
33
src/components/Icons/RaiseIcon.tsx
Normal file
33
src/components/Icons/RaiseIcon.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
interface RaiseIconType {
|
||||
className?: string;
|
||||
onClick?: () => void;
|
||||
}
|
||||
|
||||
const RaiseIcon = ({ className, onClick }: RaiseIconType) => {
|
||||
return (
|
||||
<svg
|
||||
width="18"
|
||||
height="18"
|
||||
viewBox="0 0 18 18"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className={className}
|
||||
onClick={onClick}
|
||||
>
|
||||
<path
|
||||
d="M2.25 12.75L6.75 8.25L9.75 11.25L15.75 5.25"
|
||||
stroke="#FFC600"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M12.75 5.25H15.75V8.25"
|
||||
stroke="#FFC600"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export default RaiseIcon;
|
||||
@@ -18,9 +18,9 @@ const RefershIcon = ({ className, onClick, dimension }: RefershIconType) => {
|
||||
<path
|
||||
d="M1.5 7.5C1.5 7.5 1.59099 6.86307 4.22703 4.22703C6.86307 1.59099 11.1369 1.59099 13.773 4.22703C14.7069 5.16099 15.31 6.30054 15.5821 7.5M1.5 7.5V3M1.5 7.5H6M16.5 10.5C16.5 10.5 16.409 11.1369 13.773 13.773C11.1369 16.409 6.86307 16.409 4.22703 13.773C3.29307 12.839 2.69002 11.6995 2.41787 10.5M16.5 10.5V15M16.5 10.5H12"
|
||||
stroke="#5F6C7B"
|
||||
stroke-width="1.5"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
@@ -17,7 +17,7 @@ const RateIcon = ({ className, onClick }: RateIconType) => {
|
||||
<path
|
||||
d="M26.4351 131.263C18.2009 129.563 9.45442 127.066 4.20797 120.496C-0.552335 114.535 -1.36302 105.7 2.23265 98.9722C7.4747 89.1632 20.3229 84.8319 24.4402 74.5003C27.401 67.0706 24.9679 58.7427 24.4263 50.7633C23.852 42.2992 25.5683 33.5602 30.0859 26.379C34.6035 19.1981 42.0585 13.7282 50.4398 12.4131C62.6515 10.4973 74.6822 17.2329 87.0409 17.0031C101.49 16.7343 113.941 7.17211 127.741 2.8807C135.732 0.395575 144.272 -0.303884 152.561 0.847569C160.267 1.91822 168.283 4.99429 172.479 11.5457C176.514 17.8476 176.178 25.8983 178.116 33.1264C180.159 40.7481 184.913 47.6136 191.329 52.2072C195.925 55.498 201.301 57.6369 205.675 61.218C209.591 64.4246 212.539 68.6589 215.048 73.0551C218.27 78.7035 220.892 84.8722 221.249 91.3654C221.937 103.879 213.897 115.647 203.429 122.538C192.961 129.429 180.37 132.237 167.99 134.188C158.175 135.734 148.289 136.826 138.373 137.458C128.55 138.083 118.471 138.613 108.638 137.94C105.646 137.735 102.889 136.912 99.9902 136.244C96.2772 135.389 92.5503 135.76 88.7856 135.959C78.9527 136.48 69.0879 136.405 59.2637 135.738C49.4395 135.072 39.6552 133.813 29.9831 131.967C28.7985 131.741 27.616 131.506 26.4351 131.263Z"
|
||||
fill="#FFB700"
|
||||
fill-opacity="0.12"
|
||||
fillOpacity="0.12"
|
||||
/>
|
||||
<path
|
||||
d="M149.572 150H69.6144C67.3832 150 65.5576 148.174 65.5576 145.943V145.546C65.5576 143.315 67.3832 141.489 69.6144 141.489H149.572C151.803 141.489 153.629 143.315 153.629 145.546V145.943C153.629 148.174 151.803 150 149.572 150Z"
|
||||
@@ -42,22 +42,22 @@ const RateIcon = ({ className, onClick }: RateIconType) => {
|
||||
<path
|
||||
d="M142.231 13.9854C142.231 10.7734 139.603 8.14526 136.391 8.14526H134.134C114.797 23.8846 96.1683 40.5151 79.0111 58.5438C78.5128 59.0675 78.0168 59.5935 77.5195 60.1188V89.5343C80.7173 86.54 83.9063 83.5363 87.0804 80.5174C88.8163 78.8666 90.5472 77.2104 92.2788 75.5549C108.944 60.8335 125.605 46.108 142.231 31.3423L142.231 13.9854Z"
|
||||
fill="#FFB700"
|
||||
fill-opacity="0.12"
|
||||
fillOpacity="0.12"
|
||||
/>
|
||||
<path
|
||||
d="M95.8987 119.961C93.5809 122.338 91.2756 124.728 88.9967 127.149C85.3554 131.016 81.8398 134.99 78.3311 138.969C79.3493 140.693 81.2222 141.859 83.3584 141.859H99.6052C101.571 139.656 103.547 137.461 105.543 135.286C111.156 129.168 116.897 123.176 122.743 117.288C129.246 111.064 135.741 104.833 142.23 98.5942V76.4097C137.105 80.4905 132.061 84.6713 127.162 89.03C116.203 98.7802 105.84 109.178 95.8987 119.961Z"
|
||||
fill="#FFB700"
|
||||
fill-opacity="0.12"
|
||||
fillOpacity="0.12"
|
||||
/>
|
||||
<path
|
||||
d="M142.231 116.867C136.361 122.2 130.579 127.631 124.896 133.164C121.948 136.035 119.027 138.934 116.13 141.858H130.067C132.121 139.96 134.17 138.057 136.206 136.14C138.217 134.247 140.225 132.35 142.231 130.453V116.867Z"
|
||||
fill="#FFB700"
|
||||
fill-opacity="0.12"
|
||||
fillOpacity="0.12"
|
||||
/>
|
||||
<path
|
||||
d="M114.568 8.14502H100.676C92.7524 14.6636 85.003 21.3827 77.5195 28.407V37.3576C83.8437 32.2819 90.1625 27.196 96.5533 22.2048C102.559 17.5142 108.55 12.811 114.568 8.14502Z"
|
||||
fill="#FFB700"
|
||||
fill-opacity="0.12"
|
||||
fillOpacity="0.12"
|
||||
/>
|
||||
<path
|
||||
d="M111.138 11.8819C111.138 12.6409 110.523 13.2561 109.764 13.2561C109.005 13.2561 108.39 12.6409 108.39 11.8819C108.39 11.1229 109.005 10.5073 109.764 10.5073C110.523 10.5073 111.138 11.1226 111.138 11.8819Z"
|
||||
|
||||
@@ -8,6 +8,8 @@ interface InputCardProps {
|
||||
name: string;
|
||||
placeholder: string;
|
||||
value: string;
|
||||
required?: boolean;
|
||||
reuireqMessage?: string;
|
||||
}
|
||||
|
||||
export default function InputCard({
|
||||
@@ -15,6 +17,8 @@ export default function InputCard({
|
||||
name,
|
||||
placeholder,
|
||||
value,
|
||||
required = false,
|
||||
reuireqMessage = "",
|
||||
}: InputCardProps) {
|
||||
const dispatch = useAppDispatch();
|
||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
@@ -22,11 +26,10 @@ export default function InputCard({
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<ProInputCard title={title} dividerStyle={{ margin: "5px 0 0 0" }}>
|
||||
<ProInputCard title={title}>
|
||||
<Form.Item
|
||||
label={title}
|
||||
name={name}
|
||||
style={{ position: "relative", top: -5 }}
|
||||
rules={[{ required, message: reuireqMessage }]}
|
||||
>
|
||||
<Input
|
||||
placeholder={placeholder}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { GlobalOutlined, ShakeOutlined } from "@ant-design/icons";
|
||||
import { GlobalOutlined } from "@ant-design/icons";
|
||||
import { setLocale, setLocalesThunk } from "features/locale/localeSlice";
|
||||
import i18n from "i18n/i18n";
|
||||
import { useTransition } from "react";
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import { Card, Col, Image, Row } from "antd";
|
||||
import PresentIcon from "components/Icons/cart/PresentIcon";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Link, useParams } from "react-router-dom";
|
||||
import { useGetRestaurantDetailsQuery } from "redux/api/others";
|
||||
import { ACCESS_TOKEN } from "utils/constants";
|
||||
import { colors } from "ThemeConstants.ts";
|
||||
import ProText from "../ProText";
|
||||
@@ -12,13 +10,13 @@ import { useAppSelector } from "redux/hooks";
|
||||
const LoyaltyCard = () => {
|
||||
const { t } = useTranslation();
|
||||
const { subdomain } = useParams();
|
||||
const { data: restaurant } = useGetRestaurantDetailsQuery(subdomain);
|
||||
const { restaurant } = useAppSelector((state) => state.order);
|
||||
const { isRTL } = useAppSelector((state) => state.locale);
|
||||
const token = localStorage.getItem(ACCESS_TOKEN);
|
||||
const isHasLoyaltyGift =
|
||||
(restaurant?.loyalty_stamps || 0) -
|
||||
(restaurant?.customer_loyalty_points || 0) <=
|
||||
0;
|
||||
const loyaltyStamps = restaurant?.loyalty_stamps ?? 0;
|
||||
const customerLoyaltyPoints = restaurant?.customer_loyalty_points ?? 0;
|
||||
const remainingToNextReward =
|
||||
loyaltyStamps - (customerLoyaltyPoints % loyaltyStamps);
|
||||
|
||||
return (
|
||||
<div className={styles.loyaltyContainer}>
|
||||
@@ -62,8 +60,14 @@ const LoyaltyCard = () => {
|
||||
}}
|
||||
>
|
||||
{token &&
|
||||
customerLoyaltyPoints < loyaltyStamps &&
|
||||
t("menu.loyaltyDescription", {
|
||||
value: restaurant?.loyalty_stamps ?? 0,
|
||||
value: loyaltyStamps,
|
||||
})}
|
||||
{token &&
|
||||
customerLoyaltyPoints >= loyaltyStamps &&
|
||||
t("menu.youHaveXEarnedRewardsReadyToRedeem", {
|
||||
rewards: Math.floor(customerLoyaltyPoints / loyaltyStamps),
|
||||
})}
|
||||
{!token && (
|
||||
<div style={{ paddingTop: 4 }}>
|
||||
@@ -95,21 +99,22 @@ const LoyaltyCard = () => {
|
||||
)}
|
||||
</ProText>
|
||||
</Col>
|
||||
<Col> {isHasLoyaltyGift && <PresentIcon />}</Col>
|
||||
{/* <Col> {isHasLoyaltyGift && <PresentIcon />}</Col> */}
|
||||
</Row>
|
||||
{token && (
|
||||
<>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
gap: 12,
|
||||
overflow: "hidden",
|
||||
overflow: "auto",
|
||||
scrollbarWidth: "none",
|
||||
paddingBottom: 12,
|
||||
}}
|
||||
>
|
||||
{Array.from({ length: restaurant?.loyalty_stamps || 0 }).map(
|
||||
(_, index) => {
|
||||
const currentPoints = restaurant?.customer_loyalty_points || 0;
|
||||
{Array.from({ length: loyaltyStamps }).map((_, index) => {
|
||||
const currentPoints = customerLoyaltyPoints % loyaltyStamps;
|
||||
const isCollected = index < currentPoints;
|
||||
return (
|
||||
<Col key={index}>
|
||||
@@ -126,9 +131,30 @@ const LoyaltyCard = () => {
|
||||
/>
|
||||
</Col>
|
||||
);
|
||||
},
|
||||
)}
|
||||
})}
|
||||
</div>
|
||||
<ProText
|
||||
strong
|
||||
style={{
|
||||
fontWeight: 400,
|
||||
fontStyle: "Regular",
|
||||
fontSize: "12px",
|
||||
lineHeight: "140%",
|
||||
letterSpacing: "0%",
|
||||
color:
|
||||
customerLoyaltyPoints < loyaltyStamps ? "#777580" : "#32AD6D",
|
||||
}}
|
||||
>
|
||||
{customerLoyaltyPoints < loyaltyStamps &&
|
||||
t("menu.justXMorePurchasesToUnlockYourFREEItem", {
|
||||
cups: remainingToNextReward,
|
||||
})}
|
||||
{customerLoyaltyPoints >= loyaltyStamps &&
|
||||
t("menu.youreJustXCupsAwayFromYourNextReward", {
|
||||
cups: remainingToNextReward,
|
||||
})}
|
||||
</ProText>
|
||||
</>
|
||||
)}
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-size: 16px;
|
||||
min-height: 24px;
|
||||
}
|
||||
|
||||
.summaryDivider {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Card, Checkbox, Divider, Space } from "antd";
|
||||
import { Card, Divider, Space, Tag } from "antd";
|
||||
import ArabicPrice from "components/ArabicPrice";
|
||||
import {
|
||||
selectCart,
|
||||
@@ -6,19 +6,17 @@ import {
|
||||
selectDiscountTotal,
|
||||
selectGrandTotal,
|
||||
selectHighestPricedLoyaltyItem,
|
||||
selectLoyaltyValidation,
|
||||
selectTaxAmount,
|
||||
updateUseLoyaltyPoints,
|
||||
totalTaxes,
|
||||
} from "features/order/orderSlice";
|
||||
import { OrderType } from "pages/checkout/hooks/types";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useParams } from "react-router-dom";
|
||||
import { useGetRestaurantDetailsQuery } from "redux/api/others";
|
||||
import { useAppDispatch, useAppSelector } from "redux/hooks";
|
||||
import { useAppSelector } from "redux/hooks";
|
||||
import ProText from "../ProText";
|
||||
import ProTitle from "../ProTitle";
|
||||
import styles from "./OrderSummary.module.css";
|
||||
import { CSSProperties } from "react";
|
||||
import { CSSProperties, useMemo } from "react";
|
||||
|
||||
export default function OrderSummary() {
|
||||
const { t } = useTranslation();
|
||||
@@ -26,11 +24,9 @@ export default function OrderSummary() {
|
||||
const { subdomain } = useParams();
|
||||
const { data: restaurant } = useGetRestaurantDetailsQuery(subdomain);
|
||||
const { orderType } = useAppSelector(selectCart);
|
||||
const dispatch = useAppDispatch();
|
||||
const subtotal = useAppSelector(selectCartTotal);
|
||||
const loyaltyValidation = useAppSelector(selectLoyaltyValidation);
|
||||
const highestLoyaltyItem = useAppSelector(selectHighestPricedLoyaltyItem);
|
||||
const taxAmount = useAppSelector(selectTaxAmount);
|
||||
const totalTaxesAmount = useAppSelector(totalTaxes);
|
||||
const grandTotal = useAppSelector(selectGrandTotal);
|
||||
const discountAmount = useAppSelector(selectDiscountTotal);
|
||||
|
||||
@@ -48,6 +44,28 @@ export default function OrderSummary() {
|
||||
textAlign: "center",
|
||||
};
|
||||
|
||||
const vat = ((restaurant?.vat ?? 0) / 100) * (subtotal - discountAmount) || 0;
|
||||
|
||||
// Calculate individual taxes
|
||||
const taxesList = useMemo(() => {
|
||||
if (!restaurant?.taxes) return [];
|
||||
|
||||
const subtotalAfterDiscount = subtotal - discountAmount;
|
||||
|
||||
return restaurant.taxes
|
||||
.filter((tax) => tax.is_active === 1)
|
||||
.map((tax) => {
|
||||
const amount = ((Number(tax.percentage) || 0) / 100) * subtotalAfterDiscount;
|
||||
return {
|
||||
id: tax.id,
|
||||
name: tax.name,
|
||||
name_local: tax.name_local,
|
||||
percentage: tax.percentage,
|
||||
amount,
|
||||
};
|
||||
});
|
||||
}, [restaurant?.taxes, subtotal, discountAmount]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Card className={`${styles.orderSummary}`}>
|
||||
@@ -69,7 +87,7 @@ export default function OrderSummary() {
|
||||
<ProText type="secondary" style={titlesStyle}>
|
||||
{t("cart.basketTotal")}
|
||||
</ProText>
|
||||
<ArabicPrice price={subtotal} style={titlesStyle} />
|
||||
<ArabicPrice price={subtotal} textStyle={titlesStyle} />
|
||||
</div>
|
||||
{orderType === OrderType.Delivery && (
|
||||
<div className={styles.summaryRow}>
|
||||
@@ -78,36 +96,120 @@ export default function OrderSummary() {
|
||||
</ProText>
|
||||
<ArabicPrice
|
||||
price={Number(restaurant?.delivery_fees || 0)}
|
||||
style={{ ...titlesStyle, color: "#434E5C" }}
|
||||
textStyle={{ ...titlesStyle, color: "#434E5C" }}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{orderType !== OrderType.Redeem && (
|
||||
<div className={styles.summaryRow}>
|
||||
<ProText type="secondary" style={titlesStyle}>
|
||||
{t("cart.discount")}
|
||||
</ProText>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
minHeight: "24px",
|
||||
}}
|
||||
>
|
||||
{isHasLoyaltyGift &&
|
||||
useLoyaltyPoints &&
|
||||
highestLoyaltyItem &&
|
||||
restaurant?.is_loyalty_enabled === 1 ? (
|
||||
<Tag
|
||||
color="green"
|
||||
style={{
|
||||
backgroundColor: "#EDFEF5",
|
||||
borderRadius: "4px",
|
||||
padding: "3px 10px",
|
||||
fontWeight: 500,
|
||||
fontStyle: "Medium",
|
||||
fontSize: 12,
|
||||
lineHeight: "140%",
|
||||
letterSpacing: "0%",
|
||||
placeItems: "center",
|
||||
margin: "0 10px",
|
||||
position: "relative",
|
||||
top: -1,
|
||||
}}
|
||||
>
|
||||
{t("cart.loyalty")}
|
||||
</Tag>
|
||||
) : null}
|
||||
<ArabicPrice
|
||||
price={discountAmount}
|
||||
style={{ ...titlesStyle, color: "#434E5C" }}
|
||||
price={discountAmount + (useLoyaltyPoints && restaurant?.is_loyalty_enabled === 1 ? ((highestLoyaltyItem?.price || 0) * (totalTaxesAmount / 100)) : 0)}
|
||||
textStyle={{ ...titlesStyle, color: "#434E5C" }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{/* {orderType !== OrderType.Redeem && (
|
||||
<div className={styles.summaryRow}>
|
||||
<ProText type="secondary" style={titlesStyle}>
|
||||
{t("cart.tax")}
|
||||
{t("cart.tip")}
|
||||
</ProText>
|
||||
<ArabicPrice
|
||||
price={taxAmount || 0}
|
||||
style={{ ...titlesStyle, color: "#434E5C" }}
|
||||
price={tip || 0}
|
||||
textStyle={{ ...titlesStyle, color: "#434E5C" }}
|
||||
/>
|
||||
</div>
|
||||
{splitBillAmount > 0 && (
|
||||
)} */}
|
||||
{orderType === OrderType.Redeem && (
|
||||
<div className={styles.summaryRow}>
|
||||
<ProText type="secondary" style={titlesStyle}>
|
||||
{t("cart.giftedItems")}
|
||||
</ProText>
|
||||
<ArabicPrice
|
||||
price={0}
|
||||
textStyle={{ ...titlesStyle, color: "#434E5C" }}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{orderType === OrderType.Redeem && (
|
||||
<div className={styles.summaryRow}>
|
||||
<ProText type="secondary" style={titlesStyle}>
|
||||
{t("cart.voucherApplied")}
|
||||
</ProText>
|
||||
<ArabicPrice
|
||||
price={0}
|
||||
textStyle={{ ...titlesStyle, color: "#32AD6D" }}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{orderType !== OrderType.Redeem && restaurant?.vat && (
|
||||
<div className={styles.summaryRow}>
|
||||
<ProText type="secondary" style={titlesStyle}>
|
||||
{t("cart.vat", { value: restaurant.vat })}
|
||||
</ProText>
|
||||
<ArabicPrice
|
||||
price={vat}
|
||||
textStyle={{ ...titlesStyle, color: "#434E5C" }}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{orderType !== OrderType.Redeem &&
|
||||
taxesList.map((tax) => (
|
||||
<div key={tax.id} className={styles.summaryRow}>
|
||||
<ProText type="secondary" style={titlesStyle}>
|
||||
{t("cart.otherTaxes", {
|
||||
name: tax.name || tax.name_local,
|
||||
value: tax.percentage,
|
||||
})}
|
||||
</ProText>
|
||||
<ArabicPrice
|
||||
price={tax.amount}
|
||||
textStyle={{ ...titlesStyle, color: "#434E5C" }}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
{orderType !== OrderType.Redeem && splitBillAmount > 0 && (
|
||||
<div className={styles.summaryRow}>
|
||||
<ProText type="secondary" style={titlesStyle}>
|
||||
{t("splitBill.splitBillAmount")}
|
||||
</ProText>
|
||||
<ArabicPrice
|
||||
price={splitBillAmount}
|
||||
style={{ ...titlesStyle, color: "#434E5C" }}
|
||||
textStyle={{ ...titlesStyle, color: "#434E5C" }}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
@@ -124,11 +226,13 @@ export default function OrderSummary() {
|
||||
color: "#333333",
|
||||
}}
|
||||
>
|
||||
{t("cart.totalAmount")}
|
||||
{orderType === OrderType.Redeem
|
||||
? t("cart.remainingToPay")
|
||||
: t("cart.totalAmount")}
|
||||
</ProText>
|
||||
<ArabicPrice
|
||||
price={grandTotal}
|
||||
style={{
|
||||
textStyle={{
|
||||
fontWeight: 600,
|
||||
fontStyle: "SemiBold",
|
||||
fontSize: 18,
|
||||
@@ -139,38 +243,6 @@ export default function OrderSummary() {
|
||||
/>
|
||||
</div>
|
||||
</Space>
|
||||
|
||||
{isHasLoyaltyGift && restaurant?.is_loyalty_enabled === 1 && (
|
||||
<>
|
||||
<Checkbox
|
||||
checked={useLoyaltyPoints}
|
||||
onChange={(value) => {
|
||||
dispatch(updateUseLoyaltyPoints(value.target.checked));
|
||||
}}
|
||||
style={{ marginTop: 8 }}
|
||||
>
|
||||
{t("cart.useLoyaltyPoints")}
|
||||
</Checkbox>
|
||||
</>
|
||||
)}
|
||||
|
||||
{isHasLoyaltyGift && loyaltyValidation.errorMessage && (
|
||||
<div style={{ marginTop: 8, color: "red", fontSize: "12px" }}>
|
||||
{t(loyaltyValidation.errorMessage)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isHasLoyaltyGift &&
|
||||
useLoyaltyPoints &&
|
||||
highestLoyaltyItem &&
|
||||
restaurant?.is_loyalty_enabled === 1 && (
|
||||
<div style={{ marginTop: 8, color: "green", fontSize: "12px" }}>
|
||||
{t("cart.loyaltyDiscountApplied", {
|
||||
itemName: highestLoyaltyItem.name,
|
||||
amount: Math.round(highestLoyaltyItem.price || 0).toFixed(2),
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Form, Radio, Space } from "antd";
|
||||
import { Form, Image, Radio, Space } from "antd";
|
||||
import { Group } from "antd/es/radio";
|
||||
import ArabicPrice from "components/ArabicPrice";
|
||||
import DifferentCardIcon from "components/Icons/paymentMethods/DifferentCardIcon";
|
||||
// import DifferentCardIcon from "components/Icons/paymentMethods/DifferentCardIcon";
|
||||
import ProText from "components/ProText";
|
||||
import {
|
||||
selectCart,
|
||||
@@ -14,14 +14,15 @@ import { colors, ProGray1 } from "../../ThemeConstants";
|
||||
import ProInputCard from "../ProInputCard/ProInputCard";
|
||||
import styles from "./PaymentMethods.module.css";
|
||||
import { OrderType } from "pages/checkout/hooks/types.ts";
|
||||
import RCardIcon from "components/Icons/RCardIcon";
|
||||
import { formatPriceUi } from "utils/helpers";
|
||||
|
||||
const PaymentMethods = () => {
|
||||
const { t } = useTranslation();
|
||||
const { paymentMethod, orderType } = useAppSelector(selectCart);
|
||||
const dispatch = useAppDispatch();
|
||||
const grandTotal = useAppSelector(selectGrandTotal);
|
||||
const { isRTL } = useAppSelector((state) => state.locale);
|
||||
const { restaurant } = useAppSelector((state) => state.order);
|
||||
// const { isRTL } = useAppSelector((state) => state.locale);
|
||||
|
||||
const options: {
|
||||
label: React.ReactNode;
|
||||
@@ -39,16 +40,18 @@ const PaymentMethods = () => {
|
||||
<ProText
|
||||
style={{
|
||||
color: "#E8B400",
|
||||
[isRTL ? "marginLeft" : "marginRight"]: 4,
|
||||
// [isRTL ? "marginLeft" : "marginRight"]: 4,
|
||||
}}
|
||||
>
|
||||
$
|
||||
{/* $ */}
|
||||
</ProText>
|
||||
<ProText style={{ color: "#5F6C7B" }}>
|
||||
{t("checkout.cash")}
|
||||
</ProText>
|
||||
<ProText style={{color: '#5F6C7B'}}>{t("checkout.cash")}</ProText>
|
||||
</>
|
||||
),
|
||||
value: "cash",
|
||||
price: grandTotal.toFixed(2),
|
||||
price: formatPriceUi(grandTotal, restaurant.currency_decimals ?? 3),
|
||||
style: {
|
||||
color: colors.primary,
|
||||
},
|
||||
@@ -64,14 +67,26 @@ const PaymentMethods = () => {
|
||||
{
|
||||
label: (
|
||||
<>
|
||||
<RCardIcon className={styles.eCardIcon} />
|
||||
<ProText style={{color: '#5F6C7B'}}>{t("checkout.differentCard")}</ProText>
|
||||
{/* <RCardIcon className={styles.eCardIcon} /> */}
|
||||
<ProText style={{ color: "#5F6C7B" }}>
|
||||
{t("checkout.thawani")}
|
||||
</ProText>
|
||||
</>
|
||||
),
|
||||
value: "differentCard",
|
||||
value: "thawani",
|
||||
icon: (
|
||||
<div className={styles.differentCardIcon}>
|
||||
<DifferentCardIcon />
|
||||
<Image
|
||||
preview={false}
|
||||
src={"thawani.png"}
|
||||
alt="thawani"
|
||||
width={24}
|
||||
height={24}
|
||||
style={{
|
||||
position: "relative",
|
||||
bottom: 3,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
hideCurrency: true,
|
||||
|
||||
@@ -5,9 +5,14 @@ interface ProCheckboxGroupsProps {
|
||||
options: any[];
|
||||
value?: string[];
|
||||
onChange?: (values: string[]) => void;
|
||||
showPrice?: boolean;
|
||||
}
|
||||
|
||||
const ProCheckboxGroups = ({ options, ...props }: ProCheckboxGroupsProps) => {
|
||||
const ProCheckboxGroups = ({
|
||||
options,
|
||||
showPrice,
|
||||
...props
|
||||
}: ProCheckboxGroupsProps) => {
|
||||
return (
|
||||
<Checkbox.Group
|
||||
{...props}
|
||||
@@ -15,7 +20,7 @@ const ProCheckboxGroups = ({ options, ...props }: ProCheckboxGroupsProps) => {
|
||||
width: "100%",
|
||||
}}
|
||||
>
|
||||
<Space direction="vertical" style={{ width: "100%" }}>
|
||||
<Space orientation="vertical" style={{ width: "100%" }}>
|
||||
{options.map((option: any) => (
|
||||
<Checkbox
|
||||
key={option.value}
|
||||
@@ -38,7 +43,9 @@ const ProCheckboxGroups = ({ options, ...props }: ProCheckboxGroupsProps) => {
|
||||
>
|
||||
{option.label}
|
||||
</ProText>
|
||||
{showPrice && (
|
||||
<ProText style={{ fontSize: "1rem" }}>{option.price}</ProText>
|
||||
)}
|
||||
</div>
|
||||
</Checkbox>
|
||||
))}
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
import { Loader } from "components/Loader/Loader";
|
||||
import { Navigate, useParams } from "react-router-dom";
|
||||
import { useAppSelector } from "redux/hooks";
|
||||
|
||||
export const PrivateRoute = ({
|
||||
children,
|
||||
}: // permission,
|
||||
{
|
||||
children: JSX.Element;
|
||||
permission?: string;
|
||||
}) => {
|
||||
const { token, loading } = useAppSelector((state) => state.auth);
|
||||
const { subdomain } = useParams();
|
||||
|
||||
// const { data: user, isLoading: loadingUser } = useGetSignedUserInfoQuery();
|
||||
|
||||
// useEffect(() => {
|
||||
// if (user) dispatch(setUser(user));
|
||||
// }, [dispatch, user]);
|
||||
|
||||
if (loading) {
|
||||
return <Loader />;
|
||||
}
|
||||
|
||||
// let newPermissions: any[] = [];
|
||||
// // aggregate the rules from multi
|
||||
// user?.roles?.forEach(
|
||||
// (r) => (newPermissions = [...newPermissions, ...r.permissions])
|
||||
// );
|
||||
|
||||
// const userHasRequiredPermission = permission
|
||||
// ? !!newPermissions.find((p) => p.name === permission)
|
||||
// : true;
|
||||
|
||||
console.log(token);
|
||||
|
||||
|
||||
if (!token) {
|
||||
return <Navigate to={`/${subdomain}/login`} />;
|
||||
}
|
||||
|
||||
// if (token && !userHasRequiredPermission) {
|
||||
// return <AccessDenied />; // build your won access denied page (sth like 404)
|
||||
// }
|
||||
|
||||
return children;
|
||||
};
|
||||
177
src/components/productChoicesCard/ProductChoicesCard.tsx
Normal file
177
src/components/productChoicesCard/ProductChoicesCard.tsx
Normal file
@@ -0,0 +1,177 @@
|
||||
import { Space, Divider } from "antd";
|
||||
import ProText from "components/ProText.tsx";
|
||||
import styles from "pages/cart/cart.module.css";
|
||||
import ArabicPrice from "components/ArabicPrice";
|
||||
import ImageWithFallback from "components/ImageWithFallback";
|
||||
import CartActionsButtons from "components/CartActionsButtons/CartActionsButtons.tsx";
|
||||
import { CartItem } from "utils/types/appTypes.ts";
|
||||
import { useAppSelector } from "redux/hooks.ts";
|
||||
import useBreakPoint from "hooks/useBreakPoint.ts";
|
||||
import { OrderItem } from "pages/checkout/hooks/types.ts";
|
||||
|
||||
type ProductChoicesCardProps = {
|
||||
product: CartItem | OrderItem;
|
||||
addDividerAfter: boolean;
|
||||
};
|
||||
export default function ProductChoicesCard({
|
||||
product,
|
||||
addDividerAfter,
|
||||
}: ProductChoicesCardProps) {
|
||||
const { isRTL } = useAppSelector((state) => state.locale);
|
||||
const { isMobile, isTablet } = useBreakPoint();
|
||||
|
||||
const getMenuItemImageStyle = () => {
|
||||
if (isMobile) {
|
||||
return {
|
||||
width: 115,
|
||||
height: 96,
|
||||
};
|
||||
}
|
||||
return {
|
||||
width: 120,
|
||||
height: 120,
|
||||
};
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ position: "relative" }}>
|
||||
<Space
|
||||
size="middle"
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
height: "100%",
|
||||
}}
|
||||
>
|
||||
<Space orientation="vertical" size="small">
|
||||
<div style={{}}>
|
||||
<ProText
|
||||
style={{
|
||||
margin: 0,
|
||||
lineClamp: 1,
|
||||
fontSize: isMobile ? 14 : isTablet ? 16 : 18,
|
||||
fontWeight: 600,
|
||||
}}
|
||||
>
|
||||
{product.name}
|
||||
</ProText>
|
||||
<ProText
|
||||
type="secondary"
|
||||
className={`${styles.itemDescription} responsive-text`}
|
||||
style={{
|
||||
margin: 0,
|
||||
lineClamp: 1,
|
||||
padding: isMobile ? "3px 0" : isTablet ? 8 : 10,
|
||||
fontSize: isMobile ? 14 : isTablet ? 18 : 20,
|
||||
display: "-webkit-box",
|
||||
WebkitBoxOrient: "vertical",
|
||||
WebkitLineClamp: 1,
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
wordWrap: "break-word",
|
||||
overflowWrap: "break-word",
|
||||
lineHeight: "1.4",
|
||||
maxHeight: isMobile ? "3em" : isTablet ? "5em" : "7em",
|
||||
fontWeight: 500,
|
||||
letterSpacing: "0.01em",
|
||||
width: "100%",
|
||||
}}
|
||||
>
|
||||
{product.type === "CartItem" &&
|
||||
(isRTL
|
||||
? product.variant?.optionsAR.map((o) => o.value).join(", ")
|
||||
: product.variant?.options.map((o) => o.value).join(", "))}
|
||||
{product.type === "OrderItem" && product.variantName}
|
||||
</ProText>
|
||||
|
||||
{product.extras && (
|
||||
<ProText
|
||||
type="secondary"
|
||||
className={`${styles.itemDescription} responsive-text`}
|
||||
style={{
|
||||
margin: 0,
|
||||
lineClamp: 1,
|
||||
fontSize: isMobile ? 14 : isTablet ? 18 : 20,
|
||||
display: "-webkit-box",
|
||||
WebkitBoxOrient: "vertical",
|
||||
WebkitLineClamp: 1,
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
wordWrap: "break-word",
|
||||
overflowWrap: "break-word",
|
||||
lineHeight: "1.4",
|
||||
maxHeight: isMobile ? "3em" : isTablet ? "5em" : "7em",
|
||||
fontWeight: 500,
|
||||
letterSpacing: "0.01em",
|
||||
width: "100%",
|
||||
}}
|
||||
>
|
||||
{product.extras.map((o) => (
|
||||
<span key={o.id}>{o.name}</span>
|
||||
))}
|
||||
</ProText>
|
||||
)}
|
||||
|
||||
{product.type === "CartItem"
|
||||
? product.extrasgroupnew?.map((o) => (
|
||||
<ProText
|
||||
type="secondary"
|
||||
className={`${styles.itemDescription} responsive-text`}
|
||||
style={{
|
||||
margin: 0,
|
||||
lineClamp: 1,
|
||||
padding: isMobile ? "3px 0" : isTablet ? 8 : 10,
|
||||
fontSize: isMobile ? 14 : isTablet ? 18 : 20,
|
||||
display: "-webkit-box",
|
||||
WebkitBoxOrient: "vertical",
|
||||
WebkitLineClamp: 1,
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
wordWrap: "break-word",
|
||||
overflowWrap: "break-word",
|
||||
lineHeight: "1.4",
|
||||
maxHeight: isMobile ? "3em" : isTablet ? "5em" : "7em",
|
||||
fontWeight: 500,
|
||||
letterSpacing: "0.01em",
|
||||
width: "100%",
|
||||
}}
|
||||
>
|
||||
{isRTL ? o.extrasStringAR : o.extrasString}
|
||||
</ProText>
|
||||
))
|
||||
: // ToDo
|
||||
product.itemline}
|
||||
</div>
|
||||
<div style={{}}>
|
||||
<ArabicPrice price={product.price} style={{ fontStyle: "bold" }} />
|
||||
</div>
|
||||
</Space>
|
||||
<div style={{ position: "relative" }}>
|
||||
<ImageWithFallback
|
||||
src={product.image}
|
||||
alt={product.name}
|
||||
className={`${styles.menuItemImage} responsive-image`}
|
||||
{...getMenuItemImageStyle()}
|
||||
fallbackSrc={
|
||||
"https://fascano-space.s3.me-central-1.amazonaws.com/uploads/restorants/685a8fc884a8c_large.jpg"
|
||||
}
|
||||
/>
|
||||
{product.type === "CartItem" && (
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
right: 3,
|
||||
bottom: 3,
|
||||
}}
|
||||
>
|
||||
<CartActionsButtons item={product} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Space>
|
||||
|
||||
{addDividerAfter && <Divider style={{ margin: "10px 0" }} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
22
src/components/publicRoute/PublicRoute.tsx
Normal file
22
src/components/publicRoute/PublicRoute.tsx
Normal file
@@ -0,0 +1,22 @@
|
||||
import { Loader } from "components/Loader/Loader";
|
||||
import { Navigate, useParams } from "react-router-dom";
|
||||
import { useAppSelector } from "redux/hooks";
|
||||
|
||||
export const PublicRoute = ({
|
||||
children,
|
||||
}: {
|
||||
children: JSX.Element;
|
||||
}) => {
|
||||
const { token, loading } = useAppSelector((state) => state.auth);
|
||||
const { subdomain } = useParams();
|
||||
|
||||
if (loading) {
|
||||
return <Loader />;
|
||||
}
|
||||
|
||||
if (token) {
|
||||
return <Navigate to={`/${subdomain}`} replace />;
|
||||
}
|
||||
|
||||
return children;
|
||||
};
|
||||
@@ -380,7 +380,11 @@ const orderSlice = createSlice({
|
||||
// Clear all cart data from localStorage
|
||||
if (typeof window !== "undefined") {
|
||||
Object.values(CART_STORAGE_KEYS)
|
||||
.filter((key) => key !== CART_STORAGE_KEYS.ORDER_TYPE)
|
||||
.filter(
|
||||
(key) =>
|
||||
key !== CART_STORAGE_KEYS.ORDER_TYPE &&
|
||||
key !== CART_STORAGE_KEYS.PAYMENT_METHOD,
|
||||
)
|
||||
.forEach((key) => {
|
||||
localStorage.removeItem(key);
|
||||
});
|
||||
@@ -702,7 +706,8 @@ const orderSlice = createSlice({
|
||||
}
|
||||
},
|
||||
updateOrder(state, action: PayloadAction<any>) {
|
||||
state.order = action.payload;
|
||||
state.order = { ...(state.order || {}), ...action.payload };
|
||||
|
||||
if (typeof window !== "undefined") {
|
||||
localStorage.setItem(
|
||||
CART_STORAGE_KEYS.ORDER,
|
||||
@@ -766,23 +771,21 @@ export const {
|
||||
|
||||
// Tax calculation helper functions
|
||||
const calculateTaxAmount = (
|
||||
state: RootState,
|
||||
amount: number,
|
||||
tax: Tax,
|
||||
): number => {
|
||||
const percentage = parseFloat(tax.percentage);
|
||||
return (((state.order.restaurant?.vat || 0) + percentage) * amount) / 100;
|
||||
return (percentage * amount) / 100;
|
||||
};
|
||||
|
||||
const calculateTotalTax = (
|
||||
state: RootState,
|
||||
subtotal: number,
|
||||
taxes: Tax[],
|
||||
): number => {
|
||||
return taxes
|
||||
.filter((tax) => tax.is_active === 1)
|
||||
.reduce(
|
||||
(total, tax) => total + calculateTaxAmount(state, subtotal, tax),
|
||||
(total, tax) => total + calculateTaxAmount(subtotal, tax),
|
||||
0,
|
||||
);
|
||||
};
|
||||
@@ -821,21 +824,31 @@ export const selectLoyaltyItems = createSelector([selectOrderItems], (items) =>
|
||||
items.filter((item) => item.isHasLoyalty),
|
||||
);
|
||||
|
||||
// Tax selectors
|
||||
export const selectTaxes = (state: RootState) => state.order.restaurant.taxes;
|
||||
export const totalTaxes = (state: RootState) => {
|
||||
const taxes = selectTaxes(state);
|
||||
const totalTaxes = taxes?.reduce((total, tax) => total + parseFloat(tax.percentage), 0) || 0;
|
||||
const vat = (state.order.restaurant?.vat || 0);
|
||||
return totalTaxes + vat;
|
||||
};
|
||||
|
||||
export const selectHighestPricedLoyaltyItem = (state: RootState) => {
|
||||
const loyaltyItems = selectLoyaltyItems(state);
|
||||
if (loyaltyItems.length === 0) return null;
|
||||
|
||||
return loyaltyItems.reduce((highest, current) =>
|
||||
const highestItem = loyaltyItems.reduce((highest, current) =>
|
||||
current.price > highest.price ? current : highest,
|
||||
);
|
||||
)
|
||||
return highestItem;
|
||||
};
|
||||
|
||||
export const selectDiscountTotal = (state: RootState) =>
|
||||
(state.order.discount.value / 100) * selectCartTotal(state) +
|
||||
export const selectDiscountTotal = (state: RootState) => {
|
||||
return (state.order.discount.value / 100) * selectCartTotal(state) +
|
||||
(state.order.useLoyaltyPoints &&
|
||||
state.order.restaurant?.is_loyalty_enabled === 1
|
||||
? selectHighestPricedLoyaltyItem(state)?.price || 0
|
||||
? ((selectHighestPricedLoyaltyItem(state)?.price || 0))
|
||||
: 0);
|
||||
}
|
||||
|
||||
export const selectLoyaltyValidation = createSelector(
|
||||
[(state: RootState) => state.order.useLoyaltyPoints, selectLoyaltyItems],
|
||||
@@ -850,18 +863,18 @@ export const selectLoyaltyValidation = createSelector(
|
||||
}),
|
||||
);
|
||||
|
||||
// Tax selectors
|
||||
export const selectTaxes = (state: RootState) => state.order.restaurant.taxes;
|
||||
|
||||
|
||||
export const selectTaxAmount = (state: RootState) => {
|
||||
const subtotal = selectCartTotal(state) - selectDiscountTotal(state);
|
||||
const taxes = selectTaxes(state);
|
||||
return calculateTotalTax(state, subtotal, taxes || []);
|
||||
return calculateTotalTax(subtotal, taxes || []);
|
||||
};
|
||||
|
||||
export const selectGrandTotal = (state: RootState) => {
|
||||
const totalDiscount = selectDiscountTotal(state);
|
||||
const taxAmount = selectTaxAmount(state);
|
||||
const vatAmount = ((state.order.restaurant?.vat || 0) / 100) * (selectCartTotal(state) - selectDiscountTotal(state));
|
||||
const subtotal = selectCartTotal(state);
|
||||
const deliveryFee =
|
||||
state.order.orderType === OrderType.Delivery
|
||||
@@ -869,11 +882,13 @@ export const selectGrandTotal = (state: RootState) => {
|
||||
: 0;
|
||||
|
||||
return (
|
||||
subtotal +
|
||||
taxAmount -
|
||||
subtotal -
|
||||
totalDiscount +
|
||||
deliveryFee -
|
||||
state.order.splitBillAmount
|
||||
taxAmount +
|
||||
vatAmount -
|
||||
deliveryFee
|
||||
// state.order.splitBillAmount +
|
||||
// Number(state.order.tip)
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
export function useDetectBarcode() {
|
||||
const [barcode, setBarcode] = useState<string>("");
|
||||
const [lastBarcode, setLastBarcode] = useState<string>("");
|
||||
|
||||
useEffect(() => {
|
||||
let interval: any;
|
||||
|
||||
const handleKeydown = (evt: any) => {
|
||||
if (interval) clearInterval(interval);
|
||||
|
||||
if (evt.code === "Enter") {
|
||||
if (barcode) handleBarcode(barcode);
|
||||
setBarcode("");
|
||||
return;
|
||||
}
|
||||
|
||||
if (evt.key !== "Shift") setBarcode((prev) => prev + evt.key);
|
||||
|
||||
interval = setInterval(() => setBarcode(""), 20);
|
||||
};
|
||||
|
||||
// Adding the event listener when the component mounts
|
||||
document.addEventListener("keydown", handleKeydown);
|
||||
|
||||
// Cleanup the event listener when the component unmounts
|
||||
return () => {
|
||||
document.removeEventListener("keydown", handleKeydown);
|
||||
if (interval) clearInterval(interval);
|
||||
};
|
||||
}, [barcode]);
|
||||
|
||||
const handleBarcode = (scannedBarcode: string) => {
|
||||
setLastBarcode(scannedBarcode);
|
||||
};
|
||||
|
||||
return lastBarcode;
|
||||
}
|
||||
@@ -1,17 +1,34 @@
|
||||
import { updateRestaurant } from "features/order/orderSlice";
|
||||
import { useEffect } from "react";
|
||||
import { updateRestaurant, clearCart } from "features/order/orderSlice";
|
||||
import { useEffect, useRef } from "react";
|
||||
import { useAppDispatch } from "redux/hooks";
|
||||
import { RestaurantDetails } from "utils/types/appTypes";
|
||||
|
||||
/**
|
||||
* Custom hook to automatically load restaurant into Redux store
|
||||
* when restaurant data is available
|
||||
* Clears the cart when the restaurant (subdomain) changes
|
||||
*/
|
||||
export const useRestaurant = (restaurant: RestaurantDetails | undefined) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const previousRestaurantIdRef = useRef<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (restaurant) {
|
||||
const currentRestaurantId = restaurant.restautantId;
|
||||
|
||||
// Check if restaurant has changed
|
||||
if (
|
||||
previousRestaurantIdRef.current !== null &&
|
||||
previousRestaurantIdRef.current !== currentRestaurantId
|
||||
) {
|
||||
// Restaurant changed, clear the cart
|
||||
dispatch(clearCart());
|
||||
}
|
||||
|
||||
// Update the previous restaurant ID
|
||||
previousRestaurantIdRef.current = currentRestaurantId;
|
||||
|
||||
// Update restaurant in store
|
||||
dispatch(updateRestaurant(restaurant));
|
||||
}
|
||||
}, [restaurant, dispatch]);
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
import { useRef, useState, useCallback } from "react";
|
||||
|
||||
type Props = { isEnabled: boolean; swipeAction: () => void };
|
||||
|
||||
export default function useSwipeUp({ isEnabled, swipeAction }: Props) {
|
||||
// Swipe detection
|
||||
const startYRef = useRef(0);
|
||||
const [isSwiping, setIsSwiping] = useState(false);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
// Touch event handlers for swipe detection
|
||||
const handleTouchStart = useCallback(
|
||||
(e: React.TouchEvent) => {
|
||||
if (!isEnabled) return;
|
||||
startYRef.current = e.touches[0].clientY;
|
||||
setIsSwiping(true);
|
||||
},
|
||||
[isEnabled],
|
||||
);
|
||||
|
||||
/*
|
||||
const handleTouchMove = useCallback(
|
||||
(e: React.TouchEvent) => {
|
||||
if (!isSwiping) return;
|
||||
},
|
||||
[isSwiping],
|
||||
);
|
||||
*/
|
||||
|
||||
const handleTouchEnd = useCallback(
|
||||
(e: React.TouchEvent) => {
|
||||
if (!isSwiping || !isEnabled) return;
|
||||
|
||||
const endY = e.changedTouches[0].clientY;
|
||||
const deltaY = startYRef.current - endY;
|
||||
const threshold = 70; // Threshold to detect a swipe up
|
||||
|
||||
if (deltaY > threshold) {
|
||||
// Swipe up detected
|
||||
swipeAction();
|
||||
}
|
||||
|
||||
setIsSwiping(false);
|
||||
},
|
||||
[isSwiping, isEnabled],
|
||||
);
|
||||
|
||||
return {
|
||||
containerRef,
|
||||
handleTouchStart,
|
||||
handleTouchEnd,
|
||||
};
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
import { useAppSelector } from "redux/hooks";
|
||||
|
||||
export function useTranslations() {
|
||||
const { isRTL } = useAppSelector((state) => state.locale);
|
||||
const nameProp = isRTL ? "arabic_name" : "name";
|
||||
return [nameProp] as const ;
|
||||
}
|
||||
@@ -37,6 +37,9 @@
|
||||
0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px -1px rgba(0, 0, 0, 0.1);
|
||||
--tw-ring-offset-shadow: 0 0 rgba(0, 0, 0, 0);
|
||||
--tw-ring-shadow: 0 0 rgba(0, 0, 0, 0);
|
||||
|
||||
--greylight-hover: rgba(234, 234, 234, 1);
|
||||
--variable-collection-PP: rgba(255, 183, 0, 1);
|
||||
}
|
||||
|
||||
/* Dark theme variables */
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
import { Layout } from 'antd';
|
||||
|
||||
const { Footer } = Layout;
|
||||
|
||||
type FooterNavProps = React.HTMLAttributes<HTMLDivElement>;
|
||||
|
||||
const FooterNav = ({ ...others }: FooterNavProps) => {
|
||||
return (
|
||||
<Footer {...others}>AntD Dashboard © 2023 Created by Design Sparx</Footer>
|
||||
);
|
||||
};
|
||||
|
||||
export default FooterNav;
|
||||
@@ -122,7 +122,18 @@ export default function HeaderMenuDrawer() {
|
||||
style={mobileMenuLinkStyle(locale === item.key)}
|
||||
>
|
||||
{item.icon}
|
||||
<ProText>{item.label.props.children}</ProText>
|
||||
<ProText
|
||||
style={{
|
||||
fontWeight: 500,
|
||||
fontStyle: "Medium",
|
||||
fontSize: 12,
|
||||
lineHeight: "140%",
|
||||
letterSpacing: 0,
|
||||
color: "#333333",
|
||||
}}
|
||||
>
|
||||
{item.label.props.children}
|
||||
</ProText>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
import {
|
||||
BgColorsOutlined,
|
||||
HomeOutlined,
|
||||
LoginOutlined,
|
||||
GlobalOutlined,
|
||||
LogoutOutlined,
|
||||
MenuOutlined,
|
||||
TranslationOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import BranchesIcon from "components/Icons/BranchesIcon";
|
||||
import LoginIcon from "components/Icons/LoginIcon";
|
||||
import LoyaltyAndRewardIcon from "components/Icons/LoyaltyAndRewardIcon";
|
||||
import MyOrderIcon from "components/Icons/MyOrderIcon";
|
||||
import { logoutThunk } from "features/auth/authSlice";
|
||||
import { setLocale, setLocalesThunk } from "features/locale/localeSlice";
|
||||
import { clearCart } from "features/order/orderSlice";
|
||||
import { toggleTheme } from "features/theme/themeSlice";
|
||||
import i18n from "i18n/i18n";
|
||||
import { useTranslation } from "react-i18next";
|
||||
@@ -28,7 +30,7 @@ export default function useHeaderMenu() {
|
||||
{
|
||||
key: "language",
|
||||
icon: (
|
||||
<TranslationOutlined
|
||||
<GlobalOutlined
|
||||
style={{ color: themeName === "dark" ? "white" : "#1f2937" }}
|
||||
/>
|
||||
),
|
||||
@@ -58,38 +60,44 @@ export default function useHeaderMenu() {
|
||||
},
|
||||
{
|
||||
key: "orders",
|
||||
icon: (
|
||||
<HomeOutlined
|
||||
style={{ color: themeName === "dark" ? "white" : "#1f2937" }}
|
||||
/>
|
||||
),
|
||||
icon: <MyOrderIcon />,
|
||||
label: <Link to={`/${subdomain}/orders`}>{t("common.myOrder")}</Link>,
|
||||
onClick: () => {
|
||||
navigate(`/${subdomain}/orders`);
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "branches",
|
||||
icon: (
|
||||
<MenuOutlined
|
||||
style={{ color: themeName === "dark" ? "white" : "#1f2937" }}
|
||||
/>
|
||||
key: "rewardsAndLoyalty",
|
||||
icon: <LoyaltyAndRewardIcon />,
|
||||
label: (
|
||||
<Link to={`/${subdomain}/rewards-and-loyalty`}>
|
||||
{t("common.rewardsAndLoyalty")}
|
||||
</Link>
|
||||
),
|
||||
onClick: () => {
|
||||
navigate(`/${subdomain}/rewards-and-loyalty`);
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "branches",
|
||||
icon: <BranchesIcon />,
|
||||
label: <Link to={`/${subdomain}/menu`}>{t("common.branches")}</Link>,
|
||||
},
|
||||
...(!token ? [{
|
||||
...(!token
|
||||
? [
|
||||
{
|
||||
key: "login",
|
||||
icon: (
|
||||
<LoginOutlined
|
||||
style={{ color: themeName === "dark" ? "white" : "#1f2937" }}
|
||||
/>
|
||||
),
|
||||
icon: <LoginIcon />,
|
||||
label: <Link to={`/${subdomain}/login`}>{t("common.login")}</Link>,
|
||||
onClick: () => {
|
||||
navigate(`/${subdomain}/login`);
|
||||
},
|
||||
}] : []),
|
||||
...(token ? [{
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(token
|
||||
? [
|
||||
{
|
||||
key: "logout",
|
||||
icon: (
|
||||
<LogoutOutlined
|
||||
@@ -99,8 +107,11 @@ export default function useHeaderMenu() {
|
||||
label: <div>{t("common.logout")}</div>,
|
||||
onClick: () => {
|
||||
dispatch(logoutThunk());
|
||||
dispatch(clearCart());
|
||||
},
|
||||
}] : []),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
];
|
||||
return { menuItems };
|
||||
}
|
||||
|
||||
@@ -1,70 +0,0 @@
|
||||
import { BugFilled } from "@ant-design/icons";
|
||||
import { MenuProps } from "antd";
|
||||
import { PATHS } from "utils/constants";
|
||||
|
||||
// import WarehouseIcon from "components/Icons/WarehouseIcon";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
export default function useSidebarItems() {
|
||||
type MenuItem = Required<MenuProps>["items"][number] & {
|
||||
permission: string;
|
||||
children?: MenuItem[];
|
||||
};
|
||||
const { t } = useTranslation();
|
||||
// const [isAuth] = useAuth();
|
||||
const getItem = (
|
||||
label: React.ReactNode,
|
||||
key: React.Key,
|
||||
permission?: string,
|
||||
icon?: React.ReactNode,
|
||||
children?: MenuItem[],
|
||||
type?: "group"
|
||||
): MenuItem => {
|
||||
return {
|
||||
key,
|
||||
icon,
|
||||
children,
|
||||
label,
|
||||
type,
|
||||
permission,
|
||||
} as MenuItem;
|
||||
};
|
||||
|
||||
// Recursive function to filter items based on permissions
|
||||
const getGrantedItems = (items: any[]): MenuItem[] => {
|
||||
return items
|
||||
.filter(() => true)// Filter out items without permission
|
||||
.map((item: any) => {
|
||||
if (item.children) {
|
||||
// Recursively filter children
|
||||
return {
|
||||
...item,
|
||||
children: getGrantedItems(item.children),
|
||||
};
|
||||
}
|
||||
return item;
|
||||
});
|
||||
};
|
||||
|
||||
const items: MenuProps["items"] = [
|
||||
getItem(
|
||||
t("menu"),
|
||||
PATHS.menu,
|
||||
undefined,
|
||||
<div style={{ marginTop: 10 }}>
|
||||
<Link style={{}} to={PATHS.menu}>
|
||||
<BugFilled className="icon-container pos-icon" />
|
||||
</Link>
|
||||
</div>
|
||||
),
|
||||
];
|
||||
|
||||
// if we have a menu with empty children after applying "getGrantedItems"
|
||||
// we going to remove it
|
||||
const grantedItems = getGrantedItems(items).filter(
|
||||
(i) => (i.children && i.children.length !== 0) || !i.children
|
||||
);
|
||||
|
||||
return grantedItems;
|
||||
}
|
||||
@@ -16,6 +16,7 @@ import TimeEstimateCard from "pages/cart/components/timeEstimate/TimeEstimateCar
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import GiftAmountCard from "./components/GiftAmountCard/GiftAmountCard";
|
||||
import { GiftAmountBottomSheet } from "./components/GiftAmountBottomSheet";
|
||||
import PickupTimeCard from "pages/checkout/components/pickupEstimate/TimeEstimateCard";
|
||||
|
||||
export default function CardDetailsPage() {
|
||||
const { t } = useTranslation();
|
||||
@@ -168,7 +169,7 @@ export default function CardDetailsPage() {
|
||||
)}
|
||||
<ReceivernformationCard />
|
||||
<SenderformationCard />
|
||||
<TimeEstimateCard />
|
||||
<PickupTimeCard />
|
||||
</Form>
|
||||
</Layout.Content>
|
||||
<Layout.Footer className={styles.checkoutButtonContainer}>
|
||||
|
||||
@@ -64,9 +64,9 @@ export default function AddressPage() {
|
||||
name="loginForm"
|
||||
form={form}
|
||||
>
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 10 }}>
|
||||
<div style={{ display: "flex", gap: 10 }}>
|
||||
<Form.Item
|
||||
label={t("floor")}
|
||||
name="floor"
|
||||
rules={[{ required: true, message: "" }]}
|
||||
style={{ width: "100%" }}
|
||||
@@ -81,7 +81,6 @@ export default function AddressPage() {
|
||||
/>
|
||||
</Form.Item>{" "}
|
||||
<Form.Item
|
||||
label={t("address.aptNumber")}
|
||||
name="apt"
|
||||
rules={[{ required: true, message: "" }]}
|
||||
style={{ width: "100%" }}
|
||||
@@ -97,7 +96,6 @@ export default function AddressPage() {
|
||||
</Form.Item>
|
||||
</div>
|
||||
<Form.Item
|
||||
label={t("address.street")}
|
||||
name="street"
|
||||
rules={[{ required: true, message: "" }]}
|
||||
>
|
||||
@@ -111,7 +109,6 @@ export default function AddressPage() {
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("address.additionalDirection")}
|
||||
name="additional"
|
||||
rules={[{ required: true, message: "" }]}
|
||||
>
|
||||
@@ -125,10 +122,7 @@ export default function AddressPage() {
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<ProPhoneInput label={t("address.mobileNumber")} propName="phone" />
|
||||
|
||||
<Form.Item
|
||||
label={t("address.addressLabel")}
|
||||
name="addressLabel"
|
||||
rules={[{ required: true, message: "" }]}
|
||||
>
|
||||
@@ -141,6 +135,7 @@ export default function AddressPage() {
|
||||
autoFocus={false}
|
||||
/>
|
||||
</Form.Item>
|
||||
</div>
|
||||
</Form>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
import { Card, Divider, Space, Layout, Button } from "antd";
|
||||
import ArabicPrice from "components/ArabicPrice";
|
||||
import CartActionsButtons from "components/CartActionsButtons/CartActionsButtons.tsx";
|
||||
import ImageWithFallback from "components/ImageWithFallback";
|
||||
import { Card, Divider, Space, Layout, Button, Popconfirm } from "antd";
|
||||
|
||||
import ProHeader from "components/ProHeader/ProHeader.tsx";
|
||||
import ProText from "components/ProText.tsx";
|
||||
import ProTitle from "components/ProTitle.tsx";
|
||||
import { clearCart, selectCart } from "features/order/orderSlice.ts";
|
||||
import styles from "pages/cart/cart.module.css";
|
||||
@@ -20,9 +17,10 @@ import SpecialRequestCard from "pages/cart/components/specialRequest/SpecialRequ
|
||||
import TableNumberCard from "pages/cart/components/TableNumberCard.tsx";
|
||||
import { OrderType } from "pages/checkout/hooks/types";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Variant } from "utils/types/appTypes";
|
||||
import DeleteIcon from "components/Icons/DeleteIcon";
|
||||
import PlusIcon from "components/Icons/PlusIcon";
|
||||
import { GiftItemsCard } from "pages/redeem/components/GiftItemsCard";
|
||||
import ProductChoicesCard from "components/productChoicesCard/ProductChoicesCard.tsx";
|
||||
|
||||
interface CartMobileTabletLayoutProps {
|
||||
form: FormInstance;
|
||||
@@ -35,23 +33,10 @@ export default function CartMobileTabletLayout({
|
||||
const { items, orderType } = useAppSelector(selectCart);
|
||||
const { isRTL } = useAppSelector((state) => state.locale);
|
||||
const { subdomain } = useParams();
|
||||
const { pickup_type } = useAppSelector((state) => state.order.restaurant);
|
||||
const { isMobile, isTablet } = useBreakPoint();
|
||||
const getResponsiveClass = () => (isTablet ? "tablet" : "mobile");
|
||||
const navigate = useNavigate();
|
||||
const dispatch = useAppDispatch();
|
||||
const getMenuItemImageStyle = () => {
|
||||
if (isMobile) {
|
||||
return {
|
||||
width: 115,
|
||||
height: 96,
|
||||
};
|
||||
}
|
||||
return {
|
||||
width: 120,
|
||||
height: 120,
|
||||
};
|
||||
};
|
||||
|
||||
return (
|
||||
<Layout>
|
||||
@@ -68,6 +53,8 @@ export default function CartMobileTabletLayout({
|
||||
{(orderType === OrderType.DineIn ||
|
||||
orderType === OrderType.ToOffice) && <TableNumberCard />}
|
||||
|
||||
{orderType === OrderType.Redeem && <GiftItemsCard isCart={true} />}
|
||||
|
||||
<div className={`${styles.cartContent} ${getResponsiveClass()}`}>
|
||||
<Card className={styles.cartItem}>
|
||||
<div
|
||||
@@ -98,6 +85,17 @@ export default function CartMobileTabletLayout({
|
||||
</ProTitle>
|
||||
</div>
|
||||
|
||||
<Popconfirm
|
||||
title={t("cart.clearCartConfirmation.title")}
|
||||
description={t("cart.clearCartConfirmation.content")}
|
||||
onConfirm={() => {
|
||||
dispatch(clearCart());
|
||||
}}
|
||||
okText={t("cart.clearCartConfirmation.confirm")}
|
||||
cancelText={t("cart.clearCartConfirmation.cancel")}
|
||||
okButtonProps={{ danger: true }}
|
||||
placement={isRTL ? "left" : "right"}
|
||||
>
|
||||
<Button
|
||||
shape="circle"
|
||||
iconPlacement="start"
|
||||
@@ -115,122 +113,19 @@ export default function CartMobileTabletLayout({
|
||||
height: 32,
|
||||
border: "1px solid #DEDEE0",
|
||||
}}
|
||||
onClick={() => {
|
||||
dispatch(clearCart());
|
||||
}}
|
||||
/>
|
||||
</Popconfirm>
|
||||
</div>
|
||||
|
||||
{items.length >= 1 && (
|
||||
<Divider style={{ margin: "8px 0px 12px 0px" }} />
|
||||
)}
|
||||
{items.map((item, index) => (
|
||||
<div key={index} style={{ position: "relative" }}>
|
||||
<Space
|
||||
size="middle"
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
height: "100%",
|
||||
}}
|
||||
>
|
||||
<Space orientation="vertical" size="small">
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
[isRTL ? "right" : "left"]: 0,
|
||||
}}
|
||||
>
|
||||
<ProText
|
||||
style={{
|
||||
margin: 0,
|
||||
lineClamp: 1,
|
||||
fontSize: isMobile ? 14 : isTablet ? 16 : 18,
|
||||
fontWeight: 600,
|
||||
}}
|
||||
>
|
||||
{item.name}{" "}
|
||||
<span
|
||||
style={{
|
||||
fontWeight: 400,
|
||||
}}
|
||||
>
|
||||
{isRTL
|
||||
? (item.variant as Variant)?.optionsAR?.[0]?.value
|
||||
: (item.variant as Variant)?.options?.[0]?.value}
|
||||
</span>
|
||||
</ProText>
|
||||
<br />
|
||||
<ProText
|
||||
type="secondary"
|
||||
className={`${styles.itemDescription} responsive-text`}
|
||||
style={{
|
||||
margin: 0,
|
||||
lineClamp: 1,
|
||||
padding: isMobile ? "3px 0" : isTablet ? 8 : 10,
|
||||
fontSize: isMobile ? 14 : isTablet ? 18 : 20,
|
||||
display: "-webkit-box",
|
||||
WebkitBoxOrient: "vertical",
|
||||
WebkitLineClamp: 1,
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
wordWrap: "break-word",
|
||||
overflowWrap: "break-word",
|
||||
lineHeight: "1.4",
|
||||
maxHeight: isMobile
|
||||
? "3em"
|
||||
: isTablet
|
||||
? "5em"
|
||||
: "7em",
|
||||
fontWeight: 500,
|
||||
letterSpacing: "0.01em",
|
||||
width: "55%",
|
||||
}}
|
||||
>
|
||||
{item.description}
|
||||
</ProText>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
bottom: index !== items.length - 1 ? 16 : 3,
|
||||
[isRTL ? "right" : "left"]: 0,
|
||||
}}
|
||||
>
|
||||
<ArabicPrice
|
||||
price={item.price}
|
||||
style={{ fontStyle: "bold" }}
|
||||
<ProductChoicesCard
|
||||
key={index}
|
||||
product={{ ...item, type: "CartItem" }}
|
||||
addDividerAfter={index !== items.length - 1}
|
||||
/>
|
||||
</div>
|
||||
</Space>
|
||||
<div style={{ position: "relative" }}>
|
||||
<ImageWithFallback
|
||||
src={item.image}
|
||||
alt={item.name}
|
||||
className={`${styles.menuItemImage} responsive-image`}
|
||||
{...getMenuItemImageStyle()}
|
||||
fallbackSrc={
|
||||
"https://fascano-space.s3.me-central-1.amazonaws.com/uploads/restorants/685a8fc884a8c_large.jpg"
|
||||
}
|
||||
/>
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
right: 3,
|
||||
bottom: 3,
|
||||
}}
|
||||
>
|
||||
<CartActionsButtons item={item} />
|
||||
</div>
|
||||
</div>
|
||||
</Space>
|
||||
|
||||
{index !== items.length - 1 && (
|
||||
<Divider style={{ margin: "10px 0" }} />
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
<Button
|
||||
style={{ width: "100%", marginTop: 24, color: "#4C4A58" }}
|
||||
@@ -247,13 +142,13 @@ export default function CartMobileTabletLayout({
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<YouMightAlsoLike />
|
||||
{/* <YouMightAlsoLike /> */}
|
||||
|
||||
<SpecialRequestCard />
|
||||
|
||||
{/* Car Plate*/}
|
||||
{((orderType === OrderType.Pickup && pickup_type === "car") ||
|
||||
orderType === OrderType.ScheduledOrder) && <CarPlateCard />}
|
||||
{/* {((orderType === OrderType.Pickup && pickup_type === "car") ||
|
||||
orderType === OrderType.ScheduledOrder) && <CarPlateCard />} */}
|
||||
|
||||
{/* Estimate Time */}
|
||||
{/* {(orderType === OrderType.Pickup ||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Button, Form, Input, message } from "antd";
|
||||
import { CloseCircleOutlined } from "@ant-design/icons";
|
||||
import { CouponBottomSheet } from "components/CustomBottomSheet/CouponBottomSheet";
|
||||
import { CouponDialog } from "components/CustomBottomSheet/CouponDialog";
|
||||
import CouponHeartIcon from "components/Icons/cart/CouponHeart.tsx";
|
||||
@@ -21,12 +22,14 @@ import { colors } from "ThemeConstants";
|
||||
export default function CouponCard() {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useAppDispatch();
|
||||
const { restaurant } = useAppSelector((state) => state.order);
|
||||
const { restaurant, discount } = useAppSelector((state) => state.order);
|
||||
const { coupon } = useAppSelector(selectCart);
|
||||
const { isDesktop } = useBreakPoint();
|
||||
const [getDiscount] = useGetDiscountMutation();
|
||||
const [isCouponOpen, setIsCouponOpen] = useState(false);
|
||||
|
||||
const isDiscountApplied = discount.value > 0 || discount.isDiscount || discount.isGift;
|
||||
|
||||
const handleCouponSave = (value: string) => {
|
||||
getDiscount({
|
||||
discountCode: value,
|
||||
@@ -52,6 +55,18 @@ export default function CouponCard() {
|
||||
setIsCouponOpen(false);
|
||||
};
|
||||
|
||||
const handleClearDiscount = () => {
|
||||
dispatch(updateCoupon(""));
|
||||
dispatch(
|
||||
updateDiscount({
|
||||
value: 0,
|
||||
isGift: false,
|
||||
isDiscount: false,
|
||||
}),
|
||||
);
|
||||
message.success(t("cart.couponRemoved") || "Coupon removed");
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<ProInputCard
|
||||
@@ -85,10 +100,21 @@ export default function CouponCard() {
|
||||
size="large"
|
||||
autoFocus={false}
|
||||
style={{ padding: "7px 11px", height: 48 }}
|
||||
value={coupon}
|
||||
onChange={(e) => {
|
||||
dispatch(updateCoupon(e.target.value));
|
||||
}}
|
||||
suffix={
|
||||
isDiscountApplied ? (
|
||||
<CloseCircleOutlined
|
||||
onClick={handleClearDiscount}
|
||||
style={{
|
||||
fontSize: 18,
|
||||
color: "#999",
|
||||
cursor: "pointer",
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<Button
|
||||
style={{
|
||||
width: 100,
|
||||
@@ -112,6 +138,7 @@ export default function CouponCard() {
|
||||
{t("cart.apply")}
|
||||
</ProText>
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
@@ -17,7 +17,7 @@ export default function RewardWaiterCard() {
|
||||
const dispatch = useAppDispatch();
|
||||
const { tip } = useAppSelector(selectCart);
|
||||
const { isDesktop } = useBreakPoint();
|
||||
const [selectedTip, setSelectedTip] = useState<number | null>(null);
|
||||
const [selectedTip, setSelectedTip] = useState<number | null>(parseFloat(tip));
|
||||
|
||||
const [isTipOpen, setIsTipOpen] = useState(false);
|
||||
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
.useLoyaltyPointsContainer {
|
||||
gap: 20px;
|
||||
border-radius: 16px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
background-color: var(--secondary-background);
|
||||
width: 100%;
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
import { Button, Form, Switch } from "antd";
|
||||
import ProInputCard from "components/ProInputCard/ProInputCard.tsx";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useAppDispatch, useAppSelector } from "redux/hooks.ts";
|
||||
import { selectCart, updateUseLoyaltyPoints } from "features/order/orderSlice";
|
||||
import ProText from "components/ProText";
|
||||
import styles from "./EarnLoyaltyPointsCard.module.css";
|
||||
|
||||
export default function EarnLoyaltyPointsCard() {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useAppDispatch();
|
||||
const { useLoyaltyPoints } = useAppSelector(selectCart);
|
||||
|
||||
return (
|
||||
<>
|
||||
<ProInputCard title={t("cart.loyalty")}>
|
||||
<Form.Item name="useLoyaltyPoints">
|
||||
<div className={styles.useLoyaltyPointsContainer}>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: 4,
|
||||
width: "100%",
|
||||
}}
|
||||
>
|
||||
<ProText
|
||||
style={{
|
||||
fontWeight: 500,
|
||||
fontStyle: "Medium",
|
||||
fontSize: 14,
|
||||
lineHeight: "140%",
|
||||
letterSpacing: "0%",
|
||||
color: "#333333",
|
||||
}}
|
||||
>
|
||||
{t("cart.useLoyaltyPoints")}
|
||||
</ProText>
|
||||
|
||||
<ProText
|
||||
style={{
|
||||
fontWeight: 400,
|
||||
fontStyle: "Regular",
|
||||
fontSize: 12,
|
||||
lineHeight: "140%",
|
||||
letterSpacing: "0%",
|
||||
color: "#777580",
|
||||
}}
|
||||
>
|
||||
{t(
|
||||
"cart.applyYourAvailableRewardsToGetDiscountsOnItemsInYourCart",
|
||||
)}
|
||||
</ProText>
|
||||
</div>
|
||||
<Switch
|
||||
autoFocus={false}
|
||||
checked={useLoyaltyPoints}
|
||||
onClick={() => {
|
||||
dispatch(updateUseLoyaltyPoints(!useLoyaltyPoints));
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Form.Item>
|
||||
</ProInputCard>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -106,7 +106,7 @@ export default function YouMightAlsoLike() {
|
||||
price: item.price,
|
||||
image: item.image,
|
||||
description: item.description,
|
||||
variant: "None",
|
||||
variant: undefined,
|
||||
isHasLoyalty: item.isHasLoyalty,
|
||||
no_of_stamps_give: item.no_of_stamps_give,
|
||||
},
|
||||
|
||||
@@ -106,6 +106,8 @@ export const AddressSummary = () => {
|
||||
return null;
|
||||
}
|
||||
|
||||
console.log(location);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Card
|
||||
|
||||
@@ -7,6 +7,8 @@ import CarRatioGroups from "./CarRatioGroups/CarRatioGroups";
|
||||
import PlusIcon from "components/Icons/PlusIcon";
|
||||
import styles from "../checkout.module.css";
|
||||
import { AddCarBottomSheet } from "components/CustomBottomSheet/AddCarBottomSheet";
|
||||
import { updatePlateCar } from "features/order/orderSlice";
|
||||
import { useAppDispatch } from "redux/hooks";
|
||||
|
||||
interface CarBottomSheetProps {
|
||||
isOpen: boolean;
|
||||
@@ -17,6 +19,7 @@ export function CarBottomSheet({ isOpen, onClose }: CarBottomSheetProps) {
|
||||
const { t } = useTranslation();
|
||||
const [value, setValue] = useState<string | null>(null);
|
||||
const [isAddCarOpen, setIsAddCarOpen] = useState(false);
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const handleCancel = () => {
|
||||
setValue(null);
|
||||
@@ -24,8 +27,8 @@ export function CarBottomSheet({ isOpen, onClose }: CarBottomSheetProps) {
|
||||
};
|
||||
|
||||
const handleSave = () => {
|
||||
dispatch(updatePlateCar(value || ""));
|
||||
onClose();
|
||||
setValue(value);
|
||||
};
|
||||
|
||||
const handleAddCarClick = () => {
|
||||
@@ -38,9 +41,8 @@ export function CarBottomSheet({ isOpen, onClose }: CarBottomSheetProps) {
|
||||
// The parent component should handle reopening, but we'll ensure state is correct
|
||||
};
|
||||
|
||||
const handleAddCarSave = (carDetails: any) => {
|
||||
// Handle saving the new car details
|
||||
console.log("Car details saved:", carDetails);
|
||||
const handleAddCarSave = () => {
|
||||
dispatch(updatePlateCar(value || ""));
|
||||
// After saving, close AddCarBottomSheet which will trigger reopening CarBottomSheet
|
||||
setIsAddCarOpen(false);
|
||||
};
|
||||
|
||||
@@ -13,11 +13,15 @@ export function CarCard() {
|
||||
const { t } = useTranslation();
|
||||
const { isRTL } = useAppSelector((state) => state.locale);
|
||||
const [isCarBottomSheetOpen, setIsCarBottomSheetOpen] = useState(false);
|
||||
const { plateCar } = useAppSelector((state) => state.order);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Card onClick={() => {
|
||||
<Card
|
||||
onClick={() => {
|
||||
setIsCarBottomSheetOpen(true);
|
||||
}}>
|
||||
}}
|
||||
>
|
||||
<div className={styles.carCard}>
|
||||
<Button
|
||||
shape="circle"
|
||||
@@ -54,6 +58,44 @@ export function CarCard() {
|
||||
{t("checkout.addCarDetails")}
|
||||
</ProText>
|
||||
|
||||
{plateCar ? (
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 10,
|
||||
marginTop: 6,
|
||||
}}
|
||||
>
|
||||
<ProText
|
||||
style={{
|
||||
fontWeight: 400,
|
||||
fontStyle: "Regular",
|
||||
fontSize: 16,
|
||||
lineHeight: "140%",
|
||||
color: "#777580",
|
||||
}}
|
||||
>{t("checkout.carPlateNumber")}:</ProText>
|
||||
<div
|
||||
style={{
|
||||
backgroundColor: "#FFC600",
|
||||
color: "#333333",
|
||||
padding: "3px 14px",
|
||||
borderRadius: "20px",
|
||||
fontSize: 15,
|
||||
fontWeight: 600,
|
||||
fontStyle: "SemiBold",
|
||||
letterSpacing: "0.8px",
|
||||
display: "inline-flex",
|
||||
alignItems: "center",
|
||||
boxShadow: "0 2px 6px rgba(255, 198, 0, 0.25)",
|
||||
fontFamily: "monospace",
|
||||
}}
|
||||
>
|
||||
{plateCar}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<ProText
|
||||
style={{
|
||||
fontWeight: 400,
|
||||
@@ -65,6 +107,7 @@ export function CarCard() {
|
||||
>
|
||||
{t("checkout.soTheRestaurantCanRecognizeYourCarWhenYouArrive")}
|
||||
</ProText>
|
||||
)}
|
||||
</div>
|
||||
{isRTL ? <BackIcon iconSize={24} /> : <NextIcon iconSize={24} />}
|
||||
</div>
|
||||
|
||||
@@ -24,9 +24,7 @@ const CarRatioGroups = ({
|
||||
showDivider = false,
|
||||
...props
|
||||
}: CarRatioGroupsProps) => {
|
||||
const { t } = useTranslation();
|
||||
const handleChange = (e: RadioChangeEvent) => {
|
||||
console.log(e.target.value);
|
||||
// If onChange is provided (from Form.Item), use it
|
||||
if (onChange) {
|
||||
onChange(e);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Button, FormInstance, Layout } from "antd";
|
||||
import { Button, FormInstance, Layout, message } from "antd";
|
||||
import { selectCart, updateSplitBillAmount } from "features/order/orderSlice";
|
||||
import { OrderType } from "pages/checkout/hooks/types.ts";
|
||||
import { useCallback, useMemo, useState } from "react";
|
||||
@@ -10,14 +10,17 @@ import { EqualltyChoiceBottomSheet } from "pages/pay/components/splitBill/Equall
|
||||
import { SplitBillChoiceBottomSheet } from "pages/pay/components/splitBill/SplitBillChoiceBottomSheet";
|
||||
import { CustomAmountChoiceBottomSheet } from "pages/pay/components/splitBill/CustomAmountChoiceBottomSheet";
|
||||
import { PayForYourItemsChoiceBottomSheet } from "pages/pay/components/splitBill/PayForYourItemsChoiceBottomSheet";
|
||||
import useGidtAmount from "../hooks/useGidtAmount";
|
||||
import { GiftType } from "components/CustomBottomSheet/GiftTypeBottomSheet";
|
||||
|
||||
type SplitWay = "customAmount" | "equality" | "payForItems" | null;
|
||||
|
||||
export default function CheckoutButton({ form }: { form: FormInstance }) {
|
||||
const dispatch = useAppDispatch();
|
||||
const { t } = useTranslation();
|
||||
const { orderType } = useAppSelector(selectCart);
|
||||
const { orderType, giftDetails, items } = useAppSelector(selectCart);
|
||||
const { handleCreateOrder } = useOrder();
|
||||
const { handleCreateGiftAmount } = useGidtAmount();
|
||||
const [selectedSplitWay, setSelectedSplitWay] = useState<SplitWay>(null);
|
||||
const [
|
||||
isSplitBillChoiceBottomSheetOpen,
|
||||
@@ -47,11 +50,22 @@ export default function CheckoutButton({ form }: { form: FormInstance }) {
|
||||
const handlePlaceOrderClick = useCallback(async () => {
|
||||
try {
|
||||
await form.validateFields();
|
||||
if (
|
||||
orderType === OrderType.Gift &&
|
||||
giftDetails?.giftType === GiftType.Vouchers
|
||||
) {
|
||||
handleCreateGiftAmount();
|
||||
} else {
|
||||
if (items.length > 0) {
|
||||
handleCreateOrder();
|
||||
} else {
|
||||
message.error(t("checkout.noItems"));
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
}, [handleCreateOrder, form]);
|
||||
}, [handleCreateOrder, handleCreateGiftAmount, form, orderType, giftDetails]);
|
||||
|
||||
const shouldShowSplitBill = useMemo(
|
||||
() => orderType === OrderType.DineIn,
|
||||
|
||||
@@ -15,6 +15,7 @@ import { useMemo, useState } from "react";
|
||||
import CardAmountIcon from "components/Icons/CardAmountIcon";
|
||||
import ArabicPrice from "components/ArabicPrice";
|
||||
import { GiftAmountBottomSheet } from "pages/CardDetails/components/GiftAmountBottomSheet";
|
||||
import { GiftType } from "components/CustomBottomSheet/GiftTypeBottomSheet";
|
||||
|
||||
export function GiftCard() {
|
||||
const { t } = useTranslation();
|
||||
@@ -32,6 +33,8 @@ export function GiftCard() {
|
||||
const [isGiftAmountBottomSheetOpen, setIsGiftAmountBottomSheetOpen] =
|
||||
useState(false);
|
||||
|
||||
console.log(giftDetails?.giftType);
|
||||
|
||||
return (
|
||||
<>
|
||||
<ProInputCard title={t("address.giftDetails")}>
|
||||
@@ -108,10 +111,12 @@ export function GiftCard() {
|
||||
</div>
|
||||
{isRTL ? <BackIcon iconSize={24} /> : <NextIcon iconSize={24} />}
|
||||
</div>
|
||||
|
||||
<Divider style={{ margin: "10px 0" }} />
|
||||
|
||||
{giftDetails?.giftType === "vouchers" ||
|
||||
(giftDetails?.giftType === "itemsAndVouchers" && (
|
||||
{(giftDetails?.giftType === GiftType.Vouchers ||
|
||||
giftDetails?.giftType === GiftType.ItemsAndVouchers) && (
|
||||
<>
|
||||
<div
|
||||
className={styles.orderNotes}
|
||||
onClick={() => {
|
||||
@@ -164,11 +169,14 @@ export function GiftCard() {
|
||||
</div>
|
||||
{isRTL ? <BackIcon iconSize={24} /> : <NextIcon iconSize={24} />}
|
||||
</div>
|
||||
))}
|
||||
|
||||
{giftDetails?.giftType === GiftType.ItemsAndVouchers && (
|
||||
<Divider style={{ margin: "10px 0" }} />
|
||||
{giftDetails?.giftType === "items" ||
|
||||
(giftDetails?.giftType === "itemsAndVouchers" && (
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{(giftDetails?.giftType === GiftType.Items ||
|
||||
giftDetails?.giftType === GiftType.ItemsAndVouchers) && (
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
@@ -203,7 +211,7 @@ export function GiftCard() {
|
||||
</ProText>
|
||||
{isRTL ? <BackIcon /> : <NextIcon />}
|
||||
</div>
|
||||
))}
|
||||
)}
|
||||
</ProInputCard>
|
||||
<GiftAmountBottomSheet
|
||||
isOpen={isGiftAmountBottomSheetOpen}
|
||||
|
||||
@@ -82,7 +82,7 @@ export default function PickupEstimateContent({
|
||||
let hour = nextHour;
|
||||
let minute = nextMinute;
|
||||
|
||||
for (let i = 1; i < 48; i++) {
|
||||
for (let i = 0; i < 96; i++) {
|
||||
const time = dayjs().hour(hour).minute(minute).second(0);
|
||||
const formatted = time.format("h:mm A");
|
||||
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
import { Variant } from "pages/orders/types";
|
||||
import { Extra3 } from "utils/types/appTypes";
|
||||
|
||||
export interface OrderDetails {
|
||||
orderItems: OrderItem[];
|
||||
order: Order;
|
||||
@@ -18,7 +15,18 @@ export interface OrderDetails {
|
||||
itemsImagePrefix: string;
|
||||
}
|
||||
|
||||
export interface OrderItem {
|
||||
type OrderVariant = {
|
||||
id: number;
|
||||
price: number;
|
||||
options: string;
|
||||
local_name: string;
|
||||
optionsArray: { id: number; name: string }[];
|
||||
OptionsList: string;
|
||||
extras: any[];
|
||||
};
|
||||
|
||||
export type OrderItem = {
|
||||
type: "OrderItem";
|
||||
id: number;
|
||||
is_loyalty_used: number;
|
||||
no_of_stamps_give: number;
|
||||
@@ -31,16 +39,19 @@ export interface OrderItem {
|
||||
image: string;
|
||||
imageName: string;
|
||||
variantName: string;
|
||||
variantLocalName?: string;
|
||||
variantLocalName: string;
|
||||
extras: any[];
|
||||
descriptionEN: string;
|
||||
descriptionAR: string;
|
||||
itemline: string;
|
||||
itemlineAR: string;
|
||||
itemlineAREN: string;
|
||||
extrasgroups: any[];
|
||||
extrasgroups: string[];
|
||||
extragroupnew: any[];
|
||||
itemComment: string;
|
||||
variant?: Variant;
|
||||
variant: OrderVariant;
|
||||
itemExtras: any[];
|
||||
AvaiilableVariantExtras: Extra3[];
|
||||
AvaiilableVariantExtras: any[];
|
||||
isPrinted: number;
|
||||
category_id: number;
|
||||
pos_order_id: string;
|
||||
@@ -56,7 +67,7 @@ export interface OrderItem {
|
||||
pricing_method: string;
|
||||
is_already_paid: number;
|
||||
hash_item: string;
|
||||
}
|
||||
};
|
||||
|
||||
export interface Order {
|
||||
id: number;
|
||||
@@ -246,5 +257,6 @@ export enum OrderType {
|
||||
ToRoom = "room",
|
||||
ToOffice = "office",
|
||||
Booking = "booking",
|
||||
Pay = "pay"
|
||||
Pay = "pay",
|
||||
Redeem = "redeem",
|
||||
}
|
||||
|
||||
82
src/pages/checkout/hooks/useGidtAmount.ts
Normal file
82
src/pages/checkout/hooks/useGidtAmount.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
import { message } from "antd";
|
||||
import {
|
||||
clearCart,
|
||||
selectCart,
|
||||
selectGrandTotal,
|
||||
} from "features/order/orderSlice";
|
||||
import { useCallback } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import { useCreateGiftAmountMutation } from "redux/api/others";
|
||||
import { useAppDispatch, useAppSelector } from "redux/hooks";
|
||||
import { PAYMENT_CONFIRMATION_URL } from "utils/constants";
|
||||
|
||||
export default function useGidtAmount() {
|
||||
const dispatch = useAppDispatch();
|
||||
const navigate = useNavigate();
|
||||
const { t } = useTranslation();
|
||||
const { subdomain } = useParams();
|
||||
const restaurantID = localStorage.getItem("restaurantID");
|
||||
const { giftDetails } = useAppSelector(selectCart);
|
||||
|
||||
const orderPrice = useAppSelector(selectGrandTotal);
|
||||
|
||||
const [createGiftAmount] = useCreateGiftAmountMutation();
|
||||
|
||||
const handleCreateGiftAmount = useCallback(() => {
|
||||
const loadingMessageKey = "create-order-loader";
|
||||
message.loading({
|
||||
content: t("order.creatingOrder", {
|
||||
defaultValue: "Creating order...",
|
||||
}),
|
||||
key: loadingMessageKey,
|
||||
duration: 0,
|
||||
});
|
||||
|
||||
createGiftAmount({
|
||||
sender_name: giftDetails?.senderName,
|
||||
sender_phone: giftDetails?.senderPhone,
|
||||
sender_email: giftDetails?.senderEmail,
|
||||
receiver_name: giftDetails?.receiverName,
|
||||
receiver_phone: giftDetails?.receiverPhone,
|
||||
special_message: giftDetails?.message,
|
||||
keep_name_secret: giftDetails?.isSecret,
|
||||
restaurant_id: restaurantID,
|
||||
order_amount: orderPrice,
|
||||
gift_card_id: giftDetails?.cardId,
|
||||
}).then((res: unknown) => {
|
||||
message.destroy(loadingMessageKey);
|
||||
const mutationResult = res as {
|
||||
data?: { result?: { orderID?: string } };
|
||||
error?: { data?: { message?: string } };
|
||||
};
|
||||
if (mutationResult.error)
|
||||
message.error(
|
||||
mutationResult.error.data?.message || t("order.createOrderFailed"),
|
||||
);
|
||||
else {
|
||||
const redirectMessageKey = "order-redirect-loader";
|
||||
|
||||
message.loading({
|
||||
content: t("order.redirectingToPayment", {
|
||||
defaultValue: "Redirecting to payment...",
|
||||
}),
|
||||
key: redirectMessageKey,
|
||||
duration: 0,
|
||||
});
|
||||
window.location.href = `${PAYMENT_CONFIRMATION_URL}/${mutationResult.data?.result?.orderID}`;
|
||||
dispatch(clearCart());
|
||||
}
|
||||
});
|
||||
}, [
|
||||
createGiftAmount,
|
||||
orderPrice,
|
||||
giftDetails,
|
||||
restaurantID,
|
||||
t,
|
||||
navigate,
|
||||
subdomain,
|
||||
dispatch,
|
||||
]);
|
||||
return { handleCreateGiftAmount };
|
||||
}
|
||||
@@ -44,7 +44,7 @@ export default function useOrder() {
|
||||
giftDetails,
|
||||
order,
|
||||
restaurant,
|
||||
} = useAppSelector(selectCart);
|
||||
} = useAppSelector((state) => state.order);
|
||||
const highestLoyaltyItem = useAppSelector(selectHighestPricedLoyaltyItem);
|
||||
const { useLoyaltyPoints } = useAppSelector(selectCart);
|
||||
|
||||
@@ -93,8 +93,8 @@ export default function useOrder() {
|
||||
order_item_comment: i.comment || "",
|
||||
variant: (i.variant as Variant)?.id || "",
|
||||
})),
|
||||
office_no: order?.officeNumber || "",
|
||||
room_no: order?.roomNumber || "",
|
||||
office_no: order?.officeNumber,
|
||||
room_no: order?.roomNumber,
|
||||
...(discount.isDiscount ? { couponID: coupon } : {}),
|
||||
...(discount.isGift ? { discountGiftCode: coupon } : {}),
|
||||
discountAmount: discountAmount || 0,
|
||||
@@ -115,14 +115,15 @@ export default function useOrder() {
|
||||
? `${location?.lat},${location?.lng}`
|
||||
: "",
|
||||
delivery_address: location?.address,
|
||||
vatvalue: restaurant?.vat || 0,
|
||||
vatvalue: ((restaurant?.vat || 0) / 100) * (subtotal - discountAmount),
|
||||
taxes:
|
||||
restaurant?.taxes?.map((t) => ({
|
||||
restaurant?.taxes
|
||||
?.filter((t) => t.is_active === 1)
|
||||
.map((t) => ({
|
||||
tax_id: t.id,
|
||||
percentage: t.percentage,
|
||||
amount:
|
||||
((typeof t.percentage === "number" ? t.percentage : 0) as number) *
|
||||
Number(subtotal - discountAmount),
|
||||
((Number(t.percentage) || 0) / 100) * (subtotal - discountAmount),
|
||||
})) || [],
|
||||
...(orderType === OrderType.Gift
|
||||
? {
|
||||
@@ -149,8 +150,9 @@ export default function useOrder() {
|
||||
);
|
||||
else {
|
||||
const redirectMessageKey = "order-redirect-loader";
|
||||
|
||||
if (
|
||||
orderType === OrderType.Gift &&
|
||||
localStorage.getItem("fascano_payment_method")?.toString() === '"thawani"' &&
|
||||
mutationResult.data?.result?.orderID
|
||||
) {
|
||||
message.loading({
|
||||
@@ -201,6 +203,14 @@ export default function useOrder() {
|
||||
location?.lat,
|
||||
location?.lng,
|
||||
location?.address,
|
||||
order,
|
||||
pickupTime,
|
||||
pickupDate,
|
||||
plateCar,
|
||||
pickupType,
|
||||
discountAmount,
|
||||
subtotal,
|
||||
restaurant,
|
||||
t,
|
||||
navigate,
|
||||
subdomain,
|
||||
|
||||
@@ -1,38 +1,51 @@
|
||||
import { Flex, Form, Layout } from "antd";
|
||||
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, updateCollectionMethod } from "features/order/orderSlice";
|
||||
import { selectCart } from "features/order/orderSlice";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useAppDispatch, useAppSelector } from "redux/hooks";
|
||||
import { useAppSelector } from "redux/hooks";
|
||||
import styles from "../address/address.module.css";
|
||||
import { AddressSummary } from "./components/AddressSummary";
|
||||
import CheckoutButton from "./components/CheckoutButton";
|
||||
import { GiftCard } from "./components/GiftCard";
|
||||
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";
|
||||
import BriefMenuCard from "./components/BriefMenuCard";
|
||||
import CustomerInformationCard from "./components/CustomerInformationCard";
|
||||
import Ads1 from "components/Ads/Ads1";
|
||||
import { useEffect } from "react";
|
||||
import { CarCard } from "./components/CarCard";
|
||||
import { CollectWay } from "./components/CollectWay/CollectWay";
|
||||
import PickupTimeCard from "./components/pickupEstimate/TimeEstimateCard";
|
||||
import VoucherSummary from "pages/redeem/components/VoucherSummary/VoucherSummary";
|
||||
import EarnLoyaltyPointsCard from "pages/cart/components/earnLoyaltyPointsCard/EarnLoyaltyPointsCard";
|
||||
import { Form, Layout } from "antd";
|
||||
|
||||
export default function CheckoutPage() {
|
||||
const { t } = useTranslation();
|
||||
const [form] = Form.useForm();
|
||||
const { phone, order, orderType, collectionMethod, coupon, customerName } =
|
||||
useAppSelector(selectCart);
|
||||
const {
|
||||
phone,
|
||||
order,
|
||||
orderType,
|
||||
collectionMethod,
|
||||
coupon,
|
||||
customerName,
|
||||
tip,
|
||||
restaurant,
|
||||
} = useAppSelector(selectCart);
|
||||
const { token } = useAppSelector((state) => state.auth);
|
||||
const dispatch = useAppDispatch();
|
||||
useEffect(() => {
|
||||
form.setFieldsValue({ coupon, collectionMethod, phone, customerName });
|
||||
}, [form, phone, coupon, collectionMethod, customerName]);
|
||||
form.setFieldsValue({
|
||||
coupon,
|
||||
collectionMethod,
|
||||
phone,
|
||||
customerName,
|
||||
tip,
|
||||
officeNumber: order?.officeNumber,
|
||||
roomNumber: order?.roomNumber,
|
||||
});
|
||||
}, [form, phone, coupon, collectionMethod, customerName, tip]);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -61,6 +74,8 @@ export default function CheckoutPage() {
|
||||
name="roomNumber"
|
||||
placeholder={t("address.roomNo")}
|
||||
value={order?.roomNumber}
|
||||
required
|
||||
reuireqMessage={t("address.pleaseEnterRoomNumber")}
|
||||
/>
|
||||
)}
|
||||
{orderType === OrderType.ToOffice && (
|
||||
@@ -69,17 +84,22 @@ export default function CheckoutPage() {
|
||||
name="officeNumber"
|
||||
placeholder={t("address.officeNo")}
|
||||
value={order?.officeNumber}
|
||||
required
|
||||
reuireqMessage={t("address.pleaseEnterOfficeNumber")}
|
||||
/>
|
||||
)}
|
||||
{orderType === OrderType.Redeem && <VoucherSummary />}
|
||||
{/* {orderType === OrderType.Gift && <GiftCard />} */}
|
||||
{/* <RoomDetails />
|
||||
<OfficeDetails /> */}
|
||||
{/* <GiftDetails /> */}
|
||||
{/* <BriefMenu /> */}
|
||||
{orderType !== OrderType.Redeem && orderType !== OrderType.Gift && (
|
||||
<CouponCard />
|
||||
)}
|
||||
|
||||
{/* Collection Method */}
|
||||
{orderType === OrderType.Pickup && (
|
||||
{/* {orderType === OrderType.Pickup && (
|
||||
<ProInputCard title={t("cart.collectionMethod")}>
|
||||
<Form.Item
|
||||
name="collectionMethod"
|
||||
@@ -111,12 +131,21 @@ export default function CheckoutPage() {
|
||||
/>
|
||||
</Form.Item>
|
||||
</ProInputCard>
|
||||
)}
|
||||
)} */}
|
||||
|
||||
{/* Reward Your Waiter */}
|
||||
{/* {orderType !== OrderType.Redeem && orderType !== OrderType.Gift && (
|
||||
<RewardWaiterCard />
|
||||
)} */}
|
||||
{orderType !== OrderType.Redeem &&
|
||||
orderType !== OrderType.Gift &&
|
||||
restaurant?.is_loyalty_enabled === 1 &&
|
||||
(restaurant?.customer_loyalty_points ?? 0) >
|
||||
(restaurant?.loyalty_stamps ?? 0) ? (
|
||||
<EarnLoyaltyPointsCard />
|
||||
) : null}
|
||||
<BriefMenuCard />
|
||||
<Ads1 />
|
||||
{/* <Ads1 /> */}
|
||||
<OrderSummary />
|
||||
</Layout.Content>
|
||||
|
||||
|
||||
@@ -13,13 +13,14 @@ import "react-phone-input-2/lib/style.css";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import { useSendOtpMutation } from "redux/api/auth";
|
||||
import { useGetRestaurantDetailsQuery } from "redux/api/others";
|
||||
import { useAppSelector } from "redux/hooks";
|
||||
import { useAppDispatch, useAppSelector } from "redux/hooks";
|
||||
import { colors, DisabledColor, ProGray1 } from "ThemeConstants";
|
||||
import { default_image } from "utils/constants";
|
||||
import styles from "./login.module.css";
|
||||
import { Layout } from "antd";
|
||||
import ProHeader from "components/ProHeader/ProHeader";
|
||||
import useBreakPoint from "hooks/useBreakPoint";
|
||||
import { updateCustomerName } from "features/order/orderSlice";
|
||||
|
||||
export default function LoginPage() {
|
||||
const { t } = useTranslation();
|
||||
@@ -32,7 +33,7 @@ export default function LoginPage() {
|
||||
skip: !subdomain,
|
||||
});
|
||||
const { isTablet } = useBreakPoint();
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
// const [phone, setPhone] = useState<string>("");
|
||||
const [selectedDate, setSelectedDate] = useState<string>("");
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
@@ -113,6 +114,10 @@ export default function LoginPage() {
|
||||
height: 50,
|
||||
fontSize: 14,
|
||||
}}
|
||||
onBlur={(e) => {
|
||||
dispatch(updateCustomerName(e.target.value));
|
||||
}}
|
||||
disabled={isLoading}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ export function AddToCartButton({ item }: { item: Product }) {
|
||||
// Find basic cart item (no variants/extras) - the one added by quick add
|
||||
const basicCartItem = cartItemsForProduct.find(
|
||||
(cartItem) =>
|
||||
(!cartItem.variant || cartItem.variant === "None") &&
|
||||
!cartItem.variant &&
|
||||
(!cartItem.extras || cartItem.extras.length === 0) &&
|
||||
(!cartItem.extrasgroupnew || cartItem.extrasgroupnew.length === 0),
|
||||
);
|
||||
@@ -67,7 +67,7 @@ export function AddToCartButton({ item }: { item: Product }) {
|
||||
price: item.price,
|
||||
image: item.image,
|
||||
description: item.description,
|
||||
variant: "None",
|
||||
variant: undefined,
|
||||
isHasLoyalty: item.isHasLoyalty,
|
||||
no_of_stamps_give: item.no_of_stamps_give,
|
||||
},
|
||||
@@ -145,7 +145,7 @@ export function AddToCartButton({ item }: { item: Product }) {
|
||||
price: item.price,
|
||||
image: item.image,
|
||||
description: item.description,
|
||||
variant: "None",
|
||||
variant: undefined,
|
||||
isHasLoyalty: item.isHasLoyalty,
|
||||
no_of_stamps_give: item.no_of_stamps_give,
|
||||
},
|
||||
|
||||
@@ -1,15 +1,21 @@
|
||||
import { Button } from "antd";
|
||||
import BackIcon from "components/Icons/BackIcon";
|
||||
import NextIcon from "components/Icons/NextIcon";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useAppSelector } from "redux/hooks";
|
||||
|
||||
interface BackButtonProps {
|
||||
navigateBack?: boolean; // true = use router.back(), false = just clear state
|
||||
customRoute?: string;
|
||||
}
|
||||
|
||||
export default function BackButton({ navigateBack = true }: BackButtonProps) {
|
||||
export default function BackButton({ customRoute }: BackButtonProps) {
|
||||
const router = useNavigate();
|
||||
const handleBack = () => {
|
||||
if (navigateBack) window.history.back();
|
||||
if (customRoute) {
|
||||
router(customRoute);
|
||||
} else {
|
||||
router(-1);
|
||||
}
|
||||
};
|
||||
const { isRTL } = useAppSelector((state) => state.locale);
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@ import ProText from "components/ProText";
|
||||
import { useScrollHandler } from "contexts/ScrollHandlerContext";
|
||||
import { useCallback, useEffect, useRef } from "react";
|
||||
import { useAppSelector } from "redux/hooks";
|
||||
import { colors } from "ThemeConstants";
|
||||
import { default_image } from "utils/constants";
|
||||
import { Category } from "utils/types/appTypes";
|
||||
import styles from "./CategoriesList.module.css";
|
||||
|
||||
@@ -44,13 +44,7 @@ export function MenuFooter() {
|
||||
}}
|
||||
>
|
||||
<Link
|
||||
to={
|
||||
totalItems === 0
|
||||
? "#"
|
||||
: orderType === OrderType.Pay
|
||||
? `/${subdomain}/pay`
|
||||
: `/${subdomain}/cart`
|
||||
}
|
||||
to={totalItems === 0 ? "#" : `/${subdomain}/cart`}
|
||||
onClick={(e) => {
|
||||
if (totalItems === 0) {
|
||||
e.preventDefault();
|
||||
@@ -106,9 +100,7 @@ export function MenuFooter() {
|
||||
margin: "0 10px",
|
||||
}}
|
||||
>
|
||||
{orderType === OrderType.Pay
|
||||
? t("menu.pay")
|
||||
: t("menu.viewCart")}
|
||||
{t("menu.viewCart")}
|
||||
</ProText>
|
||||
</Button>
|
||||
<div
|
||||
|
||||
@@ -8,6 +8,7 @@ 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";
|
||||
import { Category } from "utils/types/appTypes";
|
||||
|
||||
interface MenuListProps {
|
||||
data:
|
||||
@@ -54,7 +55,7 @@ export function MenuList({ data, categoryRefs }: MenuListProps) {
|
||||
return (
|
||||
<>
|
||||
<div className={styles.menuSections}>
|
||||
{data?.categories?.map((category, index) => {
|
||||
{data?.categories?.map((category: Category, index) => {
|
||||
const categoryProducts = productsByCategory?.[category.id] || [];
|
||||
if (categoryProducts.length === 0) return null;
|
||||
|
||||
@@ -81,7 +82,7 @@ export function MenuList({ data, categoryRefs }: MenuListProps) {
|
||||
}}
|
||||
level={5}
|
||||
>
|
||||
{isRTL ? category.name : category.name}
|
||||
{isRTL ? category.nameAR : category.nameEN}
|
||||
</ProTitle>
|
||||
)}
|
||||
<div className={styles.menuItemsGrid}>
|
||||
|
||||
@@ -19,7 +19,7 @@ type 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 { items, restaurant } = useAppSelector((state) => state.order);
|
||||
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
||||
// const navigate = useNavigate();
|
||||
// const { subdomain } = useParams();
|
||||
@@ -37,10 +37,7 @@ export default function ProductCard({ item, setIsBottomSheetOpen }: Props) {
|
||||
if (isDesktop) {
|
||||
setIsDialogOpen(true);
|
||||
}
|
||||
setIsBottomSheetOpen(true);
|
||||
// else {
|
||||
// navigate(`/${subdomain}/product/${item.id}`);
|
||||
// }
|
||||
if (restaurant?.isOpened) setIsBottomSheetOpen(true);
|
||||
};
|
||||
|
||||
const hasOptions =
|
||||
@@ -104,7 +101,7 @@ export default function ProductCard({ item, setIsBottomSheetOpen }: Props) {
|
||||
letterSpacing: "0%",
|
||||
}}
|
||||
>
|
||||
{isRTL ? item.nameOther : item.name}
|
||||
{isRTL ? item.nameAR : item.nameEN}
|
||||
</ProText>
|
||||
{item.description && (
|
||||
<ProText
|
||||
@@ -127,7 +124,7 @@ export default function ProductCard({ item, setIsBottomSheetOpen }: Props) {
|
||||
letterSpacing: "0%",
|
||||
}}
|
||||
>
|
||||
{item.description}
|
||||
{isRTL ? item.descriptionAR : item.descriptionEN}
|
||||
</ProText>
|
||||
)}
|
||||
|
||||
|
||||
@@ -483,9 +483,6 @@
|
||||
position: absolute;
|
||||
z-index: 999;
|
||||
top: 70px;
|
||||
opacity: 0.9;
|
||||
background: #aaa8a833;
|
||||
backdrop-filter: blur(40px);
|
||||
}
|
||||
|
||||
.backButtonContainer {
|
||||
@@ -802,3 +799,66 @@
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
.frame {
|
||||
align-items: flex-start;
|
||||
background-color: #aaa7a733;
|
||||
border-radius: 60px;
|
||||
display: inline-flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
padding: 8px 10px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.div {
|
||||
align-items: center;
|
||||
align-self: stretch;
|
||||
display: flex;
|
||||
flex: 0 0 auto;
|
||||
gap: 4px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.pickup {
|
||||
color: #ffffff;
|
||||
font-family: "Roboto-Medium", Helvetica;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0;
|
||||
line-height: normal;
|
||||
margin-top: -1px;
|
||||
position: relative;
|
||||
white-space: nowrap;
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
.elementMin {
|
||||
color: var(--greylight-hover);
|
||||
font-family: "Roboto-Regular", Helvetica;
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
letter-spacing: 0;
|
||||
line-height: normal;
|
||||
position: relative;
|
||||
white-space: nowrap;
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
.frameSelect {
|
||||
align-items: flex-start;
|
||||
background-color: #aaa7a733;
|
||||
border-radius: 60px;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.divSelect {
|
||||
align-items: center;
|
||||
align-self: stretch;
|
||||
display: flex;
|
||||
flex: 0 0 auto;
|
||||
gap: 4px;
|
||||
position: relative;
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { StarFilled } from "@ant-design/icons";
|
||||
import { Button, Image, Select, Space } from "antd";
|
||||
import { Button, Image, message, Select, Space } from "antd";
|
||||
import { FloatingButton } from "components/FloatingButton/FloatingButton";
|
||||
import LogoContainerIcon from "components/Icons/LogoContainerIcon";
|
||||
import ImageWithFallback from "components/ImageWithFallback";
|
||||
@@ -12,6 +12,7 @@ import { OrderType } from "pages/checkout/hooks/types.ts";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useParams } from "react-router-dom";
|
||||
import {
|
||||
useCallWaiterMutation,
|
||||
useGetMenuQuery,
|
||||
useGetRestaurantDetailsQuery,
|
||||
} from "redux/api/others";
|
||||
@@ -37,7 +38,8 @@ import { OrderTypesBottomSheet } from "components/CustomBottomSheet/OrderTypesBo
|
||||
function MenuPage() {
|
||||
const { subdomain } = useParams();
|
||||
const { isRTL } = useAppSelector((state) => state.locale);
|
||||
const { orderType } = useAppSelector((state) => state.order);
|
||||
const { orderType, table } = useAppSelector((state) => state.order);
|
||||
const { token } = useAppSelector((state) => state.auth);
|
||||
const { t } = useTranslation();
|
||||
const { data: restaurant, isLoading: isLoadingRestaurant } =
|
||||
useGetRestaurantDetailsQuery(subdomain, {
|
||||
@@ -55,6 +57,7 @@ function MenuPage() {
|
||||
const [isOpeningTimesOpen, setIsOpeningTimesOpen] = useState(false);
|
||||
const [isOrderTypesOpen, setIsOrderTypesOpen] = useState(false);
|
||||
const orderTypeOptions = enumToSelectOptions(OrderType, t, "orderTypes");
|
||||
const [callWaiter] = useCallWaiterMutation()
|
||||
|
||||
// Automatically load restaurant taxes when restaurant data is available
|
||||
useRestaurant(restaurant);
|
||||
@@ -91,11 +94,16 @@ function MenuPage() {
|
||||
<div
|
||||
className={`${styles.headerFloatingBtn} ${styles.backButtonContainer}`}
|
||||
>
|
||||
<BackButton />
|
||||
<BackButton
|
||||
{...(token ? { customRoute: `/${subdomain}` } : {})}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className={`${styles.headerFloatingBtn} ${styles.orderTypeSelectContainer} order-type-select-container`}
|
||||
>
|
||||
{orderType !== OrderType.Redeem && (
|
||||
<div className={styles.frameSelect}>
|
||||
<div className={styles.divSelect}>
|
||||
<Select
|
||||
value={orderType}
|
||||
options={orderTypeOptions}
|
||||
@@ -108,10 +116,28 @@ function MenuPage() {
|
||||
variant="borderless"
|
||||
size="small"
|
||||
className={styles.orderTypeSelect}
|
||||
classNames={{ popup: { root: "order-type-select-dropdown" } }}
|
||||
classNames={{
|
||||
popup: { root: "order-type-select-dropdown" },
|
||||
}}
|
||||
listHeight={150}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{orderType === OrderType.Redeem && (
|
||||
<div className={styles.frame}>
|
||||
<div className={styles.div}>
|
||||
<div className={styles.pickup}>{t("menu.balance")}</div>
|
||||
<div className={styles.elementMin}>
|
||||
60{" "}
|
||||
{isRTL
|
||||
? restaurant?.local_currency
|
||||
: restaurant?.global_currency}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<SearchButton />
|
||||
</div>
|
||||
|
||||
@@ -179,6 +205,19 @@ function MenuPage() {
|
||||
backgroundColor: "#EBEBEC",
|
||||
border: "none",
|
||||
}}
|
||||
onClick={() => {
|
||||
if (table) callWaiter({
|
||||
table_id: table,
|
||||
call_reason: "call_waiter",
|
||||
}).unwrap().then(() => {
|
||||
message.success(t("menu.calledWaiterSuccess"));
|
||||
}).catch((err) => {
|
||||
message.error(err.data.message);
|
||||
})
|
||||
else
|
||||
message.info(t("menu.selectYourTable"));
|
||||
}
|
||||
}
|
||||
>
|
||||
<ProText
|
||||
style={{
|
||||
@@ -190,7 +229,7 @@ function MenuPage() {
|
||||
color: "#09237D",
|
||||
}}
|
||||
>
|
||||
Call Waiter
|
||||
{t("menu.callWaiter")}
|
||||
</ProText>
|
||||
</Button>
|
||||
|
||||
|
||||
@@ -1,21 +1,16 @@
|
||||
import { Button, Card, Divider, Form, Layout, Space } from "antd";
|
||||
import { Button, Card, Form, Layout } from "antd";
|
||||
import { useGetOrderDetailsQuery } from "redux/api/others";
|
||||
import { useAppSelector } from "redux/hooks";
|
||||
import styles from "./OrderDetails.module.css";
|
||||
import ProHeader from "components/ProHeader/ProHeader";
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import ProText from "components/ProText";
|
||||
import useBreakPoint from "hooks/useBreakPoint";
|
||||
import ArabicPrice from "components/ArabicPrice";
|
||||
import ImageWithFallback from "components/ImageWithFallback";
|
||||
import { useParams } from "react-router-dom";
|
||||
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import ProductChoicesCard from "components/productChoicesCard/ProductChoicesCard.tsx";
|
||||
|
||||
export default function OrderDetails() {
|
||||
const { orderId } = useParams();
|
||||
const { t } = useTranslation();
|
||||
const { isRTL } = useAppSelector((state) => state.locale);
|
||||
const { isMobile, isTablet } = useBreakPoint();
|
||||
const navigate = useNavigate();
|
||||
const { data: orderDetails } = useGetOrderDetailsQuery(
|
||||
{
|
||||
orderID: orderId || "",
|
||||
@@ -25,18 +20,6 @@ export default function OrderDetails() {
|
||||
skip: !orderId,
|
||||
},
|
||||
);
|
||||
const getMenuItemImageStyle = () => {
|
||||
if (isMobile) {
|
||||
return {
|
||||
width: 115,
|
||||
height: 96,
|
||||
};
|
||||
}
|
||||
return {
|
||||
width: 120,
|
||||
height: 120,
|
||||
};
|
||||
};
|
||||
|
||||
return (
|
||||
<Form layout="vertical">
|
||||
@@ -44,118 +27,12 @@ export default function OrderDetails() {
|
||||
<ProHeader>{t("order.yourOrder")}</ProHeader>
|
||||
<Layout.Content className={styles.orderDetailsContainer}>
|
||||
<Card className={styles.cartItem}>
|
||||
{orderDetails?.orderItems.map((item: any, index: number) => (
|
||||
<div key={index} style={{ position: "relative" }}>
|
||||
<Space
|
||||
size="middle"
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
height: "100%",
|
||||
padding: 8,
|
||||
}}
|
||||
>
|
||||
<Space orientation="vertical" size="small">
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 8,
|
||||
[isRTL ? "right" : "left"]: 8,
|
||||
}}
|
||||
>
|
||||
<ProText
|
||||
style={{
|
||||
margin: 0,
|
||||
lineClamp: 1,
|
||||
fontSize: isMobile ? 14 : isTablet ? 16 : 18,
|
||||
fontWeight: 600,
|
||||
}}
|
||||
>
|
||||
{item.name}
|
||||
<span
|
||||
style={{
|
||||
fontWeight: 400,
|
||||
}}
|
||||
>
|
||||
{/* {isRTL
|
||||
? (item.variant as Variant)?.optionsAR?.[0]?.value
|
||||
: (item.variant as Variant)?.options?.[0]?.value} */}
|
||||
</span>
|
||||
</ProText>
|
||||
<br />
|
||||
<ProText
|
||||
type="secondary"
|
||||
className={`${styles.itemDescription} responsive-text`}
|
||||
style={{
|
||||
margin: 0,
|
||||
lineClamp: 1,
|
||||
padding: isMobile ? "3px 0" : isTablet ? 8 : 10,
|
||||
fontSize: isMobile ? 14 : isTablet ? 18 : 20,
|
||||
display: "-webkit-box",
|
||||
WebkitBoxOrient: "vertical",
|
||||
WebkitLineClamp: 1,
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
wordWrap: "break-word",
|
||||
overflowWrap: "break-word",
|
||||
lineHeight: "1.4",
|
||||
maxHeight: isMobile
|
||||
? "3em"
|
||||
: isTablet
|
||||
? "5em"
|
||||
: "7em",
|
||||
fontWeight: 500,
|
||||
letterSpacing: "0.01em",
|
||||
width: "55%",
|
||||
}}
|
||||
>
|
||||
{item.description}
|
||||
</ProText>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
bottom:
|
||||
index !== orderDetails?.orderItems?.length - 1
|
||||
? 20
|
||||
: 7,
|
||||
[isRTL ? "right" : "left"]: 8,
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
shape="circle"
|
||||
iconPlacement="start"
|
||||
size="small"
|
||||
className={styles.addButton}
|
||||
style={{
|
||||
background: "#F5F5F6",
|
||||
width: 28,
|
||||
height: 28,
|
||||
border: "none",
|
||||
}}
|
||||
>
|
||||
<ProText style={{color: "#1F1C2E"}}>{item.qty} </ProText>
|
||||
</Button>
|
||||
</div>
|
||||
</Space>
|
||||
<div style={{ position: "relative" }}>
|
||||
<ImageWithFallback
|
||||
src={item.image}
|
||||
alt={item.name}
|
||||
className={`${styles.menuItemImage} responsive-image`}
|
||||
{...getMenuItemImageStyle()}
|
||||
fallbackSrc={
|
||||
"https://fascano-space.s3.me-central-1.amazonaws.com/uploads/restorants/685a8fc884a8c_large.jpg"
|
||||
}
|
||||
{orderDetails?.orderItems.map((item, index) => (
|
||||
<ProductChoicesCard
|
||||
key={index}
|
||||
product={{ ...item, type: "OrderItem" }}
|
||||
addDividerAfter={index !== orderDetails?.orderItems.length - 1}
|
||||
/>
|
||||
</div>
|
||||
</Space>
|
||||
|
||||
{index !== orderDetails?.orderItems?.length - 1 && (
|
||||
<Divider style={{ margin: "16px 0" }} />
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</Card>
|
||||
</Layout.Content>
|
||||
@@ -165,7 +42,7 @@ export default function OrderDetails() {
|
||||
type="primary"
|
||||
shape="round"
|
||||
className={styles.button}
|
||||
onClick={() => {}}
|
||||
onClick={() => navigate(-1)}
|
||||
>
|
||||
{t("order.done")}
|
||||
</Button>
|
||||
|
||||
@@ -2,12 +2,9 @@ import { Button } from "antd";
|
||||
import { ProBottomSheet } from "components/ProBottomSheet/ProBottomSheet.tsx";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { useAppSelector } from "redux/hooks";
|
||||
import ProText from "components/ProText";
|
||||
import ArabicPrice from "components/ArabicPrice";
|
||||
import styles from "../order.module.css";
|
||||
import NextIcon from "components/Icons/NextIcon";
|
||||
import BackIcon from "components/Icons/BackIcon";
|
||||
|
||||
interface SplitBillParticipantsBottomSheetProps {
|
||||
isOpen: boolean;
|
||||
@@ -19,7 +16,6 @@ export function SplitBillParticipantsBottomSheet({
|
||||
onClose,
|
||||
}: SplitBillParticipantsBottomSheetProps) {
|
||||
const { t } = useTranslation();
|
||||
const { isRTL } = useAppSelector((state) => state.locale);
|
||||
|
||||
const taxesChargesStyle = {
|
||||
fontWeight: 400,
|
||||
@@ -59,13 +55,15 @@ export function SplitBillParticipantsBottomSheet({
|
||||
placeItems: "center",
|
||||
}}
|
||||
>
|
||||
<div style={{
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
justifyContent: "flex-start",
|
||||
placeItems: "center",
|
||||
gap: 10,
|
||||
}}>
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
shape="circle"
|
||||
iconPlacement="start"
|
||||
|
||||
@@ -13,29 +13,68 @@ interface StepperProps {
|
||||
export default function Stepper({ statuses = [] }: StepperProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
// Check if order has canceled_by_customer status
|
||||
const hasCanceledStatus = useMemo(
|
||||
() => statuses.some((status) => status?.alias === "canceled_by_customer"),
|
||||
[statuses],
|
||||
);
|
||||
|
||||
// Get the canceled status if it exists
|
||||
const canceledStatus = useMemo(
|
||||
() => statuses.find((status) => status?.alias === "canceled_by_customer"),
|
||||
[statuses],
|
||||
);
|
||||
|
||||
const labels = useMemo(
|
||||
() => [t("order.reserved"), t("order.prepare"), t("order.ready")],
|
||||
[t],
|
||||
);
|
||||
|
||||
// Determine number of steps: 2 if canceled, 3 otherwise
|
||||
const stepCount = hasCanceledStatus ? 2 : 3;
|
||||
|
||||
const activeIndex = useMemo(() => {
|
||||
if (hasCanceledStatus) {
|
||||
// For canceled orders, the last step (index 1) is always active
|
||||
return 1;
|
||||
}
|
||||
const reachedStatuses = statuses.length > 0 ? statuses.length - 1 : 0;
|
||||
return Math.min(Math.max(reachedStatuses, 0), 2);
|
||||
}, [statuses.length]);
|
||||
}, [statuses.length, hasCanceledStatus]);
|
||||
|
||||
const steps = useMemo(
|
||||
() =>
|
||||
Array.from({ length: 3 }, (_, index) => {
|
||||
const status = statuses[index];
|
||||
const label = status?.name || status?.alias || labels[index] || "";
|
||||
Array.from({ length: stepCount }, (_, index) => {
|
||||
let status;
|
||||
let label;
|
||||
let isCanceled = false;
|
||||
|
||||
if (hasCanceledStatus) {
|
||||
if (index === 0) {
|
||||
// First step: show first status or default label
|
||||
status = statuses[0];
|
||||
label = status?.name || status?.alias || labels[0] || "";
|
||||
} else if (index === 1) {
|
||||
// Second step: show canceled status
|
||||
status = canceledStatus;
|
||||
label = status?.name || status?.alias || t("order.canceled") || "";
|
||||
isCanceled = true;
|
||||
}
|
||||
} else {
|
||||
// Normal flow: 3 steps
|
||||
status = statuses[index];
|
||||
label = status?.name || status?.alias || labels[index] || "";
|
||||
}
|
||||
|
||||
const isPending = index > activeIndex;
|
||||
return {
|
||||
key: status ? `status-${status.id}` : `placeholder-${index}`,
|
||||
label,
|
||||
isPending,
|
||||
isCanceled,
|
||||
};
|
||||
}),
|
||||
[activeIndex, labels, statuses],
|
||||
[activeIndex, labels, statuses, stepCount, hasCanceledStatus, canceledStatus, t],
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -57,7 +96,16 @@ export default function Stepper({ statuses = [] }: StepperProps) {
|
||||
}}
|
||||
>
|
||||
{steps.map((step, index) => {
|
||||
const circleStyle: CSSProperties = step.isPending
|
||||
// Determine circle style based on state
|
||||
const circleStyle: CSSProperties = step.isCanceled
|
||||
? {
|
||||
width: 12,
|
||||
height: 12,
|
||||
borderRadius: "50%",
|
||||
backgroundColor: "#EF4444", // Red for canceled
|
||||
border: "2px solid #EF4444",
|
||||
}
|
||||
: step.isPending
|
||||
? {
|
||||
width: 12,
|
||||
height: 12,
|
||||
@@ -72,7 +120,17 @@ export default function Stepper({ statuses = [] }: StepperProps) {
|
||||
backgroundColor: colors.primary,
|
||||
};
|
||||
|
||||
const lineStyle: CSSProperties = {
|
||||
// Determine line style based on state
|
||||
// Line should be red if the next step is canceled
|
||||
const nextStep = index < steps.length - 1 ? steps[index + 1] : null;
|
||||
const lineStyle: CSSProperties =
|
||||
nextStep?.isCanceled
|
||||
? {
|
||||
height: 1.5,
|
||||
backgroundColor: "#EF4444", // Red for canceled
|
||||
margin: "0 8px",
|
||||
}
|
||||
: {
|
||||
height: 1.5,
|
||||
backgroundColor: step.isPending ? "#BDBDBD" : colors.primary,
|
||||
margin: "0 8px",
|
||||
@@ -120,7 +178,11 @@ export default function Stepper({ statuses = [] }: StepperProps) {
|
||||
lineHeight: "140%",
|
||||
letterSpacing: "0%",
|
||||
textAlign: "center",
|
||||
color: step.isPending ? "#99A2AE" : "#333333",
|
||||
color: step.isCanceled
|
||||
? "#EF4444" // Red for canceled
|
||||
: step.isPending
|
||||
? "#99A2AE"
|
||||
: "#333333",
|
||||
}}
|
||||
>
|
||||
{step.label}
|
||||
|
||||
@@ -8,7 +8,6 @@ import {
|
||||
Progress,
|
||||
Tooltip,
|
||||
} from "antd";
|
||||
import Ads2 from "components/Ads/Ads2";
|
||||
import { CancelOrderBottomSheet } from "components/CustomBottomSheet/CancelOrderBottomSheet";
|
||||
import LocationIcon from "components/Icons/LocationIcon";
|
||||
import InvoiceIcon from "components/Icons/order/InvoiceIcon";
|
||||
@@ -19,7 +18,7 @@ import ProHeader from "components/ProHeader/ProHeader";
|
||||
import ProText from "components/ProText";
|
||||
import ProTitle from "components/ProTitle";
|
||||
import dayjs from "dayjs";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import {
|
||||
@@ -37,19 +36,38 @@ import { QRBottomSheet } from "pages/pay/components/splitBill/QRBottomSheet";
|
||||
import NewRateIcon from "components/Icons/order/NewRateIcon";
|
||||
import NoteIcon from "components/Icons/NoteIcon";
|
||||
import SuccessIcon from "components/Icons/SuccessIcon";
|
||||
import ProInputCard from "components/ProInputCard/ProInputCard";
|
||||
import { SplitBillParticipantsBottomSheet } from "./components/SplitBillParticipantsBottomSheet";
|
||||
import { OrderType } from "pages/checkout/hooks/types";
|
||||
|
||||
export default function OrderPage() {
|
||||
const { t } = useTranslation();
|
||||
const { orderId } = useParams();
|
||||
const navigate = useNavigate();
|
||||
const { isRTL } = useAppSelector((state) => state.locale);
|
||||
const { restaurant } = useAppSelector((state) => state.order);
|
||||
const { restaurant, orderType } = useAppSelector((state) => state.order);
|
||||
const hasRefetchedRef = useRef(false);
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [isRateOrderOpen, setIsRateOrderOpen] = useState(false);
|
||||
const [isSplitBillParticipantsBottomSheetOpen, setIsSplitBillParticipantsBottomSheetOpen] = useState(false);
|
||||
|
||||
// Track order details in state to trigger re-evaluation of polling
|
||||
const [lastOrderDetails, setLastOrderDetails] = useState<any>(null);
|
||||
|
||||
// Compute polling interval based on order status
|
||||
const pollingInterval = useMemo(() => {
|
||||
const orderDetailsToCheck = lastOrderDetails;
|
||||
if (orderDetailsToCheck?.status) {
|
||||
const hasClosedOrCanceled = orderDetailsToCheck.status.some(
|
||||
(status: any) =>
|
||||
status?.alias === "closed" ||
|
||||
status?.alias === "canceled_by_customer",
|
||||
);
|
||||
if (hasClosedOrCanceled) {
|
||||
return 0; // Stop polling
|
||||
}
|
||||
}
|
||||
return 10000; // Continue polling
|
||||
}, [lastOrderDetails]);
|
||||
|
||||
const { data: orderDetails } = useGetOrderDetailsQuery(
|
||||
{
|
||||
orderID: orderId || "",
|
||||
@@ -58,11 +76,32 @@ export default function OrderPage() {
|
||||
{
|
||||
skip: !orderId,
|
||||
// return it t0 60000 after finish testing
|
||||
pollingInterval: 10000,
|
||||
// Stop polling if order is closed or canceled
|
||||
pollingInterval,
|
||||
refetchOnMountOrArgChange: true,
|
||||
},
|
||||
);
|
||||
|
||||
// Update state when orderDetails changes to trigger polling re-evaluation
|
||||
useEffect(() => {
|
||||
if (orderDetails) {
|
||||
setLastOrderDetails(orderDetails);
|
||||
}
|
||||
}, [orderDetails]);
|
||||
|
||||
const hasClosedStatus = orderDetails?.status?.some(
|
||||
(status) => status?.alias === "closed",
|
||||
);
|
||||
|
||||
const hasCanceledByCustomerStatus = orderDetails?.status?.some(
|
||||
(status) => status?.alias === "canceled_by_customer",
|
||||
);
|
||||
|
||||
const [
|
||||
isSplitBillParticipantsBottomSheetOpen,
|
||||
setIsSplitBillParticipantsBottomSheetOpen,
|
||||
] = useState(false);
|
||||
|
||||
// Get restaurant subdomain for refetching
|
||||
const restaurantSubdomain = restaurant?.subdomain;
|
||||
const { refetch: refetchRestaurantDetails } = useGetRestaurantDetailsQuery(
|
||||
@@ -71,9 +110,6 @@ export default function OrderPage() {
|
||||
skip: !restaurantSubdomain,
|
||||
},
|
||||
);
|
||||
const hasClosedStatus = orderDetails?.status?.some(
|
||||
(status) => status?.alias === "closed",
|
||||
);
|
||||
|
||||
// Reset refetch flag when orderId changes
|
||||
useEffect(() => {
|
||||
@@ -83,7 +119,10 @@ export default function OrderPage() {
|
||||
// Refetch restaurant details when order status has alias "closed"
|
||||
useEffect(() => {
|
||||
if (orderDetails?.status && !hasRefetchedRef.current) {
|
||||
if (hasClosedStatus && restaurantSubdomain) {
|
||||
if (
|
||||
(hasClosedStatus || hasCanceledByCustomerStatus) &&
|
||||
restaurantSubdomain
|
||||
) {
|
||||
refetchRestaurantDetails();
|
||||
hasRefetchedRef.current = true;
|
||||
}
|
||||
@@ -282,7 +321,7 @@ export default function OrderPage() {
|
||||
</div>
|
||||
|
||||
{isInProgress ? (
|
||||
<Flex gap="small" wrap justify="center">
|
||||
<Flex gap="small" wrap justify="center" style={{ marginTop: 16 }}>
|
||||
<Tooltip title="3 done / 3 in progress / 4 to do">
|
||||
<div
|
||||
style={{
|
||||
@@ -391,7 +430,7 @@ export default function OrderPage() {
|
||||
<Stepper statuses={orderDetails?.status} />
|
||||
</div>
|
||||
|
||||
{!hasClosedStatus && (
|
||||
{!hasClosedStatus && orderType === OrderType.DineIn && (
|
||||
<div className={styles.orderNotes}>
|
||||
<NoteIcon className={styles.noteIcon} />
|
||||
<div
|
||||
@@ -426,7 +465,7 @@ export default function OrderPage() {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{hasClosedStatus && (
|
||||
{hasClosedStatus && orderType === OrderType.DineIn && (
|
||||
<div className={styles.orderNotesClosed}>
|
||||
<SuccessIcon className={styles.noteIcon} />
|
||||
<ProText
|
||||
@@ -443,7 +482,7 @@ export default function OrderPage() {
|
||||
)}
|
||||
</Card>
|
||||
|
||||
<Ads2 />
|
||||
{/* <Ads2 /> */}
|
||||
|
||||
{/* <ProInputCard
|
||||
title={
|
||||
@@ -499,7 +538,7 @@ export default function OrderPage() {
|
||||
<PaymentDetails order={orderDetails?.order} />
|
||||
|
||||
{/* inviteToBill */}
|
||||
{!hasClosedStatus && (
|
||||
{/* {!hasClosedStatus && (
|
||||
<ProInputCard
|
||||
title={
|
||||
<>
|
||||
@@ -539,8 +578,10 @@ export default function OrderPage() {
|
||||
</>
|
||||
}
|
||||
>
|
||||
<div className={styles.inviteToBill}
|
||||
onClick={() => setIsSplitBillParticipantsBottomSheetOpen(true)}>
|
||||
<div
|
||||
className={styles.inviteToBill}
|
||||
onClick={() => setIsSplitBillParticipantsBottomSheetOpen(true)}
|
||||
>
|
||||
<Button
|
||||
shape="circle"
|
||||
iconPlacement="start"
|
||||
@@ -613,7 +654,7 @@ export default function OrderPage() {
|
||||
</ProText>
|
||||
</Button>
|
||||
</ProInputCard>
|
||||
)}
|
||||
)} */}
|
||||
|
||||
<QRBottomSheet isOpen={isOpen} onClose={() => setIsOpen(false)} />
|
||||
|
||||
@@ -662,9 +703,12 @@ export default function OrderPage() {
|
||||
onClose={() => setIsRateOrderOpen(false)}
|
||||
/>
|
||||
|
||||
{!hasClosedStatus && <CancelOrderBottomSheet />}
|
||||
{!hasClosedStatus && !hasCanceledByCustomerStatus && (
|
||||
<CancelOrderBottomSheet />
|
||||
)}
|
||||
</Layout.Content>
|
||||
{hasClosedStatus && (
|
||||
|
||||
{(hasClosedStatus || hasCanceledByCustomerStatus) && (
|
||||
<Layout.Footer className={styles.checkoutButtonContainer}>
|
||||
<Button
|
||||
type="primary"
|
||||
@@ -679,7 +723,10 @@ export default function OrderPage() {
|
||||
</Layout.Footer>
|
||||
)}
|
||||
</Layout>
|
||||
<SplitBillParticipantsBottomSheet isOpen={isSplitBillParticipantsBottomSheetOpen} onClose={() => setIsSplitBillParticipantsBottomSheetOpen(false)} />
|
||||
<SplitBillParticipantsBottomSheet
|
||||
isOpen={isSplitBillParticipantsBottomSheetOpen}
|
||||
onClose={() => setIsSplitBillParticipantsBottomSheetOpen(false)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,92 +1,93 @@
|
||||
export interface ProductDetails {
|
||||
data: Daum[]
|
||||
name: string
|
||||
nameAR: string
|
||||
image: string
|
||||
price: number
|
||||
variants: Variant[]
|
||||
theExtrasGroups: TheExtrasGroup[]
|
||||
discount: number
|
||||
options: Option[]
|
||||
hasVariants: number
|
||||
currency: string
|
||||
short_description: string
|
||||
short_descriptionAR: string
|
||||
data: Daum[];
|
||||
name: string;
|
||||
nameAR: string;
|
||||
image: string;
|
||||
price: number;
|
||||
variants: Variant[];
|
||||
theExtrasGroups: TheExtrasGroup[];
|
||||
discount: number;
|
||||
options: Option[];
|
||||
hasVariants: number;
|
||||
currency: string;
|
||||
short_description: string;
|
||||
short_descriptionAR: string;
|
||||
}
|
||||
|
||||
export interface Daum {
|
||||
id: number
|
||||
name: string
|
||||
data: string[]
|
||||
prices: string[]
|
||||
pricesNew: string[]
|
||||
nameAR: string
|
||||
dataAR: string[]
|
||||
id: number;
|
||||
name: string;
|
||||
data: string[];
|
||||
prices: string[];
|
||||
pricesNew: string[];
|
||||
nameAR: string;
|
||||
dataAR: string[];
|
||||
}
|
||||
type VariantOptionType = { option: string; value: string };
|
||||
|
||||
export interface Variant {
|
||||
id: number
|
||||
price: number
|
||||
options: string
|
||||
image: string
|
||||
qty: number
|
||||
enable_qty: number
|
||||
order: number
|
||||
item_id: number
|
||||
created_at: string
|
||||
updated_at: string
|
||||
deleted_at: any
|
||||
available: string
|
||||
OptionsList: string
|
||||
extras: any[]
|
||||
id: number;
|
||||
price: number;
|
||||
options: VariantOptionType[];
|
||||
optionsAR: VariantOptionType[];
|
||||
image: string;
|
||||
qty: number;
|
||||
enable_qty: number;
|
||||
order: number;
|
||||
item_id: number;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
deleted_at: any;
|
||||
available: string;
|
||||
OptionsList: string;
|
||||
extras: any[];
|
||||
}
|
||||
|
||||
export interface TheExtrasGroup {
|
||||
id: number
|
||||
name: string
|
||||
nameAR: string
|
||||
label: string
|
||||
labelAR: string
|
||||
limit: number
|
||||
item_id: number
|
||||
created_at: string
|
||||
updated_at: string
|
||||
deleted_at: any
|
||||
force_limit_selection: number
|
||||
extras: Extra[]
|
||||
id: number;
|
||||
name: string;
|
||||
nameAR: string;
|
||||
label: string;
|
||||
labelAR: string;
|
||||
limit: number;
|
||||
item_id: number;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
deleted_at: any;
|
||||
force_limit_selection: number;
|
||||
extras: Extra[];
|
||||
}
|
||||
|
||||
export interface Extra {
|
||||
id: number
|
||||
item_id: number
|
||||
price: number
|
||||
name: string
|
||||
nameAR: string
|
||||
created_at: string
|
||||
updated_at: string
|
||||
deleted_at: any
|
||||
extra_for_all_variants: number
|
||||
is_custome: number
|
||||
is_available: number
|
||||
modifier_id: any
|
||||
pivot: Pivot
|
||||
id: number;
|
||||
item_id: number;
|
||||
price: number;
|
||||
name: string;
|
||||
nameAR: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
deleted_at: any;
|
||||
extra_for_all_variants: number;
|
||||
is_custome: number;
|
||||
is_available: number;
|
||||
modifier_id: any;
|
||||
pivot: Pivot;
|
||||
}
|
||||
|
||||
export interface Pivot {
|
||||
group_id: number
|
||||
extra_id: number
|
||||
group_id: number;
|
||||
extra_id: number;
|
||||
}
|
||||
|
||||
export interface Option {
|
||||
id: number
|
||||
item_id: number
|
||||
name: string
|
||||
nameAR: string
|
||||
options: string
|
||||
optionsAR: string
|
||||
optionprices: string
|
||||
created_at: string
|
||||
updated_at: string
|
||||
deleted_at: any
|
||||
id: number;
|
||||
item_id: number;
|
||||
name: string;
|
||||
nameAR: string;
|
||||
options: string;
|
||||
optionsAR: string;
|
||||
optionprices: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
deleted_at: any;
|
||||
}
|
||||
|
||||
@@ -5,6 +5,8 @@ import { Dispatch, SetStateAction } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Extra as ExtraType } from "utils/types/appTypes";
|
||||
import styles from "../product.module.css";
|
||||
import { formatPriceUi } from "utils/helpers";
|
||||
import { useAppSelector } from "redux/hooks";
|
||||
|
||||
export default function Extra({
|
||||
extrasList,
|
||||
@@ -16,7 +18,7 @@ export default function Extra({
|
||||
setSelectedExtras: Dispatch<SetStateAction<ExtraType[]>>;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { restaurant } = useAppSelector((state) => state.order);
|
||||
return (
|
||||
<>
|
||||
{extrasList.length > 0 && (
|
||||
@@ -27,10 +29,11 @@ export default function Extra({
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
margin: "0 0 16px 0",
|
||||
}}
|
||||
>
|
||||
<ProText style={{ fontSize: "1.25rem" }}>
|
||||
{t("menu.youMightAlsoLike")}
|
||||
{t("menu.choose1")}
|
||||
</ProText>
|
||||
|
||||
<ProText strong style={{ fontSize: "0.75rem" }}>
|
||||
@@ -38,17 +41,13 @@ export default function Extra({
|
||||
</ProText>
|
||||
</div>
|
||||
|
||||
<ProText strong style={{ fontSize: "0.75rem" }}>
|
||||
{t("menu.choose1")}
|
||||
</ProText>
|
||||
|
||||
<div className={styles.productContainer}>
|
||||
<ProCheckboxGroups
|
||||
options={extrasList.map((value) => {
|
||||
return {
|
||||
value: value.id.toString(),
|
||||
label: value.name,
|
||||
price: `+${value.price}`,
|
||||
price: `+${formatPriceUi(value.price, restaurant.currency_decimals ?? 3)}`,
|
||||
};
|
||||
})}
|
||||
value={selectedExtras.map((ex) => ex.id.toString())}
|
||||
@@ -57,6 +56,7 @@ export default function Extra({
|
||||
extrasList.filter((o) => values?.includes(o.id.toString())),
|
||||
)
|
||||
}
|
||||
showPrice
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import { Divider } from "antd";
|
||||
import ProText from "components/ProText";
|
||||
import { Dispatch, SetStateAction } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { TheExtrasGroup } from "utils/types/appTypes";
|
||||
|
||||
@@ -16,18 +14,17 @@ export default function ExtraGroupsContainer({
|
||||
selectedExtrasByGroup: Record<number, string[]>;
|
||||
setSelectedExtrasByGroup: Dispatch<SetStateAction<Record<number, string[]>>>;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<>
|
||||
{groupsList.length > 0 && (
|
||||
<div>
|
||||
<Divider style={{ margin: "0 0 16px 0" }} />
|
||||
|
||||
<div
|
||||
{/* <div
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
margin: "0 0 16px 0",
|
||||
}}
|
||||
>
|
||||
<ProText style={{ fontSize: "1.25rem" }}>
|
||||
@@ -37,7 +34,7 @@ export default function ExtraGroupsContainer({
|
||||
<ProText strong style={{ fontSize: "0.75rem" }}>
|
||||
{t("menu.optional")}
|
||||
</ProText>
|
||||
</div>
|
||||
</div> */}
|
||||
|
||||
{/* <ProText strong style={{ fontSize: "0.75rem" }}>
|
||||
{t("menu.choose1")}
|
||||
|
||||
@@ -85,10 +85,22 @@ export default function ProductFooter({
|
||||
comment: specialRequest,
|
||||
variant: selectedVariant,
|
||||
extras: selectedExtras,
|
||||
extrasgroupnew: Object.keys(selectedGroups).map((key) => ({
|
||||
extrasgroupnew: Object.keys(selectedGroups).map((key) => {
|
||||
const groupInfo = product.theExtrasGroups?.find(
|
||||
(g) => g.id.toString() === key,
|
||||
);
|
||||
const selectedGroupExtrasIds = selectedGroups[Number(key)];
|
||||
|
||||
const extrasInfo = groupInfo?.extras.filter((e) =>
|
||||
selectedGroupExtrasIds.includes(e.id.toString()),
|
||||
);
|
||||
return {
|
||||
groupid: key,
|
||||
extrasid: selectedGroups[Number(key)],
|
||||
})),
|
||||
extrasid: selectedGroupExtrasIds,
|
||||
extrasString: `${groupInfo?.name}: ${extrasInfo?.map((e) => e.name).join(", ")}`,
|
||||
extrasStringAR: `${groupInfo?.nameAR}: ${extrasInfo?.map((e) => e.nameAR).join(" + ")}`,
|
||||
};
|
||||
}),
|
||||
extrasgroup: Object.entries(selectedGroups).flatMap(
|
||||
([groupId, extrasIds]) => {
|
||||
if (!extrasIds || extrasIds.length === 0) {
|
||||
@@ -211,9 +223,7 @@ export default function ProductFooter({
|
||||
}
|
||||
}}
|
||||
>
|
||||
{isValid
|
||||
? t("menu.addToCart")
|
||||
: t("menu.selectRequiredOptions")}
|
||||
{isValid ? t("menu.addToCart") : t("menu.selectRequiredOptions")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -7,6 +7,7 @@ import { useTranslation } from "react-i18next";
|
||||
import { useAppSelector } from "redux/hooks";
|
||||
import { Variant } from "utils/types/appTypes";
|
||||
import styles from "../product.module.css";
|
||||
import { formatPriceUi } from "utils/helpers";
|
||||
|
||||
export default function Variants({
|
||||
selectedVariants,
|
||||
@@ -20,13 +21,13 @@ export default function Variants({
|
||||
const { isRTL } = useAppSelector((state) => state.locale);
|
||||
const { t } = useTranslation();
|
||||
const { isDesktop } = useBreakPoint();
|
||||
|
||||
const { restaurant } = useAppSelector((state) => state.order);
|
||||
// Determine variant levels based on options array length
|
||||
const variantLevels = useMemo(() => {
|
||||
if (!variantsList || variantsList.length === 0) return [];
|
||||
|
||||
const maxOptionsLength = Math.max(
|
||||
...variantsList.map((v) => v.options.length)
|
||||
...variantsList.map((v) => v.options.length),
|
||||
);
|
||||
const levels: Array<{
|
||||
level: number;
|
||||
@@ -45,7 +46,7 @@ export default function Variants({
|
||||
variantsList
|
||||
.filter((v) => v.options[i]?.option === optionKey)
|
||||
.map((v) => v.options[i]?.value)
|
||||
.filter(Boolean)
|
||||
.filter(Boolean),
|
||||
),
|
||||
];
|
||||
|
||||
@@ -112,28 +113,10 @@ export default function Variants({
|
||||
<>
|
||||
{!isDesktop && <Divider style={{ margin: "0 0 10px 0" }} />}
|
||||
<div>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
}}
|
||||
>
|
||||
<ProText style={{ fontSize: "1.25rem" }}>
|
||||
{t("menu.youMightAlsoLike")}
|
||||
</ProText>
|
||||
|
||||
<div style={{ display: "flex", alignItems: "center" }}>
|
||||
<ProText strong style={{ fontSize: "0.75rem", color: "red" }}>
|
||||
<span style={{ color: "red", fontSize: "0.75rem" }}>*</span>
|
||||
{t("menu.required")}
|
||||
</ProText>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{variantLevels.map((level, index) => {
|
||||
const filteredVariants = getFilteredVariants(index);
|
||||
const availableValues = level.availableValues.filter((value) =>
|
||||
filteredVariants.some((v) => v.options[index]?.value === value)
|
||||
filteredVariants.some((v) => v.options[index]?.value === value),
|
||||
);
|
||||
|
||||
// Only show this level if there are available values and previous levels are selected
|
||||
@@ -155,20 +138,40 @@ export default function Variants({
|
||||
|
||||
return (
|
||||
<div key={level.level} style={{ marginBottom: "16px" }}>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
}}
|
||||
>
|
||||
<ProText strong style={{ fontSize: "1rem" }}>
|
||||
{isRTL ? level.optionKeyAR : level.optionKey}
|
||||
</ProText>
|
||||
<div style={{ display: "flex", alignItems: "center" }}>
|
||||
<ProText
|
||||
strong
|
||||
style={{ fontSize: "0.75rem", color: "red" }}
|
||||
>
|
||||
<span style={{ color: "red", fontSize: "0.75rem" }}>
|
||||
*
|
||||
</span>
|
||||
{t("menu.required")}
|
||||
</ProText>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.productContainer}>
|
||||
<ProRatioGroups
|
||||
options={availableValues.map((value) => {
|
||||
const variant = filteredVariants.find(
|
||||
(v) => v.options[index]?.value === value
|
||||
(v) => v.options[index]?.value === value,
|
||||
);
|
||||
return {
|
||||
value: value,
|
||||
label: value,
|
||||
price: variant ? `+${Number(variant.price).toFixed(2)}` : "",
|
||||
price: variant
|
||||
? `+${formatPriceUi(variant.price, restaurant.currency_decimals ?? 3)}`
|
||||
: "",
|
||||
};
|
||||
})}
|
||||
value={selectedVariants[index] || ""}
|
||||
|
||||
@@ -158,12 +158,14 @@ export default function ProductDetailPage({
|
||||
}, [product?.variants, selectedVariants]);
|
||||
|
||||
const getExtras = useCallback(() => {
|
||||
const finalSelectedVariant = getFinalSelectedVariant();
|
||||
if (!finalSelectedVariant) return [];
|
||||
const selectedVariant = product?.variants?.find(
|
||||
/* const finalSelectedVariant = getFinalSelectedVariant();
|
||||
if (!finalSelectedVariant) return []; */
|
||||
/* const selectedVariant = product?.variants?.find(
|
||||
(variant) => variant.id === finalSelectedVariant.id,
|
||||
);
|
||||
return selectedVariant?.extras || [];
|
||||
); */
|
||||
//don't show v
|
||||
if (product?.variants && product.variants.length > 0) return [];
|
||||
return product?.extras || [];
|
||||
}, [product?.variants, getFinalSelectedVariant]);
|
||||
|
||||
// Validation function to check if all required selections are made
|
||||
@@ -212,11 +214,14 @@ export default function ProductDetailPage({
|
||||
);
|
||||
}
|
||||
|
||||
console.log(product.theExtrasGroups);
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
height:
|
||||
isBottomSheetView ? "calc(90vh - 285px)" : viewportHeight > 0
|
||||
height: isBottomSheetView
|
||||
? "calc(90vh - 285px)"
|
||||
: viewportHeight > 0
|
||||
? `${viewportHeight - 195}px`
|
||||
: "calc(100dvh - 195px)",
|
||||
overflow: "auto",
|
||||
|
||||
77
src/pages/redeem/components/GiftItemsCard.module.css
Normal file
77
src/pages/redeem/components/GiftItemsCard.module.css
Normal file
@@ -0,0 +1,77 @@
|
||||
.floatingContainer {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
height: 150px;
|
||||
}
|
||||
|
||||
.floatingPresent {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
animation: float 3s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.floatingShadow {
|
||||
position: absolute;
|
||||
bottom: 10px;
|
||||
left: 50%;
|
||||
width: 80px;
|
||||
height: 20px;
|
||||
background-color: rgba(0, 0, 0, 0.2);
|
||||
border-radius: 50%;
|
||||
filter: blur(6px);
|
||||
z-index: 1;
|
||||
transform-origin: center;
|
||||
transform: translateX(-50%);
|
||||
animation: shadowPulse 3s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes float {
|
||||
0%,
|
||||
100% {
|
||||
transform: translateY(0px);
|
||||
}
|
||||
50% {
|
||||
transform: translateY(-15px);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes shadowPulse {
|
||||
0%,
|
||||
100% {
|
||||
transform: translateX(-50%) scale(1);
|
||||
opacity: 0.3;
|
||||
}
|
||||
50% {
|
||||
transform: translateX(-50%) scale(0.6);
|
||||
opacity: 0.15;
|
||||
}
|
||||
}
|
||||
|
||||
.orderNotes {
|
||||
gap: 20px;
|
||||
opacity: 1;
|
||||
border-radius: 6px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.menuItemImage {
|
||||
object-fit: cover;
|
||||
border-radius: 8px;
|
||||
transition: transform 0.3s ease;
|
||||
width: 90px;
|
||||
height: 80px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.backIcon {
|
||||
position: relative;
|
||||
top: 1px;
|
||||
}
|
||||
|
||||
.nextIcon {
|
||||
position: relative;
|
||||
top: 1px;
|
||||
}
|
||||
329
src/pages/redeem/components/GiftItemsCard.tsx
Normal file
329
src/pages/redeem/components/GiftItemsCard.tsx
Normal file
@@ -0,0 +1,329 @@
|
||||
import { Button, Divider, Space, Switch, Tag, Skeleton } from "antd";
|
||||
import ProInputCard from "components/ProInputCard/ProInputCard";
|
||||
import ProText from "components/ProText";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import styles from "./GiftItemsCard.module.css";
|
||||
import { useParams } from "react-router-dom";
|
||||
import {
|
||||
useGetOrderDetailsQuery,
|
||||
useGetRedeemDetailsQuery,
|
||||
} from "redux/api/others";
|
||||
import ImageWithFallback from "components/ImageWithFallback";
|
||||
import { useAppSelector } from "redux/hooks";
|
||||
import useBreakPoint from "hooks/useBreakPoint";
|
||||
import GiftIcon from "components/Icons/GiftIcon";
|
||||
import NextIcon from "components/Icons/NextIcon";
|
||||
import BackIcon from "components/Icons/BackIcon";
|
||||
|
||||
export function GiftItemsCard({ isCart = false }: { isCart?: boolean }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { voucherId } = useParams();
|
||||
const { data: redeemDetails, isLoading } = useGetRedeemDetailsQuery(
|
||||
voucherId || "",
|
||||
{
|
||||
skip: !voucherId,
|
||||
},
|
||||
);
|
||||
|
||||
const { isRTL } = useAppSelector((state) => state.locale);
|
||||
const { isMobile, isTablet } = useBreakPoint();
|
||||
const getMenuItemImageStyle = () => {
|
||||
if (isMobile) {
|
||||
return {
|
||||
width: 72,
|
||||
height: 72,
|
||||
};
|
||||
}
|
||||
return {
|
||||
width: 120,
|
||||
height: 120,
|
||||
};
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<ProInputCard
|
||||
title={t("redeem.giftedItems")}
|
||||
titleRight={
|
||||
<>
|
||||
{!isCart && (
|
||||
<Tag
|
||||
style={{
|
||||
height: 23,
|
||||
textAlign: "center",
|
||||
opacity: 1,
|
||||
paddingRight: 10,
|
||||
paddingLeft: 10,
|
||||
borderRadius: 100,
|
||||
fontWeight: 500,
|
||||
fontStyle: "Medium",
|
||||
fontSize: 12,
|
||||
lineHeight: "140%",
|
||||
letterSpacing: "0%",
|
||||
cursor: "pointer",
|
||||
backgroundColor: "#FFF9E6",
|
||||
color: "#B58D00",
|
||||
}}
|
||||
>
|
||||
{t("redeem.pending")}
|
||||
</Tag>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
>
|
||||
{!isCart && (
|
||||
<>
|
||||
<div className={styles.orderNotes}>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: 7,
|
||||
width: "100%",
|
||||
}}
|
||||
>
|
||||
<ProText
|
||||
style={{
|
||||
fontWeight: 500,
|
||||
fontStyle: "Medium",
|
||||
fontSize: 14,
|
||||
lineHeight: "140%",
|
||||
letterSpacing: "0%",
|
||||
color: "#333333",
|
||||
}}
|
||||
>
|
||||
{t("redeem.redeemGiftedItems")}
|
||||
</ProText>
|
||||
|
||||
<ProText
|
||||
style={{
|
||||
fontWeight: 500,
|
||||
fontStyle: "Medium",
|
||||
fontSize: 12,
|
||||
lineHeight: "140%",
|
||||
letterSpacing: "0%",
|
||||
color: "#777580",
|
||||
}}
|
||||
>
|
||||
{t("redeem.includesFreeItemsInThisOrder")}
|
||||
</ProText>
|
||||
</div>
|
||||
<Switch />
|
||||
</div>
|
||||
|
||||
<Divider style={{ margin: "16px 0" }} />
|
||||
</>
|
||||
)}
|
||||
|
||||
{isLoading ? (
|
||||
// Skeleton loading state
|
||||
<>
|
||||
{[1, 2].map((skeletonIndex) => (
|
||||
<div key={skeletonIndex} style={{ position: "relative" }}>
|
||||
<Space
|
||||
size="middle"
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
height: "100%",
|
||||
}}
|
||||
>
|
||||
<Space orientation="vertical" size="small">
|
||||
<div>
|
||||
<Skeleton.Input
|
||||
active
|
||||
style={{
|
||||
width: isMobile ? 150 : 200,
|
||||
height: 20,
|
||||
marginBottom: 8,
|
||||
}}
|
||||
/>
|
||||
<Skeleton.Input
|
||||
active
|
||||
style={{
|
||||
width: isMobile ? 120 : 180,
|
||||
height: 16,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Skeleton.Button
|
||||
active
|
||||
style={{
|
||||
width: isMobile ? 80 : 100,
|
||||
height: 21,
|
||||
}}
|
||||
size="small"
|
||||
/>
|
||||
</div>
|
||||
</Space>
|
||||
<div style={{ position: "relative" }}>
|
||||
<Skeleton.Image
|
||||
active
|
||||
style={{
|
||||
width: getMenuItemImageStyle().width,
|
||||
height: getMenuItemImageStyle().height,
|
||||
borderRadius: 8,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Space>
|
||||
{skeletonIndex !== 3 && (
|
||||
<Divider style={{ margin: "16px 0" }} />
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
) : (
|
||||
redeemDetails?.gift?.items?.map((item: any, index: number) => (
|
||||
<div key={index} style={{ position: "relative" }}>
|
||||
<Space
|
||||
size="middle"
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
height: "100%",
|
||||
}}
|
||||
>
|
||||
<Space orientation="vertical" size="small">
|
||||
<div>
|
||||
<ProText
|
||||
style={{
|
||||
fontWeight: 600,
|
||||
fontStyle: "SemiBold",
|
||||
fontSize: 14,
|
||||
lineHeight: "140%",
|
||||
letterSpacing: "0%",
|
||||
color: "#333333",
|
||||
}}
|
||||
>
|
||||
{item.name}
|
||||
<span
|
||||
style={{
|
||||
fontWeight: 400,
|
||||
}}
|
||||
>
|
||||
{/* {isRTL
|
||||
? (item.variant as Variant)?.optionsAR?.[0]?.value
|
||||
: (item.variant as Variant)?.options?.[0]?.value} */}
|
||||
</span>
|
||||
</ProText>
|
||||
<br />
|
||||
<ProText
|
||||
className={`${styles.itemDescription} responsive-text`}
|
||||
style={{
|
||||
margin: 0,
|
||||
lineClamp: 1,
|
||||
padding: isMobile ? "3px 0" : isTablet ? 8 : 10,
|
||||
fontSize: isMobile ? 12 : isTablet ? 18 : 20,
|
||||
display: "-webkit-box",
|
||||
WebkitBoxOrient: "vertical",
|
||||
WebkitLineClamp: 1,
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
wordWrap: "break-word",
|
||||
overflowWrap: "break-word",
|
||||
lineHeight: "140%",
|
||||
maxHeight: isMobile ? "3em" : isTablet ? "5em" : "7em",
|
||||
width: "55%",
|
||||
fontWeight: 400,
|
||||
fontStyle: "Regular",
|
||||
letterSpacing: "0%",
|
||||
color: "#777580",
|
||||
}}
|
||||
>
|
||||
{item.name}
|
||||
</ProText>
|
||||
</div>
|
||||
<div>
|
||||
<Button
|
||||
iconPlacement="start"
|
||||
size="small"
|
||||
style={{
|
||||
background: "#F5F5F6",
|
||||
height: 21,
|
||||
border: "none",
|
||||
borderRadius: 888,
|
||||
}}
|
||||
>
|
||||
<GiftIcon />
|
||||
<ProText
|
||||
style={{
|
||||
fontWeight: 500,
|
||||
fontStyle: "Medium",
|
||||
fontSize: 12,
|
||||
lineHeight: "140%",
|
||||
letterSpacing: "0%",
|
||||
textAlign: "right",
|
||||
color: "#7950E6",
|
||||
padding: "2px 8px 2px 0",
|
||||
}}
|
||||
>
|
||||
Gift x {item.qty}
|
||||
</ProText>
|
||||
</Button>
|
||||
</div>
|
||||
</Space>
|
||||
<div style={{ position: "relative" }}>
|
||||
<ImageWithFallback
|
||||
src={item.image}
|
||||
alt={item.name}
|
||||
className={`${styles.menuItemImage} responsive-image`}
|
||||
{...getMenuItemImageStyle()}
|
||||
fallbackSrc={
|
||||
"https://fascano-space.s3.me-central-1.amazonaws.com/uploads/restorants/685a8fc884a8c_large.jpg"
|
||||
}
|
||||
style={{
|
||||
width: 72,
|
||||
height: 72,
|
||||
borderRadius: 8,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Space>
|
||||
|
||||
{index !== redeemDetails?.gift?.items?.length - 1 && (
|
||||
<Divider style={{ margin: "16px 0" }} />
|
||||
)}
|
||||
</div>
|
||||
)) || null
|
||||
)}
|
||||
|
||||
{!isCart && (
|
||||
<>
|
||||
<Divider style={{ margin: "16px 0" }} />
|
||||
|
||||
<Button
|
||||
style={{
|
||||
height: 48,
|
||||
borderRadius: 888,
|
||||
gap: 16,
|
||||
opacity: 1,
|
||||
borderWidth: 1,
|
||||
paddingTop: 8,
|
||||
paddingRight: 32,
|
||||
paddingBottom: 8,
|
||||
paddingLeft: 32,
|
||||
width: "100%",
|
||||
color: "#4C4A58",
|
||||
}}
|
||||
icon={
|
||||
isRTL ? (
|
||||
<BackIcon className={styles.backIcon} iconColor="#FFC600" />
|
||||
) : (
|
||||
<NextIcon className={styles.nextIcon} iconColor="#FFC600" />
|
||||
)
|
||||
}
|
||||
iconPlacement={isRTL ? "start" : "end"}
|
||||
>
|
||||
{t("redeem.viewAll")}
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</ProInputCard>
|
||||
</>
|
||||
);
|
||||
}
|
||||
58
src/pages/redeem/components/LocationCard.module.css
Normal file
58
src/pages/redeem/components/LocationCard.module.css
Normal file
@@ -0,0 +1,58 @@
|
||||
.floatingContainer {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
height: 150px;
|
||||
}
|
||||
|
||||
.floatingPresent {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
animation: float 3s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.floatingShadow {
|
||||
position: absolute;
|
||||
bottom: 10px;
|
||||
left: 50%;
|
||||
width: 80px;
|
||||
height: 20px;
|
||||
background-color: rgba(0, 0, 0, 0.2);
|
||||
border-radius: 50%;
|
||||
filter: blur(6px);
|
||||
z-index: 1;
|
||||
transform-origin: center;
|
||||
transform: translateX(-50%);
|
||||
animation: shadowPulse 3s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes float {
|
||||
0%,
|
||||
100% {
|
||||
transform: translateY(0px);
|
||||
}
|
||||
50% {
|
||||
transform: translateY(-15px);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes shadowPulse {
|
||||
0%,
|
||||
100% {
|
||||
transform: translateX(-50%) scale(1);
|
||||
opacity: 0.3;
|
||||
}
|
||||
50% {
|
||||
transform: translateX(-50%) scale(0.6);
|
||||
opacity: 0.15;
|
||||
}
|
||||
}
|
||||
|
||||
.orderNotes {
|
||||
gap: 20px;
|
||||
opacity: 1;
|
||||
border-radius: 6px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
112
src/pages/redeem/components/LocationCard.tsx
Normal file
112
src/pages/redeem/components/LocationCard.tsx
Normal file
@@ -0,0 +1,112 @@
|
||||
import { Divider, Tag } from "antd";
|
||||
import ProInputCard from "components/ProInputCard/ProInputCard";
|
||||
import ProText from "components/ProText";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import styles from "./LocationCard.module.css";
|
||||
import { GoogleMap } from "components/CustomBottomSheet/GoogleMap";
|
||||
import DirectionsIcon from "components/Icons/DirectionsIcon";
|
||||
import { useGetRedeemDetailsQuery } from "redux/api/others";
|
||||
import { useParams } from "react-router-dom";
|
||||
|
||||
export function LocationCard() {
|
||||
const { t } = useTranslation();
|
||||
const { voucherId } = useParams();
|
||||
const { data: redeemDetails } = useGetRedeemDetailsQuery(voucherId || "", {
|
||||
skip: !voucherId,
|
||||
});
|
||||
|
||||
const handleGetDirections = () => {
|
||||
const lat = redeemDetails?.gift?.lat;
|
||||
const lng = redeemDetails?.gift?.lng;
|
||||
|
||||
if (lat && lng) {
|
||||
// Google Maps URL that works on both mobile and desktop
|
||||
// On mobile devices, this will open the Google Maps app if installed
|
||||
const googleMapsUrl = `https://www.google.com/maps/dir/?api=1&destination=${lat},${lng}`;
|
||||
// Use window.location.href for better mobile compatibility (works in webviews)
|
||||
window.location.href = googleMapsUrl;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<ProInputCard title={t("redeem.restaurantLocation")}>
|
||||
<div className={styles.mapContainer}>
|
||||
<GoogleMap
|
||||
readOnly={true}
|
||||
initialLocation={{
|
||||
lat: parseFloat(redeemDetails?.gift?.lat || "0"),
|
||||
lng: parseFloat(redeemDetails?.gift?.lng || "0"),
|
||||
address: "",
|
||||
}}
|
||||
height="160px"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Divider style={{ margin: "16px 0" }} />
|
||||
|
||||
<div className={styles.orderNotes}>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: 4,
|
||||
width: "100%",
|
||||
}}
|
||||
>
|
||||
<ProText
|
||||
style={{
|
||||
fontWeight: 500,
|
||||
fontStyle: "Medium",
|
||||
fontSize: 14,
|
||||
lineHeight: "140%",
|
||||
letterSpacing: "0%",
|
||||
color: "#333333",
|
||||
}}
|
||||
>
|
||||
{redeemDetails?.gift?.restaurant}
|
||||
</ProText>
|
||||
|
||||
<ProText
|
||||
style={{
|
||||
fontWeight: 500,
|
||||
fontStyle: "Medium",
|
||||
fontSize: 14,
|
||||
lineHeight: "140%",
|
||||
letterSpacing: "0%",
|
||||
color: "#777580",
|
||||
}}
|
||||
>
|
||||
{redeemDetails?.gift?.restaurant}
|
||||
</ProText>
|
||||
</div>
|
||||
<Tag
|
||||
onClick={handleGetDirections}
|
||||
style={{
|
||||
backgroundColor: "#FFF9E6",
|
||||
color: "#E8B400",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 4,
|
||||
cursor: "pointer",
|
||||
}}
|
||||
>
|
||||
<DirectionsIcon />
|
||||
<ProText
|
||||
style={{
|
||||
fontWeight: 500,
|
||||
fontStyle: "Medium",
|
||||
fontSize: 14,
|
||||
lineHeight: "140%",
|
||||
letterSpacing: "0%",
|
||||
color: "#E8B400",
|
||||
}}
|
||||
>
|
||||
{t("redeem.getDirections")}
|
||||
</ProText>
|
||||
</Tag>
|
||||
</div>
|
||||
</ProInputCard>
|
||||
</>
|
||||
);
|
||||
}
|
||||
58
src/pages/redeem/components/VoucherBalanceCard.module.css
Normal file
58
src/pages/redeem/components/VoucherBalanceCard.module.css
Normal file
@@ -0,0 +1,58 @@
|
||||
.floatingContainer {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
height: 150px;
|
||||
}
|
||||
|
||||
.floatingPresent {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
animation: float 3s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.floatingShadow {
|
||||
position: absolute;
|
||||
bottom: 10px;
|
||||
left: 50%;
|
||||
width: 80px;
|
||||
height: 20px;
|
||||
background-color: rgba(0, 0, 0, 0.2);
|
||||
border-radius: 50%;
|
||||
filter: blur(6px);
|
||||
z-index: 1;
|
||||
transform-origin: center;
|
||||
transform: translateX(-50%);
|
||||
animation: shadowPulse 3s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes float {
|
||||
0%,
|
||||
100% {
|
||||
transform: translateY(0px);
|
||||
}
|
||||
50% {
|
||||
transform: translateY(-15px);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes shadowPulse {
|
||||
0%,
|
||||
100% {
|
||||
transform: translateX(-50%) scale(1);
|
||||
opacity: 0.3;
|
||||
}
|
||||
50% {
|
||||
transform: translateX(-50%) scale(0.6);
|
||||
opacity: 0.15;
|
||||
}
|
||||
}
|
||||
|
||||
.orderNotes {
|
||||
gap: 20px;
|
||||
opacity: 1;
|
||||
border-radius: 6px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
119
src/pages/redeem/components/VoucherBalanceCard.tsx
Normal file
119
src/pages/redeem/components/VoucherBalanceCard.tsx
Normal file
@@ -0,0 +1,119 @@
|
||||
import { Divider, Switch, Tag } from "antd";
|
||||
import ProInputCard from "components/ProInputCard/ProInputCard";
|
||||
import ProText from "components/ProText";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import styles from "./VoucherBalanceCard.module.css";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import CardAmountIcon from "components/Icons/CardAmountIcon";
|
||||
import ArabicPrice from "components/ArabicPrice";
|
||||
import { useGetRedeemDetailsQuery } from "redux/api/others";
|
||||
|
||||
export function VoucherBalanceCard() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const navigate = useNavigate();
|
||||
const { voucherId } = useParams();
|
||||
const { data: redeemDetails } = useGetRedeemDetailsQuery(voucherId || "", {
|
||||
skip: !voucherId,
|
||||
});
|
||||
return (
|
||||
<>
|
||||
<ProInputCard
|
||||
title={t("redeem.voucherBalance")}
|
||||
titleRight={
|
||||
<Tag
|
||||
style={{
|
||||
height: 23,
|
||||
textAlign: "center",
|
||||
opacity: 1,
|
||||
paddingRight: 10,
|
||||
paddingLeft: 10,
|
||||
borderRadius: 100,
|
||||
fontWeight: 500,
|
||||
fontStyle: "Medium",
|
||||
fontSize: 12,
|
||||
lineHeight: "140%",
|
||||
letterSpacing: "0%",
|
||||
cursor: "pointer",
|
||||
backgroundColor: "#FFF9E6",
|
||||
color: "#B58D00",
|
||||
}}
|
||||
>
|
||||
{t("redeem.pending")}
|
||||
</Tag>
|
||||
}
|
||||
>
|
||||
<div className={styles.orderNotes}>
|
||||
<div
|
||||
style={{
|
||||
height: 42,
|
||||
width: 64,
|
||||
backgroundColor: "var(--background)",
|
||||
borderRadius: 8,
|
||||
}}
|
||||
>
|
||||
<CardAmountIcon />
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: 4,
|
||||
width: "100%",
|
||||
}}
|
||||
>
|
||||
<ProText
|
||||
style={{
|
||||
fontWeight: 500,
|
||||
fontStyle: "Medium",
|
||||
fontSize: 14,
|
||||
lineHeight: "140%",
|
||||
letterSpacing: "0%",
|
||||
color: "#777580",
|
||||
}}
|
||||
>
|
||||
{t("redeem.yourGiftCardBalance")}
|
||||
</ProText>
|
||||
|
||||
<ProText
|
||||
style={{
|
||||
fontWeight: 500,
|
||||
fontStyle: "Medium",
|
||||
fontSize: 14,
|
||||
lineHeight: "140%",
|
||||
letterSpacing: "0%",
|
||||
color: "#333333",
|
||||
}}
|
||||
>
|
||||
<ArabicPrice price={redeemDetails?.gift?.amount || 0} />
|
||||
</ProText>
|
||||
</div>
|
||||
<Switch />
|
||||
</div>
|
||||
|
||||
<Divider style={{ margin: "10px 0" }} />
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
}}
|
||||
>
|
||||
<ProText
|
||||
style={{
|
||||
fontWeight: 400,
|
||||
fontStyle: "Regular",
|
||||
fontSize: 14,
|
||||
lineHeight: "140%",
|
||||
letterSpacing: "0%",
|
||||
color: "#777580",
|
||||
cursor: "pointer",
|
||||
}}
|
||||
>
|
||||
{t("redeem.voucherWillBeAppliedAtCheckout")}
|
||||
</ProText>
|
||||
</div>
|
||||
</ProInputCard>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
.voucherSummary :global(.ant-card-body) {
|
||||
padding: 16px !important;
|
||||
}
|
||||
|
||||
.voucherSummary {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.summaryRow {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.summaryDivider {
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
.totalRow {
|
||||
font-weight: bold;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.desktopOrderSummary {
|
||||
background: linear-gradient(135deg, #f8f9fa 0%, #ffffff 100%);
|
||||
border: 1px solid rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.desktopSummaryRow {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 12px 0;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.desktopTotalRow {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 16px 0;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
/* Enhanced responsive focus states */
|
||||
.voucherSummary:focus {
|
||||
outline: 2px solid var(--primary);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
/* Enhanced responsive animations */
|
||||
@keyframes fadeInUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* ===== MEDIA QUERIES ===== */
|
||||
|
||||
/* Tablet styles (769px - 1024px) */
|
||||
@media (min-width: 769px) and (max-width: 1024px) {
|
||||
.summaryRow {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.summaryDivider {
|
||||
margin: 10px 0 !important;
|
||||
}
|
||||
|
||||
.totalRow {
|
||||
font-size: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Desktop styles (1025px+) */
|
||||
@media (min-width: 1025px) {
|
||||
.summaryRow {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.summaryDivider {
|
||||
margin: 10px 0 !important;
|
||||
}
|
||||
|
||||
.totalRow {
|
||||
font-size: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Focus states for larger screens */
|
||||
@media (min-width: 768px) {
|
||||
.voucherSummary:focus {
|
||||
outline-offset: 4px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
import { Card, Checkbox, Divider, Space } from "antd";
|
||||
import ArabicPrice from "components/ArabicPrice";
|
||||
import {
|
||||
selectCart,
|
||||
selectCartTotal,
|
||||
selectGrandTotal,
|
||||
} from "features/order/orderSlice";
|
||||
import { OrderType } from "pages/checkout/hooks/types";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useParams } from "react-router-dom";
|
||||
import { useGetRestaurantDetailsQuery } from "redux/api/others";
|
||||
import { useAppSelector } from "redux/hooks";
|
||||
import ProText from "components/ProText";
|
||||
import ProTitle from "components/ProTitle";
|
||||
import styles from "./VoucherSummary.module.css";
|
||||
import { CSSProperties } from "react";
|
||||
|
||||
export default function VoucherSummary() {
|
||||
const { t } = useTranslation();
|
||||
const { subdomain } = useParams();
|
||||
const { data: restaurant } = useGetRestaurantDetailsQuery(subdomain);
|
||||
const subtotal = useAppSelector(selectCartTotal);
|
||||
const grandTotal = useAppSelector(selectGrandTotal);
|
||||
|
||||
const titlesStyle: CSSProperties = {
|
||||
fontWeight: 400,
|
||||
fontStyle: "Regular",
|
||||
fontSize: 12,
|
||||
lineHeight: "140%",
|
||||
letterSpacing: "0%",
|
||||
textAlign: "center",
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Card className={`${styles.voucherSummary}`}>
|
||||
<ProTitle
|
||||
style={{
|
||||
fontWeight: 500,
|
||||
fontStyle: "Medium",
|
||||
fontSize: 18,
|
||||
lineHeight: "140%",
|
||||
letterSpacing: "0%",
|
||||
color: "#333333",
|
||||
}}
|
||||
>
|
||||
{t("cart.voucherSummary")}
|
||||
</ProTitle>
|
||||
<Divider style={{ margin: "15px 0 15px 0" }} />
|
||||
<Space orientation="vertical" style={{ width: "100%", gap: 16 }}>
|
||||
<div className={styles.summaryRow}>
|
||||
<ProText type="secondary" style={titlesStyle}>
|
||||
{t("cart.voucherBalance")}
|
||||
</ProText>
|
||||
<ArabicPrice price={subtotal} textStyle={titlesStyle} />
|
||||
</div>
|
||||
<div className={styles.summaryRow}>
|
||||
<ProText type="secondary" style={titlesStyle}>
|
||||
{t("cart.voucherApplied")}
|
||||
</ProText>
|
||||
<ArabicPrice
|
||||
price={Number(restaurant?.delivery_fees || 0)}
|
||||
textStyle={{ ...titlesStyle, color: "#434E5C" }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={`${styles.summaryRow} ${styles.totalRow}`}>
|
||||
<ProText
|
||||
style={{
|
||||
fontWeight: 600,
|
||||
fontStyle: "SemiBold",
|
||||
fontSize: 14,
|
||||
lineHeight: "140%",
|
||||
letterSpacing: "0%",
|
||||
textAlign: "center",
|
||||
color: "#333333",
|
||||
}}
|
||||
>
|
||||
{t("cart.remainingVoucherAmount")}
|
||||
</ProText>
|
||||
<ArabicPrice
|
||||
price={grandTotal}
|
||||
textStyle={{ ...titlesStyle, color: "#434E5C" }}
|
||||
/>
|
||||
</div>
|
||||
</Space>
|
||||
</Card>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,263 +1,290 @@
|
||||
import { Button, Card, Divider, Image } from "antd";
|
||||
import Ads2 from "components/Ads/Ads2";
|
||||
import { CancelOrderBottomSheet } from "components/CustomBottomSheet/CancelOrderBottomSheet";
|
||||
import LocationIcon from "components/Icons/LocationIcon";
|
||||
import InvoiceIcon from "components/Icons/order/InvoiceIcon";
|
||||
import TimeIcon from "components/Icons/order/TimeIcon";
|
||||
import OrderDishIcon from "components/Icons/OrderDishIcon";
|
||||
import PaymentDetails from "components/PaymentDetails/PaymentDetails";
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
Form,
|
||||
Image,
|
||||
Layout,
|
||||
QRCode,
|
||||
Skeleton,
|
||||
message,
|
||||
} from "antd";
|
||||
|
||||
import ProHeader from "components/ProHeader/ProHeader";
|
||||
import ProInputCard from "components/ProInputCard/ProInputCard";
|
||||
import ProText from "components/ProText";
|
||||
import ProTitle from "components/ProTitle";
|
||||
import dayjs from "dayjs";
|
||||
import { useEffect, useRef } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import {
|
||||
useGetOrderDetailsQuery,
|
||||
useGetRestaurantDetailsQuery,
|
||||
} from "redux/api/others";
|
||||
import { useAppSelector } from "redux/hooks";
|
||||
import { useGetRedeemDetailsQuery } from "redux/api/others";
|
||||
import styles from "./redeem.module.css";
|
||||
import BackIcon from "components/Icons/BackIcon";
|
||||
import NextIcon from "components/Icons/NextIcon";
|
||||
import { RateBottomSheet } from "components/CustomBottomSheet/RateBottomSheet";
|
||||
import Stepper from "pages/order/components/Stepper";
|
||||
import CopyIcon from "components/Icons/CopyIcon";
|
||||
import { LocationCard } from "./components/LocationCard.tsx";
|
||||
import { GiftItemsCard } from "./components/GiftItemsCard.tsx";
|
||||
import { VoucherBalanceCard } from "./components/VoucherBalanceCard.tsx";
|
||||
import { OrderType } from "pages/checkout/hooks/types.ts";
|
||||
import { Loader } from "components/Loader/Loader.tsx";
|
||||
import { CollectWay } from "pages/checkout/components/CollectWay/CollectWay.tsx";
|
||||
import PickupTimeCard from "pages/checkout/components/pickupEstimate/TimeEstimateCard.tsx";
|
||||
|
||||
export default function RedeemPage() {
|
||||
const { t } = useTranslation();
|
||||
const { orderId } = useParams();
|
||||
const { isRTL } = useAppSelector((state) => state.locale);
|
||||
const { restaurant } = useAppSelector((state) => state.order);
|
||||
const { voucherId } = useParams();
|
||||
const navigate = useNavigate();
|
||||
const hasRefetchedRef = useRef(false);
|
||||
|
||||
const { data: orderDetails } = useGetOrderDetailsQuery(
|
||||
const { subdomain } = useParams();
|
||||
const [form] = Form.useForm();
|
||||
const { data: redeemDetails, isLoading } = useGetRedeemDetailsQuery(
|
||||
voucherId || "",
|
||||
{
|
||||
orderID: orderId || "",
|
||||
restaurantID: localStorage.getItem("restaurantID") || "",
|
||||
},
|
||||
{
|
||||
skip: !orderId,
|
||||
// return it t0 60000 after finish testing
|
||||
pollingInterval: 10000,
|
||||
refetchOnMountOrArgChange: true,
|
||||
skip: !voucherId,
|
||||
},
|
||||
);
|
||||
|
||||
// Get restaurant subdomain for refetching
|
||||
const restaurantSubdomain = restaurant?.subdomain;
|
||||
const { refetch: refetchRestaurantDetails } = useGetRestaurantDetailsQuery(
|
||||
restaurantSubdomain || "",
|
||||
{
|
||||
skip: !restaurantSubdomain,
|
||||
},
|
||||
const handleCheckout = () => {
|
||||
navigate(`/${subdomain}/menu?orderType=${OrderType.Redeem}`);
|
||||
};
|
||||
|
||||
const handleCopyVoucherCode = async () => {
|
||||
const voucherCode = redeemDetails?.gift?.voucher_code;
|
||||
if (voucherCode) {
|
||||
try {
|
||||
await navigator.clipboard.writeText(voucherCode);
|
||||
message.success(
|
||||
t("redeem.voucherCodeCopied") || "Voucher code copied!",
|
||||
);
|
||||
|
||||
// Reset refetch flag when orderId changes
|
||||
useEffect(() => {
|
||||
hasRefetchedRef.current = false;
|
||||
}, [orderId]);
|
||||
|
||||
// Refetch restaurant details when order status has alias "closed"
|
||||
useEffect(() => {
|
||||
if (orderDetails?.status && !hasRefetchedRef.current) {
|
||||
const hasClosedStatus = orderDetails.status.some(
|
||||
(status) => status?.alias === "closed",
|
||||
);
|
||||
|
||||
if (hasClosedStatus && restaurantSubdomain) {
|
||||
refetchRestaurantDetails();
|
||||
hasRefetchedRef.current = true;
|
||||
} catch (error) {
|
||||
message.error(t("redeem.copyFailed") || "Failed to copy voucher code");
|
||||
}
|
||||
}
|
||||
}, [orderDetails?.status, restaurantSubdomain, refetchRestaurantDetails]);
|
||||
};
|
||||
|
||||
if (isLoading) return <Loader />;
|
||||
|
||||
return (
|
||||
<>
|
||||
<ProHeader>{t("order.title")}</ProHeader>
|
||||
<Layout>
|
||||
<ProHeader>{t("redeem.title")}</ProHeader>
|
||||
<Layout.Content className={styles.redeemContainer}>
|
||||
<div
|
||||
style={{
|
||||
textAlign: "center",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
height: "92vh",
|
||||
padding: 16,
|
||||
gap: 16,
|
||||
overflow: "auto",
|
||||
scrollbarWidth: "none",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
margin: "32px 28px 21px 28px",
|
||||
gap: 8,
|
||||
}}
|
||||
>
|
||||
<Card className={styles.orderCard}>
|
||||
<ProText
|
||||
style={{ fontSize: 16, fontWeight: 600, color: "#333333" }}
|
||||
>
|
||||
{t("redeem.hiX", { name: redeemDetails?.gift?.recipient_name })}
|
||||
</ProText>
|
||||
<ProText
|
||||
style={{ fontSize: 14, fontWeight: 400, color: "#95949C" }}
|
||||
>
|
||||
{t("redeem.youHaveReceivedAGiftCarFromX", {
|
||||
name: redeemDetails?.gift?.sender_name,
|
||||
})}
|
||||
</ProText>
|
||||
</div>
|
||||
|
||||
{isLoading || !redeemDetails?.gift?.restaurant_iimage ? (
|
||||
<div className={styles.carouselContainer}>
|
||||
<div className={styles.cardWrapper}>
|
||||
<Skeleton.Image
|
||||
active
|
||||
style={{
|
||||
width: 205,
|
||||
height: 134,
|
||||
borderRadius: 8,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className={styles.carouselContainer}>
|
||||
<div className={styles.cardWrapper}>
|
||||
<Image
|
||||
src={redeemDetails?.gift?.card_url}
|
||||
width={205}
|
||||
height={134}
|
||||
className={styles.cardImage}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div
|
||||
style={{
|
||||
textAlign: "center",
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
gap: "1rem",
|
||||
backgroundColor: "rgba(255, 183, 0, 0.08)",
|
||||
borderRadius: "12px",
|
||||
padding: 16,
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
margin: "18px 28px 26px 28px",
|
||||
gap: 12,
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
type="text"
|
||||
shape="circle"
|
||||
<ProText
|
||||
style={{
|
||||
backgroundColor: "rgba(255, 183, 0, 0.08)",
|
||||
fontWeight: 400,
|
||||
fontStyle: "Regular",
|
||||
fontSize: 14,
|
||||
lineHeight: "140%",
|
||||
letterSpacing: "0%",
|
||||
textAlign: "center",
|
||||
color: "#95949C",
|
||||
}}
|
||||
>
|
||||
<Image
|
||||
src={orderDetails?.restaurant_iimage}
|
||||
className={styles.profileImage}
|
||||
width={50}
|
||||
height={50}
|
||||
preview={false}
|
||||
/>
|
||||
</Button>
|
||||
<div>
|
||||
<ProText style={{ fontSize: "1rem" }}>
|
||||
{t("order.yourOrderFromFascanoRestaurant")}
|
||||
{redeemDetails?.gift?.message || t("redeem.description")}
|
||||
</ProText>
|
||||
<br />
|
||||
<ProText type="secondary">
|
||||
<LocationIcon className={styles.locationIcon} />{" "}
|
||||
{isRTL ? orderDetails?.restaurantAR : orderDetails?.restaurant}
|
||||
</ProText>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<OrderDishIcon className={styles.orderDishIcon} />
|
||||
|
||||
<div>
|
||||
<ProTitle
|
||||
level={5}
|
||||
<ProText
|
||||
style={{
|
||||
fontWeight: 600,
|
||||
fontSize: "18px",
|
||||
marginBottom: "0.75rem",
|
||||
fontStyle: "SemiBold",
|
||||
fontSize: 14,
|
||||
lineHeight: "140%",
|
||||
letterSpacing: "0%",
|
||||
textAlign: "center",
|
||||
color: "#333333",
|
||||
}}
|
||||
>
|
||||
{t("order.inProgressOrder")} (1)
|
||||
</ProTitle>
|
||||
<div style={{ display: "flex", flexDirection: "row", gap: 8 }}>
|
||||
<InvoiceIcon className={styles.invoiceIcon} />
|
||||
<ProText type="secondary" style={{ fontSize: "14px" }}>
|
||||
#{orderDetails?.order.id}
|
||||
</ProText>
|
||||
<TimeIcon className={styles.timeIcon} />
|
||||
<ProText type="secondary" style={{ fontSize: "14px" }}>
|
||||
ordered :- Today -{" "}
|
||||
{dayjs(orderDetails?.status[0]?.pivot?.created_at).format(
|
||||
"h:mm A",
|
||||
)}
|
||||
{redeemDetails?.gift?.sender_name}
|
||||
</ProText>
|
||||
</div>
|
||||
|
||||
<Divider style={{ margin: "12px 0" }} />
|
||||
|
||||
<Stepper statuses={orderDetails?.status} />
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Ads2 />
|
||||
|
||||
<ProInputCard
|
||||
title={
|
||||
<div style={{ marginBottom: 7 }}>
|
||||
<ProText style={{ fontSize: "1rem" }}>
|
||||
{t("order.yourOrderFrom")}
|
||||
</ProText>
|
||||
<br />
|
||||
<ProText type="secondary">
|
||||
<LocationIcon className={styles.locationIcon} />{" "}
|
||||
{t("order.muscat")}
|
||||
</ProText>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={{ display: "flex", flexDirection: "column", gap: "1rem" }}
|
||||
>
|
||||
{orderDetails?.orderItems.map((item, index) => (
|
||||
<div key={item.id}>
|
||||
<div
|
||||
<div>
|
||||
<Card
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
justifyContent: "flex-start",
|
||||
gap: "1rem",
|
||||
borderRadius: 0,
|
||||
borderTopRightRadius: 16,
|
||||
borderTopLeftRadius: 16,
|
||||
border: "1px solid transparent",
|
||||
borderImageSource:
|
||||
"radial-gradient(38.92% 103.83% at 49.85% -3.83%, #FFB700 0%, rgba(255, 233, 179, 0) 100%)",
|
||||
borderImageSlice: 1,
|
||||
}}
|
||||
styles={{
|
||||
body: {
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: 24,
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
padding: 32,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<ProText
|
||||
style={{
|
||||
fontWeight: 400,
|
||||
fontStyle: "Regular",
|
||||
fontSize: 14,
|
||||
lineHeight: "140%",
|
||||
letterSpacing: "0%",
|
||||
color: "#95949C",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
{t("redeem.showThisCodeAtTheRestaurant")}
|
||||
</ProText>
|
||||
<QRCode value={redeemDetails?.gift?.gr_url || "-"} />
|
||||
<div
|
||||
style={{ display: "flex", flexDirection: "column", gap: 12 }}
|
||||
>
|
||||
<Button
|
||||
type="text"
|
||||
shape="circle"
|
||||
style={{
|
||||
backgroundColor: "rgba(255, 183, 0, 0.08)",
|
||||
height: 40,
|
||||
borderRadius: 888,
|
||||
gap: 16,
|
||||
opacity: 1,
|
||||
borderWidth: 1,
|
||||
backgroundColor: "var(--background)",
|
||||
}}
|
||||
icon={<CopyIcon className={styles.copyIcon} />}
|
||||
iconPlacement="end"
|
||||
onClick={handleCopyVoucherCode}
|
||||
>
|
||||
{index + 1}X
|
||||
{redeemDetails?.gift?.voucher_code}
|
||||
</Button>
|
||||
<div>
|
||||
<ProText
|
||||
style={{ fontSize: "1rem", position: "relative", top: 8 }}
|
||||
>
|
||||
{item.name}
|
||||
</ProText>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</ProInputCard>
|
||||
|
||||
<PaymentDetails order={orderDetails?.order} />
|
||||
|
||||
<Card
|
||||
className={styles.backToHomePage}
|
||||
onClick={() => navigate(`/${restaurant?.subdomain}`)}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
marginTop: 1,
|
||||
}}
|
||||
>
|
||||
<Image
|
||||
src={restaurant?.restautantImage}
|
||||
width={30}
|
||||
height={30}
|
||||
preview={false}
|
||||
style={{
|
||||
borderRadius: "50%",
|
||||
objectFit: "cover",
|
||||
position: "relative",
|
||||
top: -4,
|
||||
}}
|
||||
/>
|
||||
|
||||
<ProTitle
|
||||
level={5}
|
||||
style={{
|
||||
fontWeight: 400,
|
||||
fontStyle: "Regular",
|
||||
fontSize: 14,
|
||||
lineHeight: "140%",
|
||||
letterSpacing: "0%",
|
||||
color: "#95949C",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
{isRTL ? restaurant?.nameAR : restaurant?.restautantName}
|
||||
</ProTitle>
|
||||
|
||||
{isRTL ? (
|
||||
<BackIcon className={styles.serviceIcon} />
|
||||
) : (
|
||||
<NextIcon className={styles.serviceIcon} />
|
||||
)}
|
||||
{t("redeem.useThisCodeIfScanningNotPossible")}
|
||||
</ProText>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* <RateBottomSheet /> */}
|
||||
|
||||
<CancelOrderBottomSheet />
|
||||
<div
|
||||
style={{
|
||||
width: "100%",
|
||||
height: 44,
|
||||
opacity: 1,
|
||||
borderBottomRightRadius: 16,
|
||||
borderBottomLeftRadius: 16,
|
||||
paddingTop: 12,
|
||||
paddingBottom: 12,
|
||||
gap: 10,
|
||||
borderTopWidth: 1,
|
||||
background: "#FFF9E6",
|
||||
borderTop: "1px solid #FFEDB0",
|
||||
textAlign: "center",
|
||||
}}
|
||||
>
|
||||
<ProText
|
||||
style={{
|
||||
fontSize: 14,
|
||||
fontWeight: 500,
|
||||
fontStyle: "Medium",
|
||||
lineHeight: "140%",
|
||||
letterSpacing: "0%",
|
||||
textAlign: "center",
|
||||
color: "#E8B400",
|
||||
}}
|
||||
>
|
||||
Active - Expires in 12 days
|
||||
</ProText>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Form form={form}>
|
||||
<div
|
||||
style={{
|
||||
margin: "20px 0",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: 16,
|
||||
}}
|
||||
>
|
||||
{redeemDetails?.gift?.gift_type === "ORDER" && <GiftItemsCard />}
|
||||
{redeemDetails?.gift?.gift_type === "AMOUNT" && (
|
||||
<VoucherBalanceCard />
|
||||
)}
|
||||
<LocationCard />
|
||||
<CollectWay />
|
||||
<PickupTimeCard />
|
||||
</div>
|
||||
</Form>
|
||||
</Layout.Content>
|
||||
|
||||
<Layout.Footer className={styles.checkoutButtonContainer}>
|
||||
<Button
|
||||
type="primary"
|
||||
shape="round"
|
||||
className={styles.checkoutButton}
|
||||
onClick={handleCheckout}
|
||||
>
|
||||
{t("redeem.redeemNow")}
|
||||
</Button>
|
||||
</Layout.Footer>
|
||||
</Layout>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,247 +1,81 @@
|
||||
.orderSummary :global(.ant-card-body) {
|
||||
padding: 16px !important;
|
||||
}
|
||||
|
||||
.profileImage {
|
||||
border-radius: 50%;
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
}
|
||||
|
||||
/* Enhanced responsive order summary */
|
||||
@media (min-width: 769px) and (max-width: 1024px) {
|
||||
.orderSummary {
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
}
|
||||
|
||||
.fascanoIcon {
|
||||
position: relative;
|
||||
top: 3px;
|
||||
}
|
||||
|
||||
.locationIcon {
|
||||
position: relative;
|
||||
top: 3px;
|
||||
}
|
||||
|
||||
.orderDishIcon {
|
||||
.carouselContainer {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 16px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.orderCard :global(.ant-card-body) {
|
||||
.cardWrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.orderCard :global(.ant-card-body) > *:not(:last-child) {
|
||||
margin-bottom: 3.5rem;
|
||||
}
|
||||
|
||||
.orderSummary {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.invoiceIcon {
|
||||
position: relative;
|
||||
top: 3px;
|
||||
}
|
||||
|
||||
.timeIcon {
|
||||
position: relative;
|
||||
top: 3px;
|
||||
}
|
||||
|
||||
/* Enhanced responsive order summary */
|
||||
@media (min-width: 769px) and (max-width: 1024px) {
|
||||
.orderSummary {
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
}
|
||||
|
||||
.summaryRow {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-size: 16px;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
/* Enhanced responsive summary rows */
|
||||
@media (min-width: 769px) and (max-width: 1024px) {
|
||||
.summaryRow {
|
||||
padding: 12px 0;
|
||||
font-size: 16px;
|
||||
}
|
||||
.cardImage {
|
||||
width: 205px;
|
||||
height: 134px;
|
||||
object-fit: cover;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
@media (min-width: 1025px) {
|
||||
.summaryRow {
|
||||
padding: 16px 0;
|
||||
font-size: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
.summaryDivider {
|
||||
margin: 8px 0 !important;
|
||||
}
|
||||
|
||||
/* Enhanced responsive summary divider */
|
||||
@media (min-width: 769px) and (max-width: 1024px) {
|
||||
.summaryDivider {
|
||||
margin: 20px 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1025px) {
|
||||
.summaryDivider {
|
||||
margin: 24px 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
.totalRow {
|
||||
font-weight: bold;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
/* Enhanced responsive total row */
|
||||
@media (min-width: 769px) and (max-width: 1024px) {
|
||||
.totalRow {
|
||||
font-size: 18px;
|
||||
padding-top: 20px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1025px) {
|
||||
.totalRow {
|
||||
font-size: 20px;
|
||||
padding-top: 24px;
|
||||
margin-top: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.desktopOrderSummary {
|
||||
background: linear-gradient(135deg, #f8f9fa 0%, #ffffff 100%);
|
||||
border: 1px solid rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.desktopSummaryRow {
|
||||
.arrowButton {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 12px 0;
|
||||
font-size: 16px;
|
||||
justify-content: center;
|
||||
min-width: 40px;
|
||||
height: 40px;
|
||||
padding: 0;
|
||||
border: none;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.desktopTotalRow {
|
||||
.arrowButton:hover {
|
||||
background: rgba(0, 0, 0, 0.04);
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
/* CheckoutButton Styles */
|
||||
.checkoutButtonContainer {
|
||||
width: 100%;
|
||||
padding: 16px 16px 0;
|
||||
position: sticky;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
height: 80px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 16px 0;
|
||||
margin-top: 16px;
|
||||
flex-direction: row;
|
||||
justify-content: space-around;
|
||||
gap: 1rem;
|
||||
background-color: var(--secondary-background);
|
||||
box-shadow: none;
|
||||
z-index: 999;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .orderSummary {
|
||||
background-color: #181818 !important;
|
||||
border-color: #363636 !important;
|
||||
/* Dark theme styles for checkout */
|
||||
:global(.darkApp) .checkoutButtonContainer {
|
||||
background-color: #000000 !important;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .orderSummary:hover {
|
||||
background-color: #363636 !important;
|
||||
border-color: #424242 !important;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .summaryRow {
|
||||
color: #b0b0b0;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .totalRow {
|
||||
color: #ffffff;
|
||||
border-top-color: #424242;
|
||||
}
|
||||
|
||||
/* Enhanced responsive animations */
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
.orderSummary {
|
||||
animation: fadeInUp 0.8s ease-out;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadeInUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* Enhanced responsive focus states */
|
||||
.orderSummary:focus {
|
||||
outline: 2px solid var(--primary);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.orderSummary:focus {
|
||||
outline-offset: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Enhanced responsive print styles */
|
||||
@media print {
|
||||
.orderSummary {
|
||||
box-shadow: none !important;
|
||||
border: 1px solid #ccc !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Enhanced responsive hover effects */
|
||||
@media (hover: hover) {
|
||||
.orderSummary:hover {
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.menuItemImage:hover {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .orderSummary:hover {
|
||||
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
}
|
||||
|
||||
.backToHomePage {
|
||||
.checkoutButton {
|
||||
width: 100%;
|
||||
height: 48px;
|
||||
margin-bottom: 16px;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.copyIcon {
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
top: 3px;
|
||||
}
|
||||
|
||||
.redeemContainer {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
padding: 12px 18px !important;
|
||||
row-gap: 10px;
|
||||
transition: all 0.3s ease;
|
||||
border-radius: 50px;
|
||||
flex-direction: column;
|
||||
padding: 16px;
|
||||
gap: 16px;
|
||||
overflow: auto;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
.backToHomePage :global(.ant-card-body) {
|
||||
padding: 0px !important;
|
||||
text-align: start;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.nextIcon {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
.backIcon {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
|
||||
|
||||
94
src/pages/redeem/types.ts
Normal file
94
src/pages/redeem/types.ts
Normal file
@@ -0,0 +1,94 @@
|
||||
export interface RedeemResponse {
|
||||
gift: Gift
|
||||
working_hours: WorkingHours
|
||||
notes: Notes
|
||||
is_restaurant_closed: boolean
|
||||
}
|
||||
|
||||
export interface Gift {
|
||||
id: number
|
||||
voucher_amount: string
|
||||
amount: string
|
||||
restorant_id: number
|
||||
voucher_code: string
|
||||
used_amount: string
|
||||
show_sender_info: number
|
||||
remaining_amount: string
|
||||
is_used: number
|
||||
sender_phone: string
|
||||
gift_type: string
|
||||
sender_name: string
|
||||
recipient_phone: string
|
||||
recipient_name: string
|
||||
message: any
|
||||
created_at: string
|
||||
card_url: string
|
||||
gr_url: string
|
||||
restaurant: string
|
||||
global_currency: string
|
||||
lat: string
|
||||
lng: string
|
||||
local_currency: string
|
||||
restaurant_iimage: string
|
||||
order_id: number
|
||||
items: Item[]
|
||||
itemsImagePrefix: string
|
||||
itemsImagePrefixOld: string
|
||||
}
|
||||
|
||||
export interface Item {
|
||||
id: number
|
||||
is_loyalty_used: number
|
||||
no_of_stamps_give: number
|
||||
isHasLoyalty: boolean
|
||||
is_vat_disabled: number
|
||||
name: string
|
||||
price: number
|
||||
qty: number
|
||||
variant_price: string
|
||||
image: string
|
||||
imageName: string
|
||||
variantName: string
|
||||
variantLocalName: string
|
||||
extras: any[]
|
||||
descriptionEN: string
|
||||
descriptionAR: string
|
||||
itemline: string
|
||||
itemlineAR: string
|
||||
itemlineAREN: string
|
||||
extrasgroups: any[]
|
||||
extragroupnew: any[]
|
||||
itemComment: string
|
||||
variant: any
|
||||
itemExtras: any[]
|
||||
AvaiilableVariantExtras: any[]
|
||||
isPrinted: number
|
||||
category_id: number
|
||||
pos_order_id: string
|
||||
updated_at: string
|
||||
created_at: string
|
||||
old_qty: number
|
||||
new_qty: number
|
||||
last_printed_qty: number
|
||||
deleted_qty: number
|
||||
discount_value: any
|
||||
discount_type_id: any
|
||||
original_price: number
|
||||
pricing_method: string
|
||||
is_already_paid: number
|
||||
hash_item: string
|
||||
}
|
||||
|
||||
export interface WorkingHours {
|
||||
opening_time: string
|
||||
closing_time: string
|
||||
opening_time_2: any
|
||||
closing_time_2: any
|
||||
is_open: boolean
|
||||
}
|
||||
|
||||
export interface Notes {
|
||||
en: string
|
||||
ar: string
|
||||
}
|
||||
|
||||
@@ -260,7 +260,6 @@ export default function RestaurantServices() {
|
||||
onClose={() => setIsGiftTypeBottomSheetOpen(false)}
|
||||
onSave={() => {
|
||||
setIsGiftTypeBottomSheetOpen(false);
|
||||
navigate(`/${subdomain}/menu?orderType=${OrderType.Gift}`);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -10,16 +10,13 @@ import styles from "./restaurant.module.css";
|
||||
import RestaurantServices from "./RestaurantServices";
|
||||
|
||||
// Import the Client Component for localStorage handling
|
||||
import Ads1 from "components/Ads/Ads1";
|
||||
import { OrderDetailsBottomSheet } from "components/CustomBottomSheet/orderDetailsSheet/OrderDetailsBottomSheet.tsx";
|
||||
import { Loader } from "components/Loader/Loader";
|
||||
import {
|
||||
CART_STORAGE_KEYS,
|
||||
updateOrderType,
|
||||
} from "features/order/orderSlice.ts";
|
||||
import useBreakPoint from "hooks/useBreakPoint";
|
||||
import { useRestaurant } from "hooks/useRestaurant";
|
||||
import useSwipeUp from "hooks/useSwipeUp";
|
||||
import { OrderType } from "pages/checkout/hooks/types.ts";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
@@ -41,9 +38,7 @@ export default function RestaurantPage() {
|
||||
const param = useParams();
|
||||
const [searchParams] = useSearchParams();
|
||||
const { pathname } = useLocation();
|
||||
const { orderType } = useAppSelector(
|
||||
(state) => state.order,
|
||||
);
|
||||
const { orderType } = useAppSelector((state) => state.order);
|
||||
const { isRTL } = useAppSelector((state) => state.locale);
|
||||
const { data: restaurant, isLoading } = useGetRestaurantDetailsQuery(
|
||||
param.subdomain,
|
||||
@@ -119,7 +114,7 @@ export default function RestaurantPage() {
|
||||
<Link to={`https://www.instagram.com/${restaurant?.instagram}`}>
|
||||
<InstagramIcon className={styles.socialIcon} />
|
||||
</Link>
|
||||
<Link to="https://x.com/">
|
||||
{/* <Link to="https://x.com/">
|
||||
<XIcon className={styles.socialIcon} />
|
||||
</Link>
|
||||
<Link to="https://www.snapchat.com/">
|
||||
@@ -127,7 +122,7 @@ export default function RestaurantPage() {
|
||||
</Link>
|
||||
<Link to="https://www.jordan.com/">
|
||||
<JIcon className={styles.socialIcon} />
|
||||
</Link>
|
||||
</Link> */}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
469
src/pages/rewardsAndLoyalty/page.tsx
Normal file
469
src/pages/rewardsAndLoyalty/page.tsx
Normal file
@@ -0,0 +1,469 @@
|
||||
import { Button, Card, Flex, Layout, Progress, Timeline, Tooltip } from "antd";
|
||||
|
||||
import ProHeader from "components/ProHeader/ProHeader";
|
||||
import ProText from "components/ProText";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import styles from "./rewardsAndLoyalty.module.css";
|
||||
|
||||
import { OrderType } from "pages/checkout/hooks/types.ts";
|
||||
import { useAppDispatch, useAppSelector } from "redux/hooks";
|
||||
import { selectCart, updateUseLoyaltyPoints } from "features/order/orderSlice";
|
||||
import CoinsIcon from "components/Icons/CoinsIcon";
|
||||
import ArabicPrice from "components/ArabicPrice";
|
||||
import RaiseIcon from "components/Icons/RaiseIcon";
|
||||
import CupIcon from "components/Icons/CupIcon";
|
||||
import ProInputCard from "components/ProInputCard/ProInputCard";
|
||||
import { useGetLoyaltyHistoryQuery } from "redux/api/others";
|
||||
import dayjs from "dayjs";
|
||||
import PopularIcon from "components/Icons/PopularIcon";
|
||||
|
||||
export default function RewardsAndLoyalityPage() {
|
||||
const dispatch = useAppDispatch();
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
const { subdomain } = useParams();
|
||||
|
||||
const { restaurant } = useAppSelector(selectCart);
|
||||
const loyaltyStamps = restaurant?.loyalty_stamps ?? 0;
|
||||
const customerLoyaltyPoints = restaurant?.customer_loyalty_points ?? 0;
|
||||
const { data: loyaltyHistory } = useGetLoyaltyHistoryQuery();
|
||||
const loyaltyHistories = loyaltyHistory?.filter(
|
||||
(LH) => Number(LH.restaurant_id) === Number(restaurant.restautantId),
|
||||
);
|
||||
|
||||
const handleRedeem = () => {
|
||||
navigate(`/${subdomain}/menu?orderType=${OrderType.DineIn}`);
|
||||
dispatch(updateUseLoyaltyPoints(true));
|
||||
};
|
||||
|
||||
const currentStampRound = customerLoyaltyPoints % loyaltyStamps;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Layout>
|
||||
<ProHeader>{t("rewardsAndLoyalty.title")}</ProHeader>
|
||||
<Layout.Content className={styles.redeemContainer}>
|
||||
<Flex gap="small" wrap justify="center">
|
||||
<Tooltip title="3 done / 3 in progress / 4 to do">
|
||||
<div style={{ margin: "47px 47px 20px 47px" }}>
|
||||
<Progress
|
||||
percent={(currentStampRound / loyaltyStamps) * 100}
|
||||
format={() => (
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
gap: 4,
|
||||
}}
|
||||
>
|
||||
<ProText
|
||||
style={{
|
||||
fontWeight: 600,
|
||||
fontStyle: "SemiBold",
|
||||
fontSize: 48,
|
||||
lineHeight: "100%",
|
||||
letterSpacing: "0%",
|
||||
textAlign: "center",
|
||||
}}
|
||||
>
|
||||
{currentStampRound} / {loyaltyStamps}
|
||||
</ProText>
|
||||
<ProText
|
||||
style={{
|
||||
fontWeight: 400,
|
||||
fontStyle: "Regular",
|
||||
fontSize: 16,
|
||||
lineHeight: "140%",
|
||||
letterSpacing: "0%",
|
||||
textAlign: "center",
|
||||
color: "#E8B400",
|
||||
marginTop: 4,
|
||||
}}
|
||||
>
|
||||
{t("rewardsAndLoyalty.completedPurchases")}
|
||||
</ProText>
|
||||
</div>
|
||||
)}
|
||||
strokeColor="#FFB700"
|
||||
size={250}
|
||||
type="circle"
|
||||
/>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</Flex>
|
||||
|
||||
<Flex
|
||||
gap="small"
|
||||
wrap={false}
|
||||
justify="center"
|
||||
style={{ width: "100%" }}
|
||||
>
|
||||
<div className={styles.orderNotes}>
|
||||
<div
|
||||
style={{
|
||||
height: 32,
|
||||
width: 32,
|
||||
minHeight: 32,
|
||||
minWidth: 32,
|
||||
backgroundColor: "var(--background)",
|
||||
borderRadius: "50%",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
}}
|
||||
>
|
||||
<CoinsIcon />
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: 4,
|
||||
width: "100%",
|
||||
}}
|
||||
>
|
||||
<ProText
|
||||
style={{
|
||||
fontWeight: 400,
|
||||
fontStyle: "Regular",
|
||||
fontSize: 12,
|
||||
lineHeight: "140%",
|
||||
letterSpacing: "0%",
|
||||
color: "#777580",
|
||||
}}
|
||||
>
|
||||
{t("rewardsAndLoyalty.saved")}
|
||||
</ProText>
|
||||
|
||||
<ProText
|
||||
style={{
|
||||
fontWeight: 500,
|
||||
fontStyle: "Medium",
|
||||
fontSize: 14,
|
||||
lineHeight: "140%",
|
||||
letterSpacing: "0%",
|
||||
color: "#333333",
|
||||
}}
|
||||
>
|
||||
<ArabicPrice price={customerLoyaltyPoints || 0} />
|
||||
</ProText>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.orderNotes}>
|
||||
<div
|
||||
style={{
|
||||
height: 32,
|
||||
width: 32,
|
||||
minHeight: 32,
|
||||
minWidth: 32,
|
||||
backgroundColor: "var(--background)",
|
||||
borderRadius: "50%",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
}}
|
||||
>
|
||||
<RaiseIcon />
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: 4,
|
||||
width: "100%",
|
||||
}}
|
||||
>
|
||||
<ProText
|
||||
style={{
|
||||
fontWeight: 400,
|
||||
fontStyle: "Regular",
|
||||
fontSize: 12,
|
||||
lineHeight: "140%",
|
||||
letterSpacing: "0%",
|
||||
color: "#777580",
|
||||
}}
|
||||
>
|
||||
{t("rewardsAndLoyalty.totalPurchased")}
|
||||
</ProText>
|
||||
|
||||
<ProText
|
||||
style={{
|
||||
fontWeight: 500,
|
||||
fontStyle: "Medium",
|
||||
fontSize: 14,
|
||||
lineHeight: "140%",
|
||||
letterSpacing: "0%",
|
||||
color: "#333333",
|
||||
}}
|
||||
>
|
||||
{loyaltyStamps}
|
||||
</ProText>
|
||||
</div>
|
||||
</div>
|
||||
</Flex>
|
||||
|
||||
<div className={styles.nextRewards}>
|
||||
<div
|
||||
style={{
|
||||
height: 42,
|
||||
width: 42,
|
||||
minHeight: 42,
|
||||
minWidth: 42,
|
||||
backgroundColor: "#FFF5D4",
|
||||
borderRadius: "50%",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
}}
|
||||
>
|
||||
<CupIcon />
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: 4,
|
||||
width: "100%",
|
||||
}}
|
||||
>
|
||||
<ProText
|
||||
style={{
|
||||
fontWeight: 500,
|
||||
fontStyle: "Medium",
|
||||
fontSize: 16,
|
||||
lineHeight: "140%",
|
||||
letterSpacing: "0%",
|
||||
}}
|
||||
>
|
||||
{t("rewardsAndLoyalty.almosthere")}
|
||||
</ProText>
|
||||
|
||||
<ProText
|
||||
style={{
|
||||
fontWeight: 400,
|
||||
fontStyle: "Regular",
|
||||
fontSize: 14,
|
||||
lineHeight: "140%",
|
||||
letterSpacing: "0%",
|
||||
color: "#777580",
|
||||
}}
|
||||
>
|
||||
{t("rewardsAndLoyalty.youreJustXCupsAwayFromYourNextReward", {
|
||||
cups: loyaltyStamps - (customerLoyaltyPoints % loyaltyStamps),
|
||||
})}
|
||||
</ProText>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ProInputCard title={t("rewardsAndLoyalty.yourAvailableRewards")}>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: 10,
|
||||
alignItems: "center",
|
||||
placeItems: "center",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
height: 42,
|
||||
width: 42,
|
||||
minHeight: 42,
|
||||
minWidth: 42,
|
||||
backgroundColor: "#FFF5D4",
|
||||
borderRadius: "50%",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
}}
|
||||
>
|
||||
<CupIcon />
|
||||
</div>
|
||||
<ProText
|
||||
style={{
|
||||
fontWeight: 600,
|
||||
fontStyle: "SemiBold",
|
||||
fontSize: 18,
|
||||
lineHeight: "140%",
|
||||
letterSpacing: "0%",
|
||||
textAlign: "center",
|
||||
}}
|
||||
>
|
||||
{t("rewardsAndLoyalty.youCurrentlyHave")}
|
||||
</ProText>
|
||||
<ProText
|
||||
style={{
|
||||
fontWeight: 600,
|
||||
fontStyle: "SemiBold",
|
||||
fontSize: 20,
|
||||
lineHeight: "140%",
|
||||
letterSpacing: "0%",
|
||||
textAlign: "center",
|
||||
color: "#E8B400",
|
||||
}}
|
||||
>
|
||||
<span style={{ padding: "0 4px" }}>
|
||||
{Math.floor(customerLoyaltyPoints / loyaltyStamps)}
|
||||
</span>
|
||||
{t("rewardsAndLoyalty.freeItems")}
|
||||
</ProText>
|
||||
<ProText
|
||||
style={{
|
||||
fontWeight: 400,
|
||||
fontStyle: "Regular",
|
||||
fontSize: 12,
|
||||
lineHeight: "140%",
|
||||
letterSpacing: "0%",
|
||||
textAlign: "center",
|
||||
color: "#777580",
|
||||
}}
|
||||
>
|
||||
{t("rewardsAndLoyalty.youCanRedeemDuringTheCheckout")}
|
||||
</ProText>
|
||||
<Button
|
||||
type="primary"
|
||||
shape="round"
|
||||
className={styles.checkoutButton}
|
||||
onClick={handleRedeem}
|
||||
style={{
|
||||
width: "100%",
|
||||
height: 40,
|
||||
borderRadius: 888,
|
||||
backgroundColor: "#FFB700",
|
||||
color: "white",
|
||||
}}
|
||||
>
|
||||
{t("rewardsAndLoyalty.redeemNow")}
|
||||
</Button>
|
||||
</div>
|
||||
</ProInputCard>
|
||||
|
||||
<ProInputCard title={t("rewardsAndLoyalty.loyaltyHistory")}>
|
||||
<Timeline
|
||||
items={
|
||||
loyaltyHistories && loyaltyHistories.length > 0
|
||||
? loyaltyHistories.map((item: any, index: number) => ({
|
||||
content: (
|
||||
<div key={index}>
|
||||
{/* <ProText
|
||||
style={{
|
||||
fontWeight: 500,
|
||||
fontStyle: "Medium",
|
||||
fontSize: 14,
|
||||
lineHeight: "140%",
|
||||
letterSpacing: "0%",
|
||||
color: "#333333",
|
||||
}}
|
||||
>
|
||||
{item.restaurant_name ||
|
||||
item.name ||
|
||||
item.restaurantName ||
|
||||
t("rewardsAndLoyalty.order")}
|
||||
</ProText> */}
|
||||
{(item.created_at ||
|
||||
item.date ||
|
||||
item.order_date ||
|
||||
true) && (
|
||||
<ProText
|
||||
style={{
|
||||
fontWeight: 600,
|
||||
fontStyle: "SemiBold",
|
||||
fontSize: 16,
|
||||
lineHeight: "140%",
|
||||
letterSpacing: "0%",
|
||||
color: "#777580",
|
||||
}}
|
||||
>
|
||||
{dayjs(
|
||||
item.created_at || item.date || item.order_date,
|
||||
).format("DD MMMM YYYY")}
|
||||
</ProText>
|
||||
)}
|
||||
<div className={styles.loyaltyHistoryItem}>
|
||||
<div
|
||||
style={{
|
||||
height: 30,
|
||||
width: 20,
|
||||
minHeight: 30,
|
||||
minWidth: 20,
|
||||
borderRadius: "50%",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
}}
|
||||
>
|
||||
<PopularIcon />
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: 4,
|
||||
width: "100%",
|
||||
}}
|
||||
>
|
||||
<ProText
|
||||
style={{
|
||||
fontWeight: 500,
|
||||
fontStyle: "Medium",
|
||||
fontSize: 16,
|
||||
lineHeight: "140%",
|
||||
letterSpacing: "0%",
|
||||
}}
|
||||
>
|
||||
{t("rewardsAndLoyalty.earned")}{" "}
|
||||
<ProText
|
||||
style={{
|
||||
fontWeight: 500,
|
||||
fontStyle: "Medium",
|
||||
fontSize: 16,
|
||||
lineHeight: "140%",
|
||||
letterSpacing: "0%",
|
||||
color: "#E8B400",
|
||||
}}
|
||||
>
|
||||
{t("rewardsAndLoyalty.xPoints", {
|
||||
points:
|
||||
item.total_points ||
|
||||
item.points ||
|
||||
item.loyalty_stamps,
|
||||
})}
|
||||
</ProText>
|
||||
</ProText>
|
||||
|
||||
<ProText
|
||||
style={{
|
||||
fontWeight: 400,
|
||||
fontStyle: "Regular",
|
||||
fontSize: 14,
|
||||
lineHeight: "140%",
|
||||
letterSpacing: "0%",
|
||||
color: "#777580",
|
||||
}}
|
||||
>
|
||||
{t("rewardsAndLoyalty.yourOrderFrom", {
|
||||
restaurantName:
|
||||
item.restaurant_name ||
|
||||
item.name ||
|
||||
item.restaurantName,
|
||||
})}
|
||||
</ProText>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
}))
|
||||
: []
|
||||
}
|
||||
/>
|
||||
</ProInputCard>
|
||||
</Layout.Content>
|
||||
</Layout>
|
||||
</>
|
||||
);
|
||||
}
|
||||
51
src/pages/rewardsAndLoyalty/rewardsAndLoyalty.module.css
Normal file
51
src/pages/rewardsAndLoyalty/rewardsAndLoyalty.module.css
Normal file
@@ -0,0 +1,51 @@
|
||||
.redeemContainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 16px;
|
||||
gap: 16px;
|
||||
overflow: auto;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
.orderNotes {
|
||||
gap: 20px;
|
||||
border-radius: 16px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
background-color: var(--secondary-background);
|
||||
padding: 16px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.nextRewards {
|
||||
gap: 20px;
|
||||
border-radius: 16px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
background-color: #fefbf2;
|
||||
padding: 16px;
|
||||
width: 100%;
|
||||
border: 1px solid #ffedb0;
|
||||
}
|
||||
|
||||
.loyaltyHistoryItem {
|
||||
gap: 20px;
|
||||
border-radius: 16px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
background-color: var(--background);
|
||||
width: 100%;
|
||||
width: 275;
|
||||
opacity: 1;
|
||||
padding-top: 14px;
|
||||
padding-right: 12px;
|
||||
padding-bottom: 14px;
|
||||
padding-left: 12px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
94
src/pages/rewardsAndLoyalty/types.ts
Normal file
94
src/pages/rewardsAndLoyalty/types.ts
Normal file
@@ -0,0 +1,94 @@
|
||||
export interface RedeemResponse {
|
||||
gift: Gift
|
||||
working_hours: WorkingHours
|
||||
notes: Notes
|
||||
is_restaurant_closed: boolean
|
||||
}
|
||||
|
||||
export interface Gift {
|
||||
id: number
|
||||
voucher_amount: string
|
||||
amount: string
|
||||
restorant_id: number
|
||||
voucher_code: string
|
||||
used_amount: string
|
||||
show_sender_info: number
|
||||
remaining_amount: string
|
||||
is_used: number
|
||||
sender_phone: string
|
||||
gift_type: string
|
||||
sender_name: string
|
||||
recipient_phone: string
|
||||
recipient_name: string
|
||||
message: any
|
||||
created_at: string
|
||||
card_url: string
|
||||
gr_url: string
|
||||
restaurant: string
|
||||
global_currency: string
|
||||
lat: string
|
||||
lng: string
|
||||
local_currency: string
|
||||
restaurant_iimage: string
|
||||
order_id: number
|
||||
items: Item[]
|
||||
itemsImagePrefix: string
|
||||
itemsImagePrefixOld: string
|
||||
}
|
||||
|
||||
export interface Item {
|
||||
id: number
|
||||
is_loyalty_used: number
|
||||
no_of_stamps_give: number
|
||||
isHasLoyalty: boolean
|
||||
is_vat_disabled: number
|
||||
name: string
|
||||
price: number
|
||||
qty: number
|
||||
variant_price: string
|
||||
image: string
|
||||
imageName: string
|
||||
variantName: string
|
||||
variantLocalName: string
|
||||
extras: any[]
|
||||
descriptionEN: string
|
||||
descriptionAR: string
|
||||
itemline: string
|
||||
itemlineAR: string
|
||||
itemlineAREN: string
|
||||
extrasgroups: any[]
|
||||
extragroupnew: any[]
|
||||
itemComment: string
|
||||
variant: any
|
||||
itemExtras: any[]
|
||||
AvaiilableVariantExtras: any[]
|
||||
isPrinted: number
|
||||
category_id: number
|
||||
pos_order_id: string
|
||||
updated_at: string
|
||||
created_at: string
|
||||
old_qty: number
|
||||
new_qty: number
|
||||
last_printed_qty: number
|
||||
deleted_qty: number
|
||||
discount_value: any
|
||||
discount_type_id: any
|
||||
original_price: number
|
||||
pricing_method: string
|
||||
is_already_paid: number
|
||||
hash_item: string
|
||||
}
|
||||
|
||||
export interface WorkingHours {
|
||||
opening_time: string
|
||||
closing_time: string
|
||||
opening_time_2: any
|
||||
closing_time_2: any
|
||||
is_open: boolean
|
||||
}
|
||||
|
||||
export interface Notes {
|
||||
en: string
|
||||
ar: string
|
||||
}
|
||||
|
||||
@@ -10,6 +10,11 @@ import {
|
||||
TABLES_URL,
|
||||
USER_DETAILS_URL,
|
||||
EGIFT_CARDS_URL,
|
||||
REDEEM_DETAILS_URL,
|
||||
LOYALTY_HISTORY_URL,
|
||||
CREATE_GIFT_AMOUNT_URL,
|
||||
CALL_WAITER_URL,
|
||||
OPENING_TIMES_URL,
|
||||
} from "utils/constants";
|
||||
|
||||
import { OrderDetails } from "pages/checkout/hooks/types";
|
||||
@@ -21,6 +26,8 @@ import {
|
||||
} from "utils/types/appTypes";
|
||||
import { baseApi } from "./apiSlice";
|
||||
import { EGiftCard } from "pages/EGiftCards/type";
|
||||
import { RedeemResponse } from "pages/redeem/types";
|
||||
import { OpeningTimeResponse } from "./types";
|
||||
|
||||
export const branchApi = baseApi.injectEndpoints({
|
||||
endpoints: (builder) => ({
|
||||
@@ -171,6 +178,57 @@ export const branchApi = baseApi.injectEndpoints({
|
||||
return response.result;
|
||||
},
|
||||
}),
|
||||
getRedeemDetails: builder.query<RedeemResponse, string>({
|
||||
query: (voucherId: string) => ({
|
||||
url: `${REDEEM_DETAILS_URL}/${voucherId}`,
|
||||
method: "GET",
|
||||
}),
|
||||
transformResponse: (response: any) => {
|
||||
return response.result;
|
||||
},
|
||||
}),
|
||||
getLoyaltyHistory: builder.query<
|
||||
{
|
||||
restaurant_id: number;
|
||||
restaurant_name: string;
|
||||
restaurant_icon: string;
|
||||
total_points: string;
|
||||
loyalty_stamp_image: string;
|
||||
loyalty_stamps: number;
|
||||
}[],
|
||||
void
|
||||
>({
|
||||
query: () => ({
|
||||
url: LOYALTY_HISTORY_URL,
|
||||
method: "GET",
|
||||
}),
|
||||
transformResponse: (response: any) => {
|
||||
return response.result.data.orders;
|
||||
},
|
||||
}),
|
||||
createGiftAmount: builder.mutation({
|
||||
query: (body: any) => ({
|
||||
url: CREATE_GIFT_AMOUNT_URL,
|
||||
method: "POST",
|
||||
body,
|
||||
}),
|
||||
}),
|
||||
getOpeningTimes: builder.query<OpeningTimeResponse, string | void>({
|
||||
query: (restaurantId: string) => ({
|
||||
url: OPENING_TIMES_URL +"/"+ restaurantId,
|
||||
method: "GET",
|
||||
}),
|
||||
transformResponse: (response: any) => {
|
||||
return response.result;
|
||||
},
|
||||
}),
|
||||
callWaiter: builder.mutation({
|
||||
query: (body: any) => ({
|
||||
url: CALL_WAITER_URL,
|
||||
method: "POST",
|
||||
body,
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
});
|
||||
export const {
|
||||
@@ -185,4 +243,9 @@ export const {
|
||||
useRateOrderMutation,
|
||||
useGetUserDetailsQuery,
|
||||
useGetEGiftCardsQuery,
|
||||
useGetRedeemDetailsQuery,
|
||||
useGetLoyaltyHistoryQuery,
|
||||
useCreateGiftAmountMutation,
|
||||
useGetOpeningTimesQuery,
|
||||
useCallWaiterMutation
|
||||
} = branchApi;
|
||||
|
||||
34
src/redux/api/types.ts
Normal file
34
src/redux/api/types.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
export interface OpeningTimeResponse {
|
||||
id: number;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
"0_from": string;
|
||||
"0_to": string;
|
||||
"1_from": string;
|
||||
"1_to": string;
|
||||
"2_from": string;
|
||||
"2_to": string;
|
||||
"3_from": string;
|
||||
"3_to": string;
|
||||
"4_from": string;
|
||||
"4_to": string;
|
||||
"5_from": string;
|
||||
"5_to": string;
|
||||
"6_from": string;
|
||||
"6_to": string;
|
||||
restorant_id: number;
|
||||
"2_0_from": any;
|
||||
"2_0_to": any;
|
||||
"2_1_from": any;
|
||||
"2_1_to": any;
|
||||
"2_2_from": any;
|
||||
"2_2_to": any;
|
||||
"2_3_from": any;
|
||||
"2_3_to": any;
|
||||
"2_4_from": any;
|
||||
"2_4_to": any;
|
||||
"2_5_from": any;
|
||||
"2_5_to": any;
|
||||
"2_6_from": any;
|
||||
"2_6_to": any;
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Grid } from "antd";
|
||||
import { Loader } from "components/Loader/Loader";
|
||||
import { PrivateRoute } from "components/privateRoute/PrivateRoute";
|
||||
import { PublicRoute } from "components/publicRoute/PublicRoute";
|
||||
import { AppLayout } from "layouts";
|
||||
import HeaderMenuDrawer from "layouts/app/HeaderMenuDrawer";
|
||||
import AddressPage from "pages/address/page";
|
||||
@@ -8,17 +8,17 @@ import CardDetailsPage from "pages/CardDetails/CardDetails";
|
||||
import CartPage from "pages/cart/page";
|
||||
import CheckoutPage from "pages/checkout/page";
|
||||
import EGiftCardsPage from "pages/EGiftCards/EGiftCards";
|
||||
import { Error400Page, ErrorPage } from "pages/errors";
|
||||
import { ErrorPage } from "pages/errors";
|
||||
import LoginPage from "pages/login/page";
|
||||
import MenuPage from "pages/menu/page";
|
||||
import OrderDetails from "pages/order/components/OrderDetails";
|
||||
import OrderPage from "pages/order/page";
|
||||
import OrdersPage from "pages/orders/page";
|
||||
import OtpPage from "pages/otp/page";
|
||||
import PayPage from "pages/pay/page";
|
||||
import ProductDetailPage from "pages/product/page";
|
||||
import RedeemPage from "pages/redeem/page";
|
||||
import RestaurantPage from "pages/restaurant/page";
|
||||
import RewardsAndLoyalityPage from "pages/rewardsAndLoyalty/page";
|
||||
import SearchPage from "pages/search/page";
|
||||
import SplitBillPage from "pages/split-bill/page";
|
||||
import React, { ReactNode, Suspense, useEffect } from "react";
|
||||
@@ -122,9 +122,9 @@ export const router = createHashRouter([
|
||||
element: (
|
||||
<PageWrapper
|
||||
children={
|
||||
<PrivateRoute>
|
||||
<PublicRoute>
|
||||
<OrdersPage />
|
||||
</PrivateRoute>
|
||||
</PublicRoute>
|
||||
}
|
||||
/>
|
||||
),
|
||||
@@ -132,13 +132,29 @@ export const router = createHashRouter([
|
||||
},
|
||||
{
|
||||
path: "login",
|
||||
element: <PageWrapper children={<LoginPage />} />,
|
||||
element: (
|
||||
<PageWrapper
|
||||
children={
|
||||
<PublicRoute>
|
||||
<LoginPage />
|
||||
</PublicRoute>
|
||||
}
|
||||
/>
|
||||
),
|
||||
errorElement: <ErrorPage />,
|
||||
},
|
||||
|
||||
{
|
||||
path: "otp",
|
||||
element: <PageWrapper children={<OtpPage />} />,
|
||||
element: (
|
||||
<PageWrapper
|
||||
children={
|
||||
<PublicRoute>
|
||||
<OtpPage />
|
||||
</PublicRoute>
|
||||
}
|
||||
/>
|
||||
),
|
||||
errorElement: <ErrorPage />,
|
||||
},
|
||||
{
|
||||
@@ -171,19 +187,10 @@ export const router = createHashRouter([
|
||||
element: <PageWrapper children={<RedeemPage />} />,
|
||||
errorElement: <ErrorPage />,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: "errors",
|
||||
path: "rewards-and-loyalty",
|
||||
element: <PageWrapper children={<RewardsAndLoyalityPage />} />,
|
||||
errorElement: <ErrorPage />,
|
||||
children: [
|
||||
{
|
||||
path: "400",
|
||||
element: (
|
||||
<PrivateRoute>
|
||||
<Error400Page />
|
||||
</PrivateRoute>
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -109,3 +109,8 @@ export const CONFIRM_OTP_URL = `${API_BASE_URL}confirmOtp`;
|
||||
export const PAYMENT_CONFIRMATION_URL = `https://menu.fascano.com/payment/confirmation`;
|
||||
export const DISCOUNT_URL = `${BASE_URL}getDiscount`;
|
||||
export const EGIFT_CARDS_URL = `${BASE_URL}gift/cards`;
|
||||
export const REDEEM_DETAILS_URL = `${BASE_URL}gift/getGiftOrderByVoucherCode`;
|
||||
export const LOYALTY_HISTORY_URL = `${BASE_URL}loyaltyHistory`;
|
||||
export const CREATE_GIFT_AMOUNT_URL = `${BASE_URL}gift/addGiftAmount`;
|
||||
export const OPENING_TIMES_URL = `${BASE_URL}restaurant/getWorkingHours`;
|
||||
export const CALL_WAITER_URL = `${BASE_URL}call_waiter`;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user