checkout: add card deyails & card amount BT

This commit is contained in:
2025-12-31 23:49:04 +03:00
parent 1d2c1fda87
commit 3b0b8ceab6
12 changed files with 373 additions and 108 deletions

View File

@@ -267,6 +267,10 @@
"checkout": {
"customerName": "اسم العميل",
"paymentSummary": "ملخص الدفع",
"holdayGiftCard": "هدية العيد",
"messageIncluded": "الرسالة مضمنة",
"to": "ل",
"giftSummary": "ملخص الهدية",
"customerInformation": "تفاصيل العميل",
"title": "الدفع",
"cash": "كاش",
@@ -481,6 +485,12 @@
"yourPhone": "رقم هاتفك",
"keepMyNameSecret": "الاحتفاظ باسمي مخفياً",
"receiverInformation": "تفاصيل المستلم",
"costumeAmount": "مبلغ البطاقة"
"costumeAmount": "مبلغ البطاقة",
"enterCustomOucherAmount": "أدخل مبلغ البطاقة المخصص",
"amount": "المبلغ",
"eCardAmount": "مبلغ البطاقة الإلكترونية",
"receiverName": "اسم المستلم",
"edit": "تعديل",
"yourInformation": "تفاصيلك"
}
}

View File

@@ -278,6 +278,10 @@
"customerName": "Customer Name",
"paymentSummary": "Payment Summary",
"orderSummary": "Order Summary",
"holdayGiftCard": "Holday gift card",
"messageIncluded": "Message included",
"to": "To",
"giftSummary": "Gift Summary",
"customerInformation": "Customer Information",
"title": "Checkout",
"cash": "Cash",
@@ -492,6 +496,12 @@
"yourPhone": "Your Phone",
"keepMyNameSecret": "Keep my name secret",
"receiverInformation": "Receiver Information",
"costumeAmount": "Costume amount"
"costumeAmount": "Costume amount",
"enterCustomOucherAmount": "Enter custom oucher amount",
"amount": "Amount",
"eCardAmount": "E-Card Amount",
"receiverName": "Receiver Name",
"edit": "Edit",
"yourInformation": "Your Information"
}
}

View File

@@ -0,0 +1,55 @@
interface CardAmountIconType {
className?: string;
onClick?: () => void;
}
const CardAmountIcon = ({ className, onClick }: CardAmountIconType) => {
return (
<svg
width="64"
height="42"
viewBox="0 0 64 42"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className={className}
onClick={onClick}
>
<rect width="64" height="42" rx="4" fill="#FAFAFA" />
<path
opacity="0.5"
d="M26 18H30"
stroke="#E8B400"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M40.833 19H38.231C36.446 19 35 20.343 35 22C35 23.657 36.447 25 38.23 25H40.833C40.917 25 40.958 25 40.993 24.998C41.533 24.965 41.963 24.566 41.998 24.065C42 24.033 42 23.994 42 23.917V20.083C42 20.006 42 19.967 41.998 19.935C41.962 19.434 41.533 19.035 40.993 19.002C40.958 19 40.917 19 40.833 19Z"
stroke="#4C4A58"
stroke-width="1.5"
/>
<path
d="M40.965 19C40.887 17.128 40.637 15.98 39.828 15.172C38.657 14 36.771 14 33 14H30C26.229 14 24.343 14 23.172 15.172C22.001 16.344 22 18.229 22 22C22 25.771 22 27.657 23.172 28.828C24.344 29.999 26.229 30 30 30H33C36.771 30 38.657 30 39.828 28.828C40.637 28.02 40.888 26.872 40.965 25"
stroke="#4C4A58"
stroke-width="1.5"
/>
<path
opacity="0.5"
d="M26 13.9999L29.735 11.5229C30.2604 11.1816 30.8735 11 31.5 11C32.1265 11 32.7396 11.1816 33.265 11.5229L37 13.9999"
stroke="#E8B400"
stroke-width="1.5"
stroke-linecap="round"
/>
<path
opacity="0.5"
d="M37.9922 22H38.0012"
stroke="#E8B400"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
);
};
export default CardAmountIcon;

View File

@@ -26,6 +26,7 @@ export interface OfficeDetailsType {
}
export interface GiftDetailsType {
amount: number;
receiverName: string;
receiverPhone: string;
message: string;
@@ -33,7 +34,7 @@ export interface GiftDetailsType {
senderPhone: string;
senderEmail: string;
isSecret: boolean;
cardId: string;
cardId: number;
giftType: "items" | "vouchers" | "itemsAndVouchers";
}

View File

@@ -15,6 +15,7 @@ import SenderformationCard from "./components/SenderformationCard/Senderformatio
import TimeEstimateCard from "pages/cart/components/timeEstimate/TimeEstimateCard";
import { useNavigate, useParams } from "react-router-dom";
import GiftAmountCard from "./components/GiftAmountCard/GiftAmountCard";
import { GiftAmountBottomSheet } from "./components/GiftAmountBottomSheet";
export default function CardDetailsPage() {
const { t } = useTranslation();
@@ -22,6 +23,7 @@ export default function CardDetailsPage() {
const { giftDetails } = useAppSelector(selectCart);
const { isRTL } = useAppSelector((state) => state.locale);
const [currentIndex, setCurrentIndex] = useState(0);
const [isGiftAmountBottomSheetOpen, setIsGiftAmountBottomSheetOpen] = useState(false);
const { subdomain } = useParams();
const navigate = useNavigate();
@@ -29,7 +31,7 @@ export default function CardDetailsPage() {
useEffect(() => {
if (cards && giftDetails?.cardId) {
const index = cards.findIndex(
(card) => card.id.toString() === giftDetails.cardId,
(card) => card?.id?.toString() === giftDetails.cardId,
);
if (index !== -1) {
setCurrentIndex(index);
@@ -56,6 +58,7 @@ export default function CardDetailsPage() {
}, [subdomain]);
return (
<>
<Layout>
<ProHeader>{t("cardDetails.title")}</ProHeader>
<Layout.Content className={styles.checkoutContainer}>
@@ -131,7 +134,7 @@ export default function CardDetailsPage() {
layout="vertical"
style={{ display: "flex", flexDirection: "column", gap: 16 }}
>
{giftDetails?.giftType !== "items" && <GiftAmountCard />}
{giftDetails?.giftType !== "items" && <GiftAmountCard onOpen={() => setIsGiftAmountBottomSheetOpen(true)} />}
<ReceivernformationCard />
<SenderformationCard />
<TimeEstimateCard />
@@ -148,5 +151,7 @@ export default function CardDetailsPage() {
</Button>
</Layout.Footer>
</Layout>
<GiftAmountBottomSheet isOpen={isGiftAmountBottomSheetOpen} onClose={() => setIsGiftAmountBottomSheetOpen(false)} />
</>
);
}

View File

@@ -0,0 +1,109 @@
import { Button, Form } from "antd";
import { ProBottomSheet } from "components/ProBottomSheet/ProBottomSheet.tsx";
import { useState, useEffect } from "react";
import { useTranslation } from "react-i18next";
import {
selectCart,
selectGrandTotal,
updateGiftDetails,
updateSplitBillAmount,
} from "features/order/orderSlice";
import { useAppDispatch, useAppSelector } from "redux/hooks";
import ProText from "components/ProText";
import { ProInputNumber } from "components/Inputs/ProInputNumber";
interface SplitBillChoiceBottomSheetProps {
isOpen: boolean;
onClose: () => void;
}
export function GiftAmountBottomSheet({
isOpen,
onClose,
}: SplitBillChoiceBottomSheetProps) {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const { giftDetails, } = useAppSelector(selectCart);
const [amount, setAmount] = useState<string>(
giftDetails?.amount && giftDetails?.amount > 0 ? giftDetails?.amount?.toString() : "",
);
const handleSave = () => {
const numAmount = parseFloat(amount) || 0;
dispatch(updateGiftDetails({amount: numAmount}));
onClose();
};
return (
<ProBottomSheet
isOpen={isOpen}
onClose={onClose}
title={t("cardDetails.customAmount")}
showCloseButton={true}
initialSnap={1}
height={295}
snapPoints={[295]}
contentStyle={{
padding: 0,
}}
>
<div
style={{
padding: "20px",
display: "flex",
flexDirection: "column",
gap: 20,
}}
>
<div style={{ display: "flex", flexDirection: "column", gap: 5 }}>
<ProText
style={{
fontWeight: 400,
fontStyle: "Regular",
fontSize: 16,
lineHeight: "140%",
letterSpacing: "0%",
color:"#333333"
}}
>
{t("cardDetails.enterCustomOucherAmount")}
</ProText>
<Form.Item name="amount">
<ProInputNumber
value={amount}
onChange={(value) => {
setAmount(value?.toString() || "");
dispatch(updateSplitBillAmount(Number(value) || 0));
}}
placeholder={t("cardDetails.amount")}
min={0}
/>
</Form.Item>
</div>
</div>
<div
style={{
display: "flex",
gap: 12,
margin: 20,
}}
>
<Button
type="primary"
style={{
flex: 1,
boxShadow: "none",
height: 48,
}}
onClick={handleSave}
disabled={!amount || parseFloat(amount) <= 0}
>
{t("cardDetails.add")}
</Button>
</div>
</ProBottomSheet>
);
}

View File

@@ -6,11 +6,11 @@ import ArabicPrice from "components/ArabicPrice";
import ProText from "components/ProText";
import EditIcon from "components/Icons/EditIcon";
export default function GiftAmountCard() {
export default function GiftAmountCard({ onOpen }: { onOpen: () => void }) {
const { t } = useTranslation();
return (
<ProInputCard title={t("cardDetails.eGiftCardAmount")}>
<ProInputCard title={t("cardDetails.eCardAmount")}>
<div className={styles.customerInformationCard}>
<Button
style={{
@@ -87,6 +87,7 @@ export default function GiftAmountCard() {
}}
icon={<EditIcon className={styles.editIcon} color="#FFF" />}
iconPlacement="start"
onClick={onOpen}
>
<ProText
style={{

View File

@@ -18,7 +18,6 @@ import CarPlateCard from "pages/cart/components/CarPlateCard.tsx";
import CartFooter from "pages/cart/components/cartFooter/CartFooter.tsx";
import SpecialRequestCard from "pages/cart/components/specialRequest/SpecialRequestCard.tsx";
import TableNumberCard from "pages/cart/components/TableNumberCard.tsx";
import TimeEstimateCard from "pages/cart/components/timeEstimate/TimeEstimateCard.tsx";
import { OrderType } from "pages/checkout/hooks/types";
import { useTranslation } from "react-i18next";
import { Variant } from "utils/types/appTypes";

View File

@@ -1,115 +1,181 @@
import { Checkbox, Form, Input } from "antd";
import { Divider, Image } from "antd";
import ProInputCard from "components/ProInputCard/ProInputCard";
import ProPhoneInput from "components/ProPhoneInput";
import ProText from "components/ProText";
import {
selectCart,
updateGiftDetails,
updateOrder,
} from "features/order/orderSlice";
import { selectCart } from "features/order/orderSlice";
import { EGiftCard } from "pages/CardDetails/type";
import { useTranslation } from "react-i18next";
import { useAppDispatch, useAppSelector } from "redux/hooks";
const { TextArea } = Input;
import { useGetEGiftCardsQuery } from "redux/api/others";
import { useAppSelector } from "redux/hooks";
import styles from "./GiftDetails.module.css";
import { useNavigate, useParams } from "react-router-dom";
import InvoiceIcon from "components/Icons/order/InvoiceIcon";
import BackIcon from "components/Icons/BackIcon";
import NextIcon from "components/Icons/NextIcon";
import { useMemo } from "react";
import CardAmountIcon from "components/Icons/CardAmountIcon";
import ArabicPrice from "components/ArabicPrice";
export function GiftCard() {
const { t } = useTranslation();
const { order } = useAppSelector(selectCart);
const dispatch = useAppDispatch();
const { giftDetails, items } = useAppSelector(selectCart);
const { data: cards } = useGetEGiftCardsQuery();
const currentCard = cards?.find(
(card: EGiftCard) => card.id == giftDetails?.cardId,
);
const navigate = useNavigate();
const { subdomain } = useParams();
const { isRTL } = useAppSelector((state) => state.locale);
const totalItems = useMemo(() => {
return items.length || 0;
}, [items]);
return (
<>
<ProInputCard title={t("address.giftDetails")}>
<ProPhoneInput
propName="receiverPhone"
label={t("address.receiverPhone")}
value={order?.receiverPhone}
onChange={(e) => {
dispatch(updateGiftDetails({ receiverPhone: e }));
}}
/>
<div className={styles.orderNotes}>
<Image src={currentCard?.imageURL} height={42} width={64} style={{height: 42, width: 64}} />
<div style={{ display: "flex", flexDirection: "column", gap: 4, width: "100%" }}>
<ProText
style={{
fontWeight: 500,
fontStyle: "Medium",
fontSize: 14,
lineHeight: "140%",
letterSpacing: "0%",
color: "#333333",
}}
>
{t("checkout.giftSummary")}
</ProText>
<ProText
style={{
fontWeight: 400,
fontStyle: "Regular",
fontSize: 12,
lineHeight: "140%",
letterSpacing: "0%",
color: "#5F6C7B",
}}
>
{t("checkout.holdayGiftCard")} - {t("checkout.messageIncluded")}
</ProText>
<div style={{ display: "flex", flexDirection: "row", gap: 4 }}>
<ProText
style={{
fontWeight: 400,
fontStyle: "Regular",
fontSize: 12,
lineHeight: "140%",
letterSpacing: "0%",
color: "#040F35",
}}
>
{t("checkout.to")} :
</ProText>
<ProText
style={{
fontWeight: 400,
fontStyle: "Regular",
fontSize: 12,
lineHeight: "140%",
letterSpacing: "0%",
color: "#5F6C7B",
}}
>
{giftDetails?.receiverName}
</ProText>
</div>
</div>
{isRTL ? <BackIcon iconSize={24} /> : <NextIcon iconSize={24} />}
</div>
<Divider style={{ margin: "10px 0" }} />
<Form.Item name="message" label={t("address.message")}>
<TextArea
placeholder={t("address.message")}
size="large"
{giftDetails?.giftType === "items" && (
<div
style={{
fontSize: 14,
borderRadius: 10,
display: "flex",
flexDirection: "row",
justifyContent: "space-between",
}}
rows={2}
autoFocus={false}
value={order?.message}
onChange={(e) => {
dispatch(updateGiftDetails({ message: e.target.value }));
}}
/>
</Form.Item>
<Form.Item
name="senderName"
label={t("address.senderName")}
rules={[{ required: true, message: "" }]}
colon={false}
>
<Input
placeholder={t("address.senderName")}
size="large"
style={{
fontSize: 14,
height: 50,
}}
autoFocus={false}
value={order?.senderName}
onChange={(e) => {
dispatch(updateGiftDetails({ senderName: e.target.value }));
}}
/>
</Form.Item>
<ProPhoneInput
propName="senderPhone"
label={t("address.senderPhone")}
value={order?.senderPhone}
onChange={(e) => {
dispatch(updateGiftDetails({ senderPhone: e }));
}}
/>
<Form.Item
name="senderEmail"
label={t("address.senderEmail")}
rules={[{ required: true, message: "" }]}
colon={false}
>
<Input
placeholder={t("address.senderEmail")}
size="large"
style={{
fontSize: 14,
height: 50,
}}
autoFocus={false}
value={order?.senderEmail}
onChange={(e) => {
dispatch(updateGiftDetails({ senderEmail: e.target.value }));
}}
/>
</Form.Item>
<Form.Item name="isSecret" colon={false}>
<Checkbox
style={{
fontSize: 14,
}}
autoFocus={false}
checked={order?.isSecret}
onChange={(e) => {
dispatch(updateGiftDetails({ isSecret: e.target.checked }));
onClick={() => {
navigate(`/${subdomain}/cart`);
}}
>
<ProText>{t("address.keepMyNameSecret")}</ProText>
</Checkbox>
</Form.Item>
<ProText
style={{
fontWeight: 400,
fontStyle: "Regular",
fontSize: 12,
lineHeight: "140%",
letterSpacing: "0%",
color: "#777580",
cursor: "pointer",
}}
>
<span
style={{
[isRTL ? "marginLeft" : "marginRight"]: 5,
position: "relative",
top: 3.5,
}}
>
<InvoiceIcon />
</span>
{t("checkout.viewOrder")} ( {totalItems} {t("cart.items")} )
</ProText>
{isRTL ? <BackIcon /> : <NextIcon />}
</div>
)}
{giftDetails?.giftType === "vouchers" && (
<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("checkout.giftSummary")}
</ProText>
<ProText
style={{
fontWeight: 500,
fontStyle: "Medium",
fontSize: 14,
lineHeight: "140%",
letterSpacing: "0%",
color: "#333333",
}}
>
<ArabicPrice price={giftDetails?.amount || 0} />
</ProText>
</div>
{isRTL ? <BackIcon iconSize={24} /> : <NextIcon iconSize={24} />}
</div>
)}
</ProInputCard>
</>
);

View File

@@ -46,3 +46,13 @@
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

@@ -46,7 +46,7 @@ export default function CheckoutPage() {
<Layout.Content className={styles.checkoutContainer}>
{(orderType === OrderType.Pickup ||
orderType === OrderType.ScheduledOrder) && <TimeEstimateCard />}
{orderType === OrderType.Gift && <GiftCard />}
<PaymentMethods />
{!token && <CustomerInformationCard />}
<AddressSummary />

View File

@@ -5,7 +5,6 @@ import LogoContainerIcon from "components/Icons/LogoContainerIcon";
import ImageWithFallback from "components/ImageWithFallback";
import LoyaltyCard from "components/LoyaltyCard/LoyaltyCard";
import ProText from "components/ProText";
import ProTitle from "components/ProTitle";
import { useScrollHandler } from "contexts/ScrollHandlerContext";
import useBreakPoint from "hooks/useBreakPoint";
import { useRestaurant } from "hooks/useRestaurant";