cart & checkout: apply validation based on required inputs in each service & add phone input in checkout page
This commit is contained in:
@@ -221,7 +221,8 @@
|
|||||||
"inYourCart": "في سلة المشتريات",
|
"inYourCart": "في سلة المشتريات",
|
||||||
"updatedSuccessfully": "تم التحديث بنجاح",
|
"updatedSuccessfully": "تم التحديث بنجاح",
|
||||||
"editNote": "تعديل الملاحظة",
|
"editNote": "تعديل الملاحظة",
|
||||||
"selectTimeEstimate": "أدخل وقت التقديم"
|
"selectTimeEstimate": "أدخل وقت التقديم",
|
||||||
|
"pleaseAddItemsToCart": "يرجى إضافة عناصر إلى السلة"
|
||||||
},
|
},
|
||||||
"checkout": {
|
"checkout": {
|
||||||
"title": "الدفع",
|
"title": "الدفع",
|
||||||
@@ -238,7 +239,8 @@
|
|||||||
"totalAmount": "المبلغ الكلي",
|
"totalAmount": "المبلغ الكلي",
|
||||||
"items": "العناصر",
|
"items": "العناصر",
|
||||||
"expiresIn": "تنتهي في",
|
"expiresIn": "تنتهي في",
|
||||||
"expiresInDescription": "تنتهي في:12/26"
|
"expiresInDescription": "تنتهي في:12/26",
|
||||||
|
"phoneNumber": "رقم الهاتف"
|
||||||
},
|
},
|
||||||
"address": {
|
"address": {
|
||||||
"title": "العنوان",
|
"title": "العنوان",
|
||||||
|
|||||||
@@ -231,7 +231,8 @@
|
|||||||
"plateNumber": "Plate Number",
|
"plateNumber": "Plate Number",
|
||||||
"plateNumberPlaceholder": "Enter plate number",
|
"plateNumberPlaceholder": "Enter plate number",
|
||||||
"editNote": "Edit Note",
|
"editNote": "Edit Note",
|
||||||
"selectTimeEstimate": "Select Estimate Time"
|
"selectTimeEstimate": "Select Estimate Time",
|
||||||
|
"pleaseAddItemsToCart": "Please add items to your cart"
|
||||||
},
|
},
|
||||||
"checkout": {
|
"checkout": {
|
||||||
"title": "Checkout",
|
"title": "Checkout",
|
||||||
@@ -248,7 +249,8 @@
|
|||||||
"totalAmount": "Total Amount",
|
"totalAmount": "Total Amount",
|
||||||
"items": "Items",
|
"items": "Items",
|
||||||
"expiresIn": "Expires in",
|
"expiresIn": "Expires in",
|
||||||
"expiresInDescription": "Expires in:12/26"
|
"expiresInDescription": "Expires in:12/26",
|
||||||
|
"phoneNumber": "Phone Number"
|
||||||
},
|
},
|
||||||
"address": {
|
"address": {
|
||||||
"title": "Address",
|
"title": "Address",
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Radio, Space } from "antd";
|
import { Form, Radio, Space } from "antd";
|
||||||
import { Group } from "antd/es/radio";
|
import { Group } from "antd/es/radio";
|
||||||
import ArabicPrice from "components/ArabicPrice";
|
import ArabicPrice from "components/ArabicPrice";
|
||||||
import DifferentCardIcon from "components/Icons/paymentMethods/DifferentCardIcon";
|
import DifferentCardIcon from "components/Icons/paymentMethods/DifferentCardIcon";
|
||||||
@@ -50,6 +50,7 @@ const PaymentMethods = ({ onPaymentSelect, ...props }: PaymentMethodsProps) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<ProInputCard title={t("checkout.selectedPaymentMethod")}>
|
<ProInputCard title={t("checkout.selectedPaymentMethod")}>
|
||||||
|
<Form.Item name="paymentMethod" required rules={[{ required: true }]}>
|
||||||
<Group
|
<Group
|
||||||
className={styles.paymentMethods}
|
className={styles.paymentMethods}
|
||||||
style={{
|
style={{
|
||||||
@@ -111,6 +112,7 @@ const PaymentMethods = ({ onPaymentSelect, ...props }: PaymentMethodsProps) => {
|
|||||||
))}
|
))}
|
||||||
</Space>
|
</Space>
|
||||||
</Group>
|
</Group>
|
||||||
|
</Form.Item>
|
||||||
</ProInputCard>
|
</ProInputCard>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ interface CartState {
|
|||||||
estimateTimeDate: Date;
|
estimateTimeDate: Date;
|
||||||
estimateTimeTime: string;
|
estimateTimeTime: string;
|
||||||
collectionMethod: string;
|
collectionMethod: string;
|
||||||
|
phone: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// localStorage keys
|
// localStorage keys
|
||||||
@@ -65,6 +66,7 @@ const CART_STORAGE_KEYS = {
|
|||||||
ESTIMATE_TIME_DATE: 'fascano_estimate_time_date',
|
ESTIMATE_TIME_DATE: 'fascano_estimate_time_date',
|
||||||
ESTIMATE_TIME_TIME: 'fascano_estimate_time_time',
|
ESTIMATE_TIME_TIME: 'fascano_estimate_time_time',
|
||||||
COLLECTION_METHOD: 'fascano_collection_method',
|
COLLECTION_METHOD: 'fascano_collection_method',
|
||||||
|
PHONE: 'fascano_phone',
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
// Utility functions for localStorage
|
// Utility functions for localStorage
|
||||||
@@ -95,6 +97,7 @@ const initialState: CartState = {
|
|||||||
estimateTimeDate: new Date(getFromLocalStorage(CART_STORAGE_KEYS.ESTIMATE_TIME_DATE, new Date().toISOString())),
|
estimateTimeDate: new Date(getFromLocalStorage(CART_STORAGE_KEYS.ESTIMATE_TIME_DATE, new Date().toISOString())),
|
||||||
estimateTimeTime: getFromLocalStorage(CART_STORAGE_KEYS.ESTIMATE_TIME_TIME, ""),
|
estimateTimeTime: getFromLocalStorage(CART_STORAGE_KEYS.ESTIMATE_TIME_TIME, ""),
|
||||||
collectionMethod: getFromLocalStorage(CART_STORAGE_KEYS.COLLECTION_METHOD, ""),
|
collectionMethod: getFromLocalStorage(CART_STORAGE_KEYS.COLLECTION_METHOD, ""),
|
||||||
|
phone: getFromLocalStorage(CART_STORAGE_KEYS.PHONE, ""),
|
||||||
};
|
};
|
||||||
|
|
||||||
const orderSlice = createSlice({
|
const orderSlice = createSlice({
|
||||||
@@ -273,6 +276,14 @@ const orderSlice = createSlice({
|
|||||||
localStorage.setItem(CART_STORAGE_KEYS.COLLECTION_METHOD, JSON.stringify(state.collectionMethod));
|
localStorage.setItem(CART_STORAGE_KEYS.COLLECTION_METHOD, JSON.stringify(state.collectionMethod));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
updatePhone(state, action: PayloadAction<string>) {
|
||||||
|
state.phone = action.payload;
|
||||||
|
|
||||||
|
// Sync to localStorage
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
localStorage.setItem(CART_STORAGE_KEYS.PHONE, JSON.stringify(state.phone));
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -295,6 +306,7 @@ export const {
|
|||||||
updateGiftDetails,
|
updateGiftDetails,
|
||||||
updateEstimateTime,
|
updateEstimateTime,
|
||||||
updateCollectionMethod,
|
updateCollectionMethod,
|
||||||
|
updatePhone,
|
||||||
reset,
|
reset,
|
||||||
} = orderSlice.actions;
|
} = orderSlice.actions;
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { PlusOutlined } from "@ant-design/icons";
|
import { PlusOutlined } from "@ant-design/icons";
|
||||||
import { Card, Divider, Space } from "antd";
|
import { Card, Divider, Form, Space } from "antd";
|
||||||
import ArabicPrice from "components/ArabicPrice";
|
import ArabicPrice from "components/ArabicPrice";
|
||||||
import CartActionsButtons from "components/CartActionsButtons/CartActionsButtons.tsx";
|
import CartActionsButtons from "components/CartActionsButtons/CartActionsButtons.tsx";
|
||||||
import ImageWithFallback from "components/ImageWithFallback";
|
import ImageWithFallback from "components/ImageWithFallback";
|
||||||
@@ -35,7 +35,9 @@ interface CartMobileTabletLayoutProps {
|
|||||||
form: FormInstance;
|
form: FormInstance;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function CartMobileTabletLayout({ form }: CartMobileTabletLayoutProps) {
|
export default function CartMobileTabletLayout({
|
||||||
|
form,
|
||||||
|
}: CartMobileTabletLayoutProps) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { items, collectionMethod } = useAppSelector(selectCart);
|
const { items, collectionMethod } = useAppSelector(selectCart);
|
||||||
const { id } = useParams();
|
const { id } = useParams();
|
||||||
@@ -235,6 +237,11 @@ export default function CartMobileTabletLayout({ form }: CartMobileTabletLayoutP
|
|||||||
{/* Collection Method */}
|
{/* Collection Method */}
|
||||||
{orderType === "pickup" && (
|
{orderType === "pickup" && (
|
||||||
<ProInputCard title={t("cart.collectionMethod")}>
|
<ProInputCard title={t("cart.collectionMethod")}>
|
||||||
|
<Form.Item
|
||||||
|
name="collectionMethod"
|
||||||
|
required
|
||||||
|
rules={[{ required: true }]}
|
||||||
|
>
|
||||||
<ProRatioGroups
|
<ProRatioGroups
|
||||||
options={[
|
options={[
|
||||||
{ label: t("cart.Cash"), value: "cod", price: "" },
|
{ label: t("cart.Cash"), value: "cod", price: "" },
|
||||||
@@ -253,6 +260,7 @@ export default function CartMobileTabletLayout({ form }: CartMobileTabletLayoutP
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
</Form.Item>
|
||||||
</ProInputCard>
|
</ProInputCard>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { colors } from "ThemeConstants.ts";
|
import { colors } from "ThemeConstants.ts";
|
||||||
import { Button, FormInstance } from "antd";
|
import { Button, FormInstance, message } from "antd";
|
||||||
import { selectCart } from "features/order/orderSlice.ts";
|
import { selectCart } from "features/order/orderSlice.ts";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Link, useNavigate, useParams } from "react-router-dom";
|
import { Link, useNavigate, useParams } from "react-router-dom";
|
||||||
@@ -17,16 +17,16 @@ export default function CartFooter({ form }: CartFooterProps) {
|
|||||||
const orderType = localStorage.getItem("orderType");
|
const orderType = localStorage.getItem("orderType");
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
// Check if checkout should be disabled
|
|
||||||
const isCheckoutDisabled = items.length === 0;
|
|
||||||
|
|
||||||
const handleCheckoutClick = async () => {
|
const handleCheckoutClick = async () => {
|
||||||
|
if (items.length === 0) message.warning(t("cart.pleaseAddItemsToCart"));
|
||||||
|
else {
|
||||||
try {
|
try {
|
||||||
await form.validateFields();
|
await form.validateFields();
|
||||||
navigate(`/${id}/checkout`);
|
navigate(`/${id}/checkout`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log("Form validation failed:", error);
|
console.log("Form validation failed:", error);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -60,7 +60,6 @@ export default function CartFooter({ form }: CartFooterProps) {
|
|||||||
color: "white",
|
color: "white",
|
||||||
width: "100%",
|
width: "100%",
|
||||||
}}
|
}}
|
||||||
disabled={isCheckoutDisabled}
|
|
||||||
onClick={handleCheckoutClick}
|
onClick={handleCheckoutClick}
|
||||||
>
|
>
|
||||||
{t("cart.checkout")}
|
{t("cart.checkout")}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { Form } from "antd";
|
||||||
import ProInputCard from "components/ProInputCard/ProInputCard.tsx";
|
import ProInputCard from "components/ProInputCard/ProInputCard.tsx";
|
||||||
import ProRatioGroups from "components/ProRatioGroups/ProRatioGroups.tsx";
|
import ProRatioGroups from "components/ProRatioGroups/ProRatioGroups.tsx";
|
||||||
import { selectCart, updateEstimateTime } from "features/order/orderSlice.ts";
|
import { selectCart, updateEstimateTime } from "features/order/orderSlice.ts";
|
||||||
@@ -27,6 +28,7 @@ export default function TimeEstimateCard() {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ProInputCard title={t("cart.estimateTime")}>
|
<ProInputCard title={t("cart.estimateTime")}>
|
||||||
|
<Form.Item name="estimateWay" required rules={[{ required: true }]}>
|
||||||
<ProRatioGroups
|
<ProRatioGroups
|
||||||
options={[
|
options={[
|
||||||
{ label: t("cart.now"), value: "now", price: "" },
|
{ label: t("cart.now"), value: "now", price: "" },
|
||||||
@@ -43,6 +45,7 @@ export default function TimeEstimateCard() {
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
</Form.Item>
|
||||||
</ProInputCard>
|
</ProInputCard>
|
||||||
{isDesktop ? (
|
{isDesktop ? (
|
||||||
<Dialog
|
<Dialog
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
|
.youMightAlsoLikeTitle {
|
||||||
|
margin: 0 16px 16px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
.youMightAlsoLikeContainer {
|
.youMightAlsoLikeContainer {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
@@ -66,6 +70,9 @@
|
|||||||
.youMightAlsoLikeContainer {
|
.youMightAlsoLikeContainer {
|
||||||
height: 200px !important;
|
height: 200px !important;
|
||||||
}
|
}
|
||||||
|
.youMightAlsoLikeTitle {
|
||||||
|
margin: 16px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Hover effects for devices that support hover */
|
/* Hover effects for devices that support hover */
|
||||||
|
|||||||
@@ -117,7 +117,7 @@ export default function YouMightAlsoLike() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div style={{ margin: 16 }}>
|
<div className={styles.youMightAlsoLikeTitle} >
|
||||||
<ProText
|
<ProText
|
||||||
strong
|
strong
|
||||||
style={{
|
style={{
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { Button } from "antd";
|
import { Button, FormInstance } from "antd";
|
||||||
import { useCallback, useMemo } from "react";
|
import { useCallback, useMemo } 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";
|
||||||
import styles from "../../address/address.module.css";
|
import styles from "../../address/address.module.css";
|
||||||
import useOrder from "../hooks/useOrder";
|
import useOrder from "../hooks/useOrder";
|
||||||
|
|
||||||
export default function CheckoutButton() {
|
export default function CheckoutButton({ form }: { form: FormInstance }) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const orderType = useMemo(() => localStorage.getItem("orderType"), []);
|
const orderType = useMemo(() => localStorage.getItem("orderType"), []);
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@@ -16,13 +16,18 @@ export default function CheckoutButton() {
|
|||||||
navigate(`/${id}/split-bill`);
|
navigate(`/${id}/split-bill`);
|
||||||
}, [navigate, id]);
|
}, [navigate, id]);
|
||||||
|
|
||||||
const handlePlaceOrderClick = useCallback(() => {
|
const handlePlaceOrderClick = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
await form.validateFields();
|
||||||
handleCreateOrder();
|
handleCreateOrder();
|
||||||
}, [handleCreateOrder]);
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
}
|
||||||
|
}, [handleCreateOrder, form]);
|
||||||
|
|
||||||
const shouldShowSplitBill = useMemo(
|
const shouldShowSplitBill = useMemo(
|
||||||
() => orderType === "dine-in",
|
() => orderType === "dine-in",
|
||||||
[orderType]
|
[orderType],
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
27
src/pages/checkout/components/phoneCard.tsx
Normal file
27
src/pages/checkout/components/phoneCard.tsx
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import { Form, Input } from "antd";
|
||||||
|
import ProInputCard from "components/ProInputCard/ProInputCard.tsx";
|
||||||
|
import { selectCart, updatePhone } from "features/order/orderSlice";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { useAppDispatch, useAppSelector } from "redux/hooks";
|
||||||
|
|
||||||
|
export default function PhoneCard() {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const { phone } = useAppSelector(selectCart);
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<ProInputCard title={t("checkout.phoneNumber")}>
|
||||||
|
<Form.Item name="phone" required rules={[{ required: true }]}>
|
||||||
|
<Input
|
||||||
|
placeholder={t("checkout.phoneNumber")}
|
||||||
|
size="large"
|
||||||
|
autoFocus={false}
|
||||||
|
style={{ padding: "7px 11px", height: 50, borderRadius: 888 }}
|
||||||
|
value={phone}
|
||||||
|
onChange={(e) => dispatch(updatePhone(e.target.value))}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
</ProInputCard>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -13,14 +13,14 @@ export default function useOrder() {
|
|||||||
const { mobilenumber, user_uuid } = JSON.parse(
|
const { mobilenumber, user_uuid } = JSON.parse(
|
||||||
localStorage.getItem("customer") || "{}"
|
localStorage.getItem("customer") || "{}"
|
||||||
) as Customer;
|
) as Customer;
|
||||||
const { items, coupon, tip, tables, specialRequest } =
|
const { items, coupon, tip, tables, specialRequest, phone } =
|
||||||
useAppSelector(selectCart);
|
useAppSelector(selectCart);
|
||||||
|
|
||||||
const [createOrder] = useCreateOrderMutation();
|
const [createOrder] = useCreateOrderMutation();
|
||||||
|
|
||||||
const handleCreateOrder = useCallback(() => {
|
const handleCreateOrder = useCallback(() => {
|
||||||
createOrder({
|
createOrder({
|
||||||
phone: mobilenumber,
|
phone: mobilenumber || phone,
|
||||||
couponID: coupon,
|
couponID: coupon,
|
||||||
discountAmount: 0,
|
discountAmount: 0,
|
||||||
comment: specialRequest,
|
comment: specialRequest,
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { Form } from "antd";
|
||||||
import OrderSummary from "components/OrderSummary/OrderSummary";
|
import OrderSummary from "components/OrderSummary/OrderSummary";
|
||||||
import PaymentMethods from "components/PaymentMethods/PaymentMethods";
|
import PaymentMethods from "components/PaymentMethods/PaymentMethods";
|
||||||
import ProHeader from "components/ProHeader/ProHeader";
|
import ProHeader from "components/ProHeader/ProHeader";
|
||||||
@@ -9,12 +10,15 @@ import CheckoutButton from "./components/CheckoutButton";
|
|||||||
import { GiftDetails } from "./components/GiftDetails";
|
import { GiftDetails } from "./components/GiftDetails";
|
||||||
import { OfficeDetails } from "./components/OfficeDetails";
|
import { OfficeDetails } from "./components/OfficeDetails";
|
||||||
import { RoomDetails } from "./components/RoomDetails";
|
import { RoomDetails } from "./components/RoomDetails";
|
||||||
|
import PhoneCard from "./components/phoneCard";
|
||||||
|
|
||||||
export default function CheckoutPage() {
|
export default function CheckoutPage() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const [form] = Form.useForm();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
<Form form={form}>
|
||||||
<ProHeader>{t("checkout.title")}</ProHeader>
|
<ProHeader>{t("checkout.title")}</ProHeader>
|
||||||
<div className={styles.checkoutContainer}>
|
<div className={styles.checkoutContainer}>
|
||||||
<AddressSummary />
|
<AddressSummary />
|
||||||
@@ -22,11 +26,13 @@ export default function CheckoutPage() {
|
|||||||
<OfficeDetails />
|
<OfficeDetails />
|
||||||
<GiftDetails />
|
<GiftDetails />
|
||||||
<BriefMenu />
|
<BriefMenu />
|
||||||
|
<PhoneCard />
|
||||||
<PaymentMethods />
|
<PaymentMethods />
|
||||||
<OrderSummary />
|
<OrderSummary />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<CheckoutButton />
|
<CheckoutButton form={form} />
|
||||||
|
</Form>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user