Initial commit

This commit is contained in:
2025-10-04 18:22:24 +03:00
commit 2852c2c054
291 changed files with 38109 additions and 0 deletions

View File

View File

@@ -0,0 +1,98 @@
import { Button, Card } from "antd";
import { GoogleMap } from "components/CustomBottomSheet/GoogleMap";
import { MapBottomSheet } from "components/CustomBottomSheet/MapBottomSheet";
import ProText from "components/ProText";
import { selectCart, updateLocation } from "features/order/orderSlice";
import { useCallback, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import { useAppDispatch, useAppSelector } from "redux/hooks";
import styles from "../../address/address.module.css";
interface LocationData {
lat: number;
lng: number;
address: string;
}
export const AddressSummary = () => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const { location } = useAppSelector(selectCart);
const [isMapBottomSheetOpen, setIsMapBottomSheetOpen] = useState(false);
const orderType = useMemo(() => localStorage.getItem("orderType"), []); // Default to delivery for now
const handleLocationSave = useCallback(
(locationString: string) => {
try {
const locationData = JSON.parse(locationString) as LocationData;
dispatch(updateLocation(locationData));
console.log("Location saved to Redux store:", locationData);
} catch (error) {
console.error("Failed to parse location data:", error);
}
},
[dispatch]
);
const handleMapBottomSheetOpen = useCallback(() => {
setIsMapBottomSheetOpen(true);
}, []);
const handleMapBottomSheetClose = useCallback(() => {
setIsMapBottomSheetOpen(false);
}, []);
const initialValue = useMemo(
() => (location ? JSON.stringify(location) : ""),
[location]
);
const shouldRender = useMemo(() => orderType === "delivery", [orderType]);
if (!shouldRender) {
return null;
}
return (
<>
<Card
title={t("locationDetails")}
extra={
<Button
type="primary"
size="small"
onClick={handleMapBottomSheetOpen}
>
{location ? t("changeLocation") : t("selectLocation")}
</Button>
}
className={styles.addressCard}
>
{!location ? (
<div className={styles.noLocationContainer}>
<ProText type="secondary">{t("noLocationSelected")}</ProText>
<br />
<ProText type="secondary" className={styles.smallTextStyle}>
{t("clickEditToSelect")}
</ProText>
</div>
) : (
<div className={styles.mapContainer}>
<GoogleMap
readOnly={true}
initialLocation={location}
height="160px"
/>
</div>
)}
</Card>
<MapBottomSheet
isOpen={isMapBottomSheetOpen}
onClose={handleMapBottomSheetClose}
initialValue={initialValue}
onSave={handleLocationSave}
/>
</>
);
};

View File

@@ -0,0 +1,57 @@
import { Button } from "antd";
import ArabicPrice from "components/ArabicPrice";
import ProInputCard from "components/ProInputCard/ProInputCard";
import ProText from "components/ProText";
import { selectCart } from "features/order/orderSlice";
import { useMemo } from "react";
import { useTranslation } from "react-i18next";
import { useAppSelector } from "redux/hooks";
import styles from "../../address/address.module.css";
export default function BriefMenu() {
const { tables, items } = useAppSelector(selectCart);
const { t } = useTranslation();
const { isRTL } = useAppSelector((state) => state.locale);
const orderType = useMemo(() => localStorage.getItem("orderType"), []);
const menuItems = useMemo(
() =>
items.map((item, index) => (
<div key={item.id}>
<div className={styles.briefMenuItem}>
<Button
type="text"
shape="circle"
className={styles.quantityButton}
>
{index + 1}X
</Button>
<div>
<ProText className={styles.itemName}>{item.name}</ProText>
<br />
<ArabicPrice
price={item.price}
type="secondary"
/>
</div>
</div>
</div>
)),
[items]
);
const cardTitle = useMemo(
() =>
orderType === "dine-in"
? t("checkout.table") + " " + tables
: t("checkout.items"),
[orderType, t, tables]
);
return (
<ProInputCard title={cardTitle}>
<div className={styles.briefMenuContainer}>{menuItems}</div>
</ProInputCard>
);
}

View File

@@ -0,0 +1,49 @@
import { Button } from "antd";
import { useCallback, useMemo } from "react";
import { useTranslation } from "react-i18next";
import { useNavigate, useParams } from "react-router-dom";
import styles from "../../address/address.module.css";
import useOrder from "../hooks/useOrder";
export default function CheckoutButton() {
const { t } = useTranslation();
const orderType = useMemo(() => localStorage.getItem("orderType"), []);
const navigate = useNavigate();
const { handleCreateOrder } = useOrder();
const { id } = useParams();
const handleSplitBillClick = useCallback(() => {
navigate(`/${id}/split-bill`);
}, [navigate, id]);
const handlePlaceOrderClick = useCallback(() => {
handleCreateOrder();
}, [handleCreateOrder]);
const shouldShowSplitBill = useMemo(
() => orderType === "dine-in",
[orderType]
);
return (
<div className={styles.checkoutButtonContainer}>
{shouldShowSplitBill && (
<Button
className={styles.splitBillButton}
onClick={handleSplitBillClick}
>
{t("checkout.splitBill")}
</Button>
)}
<Button
type="primary"
shape="round"
className={styles.placeOrderButton}
onClick={handlePlaceOrderClick}
>
{t("checkout.placeOrder")}
</Button>
</div>
);
}

View File

@@ -0,0 +1,48 @@
.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;
}
}

View File

@@ -0,0 +1,146 @@
import { Button, Card } from "antd";
import { GiftBottomSheet } from "components/CustomBottomSheet/GiftBottomSheet";
import { InfoButtonSheet } from "components/CustomBottomSheet/InfoButtonSheet";
import BuildsIcon from "components/Icons/address/BuildsIcon";
import GoldenHouseIcon from "components/Icons/address/GoldenHouseIcon";
import PresentIcon from "components/Icons/cart/PresentIcon";
import InfoIcon from "components/Icons/InfoIcon";
import { InfoButton } from "components/InfoButton/InfoButton";
import ProText from "components/ProText";
import {
GiftDetailsType,
selectCart,
updateGiftDetails,
} from "features/order/orderSlice";
import { useCallback, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import { useAppDispatch, useAppSelector } from "redux/hooks";
import styles from "../../address/address.module.css";
export const GiftDetails = () => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const { giftDetails } = useAppSelector(selectCart);
const [isOfficeBottomSheetOpen, setIsOfficeBottomSheetOpen] = useState(false);
const [isInfoButtonSheetOpen, setIsInfoButtonSheetOpen] = useState(false);
const orderType = useMemo(() => localStorage.getItem("orderType"), []);
const handleGiftDetailsSave = useCallback(
(giftDetailsData: GiftDetailsType) => {
try {
dispatch(updateGiftDetails(giftDetailsData));
console.log("Gift details saved to Redux store:", giftDetailsData);
} catch (error) {
console.error("Failed to parse location data:", error);
}
},
[dispatch]
);
const handleOfficeBottomSheetOpen = useCallback(() => {
setIsOfficeBottomSheetOpen(true);
}, []);
const handleOfficeBottomSheetClose = useCallback(() => {
setIsOfficeBottomSheetOpen(false);
}, []);
const handleInfoButtonSheetOpen = useCallback(() => {
setIsInfoButtonSheetOpen(true);
}, []);
const handleInfoButtonSheetClose = useCallback(() => {
setIsInfoButtonSheetOpen(false);
}, []);
const buttonText = useMemo(
() => (giftDetails ? t("address.changeGift") : t("address.selectGift")),
[giftDetails, t]
);
return (
orderType === "gift" && (
<>
<Card
title={t("address.giftDetails")}
extra={
<Button
type="primary"
size="small"
onClick={handleOfficeBottomSheetOpen}
>
{buttonText}
</Button>
}
>
<br />
<div className={styles.iconCenterContainer}>
<div className={styles.floatingContainer}>
<div className={styles.floatingPresent}>
<PresentIcon dimensions={125} />
</div>
<div className={styles.floatingShadow} />
</div>
</div>
<br />
<div className={styles.detailsRowContainer}>
<div className={styles.detailItemContainer}>
<Button
type="text"
shape="circle"
className={styles.iconButtonStyle}
>
<GoldenHouseIcon />
</Button>
<div>
<ProText className={styles.detailLabelStyle}>
{t("address.receiverName")}
</ProText>
<br />
<ProText type="secondary">{giftDetails?.receiverName}</ProText>
</div>
</div>
<div className={styles.detailItemContainer}>
<Button
type="text"
shape="circle"
className={styles.iconButtonStyle}
>
<BuildsIcon />
</Button>
<div>
<ProText className={styles.detailLabelStyle}>
{t("address.receiverPhone")}
</ProText>
<br />
<ProText type="secondary">{giftDetails?.receiverPhone}</ProText>
</div>
</div>
</div>
</Card>
<InfoButton
icon={<InfoIcon />}
title={t("address.howItWorks")}
onInfoClick={handleInfoButtonSheetOpen}
/>
<InfoButtonSheet
isOpen={isInfoButtonSheetOpen}
onClose={handleInfoButtonSheetClose}
title={t("address.howItWorks")}
description={t("address.howItWorksDescription")}
/>
<GiftBottomSheet
isOpen={isOfficeBottomSheetOpen}
onClose={handleOfficeBottomSheetClose}
initialValue={giftDetails}
onSave={handleGiftDetailsSave}
/>
</>
)
);
};

View File

@@ -0,0 +1,117 @@
import { Button, Card } from "antd";
import { OfficeBottomSheet } from "components/CustomBottomSheet/OfficeBottomSheet";
import BuildsIcon from "components/Icons/address/BuildsIcon";
import GoldenHouseIcon from "components/Icons/address/GoldenHouseIcon";
import RoomServiceIcon from "components/Icons/address/RoomServiceIcon";
import ProText from "components/ProText";
import {
OfficeDetailsType,
selectCart,
updateOfficeDetails,
} from "features/order/orderSlice";
import { useCallback, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import { useAppDispatch, useAppSelector } from "redux/hooks";
import styles from "../../address/address.module.css";
export const OfficeDetails = () => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const { officeDetails } = useAppSelector(selectCart);
const [isOfficeBottomSheetOpen, setIsOfficeBottomSheetOpen] = useState(false);
const orderType = useMemo(() => localStorage.getItem("orderType"), []);
const handleOfficeDetailsSave = useCallback(
(officeDetailsData: OfficeDetailsType) => {
try {
dispatch(updateOfficeDetails(officeDetailsData));
console.log("Office details saved to Redux store:", officeDetailsData);
} catch (error) {
console.error("Failed to parse location data:", error);
}
},
[dispatch]
);
const handleOfficeBottomSheetOpen = useCallback(() => {
setIsOfficeBottomSheetOpen(true);
}, []);
const handleOfficeBottomSheetClose = useCallback(() => {
setIsOfficeBottomSheetOpen(false);
}, []);
const buttonText = useMemo(
() =>
officeDetails ? t("address.changeOffice") : t("address.selectOffice"),
[officeDetails, t]
);
return (
orderType === "office" && (
<>
<Card
title={t("address.officeDetails")}
extra={
<Button
type="primary"
size="small"
onClick={handleOfficeBottomSheetOpen}
>
{buttonText}
</Button>
}
>
<br />
<div className={styles.iconCenterContainer}>
<RoomServiceIcon />
</div>
<br />
<div className={styles.detailsRowContainer}>
<div className={styles.detailItemContainer}>
<Button
type="text"
shape="circle"
className={styles.iconButtonStyle}
>
<GoldenHouseIcon />
</Button>
<div>
<ProText className={styles.detailLabelStyle}>
{t("address.floorNo")}
</ProText>
<br />
<ProText type="secondary">{officeDetails?.floorNo}</ProText>
</div>
</div>
<div className={styles.detailItemContainer}>
<Button
type="text"
shape="circle"
className={styles.iconButtonStyle}
>
<BuildsIcon />
</Button>
<div>
<ProText className={styles.detailLabelStyle}>
{t("address.officeNo")}
</ProText>
<br />
<ProText type="secondary">{officeDetails?.officeNo}</ProText>
</div>
</div>
</div>
</Card>
<OfficeBottomSheet
isOpen={isOfficeBottomSheetOpen}
onClose={handleOfficeBottomSheetClose}
initialValue={officeDetails}
onSave={handleOfficeDetailsSave}
/>
</>
)
);
};

View File

@@ -0,0 +1,116 @@
import { Button, Card } from "antd";
import { RoomBottomSheet } from "components/CustomBottomSheet/RoomBottomSheet";
import BuildsIcon from "components/Icons/address/BuildsIcon";
import GoldenHouseIcon from "components/Icons/address/GoldenHouseIcon";
import RoomServiceIcon from "components/Icons/address/RoomServiceIcon";
import ProText from "components/ProText";
import {
RoomDetailsType,
selectCart,
updateRoomDetails,
} from "features/order/orderSlice";
import { useCallback, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import { useAppDispatch, useAppSelector } from "redux/hooks";
import styles from "../../address/address.module.css";
export const RoomDetails = () => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const { roomDetails } = useAppSelector(selectCart);
const [isRoomBottomSheetOpen, setIsRoomBottomSheetOpen] = useState(false);
const orderType = useMemo(() => localStorage.getItem("orderType"), []);
const handleRoomDetailsSave = useCallback(
(roomDetailsData: RoomDetailsType) => {
try {
dispatch(updateRoomDetails(roomDetailsData));
console.log("Room details saved to Redux store:", roomDetailsData);
} catch (error) {
console.error("Failed to parse location data:", error);
}
},
[dispatch]
);
const handleRoomBottomSheetOpen = useCallback(() => {
setIsRoomBottomSheetOpen(true);
}, []);
const handleRoomBottomSheetClose = useCallback(() => {
setIsRoomBottomSheetOpen(false);
}, []);
const buttonText = useMemo(
() => (roomDetails ? t("address.changeRoom") : t("address.selectRoom")),
[roomDetails, t]
);
return (
orderType === "room" && (
<>
<Card
title={t("address.roomDetails")}
extra={
<Button
type="primary"
size="small"
onClick={handleRoomBottomSheetOpen}
>
{buttonText}
</Button>
}
>
<br />
<div className={styles.iconCenterContainer}>
<RoomServiceIcon />
</div>
<br />
<div className={styles.detailsRowContainer}>
<div className={styles.detailItemContainer}>
<Button
type="text"
shape="circle"
className={styles.iconButtonStyle}
>
<GoldenHouseIcon />
</Button>
<div>
<ProText className={styles.detailLabelStyle}>
{t("address.floorNo")}
</ProText>
<br />
<ProText type="secondary">{roomDetails?.floorNo}</ProText>
</div>
</div>
<div className={styles.detailItemContainer}>
<Button
type="text"
shape="circle"
className={styles.iconButtonStyle}
>
<BuildsIcon />
</Button>
<div>
<ProText className={styles.detailLabelStyle}>
{t("address.roomNo")}
</ProText>
<br />
<ProText type="secondary">{roomDetails?.roomNo}</ProText>
</div>
</div>
</div>
</Card>
<RoomBottomSheet
isOpen={isRoomBottomSheetOpen}
onClose={handleRoomBottomSheetClose}
initialValue={roomDetails}
onSave={handleRoomDetailsSave}
/>
</>
)
);
};

View File

@@ -0,0 +1,74 @@
import { clearCart, selectCart } from "features/order/orderSlice";
import { useCallback } from "react";
import { useNavigate, useParams } from "react-router-dom";
import { useCreateOrderMutation } from "redux/api/others";
import { useAppDispatch, useAppSelector } from "redux/hooks";
import { Customer } from "../../otp/types";
export default function useOrder() {
const dispatch = useAppDispatch();
const router = useNavigate();
const { id } = useParams();
const restaurantID = localStorage.getItem("restaurantID");
const { mobilenumber, user_uuid } = JSON.parse(
localStorage.getItem("customer") || "{}"
) as Customer;
const { items, coupon, tip, tables, specialRequest } =
useAppSelector(selectCart);
const [createOrder] = useCreateOrderMutation();
const handleCreateOrder = useCallback(() => {
createOrder({
phone: mobilenumber,
couponID: coupon,
discountAmount: 0,
comment: specialRequest,
timeslot: "",
table_id: tables,
deliveryType: "table",
type: "table-pickup",
user_id: id,
restorant_id: restaurantID,
items: items.map((i) => ({
...i,
qty: i.quantity,
})),
office_no: "",
vatvalue: 0,
discountGiftCode: "",
paymentType: "cod",
uuid: user_uuid,
pickup_comments: "",
pickup_time: "",
delivery_pickup_interval: "",
orderPrice: items.reduce(
(acc, item) => acc + item.price * item.quantity,
0
),
useWallet: 0,
tip,
})
.then(() => {
dispatch(clearCart());
router(`/${id}/order`);
})
.catch((error) => {
console.error("Create Order failed:", error);
});
}, [
createOrder,
mobilenumber,
coupon,
specialRequest,
tables,
id,
restaurantID,
items,
user_uuid,
tip,
dispatch,
router,
]);
return { handleCreateOrder };
}

View File

@@ -0,0 +1,32 @@
import OrderSummary from "components/OrderSummary/OrderSummary";
import PaymentMethods from "components/PaymentMethods/PaymentMethods";
import ProHeader from "components/ProHeader/ProHeader";
import { useTranslation } from "react-i18next";
import styles from "../address/address.module.css";
import { AddressSummary } from "./components/AddressSummary";
import BriefMenu from "./components/BriefMenu";
import CheckoutButton from "./components/CheckoutButton";
import { GiftDetails } from "./components/GiftDetails";
import { OfficeDetails } from "./components/OfficeDetails";
import { RoomDetails } from "./components/RoomDetails";
export default function CheckoutPage() {
const { t } = useTranslation();
return (
<>
<ProHeader>{t("checkout.title")}</ProHeader>
<div className={styles.checkoutContainer}>
<AddressSummary />
<RoomDetails />
<OfficeDetails />
<GiftDetails />
<BriefMenu />
<PaymentMethods />
<OrderSummary />
</div>
<CheckoutButton />
</>
);
}