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

@@ -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) && <TableNumberCard />}
orderType === OrderType.ToOffice) && <TableNumberCard />}
{orderType === OrderType.Redeem && <GiftItemsCard isCart={true} />}
<div className={`${styles.cartContent} ${getResponsiveClass()}`}>
<Card className={styles.cartItem}>

View File

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

View File

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

View File

@@ -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 && <VoucherSummary />}
{/* {orderType === OrderType.Gift && <GiftCard />} */}
{/* <RoomDetails />
<OfficeDetails /> */}
{/* <GiftDetails /> */}
{/* <BriefMenu /> */}
<CouponCard />
{orderType !== OrderType.Redeem && <CouponCard />}
{/* Collection Method */}
{orderType === OrderType.Pickup && (
@@ -114,7 +123,7 @@ export default function CheckoutPage() {
)}
{/* Reward Your Waiter */}
<RewardWaiterCard />
{orderType !== OrderType.Redeem && <RewardWaiterCard />}
<BriefMenuCard />
<Ads1 />
<OrderSummary />

View File

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

View File

@@ -96,21 +96,31 @@ function MenuPage() {
<div
className={`${styles.headerFloatingBtn} ${styles.orderTypeSelectContainer} order-type-select-container`}
>
<Select
value={orderType}
options={orderTypeOptions}
open={false}
onOpenChange={() => 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 && (
<Select
value={orderType}
options={orderTypeOptions}
open={false}
onOpenChange={() => 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 && (
<div className={styles.frame}>
<div className={styles.div}>
<div className={styles.pickup}>Balance</div>
<div className={styles.elementMin}>60 OMR</div>
</div>
</div>
)}
</div>
<SearchButton />
</div>

View File

@@ -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() {
<br />
<ProText
type="secondary"
className={`${styles.itemDescription} responsive-text`}
style={{
margin: 0,
lineClamp: 1,
@@ -135,7 +134,9 @@ export default function OrderDetails() {
border: "none",
}}
>
<ProText style={{color: "#1F1C2E"}}>{item.qty} </ProText>
<ProText style={{ color: "#1F1C2E" }}>
{item.qty}{" "}
</ProText>
</Button>
</div>
</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 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 (
<>
<ProHeader>{t("order.title")}</ProHeader>
<div
style={{
display: "flex",
flexDirection: "column",
height: "92vh",
padding: 16,
gap: 16,
overflow: "auto",
scrollbarWidth: "none",
}}
>
<Card className={styles.orderCard}>
<Layout>
<ProHeader>{t("redeem.title")}</ProHeader>
<Layout.Content className={styles.redeemContainer}>
<div
style={{
textAlign: "center",
display: "flex",
flexDirection: "row",
gap: "1rem",
backgroundColor: "rgba(255, 183, 0, 0.08)",
borderRadius: "12px",
padding: 16,
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
margin: "32px 28px 21px 28px",
gap: 8,
}}
>
<Button
type="text"
shape="circle"
style={{
backgroundColor: "rgba(255, 183, 0, 0.08)",
}}
<ProText
style={{ fontSize: 16, fontWeight: 600, color: "#333333" }}
>
<Image
src={orderDetails?.restaurant_iimage}
className={styles.profileImage}
width={50}
height={50}
preview={false}
/>
</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>
{t("redeem.addGiftDetails")}
</ProText>
<ProText
style={{ fontSize: 14, fontWeight: 400, color: "#95949C" }}
>
{t("redeem.description")}
</ProText>
</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>
<ProTitle
level={5}
<div
style={{
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={{
fontWeight: 600,
fontSize: "18px",
marginBottom: "0.75rem",
fontStyle: "SemiBold",
fontSize: 14,
lineHeight: "140%",
letterSpacing: "0%",
textAlign: "center",
color: "#333333",
}}
>
{t("order.inProgressOrder")} (1)
</ProTitle>
<div style={{ display: "flex", flexDirection: "row", gap: 8 }}>
<InvoiceIcon className={styles.invoiceIcon} />
<ProText type="secondary" style={{ fontSize: "14px" }}>
#{orderDetails?.order.id}
</ProText>
<TimeIcon className={styles.timeIcon} />
<ProText type="secondary" style={{ fontSize: "14px" }}>
ordered :- Today -{" "}
{dayjs(orderDetails?.status[0]?.pivot?.created_at).format(
"h:mm A",
)}
</ProText>
</div>
<Divider style={{ margin: "12px 0" }} />
<Stepper statuses={orderDetails?.status} />
{t("redeem.addGiftDetails")}
</ProText>
</div>
</Card>
<Ads2 />
<ProInputCard
title={
<div style={{ marginBottom: 7 }}>
<ProText style={{ fontSize: "1rem" }}>
{t("order.yourOrderFrom")}
</ProText>
<br />
<ProText type="secondary">
<LocationIcon className={styles.locationIcon} />{" "}
{t("order.muscat")}
</ProText>
</div>
}
>
<div
style={{ display: "flex", flexDirection: "column", gap: "1rem" }}
>
{orderDetails?.orderItems.map((item, index) => (
<div key={item.id}>
<div
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
<Card
style={{
display: "flex",
flexDirection: "row",
justifyContent: "space-between",
marginTop: 1,
borderRadius: 0,
borderTopRightRadius: 16,
borderTopLeftRadius: 16,
border: "1px solid transparent",
borderImageSource:
"radial-gradient(38.92% 103.83% at 49.85% -3.83%, #FFB700 0%, rgba(255, 233, 179, 0) 100%)",
borderImageSlice: 1,
}}
styles={{
body: {
display: "flex",
flexDirection: "column",
gap: 24,
alignItems: "center",
justifyContent: "center",
padding: 32,
},
}}
>
<Image
src={restaurant?.restautantImage}
width={30}
height={30}
preview={false}
style={{
borderRadius: "50%",
objectFit: "cover",
position: "relative",
top: -4,
}}
/>
<ProTitle
level={5}
<ProText
style={{
fontWeight: 400,
fontStyle: "Regular",
fontSize: 14,
lineHeight: "140%",
letterSpacing: "0%",
color: "#95949C",
alignItems: "center",
}}
>
{isRTL ? restaurant?.nameAR : restaurant?.restautantName}
</ProTitle>
{t("redeem.showThisCodeAtTheRestaurant")}
</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 ? (
<BackIcon className={styles.serviceIcon} />
) : (
<NextIcon className={styles.serviceIcon} />
)}
<div
style={{
width: "100%",
height: 44,
opacity: 1,
borderBottomRightRadius: 16,
borderBottomLeftRadius: 16,
paddingTop: 12,
paddingBottom: 12,
gap: 10,
borderTopWidth: 1,
background: "#FFF9E6",
borderTop: "1px solid #FFEDB0",
textAlign: "center",
}}
>
<ProText
style={{
fontSize: 14,
fontWeight: 500,
fontStyle: "Medium",
lineHeight: "140%",
letterSpacing: "0%",
textAlign: "center",
color: "#E8B400",
}}
>
Active - Expires in 12 days
</ProText>
</div>
</Card>
{/* <RateBottomSheet /> */}
<div
style={{
margin: "20px 0",
display: "flex",
flexDirection: "column",
gap: 16,
}}
>
<GiftItemsCard />
<VoucherBalanceCard />
<LocationCard />
</div>
</Layout.Content>
<CancelOrderBottomSheet />
</div>
<Layout.Footer className={styles.checkoutButtonContainer}>
<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) {
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;
}