Compare commits

..

5 Commits

10 changed files with 379 additions and 422 deletions

View File

@@ -0,0 +1,162 @@
import { Space, Divider } from "antd";
import ProText from "components/ProText.tsx";
import styles from "pages/cart/cart.module.css";
import ArabicPrice from "components/ArabicPrice";
import ImageWithFallback from "components/ImageWithFallback";
import CartActionsButtons from "components/CartActionsButtons/CartActionsButtons.tsx";
import { CartItem } from "utils/types/appTypes.ts";
import { useAppSelector } from "redux/hooks.ts";
import useBreakPoint from "hooks/useBreakPoint.ts";
import { OrderItem } from "pages/checkout/hooks/types.ts";
type ProductChoicesCardProps = {
product: CartItem | OrderItem;
addDividerAfter: boolean;
};
export default function ProductChoicesCard({
product,
addDividerAfter,
}: ProductChoicesCardProps) {
const { isRTL } = useAppSelector((state) => state.locale);
const { isMobile, isTablet } = useBreakPoint();
const getMenuItemImageStyle = () => {
if (isMobile) {
return {
width: 115,
height: 96,
};
}
return {
width: 120,
height: 120,
};
};
return (
<div style={{ position: "relative" }}>
<Space
size="middle"
style={{
display: "flex",
alignItems: "center",
justifyContent: "space-between",
height: "100%",
}}
>
<Space orientation="vertical" size="small">
<div
style={{
position: "absolute",
top: 0,
[isRTL ? "right" : "left"]: 0,
}}
>
<ProText
style={{
margin: 0,
lineClamp: 1,
fontSize: isMobile ? 14 : isTablet ? 16 : 18,
fontWeight: 600,
}}
>
{product.name}
</ProText>
<br />
<ProText
type="secondary"
className={`${styles.itemDescription} responsive-text`}
style={{
margin: 0,
lineClamp: 1,
padding: isMobile ? "3px 0" : isTablet ? 8 : 10,
fontSize: isMobile ? 14 : isTablet ? 18 : 20,
display: "-webkit-box",
WebkitBoxOrient: "vertical",
WebkitLineClamp: 1,
overflow: "hidden",
textOverflow: "ellipsis",
wordWrap: "break-word",
overflowWrap: "break-word",
lineHeight: "1.4",
maxHeight: isMobile ? "3em" : isTablet ? "5em" : "7em",
fontWeight: 500,
letterSpacing: "0.01em",
width: "100%",
}}
>
{product.type === "CartItem" &&
(isRTL
? product.variant?.optionsAR.map((o) => o.value).join(", ")
: product.variant?.options.map((o) => o.value).join(", "))}
{product.type === "OrderItem" && product.variantName}
</ProText>
{product.type === "CartItem"
? product.extrasgroupnew?.map((o) => (
<ProText
type="secondary"
className={`${styles.itemDescription} responsive-text`}
style={{
margin: 0,
lineClamp: 1,
padding: isMobile ? "3px 0" : isTablet ? 8 : 10,
fontSize: isMobile ? 14 : isTablet ? 18 : 20,
display: "-webkit-box",
WebkitBoxOrient: "vertical",
WebkitLineClamp: 1,
overflow: "hidden",
textOverflow: "ellipsis",
wordWrap: "break-word",
overflowWrap: "break-word",
lineHeight: "1.4",
maxHeight: isMobile ? "3em" : isTablet ? "5em" : "7em",
fontWeight: 500,
letterSpacing: "0.01em",
width: "100%",
}}
>
{isRTL ? o.extrasStringAR : o.extrasString}
</ProText>
))
: // ToDo
product.itemline}
</div>
<div
style={{
position: "absolute",
bottom: addDividerAfter ? 16 : 3,
[isRTL ? "right" : "left"]: 0,
}}
>
<ArabicPrice price={product.price} style={{ fontStyle: "bold" }} />
</div>
</Space>
<div style={{ position: "relative" }}>
<ImageWithFallback
src={product.image}
alt={product.name}
className={`${styles.menuItemImage} responsive-image`}
{...getMenuItemImageStyle()}
fallbackSrc={
"https://fascano-space.s3.me-central-1.amazonaws.com/uploads/restorants/685a8fc884a8c_large.jpg"
}
/>
{product.type === "CartItem" && (
<div
style={{
position: "absolute",
right: 3,
bottom: 3,
}}
>
<CartActionsButtons item={product} />
</div>
)}
</div>
</Space>
{addDividerAfter && <Divider style={{ margin: "10px 0" }} />}
</div>
);
}

View File

@@ -1,9 +1,6 @@
import { Card, Divider, Space, Layout, Button, Popconfirm } from "antd";
import ArabicPrice from "components/ArabicPrice";
import CartActionsButtons from "components/CartActionsButtons/CartActionsButtons.tsx";
import ImageWithFallback from "components/ImageWithFallback";
import ProHeader from "components/ProHeader/ProHeader.tsx";
import ProText from "components/ProText.tsx";
import ProTitle from "components/ProTitle.tsx";
import { clearCart, selectCart } from "features/order/orderSlice.ts";
import styles from "pages/cart/cart.module.css";
@@ -20,10 +17,10 @@ import SpecialRequestCard from "pages/cart/components/specialRequest/SpecialRequ
import TableNumberCard from "pages/cart/components/TableNumberCard.tsx";
import { OrderType } from "pages/checkout/hooks/types";
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";
import ProductChoicesCard from "components/productChoicesCard/ProductChoicesCard.tsx";
interface CartMobileTabletLayoutProps {
form: FormInstance;
@@ -41,18 +38,6 @@ export default function CartMobileTabletLayout({
const getResponsiveClass = () => (isTablet ? "tablet" : "mobile");
const navigate = useNavigate();
const dispatch = useAppDispatch();
const getMenuItemImageStyle = () => {
if (isMobile) {
return {
width: 115,
height: 96,
};
}
return {
width: 120,
height: 120,
};
};
return (
<Layout>
@@ -137,112 +122,11 @@ export default function CartMobileTabletLayout({
<Divider style={{ margin: "8px 0px 12px 0px" }} />
)}
{items.map((item, index) => (
<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
style={{
position: "absolute",
top: 0,
[isRTL ? "right" : "left"]: 0,
}}
>
<ProText
style={{
margin: 0,
lineClamp: 1,
fontSize: isMobile ? 14 : isTablet ? 16 : 18,
fontWeight: 600,
}}
>
{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
type="secondary"
className={`${styles.itemDescription} responsive-text`}
style={{
margin: 0,
lineClamp: 1,
padding: isMobile ? "3px 0" : isTablet ? 8 : 10,
fontSize: isMobile ? 14 : isTablet ? 18 : 20,
display: "-webkit-box",
WebkitBoxOrient: "vertical",
WebkitLineClamp: 1,
overflow: "hidden",
textOverflow: "ellipsis",
wordWrap: "break-word",
overflowWrap: "break-word",
lineHeight: "1.4",
maxHeight: isMobile
? "3em"
: isTablet
? "5em"
: "7em",
fontWeight: 500,
letterSpacing: "0.01em",
width: "55%",
}}
>
{item.description}
</ProText>
</div>
<div
style={{
position: "absolute",
bottom: index !== items.length - 1 ? 16 : 3,
[isRTL ? "right" : "left"]: 0,
}}
>
<ArabicPrice
price={item.price}
style={{ fontStyle: "bold" }}
/>
</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"
}
/>
<div
style={{
position: "absolute",
right: 3,
bottom: 3,
}}
>
<CartActionsButtons item={item} />
</div>
</div>
</Space>
{index !== items.length - 1 && (
<Divider style={{ margin: "10px 0" }} />
)}
</div>
<ProductChoicesCard
key={index}
product={item}
addDividerAfter={index !== items.length - 1}
/>
))}
<Button
style={{ width: "100%", marginTop: 24, color: "#4C4A58" }}

View File

@@ -106,7 +106,7 @@ export default function YouMightAlsoLike() {
price: item.price,
image: item.image,
description: item.description,
variant: "None",
variant: undefined,
isHasLoyalty: item.isHasLoyalty,
no_of_stamps_give: item.no_of_stamps_give,
},

View File

@@ -1,6 +1,3 @@
import { Variant } from "pages/orders/types";
import { Extra3 } from "utils/types/appTypes";
export interface OrderDetails {
orderItems: OrderItem[];
order: Order;
@@ -18,7 +15,18 @@ export interface OrderDetails {
itemsImagePrefix: string;
}
export interface OrderItem {
type OrderVariant = {
id: number;
price: number;
options: string;
local_name: string;
optionsArray: { id: number; name: string }[];
OptionsList: string;
extras: any[];
};
export type OrderItem = {
type: "OrderItem";
id: number;
is_loyalty_used: number;
no_of_stamps_give: number;
@@ -31,16 +39,19 @@ export interface OrderItem {
image: string;
imageName: string;
variantName: string;
variantLocalName?: string;
variantLocalName: string;
extras: any[];
descriptionEN: string;
descriptionAR: string;
itemline: string;
itemlineAR: string;
itemlineAREN: string;
extrasgroups: any[];
extrasgroups: string[];
extragroupnew: any[];
itemComment: string;
variant?: Variant;
variant: OrderVariant;
itemExtras: any[];
AvaiilableVariantExtras: Extra3[];
AvaiilableVariantExtras: any[];
isPrinted: number;
category_id: number;
pos_order_id: string;
@@ -56,7 +67,7 @@ export interface OrderItem {
pricing_method: string;
is_already_paid: number;
hash_item: string;
}
};
export interface Order {
id: number;
@@ -247,5 +258,5 @@ export enum OrderType {
ToOffice = "office",
Booking = "booking",
Pay = "pay",
Redeem = "redeem"
Redeem = "redeem",
}

View File

@@ -38,7 +38,7 @@ export function AddToCartButton({ item }: { item: Product }) {
// Find basic cart item (no variants/extras) - the one added by quick add
const basicCartItem = cartItemsForProduct.find(
(cartItem) =>
(!cartItem.variant || cartItem.variant === "None") &&
!cartItem.variant &&
(!cartItem.extras || cartItem.extras.length === 0) &&
(!cartItem.extrasgroupnew || cartItem.extrasgroupnew.length === 0),
);
@@ -67,7 +67,7 @@ export function AddToCartButton({ item }: { item: Product }) {
price: item.price,
image: item.image,
description: item.description,
variant: "None",
variant: undefined,
isHasLoyalty: item.isHasLoyalty,
no_of_stamps_give: item.no_of_stamps_give,
},
@@ -145,7 +145,7 @@ export function AddToCartButton({ item }: { item: Product }) {
price: item.price,
image: item.image,
description: item.description,
variant: "None",
variant: undefined,
isHasLoyalty: item.isHasLoyalty,
no_of_stamps_give: item.no_of_stamps_give,
},

View File

@@ -1,19 +1,15 @@
import { Button, Card, Divider, Form, Layout, Space } from "antd";
import { Button, Card, Form, Layout } from "antd";
import { useGetOrderDetailsQuery } from "redux/api/others";
import { useAppSelector } from "redux/hooks";
import styles from "./OrderDetails.module.css";
import ProHeader from "components/ProHeader/ProHeader";
import { useTranslation } from "react-i18next";
import ProText from "components/ProText";
import useBreakPoint from "hooks/useBreakPoint";
import ImageWithFallback from "components/ImageWithFallback";
import { useParams } from "react-router-dom";
import ProductChoicesCard from "components/productChoicesCard/ProductChoicesCard.tsx";
export default function OrderDetails() {
const { orderId } = useParams();
const { t } = useTranslation();
const { isRTL } = useAppSelector((state) => state.locale);
const { isMobile, isTablet } = useBreakPoint();
const { data: orderDetails } = useGetOrderDetailsQuery(
{
@@ -25,138 +21,18 @@ export default function OrderDetails() {
},
);
const getMenuItemImageStyle = () => {
if (isMobile) {
return {
width: 115,
height: 96,
};
}
return {
width: 120,
height: 120,
};
};
return (
<Form layout="vertical">
<Layout>
<ProHeader>{t("order.yourOrder")}</ProHeader>
<Layout.Content className={styles.orderDetailsContainer}>
<Card className={styles.cartItem}>
{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%",
padding: 8,
}}
>
<Space orientation="vertical" size="small">
<div
style={{
position: "absolute",
top: 8,
[isRTL ? "right" : "left"]: 8,
}}
>
<ProText
style={{
margin: 0,
lineClamp: 1,
fontSize: isMobile ? 14 : isTablet ? 16 : 18,
fontWeight: 600,
}}
>
{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
type="secondary"
style={{
margin: 0,
lineClamp: 1,
padding: isMobile ? "3px 0" : isTablet ? 8 : 10,
fontSize: isMobile ? 14 : isTablet ? 18 : 20,
display: "-webkit-box",
WebkitBoxOrient: "vertical",
WebkitLineClamp: 1,
overflow: "hidden",
textOverflow: "ellipsis",
wordWrap: "break-word",
overflowWrap: "break-word",
lineHeight: "1.4",
maxHeight: isMobile
? "3em"
: isTablet
? "5em"
: "7em",
fontWeight: 500,
letterSpacing: "0.01em",
width: "55%",
}}
>
{item.description}
</ProText>
</div>
<div
style={{
position: "absolute",
bottom:
index !== orderDetails?.orderItems?.length - 1
? 20
: 7,
[isRTL ? "right" : "left"]: 8,
}}
>
<Button
shape="circle"
iconPlacement="start"
size="small"
className={styles.addButton}
style={{
background: "#F5F5F6",
width: 28,
height: 28,
border: "none",
}}
>
<ProText style={{ color: "#1F1C2E" }}>
{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"
}
/>
</div>
</Space>
{index !== orderDetails?.orderItems?.length - 1 && (
<Divider style={{ margin: "16px 0" }} />
)}
</div>
{orderDetails?.orderItems.map((item, index) => (
<ProductChoicesCard
key={index}
product={item}
addDividerAfter={index !== orderDetails?.orderItems.length - 1}
/>
))}
</Card>
</Layout.Content>

View File

@@ -2,12 +2,9 @@ import { Button } from "antd";
import { ProBottomSheet } from "components/ProBottomSheet/ProBottomSheet.tsx";
import { useTranslation } from "react-i18next";
import { useAppSelector } from "redux/hooks";
import ProText from "components/ProText";
import ArabicPrice from "components/ArabicPrice";
import styles from "../order.module.css";
import NextIcon from "components/Icons/NextIcon";
import BackIcon from "components/Icons/BackIcon";
interface SplitBillParticipantsBottomSheetProps {
isOpen: boolean;
@@ -19,7 +16,6 @@ export function SplitBillParticipantsBottomSheet({
onClose,
}: SplitBillParticipantsBottomSheetProps) {
const { t } = useTranslation();
const { isRTL } = useAppSelector((state) => state.locale);
const taxesChargesStyle = {
fontWeight: 400,
@@ -59,13 +55,15 @@ export function SplitBillParticipantsBottomSheet({
placeItems: "center",
}}
>
<div style={{
display: "flex",
flexDirection: "row",
justifyContent: "flex-start",
placeItems: "center",
gap: 10,
}}>
<div
style={{
display: "flex",
flexDirection: "row",
justifyContent: "flex-start",
placeItems: "center",
gap: 10,
}}
>
<Button
shape="circle"
iconPlacement="start"

View File

@@ -1,92 +1,93 @@
export interface ProductDetails {
data: Daum[]
name: string
nameAR: string
image: string
price: number
variants: Variant[]
theExtrasGroups: TheExtrasGroup[]
discount: number
options: Option[]
hasVariants: number
currency: string
short_description: string
short_descriptionAR: string
}
data: Daum[];
name: string;
nameAR: string;
image: string;
price: number;
variants: Variant[];
theExtrasGroups: TheExtrasGroup[];
discount: number;
options: Option[];
hasVariants: number;
currency: string;
short_description: string;
short_descriptionAR: string;
}
export interface Daum {
id: number
name: string
data: string[]
prices: string[]
pricesNew: string[]
nameAR: string
dataAR: string[]
}
export interface Daum {
id: number;
name: string;
data: string[];
prices: string[];
pricesNew: string[];
nameAR: string;
dataAR: string[];
}
type VariantOptionType = { option: string; value: string };
export interface Variant {
id: number
price: number
options: string
image: string
qty: number
enable_qty: number
order: number
item_id: number
created_at: string
updated_at: string
deleted_at: any
available: string
OptionsList: string
extras: any[]
}
export interface Variant {
id: number;
price: number;
options: VariantOptionType[];
optionsAR: VariantOptionType[];
image: string;
qty: number;
enable_qty: number;
order: number;
item_id: number;
created_at: string;
updated_at: string;
deleted_at: any;
available: string;
OptionsList: string;
extras: any[];
}
export interface TheExtrasGroup {
id: number
name: string
nameAR: string
label: string
labelAR: string
limit: number
item_id: number
created_at: string
updated_at: string
deleted_at: any
force_limit_selection: number
extras: Extra[]
}
export interface TheExtrasGroup {
id: number;
name: string;
nameAR: string;
label: string;
labelAR: string;
limit: number;
item_id: number;
created_at: string;
updated_at: string;
deleted_at: any;
force_limit_selection: number;
extras: Extra[];
}
export interface Extra {
id: number
item_id: number
price: number
name: string
nameAR: string
created_at: string
updated_at: string
deleted_at: any
extra_for_all_variants: number
is_custome: number
is_available: number
modifier_id: any
pivot: Pivot
}
export interface Extra {
id: number;
item_id: number;
price: number;
name: string;
nameAR: string;
created_at: string;
updated_at: string;
deleted_at: any;
extra_for_all_variants: number;
is_custome: number;
is_available: number;
modifier_id: any;
pivot: Pivot;
}
export interface Pivot {
group_id: number
extra_id: number
}
export interface Option {
id: number
item_id: number
name: string
nameAR: string
options: string
optionsAR: string
optionprices: string
created_at: string
updated_at: string
deleted_at: any
}
export interface Pivot {
group_id: number;
extra_id: number;
}
export interface Option {
id: number;
item_id: number;
name: string;
nameAR: string;
options: string;
optionsAR: string;
optionprices: string;
created_at: string;
updated_at: string;
deleted_at: any;
}

View File

@@ -85,10 +85,23 @@ export default function ProductFooter({
comment: specialRequest,
variant: selectedVariant,
extras: selectedExtras,
extrasgroupnew: Object.keys(selectedGroups).map((key) => ({
groupid: key,
extrasid: selectedGroups[Number(key)],
})),
extrasgroupnew: Object.keys(selectedGroups).map((key) => {
const groupInfo = product.theExtrasGroups?.find(
(g) => g.id.toString() === key,
);
const selectedGroupExtrasIds = selectedGroups[Number(key)];
const extrasInfo = groupInfo?.extras.filter((e) =>
selectedGroupExtrasIds.includes(e.id.toString()),
);
console.log(extrasInfo);
return {
groupid: key,
extrasid: selectedGroupExtrasIds,
extrasString: `${groupInfo?.name}: ${extrasInfo?.map((e) => e.name).join(", ")}`,
extrasStringAR: `${groupInfo?.nameAR}: ${extrasInfo?.map((e) => e.nameAR).join(" + ")}`,
};
}),
extrasgroup: Object.entries(selectedGroups).flatMap(
([groupId, extrasIds]) => {
if (!extrasIds || extrasIds.length === 0) {
@@ -117,7 +130,7 @@ export default function ProductFooter({
}),
);
// Navigate back to menu - scroll position will be restored automatically
if (!isDesktop && !isBottomSheetView ) window.history.back();
if (!isDesktop && !isBottomSheetView) window.history.back();
else {
onClose?.();
}
@@ -175,46 +188,44 @@ export default function ProductFooter({
width: "100%",
}}
>
<ActionsButtons
quantity={quantity}
setQuantity={setQuantity}
max={100}
min={1}
/>
<Button
type="primary"
icon={<ShoppingCartOutlined />}
onClick={handleAddToCart}
disabled={!isValid}
style={{
flex: 1,
height: 48,
fontSize: isMobile ? "1rem" : "16px",
transition: "all 0.3s ease",
width: "100%",
borderRadius: 888,
boxShadow: "none",
backgroundColor: isValid
? colors.primary
: "rgba(233, 233, 233, 1)",
color: isValid ? "#FFF" : "#999",
cursor: isValid ? "pointer" : "not-allowed",
}}
onMouseEnter={(e) => {
if (!isMobile && isValid) {
e.currentTarget.style.transform = "translateY(-2px)";
}
}}
onMouseLeave={(e) => {
if (!isMobile) {
e.currentTarget.style.transform = "translateY(0)";
}
}}
>
{isValid
? t("menu.addToCart")
: t("menu.selectRequiredOptions")}
</Button>
<ActionsButtons
quantity={quantity}
setQuantity={setQuantity}
max={100}
min={1}
/>
<Button
type="primary"
icon={<ShoppingCartOutlined />}
onClick={handleAddToCart}
disabled={!isValid}
style={{
flex: 1,
height: 48,
fontSize: isMobile ? "1rem" : "16px",
transition: "all 0.3s ease",
width: "100%",
borderRadius: 888,
boxShadow: "none",
backgroundColor: isValid
? colors.primary
: "rgba(233, 233, 233, 1)",
color: isValid ? "#FFF" : "#999",
cursor: isValid ? "pointer" : "not-allowed",
}}
onMouseEnter={(e) => {
if (!isMobile && isValid) {
e.currentTarget.style.transform = "translateY(-2px)";
}
}}
onMouseLeave={(e) => {
if (!isMobile) {
e.currentTarget.style.transform = "translateY(0)";
}
}}
>
{isValid ? t("menu.addToCart") : t("menu.selectRequiredOptions")}
</Button>
</div>
</div>
</Row>

View File

@@ -314,24 +314,29 @@ export interface Translation {
// #################################################
export interface CartItem {
export type CartItem = {
type: "CartItem";
id: number | string;
name: string;
price: number;
image: string;
quantity: number;
description: string;
variant?: Variant | string;
variant?: Variant;
extras?: Extra[];
extrasgroupnew?: Array<{ groupid: string; extrasid: Array<string> }>;
extrasgroupnew?: Array<{
groupid: string;
extrasid: Array<string>;
extrasString: string;
extrasStringAR: string;
}>;
extrasgroup?: Array<string>;
isHasLoyalty?: boolean;
no_of_stamps_give?: number;
comment?: string;
// Unique identifier for cart items with the same product but different variants/extras/comments
uniqueId?: string;
}
};
export interface UserType {
id: number;
@@ -616,8 +621,17 @@ export interface Option {
export interface Extra {
id: number;
item_id: number;
price: number;
name: string;
nameAR: string;
created_at: string;
updated_at: string;
deleted_at: any;
extra_for_all_variants: number;
is_custome: number;
is_available: number;
modifier_id: any;
}
export interface TheExtrasGroup {