redeem: initial commit

This commit is contained in:
2026-01-08 23:26:45 +03:00
parent ebe9928091
commit 6271c14eff
26 changed files with 1577 additions and 491 deletions

View File

@@ -274,7 +274,7 @@
"paymentSummary": "ملخص الدفع", "paymentSummary": "ملخص الدفع",
"holdayGiftCard": "هدية العيد", "holdayGiftCard": "هدية العيد",
"messageIncluded": "الرسالة مضمنة", "messageIncluded": "الرسالة مضمنة",
"save":"حفظ", "save": "حفظ",
"to": "ل", "to": "ل",
"giftSummary": "ملخص الهدية", "giftSummary": "ملخص الهدية",
"customerInformation": "تفاصيل العميل", "customerInformation": "تفاصيل العميل",
@@ -308,8 +308,8 @@
"pickupEstimate": "تقدير الاستلام", "pickupEstimate": "تقدير الاستلام",
"today": "اليوم", "today": "اليوم",
"change": "تغيير", "change": "تغيير",
"pickup":"استلام", "pickup": "استلام",
"setPickupTime":"تحديد وقت الاستلام" "setPickupTime": "تحديد وقت الاستلام"
}, },
"address": { "address": {
"title": "العنوان", "title": "العنوان",
@@ -505,7 +505,7 @@
"yourName": "اسمك", "yourName": "اسمك",
"yourPhone": "رقم هاتفك", "yourPhone": "رقم هاتفك",
"keepMyNameSecret": "الاحتفاظ باسمي مخفياً", "keepMyNameSecret": "الاحتفاظ باسمي مخفياً",
"receiverInformation": "تفاصيل المستلم", "receiverInformation": "تفاصيل المستلم",
"costumeAmount": "مبلغ البطاقة", "costumeAmount": "مبلغ البطاقة",
"enterCustomOucherAmount": "أدخل مبلغ البطاقة المخصص", "enterCustomOucherAmount": "أدخل مبلغ البطاقة المخصص",
"amount": "المبلغ", "amount": "المبلغ",
@@ -518,13 +518,51 @@
"senderNameRequired": "يجب أن يكون اسم المرسل مطلوب", "senderNameRequired": "يجب أن يكون اسم المرسل مطلوب",
"receiverNameRequired": "يجب أن يكون اسم المستلم مطلوب" "receiverNameRequired": "يجب أن يكون اسم المستلم مطلوب"
}, },
"car":{ "car": {
"addCar":"إضافة سيارة", "addCar": "إضافة سيارة",
"selectCar":"اختر السيارة", "selectCar": "اختر السيارة",
"addCarDetails":"إضافة تفاصيل السيارة", "addCarDetails": "إضافة تفاصيل السيارة",
"brand":"العلامة التجارية", "brand": "العلامة التجارية",
"color":"اللون", "color": "اللون",
"category":"الفئة", "category": "الفئة",
"plateNumber":"رقم السيارة" "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": "عرض الكل"
} }
} }

View File

@@ -179,6 +179,11 @@
"customizable": "Customizable" "customizable": "Customizable"
}, },
"cart": { "cart": {
"remainingToPay": "Remaining to Pay",
"remainingVoucherAmount": "Remaining Voucher Amount",
"voucherApplied": "Voucher Applied",
"giftedItems": "Gifted Items",
"voucherBalance": "Voucher Balance",
"addSpecialRequestOptional": "Add Special Request (Optional)", "addSpecialRequestOptional": "Add Special Request (Optional)",
"continueToGiftDetails": "Continue to Gift Details", "continueToGiftDetails": "Continue to Gift Details",
"continueToGiftDetailsDescription": "Please fill in the details of the gift recipient and sender to continue.", "continueToGiftDetailsDescription": "Please fill in the details of the gift recipient and sender to continue.",
@@ -539,5 +544,43 @@
"color": "Color", "color": "Color",
"category": "Category", "category": "Category",
"plateNumber": "Plate Number" "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"
} }
} }

View 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;

View 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;

View 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;

View File

@@ -25,7 +25,7 @@ export default function OrderSummary() {
const { useLoyaltyPoints, splitBillAmount } = useAppSelector(selectCart); const { useLoyaltyPoints, splitBillAmount } = useAppSelector(selectCart);
const { subdomain } = useParams(); const { subdomain } = useParams();
const { data: restaurant } = useGetRestaurantDetailsQuery(subdomain); const { data: restaurant } = useGetRestaurantDetailsQuery(subdomain);
const { orderType } = useAppSelector(selectCart); const { orderType, tip } = useAppSelector(selectCart);
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const subtotal = useAppSelector(selectCartTotal); const subtotal = useAppSelector(selectCartTotal);
const loyaltyValidation = useAppSelector(selectLoyaltyValidation); const loyaltyValidation = useAppSelector(selectLoyaltyValidation);
@@ -82,25 +82,62 @@ export default function OrderSummary() {
/> />
</div> </div>
)} )}
<div className={styles.summaryRow}> {orderType !== OrderType.Redeem && (
<ProText type="secondary" style={titlesStyle}> <div className={styles.summaryRow}>
{t("cart.discount")} <ProText type="secondary" style={titlesStyle}>
</ProText> {t("cart.discount")}
<ArabicPrice </ProText>
price={discountAmount} <ArabicPrice
style={{ ...titlesStyle, color: "#434E5C" }} price={discountAmount}
/> style={{ ...titlesStyle, color: "#434E5C" }}
</div> />
<div className={styles.summaryRow}> </div>
<ProText type="secondary" style={titlesStyle}> )}
{t("cart.tax")} {orderType !== OrderType.Redeem && (
</ProText> <div className={styles.summaryRow}>
<ArabicPrice <ProText type="secondary" style={titlesStyle}>
price={taxAmount || 0} {t("cart.tip")}
style={{ ...titlesStyle, color: "#434E5C" }} </ProText>
/> <ArabicPrice
</div> price={tip || 0}
{splitBillAmount > 0 && ( style={{ ...titlesStyle, color: "#434E5C" }}
/>
</div>
)}
{orderType === OrderType.Redeem && (
<div className={styles.summaryRow}>
<ProText type="secondary" style={titlesStyle}>
{t("cart.giftedItems")}
</ProText>
<ArabicPrice
price={0}
style={{ ...titlesStyle, color: "#434E5C" }}
/>
</div>
)}
{orderType === OrderType.Redeem && (
<div className={styles.summaryRow}>
<ProText type="secondary" style={titlesStyle}>
{t("cart.voucherApplied")}
</ProText>
<ArabicPrice
price={0}
style={{ ...titlesStyle, color: "#32AD6D" }}
/>
</div>
)}
{orderType !== OrderType.Redeem && (
<div className={styles.summaryRow}>
<ProText type="secondary" style={titlesStyle}>
{t("cart.tax")}
</ProText>
<ArabicPrice
price={taxAmount || 0}
style={{ ...titlesStyle, color: "#434E5C" }}
/>
</div>
)}
{orderType !== OrderType.Redeem && splitBillAmount > 0 && (
<div className={styles.summaryRow}> <div className={styles.summaryRow}>
<ProText type="secondary" style={titlesStyle}> <ProText type="secondary" style={titlesStyle}>
{t("splitBill.splitBillAmount")} {t("splitBill.splitBillAmount")}
@@ -124,7 +161,9 @@ export default function OrderSummary() {
color: "#333333", color: "#333333",
}} }}
> >
{t("cart.totalAmount")} {orderType === OrderType.Redeem
? t("cart.remainingToPay")
: t("cart.totalAmount")}
</ProText> </ProText>
<ArabicPrice <ArabicPrice
price={grandTotal} price={grandTotal}
@@ -140,27 +179,32 @@ export default function OrderSummary() {
</div> </div>
</Space> </Space>
{isHasLoyaltyGift && restaurant?.is_loyalty_enabled === 1 && ( {isHasLoyaltyGift &&
<> restaurant?.is_loyalty_enabled === 1 &&
<Checkbox orderType !== OrderType.Redeem && (
checked={useLoyaltyPoints} <>
onChange={(value) => { <Checkbox
dispatch(updateUseLoyaltyPoints(value.target.checked)); checked={useLoyaltyPoints}
}} onChange={(value) => {
style={{ marginTop: 8 }} dispatch(updateUseLoyaltyPoints(value.target.checked));
> }}
{t("cart.useLoyaltyPoints")} style={{ marginTop: 8 }}
</Checkbox> >
</> {t("cart.useLoyaltyPoints")}
)} </Checkbox>
</>
{isHasLoyaltyGift && loyaltyValidation.errorMessage && ( )}
<div style={{ marginTop: 8, color: "red", fontSize: "12px" }}>
{t(loyaltyValidation.errorMessage)}
</div>
)}
{isHasLoyaltyGift && {isHasLoyaltyGift &&
loyaltyValidation.errorMessage &&
orderType !== OrderType.Redeem && (
<div style={{ marginTop: 8, color: "red", fontSize: "12px" }}>
{t(loyaltyValidation.errorMessage)}
</div>
)}
{isHasLoyaltyGift &&
orderType !== OrderType.Redeem &&
useLoyaltyPoints && useLoyaltyPoints &&
highestLoyaltyItem && highestLoyaltyItem &&
restaurant?.is_loyalty_enabled === 1 && ( restaurant?.is_loyalty_enabled === 1 && (

View File

@@ -873,7 +873,8 @@ export const selectGrandTotal = (state: RootState) => {
taxAmount - taxAmount -
totalDiscount + totalDiscount +
deliveryFee - deliveryFee -
state.order.splitBillAmount state.order.splitBillAmount -
Number(state.order.tip)
); );
}; };

View File

@@ -37,6 +37,9 @@
0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px -1px rgba(0, 0, 0, 0.1); 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-offset-shadow: 0 0 rgba(0, 0, 0, 0);
--tw-ring-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 */ /* Dark theme variables */

View File

@@ -23,6 +23,7 @@ import { useTranslation } from "react-i18next";
import { Variant } from "utils/types/appTypes"; import { Variant } from "utils/types/appTypes";
import DeleteIcon from "components/Icons/DeleteIcon"; import DeleteIcon from "components/Icons/DeleteIcon";
import PlusIcon from "components/Icons/PlusIcon"; import PlusIcon from "components/Icons/PlusIcon";
import { GiftItemsCard } from "pages/redeem/components/GiftItemsCard";
interface CartMobileTabletLayoutProps { interface CartMobileTabletLayoutProps {
form: FormInstance; form: FormInstance;
@@ -66,7 +67,9 @@ export default function CartMobileTabletLayout({
> >
{/* Table Number */} {/* Table Number */}
{(orderType === OrderType.DineIn || {(orderType === OrderType.DineIn ||
orderType === OrderType.ToOffice) && <TableNumberCard />} orderType === OrderType.ToOffice) && <TableNumberCard />}
{orderType === OrderType.Redeem && <GiftItemsCard isCart={true} />}
<div className={`${styles.cartContent} ${getResponsiveClass()}`}> <div className={`${styles.cartContent} ${getResponsiveClass()}`}>
<Card className={styles.cartItem}> <Card className={styles.cartItem}>

View File

@@ -17,7 +17,7 @@ export default function RewardWaiterCard() {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const { tip } = useAppSelector(selectCart); const { tip } = useAppSelector(selectCart);
const { isDesktop } = useBreakPoint(); const { isDesktop } = useBreakPoint();
const [selectedTip, setSelectedTip] = useState<number | null>(null); const [selectedTip, setSelectedTip] = useState<number | null>(parseFloat(tip));
const [isTipOpen, setIsTipOpen] = useState(false); const [isTipOpen, setIsTipOpen] = useState(false);

View File

@@ -246,5 +246,6 @@ export enum OrderType {
ToRoom = "room", ToRoom = "room",
ToOffice = "office", ToOffice = "office",
Booking = "booking", Booking = "booking",
Pay = "pay" Pay = "pay",
Redeem = "redeem"
} }

View File

@@ -22,17 +22,25 @@ import { useEffect } from "react";
import { CarCard } from "./components/CarCard"; import { CarCard } from "./components/CarCard";
import { CollectWay } from "./components/CollectWay/CollectWay"; import { CollectWay } from "./components/CollectWay/CollectWay";
import PickupTimeCard from "./components/pickupEstimate/TimeEstimateCard"; import PickupTimeCard from "./components/pickupEstimate/TimeEstimateCard";
import VoucherSummary from "pages/redeem/components/VoucherSummary/VoucherSummary";
export default function CheckoutPage() { export default function CheckoutPage() {
const { t } = useTranslation(); const { t } = useTranslation();
const [form] = Form.useForm(); const [form] = Form.useForm();
const { phone, order, orderType, collectionMethod, coupon, customerName } = const {
useAppSelector(selectCart); phone,
order,
orderType,
collectionMethod,
coupon,
customerName,
tip,
} = useAppSelector(selectCart);
const { token } = useAppSelector((state) => state.auth); const { token } = useAppSelector((state) => state.auth);
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
useEffect(() => { useEffect(() => {
form.setFieldsValue({ coupon, collectionMethod, phone, customerName }); form.setFieldsValue({ coupon, collectionMethod, phone, customerName, tip });
}, [form, phone, coupon, collectionMethod, customerName]); }, [form, phone, coupon, collectionMethod, customerName, tip]);
return ( return (
<> <>
@@ -71,12 +79,13 @@ export default function CheckoutPage() {
value={order?.officeNumber} value={order?.officeNumber}
/> />
)} )}
{orderType === OrderType.Redeem && <VoucherSummary />}
{/* {orderType === OrderType.Gift && <GiftCard />} */} {/* {orderType === OrderType.Gift && <GiftCard />} */}
{/* <RoomDetails /> {/* <RoomDetails />
<OfficeDetails /> */} <OfficeDetails /> */}
{/* <GiftDetails /> */} {/* <GiftDetails /> */}
{/* <BriefMenu /> */} {/* <BriefMenu /> */}
<CouponCard /> {orderType !== OrderType.Redeem && <CouponCard />}
{/* Collection Method */} {/* Collection Method */}
{orderType === OrderType.Pickup && ( {orderType === OrderType.Pickup && (
@@ -114,7 +123,7 @@ export default function CheckoutPage() {
)} )}
{/* Reward Your Waiter */} {/* Reward Your Waiter */}
<RewardWaiterCard /> {orderType !== OrderType.Redeem && <RewardWaiterCard />}
<BriefMenuCard /> <BriefMenuCard />
<Ads1 /> <Ads1 />
<OrderSummary /> <OrderSummary />

View File

@@ -483,9 +483,6 @@
position: absolute; position: absolute;
z-index: 999; z-index: 999;
top: 70px; top: 70px;
opacity: 0.9;
background: #aaa8a833;
backdrop-filter: blur(40px);
} }
.backButtonContainer { .backButtonContainer {
@@ -567,7 +564,7 @@
.ratingScore { .ratingScore {
position: relative; position: relative;
top:6px; top: 6px;
font-family: Roboto; font-family: Roboto;
font-weight: 600; font-weight: 600;
font-style: SemiBold; font-style: SemiBold;
@@ -802,3 +799,48 @@
display: none !important; 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;
}

View File

@@ -96,21 +96,31 @@ function MenuPage() {
<div <div
className={`${styles.headerFloatingBtn} ${styles.orderTypeSelectContainer} order-type-select-container`} className={`${styles.headerFloatingBtn} ${styles.orderTypeSelectContainer} order-type-select-container`}
> >
<Select {orderType !== OrderType.Redeem && (
value={orderType} <Select
options={orderTypeOptions} value={orderType}
open={false} options={orderTypeOptions}
onOpenChange={() => false} open={false}
onClick={(e) => { onOpenChange={() => false}
e.stopPropagation(); onClick={(e) => {
setIsOrderTypesOpen(true); e.stopPropagation();
}} setIsOrderTypesOpen(true);
variant="borderless" }}
size="small" variant="borderless"
className={styles.orderTypeSelect} size="small"
classNames={{ popup: { root: "order-type-select-dropdown" } }} className={styles.orderTypeSelect}
listHeight={150} classNames={{ popup: { root: "order-type-select-dropdown" } }}
/> listHeight={150}
/>
)}
{orderType === OrderType.Redeem && (
<div className={styles.frame}>
<div className={styles.div}>
<div className={styles.pickup}>Balance</div>
<div className={styles.elementMin}>60 OMR</div>
</div>
</div>
)}
</div> </div>
<SearchButton /> <SearchButton />
</div> </div>

View File

@@ -3,11 +3,9 @@ import { useGetOrderDetailsQuery } from "redux/api/others";
import { useAppSelector } from "redux/hooks"; import { useAppSelector } from "redux/hooks";
import styles from "./OrderDetails.module.css"; import styles from "./OrderDetails.module.css";
import ProHeader from "components/ProHeader/ProHeader"; import ProHeader from "components/ProHeader/ProHeader";
import { useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import ProText from "components/ProText"; import ProText from "components/ProText";
import useBreakPoint from "hooks/useBreakPoint"; import useBreakPoint from "hooks/useBreakPoint";
import ArabicPrice from "components/ArabicPrice";
import ImageWithFallback from "components/ImageWithFallback"; import ImageWithFallback from "components/ImageWithFallback";
import { useParams } from "react-router-dom"; import { useParams } from "react-router-dom";
@@ -16,6 +14,7 @@ export default function OrderDetails() {
const { t } = useTranslation(); const { t } = useTranslation();
const { isRTL } = useAppSelector((state) => state.locale); const { isRTL } = useAppSelector((state) => state.locale);
const { isMobile, isTablet } = useBreakPoint(); const { isMobile, isTablet } = useBreakPoint();
const { data: orderDetails } = useGetOrderDetailsQuery( const { data: orderDetails } = useGetOrderDetailsQuery(
{ {
orderID: orderId || "", orderID: orderId || "",
@@ -25,6 +24,7 @@ export default function OrderDetails() {
skip: !orderId, skip: !orderId,
}, },
); );
const getMenuItemImageStyle = () => { const getMenuItemImageStyle = () => {
if (isMobile) { if (isMobile) {
return { return {
@@ -86,7 +86,6 @@ export default function OrderDetails() {
<br /> <br />
<ProText <ProText
type="secondary" type="secondary"
className={`${styles.itemDescription} responsive-text`}
style={{ style={{
margin: 0, margin: 0,
lineClamp: 1, lineClamp: 1,
@@ -135,7 +134,9 @@ export default function OrderDetails() {
border: "none", border: "none",
}} }}
> >
<ProText style={{color: "#1F1C2E"}}>{item.qty} </ProText> <ProText style={{ color: "#1F1C2E" }}>
{item.qty}{" "}
</ProText>
</Button> </Button>
</div> </div>
</Space> </Space>

View 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;
}

View File

@@ -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 (
<>
<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>
))}
</>
) : (
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%",
}}
>
<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 !== orderDetails?.orderItems?.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>
</>
);
}

View 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;
}

View File

@@ -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 (
<>
<ProInputCard title={t("redeem.restaurantLocation")}>
<div className={styles.mapContainer}>
<GoogleMap
readOnly={true}
initialLocation={{
lat: parseFloat(restaurant.lat || "0"),
lng: parseFloat(restaurant.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",
}}
>
{restaurant.restautantName}
</ProText>
<ProText
style={{
fontWeight: 500,
fontStyle: "Medium",
fontSize: 14,
lineHeight: "140%",
letterSpacing: "0%",
color: "#777580",
}}
>
{restaurant.address}
</ProText>
</div>
<Tag
style={{
backgroundColor: "#FFF9E6",
color: "#E8B400",
display: "flex",
alignItems: "center",
gap: 4,
}}
>
<DirectionsIcon />
<ProText
style={{
fontWeight: 500,
fontStyle: "Medium",
fontSize: 14,
lineHeight: "140%",
letterSpacing: "0%",
color: "#E8B400",
}}
>
{t("redeem.getDirections")}
</ProText>
</Tag>
</div>
</ProInputCard>
</>
);
}

View 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;
}

View File

@@ -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 (
<>
<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={giftDetails?.amount || 0} />
</ProText>
</div>
<Switch />
</div>
<Divider style={{ margin: "10px 0" }} />
<div
style={{
display: "flex",
flexDirection: "row",
justifyContent: "space-between",
}}
onClick={() => {
navigate(`/${subdomain}/cart`);
}}
>
<ProText
style={{
fontWeight: 400,
fontStyle: "Regular",
fontSize: 14,
lineHeight: "140%",
letterSpacing: "0%",
color: "#777580",
cursor: "pointer",
}}
>
{t("redeem.voucherWillBeAppliedAtCheckout")}
</ProText>
</div>
</ProInputCard>
</>
);
}

View File

@@ -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;
}
}

View File

@@ -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>
</>
);
}

View File

@@ -1,16 +1,7 @@
import { Button, Card, Divider, Image } from "antd"; import { Button, Card, Image, Layout, Skeleton } 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 ProHeader from "components/ProHeader/ProHeader"; import ProHeader from "components/ProHeader/ProHeader";
import ProInputCard from "components/ProInputCard/ProInputCard";
import ProText from "components/ProText"; import ProText from "components/ProText";
import ProTitle from "components/ProTitle";
import dayjs from "dayjs";
import { useEffect, useRef } from "react"; import { useEffect, useRef } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { useNavigate, useParams } from "react-router-dom"; import { useNavigate, useParams } from "react-router-dom";
@@ -18,31 +9,30 @@ import {
useGetOrderDetailsQuery, useGetOrderDetailsQuery,
useGetRestaurantDetailsQuery, useGetRestaurantDetailsQuery,
} from "redux/api/others"; } from "redux/api/others";
import { useAppSelector } from "redux/hooks";
import styles from "./redeem.module.css"; import styles from "./redeem.module.css";
import BackIcon from "components/Icons/BackIcon"; import QRIcon from "components/Icons/QRIcon";
import NextIcon from "components/Icons/NextIcon"; import CopyIcon from "components/Icons/CopyIcon";
import { RateBottomSheet } from "components/CustomBottomSheet/RateBottomSheet"; import { useAppSelector } from "redux/hooks";
import Stepper from "pages/order/components/Stepper"; 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() { export default function RedeemPage() {
const { t } = useTranslation(); const { t } = useTranslation();
const { orderId } = useParams(); const { voucherId } = useParams();
const { isRTL } = useAppSelector((state) => state.locale);
const { restaurant } = useAppSelector((state) => state.order); const { restaurant } = useAppSelector((state) => state.order);
const navigate = useNavigate();
const hasRefetchedRef = useRef(false); 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") || "", restaurantID: localStorage.getItem("restaurantID") || "",
}, },
{ {
skip: !orderId, skip: !voucherId,
// return it t0 60000 after finish testing
pollingInterval: 10000,
refetchOnMountOrArgChange: true,
}, },
); );
@@ -58,7 +48,7 @@ export default function RedeemPage() {
// Reset refetch flag when orderId changes // Reset refetch flag when orderId changes
useEffect(() => { useEffect(() => {
hasRefetchedRef.current = false; hasRefetchedRef.current = false;
}, [orderId]); }, [voucherId]);
// Refetch restaurant details when order status has alias "closed" // Refetch restaurant details when order status has alias "closed"
useEffect(() => { useEffect(() => {
@@ -74,190 +64,225 @@ export default function RedeemPage() {
} }
}, [orderDetails?.status, restaurantSubdomain, refetchRestaurantDetails]); }, [orderDetails?.status, restaurantSubdomain, refetchRestaurantDetails]);
const handleCheckout = () => {
navigate(`/${subdomain}/menu?orderType=${OrderType.Redeem}`);
};
return ( return (
<> <>
<ProHeader>{t("order.title")}</ProHeader> <Layout>
<div <ProHeader>{t("redeem.title")}</ProHeader>
style={{ <Layout.Content className={styles.redeemContainer}>
display: "flex",
flexDirection: "column",
height: "92vh",
padding: 16,
gap: 16,
overflow: "auto",
scrollbarWidth: "none",
}}
>
<Card className={styles.orderCard}>
<div <div
style={{ style={{
textAlign: "center",
display: "flex", display: "flex",
flexDirection: "row", flexDirection: "column",
gap: "1rem", alignItems: "center",
backgroundColor: "rgba(255, 183, 0, 0.08)", justifyContent: "center",
borderRadius: "12px", margin: "32px 28px 21px 28px",
padding: 16, gap: 8,
}} }}
> >
<Button <ProText
type="text" style={{ fontSize: 16, fontWeight: 600, color: "#333333" }}
shape="circle"
style={{
backgroundColor: "rgba(255, 183, 0, 0.08)",
}}
> >
<Image {t("redeem.addGiftDetails")}
src={orderDetails?.restaurant_iimage} </ProText>
className={styles.profileImage} <ProText
width={50} style={{ fontSize: 14, fontWeight: 400, color: "#95949C" }}
height={50} >
preview={false} {t("redeem.description")}
/> </ProText>
</Button>
<div>
<ProText style={{ fontSize: "1rem" }}>
{t("order.yourOrderFromFascanoRestaurant")}
</ProText>
<br />
<ProText type="secondary">
<LocationIcon className={styles.locationIcon} />{" "}
{isRTL ? orderDetails?.restaurantAR : orderDetails?.restaurant}
</ProText>
</div>
</div> </div>
<OrderDishIcon className={styles.orderDishIcon} /> {isLoading || !orderDetails?.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={orderDetails?.restaurant_iimage}
width={205}
height={134}
className={styles.cardImage}
/>
</div>
</div>
)}
<div> <div
<ProTitle style={{
level={5} textAlign: "center",
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
margin: "18px 28px 26px 28px",
gap: 12,
}}
>
<ProText
style={{
fontWeight: 400,
fontStyle: "Regular",
fontSize: 14,
lineHeight: "140%",
letterSpacing: "0%",
textAlign: "center",
color: "#95949C",
}}
>
{t("redeem.description")}
</ProText>
<ProText
style={{ style={{
fontWeight: 600, fontWeight: 600,
fontSize: "18px", fontStyle: "SemiBold",
marginBottom: "0.75rem", fontSize: 14,
lineHeight: "140%",
letterSpacing: "0%",
textAlign: "center",
color: "#333333",
}} }}
> >
{t("order.inProgressOrder")} (1) {t("redeem.addGiftDetails")}
</ProTitle> </ProText>
<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",
)}
</ProText>
</div>
<Divider style={{ margin: "12px 0" }} />
<Stepper statuses={orderDetails?.status} />
</div> </div>
</Card>
<Ads2 /> <Card
<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
style={{
display: "flex",
flexDirection: "row",
justifyContent: "flex-start",
gap: "1rem",
}}
>
<Button
type="text"
shape="circle"
style={{
backgroundColor: "rgba(255, 183, 0, 0.08)",
}}
>
{index + 1}X
</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={{ style={{
display: "flex", borderRadius: 0,
flexDirection: "row", borderTopRightRadius: 16,
justifyContent: "space-between", borderTopLeftRadius: 16,
marginTop: 1, 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,
},
}} }}
> >
<Image <ProText
src={restaurant?.restautantImage}
width={30}
height={30}
preview={false}
style={{
borderRadius: "50%",
objectFit: "cover",
position: "relative",
top: -4,
}}
/>
<ProTitle
level={5}
style={{ style={{
fontWeight: 400,
fontStyle: "Regular",
fontSize: 14, fontSize: 14,
lineHeight: "140%",
letterSpacing: "0%",
color: "#95949C",
alignItems: "center",
}} }}
> >
{isRTL ? restaurant?.nameAR : restaurant?.restautantName} {t("redeem.showThisCodeAtTheRestaurant")}
</ProTitle> </ProText>
<QRIcon />
<div style={{ display: "flex", flexDirection: "column", gap: 12 }}>
<Button
style={{
height: 40,
borderRadius: 888,
gap: 16,
opacity: 1,
borderWidth: 1,
backgroundColor: "var(--background)",
}}
icon={<CopyIcon className={styles.copyIcon} />}
iconPlacement="end"
>
GFT - 92KD - 7X84
</Button>
<ProText
style={{
fontWeight: 400,
fontStyle: "Regular",
fontSize: 14,
lineHeight: "140%",
letterSpacing: "0%",
color: "#95949C",
alignItems: "center",
}}
>
{t("redeem.useThisCodeIfScanningNotPossible")}
</ProText>
</div>
</Card>
{isRTL ? ( <div
<BackIcon className={styles.serviceIcon} /> style={{
) : ( width: "100%",
<NextIcon className={styles.serviceIcon} /> 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>
</Card>
{/* <RateBottomSheet /> */} <div
style={{
margin: "20px 0",
display: "flex",
flexDirection: "column",
gap: 16,
}}
>
<GiftItemsCard />
<VoucherBalanceCard />
<LocationCard />
</div>
</Layout.Content>
<CancelOrderBottomSheet /> <Layout.Footer className={styles.checkoutButtonContainer}>
</div> <Button
type="primary"
shape="round"
className={styles.checkoutButton}
onClick={handleCheckout}
>
{t("redeem.redeemNow")}
</Button>
</Layout.Footer>
</Layout>
</> </>
); );
} }

View File

@@ -1,247 +1,81 @@
.orderSummary :global(.ant-card-body) { .carouselContainer {
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 {
display: flex; display: flex;
justify-content: center;
align-items: center; align-items: center;
justify-content: center;
gap: 16px;
width: 100%; width: 100%;
} }
.orderCard :global(.ant-card-body) { .cardWrapper {
display: flex; 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; align-items: center;
font-size: 16px; justify-content: center;
} }
/* Enhanced responsive summary rows */ .cardImage {
@media (min-width: 769px) and (max-width: 1024px) { width: 205px;
.summaryRow { height: 134px;
padding: 12px 0; object-fit: cover;
font-size: 16px; border-radius: 8px;
}
} }
@media (min-width: 1025px) { .arrowButton {
.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 {
display: flex; display: flex;
justify-content: space-between;
align-items: center; align-items: center;
padding: 12px 0; justify-content: center;
font-size: 16px; 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; display: flex;
justify-content: space-between; flex-direction: row;
align-items: center; justify-content: space-around;
padding: 16px 0; gap: 1rem;
margin-top: 16px; background-color: var(--secondary-background);
box-shadow: none;
z-index: 999;
} }
[data-theme="dark"] .orderSummary { /* Dark theme styles for checkout */
background-color: #181818 !important; :global(.darkApp) .checkoutButtonContainer {
border-color: #363636 !important; background-color: #000000 !important;
} }
[data-theme="dark"] .orderSummary:hover { .checkoutButton {
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 {
width: 100%; width: 100%;
height: 48px; height: 48px;
margin-bottom: 16px;
box-shadow: none;
}
.copyIcon {
cursor: pointer;
position: relative;
top: 3px;
}
.redeemContainer {
display: flex; display: flex;
justify-content: flex-start; flex-direction: column;
padding: 12px 18px !important; padding: 16px;
row-gap: 10px; gap: 16px;
transition: all 0.3s ease; overflow: auto;
border-radius: 50px; 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;
}

View File

@@ -15,7 +15,6 @@ import OrderDetails from "pages/order/components/OrderDetails";
import OrderPage from "pages/order/page"; import OrderPage from "pages/order/page";
import OrdersPage from "pages/orders/page"; import OrdersPage from "pages/orders/page";
import OtpPage from "pages/otp/page"; import OtpPage from "pages/otp/page";
import PayPage from "pages/pay/page";
import ProductDetailPage from "pages/product/page"; import ProductDetailPage from "pages/product/page";
import RedeemPage from "pages/redeem/page"; import RedeemPage from "pages/redeem/page";
import RestaurantPage from "pages/restaurant/page"; import RestaurantPage from "pages/restaurant/page";