From 6271c14effa5370d5f4c8aebff98db2f752df74b Mon Sep 17 00:00:00 2001 From: Mohammed Al-yaseen Date: Thu, 8 Jan 2026 23:26:45 +0300 Subject: [PATCH] redeem: initial commit --- src/assets/locals/ar.json | 62 ++- src/assets/locals/en.json | 43 ++ src/components/Icons/CopyIcon.tsx | 33 ++ src/components/Icons/DirectionsIcon.tsx | 25 ++ src/components/Icons/GiftIcon.tsx | 45 ++ src/components/OrderSummary/OrderSummary.tsx | 124 ++++-- src/features/order/orderSlice.ts | 3 +- src/index.css | 3 + .../components/CartMobileTabletLayout.tsx | 5 +- .../cart/components/RewardWaiterCard.tsx | 2 +- src/pages/checkout/hooks/types.ts | 3 +- src/pages/checkout/page.tsx | 21 +- src/pages/menu/menu.module.css | 50 ++- src/pages/menu/page.tsx | 40 +- src/pages/order/components/OrderDetails.tsx | 9 +- .../components/GiftItemsCard.module.css | 77 ++++ src/pages/redeem/components/GiftItemsCard.tsx | 328 +++++++++++++++ .../redeem/components/LocationCard.module.css | 58 +++ src/pages/redeem/components/LocationCard.tsx | 94 +++++ .../components/VoucherBalanceCard.module.css | 58 +++ .../redeem/components/VoucherBalanceCard.tsx | 124 ++++++ .../VoucherSummary/VoucherSummary.module.css | 101 +++++ .../VoucherSummary/VoucherSummary.tsx | 90 ++++ src/pages/redeem/page.tsx | 389 ++++++++++-------- src/pages/redeem/redeem.module.css | 280 +++---------- src/routes/routes.tsx | 1 - 26 files changed, 1577 insertions(+), 491 deletions(-) create mode 100644 src/components/Icons/CopyIcon.tsx create mode 100644 src/components/Icons/DirectionsIcon.tsx create mode 100644 src/components/Icons/GiftIcon.tsx create mode 100644 src/pages/redeem/components/GiftItemsCard.module.css create mode 100644 src/pages/redeem/components/GiftItemsCard.tsx create mode 100644 src/pages/redeem/components/LocationCard.module.css create mode 100644 src/pages/redeem/components/LocationCard.tsx create mode 100644 src/pages/redeem/components/VoucherBalanceCard.module.css create mode 100644 src/pages/redeem/components/VoucherBalanceCard.tsx create mode 100644 src/pages/redeem/components/VoucherSummary/VoucherSummary.module.css create mode 100644 src/pages/redeem/components/VoucherSummary/VoucherSummary.tsx diff --git a/src/assets/locals/ar.json b/src/assets/locals/ar.json index 24fe0d8..d4dd557 100644 --- a/src/assets/locals/ar.json +++ b/src/assets/locals/ar.json @@ -274,7 +274,7 @@ "paymentSummary": "ملخص الدفع", "holdayGiftCard": "هدية العيد", "messageIncluded": "الرسالة مضمنة", - "save":"حفظ", + "save": "حفظ", "to": "ل", "giftSummary": "ملخص الهدية", "customerInformation": "تفاصيل العميل", @@ -308,8 +308,8 @@ "pickupEstimate": "تقدير الاستلام", "today": "اليوم", "change": "تغيير", - "pickup":"استلام", - "setPickupTime":"تحديد وقت الاستلام" + "pickup": "استلام", + "setPickupTime": "تحديد وقت الاستلام" }, "address": { "title": "العنوان", @@ -505,7 +505,7 @@ "yourName": "اسمك", "yourPhone": "رقم هاتفك", "keepMyNameSecret": "الاحتفاظ باسمي مخفياً", - "receiverInformation": "تفاصيل المستلم", + "receiverInformation": "تفاصيل المستلم", "costumeAmount": "مبلغ البطاقة", "enterCustomOucherAmount": "أدخل مبلغ البطاقة المخصص", "amount": "المبلغ", @@ -518,13 +518,51 @@ "senderNameRequired": "يجب أن يكون اسم المرسل مطلوب", "receiverNameRequired": "يجب أن يكون اسم المستلم مطلوب" }, - "car":{ - "addCar":"إضافة سيارة", - "selectCar":"اختر السيارة", - "addCarDetails":"إضافة تفاصيل السيارة", - "brand":"العلامة التجارية", - "color":"اللون", - "category":"الفئة", - "plateNumber":"رقم السيارة" + "car": { + "addCar": "إضافة سيارة", + "selectCar": "اختر السيارة", + "addCarDetails": "إضافة تفاصيل السيارة", + "brand": "العلامة التجارية", + "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": "عرض الكل" } } diff --git a/src/assets/locals/en.json b/src/assets/locals/en.json index 4928bad..ba3e5b1 100644 --- a/src/assets/locals/en.json +++ b/src/assets/locals/en.json @@ -179,6 +179,11 @@ "customizable": "Customizable" }, "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.", @@ -539,5 +544,43 @@ "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" } } diff --git a/src/components/Icons/CopyIcon.tsx b/src/components/Icons/CopyIcon.tsx new file mode 100644 index 0000000..be5ba84 --- /dev/null +++ b/src/components/Icons/CopyIcon.tsx @@ -0,0 +1,33 @@ +interface CopyIconType { + className?: string; + onClick?: () => void; +} + +const CopyIcon = ({ className, onClick }: CopyIconType) => { + return ( + + + + + ); +}; + +export default CopyIcon; diff --git a/src/components/Icons/DirectionsIcon.tsx b/src/components/Icons/DirectionsIcon.tsx new file mode 100644 index 0000000..7400fe3 --- /dev/null +++ b/src/components/Icons/DirectionsIcon.tsx @@ -0,0 +1,25 @@ +interface DirectionsIconType { + className?: string; + onClick?: () => void; +} + +const DirectionsIcon = ({ className, onClick }: DirectionsIconType) => { + return ( + + + + ); +}; + +export default DirectionsIcon; diff --git a/src/components/Icons/GiftIcon.tsx b/src/components/Icons/GiftIcon.tsx new file mode 100644 index 0000000..883993b --- /dev/null +++ b/src/components/Icons/GiftIcon.tsx @@ -0,0 +1,45 @@ +interface GiftIconType { + className?: string; + onClick?: () => void; +} + +const GiftIcon = ({ className, onClick }: GiftIconType) => { + return ( + + + + + + + + ); +}; + +export default GiftIcon; diff --git a/src/components/OrderSummary/OrderSummary.tsx b/src/components/OrderSummary/OrderSummary.tsx index e8c1021..dd0f362 100644 --- a/src/components/OrderSummary/OrderSummary.tsx +++ b/src/components/OrderSummary/OrderSummary.tsx @@ -25,7 +25,7 @@ export default function OrderSummary() { const { useLoyaltyPoints, splitBillAmount } = useAppSelector(selectCart); const { subdomain } = useParams(); const { data: restaurant } = useGetRestaurantDetailsQuery(subdomain); - const { orderType } = useAppSelector(selectCart); + const { orderType, tip } = useAppSelector(selectCart); const dispatch = useAppDispatch(); const subtotal = useAppSelector(selectCartTotal); const loyaltyValidation = useAppSelector(selectLoyaltyValidation); @@ -82,25 +82,62 @@ export default function OrderSummary() { /> )} -
- - {t("cart.discount")} - - -
-
- - {t("cart.tax")} - - -
- {splitBillAmount > 0 && ( + {orderType !== OrderType.Redeem && ( +
+ + {t("cart.discount")} + + +
+ )} + {orderType !== OrderType.Redeem && ( +
+ + {t("cart.tip")} + + +
+ )} + {orderType === OrderType.Redeem && ( +
+ + {t("cart.giftedItems")} + + +
+ )} + {orderType === OrderType.Redeem && ( +
+ + {t("cart.voucherApplied")} + + +
+ )} + {orderType !== OrderType.Redeem && ( +
+ + {t("cart.tax")} + + +
+ )} + {orderType !== OrderType.Redeem && splitBillAmount > 0 && (
{t("splitBill.splitBillAmount")} @@ -124,7 +161,9 @@ export default function OrderSummary() { color: "#333333", }} > - {t("cart.totalAmount")} + {orderType === OrderType.Redeem + ? t("cart.remainingToPay") + : t("cart.totalAmount")} - {isHasLoyaltyGift && restaurant?.is_loyalty_enabled === 1 && ( - <> - { - dispatch(updateUseLoyaltyPoints(value.target.checked)); - }} - style={{ marginTop: 8 }} - > - {t("cart.useLoyaltyPoints")} - - - )} - - {isHasLoyaltyGift && loyaltyValidation.errorMessage && ( -
- {t(loyaltyValidation.errorMessage)} -
- )} + {isHasLoyaltyGift && + restaurant?.is_loyalty_enabled === 1 && + orderType !== OrderType.Redeem && ( + <> + { + dispatch(updateUseLoyaltyPoints(value.target.checked)); + }} + style={{ marginTop: 8 }} + > + {t("cart.useLoyaltyPoints")} + + + )} {isHasLoyaltyGift && + loyaltyValidation.errorMessage && + orderType !== OrderType.Redeem && ( +
+ {t(loyaltyValidation.errorMessage)} +
+ )} + + {isHasLoyaltyGift && + orderType !== OrderType.Redeem && useLoyaltyPoints && highestLoyaltyItem && restaurant?.is_loyalty_enabled === 1 && ( diff --git a/src/features/order/orderSlice.ts b/src/features/order/orderSlice.ts index 548f6aa..3e5fd58 100644 --- a/src/features/order/orderSlice.ts +++ b/src/features/order/orderSlice.ts @@ -873,7 +873,8 @@ export const selectGrandTotal = (state: RootState) => { taxAmount - totalDiscount + deliveryFee - - state.order.splitBillAmount + state.order.splitBillAmount - + Number(state.order.tip) ); }; diff --git a/src/index.css b/src/index.css index 469213d..1ee4509 100644 --- a/src/index.css +++ b/src/index.css @@ -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 */ diff --git a/src/pages/cart/components/CartMobileTabletLayout.tsx b/src/pages/cart/components/CartMobileTabletLayout.tsx index 8c5c90b..8337cf6 100644 --- a/src/pages/cart/components/CartMobileTabletLayout.tsx +++ b/src/pages/cart/components/CartMobileTabletLayout.tsx @@ -23,6 +23,7 @@ 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"; interface CartMobileTabletLayoutProps { form: FormInstance; @@ -66,7 +67,9 @@ export default function CartMobileTabletLayout({ > {/* Table Number */} {(orderType === OrderType.DineIn || - orderType === OrderType.ToOffice) && } + orderType === OrderType.ToOffice) && } + + {orderType === OrderType.Redeem && }
diff --git a/src/pages/cart/components/RewardWaiterCard.tsx b/src/pages/cart/components/RewardWaiterCard.tsx index df40896..a653d75 100644 --- a/src/pages/cart/components/RewardWaiterCard.tsx +++ b/src/pages/cart/components/RewardWaiterCard.tsx @@ -17,7 +17,7 @@ export default function RewardWaiterCard() { const dispatch = useAppDispatch(); const { tip } = useAppSelector(selectCart); const { isDesktop } = useBreakPoint(); - const [selectedTip, setSelectedTip] = useState(null); + const [selectedTip, setSelectedTip] = useState(parseFloat(tip)); const [isTipOpen, setIsTipOpen] = useState(false); diff --git a/src/pages/checkout/hooks/types.ts b/src/pages/checkout/hooks/types.ts index 8876b27..25f5f18 100644 --- a/src/pages/checkout/hooks/types.ts +++ b/src/pages/checkout/hooks/types.ts @@ -246,5 +246,6 @@ export enum OrderType { ToRoom = "room", ToOffice = "office", Booking = "booking", - Pay = "pay" + Pay = "pay", + Redeem = "redeem" } diff --git a/src/pages/checkout/page.tsx b/src/pages/checkout/page.tsx index db097c6..379cb13 100644 --- a/src/pages/checkout/page.tsx +++ b/src/pages/checkout/page.tsx @@ -22,17 +22,25 @@ 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"; 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, + } = 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 }); + }, [form, phone, coupon, collectionMethod, customerName, tip]); return ( <> @@ -71,12 +79,13 @@ export default function CheckoutPage() { value={order?.officeNumber} /> )} + {orderType === OrderType.Redeem && } {/* {orderType === OrderType.Gift && } */} {/* */} {/* */} {/* */} - + {orderType !== OrderType.Redeem && } {/* Collection Method */} {orderType === OrderType.Pickup && ( @@ -114,7 +123,7 @@ export default function CheckoutPage() { )} {/* Reward Your Waiter */} - + {orderType !== OrderType.Redeem && } diff --git a/src/pages/menu/menu.module.css b/src/pages/menu/menu.module.css index d3a0465..218b970 100644 --- a/src/pages/menu/menu.module.css +++ b/src/pages/menu/menu.module.css @@ -483,9 +483,6 @@ position: absolute; z-index: 999; top: 70px; - opacity: 0.9; - background: #aaa8a833; - backdrop-filter: blur(40px); } .backButtonContainer { @@ -567,7 +564,7 @@ .ratingScore { position: relative; - top:6px; + top: 6px; font-family: Roboto; font-weight: 600; font-style: SemiBold; @@ -802,3 +799,48 @@ 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; +} diff --git a/src/pages/menu/page.tsx b/src/pages/menu/page.tsx index 90aa3cf..07726ea 100644 --- a/src/pages/menu/page.tsx +++ b/src/pages/menu/page.tsx @@ -96,21 +96,31 @@ function MenuPage() {
- false} + onClick={(e) => { + e.stopPropagation(); + setIsOrderTypesOpen(true); + }} + variant="borderless" + size="small" + className={styles.orderTypeSelect} + classNames={{ popup: { root: "order-type-select-dropdown" } }} + listHeight={150} + /> + )} + {orderType === OrderType.Redeem && ( +
+
+
Balance
+
60 OMR
+
+
+ )}
diff --git a/src/pages/order/components/OrderDetails.tsx b/src/pages/order/components/OrderDetails.tsx index 7b48191..6b73be3 100644 --- a/src/pages/order/components/OrderDetails.tsx +++ b/src/pages/order/components/OrderDetails.tsx @@ -3,11 +3,9 @@ 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"; @@ -16,6 +14,7 @@ export default function OrderDetails() { const { t } = useTranslation(); const { isRTL } = useAppSelector((state) => state.locale); const { isMobile, isTablet } = useBreakPoint(); + const { data: orderDetails } = useGetOrderDetailsQuery( { orderID: orderId || "", @@ -25,6 +24,7 @@ export default function OrderDetails() { skip: !orderId, }, ); + const getMenuItemImageStyle = () => { if (isMobile) { return { @@ -86,7 +86,6 @@ export default function OrderDetails() {
- {item.qty} + + {item.qty}{" "} +
diff --git a/src/pages/redeem/components/GiftItemsCard.module.css b/src/pages/redeem/components/GiftItemsCard.module.css new file mode 100644 index 0000000..b3387e0 --- /dev/null +++ b/src/pages/redeem/components/GiftItemsCard.module.css @@ -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; +} diff --git a/src/pages/redeem/components/GiftItemsCard.tsx b/src/pages/redeem/components/GiftItemsCard.tsx new file mode 100644 index 0000000..044dd2f --- /dev/null +++ b/src/pages/redeem/components/GiftItemsCard.tsx @@ -0,0 +1,328 @@ +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 } 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: orderDetails, isLoading } = useGetOrderDetailsQuery( + { + orderID: voucherId || "5711385", + restaurantID: localStorage.getItem("restaurantID") || "", + }, + // { + // 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 ( + <> + + {!isCart && ( + + {t("redeem.pending")} + + )} + + } + > + {!isCart && ( + <> +
+
+ + {t("redeem.redeemGiftedItems")} + + + + {t("redeem.includesFreeItemsInThisOrder")} + +
+ +
+ + + + )} + + {isLoading ? ( + // Skeleton loading state + <> + {[1, 2].map((skeletonIndex) => ( +
+ + +
+ + +
+
+ +
+
+
+ +
+
+ {skeletonIndex !== 3 && ( + + )} +
+ ))} + + ) : ( + orderDetails?.orderItems?.map((item: any, index: number) => ( +
+ + +
+ + {item.name} + + {/* {isRTL + ? (item.variant as Variant)?.optionsAR?.[0]?.value + : (item.variant as Variant)?.options?.[0]?.value} */} + + +
+ + {item.name} + +
+
+ +
+
+
+ +
+
+ + {index !== orderDetails?.orderItems?.length - 1 && ( + + )} +
+ )) || null + )} + + {!isCart && ( + <> + + + + + )} +
+ + ); +} diff --git a/src/pages/redeem/components/LocationCard.module.css b/src/pages/redeem/components/LocationCard.module.css new file mode 100644 index 0000000..2ac5ad2 --- /dev/null +++ b/src/pages/redeem/components/LocationCard.module.css @@ -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; +} diff --git a/src/pages/redeem/components/LocationCard.tsx b/src/pages/redeem/components/LocationCard.tsx new file mode 100644 index 0000000..a4234bb --- /dev/null +++ b/src/pages/redeem/components/LocationCard.tsx @@ -0,0 +1,94 @@ +import { Divider, Tag } from "antd"; +import ProInputCard from "components/ProInputCard/ProInputCard"; +import ProText from "components/ProText"; +import { selectCart } from "features/order/orderSlice"; +import { useTranslation } from "react-i18next"; +import { useAppSelector } from "redux/hooks"; +import styles from "./LocationCard.module.css"; +import { GoogleMap } from "components/CustomBottomSheet/GoogleMap"; +import DirectionsIcon from "components/Icons/DirectionsIcon"; + +export function LocationCard() { + const { t } = useTranslation(); + const { restaurant } = useAppSelector(selectCart); + + return ( + <> + +
+ +
+ + + +
+
+ + {restaurant.restautantName} + + + + {restaurant.address} + +
+ + + + {t("redeem.getDirections")} + + +
+
+ + ); +} diff --git a/src/pages/redeem/components/VoucherBalanceCard.module.css b/src/pages/redeem/components/VoucherBalanceCard.module.css new file mode 100644 index 0000000..2ac5ad2 --- /dev/null +++ b/src/pages/redeem/components/VoucherBalanceCard.module.css @@ -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; +} diff --git a/src/pages/redeem/components/VoucherBalanceCard.tsx b/src/pages/redeem/components/VoucherBalanceCard.tsx new file mode 100644 index 0000000..65cc299 --- /dev/null +++ b/src/pages/redeem/components/VoucherBalanceCard.tsx @@ -0,0 +1,124 @@ +import { Divider, Switch, Tag } from "antd"; +import ProInputCard from "components/ProInputCard/ProInputCard"; +import ProText from "components/ProText"; +import { selectCart } from "features/order/orderSlice"; +import { useTranslation } from "react-i18next"; +import { useAppSelector } from "redux/hooks"; +import styles from "./VoucherBalanceCard.module.css"; +import { useNavigate, useParams } from "react-router-dom"; +import CardAmountIcon from "components/Icons/CardAmountIcon"; +import ArabicPrice from "components/ArabicPrice"; + +export function VoucherBalanceCard() { + const { t } = useTranslation(); + const { giftDetails } = useAppSelector(selectCart); + + const navigate = useNavigate(); + const { subdomain } = useParams(); + + return ( + <> + + + {t("redeem.pending")} + + + } + > +
+
+ +
+
+ + {t("redeem.yourGiftCardBalance")} + + + + + +
+ +
+ + +
{ + navigate(`/${subdomain}/cart`); + }} + > + + {t("redeem.voucherWillBeAppliedAtCheckout")} + +
+
+ + ); +} diff --git a/src/pages/redeem/components/VoucherSummary/VoucherSummary.module.css b/src/pages/redeem/components/VoucherSummary/VoucherSummary.module.css new file mode 100644 index 0000000..e5b006f --- /dev/null +++ b/src/pages/redeem/components/VoucherSummary/VoucherSummary.module.css @@ -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; + } +} diff --git a/src/pages/redeem/components/VoucherSummary/VoucherSummary.tsx b/src/pages/redeem/components/VoucherSummary/VoucherSummary.tsx new file mode 100644 index 0000000..4585e56 --- /dev/null +++ b/src/pages/redeem/components/VoucherSummary/VoucherSummary.tsx @@ -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 ( + <> + + + {t("cart.voucherSummary")} + + + +
+ + {t("cart.voucherBalance")} + + +
+
+ + {t("cart.voucherApplied")} + + +
+ +
+ + {t("cart.remainingVoucherAmount")} + + +
+
+
+ + ); +} diff --git a/src/pages/redeem/page.tsx b/src/pages/redeem/page.tsx index 8fe0e1b..ca179b5 100644 --- a/src/pages/redeem/page.tsx +++ b/src/pages/redeem/page.tsx @@ -1,16 +1,7 @@ -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, Image, Layout, Skeleton } 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"; @@ -18,31 +9,30 @@ import { useGetOrderDetailsQuery, useGetRestaurantDetailsQuery, } from "redux/api/others"; -import { useAppSelector } from "redux/hooks"; 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 QRIcon from "components/Icons/QRIcon"; +import CopyIcon from "components/Icons/CopyIcon"; +import { useAppSelector } from "redux/hooks"; +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"; export default function RedeemPage() { const { t } = useTranslation(); - const { orderId } = useParams(); - const { isRTL } = useAppSelector((state) => state.locale); + const { voucherId } = useParams(); const { restaurant } = useAppSelector((state) => state.order); - const navigate = useNavigate(); const hasRefetchedRef = useRef(false); + const navigate = useNavigate(); + const { subdomain } = useParams(); - const { data: orderDetails } = useGetOrderDetailsQuery( + const { data: orderDetails, isLoading } = useGetOrderDetailsQuery( { - orderID: orderId || "", + orderID: voucherId || "", restaurantID: localStorage.getItem("restaurantID") || "", }, { - skip: !orderId, - // return it t0 60000 after finish testing - pollingInterval: 10000, - refetchOnMountOrArgChange: true, + skip: !voucherId, }, ); @@ -58,7 +48,7 @@ export default function RedeemPage() { // Reset refetch flag when orderId changes useEffect(() => { hasRefetchedRef.current = false; - }, [orderId]); + }, [voucherId]); // Refetch restaurant details when order status has alias "closed" useEffect(() => { @@ -74,190 +64,225 @@ export default function RedeemPage() { } }, [orderDetails?.status, restaurantSubdomain, refetchRestaurantDetails]); + const handleCheckout = () => { + navigate(`/${subdomain}/menu?orderType=${OrderType.Redeem}`); + }; + return ( <> - {t("order.title")} -
- + + {t("redeem.title")} +
- -
- - {t("order.yourOrderFromFascanoRestaurant")} - -
- - {" "} - {isRTL ? orderDetails?.restaurantAR : orderDetails?.restaurant} - -
+ {t("redeem.addGiftDetails")} + + + {t("redeem.description")} +
- + {isLoading || !orderDetails?.restaurant_iimage ? ( +
+
+ +
+
+ ) : ( +
+
+ +
+
+ )} -
- + + {t("redeem.description")} + + - {t("order.inProgressOrder")} (1) - -
- - - #{orderDetails?.order.id} - - - - ordered :- Today -{" "} - {dayjs(orderDetails?.status[0]?.pivot?.created_at).format( - "h:mm A", - )} - -
- - - - + {t("redeem.addGiftDetails")} +
-
- - - - - {t("order.yourOrderFrom")} - -
- - {" "} - {t("order.muscat")} - -
- } - > -
- {orderDetails?.orderItems.map((item, index) => ( -
-
- -
- - {item.name} - -
-
-
- ))} -
- - - - - navigate(`/${restaurant?.subdomain}`)} - > -
- - - - {isRTL ? restaurant?.nameAR : restaurant?.restautantName} - + {t("redeem.showThisCodeAtTheRestaurant")} + + +
+ + + {t("redeem.useThisCodeIfScanningNotPossible")} + +
+ - {isRTL ? ( - - ) : ( - - )} +
+ + Active - Expires in 12 days +
- - {/* */} +
+ + + +
+ - -
+ + + + ); } diff --git a/src/pages/redeem/redeem.module.css b/src/pages/redeem/redeem.module.css index 78b105e..d715277 100644 --- a/src/pages/redeem/redeem.module.css +++ b/src/pages/redeem/redeem.module.css @@ -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; -} - - diff --git a/src/routes/routes.tsx b/src/routes/routes.tsx index 793c892..533dbf4 100644 --- a/src/routes/routes.tsx +++ b/src/routes/routes.tsx @@ -15,7 +15,6 @@ 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";