Initial commit
This commit is contained in:
273
src/pages/address/address.module.css
Normal file
273
src/pages/address/address.module.css
Normal file
@@ -0,0 +1,273 @@
|
||||
.productContainer :global(.ant-radio-wrapper:last-child) {
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
.productContainer :global(.ant-radio-label) {
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
/* radio.module.css */
|
||||
.productContainer :global(.ant-radio-inner) {
|
||||
width: 24px !important;
|
||||
height: 24px !important;
|
||||
border-radius: 30px !important;
|
||||
}
|
||||
|
||||
.productContainer :global(.ant-radio) {
|
||||
width: 24px !important;
|
||||
height: 24px !important;
|
||||
}
|
||||
|
||||
.productContainer :global(.ant-radio-checked .ant-radio-inner) {
|
||||
border-radius: 30px !important;
|
||||
}
|
||||
|
||||
.productContainer :global(.ant-checkbox-wrapper:last-child) {
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
.productContainer :global(.ant-checkbox-label) {
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
/* .productContainer :global(.ant-checkbox-input) {
|
||||
margin-top: 5px !important;
|
||||
} */
|
||||
|
||||
.productContainer :global(.ant-checkbox-inner) {
|
||||
border-radius: 40px !important;
|
||||
}
|
||||
|
||||
/* CheckboxGroup.module.css */
|
||||
.productContainer :global(.ant-checkbox-inner) {
|
||||
width: 24px !important;
|
||||
height: 24px !important;
|
||||
border-radius: 30px !important;
|
||||
}
|
||||
|
||||
.productContainer :global(.ant-checkbox) {
|
||||
width: 24px !important;
|
||||
height: 24px !important;
|
||||
}
|
||||
|
||||
.productContainer :global(.ant-checkbox-checked .ant-checkbox-inner) {
|
||||
border-radius: 30px !important;
|
||||
}
|
||||
|
||||
.services {
|
||||
width: 100%;
|
||||
padding: 0 5px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
gap: 3px;
|
||||
}
|
||||
|
||||
.services :global(.ant-btn) {
|
||||
padding: 0 11px !important;
|
||||
}
|
||||
|
||||
.serviceButton {
|
||||
position: relative;
|
||||
top: 5px;
|
||||
height: 32px;
|
||||
color: #99a2ae;
|
||||
background-color: #f7f7f7;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.activeServiceButton {
|
||||
position: relative;
|
||||
top: 6px;
|
||||
height: 24px;
|
||||
border: none;
|
||||
color: #ffb700;
|
||||
background-color: rgba(255, 183, 0, 0.12);
|
||||
width: 70px !important;
|
||||
}
|
||||
|
||||
:global(.darkApp) .services {
|
||||
color: #ffffff !important;
|
||||
background-color: #000000 !important;
|
||||
border: none;
|
||||
}
|
||||
|
||||
:global(.darkApp) .serviceButton {
|
||||
color: #ffffff !important;
|
||||
background-color: rgba(42, 42, 42, 0.8) !important;
|
||||
border: none;
|
||||
width: 70px !important;
|
||||
border-radius: 888px;
|
||||
}
|
||||
|
||||
:global(.darkApp) .activeServiceButton {
|
||||
color: #ffb700 !important;
|
||||
background-color: rgba(42, 42, 42, 0.8) !important;
|
||||
border: none;
|
||||
width: 70px !important;
|
||||
border-radius: 888px;
|
||||
}
|
||||
|
||||
/* Checkout Page Styles */
|
||||
.checkoutContainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 80vh;
|
||||
min-height: 80vh;
|
||||
padding: 16px;
|
||||
gap: 16px;
|
||||
overflow: auto;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
/* AddressSummary Styles */
|
||||
.addressCard {
|
||||
height: 250px;
|
||||
}
|
||||
|
||||
.noLocationContainer {
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.mapContainer {
|
||||
height: 260px;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* BriefMenu Styles */
|
||||
.briefMenuContainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.briefMenuItem {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.quantityButton {
|
||||
background-color: rgba(255, 183, 0, 0.08);
|
||||
}
|
||||
|
||||
.itemName {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
/* CheckoutButton Styles */
|
||||
.checkoutButtonContainer {
|
||||
width: 100%;
|
||||
padding: 16px 16px 0;
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
height: 10vh;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-around;
|
||||
gap: 1rem;
|
||||
background-color: white;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.splitBillButton {
|
||||
border-radius: 100px;
|
||||
height: 48px;
|
||||
border-color: black;
|
||||
width: 100%;
|
||||
font-size: 16px;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.placeOrderButton {
|
||||
width: 100%;
|
||||
height: 48px;
|
||||
margin-bottom: 16px;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
/* GiftDetails, OfficeDetails, RoomDetails Styles */
|
||||
.detailsCard {
|
||||
/* Base card styles if needed */
|
||||
}
|
||||
|
||||
.iconContainer {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.detailsRow {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.detailItem {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.iconButton {
|
||||
background-color: rgba(255, 183, 0, 0.08);
|
||||
}
|
||||
|
||||
.detailLabel {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* Dark theme styles for checkout */
|
||||
:global(.darkApp) .checkoutButtonContainer {
|
||||
background-color: #000000 !important;
|
||||
}
|
||||
|
||||
:global(.darkApp) .splitBillButton {
|
||||
color: #ffffff !important;
|
||||
background-color: rgba(42, 42, 42, 0.8) !important;
|
||||
border-color: #ffffff !important;
|
||||
}
|
||||
|
||||
:global(.darkApp) .placeOrderButton {
|
||||
background-color: #ffb700 !important;
|
||||
border-color: #ffb700 !important;
|
||||
}
|
||||
|
||||
/* Additional styles for checkout components */
|
||||
.iconCenterContainer {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.detailsRowContainer {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.detailItemContainer {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.iconButtonStyle {
|
||||
background-color: rgba(255, 183, 0, 0.08);
|
||||
}
|
||||
|
||||
.detailLabelStyle {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.smallTextStyle {
|
||||
font-size: 12px;
|
||||
}
|
||||
196
src/pages/address/page.tsx
Normal file
196
src/pages/address/page.tsx
Normal file
@@ -0,0 +1,196 @@
|
||||
import { Button, Card, Divider, Form, Input, Row } from "antd";
|
||||
import ApartmentIcon from "components/Icons/address/ApartmentIcon";
|
||||
import HouseIcon from "components/Icons/address/HouseIcon";
|
||||
import OfficeIcon from "components/Icons/address/OfficeIcon";
|
||||
import ProHeader from "components/ProHeader/ProHeader";
|
||||
import ProText from "components/ProText";
|
||||
import { selectCart } from "features/order/orderSlice";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Link, useParams } from "react-router-dom";
|
||||
import { useAppSelector } from "redux/hooks";
|
||||
import { colors } from "ThemeConstants";
|
||||
import { AddressSummary } from "../checkout/components/AddressSummary";
|
||||
import styles from "./address.module.css";
|
||||
|
||||
export default function AddressPage() {
|
||||
const { t } = useTranslation();
|
||||
const { location } = useAppSelector(selectCart);
|
||||
const [form] = Form.useForm();
|
||||
const { id } = useParams();
|
||||
|
||||
return (
|
||||
<>
|
||||
<ProHeader>{t("address.title")}</ProHeader>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
height: "80vh",
|
||||
minHeight: "80vh",
|
||||
padding: 16,
|
||||
overflowY: "auto",
|
||||
scrollbarWidth: "none",
|
||||
marginBottom: "10vh",
|
||||
}}
|
||||
>
|
||||
<AddressSummary />
|
||||
|
||||
<Card style={{ marginTop: 16 }} title={t("addressDetails")}>
|
||||
<div className={styles.services}>
|
||||
<Button className={styles.serviceButton} icon={<ApartmentIcon />}>
|
||||
<ProText
|
||||
style={{ fontWeight: "bold", color: "rgba(95, 108, 123, 1)" }}
|
||||
>
|
||||
{t("apartment")}
|
||||
</ProText>
|
||||
</Button>
|
||||
<Button className={styles.serviceButton} icon={<HouseIcon />}>
|
||||
<ProText
|
||||
style={{ fontWeight: "bold", color: "rgba(95, 108, 123, 1)" }}
|
||||
>
|
||||
{t("house")}
|
||||
</ProText>
|
||||
</Button>
|
||||
<Button className={styles.serviceButton} icon={<OfficeIcon />}>
|
||||
<ProText
|
||||
style={{ fontWeight: "bold", color: "rgba(95, 108, 123, 1)" }}
|
||||
>
|
||||
{t("office")}
|
||||
</ProText>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Divider style={{ margin: "17px 0 0 0" }} />
|
||||
|
||||
<Form
|
||||
layout="vertical"
|
||||
style={{ marginTop: 12 }}
|
||||
name="loginForm"
|
||||
form={form}
|
||||
>
|
||||
<div style={{ display: "flex", gap: 10 }}>
|
||||
<Form.Item
|
||||
label={t("floor")}
|
||||
name="floor"
|
||||
rules={[{ required: true, message: "" }]}
|
||||
>
|
||||
<Input
|
||||
placeholder={t("floor")}
|
||||
style={{
|
||||
fontSize: 14,
|
||||
}}
|
||||
autoFocus={false}
|
||||
/>
|
||||
</Form.Item>{" "}
|
||||
<Form.Item
|
||||
label={t("aptNumber")}
|
||||
name="apt"
|
||||
rules={[{ required: true, message: "" }]}
|
||||
>
|
||||
<Input
|
||||
placeholder={t("aptNumber")}
|
||||
style={{
|
||||
fontSize: 14,
|
||||
}}
|
||||
autoFocus={false}
|
||||
/>
|
||||
</Form.Item>
|
||||
</div>
|
||||
<Form.Item
|
||||
label={t("street")}
|
||||
name="street"
|
||||
rules={[{ required: true, message: "" }]}
|
||||
>
|
||||
<Input
|
||||
placeholder={t("street")}
|
||||
style={{
|
||||
fontSize: 14,
|
||||
}}
|
||||
autoFocus={false}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("additionalDirection")}
|
||||
name="additional"
|
||||
rules={[{ required: true, message: "" }]}
|
||||
>
|
||||
<Input
|
||||
placeholder={t("additionalDirection")}
|
||||
style={{
|
||||
fontSize: 14,
|
||||
}}
|
||||
autoFocus={false}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label={t("mobileNumber")}
|
||||
name="phone"
|
||||
rules={[{ required: true, message: "" }]}
|
||||
>
|
||||
<Input
|
||||
placeholder={t("mobileNumber")}
|
||||
style={{
|
||||
fontSize: 14,
|
||||
}}
|
||||
autoFocus={false}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label={t("addressLabel")}
|
||||
name="addressLabel"
|
||||
rules={[{ required: true, message: "" }]}
|
||||
>
|
||||
<Input
|
||||
placeholder={t("addressLabel")}
|
||||
style={{
|
||||
fontSize: 14,
|
||||
}}
|
||||
autoFocus={false}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Card>
|
||||
</div>
|
||||
<Row
|
||||
style={{
|
||||
width: "100%",
|
||||
padding: "16px 16px 0",
|
||||
position: "fixed",
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
backgroundColor: "white",
|
||||
boxShadow: "0px -1px 3px rgba(0, 0, 0, 0.1)",
|
||||
height: "10vh",
|
||||
}}
|
||||
>
|
||||
<Link
|
||||
to={`/${id}/menu?orderType=delivery`}
|
||||
style={{ width: "100%" }}
|
||||
onClick={() => {
|
||||
localStorage.setItem("orderType", "delivery");
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
type="primary"
|
||||
size="large"
|
||||
style={{
|
||||
width: "100%",
|
||||
borderRadius: 1000,
|
||||
backgroundColor: !location
|
||||
? "rgba(233, 233, 233, 1)"
|
||||
: colors.primary,
|
||||
color: "#FFF",
|
||||
height: 50,
|
||||
border: "none",
|
||||
}}
|
||||
disabled={false}
|
||||
>
|
||||
{t("saveAddress")}
|
||||
</Button>
|
||||
</Link>
|
||||
</Row>
|
||||
</>
|
||||
);
|
||||
}
|
||||
92
src/pages/address/types.ts
Normal file
92
src/pages/address/types.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
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
|
||||
}
|
||||
|
||||
export interface Daum {
|
||||
id: number
|
||||
name: string
|
||||
data: string[]
|
||||
prices: string[]
|
||||
pricesNew: string[]
|
||||
nameAR: string
|
||||
dataAR: 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 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 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
|
||||
}
|
||||
|
||||
190
src/pages/authentication/SignIn.tsx
Normal file
190
src/pages/authentication/SignIn.tsx
Normal file
@@ -0,0 +1,190 @@
|
||||
import {
|
||||
Button,
|
||||
Checkbox,
|
||||
Col,
|
||||
Flex,
|
||||
Form,
|
||||
Grid,
|
||||
Input,
|
||||
message,
|
||||
Row,
|
||||
Typography,
|
||||
} from "antd";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
import { LogoutOutlined } from "@ant-design/icons";
|
||||
import BackIcon from "components/Icons/BackIcon";
|
||||
import NextIcon from "components/Icons/NextIcon";
|
||||
import { loginSuccess } from "features/auth/authSlice";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useCreateLoginMutation } from "redux/api/auth";
|
||||
import { useAppDispatch, useAppSelector } from "redux/hooks";
|
||||
import { PATH_AUTH, PATHS } from "utils/constants";
|
||||
import { ResponseType } from "utils/types/appTypes";
|
||||
import "./styles.css";
|
||||
|
||||
const { Title, Text, Link } = Typography;
|
||||
|
||||
type FieldType = {
|
||||
username?: string;
|
||||
password?: string;
|
||||
remember?: boolean;
|
||||
};
|
||||
const { useBreakpoint } = Grid;
|
||||
|
||||
export const SignInPage = () => {
|
||||
const { t } = useTranslation("login");
|
||||
const dispatch = useAppDispatch();
|
||||
const navigate = useNavigate();
|
||||
const { xs } = useBreakpoint();
|
||||
const [createLogin, { isLoading }] = useCreateLoginMutation({
|
||||
fixedCacheKey: "shared-update-post",
|
||||
});
|
||||
|
||||
const onFinish = async (values: any) => {
|
||||
createLogin({ password: values.password, username: values.username })
|
||||
.unwrap()
|
||||
.then((response: any) => {
|
||||
dispatch(loginSuccess(response));
|
||||
message.open({
|
||||
type: "success",
|
||||
content: t("msg-login-success"),
|
||||
});
|
||||
navigate(PATHS.menu);
|
||||
})
|
||||
.catch((response: ResponseType) => {
|
||||
message.open({
|
||||
type: "error",
|
||||
content: response.data.message,
|
||||
});
|
||||
});
|
||||
};
|
||||
const { locale } = useAppSelector((state) => state.locale);
|
||||
|
||||
return (
|
||||
<Row style={{ minHeight: "100vh", overflow: "hidden" }}>
|
||||
{!xs && (
|
||||
<>
|
||||
<Col xs={24} lg={13}>
|
||||
<Flex
|
||||
vertical
|
||||
align="center"
|
||||
justify="center"
|
||||
className="text-center"
|
||||
style={{
|
||||
backgroundImage: "linear-gradient(#01304A, #005488)",
|
||||
height: "100%",
|
||||
padding: "1rem",
|
||||
zIndex: 4,
|
||||
}}
|
||||
>
|
||||
{locale === "en" ? (
|
||||
<NextIcon className="ltr-signin-ellipse" />
|
||||
) : (
|
||||
<BackIcon className="rtl-signin-ellipse" />
|
||||
)}
|
||||
</Flex>
|
||||
</Col>
|
||||
</>
|
||||
)}
|
||||
<Col xs={24} lg={11}>
|
||||
<Flex
|
||||
vertical
|
||||
align={xs ? "center" : "center"}
|
||||
justify="center"
|
||||
gap="middle"
|
||||
style={{ height: xs ? "90%" : "100%", padding: "2rem" }}
|
||||
>
|
||||
{!xs && (
|
||||
<LogoutOutlined
|
||||
color="white"
|
||||
style={{ position: "absolute", top: 20, left: 20 }}
|
||||
/>
|
||||
)}
|
||||
{xs && <LogoutOutlined color="white" />}{" "}
|
||||
<Title className="m-0">{t("login")}</Title>
|
||||
<Flex gap={4}>
|
||||
{/* // أهلا بعودتك ! من فضلك قم بتسجيل الدخول لحسابك */}
|
||||
<Text>{t("msg-welcome")}</Text>
|
||||
</Flex>
|
||||
<Form
|
||||
name="sign-up-form"
|
||||
layout="vertical"
|
||||
labelCol={{ span: 24 }}
|
||||
wrapperCol={{ span: 24 }}
|
||||
initialValues={{
|
||||
username: "admin",
|
||||
password: "123",
|
||||
remember: true,
|
||||
}}
|
||||
onFinish={onFinish}
|
||||
autoComplete="off"
|
||||
requiredMark={false}
|
||||
>
|
||||
<Row gutter={[8, 0]}>
|
||||
<Col xs={24}>
|
||||
<Form.Item<FieldType>
|
||||
label={t("user name")}
|
||||
name="username"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: t("msg-add-var-err", { var: t("user name") }),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col xs={24}>
|
||||
<Form.Item<FieldType>
|
||||
label={t("password")}
|
||||
name="password"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: t("msg-add-var-err", { var: t("password") }),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input.Password />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col xs={24}>
|
||||
<Form.Item<FieldType> name="remember" valuePropName="checked">
|
||||
<Checkbox>{t("remember me")}</Checkbox>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
<Form.Item>
|
||||
<Flex align="center" justify="space-between">
|
||||
<Button
|
||||
type="primary"
|
||||
htmlType="submit"
|
||||
size="middle"
|
||||
loading={isLoading}
|
||||
>
|
||||
{t("login")}
|
||||
</Button>
|
||||
<Link href={PATH_AUTH.passwordReset}>
|
||||
{t("forget password")}?
|
||||
</Link>
|
||||
</Flex>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
<Title
|
||||
style={{
|
||||
position: "absolute",
|
||||
bottom: 20,
|
||||
left: 20,
|
||||
color: "#8A8A8C",
|
||||
fontSize: 20,
|
||||
}}
|
||||
>
|
||||
{t("app version")} : 2.0.0
|
||||
</Title>
|
||||
</Flex>
|
||||
</Col>
|
||||
</Row>
|
||||
);
|
||||
};
|
||||
24
src/pages/authentication/Welcome.tsx
Normal file
24
src/pages/authentication/Welcome.tsx
Normal file
@@ -0,0 +1,24 @@
|
||||
import { Flex, Typography } from 'antd';
|
||||
|
||||
export const WelcomePage = () => {
|
||||
return (
|
||||
<Flex
|
||||
vertical
|
||||
gap="large"
|
||||
align="center"
|
||||
justify="center"
|
||||
style={{ height: '80vh' }}
|
||||
>
|
||||
<Typography.Title className="m-0">Welcome to Antd</Typography.Title>
|
||||
<Typography.Text style={{ fontSize: 18 }}>
|
||||
A dynamic and versatile multipurpose dashboard utilizing Ant Design,
|
||||
React, TypeScript, and Vite.
|
||||
</Typography.Text>
|
||||
{/* <Link to={PATH_DASHBOARD.default}>
|
||||
<Button type="primary" size="middle">
|
||||
Go to Homepage
|
||||
</Button>
|
||||
</Link> */}
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
3
src/pages/authentication/index.ts
Normal file
3
src/pages/authentication/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export { SignInPage } from "./SignIn.tsx";
|
||||
export { WelcomePage } from "./Welcome.tsx";
|
||||
|
||||
9
src/pages/authentication/styles.css
Normal file
9
src/pages/authentication/styles.css
Normal file
@@ -0,0 +1,9 @@
|
||||
.ltr-signin-ellipse {
|
||||
position: absolute;
|
||||
left: 0px;
|
||||
}
|
||||
|
||||
.rtl-signin-ellipse {
|
||||
position: absolute;
|
||||
right: 0px;
|
||||
}
|
||||
703
src/pages/cart/cart.module.css
Normal file
703
src/pages/cart/cart.module.css
Normal file
@@ -0,0 +1,703 @@
|
||||
.cartContainer {
|
||||
padding: 15px;
|
||||
transition: all 0.3s ease;
|
||||
height: 92vh;
|
||||
overflow: auto;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
.youMightAlsoLikeContainer {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
overflow: auto;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
:global(.darkApp) .youMightAlsoLikeContainer path {
|
||||
fill: var(--primary);
|
||||
}
|
||||
|
||||
/* Prevent keyboard from appearing automatically on mobile */
|
||||
@media (max-width: 768px) {
|
||||
.cartContainer input,
|
||||
.cartContainer textarea,
|
||||
.cartContainer select {
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
-webkit-touch-callout: none;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
font-size: 16px; /* Prevents zoom on iOS */
|
||||
}
|
||||
|
||||
.cartContainer input:focus,
|
||||
.cartContainer textarea:focus,
|
||||
.cartContainer select:focus {
|
||||
outline: none;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
font-size: 16px; /* Prevents zoom on iOS */
|
||||
}
|
||||
|
||||
/* Additional mobile-specific rules */
|
||||
.cartContainer input[type="text"],
|
||||
.cartContainer input[type="email"],
|
||||
.cartContainer input[type="tel"],
|
||||
.cartContainer input[type="number"],
|
||||
.cartContainer textarea {
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: none;
|
||||
appearance: none;
|
||||
border-radius: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* Enhanced responsive cart container */
|
||||
@media (min-width: 769px) and (max-width: 1024px) {
|
||||
.cartContainer {
|
||||
padding: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1025px) {
|
||||
.cartContainer {
|
||||
padding: 32px;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
}
|
||||
|
||||
.cartContent {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.cartItems {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
/* Enhanced responsive cart items */
|
||||
@media (min-width: 769px) and (max-width: 1024px) {
|
||||
.cartItems {
|
||||
gap: 20px;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1025px) {
|
||||
.cartItems {
|
||||
gap: 24px;
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
}
|
||||
|
||||
.container {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
overflow-x: auto;
|
||||
scrollbar-width: none;
|
||||
-ms-overflow-style: none;
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
.container::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Enhanced responsive container */
|
||||
@media (min-width: 769px) and (max-width: 1024px) {
|
||||
.container {
|
||||
gap: 20px;
|
||||
padding: 12px 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1025px) {
|
||||
.container {
|
||||
gap: 24px;
|
||||
padding: 16px 0;
|
||||
}
|
||||
}
|
||||
|
||||
.menuItemImage {
|
||||
object-fit: cover;
|
||||
border-radius: 8px;
|
||||
transition: transform 0.3s ease;
|
||||
width: 90px;
|
||||
height: 80px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.menuItemImage:hover {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
/* Enhanced responsive menu item images */
|
||||
@media (min-width: 769px) and (max-width: 1024px) {
|
||||
.menuItemImage {
|
||||
border-radius: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1025px) {
|
||||
.menuItemImage {
|
||||
border-radius: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.popularMenuItemImage {
|
||||
width: 73px;
|
||||
height: 73px;
|
||||
object-fit: cover;
|
||||
border-radius: 8px;
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.popularMenuItemImage:hover {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.popularMenuItemImageMobile {
|
||||
width: 73px;
|
||||
height: 73px;
|
||||
min-height: 73px;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.popularMenuItemImageTablet {
|
||||
width: 90px;
|
||||
height: 90px;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.popularMenuItemImageDesktop {
|
||||
width: 110px;
|
||||
height: 110px;
|
||||
border-radius: 16px;
|
||||
}
|
||||
|
||||
.itemDescription {
|
||||
font-size: 12px !important;
|
||||
transition: color 0.3s ease;
|
||||
}
|
||||
|
||||
.tableNumberCard :global(.ant-select-selector) {
|
||||
border-radius: 888px !important;
|
||||
}
|
||||
|
||||
.tableNumberCard :global(.ant-select-selection-overflow) {
|
||||
top: 5px !important;
|
||||
}
|
||||
|
||||
/* Enhanced responsive item description */
|
||||
@media (min-width: 769px) and (max-width: 1024px) {
|
||||
.itemDescription {
|
||||
font-size: 14px !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1025px) {
|
||||
.itemDescription {
|
||||
font-size: 16px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.couponApplyIcon {
|
||||
margin-left: 8px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* Enhanced responsive coupon apply icon */
|
||||
@media (min-width: 769px) and (max-width: 1024px) {
|
||||
.couponApplyIcon {
|
||||
margin-left: 10px;
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1025px) {
|
||||
.couponApplyIcon {
|
||||
margin-left: 12px;
|
||||
font-size: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
.donateHandIcon {
|
||||
color: #ffb700;
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
/* Enhanced responsive donate hand icon */
|
||||
@media (min-width: 769px) and (max-width: 1024px) {
|
||||
.donateHandIcon {
|
||||
font-size: 28px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1025px) {
|
||||
.donateHandIcon {
|
||||
font-size: 32px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Enhanced responsive cart sidebar for desktop */
|
||||
|
||||
/* Desktop Cart Layout Styles */
|
||||
.desktopCartContainer {
|
||||
max-width: 100vw;
|
||||
margin: 0 auto;
|
||||
padding: 40px 24px;
|
||||
background: linear-gradient(135deg, #f8f9fa 0%, #ffffff 100%);
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.desktopMainContent {
|
||||
background: #ffffff;
|
||||
border-radius: 24px;
|
||||
padding: 32px;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.08);
|
||||
border: 1px solid rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
.desktopSectionHeader {
|
||||
margin-bottom: 32px;
|
||||
padding-bottom: 16px;
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
.desktopEmptyCart {
|
||||
text-align: center;
|
||||
padding: 80px 40px;
|
||||
background: linear-gradient(135deg, #f8f9fa 0%, #ffffff 100%);
|
||||
border-radius: 20px;
|
||||
border: 2px dashed rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.desktopEmptyCartIcon {
|
||||
margin-bottom: 24px;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.desktopEmptyCartIcon svg {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
color: rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.desktopCartItemsSection {
|
||||
margin-bottom: 48px;
|
||||
}
|
||||
|
||||
.desktopCartItems {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.desktopCartItem {
|
||||
border: 1px solid rgba(0, 0, 0, 0.08);
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.desktopImageContainer {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
overflow: hidden;
|
||||
border-radius: 16px;
|
||||
}
|
||||
|
||||
.desktopMenuItemImage {
|
||||
object-fit: cover;
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.desktopCartItem:hover .desktopMenuItemImage {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.desktopItemDetails {
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
.desktopItemDescription {
|
||||
line-height: 1.6;
|
||||
color: rgba(0, 0, 0, 0.65);
|
||||
}
|
||||
|
||||
.desktopPriceContainer {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.desktopPrice {
|
||||
font-size: 18px;
|
||||
color: #1890ff;
|
||||
}
|
||||
|
||||
.desktopActionsContainer {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.desktopRecommendationsSection {
|
||||
margin-top: 48px;
|
||||
padding-top: 48px;
|
||||
border-top: 1px solid rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.desktopRecommendationsGrid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 24px;
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
.desktopRecommendationCard {
|
||||
border: 1px solid rgba(0, 0, 0, 0.06);
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.desktopRecommendationCard:hover {
|
||||
transform: translateY(-6px);
|
||||
box-shadow: 0 16px 48px rgba(0, 0, 0, 0.15);
|
||||
border-color: rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
|
||||
.desktopRecommendationContent {
|
||||
position: relative;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.desktopQuickAddButton {
|
||||
position: absolute;
|
||||
top: 12px;
|
||||
right: 12px;
|
||||
z-index: 2;
|
||||
opacity: 0;
|
||||
transform: scale(0.8);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.desktopRecommendationCard:hover .desktopQuickAddButton {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
|
||||
.desktopQuickAddIcon {
|
||||
background: linear-gradient(135deg, #1890ff 0%, #40a9ff 100%);
|
||||
border: none;
|
||||
box-shadow: 0 4px 16px rgba(24, 144, 255, 0.3);
|
||||
}
|
||||
|
||||
.desktopQuickAddIcon:hover {
|
||||
transform: scale(1.1);
|
||||
box-shadow: 0 6px 20px rgba(24, 144, 255, 0.4);
|
||||
}
|
||||
|
||||
.desktopRecommendationImage {
|
||||
border-radius: 16px;
|
||||
margin-bottom: 16px;
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.desktopRecommendationCard:hover .desktopRecommendationImage {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.desktopRecommendationInfo {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.desktopRecommendationName {
|
||||
font-size: 16px;
|
||||
margin-bottom: 8px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.desktopRecommendationPrice {
|
||||
font-size: 14px;
|
||||
color: #1890ff;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* Desktop Sidebar Styles */
|
||||
.desktopSidebar {
|
||||
position: sticky;
|
||||
top: 24px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.desktopSidebarCard {
|
||||
border: 1px solid rgba(0, 0, 0, 0.06);
|
||||
border-radius: 20px;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.06);
|
||||
transition: all 0.3s ease;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.desktopSidebarCard:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 28px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.desktopTipCard {
|
||||
background: linear-gradient(135deg, #fff7e6 0%, #fff2d9 100%);
|
||||
border: 1px solid rgba(255, 183, 0, 0.2);
|
||||
}
|
||||
|
||||
.desktopTipHeader {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.desktopTipIcon {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgba(255, 183, 0, 0.1);
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.desktopTipButtons {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.desktopTipButton {
|
||||
height: 44px;
|
||||
font-weight: 500;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.desktopTipButton:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.desktopCheckoutButton {
|
||||
background: linear-gradient(135deg, #1890ff 0%, #40a9ff 100%);
|
||||
border: none;
|
||||
box-shadow: 0 6px 24px rgba(24, 144, 255, 0.3);
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.desktopCheckoutButton:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 32px rgba(24, 144, 255, 0.4);
|
||||
background: linear-gradient(135deg, #40a9ff 0%, #69c0ff 100%);
|
||||
}
|
||||
|
||||
.desktopCheckoutButton:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.cartItems :global(.ant-card-hoverable:hover) {
|
||||
box-shadow: 0 0 0 0 !important;
|
||||
}
|
||||
|
||||
|
||||
[data-theme="dark"] .menuItemImage,
|
||||
[data-theme="dark"] .popularMenuItemImage {
|
||||
border-color: rgba(255, 255, 255, 0.1);
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .itemDescription {
|
||||
color: rgba(255, 255, 255, 0.75) !important;
|
||||
}
|
||||
|
||||
/* Enhanced responsive animations */
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
.cartItems {
|
||||
animation: fadeInUp 0.6s ease-out;
|
||||
}
|
||||
|
||||
.container {
|
||||
animation: fadeInUp 1s ease-out;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadeInUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* Enhanced responsive focus states */
|
||||
.cartContainer:focus,
|
||||
.menuItemImage:focus,
|
||||
.popularMenuItemImage:focus {
|
||||
outline: 2px solid var(--primary);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.cartContainer:focus,
|
||||
.menuItemImage:focus,
|
||||
.popularMenuItemImage:focus {
|
||||
outline-offset: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Enhanced responsive print styles */
|
||||
@media print {
|
||||
.cartContainer {
|
||||
background-color: white !important;
|
||||
color: black !important;
|
||||
}
|
||||
|
||||
.container {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Enhanced responsive hover effects */
|
||||
@media (hover: hover) {
|
||||
.menuItemImage:hover,
|
||||
.popularMenuItemImage:hover {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .orderSummary:hover {
|
||||
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
}
|
||||
|
||||
/* Enhanced responsive grid for you might also like section */
|
||||
@media (min-width: 768px) {
|
||||
.responsive-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
|
||||
gap: 16px;
|
||||
overflow-x: visible;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
.responsive-grid {
|
||||
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
|
||||
gap: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1280px) {
|
||||
.responsive-grid {
|
||||
grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
|
||||
gap: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Enhanced responsive animations */
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
.desktopCartItem {
|
||||
animation: fadeInUp 0.6s ease-out;
|
||||
}
|
||||
|
||||
.desktopRecommendationCard {
|
||||
animation: fadeInUp 0.6s ease-out;
|
||||
animation-delay: calc(var(--animation-order, 0) * 0.1s);
|
||||
}
|
||||
|
||||
.desktopSidebarCard {
|
||||
animation: fadeInRight 0.6s ease-out;
|
||||
animation-delay: calc(var(--animation-order, 0) * 0.1s);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadeInRight {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateX(30px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* Enhanced focus states for accessibility */
|
||||
.desktopCartItem:focus-within,
|
||||
.desktopRecommendationCard:focus-within,
|
||||
.desktopSidebarCard:focus-within {
|
||||
outline: 2px solid #1890ff;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.desktopCheckoutButton:focus {
|
||||
outline: 2px solid #ffffff;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
/* Print styles for desktop */
|
||||
@media print {
|
||||
.desktopCartContainer {
|
||||
background: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.desktopMainContent,
|
||||
.desktopSidebarCard {
|
||||
box-shadow: none;
|
||||
border: 1px solid #000;
|
||||
}
|
||||
}
|
||||
|
||||
/* Responsive adjustments for large desktop */
|
||||
@media (min-width: 1440px) {
|
||||
.desktopCartContainer {
|
||||
max-width: 100vw;
|
||||
padding: 48px 32px;
|
||||
}
|
||||
|
||||
.desktopMainContent {
|
||||
padding: 40px;
|
||||
}
|
||||
|
||||
.desktopRecommendationsGrid {
|
||||
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
||||
gap: 32px;
|
||||
}
|
||||
|
||||
.desktopCartItems {
|
||||
gap: 32px;
|
||||
}
|
||||
|
||||
.desktopSidebar {
|
||||
gap: 32px;
|
||||
}
|
||||
|
||||
.desktopEmptyCart {
|
||||
padding: 100px 60px;
|
||||
}
|
||||
|
||||
.desktopEmptyCartIcon svg {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
}
|
||||
}
|
||||
|
||||
.itemDescriptionIcons svg {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
170
src/pages/cart/components/YouMightAlsoLike.tsx
Normal file
170
src/pages/cart/components/YouMightAlsoLike.tsx
Normal file
@@ -0,0 +1,170 @@
|
||||
import { PlusOutlined } from "@ant-design/icons";
|
||||
import { Grid, Space } from "antd";
|
||||
import ArabicPrice from "components/ArabicPrice";
|
||||
import ImageWithFallback from "components/ImageWithFallback";
|
||||
import { ItemDescriptionIcons } from "components/ItemDescriptionIcons/ItemDescriptionIcons";
|
||||
import ProText from "components/ProText";
|
||||
import { menuItems } from "data/menuItems";
|
||||
import { addItem } from "features/order/orderSlice";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useAppDispatch, useAppSelector } from "redux/hooks";
|
||||
import { colors } from "ThemeConstants";
|
||||
import { default_image } from "utils/constants";
|
||||
import { Product } from "utils/types/appTypes";
|
||||
import styles from "../cart.module.css";
|
||||
|
||||
const { useBreakpoint } = Grid;
|
||||
|
||||
export default function YouMightAlsoLike() {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useAppDispatch();
|
||||
const { isRTL } = useAppSelector((state) => state.locale);
|
||||
const { sm, md } = useBreakpoint();
|
||||
const isMobile = !sm;
|
||||
const isTablet = sm && !md;
|
||||
const handleQuickAdd = (item: Product) => {
|
||||
dispatch(
|
||||
addItem({
|
||||
item: {
|
||||
id: Number(item.id),
|
||||
name: item.name,
|
||||
price: item.price,
|
||||
image: item.image,
|
||||
description: item.description,
|
||||
variant: "None",
|
||||
},
|
||||
quantity: 1,
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<ProText
|
||||
strong
|
||||
style={{
|
||||
fontSize: isMobile ? 18 : isTablet ? 18 : 20,
|
||||
}}
|
||||
>
|
||||
{t("cart.youMightAlsoLike")}
|
||||
</ProText>
|
||||
</div>
|
||||
|
||||
<div className={`${styles.youMightAlsoLikeContainer} responsive-grid`}>
|
||||
{menuItems.map((item: Product) => (
|
||||
<div key={item.id}>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
width: isMobile ? "95px" : isTablet ? "120px" : "140px",
|
||||
position: "relative",
|
||||
height: 155,
|
||||
overflow: "hidden",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
width: isMobile ? 18 : 24,
|
||||
height: isMobile ? 18 : 24,
|
||||
borderRadius: "50%",
|
||||
top: isMobile ? 50 : 80,
|
||||
position: "absolute",
|
||||
right: isMobile ? 15 : 20,
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
justifyContent: "center",
|
||||
cursor: "pointer",
|
||||
backgroundColor: "white",
|
||||
zIndex: 999,
|
||||
}}
|
||||
>
|
||||
<PlusOutlined
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleQuickAdd(item);
|
||||
}}
|
||||
style={{
|
||||
color: colors.primary,
|
||||
fontSize: isMobile ? 14 : 16,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<ImageWithFallback
|
||||
src={item.image}
|
||||
alt={item.name}
|
||||
className={`${styles.popularMenuItemImage} ${
|
||||
isMobile
|
||||
? styles.popularMenuItemImageMobile
|
||||
: isTablet
|
||||
? styles.popularMenuItemImageTablet
|
||||
: styles.popularMenuItemImageDesktop
|
||||
}`}
|
||||
width={isMobile ? 73 : isTablet ? 90 : 110}
|
||||
height={isMobile ? 73 : isTablet ? 90 : 110}
|
||||
fallbackSrc={default_image}
|
||||
/>
|
||||
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
justifyContent: "start",
|
||||
marginTop: 5,
|
||||
...(isRTL ? { marginRight: -20 } : { marginLeft: -10 }),
|
||||
}}
|
||||
>
|
||||
<ItemDescriptionIcons className={styles.itemDescriptionIcons} />
|
||||
</div>
|
||||
|
||||
<Space
|
||||
direction="vertical"
|
||||
size="small"
|
||||
style={{
|
||||
flex: 1,
|
||||
rowGap: 0,
|
||||
height: 40,
|
||||
}}
|
||||
>
|
||||
<div style={{ height: 25 }}>
|
||||
<ProText
|
||||
style={{
|
||||
whiteSpace: "nowrap",
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
padding: 3,
|
||||
fontSize: isMobile ? 12 : isTablet ? 14 : 16,
|
||||
width: isMobile ? 80 : isTablet ? 100 : 120,
|
||||
display: "inline-block",
|
||||
fontWeight: 600,
|
||||
}}
|
||||
>
|
||||
{item.name}
|
||||
</ProText>
|
||||
</div>
|
||||
|
||||
<ArabicPrice
|
||||
price={item.price}
|
||||
style={{
|
||||
margin: 0,
|
||||
WebkitLineClamp: 1,
|
||||
WebkitBoxOrient: "vertical",
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
padding: 3,
|
||||
fontSize: isMobile ? 12 : isTablet ? 14 : 16,
|
||||
fontWeight: 500,
|
||||
}}
|
||||
/>
|
||||
</Space>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
1113
src/pages/cart/page.tsx
Normal file
1113
src/pages/cart/page.tsx
Normal file
File diff suppressed because it is too large
Load Diff
0
src/pages/checkout/checkout.module.css
Normal file
0
src/pages/checkout/checkout.module.css
Normal file
98
src/pages/checkout/components/AddressSummary.tsx
Normal file
98
src/pages/checkout/components/AddressSummary.tsx
Normal 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}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
57
src/pages/checkout/components/BriefMenu.tsx
Normal file
57
src/pages/checkout/components/BriefMenu.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
49
src/pages/checkout/components/CheckoutButton.tsx
Normal file
49
src/pages/checkout/components/CheckoutButton.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
48
src/pages/checkout/components/GiftDetails.module.css
Normal file
48
src/pages/checkout/components/GiftDetails.module.css
Normal 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;
|
||||
}
|
||||
}
|
||||
146
src/pages/checkout/components/GiftDetails.tsx
Normal file
146
src/pages/checkout/components/GiftDetails.tsx
Normal 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}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
);
|
||||
};
|
||||
117
src/pages/checkout/components/OfficeDetails.tsx
Normal file
117
src/pages/checkout/components/OfficeDetails.tsx
Normal 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}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
);
|
||||
};
|
||||
116
src/pages/checkout/components/RoomDetails.tsx
Normal file
116
src/pages/checkout/components/RoomDetails.tsx
Normal 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}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
);
|
||||
};
|
||||
74
src/pages/checkout/hooks/useOrder.ts
Normal file
74
src/pages/checkout/hooks/useOrder.ts
Normal 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 };
|
||||
}
|
||||
32
src/pages/checkout/page.tsx
Normal file
32
src/pages/checkout/page.tsx
Normal 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 />
|
||||
</>
|
||||
);
|
||||
}
|
||||
36
src/pages/errors/Error.tsx
Normal file
36
src/pages/errors/Error.tsx
Normal file
@@ -0,0 +1,36 @@
|
||||
import { ReloadOutlined } from "@ant-design/icons";
|
||||
import { Result, Typography } from "antd";
|
||||
import BackIcon from "components/Icons/BackIcon";
|
||||
import { useRouteError } from "react-router-dom";
|
||||
|
||||
const { Paragraph, Text } = Typography;
|
||||
|
||||
type Error = unknown | any;
|
||||
|
||||
export const ErrorPage = () => {
|
||||
const error: Error = useRouteError();
|
||||
console.error(error);
|
||||
|
||||
return (
|
||||
<Result
|
||||
status="error"
|
||||
title="Oops!"
|
||||
subTitle="Sorry, an unexpected error has occurred."
|
||||
extra={[<BackIcon />, <ReloadOutlined />]}
|
||||
>
|
||||
<div className="desc">
|
||||
<Paragraph>
|
||||
<Text
|
||||
strong
|
||||
style={{
|
||||
fontSize: 16,
|
||||
}}
|
||||
>
|
||||
The page you tried to open has the following error:
|
||||
</Text>
|
||||
</Paragraph>
|
||||
<Paragraph copyable>{error.statusText || error.message}</Paragraph>
|
||||
</div>
|
||||
</Result>
|
||||
);
|
||||
};
|
||||
39
src/pages/errors/Error400.tsx
Normal file
39
src/pages/errors/Error400.tsx
Normal file
@@ -0,0 +1,39 @@
|
||||
import { red } from '@ant-design/colors';
|
||||
import { CloseCircleOutlined, ReloadOutlined } from '@ant-design/icons';
|
||||
import { Result, Typography } from 'antd';
|
||||
import BackIcon from 'components/Icons/BackIcon';
|
||||
|
||||
const { Paragraph, Text } = Typography;
|
||||
|
||||
export const Error400Page = () => {
|
||||
return (
|
||||
<Result
|
||||
status="error"
|
||||
title="400"
|
||||
subTitle="Bad request. The request could not be understood by the server due to malformed syntax. The client should not repeat the request without modifications"
|
||||
extra={[<BackIcon />, <ReloadOutlined />]}
|
||||
>
|
||||
<div className="desc">
|
||||
<Paragraph>
|
||||
<Text
|
||||
strong
|
||||
style={{
|
||||
fontSize: 16,
|
||||
}}
|
||||
>
|
||||
The content you submitted has the following error:
|
||||
</Text>
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
<CloseCircleOutlined style={{ color: red[5] }} />
|
||||
Bad Request - Invalid URL <a>Forward error ></a>
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
<CloseCircleOutlined style={{ color: red[5] }} />
|
||||
Bad Request. Your browser sent a request that this server could
|
||||
not understand <a>Go to console ></a>
|
||||
</Paragraph>
|
||||
</div>
|
||||
</Result>
|
||||
);
|
||||
};
|
||||
13
src/pages/errors/Error403.tsx
Normal file
13
src/pages/errors/Error403.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
import { Result } from 'antd';
|
||||
import BackIcon from 'components/Icons/BackIcon';
|
||||
|
||||
export const Error403Page = () => {
|
||||
return (
|
||||
<Result
|
||||
status="403"
|
||||
title="403"
|
||||
subTitle="Sorry, you are not authorized to access this page."
|
||||
extra={<BackIcon />}
|
||||
/>
|
||||
);
|
||||
};
|
||||
14
src/pages/errors/Error404.tsx
Normal file
14
src/pages/errors/Error404.tsx
Normal file
@@ -0,0 +1,14 @@
|
||||
import { Result } from 'antd';
|
||||
import BackIcon from 'components/Icons/BackIcon';
|
||||
|
||||
export const Error404Page = () => {
|
||||
return (
|
||||
<Result
|
||||
status="404"
|
||||
title="404"
|
||||
subTitle="Sorry, the page you visited does not exist."
|
||||
extra={<BackIcon />}
|
||||
|
||||
/>
|
||||
);
|
||||
};
|
||||
14
src/pages/errors/Error500.tsx
Normal file
14
src/pages/errors/Error500.tsx
Normal file
@@ -0,0 +1,14 @@
|
||||
import { ReloadOutlined } from '@ant-design/icons';
|
||||
import { Result } from 'antd';
|
||||
import BackIcon from 'components/Icons/BackIcon';
|
||||
|
||||
export const Error500Page = () => {
|
||||
return (
|
||||
<Result
|
||||
status="500"
|
||||
title="500"
|
||||
subTitle="Sorry, something went wrong."
|
||||
extra={[<BackIcon />, <ReloadOutlined />]}
|
||||
/>
|
||||
);
|
||||
};
|
||||
14
src/pages/errors/Error503.tsx
Normal file
14
src/pages/errors/Error503.tsx
Normal file
@@ -0,0 +1,14 @@
|
||||
import { ReloadOutlined } from '@ant-design/icons';
|
||||
import { Result } from 'antd';
|
||||
import BackIcon from 'components/Icons/BackIcon';
|
||||
|
||||
export const Error503Page = () => {
|
||||
return (
|
||||
<Result
|
||||
status="500"
|
||||
title="500"
|
||||
subTitle="Sorry, something went wrong."
|
||||
extra={[<BackIcon />, <ReloadOutlined />]}
|
||||
/>
|
||||
);
|
||||
};
|
||||
6
src/pages/errors/index.ts
Normal file
6
src/pages/errors/index.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export { ErrorPage } from './Error.tsx';
|
||||
export { Error400Page } from './Error400.tsx';
|
||||
export { Error403Page } from './Error403.tsx';
|
||||
export { Error404Page } from './Error404.tsx';
|
||||
export { Error500Page } from './Error500.tsx';
|
||||
export { Error503Page } from './Error503.tsx';
|
||||
8
src/pages/login/login.module.css
Normal file
8
src/pages/login/login.module.css
Normal file
@@ -0,0 +1,8 @@
|
||||
.loginPage {
|
||||
background-color: #FFF;
|
||||
}
|
||||
|
||||
:global(.darkApp) .loginPage {
|
||||
background-color: var(--background);
|
||||
}
|
||||
|
||||
207
src/pages/login/page.tsx
Normal file
207
src/pages/login/page.tsx
Normal file
@@ -0,0 +1,207 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
|
||||
import { Button, Form, Input, message } from "antd";
|
||||
import DatePickerBottomSheet from "components/CustomBottomSheet/DatePickerBottomSheet";
|
||||
import LoginManIcon from "components/Icons/LoginManIcon";
|
||||
import ProText from "components/ProText";
|
||||
import ProTitle from "components/ProTitle";
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import PhoneInput from "react-phone-input-2";
|
||||
import "react-phone-input-2/lib/style.css";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import { useSendOtpMutation } from "redux/api/auth";
|
||||
import { useAppSelector } from "redux/hooks";
|
||||
import { colors, DisabledColor, ProBlack1 } from "ThemeConstants";
|
||||
import styles from "./login.module.css";
|
||||
|
||||
export default function LoginPage() {
|
||||
const { t } = useTranslation();
|
||||
const { isRTL } = useAppSelector((state) => state.locale);
|
||||
const { themeName } = useAppSelector((state) => state.theme);
|
||||
const [form] = Form.useForm();
|
||||
const [sendOtp, { isLoading }] = useSendOtpMutation();
|
||||
const { id } = useParams();
|
||||
|
||||
const [phone, setPhone] = useState<string>("");
|
||||
const [selectedDate, setSelectedDate] = useState<string>("");
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const navigate = useNavigate();
|
||||
|
||||
const handleLogin = async () => {
|
||||
localStorage.setItem("userPhone", form.getFieldValue("phone"));
|
||||
if (form.getFieldsValue())
|
||||
sendOtp(form.getFieldsValue()).then((response: any) => {
|
||||
message.info(t("login.OTPSentToYourPhoneNumber"));
|
||||
navigate(`/${id}/otp`);
|
||||
localStorage.setItem("otp", response.data.result.otp);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
padding: 24,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
justifyContent: "center",
|
||||
height: "100vh",
|
||||
}}
|
||||
className={styles.loginPage}
|
||||
>
|
||||
<ProTitle
|
||||
level={5}
|
||||
style={{ textAlign: "center", margin: "20px 0 10% 0", fontSize: 18 }}
|
||||
>
|
||||
{t("login.singup/Login")}
|
||||
</ProTitle>
|
||||
|
||||
<div
|
||||
style={{
|
||||
position: "relative",
|
||||
marginBottom: 20,
|
||||
[isRTL ? "right" : "left"]: "50%",
|
||||
width: "fit-content",
|
||||
transform: isRTL ? "translateX(50%)" : "translateX(-50%)",
|
||||
}}
|
||||
>
|
||||
<LoginManIcon />
|
||||
</div>
|
||||
|
||||
<ProTitle level={5}>{t("login.EnterYourNumber")} 👋</ProTitle>
|
||||
<ProText style={{ fontSize: 12, color: "#3E3E3E" }}>
|
||||
{t("login.WeWillSendYouAWhatsAppMessageWithAOneTimeVerificationCode")}
|
||||
</ProText>
|
||||
|
||||
<Form
|
||||
layout="vertical"
|
||||
style={{ marginTop: 20 }}
|
||||
name="loginForm"
|
||||
form={form}
|
||||
>
|
||||
<Form.Item
|
||||
label={t("name")}
|
||||
name="name"
|
||||
rules={[{ required: true, message: "" }]}
|
||||
>
|
||||
<Input
|
||||
placeholder={t("login.EnterYourName")}
|
||||
size="large"
|
||||
style={{
|
||||
height: 50,
|
||||
fontSize: 14,
|
||||
}}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label={t("date")} name="date" required>
|
||||
<Input
|
||||
placeholder={t("login.DateOfBirth")}
|
||||
size="large"
|
||||
onClick={() => setIsOpen(true)}
|
||||
readOnly
|
||||
value={selectedDate}
|
||||
style={{
|
||||
cursor: "pointer",
|
||||
height: 50,
|
||||
fontSize: 14,
|
||||
}}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="phone"
|
||||
label={t("phone")}
|
||||
rules={[
|
||||
{ required: true, message: "" },
|
||||
{ type: "number", message: "" },
|
||||
]}
|
||||
>
|
||||
<div className={styles.proPhoneNumber}>
|
||||
<PhoneInput
|
||||
country={"om"}
|
||||
inputStyle={{
|
||||
borderRadius: 1000,
|
||||
height: 50,
|
||||
width: "100%",
|
||||
color: themeName === "light" ? "#000" : "#FFF",
|
||||
backgroundColor: themeName === "light" ? "#FFF" : ProBlack1,
|
||||
textAlign: isRTL ? "right" : "left",
|
||||
direction: isRTL ? "rtl" : "ltr",
|
||||
paddingLeft: "50px",
|
||||
paddingRight: "50px",
|
||||
borderColor: themeName === "light" ? "#d9d9d9" : "#363636",
|
||||
}}
|
||||
placeholder={t("login.mobileNumber")}
|
||||
value={phone}
|
||||
buttonStyle={{
|
||||
backgroundColor: "transparent",
|
||||
border: 0,
|
||||
borderLeft: "1px solid #363636",
|
||||
borderRadius: 0,
|
||||
position: "relative",
|
||||
...(isRTL && {
|
||||
top: -25,
|
||||
right: 25,
|
||||
}),
|
||||
...(!isRTL && {
|
||||
top: -25,
|
||||
}),
|
||||
}}
|
||||
onBlur={(e) => setPhone(e.target.value)}
|
||||
autocompleteSearch
|
||||
inputProps={{
|
||||
id: "phone-number", // Required for accessibility & autofill
|
||||
name: "phone-number",
|
||||
required: true,
|
||||
autoFocus: false,
|
||||
autoComplete: "tel",
|
||||
type: "tel",
|
||||
inputMode: "numeric",
|
||||
pattern: "[0-9]*",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label={null}>
|
||||
<Button
|
||||
onClick={handleLogin}
|
||||
type="primary"
|
||||
size="large"
|
||||
loading={isLoading}
|
||||
style={{
|
||||
width: "100%",
|
||||
borderRadius: 1000,
|
||||
backgroundColor:
|
||||
phone.length <= 3 || isLoading
|
||||
? themeName === "light"
|
||||
? "rgba(233, 233, 233, 1)"
|
||||
: DisabledColor
|
||||
: colors.primary,
|
||||
color: "#FFF",
|
||||
height: 50,
|
||||
border: "none",
|
||||
marginTop: "2rem",
|
||||
}}
|
||||
disabled={phone.length <= 3 || isLoading}
|
||||
htmlType="submit"
|
||||
>
|
||||
{t("login.sendCode")}
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
|
||||
<DatePickerBottomSheet
|
||||
isOpen={isOpen}
|
||||
onClose={() => setIsOpen(false)}
|
||||
onDateSelect={(date) => {
|
||||
const formattedDate = `${date.month}/${date.day}/${date.year}`;
|
||||
setSelectedDate(formattedDate);
|
||||
form.setFieldValue("date", formattedDate);
|
||||
}}
|
||||
initialDate={new Date()}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
78
src/pages/menu/components/AddToCart.tsx
Normal file
78
src/pages/menu/components/AddToCart.tsx
Normal file
@@ -0,0 +1,78 @@
|
||||
import { PlusOutlined } from "@ant-design/icons";
|
||||
import { Button, Grid } from "antd";
|
||||
import { addItem } from "features/order/orderSlice";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useAppDispatch, useAppSelector } from "redux/hooks";
|
||||
import { colors } from "ThemeConstants";
|
||||
import { Product } from "utils/types/appTypes";
|
||||
|
||||
const { useBreakpoint } = Grid;
|
||||
|
||||
export function AddToCart({ item }: { item: Product }) {
|
||||
const { isRTL } = useAppSelector((state) => state.locale);
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useAppDispatch();
|
||||
const { xs, sm } = useBreakpoint();
|
||||
|
||||
const handleQuickAdd = (item: Product) => {
|
||||
dispatch(
|
||||
addItem({
|
||||
item: {
|
||||
id: item.id,
|
||||
name: item.name,
|
||||
price: item.price,
|
||||
image: item.image,
|
||||
description: item.description,
|
||||
variant: "None",
|
||||
extras: [],
|
||||
extrasgroup: [],
|
||||
},
|
||||
quantity: 1,
|
||||
})
|
||||
);
|
||||
};
|
||||
return (
|
||||
<Button
|
||||
shape="round"
|
||||
title="add"
|
||||
iconPosition="start"
|
||||
disabled={item.isHasVarint}
|
||||
icon={
|
||||
<PlusOutlined
|
||||
title="add"
|
||||
style={{
|
||||
position: "relative",
|
||||
top: "-1px",
|
||||
}}
|
||||
/>
|
||||
}
|
||||
size={xs || sm ? "small" : "middle"}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
handleQuickAdd(item);
|
||||
}}
|
||||
style={{
|
||||
position: "absolute",
|
||||
bottom: -10,
|
||||
[isRTL ? "right" : "left"]: xs || sm ? "5%" : "15%",
|
||||
zIndex: 1,
|
||||
width: xs || sm ? 82 : 100,
|
||||
height: xs || sm ? 32 : 44,
|
||||
fontSize: xs || sm ? "1rem" : 18,
|
||||
fontWeight: 600,
|
||||
border: 0,
|
||||
backgroundColor: item.isHasVarint
|
||||
? "rgba(233, 233, 233, 1)"
|
||||
: colors.primary,
|
||||
color: "#FFF",
|
||||
// boxShadow:
|
||||
// theme === "light"
|
||||
// ? "0 2px 0 rgba(0,0,0,0.02)"
|
||||
// : "0 2px 0 #6b6b6b",
|
||||
}}
|
||||
>
|
||||
{t("common.add")}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
33
src/pages/menu/components/BackButton.tsx
Normal file
33
src/pages/menu/components/BackButton.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
import { Button } from "antd";
|
||||
import BackIcon from "components/Icons/BackIcon";
|
||||
|
||||
|
||||
interface BackButtonProps {
|
||||
navigateBack?: boolean; // true = use router.back(), false = just clear state
|
||||
}
|
||||
|
||||
export default function BackButton({ navigateBack = true }: BackButtonProps) {
|
||||
const handleBack = () => {
|
||||
if (navigateBack) window.history.back();
|
||||
};
|
||||
|
||||
return (
|
||||
<Button
|
||||
style={{
|
||||
width: 32,
|
||||
height: 32,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
padding: 0,
|
||||
borderRadius: "50%",
|
||||
}}
|
||||
icon={
|
||||
<div style={{ position: "relative", top: 2.5, right: 1 }}>
|
||||
<BackIcon />
|
||||
</div>
|
||||
}
|
||||
onClick={handleBack}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,634 @@
|
||||
/* MenuPage.module.css */
|
||||
|
||||
/* Product Link Transitions */
|
||||
/*
|
||||
.productLink {
|
||||
display: block;
|
||||
transition: all 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94);
|
||||
transform: translateY(0);
|
||||
opacity: 1;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
/* }
|
||||
|
||||
.productLink::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: -100%;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
transparent,
|
||||
var(--link-glow),
|
||||
transparent
|
||||
);
|
||||
transition: left 0.5s ease;
|
||||
z-index: 1;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.productLink:hover::before {
|
||||
left: 100%;
|
||||
}
|
||||
|
||||
.productLink:hover {
|
||||
transform: translateY(-8px);
|
||||
filter: brightness(1.05);
|
||||
}
|
||||
|
||||
.productLink:active {
|
||||
transform: translateY(-4px);
|
||||
transition: all 0.1s ease;
|
||||
} */
|
||||
|
||||
/* Enhanced card hover effects */
|
||||
/* .productLink:hover .ant-card {
|
||||
box-shadow: 0 12px 32px var(--link-shadow);
|
||||
transform: scale(1.02);
|
||||
border-color: var(--link-border);
|
||||
}
|
||||
|
||||
.productLink .ant-card {
|
||||
transition: all 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94);
|
||||
will-change: transform, box-shadow;
|
||||
border: 1px solid transparent;
|
||||
} */
|
||||
|
||||
/* Theme-aware glow effect */
|
||||
/* .productLink:hover .ant-card::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 0 20px var(--link-shadow-glow);
|
||||
opacity: 0;
|
||||
animation: glowPulse 2s ease-in-out infinite;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@keyframes glowPulse {
|
||||
0%, 100% { opacity: 0; }
|
||||
50% { opacity: 1; }
|
||||
} */
|
||||
|
||||
/* Dark theme adjustments for product links */
|
||||
/* @media (prefers-color-scheme: dark) {
|
||||
.productLink {
|
||||
--link-glow: rgba(255, 198, 0, 0.15);
|
||||
--link-glow-hover: rgba(255, 198, 0, 0.25);
|
||||
--link-border: rgba(255, 198, 0, 0.4);
|
||||
--link-shadow: rgba(255, 198, 0, 0.25);
|
||||
--link-shadow-glow: rgba(255, 198, 0, 0.4);
|
||||
}
|
||||
} */
|
||||
|
||||
.container {
|
||||
display: flex;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
scrollbar-width: none;
|
||||
-ms-overflow-style: none;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
background: var(--ant-bg-container);
|
||||
z-index: 10;
|
||||
border-bottom: 1px solid var(--ant-color-border);
|
||||
overflow: auto;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.categoriesContainer {
|
||||
display: flex;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
scrollbar-width: none;
|
||||
-ms-overflow-style: none;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
background: #fff;
|
||||
z-index: 10;
|
||||
overflow: hidden;
|
||||
text-align: center;
|
||||
height: 8.5rem;
|
||||
overflow-x: auto;
|
||||
transition: all 0.4s cubic-bezier(0.25, 0.46, 0.45, 0.94);
|
||||
will-change: position, transform, opacity, filter;
|
||||
transform-origin: top center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.5rem 1rem 1rem 1rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
/* Enhanced responsive categories container */
|
||||
@media (min-width: 769px) and (max-width: 1024px) {
|
||||
.categoriesContainer {
|
||||
height: 160px;
|
||||
padding: 16px;
|
||||
gap: 16px;
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.categoriesSticky {
|
||||
padding: 12px 16px !important;
|
||||
height: 70px !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1025px) {
|
||||
.categoriesContainer {
|
||||
height: 180px;
|
||||
padding: 24px;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
gap: 16px;
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.categoriesSticky {
|
||||
padding: 16px 24px !important;
|
||||
height: 70px !important;
|
||||
max-width: 100vw !important;
|
||||
margin: 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
.container::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.categoryMenuItemImage {
|
||||
width: 104px !important;
|
||||
min-width: 90px !important;
|
||||
height: 78px;
|
||||
object-fit: cover;
|
||||
border-radius: 0.375rem;
|
||||
transition: transform 0.3s ease;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Responsive adjustments */
|
||||
@media (max-width: 768px) {
|
||||
.container {
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.categoryTab {
|
||||
padding: 0px 6px;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 769px) and (max-width: 1024px) {
|
||||
.container {
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.categoryTab {
|
||||
padding: 0px 14px;
|
||||
font-size: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
:global(.darkApp) .categoryTab {
|
||||
color: rgba(255, 255, 255, 0.75) !important;
|
||||
background-color: rgba(42, 42, 42, 0.8) !important;
|
||||
border-color: rgba(255, 255, 255, 0.1) !important;
|
||||
backdrop-filter: blur(12px);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
:global(.darkApp) .categoryTab:hover {
|
||||
color: var(--primary) !important;
|
||||
background-color: rgba(54, 54, 54, 0.9) !important;
|
||||
border-color: rgba(255, 198, 0, 0.3) !important;
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
:global(.darkApp) .categoryTab.active {
|
||||
color: var(--primary) !important;
|
||||
background-color: rgba(255, 198, 0, 0.1) !important;
|
||||
border-bottom-color: var(--primary) !important;
|
||||
box-shadow: 0 2px 8px rgba(255, 198, 0, 0.2);
|
||||
}
|
||||
|
||||
/* Active category card styles */
|
||||
.activeCategoryCard {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.activeCategoryCard::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: -2px;
|
||||
left: -2px;
|
||||
right: -2px;
|
||||
bottom: -2px;
|
||||
background: linear-gradient(45deg, var(--ant-color-primary), transparent);
|
||||
border-radius: 8px;
|
||||
opacity: 0.1;
|
||||
z-index: -1;
|
||||
animation: activePulse 2s ease-in-out infinite;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.activeCategoryCard:hover {
|
||||
border-color: var(--ant-color-primary) !important;
|
||||
}
|
||||
|
||||
/* Dark theme active category card styles */
|
||||
:global(.darkApp) .activeCategoryCard {
|
||||
box-shadow: 0 4px 12px rgba(255, 198, 0, 0.3) !important;
|
||||
background-color: rgba(255, 198, 0, 0.05) !important;
|
||||
}
|
||||
|
||||
:global(.darkApp) .activeCategoryCard::before {
|
||||
background: linear-gradient(45deg, var(--primary), transparent);
|
||||
animation: darkActivePulse 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes darkActivePulse {
|
||||
0%,
|
||||
100% {
|
||||
opacity: 0.05;
|
||||
}
|
||||
50% {
|
||||
opacity: 0.15;
|
||||
}
|
||||
}
|
||||
|
||||
:global(.darkApp) .activeCategoryCard:hover {
|
||||
border-color: var(--primary) !important;
|
||||
box-shadow: 0 6px 16px rgba(255, 198, 0, 0.4) !important;
|
||||
background-color: rgba(255, 198, 0, 0.08) !important;
|
||||
}
|
||||
|
||||
/* Smooth transitions for category cards */
|
||||
.categoryCard {
|
||||
transition: all 0.3s ease;
|
||||
transform: scale(1);
|
||||
}
|
||||
|
||||
.categoryCard:hover {
|
||||
transform: scale(1.02);
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
/* Enhanced smooth scrolling for category container */
|
||||
.categoriesContainer {
|
||||
scroll-behavior: smooth;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
transition: all 0.4s cubic-bezier(0.25, 0.46, 0.45, 0.94);
|
||||
}
|
||||
|
||||
/* Staggered animation for category cards when becoming sticky */
|
||||
.categoriesSticky .ant-card {
|
||||
animation: cardEntrance 0.5s cubic-bezier(0.25, 0.46, 0.45, 0.94) both;
|
||||
animation-delay: calc(var(--card-index, 0) * 0.05s);
|
||||
}
|
||||
|
||||
/* Smooth animation for category cards when returning to normal */
|
||||
.categoriesContainer:not(.categoriesSticky) .ant-card {
|
||||
animation: cardReturn 0.4s cubic-bezier(0.25, 0.46, 0.45, 0.94) both;
|
||||
animation-delay: calc(var(--card-index, 0) * 0.03s);
|
||||
}
|
||||
|
||||
/* Smooth entrance for category images */
|
||||
.categoriesSticky .popularMenuItemImage {
|
||||
animation: imageEntrance 0.6s cubic-bezier(0.25, 0.46, 0.45, 0.94) both;
|
||||
animation-delay: calc(var(--card-index, 0) * 0.05s + 0.1s);
|
||||
}
|
||||
|
||||
/* Smooth return animation for category images */
|
||||
.categoriesContainer:not(.categoriesSticky) .popularMenuItemImage {
|
||||
animation: imageReturn 0.4s cubic-bezier(0.25, 0.46, 0.45, 0.94) both;
|
||||
animation-delay: calc(var(--card-index, 0) * 0.03s + 0.05s);
|
||||
}
|
||||
|
||||
@keyframes imageEntrance {
|
||||
0% {
|
||||
transform: scale(0.9) rotate(-2deg);
|
||||
opacity: 0.7;
|
||||
}
|
||||
100% {
|
||||
transform: scale(1) rotate(0deg);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes imageReturn {
|
||||
0% {
|
||||
transform: scale(0.95) rotate(-1deg);
|
||||
opacity: 0.85;
|
||||
}
|
||||
100% {
|
||||
transform: scale(1) rotate(0deg);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* Sticky categories when scrolled past */
|
||||
.categoriesSticky {
|
||||
position: fixed !important;
|
||||
top: 0 !important;
|
||||
left: 0 !important;
|
||||
right: 0 !important;
|
||||
width: 100vw !important;
|
||||
z-index: 1000 !important;
|
||||
border-bottom: 1px solid var(--ant-color-border) !important;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15) !important;
|
||||
background: #fff !important;
|
||||
animation: slideDown 0.4s cubic-bezier(0.25, 0.46, 0.45, 0.94) !important;
|
||||
transform-origin: top center !important;
|
||||
}
|
||||
|
||||
/* Subtle glow effect for sticky state */
|
||||
.categoriesSticky::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 2px;
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
transparent,
|
||||
rgba(24, 144, 255, 0.3),
|
||||
transparent
|
||||
);
|
||||
animation: glowPulse 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
/* Fade out glow effect when returning to normal */
|
||||
.categoriesContainer:not(.categoriesSticky)::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 2px;
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
transparent,
|
||||
rgba(24, 144, 255, 0.1),
|
||||
transparent
|
||||
);
|
||||
animation: glowFadeOut 0.4s cubic-bezier(0.25, 0.46, 0.45, 0.94) forwards;
|
||||
}
|
||||
|
||||
@keyframes glowPulse {
|
||||
0%,
|
||||
100% {
|
||||
opacity: 0.3;
|
||||
transform: scaleX(0.8);
|
||||
}
|
||||
50% {
|
||||
opacity: 0.6;
|
||||
transform: scaleX(1);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes glowFadeOut {
|
||||
0% {
|
||||
opacity: 0.1;
|
||||
transform: scaleX(1);
|
||||
}
|
||||
100% {
|
||||
opacity: 0;
|
||||
transform: scaleX(0.8);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slideDown {
|
||||
0% {
|
||||
transform: translateY(-100%) scale(0.95);
|
||||
opacity: 0;
|
||||
filter: blur(10px);
|
||||
}
|
||||
50% {
|
||||
transform: translateY(-20%) scale(0.98);
|
||||
opacity: 0.7;
|
||||
filter: blur(5px);
|
||||
}
|
||||
100% {
|
||||
transform: translateY(0) scale(1);
|
||||
opacity: 1;
|
||||
filter: blur(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* Smooth exit animation when returning to normal */
|
||||
.categoriesContainer:not(.categoriesSticky) {
|
||||
animation: slideUp 0.4s cubic-bezier(0.25, 0.46, 0.45, 0.94) !important;
|
||||
transform-origin: top center !important;
|
||||
}
|
||||
|
||||
@keyframes slideUp {
|
||||
0% {
|
||||
transform: translateY(-10px) scale(0.98);
|
||||
opacity: 0.9;
|
||||
filter: blur(2px);
|
||||
}
|
||||
50% {
|
||||
transform: translateY(-5px) scale(0.99);
|
||||
opacity: 0.95;
|
||||
filter: blur(1px);
|
||||
}
|
||||
100% {
|
||||
transform: translateY(0) scale(1);
|
||||
opacity: 1;
|
||||
filter: blur(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* Dark theme sticky categories */
|
||||
:global(.darkApp) .categoriesSticky {
|
||||
background: rgba(10, 10, 10, 0.95) !important;
|
||||
border-bottom-color: #363636 !important;
|
||||
box-shadow:
|
||||
0 4px 25px rgba(0, 0, 0, 0.4),
|
||||
0 0 0 1px rgba(255, 255, 255, 0.05) !important;
|
||||
backdrop-filter: blur(25px) saturate(1.2) !important;
|
||||
border-image: linear-gradient(
|
||||
90deg,
|
||||
transparent,
|
||||
rgba(255, 198, 0, 0.2),
|
||||
transparent
|
||||
)
|
||||
1 !important;
|
||||
}
|
||||
|
||||
/* Dark theme glow effect */
|
||||
:global(.darkApp) .categoriesSticky::before {
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
transparent,
|
||||
rgba(255, 198, 0, 0.4),
|
||||
transparent
|
||||
);
|
||||
animation: darkGlowPulse 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
/* Dark theme glow fade out when returning to normal */
|
||||
:global(.darkApp) .categoriesContainer:not(.categoriesSticky)::before {
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
transparent,
|
||||
rgba(255, 198, 0, 0.2),
|
||||
transparent
|
||||
);
|
||||
animation: darkGlowFadeOut 0.4s cubic-bezier(0.25, 0.46, 0.45, 0.94) forwards;
|
||||
}
|
||||
|
||||
/* Updating category indicator */
|
||||
.updatingCategory {
|
||||
animation: categoryUpdate 0.6s ease-in-out;
|
||||
}
|
||||
|
||||
@keyframes updatingCategory {
|
||||
0% {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
50% {
|
||||
transform: scale(1.1);
|
||||
box-shadow: 0 8px 32px rgba(24, 144, 255, 0.4);
|
||||
}
|
||||
100% {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
}
|
||||
|
||||
/* Dark theme updating category indicator */
|
||||
:global(.darkApp) .updatingCategory {
|
||||
animation: darkCategoryUpdate 0.6s ease-in-out;
|
||||
}
|
||||
|
||||
@keyframes darkCategoryUpdate {
|
||||
0% {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
50% {
|
||||
transform: scale(1.1);
|
||||
box-shadow: 0 8px 32px rgba(255, 198, 0, 0.4);
|
||||
}
|
||||
100% {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
}
|
||||
|
||||
/* Enhanced dark theme for container backgrounds */
|
||||
:global(.darkApp) .container,
|
||||
:global(.darkApp) .categoriesContainer {
|
||||
background-color: #0a0a0a !important;
|
||||
border-bottom-color: #363636 !important;
|
||||
}
|
||||
|
||||
:global(.darkApp) .itemDescriptionIcons path {
|
||||
fill: #ffffff;
|
||||
}
|
||||
|
||||
/* Enhanced dark theme animations */
|
||||
:global(.darkApp) .categoryTab {
|
||||
animation: fadeInUp 0.3s ease-out;
|
||||
}
|
||||
|
||||
@keyframes fadeInUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* Smooth transitions for all elements */
|
||||
.categoryTab * {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
/* Enhanced responsive category cards */
|
||||
@media (min-width: 769px) and (max-width: 1024px) {
|
||||
.categoryCard {
|
||||
border-radius: 12px !important;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1) !important;
|
||||
}
|
||||
|
||||
.categoryCard:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.15) !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1025px) {
|
||||
.categoryCard {
|
||||
border-radius: 16px !important;
|
||||
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.1) !important;
|
||||
}
|
||||
|
||||
.categoryCard:hover {
|
||||
transform: translateY(-3px);
|
||||
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.15) !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Enhanced responsive focus states */
|
||||
.menuItem:focus,
|
||||
.categoryCard:focus {
|
||||
outline: 2px solid var(--primary);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.menuItem:focus,
|
||||
.categoryCard:focus {
|
||||
outline-offset: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Enhanced responsive print styles */
|
||||
@media print {
|
||||
.menuItem,
|
||||
.categoryCard {
|
||||
box-shadow: none !important;
|
||||
border: 1px solid #ccc !important;
|
||||
}
|
||||
|
||||
.categoriesContainer {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Mobile-specific sticky categories */
|
||||
@media (max-width: 768px) {
|
||||
.categoriesSticky {
|
||||
padding: 16px !important;
|
||||
height: 70px !important;
|
||||
transition: all 0.3s ease !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Smooth placeholder transition */
|
||||
.categoriesSticky + div {
|
||||
transition: height 0.4s cubic-bezier(0.25, 0.46, 0.45, 0.94);
|
||||
}
|
||||
|
||||
/* Enhanced smooth transitions for all sticky-related elements */
|
||||
.categoriesContainer,
|
||||
.categoriesContainer *,
|
||||
.categoriesSticky,
|
||||
.categoriesSticky * {
|
||||
transition: all 0.4s cubic-bezier(0.25, 0.46, 0.45, 0.94);
|
||||
}
|
||||
232
src/pages/menu/components/CategoriesList/CategoriesList.tsx
Normal file
232
src/pages/menu/components/CategoriesList/CategoriesList.tsx
Normal file
@@ -0,0 +1,232 @@
|
||||
import { Card, Grid, Space } from "antd";
|
||||
import ImageWithFallback from "components/ImageWithFallback";
|
||||
import ProText from "components/ProText";
|
||||
import { useScrollHandler } from "contexts/ScrollHandlerContext";
|
||||
import { useCallback, useEffect } from "react";
|
||||
import { useAppSelector } from "redux/hooks";
|
||||
import { colors } from "ThemeConstants";
|
||||
import { default_image } from "utils/constants";
|
||||
import { Category } from "utils/types/appTypes";
|
||||
import styles from "./CategoriesList.module.css";
|
||||
|
||||
interface CategoriesListProps {
|
||||
categories: Category[];
|
||||
}
|
||||
|
||||
const { useBreakpoint } = Grid;
|
||||
|
||||
export function CategoriesList({ categories }: CategoriesListProps) {
|
||||
const { xs, md } = useBreakpoint();
|
||||
const {
|
||||
isCategoriesSticky,
|
||||
categoriesContainerRef,
|
||||
scrollToCategory,
|
||||
activeCategory,
|
||||
setActiveCategory,
|
||||
} = useScrollHandler();
|
||||
const { isRTL } = useAppSelector((state) => state.locale);
|
||||
|
||||
const getCategoryCardStyle = useCallback(() => {
|
||||
if (xs) {
|
||||
return {
|
||||
width: 90,
|
||||
height: isCategoriesSticky ? 38 : 110,
|
||||
};
|
||||
} else if (md) {
|
||||
return {
|
||||
width: 120,
|
||||
height: isCategoriesSticky ? 38 : 110,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
width: 140,
|
||||
height: isCategoriesSticky ? 38 : 160,
|
||||
};
|
||||
}
|
||||
}, [isCategoriesSticky, xs, md]);
|
||||
|
||||
// Set first category as active on initial load
|
||||
useEffect(() => {
|
||||
if (categories.length > 0 && !activeCategory) {
|
||||
setActiveCategory(categories[0].id);
|
||||
}
|
||||
}, [categories, activeCategory, setActiveCategory]);
|
||||
|
||||
// Function to scroll category card into view when active category changes automatically
|
||||
const scrollCategoryCardIntoView = useCallback((categoryId: number) => {
|
||||
const categoryCard = document.querySelector(
|
||||
`[data-category-id="${categoryId}"]`
|
||||
);
|
||||
if (categoryCard) {
|
||||
const categoriesContainer = categoryCard.closest(
|
||||
`.${styles.categoriesContainer}`
|
||||
);
|
||||
if (categoriesContainer) {
|
||||
const cardElement = categoryCard as HTMLElement;
|
||||
const containerElement = categoriesContainer as HTMLElement;
|
||||
const cardRect = cardElement.getBoundingClientRect();
|
||||
const containerRect = containerElement.getBoundingClientRect();
|
||||
|
||||
if (
|
||||
cardRect.left < containerRect.left ||
|
||||
cardRect.right > containerRect.right
|
||||
) {
|
||||
cardElement.scrollIntoView({
|
||||
behavior: "smooth",
|
||||
block: "nearest",
|
||||
inline: "center",
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Auto-scroll category card into view when active category changes
|
||||
useEffect(() => {
|
||||
if (activeCategory) {
|
||||
// Use a small delay to ensure the DOM has updated
|
||||
const timer = setTimeout(() => {
|
||||
scrollCategoryCardIntoView(activeCategory);
|
||||
}, 150);
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}
|
||||
}, [activeCategory, scrollCategoryCardIntoView]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
ref={categoriesContainerRef as React.RefObject<HTMLDivElement>}
|
||||
className={`${styles.categoriesContainer} ${
|
||||
isCategoriesSticky ? styles.categoriesSticky : ""
|
||||
}`}
|
||||
style={!isCategoriesSticky ? { paddingTop: "1rem" } : {}}
|
||||
>
|
||||
{categories?.map((category) => (
|
||||
<div key={category.id}>
|
||||
<Card
|
||||
key={category.id}
|
||||
onClick={() => {
|
||||
scrollToCategory(category.id);
|
||||
}}
|
||||
data-category-id={category.id}
|
||||
style={{
|
||||
borderRadius: 8,
|
||||
border: "none",
|
||||
}}
|
||||
styles={{
|
||||
body: {
|
||||
...getCategoryCardStyle(),
|
||||
width: 105,
|
||||
padding: 0,
|
||||
},
|
||||
}}
|
||||
className={`${
|
||||
activeCategory === category.id ? styles.activeCategoryCard : ""
|
||||
}`}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
gap: "0px",
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
overflow: "hidden",
|
||||
}}
|
||||
>
|
||||
{!isCategoriesSticky && (
|
||||
<ImageWithFallback
|
||||
src={category.image || default_image}
|
||||
fallbackSrc={default_image}
|
||||
alt={category.name}
|
||||
className={`${styles.categoryMenuItemImage} ${
|
||||
xs
|
||||
? styles.categoryMenuItemImageMobile
|
||||
: md
|
||||
? styles.categoryMenuItemImageTablet
|
||||
: styles.categoryMenuItemImageDesktop
|
||||
}`}
|
||||
// {...getCategoryImageStyle()}
|
||||
width={105}
|
||||
height={80}
|
||||
style={{
|
||||
borderEndEndRadius: isCategoriesSticky ? 8 : 0,
|
||||
borderEndStartRadius: isCategoriesSticky ? 8 : 0,
|
||||
}}
|
||||
loadingContainerStyle={{
|
||||
borderEndEndRadius: isCategoriesSticky ? 8 : 0,
|
||||
borderEndStartRadius: isCategoriesSticky ? 8 : 0,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Space
|
||||
direction="vertical"
|
||||
size="small"
|
||||
style={{
|
||||
flex: 1,
|
||||
padding: xs ? "4px" : md ? "4px" : "4px",
|
||||
textAlign: "center",
|
||||
...(xs || md
|
||||
? {
|
||||
// backgroundColor: "#fff6e0",
|
||||
borderBottom: "solid 1px var(--primary)",
|
||||
borderRight: "solid 1px var(--primary)",
|
||||
borderLeft: "solid 1px var(--primary)",
|
||||
borderTop: isCategoriesSticky
|
||||
? "solid 1px var(--primary)"
|
||||
: 0,
|
||||
borderStartStartRadius: isCategoriesSticky ? 8 : 0,
|
||||
borderStartEndRadius: isCategoriesSticky ? 8 : 0,
|
||||
borderEndEndRadius: 8,
|
||||
borderEndStartRadius: 8,
|
||||
backgroundColor:
|
||||
activeCategory === category.id
|
||||
? colors.primary
|
||||
: undefined,
|
||||
}
|
||||
: { borderRadius: 8 }),
|
||||
width: 104,
|
||||
height: 30,
|
||||
marginBottom: 1,
|
||||
}}
|
||||
>
|
||||
<ProText
|
||||
style={{
|
||||
margin: 0,
|
||||
lineClamp: 1,
|
||||
WebkitBoxOrient: "vertical",
|
||||
WebkitLineClamp: 1,
|
||||
lineHeight: isCategoriesSticky ? "1.5" : "1",
|
||||
maxHeight: "2.8em",
|
||||
textAlign: "center",
|
||||
|
||||
color:
|
||||
activeCategory === category.id ? "#FFF" : undefined,
|
||||
fontWeight:
|
||||
activeCategory === category.id ? "600" : "500",
|
||||
whiteSpace: "nowrap",
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
padding: 3,
|
||||
fontSize: xs ? 14 : md ? 14 : 16,
|
||||
width: xs ? 80 : md ? 100 : 120,
|
||||
display: "inline-block",
|
||||
}}
|
||||
>
|
||||
{isRTL
|
||||
? category.name || category.nameEN
|
||||
: category.nameEN || category.name}
|
||||
</ProText>
|
||||
</Space>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
54
src/pages/menu/components/LocalStorageHandler.tsx
Normal file
54
src/pages/menu/components/LocalStorageHandler.tsx
Normal file
@@ -0,0 +1,54 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect } from "react";
|
||||
|
||||
// Cart storage keys - same as in CartContext
|
||||
const CART_STORAGE_KEYS = {
|
||||
ITEMS: 'fascano_cart_items',
|
||||
SPECIAL_REQUEST: 'fascano_special_request',
|
||||
COUPON: 'fascano_coupon',
|
||||
TIP: 'fascano_tip',
|
||||
TABLES: 'fascano_tables',
|
||||
LOCATION: 'fascano_location',
|
||||
ROOM_DETAILS: 'fascano_room_details',
|
||||
OFFICE_DETAILS: 'fascano_office_details',
|
||||
GIFT_DETAILS: 'fascano_gift_details',
|
||||
ESTIMATE_TIME: 'fascano_estimate_time',
|
||||
ESTIMATE_TIME_DATE: 'fascano_estimate_time_date',
|
||||
ESTIMATE_TIME_TIME: 'fascano_estimate_time_time',
|
||||
COLLECTION_METHOD: 'fascano_collection_method',
|
||||
} as const;
|
||||
|
||||
const clearCartFromLocalStorage = () => {
|
||||
// Clear all cart-related data from localStorage
|
||||
Object.values(CART_STORAGE_KEYS).forEach(key => {
|
||||
localStorage.removeItem(key);
|
||||
});
|
||||
};
|
||||
|
||||
export default function LocalStorageHandler({
|
||||
restaurantID,
|
||||
restaurantName,
|
||||
orderType,
|
||||
}: {
|
||||
restaurantID: string;
|
||||
restaurantName: string;
|
||||
orderType: string;
|
||||
}) {
|
||||
useEffect(() => {
|
||||
// Check if restaurant has changed
|
||||
const currentStoredRestaurantID = localStorage.getItem("restaurantID");
|
||||
|
||||
// If there's a stored restaurant ID and it's different from the current one, clear the cart
|
||||
if (currentStoredRestaurantID && currentStoredRestaurantID !== restaurantID) {
|
||||
clearCartFromLocalStorage();
|
||||
}
|
||||
|
||||
// Update localStorage with new values
|
||||
localStorage.setItem("restaurantID", restaurantID);
|
||||
localStorage.setItem("restaurantName", restaurantName);
|
||||
localStorage.setItem("orderType", orderType);
|
||||
}, [restaurantID, restaurantName, orderType]);
|
||||
|
||||
return null;
|
||||
}
|
||||
92
src/pages/menu/components/MenuFooter/MenuFooter.tsx
Normal file
92
src/pages/menu/components/MenuFooter/MenuFooter.tsx
Normal file
@@ -0,0 +1,92 @@
|
||||
import { Badge, Button, Grid } from "antd";
|
||||
import CartIcon from "components/Icons/cart/CartIcon";
|
||||
import ProText from "components/ProText";
|
||||
import { selectCartItems } from "features/order/orderSlice";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Link } from "react-router-dom";
|
||||
import { useAppSelector } from "redux/hooks";
|
||||
import { colors, ProBlack2 } from "ThemeConstants";
|
||||
|
||||
const { useBreakpoint } = Grid;
|
||||
|
||||
export function MenuFooter() {
|
||||
const items = useAppSelector(selectCartItems);
|
||||
const restaurantName = localStorage.getItem("restaurantName");
|
||||
const { themeName } = useAppSelector((state) => state.theme);
|
||||
const { xs, sm } = useBreakpoint();
|
||||
const isMobile = xs;
|
||||
const isTablet = sm && !xs;
|
||||
const { t } = useTranslation();
|
||||
|
||||
const totalItems = items.reduce((sum, item) => sum + item.quantity, 0);
|
||||
|
||||
return (
|
||||
<>
|
||||
{(isMobile || isTablet) && (
|
||||
<div
|
||||
style={{
|
||||
width: "100%",
|
||||
padding: "16px 16px 0",
|
||||
position: "fixed",
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
height: "10vh",
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-around",
|
||||
gap: "1rem",
|
||||
zIndex: 999,
|
||||
backgroundColor: themeName === "light" ? "white" : ProBlack2,
|
||||
}}
|
||||
>
|
||||
<Link
|
||||
to={`/${restaurantName}/cart`}
|
||||
style={{
|
||||
width: "100%",
|
||||
padding: "0 16px",
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
type="primary"
|
||||
shape="round"
|
||||
style={{
|
||||
width: "100%",
|
||||
height: 48,
|
||||
marginBottom: 16,
|
||||
boxShadow: "none",
|
||||
}}
|
||||
>
|
||||
<Badge
|
||||
count={totalItems}
|
||||
size="default"
|
||||
style={{
|
||||
borderColor: colors.primary,
|
||||
color: "white",
|
||||
fontSize: isMobile ? 10 : 12,
|
||||
width: isMobile ? 10 : 12,
|
||||
}}
|
||||
>
|
||||
<ProText
|
||||
style={{
|
||||
color: "white",
|
||||
margin: "0 10px",
|
||||
}}
|
||||
>
|
||||
{t("menu.viewCart")}
|
||||
</ProText>
|
||||
<span
|
||||
style={{
|
||||
position: "relative",
|
||||
top: 2,
|
||||
}}
|
||||
>
|
||||
<CartIcon />
|
||||
</span>
|
||||
</Badge>
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
769
src/pages/menu/components/MenuList/MenuList.module.css
Normal file
769
src/pages/menu/components/MenuList/MenuList.module.css
Normal file
@@ -0,0 +1,769 @@
|
||||
.container {
|
||||
display: flex;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
scrollbar-width: none;
|
||||
-ms-overflow-style: none;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
background: var(--ant-bg-container);
|
||||
z-index: 10;
|
||||
border-bottom: 1px solid var(--ant-color-border);
|
||||
overflow: auto;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.container::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.menuSections {
|
||||
margin-bottom: 105px !important;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
padding: 0 1rem;
|
||||
}
|
||||
|
||||
.menuSection:first-child h3 {
|
||||
margin-top: 0px !important;
|
||||
}
|
||||
|
||||
.menuSection h3 {
|
||||
margin: 30px 0px 15px 0px;
|
||||
transition: color 0.3s ease;
|
||||
}
|
||||
|
||||
/* Enhanced responsive menu section headers */
|
||||
@media (min-width: 769px) and (max-width: 1024px) {
|
||||
.menuSection h3 {
|
||||
margin: 40px 0px 20px 0px;
|
||||
font-size: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1025px) {
|
||||
.menuSection h3 {
|
||||
margin: 50px 0px 25px 0px;
|
||||
font-size: 28px;
|
||||
}
|
||||
}
|
||||
|
||||
.menuItemsGrid {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
grid-template-columns: 1fr;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.menuItemsGridMobile {
|
||||
gap: 12px;
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.menuItemsGridTablet {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 16px;
|
||||
padding: 0 12px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.menuItemsGridDesktop {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 20px;
|
||||
padding: 0 16px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
/* Enhanced responsive menu items grid */
|
||||
@media (min-width: 1280px) {
|
||||
.menuItemsGridDesktop {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 24px;
|
||||
padding: 0 20px;
|
||||
margin-bottom: 28px;
|
||||
}
|
||||
}
|
||||
|
||||
.popularMenuItemsGrid {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
overflow-x: hidden;
|
||||
scroll-behavior: smooth;
|
||||
padding: 8px 0;
|
||||
-ms-overflow-style: none;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
.popularMenuItemsGrid::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.popularMenuItemImage {
|
||||
width: 90px;
|
||||
height: 90px;
|
||||
object-fit: cover;
|
||||
border-radius: 8px;
|
||||
transition: transform 0.3s ease;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.popularMenuItemImageMobile {
|
||||
height: 90px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.popularMenuItemImageTablet {
|
||||
height: 95px;
|
||||
width: 120px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.popularMenuItemImageDesktop {
|
||||
height: 120px;
|
||||
width: 140px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.menuItemImage {
|
||||
object-fit: cover;
|
||||
border-radius: 8px;
|
||||
transition: transform 0.3s ease;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.menuItemImage:hover {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
/* Enhanced responsive menu item images */
|
||||
@media (min-width: 769px) and (max-width: 1024px) {
|
||||
.menuItemImage {
|
||||
border-radius: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1025px) {
|
||||
.menuItemImage {
|
||||
border-radius: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.menuItemDetails {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.itemDescription {
|
||||
/* font-size: 12px !important; */
|
||||
transition: color 0.3s ease;
|
||||
}
|
||||
|
||||
/* Enhanced responsive item descriptions */
|
||||
@media (min-width: 769px) and (max-width: 1024px) {
|
||||
.itemDescription {
|
||||
font-size: 14px !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1025px) {
|
||||
.itemDescription {
|
||||
font-size: 16px !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Responsive adjustments */
|
||||
@media (max-width: 768px) {
|
||||
.container {
|
||||
gap: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 769px) and (max-width: 1024px) {
|
||||
.container {
|
||||
gap: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.pageContainer {
|
||||
padding: 0 16px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
/* Enhanced responsive page container */
|
||||
@media (min-width: 769px) and (max-width: 1024px) {
|
||||
.pageContainer {
|
||||
padding: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1025px) {
|
||||
.pageContainer {
|
||||
padding: 32px;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
}
|
||||
|
||||
/* Sidebar state adjustments */
|
||||
.sidebarCollapsed .pageContainer {
|
||||
margin-left: 80px;
|
||||
}
|
||||
|
||||
.sidebarExpanded .pageContainer {
|
||||
margin-left: 200px;
|
||||
}
|
||||
|
||||
/* .itemDescriptionIcons path {
|
||||
fill: 000044 !important;
|
||||
} */
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.sidebarCollapsed .pageContainer,
|
||||
.sidebarExpanded .pageContainer {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* Enhanced Dark theme styles */
|
||||
:global(.darkApp) .itemName {
|
||||
color: rgba(255, 255, 255, 0.95) !important;
|
||||
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
|
||||
transition: color 0.3s ease;
|
||||
}
|
||||
|
||||
:global(.darkApp) .itemName:hover {
|
||||
color: var(--primary) !important;
|
||||
}
|
||||
|
||||
:global(.darkApp) .itemDescription {
|
||||
color: rgba(255, 255, 255, 0.75) !important;
|
||||
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
:global(.darkApp) .itemPrice {
|
||||
color: var(--primary) !important;
|
||||
font-weight: 600;
|
||||
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
/* Enhanced dark theme for menu sections */
|
||||
:global(.darkApp) .menuSection h3 {
|
||||
color: #ffffff;
|
||||
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
:global(.darkApp) .menuSection {
|
||||
background-color: #0a0a0a;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
/* Enhanced dark theme for menu items */
|
||||
:global(.darkApp) .menuItem {
|
||||
background-color: #181818 !important;
|
||||
border-color: #363636 !important;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
:global(.darkApp) .menuItem:hover {
|
||||
background-color: #363636 !important;
|
||||
border-color: #424242 !important;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
/* Enhanced dark theme for popular menu items */
|
||||
:global(.darkApp) .popularMenuItem {
|
||||
background-color: #181818 !important;
|
||||
border-color: #363636 !important;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
:global(.darkApp) .popularMenuItem:hover {
|
||||
background-color: #363636 !important;
|
||||
border-color: #424242 !important;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
@keyframes activePulse {
|
||||
0%,
|
||||
100% {
|
||||
opacity: 0.1;
|
||||
}
|
||||
50% {
|
||||
opacity: 0.2;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes darkActivePulse {
|
||||
0%,
|
||||
100% {
|
||||
opacity: 0.05;
|
||||
}
|
||||
50% {
|
||||
opacity: 0.15;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes imageEntrance {
|
||||
0% {
|
||||
transform: scale(0.9) rotate(-2deg);
|
||||
opacity: 0.7;
|
||||
}
|
||||
100% {
|
||||
transform: scale(1) rotate(0deg);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes imageReturn {
|
||||
0% {
|
||||
transform: scale(0.95) rotate(-1deg);
|
||||
opacity: 0.85;
|
||||
}
|
||||
100% {
|
||||
transform: scale(1) rotate(0deg);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes cardEntrance {
|
||||
0% {
|
||||
transform: translateY(20px) scale(0.95);
|
||||
opacity: 0.8;
|
||||
}
|
||||
100% {
|
||||
transform: translateY(0) scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes cardReturn {
|
||||
0% {
|
||||
transform: translateY(-5px) scale(0.98);
|
||||
opacity: 0.9;
|
||||
}
|
||||
100% {
|
||||
transform: translateY(0) scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes glowPulse {
|
||||
0%,
|
||||
100% {
|
||||
opacity: 0.3;
|
||||
transform: scaleX(0.8);
|
||||
}
|
||||
50% {
|
||||
opacity: 0.6;
|
||||
transform: scaleX(1);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes glowFadeOut {
|
||||
0% {
|
||||
opacity: 0.1;
|
||||
transform: scaleX(1);
|
||||
}
|
||||
100% {
|
||||
opacity: 0;
|
||||
transform: scaleX(0.8);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slideDown {
|
||||
0% {
|
||||
transform: translateY(-100%) scale(0.95);
|
||||
opacity: 0;
|
||||
filter: blur(10px);
|
||||
}
|
||||
50% {
|
||||
transform: translateY(-20%) scale(0.98);
|
||||
opacity: 0.7;
|
||||
filter: blur(5px);
|
||||
}
|
||||
100% {
|
||||
transform: translateY(0) scale(1);
|
||||
opacity: 1;
|
||||
filter: blur(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slideUp {
|
||||
0% {
|
||||
transform: translateY(-10px) scale(0.98);
|
||||
opacity: 0.9;
|
||||
filter: blur(2px);
|
||||
}
|
||||
50% {
|
||||
transform: translateY(-5px) scale(0.99);
|
||||
opacity: 0.95;
|
||||
filter: blur(1px);
|
||||
}
|
||||
100% {
|
||||
transform: translateY(0) scale(1);
|
||||
opacity: 1;
|
||||
filter: blur(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes darkGlowPulse {
|
||||
0%,
|
||||
100% {
|
||||
opacity: 0.4;
|
||||
transform: scaleX(0.8);
|
||||
}
|
||||
50% {
|
||||
opacity: 0.7;
|
||||
transform: scaleX(1);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes darkGlowFadeOut {
|
||||
0% {
|
||||
opacity: 0.2;
|
||||
transform: scaleX(1);
|
||||
}
|
||||
100% {
|
||||
opacity: 0;
|
||||
transform: scaleX(0.8);
|
||||
}
|
||||
}
|
||||
|
||||
/* Enhanced dark theme for menu item images */
|
||||
:global(.darkApp) .menuItemImage,
|
||||
:global(.darkApp) .popularMenuItemImage {
|
||||
border-color: rgba(255, 255, 255, 0.1);
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
/* Enhanced dark theme for add to cart buttons */
|
||||
:global(.darkApp) .addToCartButton {
|
||||
background-color: var(--primary) !important;
|
||||
border-color: var(--primary) !important;
|
||||
color: #000000 !important;
|
||||
font-weight: 600;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
:global(.darkApp) .addToCartButton:hover {
|
||||
background-color: #ffd633 !important;
|
||||
border-color: #ffd633 !important;
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 8px rgba(255, 198, 0, 0.3);
|
||||
}
|
||||
|
||||
/* Enhanced dark theme for quantity controls */
|
||||
:global(.darkApp) .quantityControl {
|
||||
background-color: rgba(42, 42, 42, 0.8);
|
||||
border-color: rgba(255, 255, 255, 0.1);
|
||||
backdrop-filter: blur(8px);
|
||||
}
|
||||
|
||||
:global(.darkApp) .quantityButton {
|
||||
background-color: rgba(54, 54, 54, 0.8) !important;
|
||||
border-color: rgba(255, 255, 255, 0.1) !important;
|
||||
color: #ffffff !important;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
:global(.darkApp) .quantityButton:hover {
|
||||
background-color: rgba(66, 66, 66, 0.9) !important;
|
||||
border-color: var(--primary) !important;
|
||||
}
|
||||
|
||||
:global(.darkApp) .quantityInput {
|
||||
background-color: rgba(42, 42, 42, 0.8) !important;
|
||||
border-color: rgba(255, 255, 255, 0.1) !important;
|
||||
color: #ffffff !important;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* Enhanced dark theme for container backgrounds */
|
||||
:global(.darkApp) .container {
|
||||
background-color: #0a0a0a !important;
|
||||
border-bottom-color: #363636 !important;
|
||||
}
|
||||
|
||||
:global(.darkApp) .itemDescriptionIcons path {
|
||||
fill: #ffffff;
|
||||
}
|
||||
|
||||
/* Cart badge styles */
|
||||
.cartBadge {
|
||||
position: absolute;
|
||||
top: -8px;
|
||||
z-index: 2;
|
||||
color: white;
|
||||
border-radius: 50%;
|
||||
min-width: 20px;
|
||||
height: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
transition: all 0.3s ease;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.cartBadge:hover {
|
||||
transform: scale(1.1);
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.cartBadgeRTL {
|
||||
left: -16px;
|
||||
}
|
||||
|
||||
.cartBadgeLTR {
|
||||
right: -8px;
|
||||
}
|
||||
|
||||
/* Enhanced responsive cart badge */
|
||||
@media (min-width: 769px) and (max-width: 1024px) {
|
||||
.cartBadge {
|
||||
min-width: 24px;
|
||||
height: 24px;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1025px) {
|
||||
.cartBadge {
|
||||
min-width: 28px;
|
||||
height: 28px;
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
/* RTL support for cart badge */
|
||||
/* [dir="rtl"] .cartBadge {
|
||||
right: auto;
|
||||
left: -8px;
|
||||
} */
|
||||
|
||||
/* Animation for newly added items */
|
||||
@keyframes badgePulse {
|
||||
0% {
|
||||
transform: scale(1);
|
||||
}
|
||||
50% {
|
||||
transform: scale(1.2);
|
||||
}
|
||||
100% {
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
.cartBadge.animate {
|
||||
animation: badgePulse 0.6s ease-in-out;
|
||||
}
|
||||
|
||||
/* Enhanced dark theme animations */
|
||||
:global(.darkApp) .menuItem,
|
||||
:global(.darkApp) .popularMenuItem {
|
||||
animation: fadeInUp 0.3s ease-out;
|
||||
}
|
||||
|
||||
@keyframes fadeInUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* Smooth transitions for all elements */
|
||||
.container *,
|
||||
.menuItem *,
|
||||
.popularMenuItem * {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
/* Enhanced responsive menu item cards */
|
||||
@media (min-width: 769px) and (max-width: 1024px) {
|
||||
.menuItem {
|
||||
border-radius: 12px !important;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1) !important;
|
||||
}
|
||||
|
||||
.menuItem:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.15) !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1025px) {
|
||||
.menuItem {
|
||||
border-radius: 16px !important;
|
||||
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.1) !important;
|
||||
}
|
||||
|
||||
.menuItem:hover {
|
||||
transform: translateY(-6px);
|
||||
box-shadow: 0 12px 24px rgba(0, 0, 0, 0.15) !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Enhanced responsive focus states */
|
||||
.menuItem:focus {
|
||||
outline: 2px solid var(--primary);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.menuItem:focus {
|
||||
outline-offset: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Enhanced responsive print styles */
|
||||
@media print {
|
||||
.menuItem {
|
||||
box-shadow: none !important;
|
||||
border: 1px solid #ccc !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Skeleton Loading Styles */
|
||||
.skeletonContainer {
|
||||
animation: skeletonPulse 1.5s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.skeletonContainer .ant-skeleton {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.skeletonContainer .ant-skeleton-image {
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.skeletonContainer .ant-skeleton-input {
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.skeletonContainer .ant-skeleton-button {
|
||||
border-radius: 20px;
|
||||
}
|
||||
|
||||
@keyframes skeletonPulse {
|
||||
0% {
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
opacity: 0.7;
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes skeletonShimmer {
|
||||
0% {
|
||||
background-position: -200px 0;
|
||||
}
|
||||
100% {
|
||||
background-position: calc(200px + 100%) 0;
|
||||
}
|
||||
}
|
||||
|
||||
.skeletonContainer .ant-skeleton-active .ant-skeleton-input,
|
||||
.skeletonContainer .ant-skeleton-active .ant-skeleton-image,
|
||||
.skeletonContainer .ant-skeleton-active .ant-skeleton-button {
|
||||
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
|
||||
background-size: 200px 100%;
|
||||
animation: skeletonShimmer 1.5s infinite;
|
||||
}
|
||||
|
||||
/* Mobile-specific skeleton styles */
|
||||
@media (max-width: 768px) {
|
||||
.skeletonContainer .ant-skeleton {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.skeletonContainer .ant-skeleton-image {
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.skeletonContainer .ant-skeleton-input {
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.skeletonContainer .ant-skeleton-button {
|
||||
border-radius: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Dark theme skeleton styles */
|
||||
:global(.darkApp) .skeletonContainer .ant-skeleton-active .ant-skeleton-input,
|
||||
:global(.darkApp) .skeletonContainer .ant-skeleton-active .ant-skeleton-image,
|
||||
:global(.darkApp) .skeletonContainer .ant-skeleton-active .ant-skeleton-button {
|
||||
background: linear-gradient(90deg, #181818 25%, #363636 50%, #181818 75%);
|
||||
background-size: 200px 100%;
|
||||
animation: skeletonShimmer 1.5s infinite;
|
||||
}
|
||||
|
||||
:global(.darkApp) .skeletonContainer .ant-skeleton-input,
|
||||
:global(.darkApp) .skeletonContainer .ant-skeleton-image,
|
||||
:global(.darkApp) .skeletonContainer .ant-skeleton-button {
|
||||
background-color: #181818;
|
||||
border-color: #363636;
|
||||
}
|
||||
|
||||
/* Tablet-specific skeleton styles */
|
||||
@media (min-width: 769px) and (max-width: 1024px) {
|
||||
.skeletonContainer {
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.skeletonContainer .ant-skeleton {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.skeletonContainer .ant-skeleton-image {
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.skeletonContainer .ant-skeleton-input {
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.skeletonContainer .ant-skeleton-button {
|
||||
border-radius: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
.menuItemsGridSticky {
|
||||
height: 60;
|
||||
}
|
||||
|
||||
.heartButton {
|
||||
position: absolute;
|
||||
top: 5px;
|
||||
left: 5px;
|
||||
}
|
||||
|
||||
:global(.rtl) .heartButton {
|
||||
right: 5px;
|
||||
}
|
||||
|
||||
.productLink {
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.categoryMenuItemImage {
|
||||
width: 100%;
|
||||
object-fit: cover;
|
||||
border-radius: 8px;
|
||||
text-align: center;
|
||||
}
|
||||
281
src/pages/menu/components/MenuList/MenuList.tsx
Normal file
281
src/pages/menu/components/MenuList/MenuList.tsx
Normal file
@@ -0,0 +1,281 @@
|
||||
import { Badge, Card, Grid } from "antd";
|
||||
import ArabicPrice from "components/ArabicPrice";
|
||||
import ImageWithFallback from "components/ImageWithFallback";
|
||||
import { ItemDescriptionIcons } from "components/ItemDescriptionIcons/ItemDescriptionIcons";
|
||||
import ProText from "components/ProText";
|
||||
import ProTitle from "components/ProTitle";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useAppSelector } from "redux/hooks";
|
||||
import { colors } from "ThemeConstants";
|
||||
import { Product } from "utils/types/appTypes";
|
||||
import { AddToCart } from "../AddToCart";
|
||||
import styles from "./MenuList.module.css";
|
||||
|
||||
interface MenuListProps {
|
||||
data:
|
||||
| {
|
||||
products: Product[];
|
||||
categories: { id: number; name: string; image?: string }[];
|
||||
}
|
||||
| undefined;
|
||||
id: string;
|
||||
categoryRefs: React.RefObject<{ [key: number]: HTMLDivElement | null }>;
|
||||
}
|
||||
|
||||
const { useBreakpoint } = Grid;
|
||||
|
||||
export function MenuList({ data, categoryRefs }: MenuListProps) {
|
||||
const { isRTL } = useAppSelector((state) => state.locale);
|
||||
const products = data?.products;
|
||||
const { xs, md } = useBreakpoint();
|
||||
const { items } = useAppSelector((state) => state.order);
|
||||
const restaurantName = localStorage.getItem("restaurantName");
|
||||
const navigate = useNavigate();
|
||||
const { t } = useTranslation();
|
||||
const { themeName } = useAppSelector((state) => state.theme);
|
||||
|
||||
// Show error state if data exists but has no products
|
||||
if (data && (!data.products || data.products.length === 0)) {
|
||||
return (
|
||||
<div
|
||||
className={styles.menuSections}
|
||||
style={{ padding: "40px", textAlign: "center" }}
|
||||
>
|
||||
<ProText type="secondary">{t("menu.noMenuItemsAvailable")}</ProText>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Group products by category
|
||||
const productsByCategory = products?.reduce((acc, product) => {
|
||||
if (product.categoryId && !acc[product?.categoryId]) {
|
||||
acc[product?.categoryId] = [];
|
||||
}
|
||||
acc[product?.categoryId || 0].push(product);
|
||||
return acc;
|
||||
}, {} as Record<number, Product[]>);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={styles.menuSections}>
|
||||
{data?.categories?.map((category) => {
|
||||
const categoryProducts = productsByCategory?.[category.id] || [];
|
||||
if (categoryProducts.length === 0) return null;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={category.id}
|
||||
ref={(el) => {
|
||||
if (categoryRefs.current) {
|
||||
categoryRefs.current[category.id] = el;
|
||||
}
|
||||
}}
|
||||
style={{ marginBottom: "1rem" }}
|
||||
>
|
||||
<ImageWithFallback
|
||||
src={category.image || "/default.png"}
|
||||
fallbackSrc="/default.png"
|
||||
alt={category.name}
|
||||
width="100%"
|
||||
height={130}
|
||||
style={{
|
||||
width: "100%",
|
||||
objectFit: "cover",
|
||||
borderRadius: 8,
|
||||
overflow: "hidden",
|
||||
}}
|
||||
className={styles.categoryMenuItemImage}
|
||||
loadingContainerStyle={{
|
||||
width: "100%",
|
||||
}}
|
||||
/>
|
||||
<ProTitle
|
||||
style={{
|
||||
fontSize: "1.25rem",
|
||||
fontWeight: "bold",
|
||||
marginBottom: "1rem",
|
||||
textAlign: "center",
|
||||
color: themeName === "dark" ? "#fff" : "#000044",
|
||||
}}
|
||||
level={5}
|
||||
>
|
||||
{isRTL ? category.name : category.name}
|
||||
</ProTitle>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: "1rem",
|
||||
}}
|
||||
>
|
||||
{categoryProducts.map((item: Product) => (
|
||||
<div
|
||||
key={item.id}
|
||||
className={styles.productLink}
|
||||
onClick={() => {
|
||||
localStorage.setItem("product", JSON.stringify(item));
|
||||
navigate(`/${restaurantName}/product/${item.id}`);
|
||||
}}
|
||||
>
|
||||
<Card
|
||||
key={item.id}
|
||||
style={{
|
||||
borderRadius: 8,
|
||||
overflow: "hide",
|
||||
}}
|
||||
styles={{
|
||||
body: {
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
justifyContent: "space-between",
|
||||
borderRadius: 8,
|
||||
padding: item.description
|
||||
? "16px 16px 8px 16px"
|
||||
: "16px 16px 24px 16px",
|
||||
overflow: "hide",
|
||||
boxShadow:
|
||||
"0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -2px rgba(0, 0, 0, 0.05)",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
height: "100%",
|
||||
gap: xs ? 10 : 16,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
justifyContent: "space-between",
|
||||
gap: "0.5rem",
|
||||
}}
|
||||
>
|
||||
<ProText
|
||||
style={{
|
||||
margin: 0,
|
||||
display: "inline-block",
|
||||
fontSize: xs ? "1rem" : 18,
|
||||
fontWeight: 600,
|
||||
letterSpacing: "-0.01em",
|
||||
lineHeight: 1.2,
|
||||
color: themeName === "dark" ? "#fff" : "#000044",
|
||||
}}
|
||||
>
|
||||
{isRTL ? item.name : item.nameOther}
|
||||
</ProText>
|
||||
{item.description && (
|
||||
<ProText
|
||||
type="secondary"
|
||||
className={styles.itemDescription}
|
||||
style={{
|
||||
display: "-webkit-box",
|
||||
WebkitBoxOrient: "vertical",
|
||||
WebkitLineClamp: xs ? 2 : 4,
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
wordWrap: "break-word",
|
||||
overflowWrap: "break-word",
|
||||
lineHeight: "1.5rem",
|
||||
maxHeight: xs ? "3em" : "6.6em",
|
||||
fontSize: xs ? "1rem" : 18,
|
||||
letterSpacing: "0.01em",
|
||||
}}
|
||||
>
|
||||
{item.description}
|
||||
</ProText>
|
||||
)}
|
||||
|
||||
<div>
|
||||
{item.original_price !== item.price && (
|
||||
<ArabicPrice
|
||||
price={item.original_price}
|
||||
strong
|
||||
style={{
|
||||
fontSize: xs ? "1rem" : 22,
|
||||
fontWeight: 700,
|
||||
color: colors.primary,
|
||||
textDecoration: "line-through",
|
||||
marginRight: isRTL ? 0 : 10,
|
||||
marginLeft: isRTL ? 10 : 0,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<ArabicPrice
|
||||
price={item.price}
|
||||
strong
|
||||
style={{
|
||||
fontSize: xs ? "1rem" : 22,
|
||||
fontWeight: 700,
|
||||
color: colors.primary,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
style={{
|
||||
position: "relative",
|
||||
...(isRTL ? { right: -5 } : {}),
|
||||
}}
|
||||
>
|
||||
<ItemDescriptionIcons
|
||||
className={styles.itemDescriptionIcons}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ position: "relative" }}>
|
||||
<ImageWithFallback
|
||||
src={item.image_small || "/default.png"}
|
||||
fallbackSrc="/default.png"
|
||||
alt={item.name}
|
||||
className={`${styles.popularMenuItemImage} ${
|
||||
xs
|
||||
? styles.popularMenuItemImageMobile
|
||||
: md
|
||||
? styles.popularMenuItemImageTablet
|
||||
: styles.popularMenuItemImageDesktop
|
||||
}`}
|
||||
width={90}
|
||||
height={90}
|
||||
/>
|
||||
|
||||
<AddToCart item={item} />
|
||||
|
||||
{items.find((i) => i.id === item.id) && (
|
||||
<Badge
|
||||
count={
|
||||
items.find((i) => i.id === item.id)?.quantity
|
||||
}
|
||||
className={
|
||||
styles.cartBadge +
|
||||
" " +
|
||||
(isRTL
|
||||
? styles.cartBadgeRTL
|
||||
: styles.cartBadgeLTR)
|
||||
}
|
||||
style={{
|
||||
backgroundColor: colors.primary,
|
||||
}}
|
||||
title={`${
|
||||
items.find((i) => i.id === item.id)?.quantity
|
||||
} in cart`}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
338
src/pages/menu/components/MenuSkeleton/MenuSkeleton.module.css
Normal file
338
src/pages/menu/components/MenuSkeleton/MenuSkeleton.module.css
Normal file
@@ -0,0 +1,338 @@
|
||||
.categoriesContainer {
|
||||
display: flex;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
scrollbar-width: none;
|
||||
-ms-overflow-style: none;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
background: var(--ant-bg-container);
|
||||
z-index: 10;
|
||||
border-bottom: 1px solid var(--ant-color-border);
|
||||
overflow: hidden;
|
||||
text-align: center;
|
||||
height: 96px;
|
||||
overflow-x: auto;
|
||||
transition: all 0.4s cubic-bezier(0.25, 0.46, 0.45, 0.94);
|
||||
will-change: position, transform, opacity, filter;
|
||||
transform-origin: top center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
/* Enhanced responsive categories container */
|
||||
@media (min-width: 769px) and (max-width: 1024px) {
|
||||
.categoriesContainer {
|
||||
height: 160px;
|
||||
padding: 16px;
|
||||
gap: 16px;
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.categoriesSticky {
|
||||
padding: 12px 16px !important;
|
||||
height: 30px !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1025px) {
|
||||
.categoriesContainer {
|
||||
height: 180px;
|
||||
padding: 24px;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
gap: 16px;
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.categoriesSticky {
|
||||
padding: 16px 24px !important;
|
||||
height: 70px !important;
|
||||
max-width: 100vw !important;
|
||||
margin: 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Menu Sections and Grid Layout */
|
||||
/* .menuSections {
|
||||
margin-bottom: 105px !important;
|
||||
} */
|
||||
|
||||
.menuSection:first-child h3 {
|
||||
margin-top: 0px !important;
|
||||
}
|
||||
|
||||
.menuSection h3 {
|
||||
margin: 30px 0px 15px 0px;
|
||||
transition: color 0.3s ease;
|
||||
}
|
||||
|
||||
/* Enhanced responsive menu section headers */
|
||||
@media (min-width: 769px) and (max-width: 1024px) {
|
||||
.menuSection h3 {
|
||||
margin: 40px 0px 20px 0px;
|
||||
font-size: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1025px) {
|
||||
.menuSection h3 {
|
||||
margin: 50px 0px 25px 0px;
|
||||
font-size: 28px;
|
||||
}
|
||||
}
|
||||
|
||||
.menuItemsGrid {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
grid-template-columns: 1fr;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.menuItemsGridMobile {
|
||||
gap: 12px;
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.menuItemsGridTablet {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 16px;
|
||||
padding: 0 12px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.menuItemsGridDesktop {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 20px;
|
||||
padding: 0 16px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
/* Enhanced responsive menu items grid */
|
||||
@media (min-width: 1280px) {
|
||||
.menuItemsGridDesktop {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 24px;
|
||||
padding: 0 20px;
|
||||
margin-bottom: 28px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Restaurant Header Skeleton */
|
||||
.restaurantHeader {
|
||||
position: relative;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.leftShape {
|
||||
position: absolute;
|
||||
top: 133px;
|
||||
width: 47px;
|
||||
height: 50px;
|
||||
left: -11px;
|
||||
background-color: var(--background);
|
||||
clip-path: path("M 0 53 Q 50 50, 50 0 Q 50 50, 100 100 L 0 100 Z");
|
||||
}
|
||||
|
||||
:global(.darkApp) .leftShape {
|
||||
background-color: var(--background);
|
||||
}
|
||||
|
||||
.rightShape {
|
||||
position: absolute;
|
||||
top: 133px;
|
||||
width: 47px;
|
||||
height: 50px;
|
||||
left: 102px;
|
||||
background-color: var(--background);
|
||||
clip-path: path("M 0 53 Q 50 50, 50 0 Q 50 50, 100 100 L 0 100 Z");
|
||||
transform: scale(-1, 1);
|
||||
}
|
||||
|
||||
:global(.darkApp) .rightShape {
|
||||
background-color: var(--background);
|
||||
}
|
||||
|
||||
/* Restaurant Info Skeleton */
|
||||
.restaurantInfoSkeleton {
|
||||
text-align: center;
|
||||
padding: 0 16px;
|
||||
}
|
||||
|
||||
/* Loyalty Card Skeleton */
|
||||
.loyaltySkeleton {
|
||||
margin: 16px 0;
|
||||
padding: 0 16px;
|
||||
}
|
||||
|
||||
.loyaltyCardSkeleton {
|
||||
height: 115px;
|
||||
border-radius: 16px;
|
||||
}
|
||||
|
||||
.loyaltyCardSkeleton :global(.ant-card-body) {
|
||||
height: 115px;
|
||||
padding: 12px 16px !important;
|
||||
}
|
||||
|
||||
/* Page Container */
|
||||
.pageContainer {
|
||||
transition: all 0.3s ease;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
/* Enhanced responsive page container */
|
||||
@media (min-width: 769px) and (max-width: 1024px) {
|
||||
.pageContainer {
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.restaurantInfoSkeleton {
|
||||
padding: 0 24px;
|
||||
margin: 32px 0;
|
||||
}
|
||||
|
||||
.loyaltySkeleton {
|
||||
padding: 0 24px;
|
||||
}
|
||||
|
||||
.restaurantHeader {
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1025px) {
|
||||
.pageContainer {
|
||||
padding: 32px;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.restaurantInfoSkeleton {
|
||||
padding: 0 32px;
|
||||
margin: 40px 0;
|
||||
}
|
||||
|
||||
.loyaltySkeleton {
|
||||
padding: 0 32px;
|
||||
}
|
||||
|
||||
.restaurantHeader {
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Skeleton Loading Styles */
|
||||
.skeletonContainer {
|
||||
animation: skeletonPulse 1.5s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.skeletonContainer .ant-skeleton {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.skeletonContainer .ant-skeleton-image {
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.skeletonContainer .ant-skeleton-input {
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.skeletonContainer .ant-skeleton-button {
|
||||
border-radius: 20px;
|
||||
}
|
||||
|
||||
@keyframes skeletonPulse {
|
||||
0% {
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
opacity: 0.7;
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes skeletonShimmer {
|
||||
0% {
|
||||
background-position: -200px 0;
|
||||
}
|
||||
100% {
|
||||
background-position: calc(200px + 100%) 0;
|
||||
}
|
||||
}
|
||||
|
||||
.skeletonContainer .ant-skeleton-active .ant-skeleton-input,
|
||||
.skeletonContainer .ant-skeleton-active .ant-skeleton-image,
|
||||
.skeletonContainer .ant-skeleton-active .ant-skeleton-button {
|
||||
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
|
||||
background-size: 200px 100%;
|
||||
animation: skeletonShimmer 1.5s infinite;
|
||||
}
|
||||
|
||||
/* Mobile-specific skeleton styles */
|
||||
@media (max-width: 768px) {
|
||||
.skeletonContainer .ant-skeleton {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.skeletonContainer .ant-skeleton-image {
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.skeletonContainer .ant-skeleton-input {
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.skeletonContainer .ant-skeleton-button {
|
||||
border-radius: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Dark theme skeleton styles */
|
||||
:global(.darkApp) .skeletonContainer .ant-skeleton-active .ant-skeleton-input,
|
||||
:global(.darkApp) .skeletonContainer .ant-skeleton-active .ant-skeleton-image,
|
||||
:global(.darkApp) .skeletonContainer .ant-skeleton-active .ant-skeleton-button {
|
||||
background: linear-gradient(90deg, #181818 25%, #363636 50%, #181818 75%);
|
||||
background-size: 200px 100%;
|
||||
animation: skeletonShimmer 1.5s infinite;
|
||||
}
|
||||
|
||||
:global(.darkApp) .skeletonContainer .ant-skeleton-input,
|
||||
:global(.darkApp) .skeletonContainer .ant-skeleton-image,
|
||||
:global(.darkApp) .skeletonContainer .ant-skeleton-button {
|
||||
background-color: #181818;
|
||||
border-color: #363636;
|
||||
}
|
||||
|
||||
/* Tablet-specific skeleton styles */
|
||||
@media (min-width: 769px) and (max-width: 1024px) {
|
||||
.skeletonContainer {
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.skeletonContainer .ant-skeleton {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.skeletonContainer .ant-skeleton-image {
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.skeletonContainer .ant-skeleton-input {
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.skeletonContainer .ant-skeleton-button {
|
||||
border-radius: 18px;
|
||||
}
|
||||
}
|
||||
438
src/pages/menu/components/MenuSkeleton/MenuSkeleton.tsx
Normal file
438
src/pages/menu/components/MenuSkeleton/MenuSkeleton.tsx
Normal file
@@ -0,0 +1,438 @@
|
||||
import { Card, Grid, Skeleton } from "antd";
|
||||
import styles from "./MenuSkeleton.module.css";
|
||||
|
||||
interface MenuSkeletonProps {
|
||||
categoryCount?: number;
|
||||
itemCount?: number;
|
||||
variant?:
|
||||
| "default"
|
||||
| "minimal"
|
||||
| "detailed"
|
||||
| "categories-only"
|
||||
| "menu-only";
|
||||
}
|
||||
|
||||
const { useBreakpoint } = Grid;
|
||||
|
||||
const MenuSkeleton = ({
|
||||
categoryCount = 6,
|
||||
itemCount = 8,
|
||||
variant = "default",
|
||||
}: MenuSkeletonProps) => {
|
||||
const { xs, sm, md } = useBreakpoint();
|
||||
const isMobile = xs;
|
||||
const isTablet = sm && !md;
|
||||
const isDesktop = md;
|
||||
|
||||
const getCategoryCardStyle = () => {
|
||||
if (isMobile) {
|
||||
return {
|
||||
width: 90,
|
||||
height: 95,
|
||||
};
|
||||
} else if (isTablet) {
|
||||
return {
|
||||
width: 120,
|
||||
height: 140,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
width: 140,
|
||||
height: 160,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const getMenuItemCardStyle = () => {
|
||||
if (isMobile) {
|
||||
return {
|
||||
height: 140,
|
||||
padding: "12px 12px 12px 16px",
|
||||
};
|
||||
} else if (isTablet) {
|
||||
return {
|
||||
height: 160,
|
||||
padding: "16px",
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
height: 180,
|
||||
padding: "20px",
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const getCategoryImageStyle = () => {
|
||||
if (isMobile) {
|
||||
return {
|
||||
width: 90,
|
||||
height: 60,
|
||||
borderTopLeftRadius: 8,
|
||||
borderTopRightRadius: 8,
|
||||
};
|
||||
} else if (isTablet) {
|
||||
return {
|
||||
width: 120,
|
||||
height: 90,
|
||||
borderTopLeftRadius: 12,
|
||||
borderTopRightRadius: 12,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
width: 120,
|
||||
height: 100,
|
||||
borderTopLeftRadius: 16,
|
||||
borderTopRightRadius: 16,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const getMenuItemImageStyle = () => {
|
||||
if (isMobile) {
|
||||
return {
|
||||
width: 90,
|
||||
height: 95,
|
||||
};
|
||||
} else if (isTablet) {
|
||||
return {
|
||||
width: 120,
|
||||
height: 120,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
width: 140,
|
||||
height: 140,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const getGridClass = () => {
|
||||
if (isMobile) {
|
||||
return styles.menuItemsGridMobile;
|
||||
} else if (isTablet) {
|
||||
return styles.menuItemsGridTablet;
|
||||
} else {
|
||||
return styles.menuItemsGridDesktop;
|
||||
}
|
||||
};
|
||||
|
||||
const getPageContainerStyle = () => {
|
||||
if (isDesktop) {
|
||||
return {
|
||||
maxWidth: "1200px",
|
||||
margin: "0 auto",
|
||||
padding: "32px",
|
||||
};
|
||||
} else if (isTablet) {
|
||||
return {
|
||||
padding: "24px",
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`${styles.pageContainer} ${styles.skeletonContainer}`}
|
||||
style={getPageContainerStyle()}
|
||||
>
|
||||
{/* Restaurant Header Skeleton */}
|
||||
<div className={styles.restaurantHeader}>
|
||||
{/* Cover Image Skeleton */}
|
||||
<Skeleton.Image
|
||||
active
|
||||
style={{
|
||||
width: "100vw",
|
||||
height: isMobile ? 182 : isTablet ? 200 : 220,
|
||||
borderRadius: 0,
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Logo Skeleton */}
|
||||
<Skeleton.Image
|
||||
active
|
||||
style={{
|
||||
position: "absolute",
|
||||
left: isMobile ? "33px" : "40px",
|
||||
top: isMobile ? "133px" : "-70px",
|
||||
borderRadius: "50%",
|
||||
width: isMobile ? "72px" : "80px",
|
||||
height: isMobile ? "72px" : "80px",
|
||||
border: "3px solid var(--background)",
|
||||
zIndex: 10,
|
||||
overflow: "hidden",
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Decorative Shapes Skeleton */}
|
||||
<div className={styles.leftShape}></div>
|
||||
<div className={styles.rightShape}></div>
|
||||
</div>
|
||||
|
||||
{/* Restaurant Info Skeleton */}
|
||||
<div className={styles.restaurantInfoSkeleton}>
|
||||
<div
|
||||
className={
|
||||
styles.restaurantDescriptionSkeleton +
|
||||
" " +
|
||||
"restaurant-description-skeleton"
|
||||
}
|
||||
>
|
||||
<Skeleton
|
||||
active
|
||||
paragraph={{
|
||||
rows: 1,
|
||||
width: ["100%"],
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Loyalty Card Skeleton */}
|
||||
<div className={styles.loyaltySkeleton}>
|
||||
<Card className={styles.loyaltyCardSkeleton}>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
marginBottom: "0.5rem",
|
||||
}}
|
||||
>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: "8px" }}>
|
||||
<Skeleton.Image
|
||||
active
|
||||
style={{
|
||||
width: "24px",
|
||||
height: "24px",
|
||||
borderRadius: "4px",
|
||||
}}
|
||||
/>
|
||||
<Skeleton.Input
|
||||
active
|
||||
style={{
|
||||
width: "120px",
|
||||
height: "16px",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<Skeleton.Image
|
||||
active
|
||||
style={{
|
||||
width: "24px",
|
||||
height: "24px",
|
||||
borderRadius: "4px",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<br />
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<div style={{ display: "flex", gap: "4px" }}>
|
||||
{Array.from({ length: 5 }).map((_, index) => (
|
||||
<Skeleton.Image
|
||||
key={index}
|
||||
active
|
||||
style={{
|
||||
width: "32px",
|
||||
height: "32px",
|
||||
borderRadius: "50%",
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<Skeleton.Button
|
||||
active
|
||||
style={{
|
||||
width: "80px",
|
||||
height: "32px",
|
||||
borderRadius: "16px",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Categories Skeleton */}
|
||||
{(variant === "default" ||
|
||||
variant === "minimal" ||
|
||||
variant === "detailed" ||
|
||||
variant === "categories-only") && (
|
||||
<div style={{ padding: "0 1rem", display: "flex", gap: 8, overflow: "hidden" }}>
|
||||
{Array.from({
|
||||
length:
|
||||
variant === "minimal"
|
||||
? Math.min(categoryCount, 4)
|
||||
: categoryCount,
|
||||
}).map((_, index) => (
|
||||
<div
|
||||
key={index}
|
||||
style={{
|
||||
marginRight: isMobile ? "3px" : "8px",
|
||||
}}
|
||||
>
|
||||
<Card
|
||||
style={{ borderRadius: 8 }}
|
||||
styles={{
|
||||
body: {
|
||||
...getCategoryCardStyle(),
|
||||
padding: 0,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
gap: "0px",
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
}}
|
||||
>
|
||||
<Skeleton.Image
|
||||
active
|
||||
style={{
|
||||
...getCategoryImageStyle(),
|
||||
borderBottomLeftRadius: 0,
|
||||
borderBottomRightRadius: 0,
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
style={{
|
||||
flex: 1,
|
||||
padding: isMobile ? "4px" : "8px",
|
||||
textAlign: "center",
|
||||
width: "100%",
|
||||
}}
|
||||
>
|
||||
<Skeleton
|
||||
active
|
||||
paragraph={{ rows: 1 }}
|
||||
style={{
|
||||
margin: 0,
|
||||
padding: isMobile ? 3 : 8,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Menu Items Skeleton */}
|
||||
{(variant === "default" ||
|
||||
variant === "minimal" ||
|
||||
variant === "detailed" ||
|
||||
variant === "menu-only") && (
|
||||
<div className={styles.menuSections} style={{ padding: "0 1rem" }}>
|
||||
{Array.from({ length: variant === "minimal" ? 1 : 3 }).map(
|
||||
(_, sectionIndex) => (
|
||||
<div key={sectionIndex} className={styles.menuSection}>
|
||||
{/* Section Header Skeleton */}
|
||||
{/* <div
|
||||
style={{
|
||||
margin: isMobile ? "20px 0 15px 0" : isTablet ? "30px 0 20px 0" : "40px 0 25px 0",
|
||||
padding: isTablet ? "0 12px" : isDesktop ? "0 16px" : "0"
|
||||
}}
|
||||
>
|
||||
<Skeleton.Input
|
||||
active
|
||||
size="large"
|
||||
style={{
|
||||
width: isMobile ? 120 : isTablet ? 160 : 200,
|
||||
height: isMobile ? 24 : isTablet ? 28 : 32,
|
||||
}}
|
||||
/>
|
||||
</div> */}
|
||||
|
||||
<div className={`${styles.menuItemsGrid} ${getGridClass()}`}>
|
||||
{Array.from({
|
||||
length:
|
||||
variant === "minimal"
|
||||
? Math.min(itemCount, 4)
|
||||
: itemCount,
|
||||
}).map((_, itemIndex) => (
|
||||
<Card
|
||||
key={itemIndex}
|
||||
className="responsive-card"
|
||||
style={{ borderRadius: 8 }}
|
||||
styles={{
|
||||
body: {
|
||||
...getMenuItemCardStyle(),
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
justifyContent: "space-between",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-around",
|
||||
gap: isMobile ? 12 : 16,
|
||||
position: "relative",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
lineHeight: 1,
|
||||
height: "100%",
|
||||
flex: 1,
|
||||
}}
|
||||
>
|
||||
{/* Item Name Skeleton */}
|
||||
<Skeleton.Input
|
||||
active
|
||||
style={{
|
||||
marginBottom: isMobile ? 8 : isTablet ? 16 : 20,
|
||||
height: isMobile ? 16 : isTablet ? 18 : 20,
|
||||
width: "80%",
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Item Description Skeleton */}
|
||||
<Skeleton
|
||||
active
|
||||
paragraph={{
|
||||
rows: isMobile ? 1 : 2,
|
||||
width: ["100%", "90%", "70%"],
|
||||
}}
|
||||
style={{
|
||||
marginBottom: isMobile ? 8 : isTablet ? 16 : 20,
|
||||
}}
|
||||
/>
|
||||
{/* Action Icons Skeleton */}
|
||||
</div>
|
||||
|
||||
<div style={{ position: "relative" }}>
|
||||
{/* Item Image Skeleton */}
|
||||
<Skeleton.Image
|
||||
active
|
||||
style={{
|
||||
...getMenuItemImageStyle(),
|
||||
}}
|
||||
/>{" "}
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default MenuSkeleton;
|
||||
276
src/pages/menu/components/ProductCard/ProductCard.tsx
Normal file
276
src/pages/menu/components/ProductCard/ProductCard.tsx
Normal file
@@ -0,0 +1,276 @@
|
||||
import { PlusOutlined } from "@ant-design/icons";
|
||||
import { Badge, Button, Card, Grid } from "antd";
|
||||
import ArabicPrice from "components/ArabicPrice";
|
||||
import HeartIcon from "components/Icons/HeartIcon";
|
||||
import ImageWithFallback from "components/ImageWithFallback";
|
||||
import { ItemDescriptionIcons } from "components/ItemDescriptionIcons/ItemDescriptionIcons";
|
||||
import ProText from "components/ProText";
|
||||
import { addItem, selectCartItems } from "features/order/orderSlice";
|
||||
import { useCallback } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Link, useParams } from "react-router-dom";
|
||||
import { useAppDispatch, useAppSelector } from "redux/hooks";
|
||||
import { colors } from "ThemeConstants";
|
||||
import { Product } from "utils/types/appTypes";
|
||||
import styles from "../MenuList/MenuList.module.css";
|
||||
|
||||
const { useBreakpoint } = Grid;
|
||||
|
||||
interface ProductCardProps {
|
||||
products: Product[];
|
||||
}
|
||||
|
||||
export function ProductCard({ products }: ProductCardProps) {
|
||||
const { id } = useParams();
|
||||
const { isRTL, locale } = useAppSelector((state) => state.locale);
|
||||
const { sm, md } = useBreakpoint();
|
||||
const isMobile = !sm;
|
||||
const isTablet = sm && !md;
|
||||
const { themeName } = useAppSelector((state) => state.theme);
|
||||
const dispatch = useAppDispatch();
|
||||
const cartItems = useAppSelector(selectCartItems);
|
||||
const getItemQuantity = (id: number | string) => {
|
||||
const item = cartItems.find((i) => i.id === id);
|
||||
return item ? item.quantity : 0;
|
||||
};
|
||||
const { t } = useTranslation();
|
||||
|
||||
// Memoized handlers for better performance
|
||||
const handleQuickAdd = useCallback(
|
||||
(item: Product) => {
|
||||
dispatch(
|
||||
addItem({
|
||||
item: {
|
||||
id: item.id,
|
||||
name: item.name,
|
||||
price: item.price,
|
||||
image: item.image,
|
||||
description: item.description,
|
||||
variant: "None",
|
||||
extras: [],
|
||||
extrasgroup: [],
|
||||
},
|
||||
quantity: 1,
|
||||
})
|
||||
);
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
const getMenuItemCardStyle = useCallback(
|
||||
(itemDescription: string) => {
|
||||
if (isMobile) {
|
||||
return {
|
||||
height: itemDescription?.length > 0 ? 160 : 130,
|
||||
padding: "12px 12px 12px 16px",
|
||||
};
|
||||
} else if (isTablet) {
|
||||
return {
|
||||
height: 160,
|
||||
padding: "16px",
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
height: 180,
|
||||
padding: "20px",
|
||||
};
|
||||
}
|
||||
},
|
||||
[isMobile, isTablet]
|
||||
);
|
||||
|
||||
const getMenuItemImageStyle = useCallback(() => {
|
||||
if (isMobile) {
|
||||
return {
|
||||
width: 90,
|
||||
height: 95,
|
||||
};
|
||||
} else if (isTablet) {
|
||||
return {
|
||||
width: 120,
|
||||
height: 120,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
width: 140,
|
||||
height: 140,
|
||||
};
|
||||
}
|
||||
}, [isMobile, isTablet]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{products?.map((item) => (
|
||||
<Link
|
||||
to={`/${id}/product/${item.id}`}
|
||||
key={item.id}
|
||||
className={styles.productLink}
|
||||
>
|
||||
<Card
|
||||
key={item.id}
|
||||
styles={{
|
||||
body: {
|
||||
...getMenuItemCardStyle(item.description),
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
justifyContent: "space-between",
|
||||
borderRadius: 16,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
height: "100%",
|
||||
gap: isMobile ? 10 : 16,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
lineHeight: 1,
|
||||
height: "100%",
|
||||
flex: 1,
|
||||
}}
|
||||
>
|
||||
<ProText
|
||||
style={{
|
||||
margin: 0,
|
||||
marginBottom: isMobile ? 8 : isTablet ? 16 : 20,
|
||||
display: "inline-block",
|
||||
fontSize: isMobile ? "1rem" : isTablet ? 16 : 18,
|
||||
fontWeight: 600,
|
||||
letterSpacing: "-0.01em",
|
||||
lineHeight: 1.2,
|
||||
}}
|
||||
>
|
||||
{locale == "ar" ? item.nameOther : item.name}
|
||||
</ProText>
|
||||
<ProText
|
||||
type="secondary"
|
||||
className={styles.itemDescription}
|
||||
style={{
|
||||
display: "-webkit-box",
|
||||
WebkitBoxOrient: "vertical",
|
||||
WebkitLineClamp: isMobile ? 2 : 4,
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
wordWrap: "break-word",
|
||||
overflowWrap: "break-word",
|
||||
lineHeight: "1.5rem",
|
||||
maxHeight: isMobile ? "3em" : "6.6em",
|
||||
fontSize: isMobile ? "1rem" : isTablet ? 16 : 18,
|
||||
letterSpacing: "0.01em",
|
||||
}}
|
||||
>
|
||||
{item.description}
|
||||
</ProText>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
justifyContent: "start",
|
||||
gap: isMobile ? 16 : 24,
|
||||
position: "absolute",
|
||||
bottom: isMobile ? 16 : 20,
|
||||
[isRTL ? "right" : "left"]: isMobile ? 16 : 20,
|
||||
}}
|
||||
>
|
||||
<ArabicPrice
|
||||
price={item.price}
|
||||
strong
|
||||
style={{
|
||||
fontSize: isMobile ? "1rem" : isTablet ? 18 : 22,
|
||||
fontWeight: 700,
|
||||
color: themeName === "dark" ? "#FFC600" : "#1a1a1a",
|
||||
}}
|
||||
/>
|
||||
<ItemDescriptionIcons className={styles.itemDescriptionIcons} />
|
||||
</div>
|
||||
<div style={{ position: "relative" }}>
|
||||
<ImageWithFallback
|
||||
src={item.image_small || ""}
|
||||
alt={item.name}
|
||||
fallbackSrc="/default.png"
|
||||
className={`${styles.popularMenuItemImage} ${
|
||||
isMobile
|
||||
? styles.popularMenuItemImageMobile
|
||||
: isTablet
|
||||
? styles.popularMenuItemImageTablet
|
||||
: styles.popularMenuItemImageDesktop
|
||||
}`}
|
||||
{...getMenuItemImageStyle()}
|
||||
/>
|
||||
<Button
|
||||
className={styles.heartButton}
|
||||
icon={<HeartIcon />}
|
||||
style={{ width: 24, height: 24 }}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
console.log("heart");
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
shape="round"
|
||||
title="add"
|
||||
iconPosition="start"
|
||||
icon={
|
||||
<PlusOutlined
|
||||
title="add"
|
||||
style={{
|
||||
position: "relative",
|
||||
top: "-1px",
|
||||
}}
|
||||
/>
|
||||
}
|
||||
size={isMobile ? "small" : "middle"}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
handleQuickAdd(item);
|
||||
}}
|
||||
style={{
|
||||
position: "absolute",
|
||||
bottom: -10,
|
||||
[isRTL ? "right" : "left"]: isMobile ? "5%" : "15%",
|
||||
zIndex: 1,
|
||||
width: isMobile ? 82 : isTablet ? 90 : 100,
|
||||
height: isMobile ? 32 : isTablet ? 40 : 44,
|
||||
fontSize: isMobile ? "1rem" : isTablet ? 16 : 18,
|
||||
fontWeight: 600,
|
||||
border: 0,
|
||||
color: themeName === "light" ? "#333333" : "#FFFFFF",
|
||||
boxShadow:
|
||||
themeName === "light"
|
||||
? "0 2px 0 rgba(0,0,0,0.02)"
|
||||
: "0 2px 0 #6b6b6b",
|
||||
}}
|
||||
>
|
||||
{t("common.add")}
|
||||
</Button>
|
||||
{/* Cart quantity badge - shows current quantity in cart */}
|
||||
{getItemQuantity(item.id) > 0 && (
|
||||
<Badge
|
||||
count={getItemQuantity(item.id)}
|
||||
className={
|
||||
styles.cartBadge +
|
||||
" " +
|
||||
(isRTL ? styles.cartBadgeRTL : styles.cartBadgeLTR)
|
||||
}
|
||||
style={{
|
||||
backgroundColor: colors.primary,
|
||||
}}
|
||||
title={`${getItemQuantity(item.id)} in cart`}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</Link>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
74
src/pages/menu/components/ResponsiveServices.tsx
Normal file
74
src/pages/menu/components/ResponsiveServices.tsx
Normal file
@@ -0,0 +1,74 @@
|
||||
"use client";
|
||||
|
||||
import { Button, Grid } from "antd";
|
||||
import DineInIcon from "components/Icons/DineInIcon";
|
||||
import DownIcon from "components/Icons/DownIcon";
|
||||
import PickupIcon from "components/Icons/PickupIcon";
|
||||
import styles from "../menu.module.css";
|
||||
|
||||
interface ResponsiveServicesProps {
|
||||
orderType: string;
|
||||
translations: {
|
||||
common: {
|
||||
dineIn: string;
|
||||
pickup: string;
|
||||
more: string;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
const { useBreakpoint } = Grid;
|
||||
|
||||
export default function ResponsiveServices({ orderType, translations }: ResponsiveServicesProps) {
|
||||
const { xs } = useBreakpoint();
|
||||
|
||||
// Hide pickup service if screen width is less than 400px (insufficient for 3 services)
|
||||
const shouldHidePickup = xs;
|
||||
|
||||
return (
|
||||
<div className={styles.services}>
|
||||
<Button
|
||||
className={
|
||||
orderType == "dine-in"
|
||||
? styles.activeServiceButton
|
||||
: styles.serviceButton
|
||||
}
|
||||
icon={
|
||||
<div className={styles.dineInIcon}>
|
||||
<DineInIcon className={`${styles.icon}`} />
|
||||
</div>
|
||||
}
|
||||
>
|
||||
{translations.common.dineIn}
|
||||
</Button>
|
||||
|
||||
{!shouldHidePickup && (
|
||||
<Button
|
||||
className={
|
||||
orderType == "pickup"
|
||||
? styles.activeServiceButton
|
||||
: styles.serviceButton
|
||||
}
|
||||
icon={
|
||||
<div className={styles.pickupIcon}>
|
||||
<PickupIcon className={`${styles.pickupIcon} ${styles.icon}`} />
|
||||
</div>
|
||||
}
|
||||
>
|
||||
{translations.common.pickup}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<Button
|
||||
className={
|
||||
orderType == "more"
|
||||
? styles.activeServiceButton
|
||||
: styles.serviceButton
|
||||
}
|
||||
>
|
||||
{translations.common.more}{" "}
|
||||
<DownIcon className={`${styles.downIcon} ${styles.icon}`} />
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
97
src/pages/menu/components/ScrollEventHandler.tsx
Normal file
97
src/pages/menu/components/ScrollEventHandler.tsx
Normal file
@@ -0,0 +1,97 @@
|
||||
import { useScrollHandler } from "contexts/ScrollHandlerContext";
|
||||
import { useEffect, useRef } from "react";
|
||||
|
||||
export default function ScrollEventHandler() {
|
||||
const {
|
||||
setShowScrollTop,
|
||||
setIsCategoriesSticky,
|
||||
categoriesContainerRef,
|
||||
categoryRefs,
|
||||
activeCategory,
|
||||
setActiveCategory
|
||||
} = useScrollHandler();
|
||||
|
||||
const hasSetInitialCategory = useRef(false);
|
||||
|
||||
// Set initial active category when categories are available
|
||||
useEffect(() => {
|
||||
if (categoryRefs.current && Object.keys(categoryRefs.current).length > 0 && !hasSetInitialCategory.current) {
|
||||
// Set the first category as active initially
|
||||
const firstCategoryId = parseInt(Object.keys(categoryRefs.current)[0]);
|
||||
setActiveCategory(firstCategoryId);
|
||||
hasSetInitialCategory.current = true;
|
||||
}
|
||||
}, [categoryRefs, setActiveCategory]);
|
||||
|
||||
useEffect(() => {
|
||||
const handleScroll = () => {
|
||||
const scrollTop =
|
||||
window.pageYOffset || document.documentElement.scrollTop;
|
||||
setShowScrollTop(scrollTop > 300); // Show button after scrolling 300px
|
||||
|
||||
// Check if we should make categories sticky
|
||||
if (categoriesContainerRef.current) {
|
||||
// Get the original position of the categories container
|
||||
const originalTop = categoriesContainerRef.current.offsetTop;
|
||||
|
||||
// Only make sticky when we've scrolled past the original position
|
||||
// and return to normal when we scroll back above it
|
||||
// Add a small threshold (10px) to prevent flickering
|
||||
const shouldBeSticky = scrollTop > originalTop + 10;
|
||||
setIsCategoriesSticky(shouldBeSticky);
|
||||
}
|
||||
|
||||
// Find the most visible category based on scroll position
|
||||
if (categoryRefs.current) {
|
||||
let mostVisibleCategory: { id: number; visibility: number } | null = null;
|
||||
|
||||
Object.entries(categoryRefs.current).forEach(([categoryId, element]) => {
|
||||
if (element) {
|
||||
const rect = element.getBoundingClientRect();
|
||||
const viewportHeight = window.innerHeight;
|
||||
|
||||
// Calculate visibility ratio
|
||||
const elementTop = rect.top;
|
||||
const elementBottom = rect.bottom;
|
||||
const elementHeight = rect.height;
|
||||
|
||||
// Calculate how much of the element is visible
|
||||
let visibleHeight = 0;
|
||||
if (elementTop >= 0 && elementBottom <= viewportHeight) {
|
||||
// Element is fully visible
|
||||
visibleHeight = elementHeight;
|
||||
} else if (elementTop < 0 && elementBottom > 0) {
|
||||
// Element is partially visible from top
|
||||
visibleHeight = elementBottom;
|
||||
} else if (elementTop < viewportHeight && elementBottom > viewportHeight) {
|
||||
// Element is partially visible from bottom
|
||||
visibleHeight = viewportHeight - elementTop;
|
||||
}
|
||||
|
||||
const visibility = visibleHeight / elementHeight;
|
||||
|
||||
// Only consider elements that are at least 30% visible
|
||||
if (visibility > 0.3) {
|
||||
if (!mostVisibleCategory || visibility > mostVisibleCategory.visibility) {
|
||||
mostVisibleCategory = {
|
||||
id: parseInt(categoryId),
|
||||
visibility
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Update active category if we found a visible one
|
||||
if (mostVisibleCategory) {
|
||||
setActiveCategory((mostVisibleCategory as { id: number; visibility: number }).id);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("scroll", handleScroll);
|
||||
return () => window.removeEventListener("scroll", handleScroll);
|
||||
}, [categoriesContainerRef, setIsCategoriesSticky, setShowScrollTop, categoryRefs, setActiveCategory, activeCategory]);
|
||||
|
||||
return null;
|
||||
}
|
||||
23
src/pages/menu/components/SearchButton.tsx
Normal file
23
src/pages/menu/components/SearchButton.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
import { Button } from "antd";
|
||||
import SearchIcon from "components/Icons/SearchIcon";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import styles from "../menu.module.css";
|
||||
|
||||
export default function SearchButton() {
|
||||
const navigate = useNavigate();
|
||||
const { id } = useParams();
|
||||
|
||||
return (
|
||||
<div className={styles.searchButtonContainer}>
|
||||
<Button
|
||||
className={styles.searchButton}
|
||||
icon={<SearchIcon />}
|
||||
onClick={() =>
|
||||
// router.push(`?orderType=${orderType}&search=true`)
|
||||
// setSelectedClientRoute("search")
|
||||
navigate(`/${id}/search`)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
39
src/pages/menu/helper.ts
Normal file
39
src/pages/menu/helper.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
const menuParser = (data: any) => {
|
||||
// Transform the data to match the expected format
|
||||
let products: any[] = [];
|
||||
(data.result?.menu || []).forEach((m: any) => {
|
||||
if (!m?.items?.data) return;
|
||||
|
||||
products = [
|
||||
...m.items.data.map((i: any) => ({
|
||||
...i,
|
||||
categoryId: m.caregory?.id || '',
|
||||
category_name: m.caregory?.name || '',
|
||||
is_available: true,
|
||||
nameAR: i.name,
|
||||
descriptionAR: i.description,
|
||||
category_nameAR: m.caregory?.name || '',
|
||||
is_featured: false,
|
||||
order: 0,
|
||||
created_at: new Date().toISOString(),
|
||||
updated_at: new Date().toISOString(),
|
||||
ingredients: [],
|
||||
nutritionalInfo: {
|
||||
calories: 0,
|
||||
protein: 0,
|
||||
carbs: 0,
|
||||
fat: 0,
|
||||
},
|
||||
})),
|
||||
...products,
|
||||
];
|
||||
});
|
||||
return {
|
||||
products,
|
||||
categories: data.result.menu
|
||||
.filter((m: any) => m.items.data.length > 0)
|
||||
.map((m: any) => m.caregory),
|
||||
};
|
||||
};
|
||||
|
||||
export default menuParser;
|
||||
9
src/pages/menu/loading.tsx
Normal file
9
src/pages/menu/loading.tsx
Normal file
@@ -0,0 +1,9 @@
|
||||
import LoadingSpinner from "components/LoadingSpinner";
|
||||
|
||||
const MenuLoading = () => (
|
||||
<>
|
||||
<LoadingSpinner />
|
||||
</>
|
||||
);
|
||||
|
||||
export default MenuLoading;
|
||||
651
src/pages/menu/menu.module.css
Normal file
651
src/pages/menu/menu.module.css
Normal file
@@ -0,0 +1,651 @@
|
||||
.menuContainer {
|
||||
overflow: auto;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
.itemDescription {
|
||||
font-size: 14px !important;
|
||||
transition: color 0.3s ease;
|
||||
}
|
||||
|
||||
.cover {
|
||||
width: 100%;
|
||||
height: "auto";
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
/* .restaurantHeader {
|
||||
margin-bottom: 24px;
|
||||
} */
|
||||
|
||||
.leftShape {
|
||||
position: absolute;
|
||||
top: 133px;
|
||||
width: 47px;
|
||||
height: 50px;
|
||||
left: -11px;
|
||||
background-color: var(--background); /* Color of the shape */
|
||||
clip-path: path("M 0 53 Q 50 50, 50 0 Q 50 50, 100 100 L 0 100 Z");
|
||||
}
|
||||
|
||||
:global(.darkApp) .leftShape {
|
||||
background-color: var(--background);
|
||||
}
|
||||
|
||||
.rightShape {
|
||||
position: absolute;
|
||||
top: 133px;
|
||||
width: 47px;
|
||||
height: 50px;
|
||||
left: 102px;
|
||||
background-color: var(--background); /* Color of the shape */
|
||||
clip-path: path("M 0 53 Q 50 50, 50 0 Q 50 50, 100 100 L 0 100 Z");
|
||||
transform: scale(-1, 1); /* Mirror the shape on the Y-axis */
|
||||
}
|
||||
|
||||
:global(.darkApp) .rightShape {
|
||||
background-color: var(--background);
|
||||
}
|
||||
|
||||
.logo {
|
||||
position: absolute;
|
||||
left: 33px;
|
||||
top: -64px;
|
||||
border-radius: 50%;
|
||||
/* box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); */
|
||||
z-index: 10;
|
||||
border: 3px solid var(--background);
|
||||
width: 72px !important;
|
||||
height: 72px !important;
|
||||
}
|
||||
|
||||
:global(.darkApp) .logo {
|
||||
border-color: var(--background);
|
||||
}
|
||||
|
||||
.services {
|
||||
height: 36px;
|
||||
position: absolute;
|
||||
left: 105px;
|
||||
top: 163px;
|
||||
border-radius: 154px;
|
||||
background-color: #fff;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
padding: 6px 9px;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
z-index: 999;
|
||||
}
|
||||
|
||||
.serviceButton {
|
||||
height: 24px;
|
||||
color: #99a2ae;
|
||||
background-color: #f7f7f7;
|
||||
border: none;
|
||||
padding: 8px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.activeServiceButton {
|
||||
height: 24px;
|
||||
border: none;
|
||||
color: var(--primary);
|
||||
background-color: rgba(255, 183, 0, 0.12);
|
||||
font-size: 16px;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.activeServiceButton path {
|
||||
fill: var(--primary);
|
||||
}
|
||||
|
||||
.logo[dir="rtl"] {
|
||||
right: 20px;
|
||||
left: auto;
|
||||
}
|
||||
|
||||
/* Enhanced responsive item description */
|
||||
@media (min-width: 769px) and (max-width: 1024px) {
|
||||
.itemDescription {
|
||||
font-size: 14px !important;
|
||||
}
|
||||
/* .logo {
|
||||
display: none;
|
||||
} */
|
||||
|
||||
.cover {
|
||||
width: 100%;
|
||||
height: "auto";
|
||||
object-fit: cover;
|
||||
}
|
||||
.restaurantHeader {
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1025px) {
|
||||
.itemDescription {
|
||||
font-size: 16px !important;
|
||||
}
|
||||
/* .logo {
|
||||
display: none !important;
|
||||
} */
|
||||
.restaurantHeader {
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Responsive adjustments */
|
||||
@media (max-width: 768px) {
|
||||
}
|
||||
|
||||
@media (min-width: 769px) and (max-width: 1024px) {
|
||||
}
|
||||
|
||||
.pageContainer {
|
||||
padding: 0;
|
||||
transition: all 0.3s ease;
|
||||
background-color: "#F7F7F7"
|
||||
}
|
||||
|
||||
/* Enhanced responsive page container */
|
||||
@media (min-width: 769px) and (max-width: 1024px) {
|
||||
.pageContainer {
|
||||
padding: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1025px) {
|
||||
.pageContainer {
|
||||
padding: 32px;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
}
|
||||
|
||||
/* Sidebar state adjustments */
|
||||
.sidebarCollapsed .pageContainer {
|
||||
margin-left: 80px;
|
||||
}
|
||||
|
||||
.sidebarExpanded .pageContainer {
|
||||
margin-left: 200px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.sidebarCollapsed .pageContainer,
|
||||
.sidebarExpanded .pageContainer {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
:global(.darkApp) .restaurantHeader path {
|
||||
fill: none !important;
|
||||
stroke: #ffffff !important;
|
||||
}
|
||||
|
||||
/* Enhanced Dark theme styles */
|
||||
:global(.darkApp) .itemName {
|
||||
color: rgba(255, 255, 255, 0.95) !important;
|
||||
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
|
||||
transition: color 0.3s ease;
|
||||
}
|
||||
|
||||
:global(.darkApp) .itemName:hover {
|
||||
color: var(--primary) !important;
|
||||
}
|
||||
|
||||
:global(.darkApp) .itemDescription {
|
||||
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
:global(.darkApp) .itemPrice {
|
||||
color: var(--primary) !important;
|
||||
font-weight: 600;
|
||||
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
:global(.darkApp) .categoryTab {
|
||||
color: rgba(255, 255, 255, 0.75) !important;
|
||||
background-color: rgba(42, 42, 42, 0.8) !important;
|
||||
border-color: rgba(255, 255, 255, 0.1) !important;
|
||||
backdrop-filter: blur(12px);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
:global(.darkApp) .categoryTab:hover {
|
||||
color: var(--primary) !important;
|
||||
background-color: rgba(54, 54, 54, 0.9) !important;
|
||||
border-color: rgba(255, 198, 0, 0.3) !important;
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
:global(.darkApp) .categoryTab.active {
|
||||
color: var(--primary) !important;
|
||||
background-color: rgba(255, 198, 0, 0.1) !important;
|
||||
border-bottom-color: var(--primary) !important;
|
||||
box-shadow: 0 2px 8px rgba(255, 198, 0, 0.2);
|
||||
}
|
||||
|
||||
/* Enhanced dark theme for page container */
|
||||
:global(.darkApp) .pageContainer {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
:global(.darkApp) .pageContainer h1,
|
||||
:global(.darkApp) .pageContainer h2,
|
||||
:global(.darkApp) .pageContainer h3,
|
||||
:global(.darkApp) .pageContainer h4,
|
||||
:global(.darkApp) .pageContainer h5,
|
||||
:global(.darkApp) .pageContainer h6 {
|
||||
color: #ffffff;
|
||||
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
/* Dark theme for menu items */
|
||||
:global(.darkApp) .menuItem {
|
||||
background-color: #181818 !important;
|
||||
border-color: #363636 !important;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
:global(.darkApp) .menuItem:hover {
|
||||
background-color: #363636 !important;
|
||||
border-color: #424242 !important;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
/* Dark theme for restaurant header */
|
||||
|
||||
:global(.darkApp) .restaurantLogo {
|
||||
border-color: rgba(255, 198, 0, 0.3) !important;
|
||||
}
|
||||
|
||||
/* Dark theme for navigation buttons */
|
||||
:global(.darkApp) .navButton {
|
||||
background-color: rgba(255, 255, 255, 0.9) !important;
|
||||
border-color: rgba(255, 255, 255, 0.2) !important;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
:global(.darkApp) .navButton:hover {
|
||||
background-color: #ffffff !important;
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
/* Dark theme for search functionality */
|
||||
:global(.darkApp) .searchContainer {
|
||||
background-color: rgba(42, 42, 42, 0.8);
|
||||
border-color: rgba(255, 255, 255, 0.1);
|
||||
backdrop-filter: blur(12px);
|
||||
}
|
||||
|
||||
:global(.darkApp) .searchInput {
|
||||
background-color: rgba(54, 54, 54, 0.8) !important;
|
||||
border-color: rgba(255, 255, 255, 0.1) !important;
|
||||
color: #ffffff !important;
|
||||
}
|
||||
|
||||
:global(.darkApp) .searchInput::placeholder {
|
||||
color: rgba(255, 255, 255, 0.5) !important;
|
||||
}
|
||||
|
||||
/* Dark theme for ratings and reviews */
|
||||
:global(.darkApp) .ratingContainer {
|
||||
border-color: rgba(255, 255, 255, 0.1);
|
||||
backdrop-filter: blur(8px);
|
||||
}
|
||||
|
||||
:global(.darkApp) .ratingText {
|
||||
color: #b0b0b0;
|
||||
}
|
||||
|
||||
:global(.darkApp) .ratingCount {
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
}
|
||||
|
||||
/* Dark theme for product cards */
|
||||
:global(.darkApp) .productCard {
|
||||
background-color: #181818 !important;
|
||||
border-color: #363636 !important;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
:global(.darkApp) .productCard:hover {
|
||||
background-color: #363636 !important;
|
||||
border-color: #424242 !important;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
/* Dark theme for product images */
|
||||
:global(.darkApp) .productImage {
|
||||
border-color: rgba(255, 255, 255, 0.1);
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
/* Dark theme for add to cart buttons */
|
||||
:global(.darkApp) .addToCartButton {
|
||||
background-color: var(--primary) !important;
|
||||
border-color: var(--primary) !important;
|
||||
color: #000000 !important;
|
||||
font-weight: 600;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
:global(.darkApp) .addToCartButton:hover {
|
||||
background-color: #ffd633 !important;
|
||||
border-color: #ffd633 !important;
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 8px rgba(255, 198, 0, 0.3);
|
||||
}
|
||||
|
||||
/* Dark theme for quantity controls */
|
||||
:global(.darkApp) .quantityControl {
|
||||
background-color: rgba(42, 42, 42, 0.8);
|
||||
border-color: rgba(255, 255, 255, 0.1);
|
||||
backdrop-filter: blur(8px);
|
||||
}
|
||||
|
||||
:global(.darkApp) .quantityButton {
|
||||
background-color: rgba(54, 54, 54, 0.8) !important;
|
||||
border-color: rgba(255, 255, 255, 0.1) !important;
|
||||
color: #ffffff !important;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
:global(.darkApp) .quantityButton:hover {
|
||||
background-color: rgba(66, 66, 66, 0.9) !important;
|
||||
border-color: var(--primary) !important;
|
||||
}
|
||||
|
||||
:global(.darkApp) .quantityInput {
|
||||
background-color: rgba(42, 42, 42, 0.8) !important;
|
||||
border-color: rgba(255, 255, 255, 0.1) !important;
|
||||
color: #ffffff !important;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
:global(.darkApp) .services {
|
||||
color: #ffffff !important;
|
||||
background-color: #000000 !important;
|
||||
border: none;
|
||||
}
|
||||
|
||||
:global(.darkApp) .serviceButton {
|
||||
color: #ffffff !important;
|
||||
background-color: rgba(42, 42, 42, 0.8) !important;
|
||||
border: none;
|
||||
border-radius: 888px;
|
||||
}
|
||||
|
||||
:global(.darkApp) .activeServiceButton {
|
||||
color: var(--primary) !important;
|
||||
background-color: rgba(42, 42, 42, 0.8) !important;
|
||||
border: none;
|
||||
border-radius: 888px;
|
||||
}
|
||||
|
||||
:global(.darkApp) .activeServiceButton path {
|
||||
stroke: var(--primary) !important;
|
||||
}
|
||||
|
||||
/* Smooth transitions for all elements */
|
||||
.pageContainer *,
|
||||
.menuItem *,
|
||||
.productCard *,
|
||||
.categoryTab * {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
/* Enhanced responsive restaurant header */
|
||||
.restaurantHeader {
|
||||
position: relative;
|
||||
/* overflow: hidden; */
|
||||
border-radius: 0 0 16px 16px;
|
||||
}
|
||||
|
||||
@media (min-width: 769px) and (max-width: 1024px) {
|
||||
.restaurantHeader {
|
||||
border-radius: 0 0 20px 20px;
|
||||
/* box-shadow: 0 6px 16px rgba(0, 0, 0, 0.15); */
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1025px) {
|
||||
.restaurantHeader {
|
||||
border-radius: 0 0 24px 24px;
|
||||
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
}
|
||||
|
||||
/* Enhanced responsive restaurant logo */
|
||||
/* .restaurantLogo {
|
||||
transition: all 0.3s ease;
|
||||
} */
|
||||
|
||||
@media (min-width: 769px) and (max-width: 1024px) {
|
||||
.restaurantLogo {
|
||||
width: 96px !important;
|
||||
height: 96px !important;
|
||||
border-radius: 12px !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1025px) {
|
||||
.restaurantLogo {
|
||||
width: 120px !important;
|
||||
height: 120px !important;
|
||||
border-radius: 16px !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Enhanced responsive navigation buttons */
|
||||
.navButton {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
@media (min-width: 769px) and (max-width: 1024px) {
|
||||
.navButton {
|
||||
width: 40px !important;
|
||||
height: 40px !important;
|
||||
border-radius: 12px !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1025px) {
|
||||
.navButton {
|
||||
width: 48px !important;
|
||||
height: 48px !important;
|
||||
border-radius: 16px !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Enhanced responsive animations */
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
.pageContainer {
|
||||
animation: fadeInUp 0.6s ease-out;
|
||||
}
|
||||
|
||||
/* .restaurantHeader {
|
||||
animation: fadeInDown 0.8s ease-out;
|
||||
} */
|
||||
}
|
||||
|
||||
@keyframes fadeInUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadeInDown {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* Enhanced responsive focus states */
|
||||
.pageContainer:focus,
|
||||
.restaurantHeader:focus,
|
||||
.navButton:focus {
|
||||
outline: 2px solid var(--primary);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.pageContainer:focus,
|
||||
.restaurantHeader:focus,
|
||||
.navButton:focus {
|
||||
outline-offset: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Enhanced responsive print styles */
|
||||
@media print {
|
||||
.pageContainer {
|
||||
background-color: white !important;
|
||||
color: black !important;
|
||||
}
|
||||
|
||||
.restaurantHeader {
|
||||
box-shadow: none !important;
|
||||
border: 1px solid #ccc !important;
|
||||
}
|
||||
|
||||
.navButton {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
.dineInIcon {
|
||||
position: relative;
|
||||
top: 2px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.pickupIcon {
|
||||
position: relative;
|
||||
top: 2px;
|
||||
margin-bottom: 3px;
|
||||
}
|
||||
|
||||
.icon {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
}
|
||||
|
||||
.downIcon {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
}
|
||||
|
||||
.backButtonContainer {
|
||||
position: absolute;
|
||||
z-index: 999;
|
||||
left: 20px;
|
||||
top: 70px;
|
||||
background-color: #fff;
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.searchButtonContainer {
|
||||
position: absolute;
|
||||
z-index: 999;
|
||||
right: 20px;
|
||||
top: 70px;
|
||||
background-color: #fff;
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.searchButton {
|
||||
width: 32px !important;
|
||||
height: 32px !important;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.contentWrapper {
|
||||
max-width: 800px;
|
||||
margin: 0px auto;
|
||||
padding: 0 1rem;
|
||||
}
|
||||
|
||||
.restaurantTitle {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 0px !important;
|
||||
}
|
||||
|
||||
.restaurantDescription {
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.ratingContainer {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.ratingStar {
|
||||
color: var(--primary);
|
||||
font-size: 12px;
|
||||
position: relative;
|
||||
top: 0.5px;
|
||||
}
|
||||
|
||||
.ratingScore {
|
||||
position: relative;
|
||||
top: 3px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.ratingCount {
|
||||
position: relative;
|
||||
top: 3px;
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
/* RTL support for back button */
|
||||
.backButtonContainer[dir="rtl"] {
|
||||
left: auto;
|
||||
right: 20px;
|
||||
}
|
||||
|
||||
/* RTL support for search button */
|
||||
.searchButtonContainer[dir="rtl"] {
|
||||
right: auto;
|
||||
left: 20px;
|
||||
}
|
||||
|
||||
:global(.rtl) .dineInIcon {
|
||||
margin-bottom: 1px;
|
||||
}
|
||||
|
||||
.restaurantDescriptionSkeleton .ant-skeleton-content .ant-skeleton-paragraph{
|
||||
margin-block-start: 8px !important;
|
||||
}
|
||||
164
src/pages/menu/page.tsx
Normal file
164
src/pages/menu/page.tsx
Normal file
@@ -0,0 +1,164 @@
|
||||
import { StarFilled } from "@ant-design/icons";
|
||||
import { Grid, Image, Space } from "antd";
|
||||
import { FloatingButton } from "components/FloatingButton/FloatingButton";
|
||||
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 { useTranslation } from "react-i18next";
|
||||
import { useParams, useSearchParams } from "react-router-dom";
|
||||
import {
|
||||
useGetMenuQuery,
|
||||
useGetRestaurantDetailsQuery,
|
||||
} from "redux/api/others";
|
||||
import { useAppSelector } from "redux/hooks";
|
||||
import { default_image } from "utils/constants";
|
||||
import BackButton from "./components/BackButton";
|
||||
import { CategoriesList } from "./components/CategoriesList/CategoriesList";
|
||||
import LocalStorageHandler from "./components/LocalStorageHandler";
|
||||
import { MenuFooter } from "./components/MenuFooter/MenuFooter";
|
||||
import { MenuList } from "./components/MenuList/MenuList";
|
||||
import MenuSkeleton from "./components/MenuSkeleton/MenuSkeleton";
|
||||
import ScrollEventHandler from "./components/ScrollEventHandler";
|
||||
import SearchButton from "./components/SearchButton";
|
||||
import styles from "./menu.module.css";
|
||||
|
||||
const { useBreakpoint } = Grid;
|
||||
|
||||
function MenuPage() {
|
||||
const { id } = useParams();
|
||||
const [searchParams] = useSearchParams();
|
||||
const orderType = searchParams.get("orderType");
|
||||
const { isRTL } = useAppSelector((state) => state.locale);
|
||||
const { t } = useTranslation();
|
||||
const { data: restaurantDetails, isLoading: isLoadingRestaurant } =
|
||||
useGetRestaurantDetailsQuery(id, {
|
||||
skip: !id,
|
||||
});
|
||||
const { restaurant } = restaurantDetails || {};
|
||||
const { data: menuData, isLoading: isLoadingMenu } = useGetMenuQuery(
|
||||
restaurantDetails?.restaurant.id,
|
||||
{
|
||||
skip: !restaurantDetails?.restaurant.id,
|
||||
}
|
||||
);
|
||||
const { categoryRefs, isCategoriesSticky } = useScrollHandler();
|
||||
const { xs, md } = useBreakpoint();
|
||||
|
||||
const isLoading = isLoadingRestaurant || isLoadingMenu;
|
||||
|
||||
return (
|
||||
<>
|
||||
<LocalStorageHandler
|
||||
restaurantID={restaurant?.id || ""}
|
||||
restaurantName={restaurant?.subdomain || ""}
|
||||
orderType={orderType || ""}
|
||||
/>
|
||||
|
||||
{isLoading ? (
|
||||
<MenuSkeleton categoryCount={6} itemCount={8} variant="default" />
|
||||
) : (
|
||||
<div className={styles.menuContainer}>
|
||||
<div className={styles.restaurantHeader}>
|
||||
<ImageWithFallback
|
||||
src={restaurant?.coverm}
|
||||
fallbackSrc={default_image}
|
||||
alt={t("menu.restaurantCover")}
|
||||
className={styles.cover}
|
||||
width={"100%"}
|
||||
height={182}
|
||||
preview={false}
|
||||
loadingContainerStyle={{
|
||||
width:"100vw"
|
||||
}}
|
||||
/>
|
||||
|
||||
<Image
|
||||
src={restaurant?.logom}
|
||||
alt={t("menu.restaurantLogo")}
|
||||
className={styles.logo}
|
||||
width={"100%"}
|
||||
preview={false}
|
||||
/>
|
||||
|
||||
<div className={styles.leftShape}></div>
|
||||
<div className={styles.rightShape}></div>
|
||||
|
||||
{/* <ResponsiveServices
|
||||
orderType={orderType}
|
||||
translations={{
|
||||
common: {
|
||||
dineIn: translations.common?.dineIn || "Dine In",
|
||||
pickup: translations.common?.pickup || "Pickup",
|
||||
more: translations.common?.more || "More",
|
||||
},
|
||||
}}
|
||||
/> */}
|
||||
|
||||
<div className={styles.backButtonContainer}>
|
||||
<BackButton />
|
||||
</div>
|
||||
<SearchButton />
|
||||
</div>
|
||||
|
||||
<div className={`${styles.pageContainer}`}>
|
||||
<div className={styles.contentWrapper}>
|
||||
<ProTitle level={4} className={styles.restaurantTitle}>
|
||||
{isRTL ? restaurant?.nameAR : restaurant?.name}
|
||||
</ProTitle>
|
||||
|
||||
<div className={styles.ratingContainer}>
|
||||
<StarFilled className={styles.ratingStar} />
|
||||
<ProText className={styles.ratingScore}>4.5</ProText>
|
||||
<ProText className={styles.ratingCount}>(2567)</ProText>
|
||||
<ProText
|
||||
className={`${styles.itemDescription} ${styles.restaurantDescription} responsive-text`}
|
||||
>
|
||||
{isRTL ? restaurant?.descriptionAR : restaurant?.description}
|
||||
</ProText>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={`${styles.pageContainer}`}
|
||||
// style={{
|
||||
// maxWidth: isDesktop ? "1200px" : "100%",
|
||||
// margin: isDesktop ? "0 auto" : "0",
|
||||
// }}
|
||||
>
|
||||
<Space
|
||||
direction="vertical"
|
||||
// size={isMobile ? "middle" : isTablet ? "large" : "large"}
|
||||
style={{ width: "100%", gap: 16 }}
|
||||
>
|
||||
{/* Placeholder to prevent content jumping when categories become sticky */}
|
||||
{/* {isCategoriesSticky && (
|
||||
<div style={{ height: xs ? 95 : md ? 160 : 180 }} />
|
||||
)} */}
|
||||
|
||||
<div>
|
||||
<LoyaltyCard />
|
||||
<CategoriesList categories={menuData?.categories || []} />
|
||||
</div>
|
||||
|
||||
<MenuList
|
||||
data={menuData}
|
||||
id={id || ""}
|
||||
categoryRefs={categoryRefs}
|
||||
/>
|
||||
</Space>
|
||||
</div>
|
||||
|
||||
<MenuFooter />
|
||||
|
||||
<ScrollEventHandler />
|
||||
<FloatingButton />
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default MenuPage;
|
||||
108
src/pages/order/components/Stepper.tsx
Normal file
108
src/pages/order/components/Stepper.tsx
Normal file
@@ -0,0 +1,108 @@
|
||||
import { Col, Row } from "antd";
|
||||
import ProText from "components/ProText";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { colors } from "ThemeConstants";
|
||||
|
||||
export default function Stepper() {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<Row
|
||||
align={"middle"}
|
||||
style={{
|
||||
width: 330,
|
||||
position: "relative",
|
||||
padding: "12px",
|
||||
}}
|
||||
>
|
||||
<Col>
|
||||
<div
|
||||
style={{
|
||||
width: 12,
|
||||
height: 12,
|
||||
borderRadius: "50%",
|
||||
backgroundColor: colors.primary,
|
||||
}}
|
||||
></div>
|
||||
</Col>
|
||||
<Col flex={"auto"}>
|
||||
<div
|
||||
style={{
|
||||
height: 1.5,
|
||||
backgroundColor: colors.primary,
|
||||
margin: "0 8px",
|
||||
}}
|
||||
></div>
|
||||
</Col>
|
||||
<Col>
|
||||
<div
|
||||
style={{
|
||||
width: 12,
|
||||
height: 12,
|
||||
borderRadius: "50%",
|
||||
backgroundColor: colors.primary,
|
||||
}}
|
||||
></div>
|
||||
</Col>
|
||||
<Col flex={"auto"}>
|
||||
<div
|
||||
style={{
|
||||
height: 1.5,
|
||||
backgroundColor: colors.primary,
|
||||
margin: "0 8px",
|
||||
}}
|
||||
></div>
|
||||
</Col>
|
||||
<Col>
|
||||
{" "}
|
||||
<div
|
||||
style={{
|
||||
width: 12,
|
||||
height: 12,
|
||||
borderRadius: "50%",
|
||||
backgroundColor: "#FFF",
|
||||
border: `2px solid #BDBDBD`,
|
||||
}}
|
||||
></div>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
{" "}
|
||||
<Row
|
||||
align={"middle"}
|
||||
style={{
|
||||
width: 330,
|
||||
position: "relative",
|
||||
padding: "12px",
|
||||
}}
|
||||
>
|
||||
<Col>
|
||||
<ProText>{t("order.reserved")}</ProText>
|
||||
</Col>
|
||||
<Col flex={"auto"}></Col>
|
||||
<Col>
|
||||
<ProText>{t("order.prepare")}</ProText>
|
||||
</Col>
|
||||
<Col flex={"auto"}></Col>
|
||||
<Col>
|
||||
<ProText type="secondary">{t("order.ready")}</ProText>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
213
src/pages/order/order.module.css
Normal file
213
src/pages/order/order.module.css
Normal file
@@ -0,0 +1,213 @@
|
||||
.orderSummary :global(.ant-card-body) {
|
||||
padding: 16px !important;
|
||||
}
|
||||
|
||||
/* 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 {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.orderCard :global(.ant-card-body) {
|
||||
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;
|
||||
}
|
||||
|
||||
/* Enhanced responsive summary rows */
|
||||
@media (min-width: 769px) and (max-width: 1024px) {
|
||||
.summaryRow {
|
||||
padding: 12px 0;
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
@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 {
|
||||
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;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .orderSummary {
|
||||
background-color: #181818 !important;
|
||||
border-color: #363636 !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);
|
||||
}
|
||||
}
|
||||
|
||||
162
src/pages/order/page.tsx
Normal file
162
src/pages/order/page.tsx
Normal file
@@ -0,0 +1,162 @@
|
||||
import { Button, Card, Divider } from "antd";
|
||||
import Ads2 from "components/Ads/Ads2";
|
||||
import { CancelOrderBottomSheet } from "components/CustomBottomSheet/CancelOrderBottomSheet";
|
||||
import Fascano50X50Icon from "components/Icons/fascano/Fascano50X50Icon";
|
||||
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 ProHeader from "components/ProHeader/ProHeader";
|
||||
import ProInputCard from "components/ProInputCard/ProInputCard";
|
||||
import ProText from "components/ProText";
|
||||
import ProTitle from "components/ProTitle";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import Stepper from "./components/Stepper";
|
||||
import styles from "./order.module.css";
|
||||
|
||||
export default function OrderPage() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
// const subtotal = getTotal();
|
||||
// const tax = subtotal * 0.1; // 10% tax
|
||||
// const total = subtotal + tax;
|
||||
|
||||
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}>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
gap: "1rem",
|
||||
backgroundColor: "rgba(255, 183, 0, 0.08)",
|
||||
borderRadius: "12px",
|
||||
padding: 16,
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
type="text"
|
||||
shape="circle"
|
||||
style={{
|
||||
backgroundColor: "rgba(255, 183, 0, 0.08)",
|
||||
}}
|
||||
>
|
||||
<Fascano50X50Icon className={styles.fascanoIcon} />
|
||||
</Button>
|
||||
<div>
|
||||
<ProText style={{ fontSize: "1rem" }}>
|
||||
{t("order.yourOrderFromFascanoRestaurant")}
|
||||
</ProText>
|
||||
<br />
|
||||
<ProText type="secondary">
|
||||
<LocationIcon className={styles.locationIcon} />{" "}
|
||||
{t("order.muscat")}
|
||||
</ProText>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<OrderDishIcon className={styles.orderDishIcon} />
|
||||
|
||||
<div>
|
||||
<ProTitle
|
||||
level={5}
|
||||
style={{
|
||||
fontWeight: 600,
|
||||
fontSize: "18px",
|
||||
marginBottom: "0.75rem",
|
||||
}}
|
||||
>
|
||||
{t("order.inProgressOrder")} (1)
|
||||
</ProTitle>
|
||||
<div style={{ display: "flex", flexDirection: "row", gap: 8 }}>
|
||||
<InvoiceIcon className={styles.invoiceIcon} />
|
||||
<ProText type="secondary" style={{ fontSize: "14px" }}>
|
||||
#A54363
|
||||
</ProText>
|
||||
<TimeIcon className={styles.timeIcon} />
|
||||
<ProText type="secondary" style={{ fontSize: "14px" }}>
|
||||
ordered :- Today - 13:55 PM
|
||||
</ProText>
|
||||
</div>
|
||||
|
||||
<Divider style={{ margin: "12px 0" }} />
|
||||
|
||||
<Stepper />
|
||||
</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" }}
|
||||
>
|
||||
{[
|
||||
{ id: 1, name: "Lazord Grill - half kilo" },
|
||||
{ id: 2, name: "Lazord Grill - half kilo" },
|
||||
{ id: 3, name: "Lazord Grill - half kilo" },
|
||||
].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 />
|
||||
|
||||
<CancelOrderBottomSheet />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
160
src/pages/orders/OrdersList.tsx
Normal file
160
src/pages/orders/OrdersList.tsx
Normal file
@@ -0,0 +1,160 @@
|
||||
import { Button, Card, Divider } from "antd";
|
||||
import EmptyOrdersIcon from "components/Icons/EmptyOrdersIcon";
|
||||
import RateIcon from "components/Icons/RateIcon";
|
||||
import ReOrderIcon from "components/Icons/ReOrderIcon";
|
||||
import LoadingSpinner from "components/LoadingSpinner";
|
||||
import ProText from "components/ProText";
|
||||
import ProTitle from "components/ProTitle";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useGetOrdersQuery } from "redux/api/others";
|
||||
import { colors } from "ThemeConstants";
|
||||
import styles from "./orders.module.css";
|
||||
|
||||
// Utility function to format date
|
||||
const formatDate = (dateString: string): string => {
|
||||
const date = new Date(dateString);
|
||||
|
||||
// Format: "14 December 2022 18:45"
|
||||
const options: Intl.DateTimeFormatOptions = {
|
||||
day: "numeric",
|
||||
month: "long",
|
||||
year: "numeric",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
hour12: false,
|
||||
};
|
||||
|
||||
return date.toLocaleDateString("en-GB", options);
|
||||
};
|
||||
|
||||
export default function OrdersList() {
|
||||
const { data: orders = [], isLoading, error, refetch } = useGetOrdersQuery();
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
if (isLoading) {
|
||||
return <LoadingSpinner />;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
height: "90vh",
|
||||
}}
|
||||
>
|
||||
<ProTitle level={4}>{t("orders.errorLoadingOrders")}</ProTitle>
|
||||
<Button onClick={refetch}>{t("orders.retry")}</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{orders.length === 0 ? (
|
||||
// Empty state
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
height: "90vh",
|
||||
padding: "1rem"
|
||||
}}
|
||||
>
|
||||
<EmptyOrdersIcon />
|
||||
<ProTitle level={4}>{t("orders.noOrdersFound")}</ProTitle>
|
||||
<ProText>
|
||||
{t("orders.youHavenTPlacedAnyOrdersYet")}
|
||||
</ProText>
|
||||
</div>
|
||||
) : (
|
||||
// Orders list
|
||||
<div style={{ padding: "16px" }}>
|
||||
{orders.map((order: any) => (
|
||||
<Card
|
||||
key={order.id}
|
||||
style={{
|
||||
borderRadius: "12px",
|
||||
boxShadow: "0 2px 8px rgba(0, 0, 0, 0.1)",
|
||||
marginBottom: 12,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
justifyContent: "flex-start",
|
||||
gap: "1rem",
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
type="text"
|
||||
shape="circle"
|
||||
style={{
|
||||
backgroundColor: "rgba(255, 183, 0, 0.08)",
|
||||
}}
|
||||
>
|
||||
{order.restaurant_name[0]}
|
||||
</Button>
|
||||
<div
|
||||
style={{ display: "flex", flexDirection: "column", gap: 8 }}
|
||||
>
|
||||
<ProText style={{ fontSize: "1rem" }}>
|
||||
{order.restaurant_name}
|
||||
</ProText>
|
||||
<ProText type="secondary">
|
||||
{formatDate(order?.created_at)}
|
||||
</ProText>
|
||||
<ProText type="secondary">
|
||||
{t("orders.orderID")} {order.id}
|
||||
</ProText>
|
||||
<Button
|
||||
style={{
|
||||
borderRadius: 888,
|
||||
height: 24,
|
||||
border: "none",
|
||||
backgroundColor: colors.primary,
|
||||
color: "white",
|
||||
padding: "0 8px",
|
||||
width: "fit-content",
|
||||
}}
|
||||
>
|
||||
{order.status}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<Divider style={{ margin: "15px 0 10px 0" }} />
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-around",
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<ProText style={{ fontSize: "1rem", color: colors.primary }}>
|
||||
<RateIcon className={styles.rateIcon} />
|
||||
{t("orders.rateOrder")}
|
||||
</ProText>{" "}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<ProText style={{ fontSize: "1rem", color: colors.primary }}>
|
||||
<ReOrderIcon className={styles.reorderIcon} />
|
||||
{t("orders.reOrder")}
|
||||
</ProText>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
67
src/pages/orders/orders.module.css
Normal file
67
src/pages/orders/orders.module.css
Normal file
@@ -0,0 +1,67 @@
|
||||
.productContainer :global(.ant-radio-wrapper:last-child) {
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
.productContainer :global(.ant-radio-label) {
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
/* radio.module.css */
|
||||
.productContainer :global(.ant-radio-inner) {
|
||||
width: 24px !important;
|
||||
height: 24px !important;
|
||||
border-radius: 30px !important;
|
||||
}
|
||||
|
||||
.productContainer :global(.ant-radio) {
|
||||
width: 24px !important;
|
||||
height: 24px !important;
|
||||
}
|
||||
|
||||
.productContainer :global(.ant-radio-checked .ant-radio-inner) {
|
||||
border-radius: 30px !important;
|
||||
}
|
||||
|
||||
.productContainer :global(.ant-checkbox-wrapper:last-child) {
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
.productContainer :global(.ant-checkbox-label) {
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
/* .productContainer :global(.ant-checkbox-input) {
|
||||
margin-top: 5px !important;
|
||||
} */
|
||||
|
||||
.productContainer :global(.ant-checkbox-inner) {
|
||||
border-radius: 40px !important;
|
||||
}
|
||||
|
||||
/* CheckboxGroup.module.css */
|
||||
.productContainer :global(.ant-checkbox-inner) {
|
||||
width: 24px !important;
|
||||
height: 24px !important;
|
||||
border-radius: 30px !important;
|
||||
}
|
||||
|
||||
.productContainer :global(.ant-checkbox) {
|
||||
width: 24px !important;
|
||||
height: 24px !important;
|
||||
}
|
||||
|
||||
.productContainer :global(.ant-checkbox-checked .ant-checkbox-inner) {
|
||||
border-radius: 30px !important;
|
||||
}
|
||||
|
||||
.rateIcon{
|
||||
position: relative;
|
||||
top: 2px;
|
||||
margin:0 5px;
|
||||
}
|
||||
|
||||
.reorderIcon{
|
||||
position: relative;
|
||||
top: 2px;
|
||||
margin:0 5px;
|
||||
}
|
||||
42
src/pages/orders/page.tsx
Normal file
42
src/pages/orders/page.tsx
Normal file
@@ -0,0 +1,42 @@
|
||||
import { Button, Row } from "antd";
|
||||
import ProHeader from "components/ProHeader/ProHeader";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Link, useParams } from "react-router-dom";
|
||||
import { useAppSelector } from "redux/hooks";
|
||||
import { ProBlack2 } from "ThemeConstants";
|
||||
import OrdersList from "./OrdersList";
|
||||
|
||||
export default function OrdersPage() {
|
||||
const { t } = useTranslation();
|
||||
const { id } = useParams();
|
||||
const { themeName } = useAppSelector((state) => state.theme);
|
||||
|
||||
return (
|
||||
<>
|
||||
<ProHeader>{t("orders.title")}</ProHeader>
|
||||
<OrdersList />
|
||||
<Row
|
||||
style={{
|
||||
width: "100%",
|
||||
padding: "16px 16px 0",
|
||||
position: "fixed",
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
boxShadow: "0px -1px 3px rgba(0, 0, 0, 0.1)",
|
||||
backgroundColor: themeName === "light" ? "white" : ProBlack2,
|
||||
|
||||
}}
|
||||
>
|
||||
<Link to={`/${id}/menu`} style={{ width: "100%" }}>
|
||||
<Button
|
||||
type="primary"
|
||||
shape="round"
|
||||
style={{ width: "100%", height: 48, marginBottom: 16 }}
|
||||
>
|
||||
{t('orders.browseMenu')}
|
||||
</Button>
|
||||
</Link>
|
||||
</Row>
|
||||
</>
|
||||
);
|
||||
}
|
||||
92
src/pages/orders/types.ts
Normal file
92
src/pages/orders/types.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
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
|
||||
}
|
||||
|
||||
export interface Daum {
|
||||
id: number
|
||||
name: string
|
||||
data: string[]
|
||||
prices: string[]
|
||||
pricesNew: string[]
|
||||
nameAR: string
|
||||
dataAR: 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 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 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
|
||||
}
|
||||
|
||||
110
src/pages/otp/otp.module.css
Normal file
110
src/pages/otp/otp.module.css
Normal file
@@ -0,0 +1,110 @@
|
||||
.productContainer :global(.ant-radio-wrapper:last-child) {
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
.productContainer :global(.ant-radio-label) {
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
/* radio.module.css */
|
||||
.productContainer :global(.ant-radio-inner) {
|
||||
width: 24px !important;
|
||||
height: 24px !important;
|
||||
border-radius: 30px !important;
|
||||
}
|
||||
|
||||
.productContainer :global(.ant-radio) {
|
||||
width: 24px !important;
|
||||
height: 24px !important;
|
||||
}
|
||||
|
||||
.productContainer :global(.ant-radio-checked .ant-radio-inner) {
|
||||
border-radius: 30px !important;
|
||||
}
|
||||
|
||||
.productContainer :global(.ant-checkbox-wrapper:last-child) {
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
.productContainer :global(.ant-checkbox-label) {
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
/* .productContainer :global(.ant-checkbox-input) {
|
||||
margin-top: 5px !important;
|
||||
} */
|
||||
|
||||
.productContainer :global(.ant-checkbox-inner) {
|
||||
border-radius: 40px !important;
|
||||
}
|
||||
|
||||
/* CheckboxGroup.module.css */
|
||||
.productContainer :global(.ant-checkbox-inner) {
|
||||
width: 24px !important;
|
||||
height: 24px !important;
|
||||
border-radius: 30px !important;
|
||||
}
|
||||
|
||||
.productContainer :global(.ant-checkbox) {
|
||||
width: 24px !important;
|
||||
height: 24px !important;
|
||||
}
|
||||
|
||||
.productContainer :global(.ant-checkbox-checked .ant-checkbox-inner) {
|
||||
border-radius: 30px !important;
|
||||
}
|
||||
|
||||
.services {
|
||||
width: 100%;
|
||||
padding: 0 5px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
gap: 3px;
|
||||
}
|
||||
|
||||
.services :global(.ant-btn) {
|
||||
padding: 0 11px !important;
|
||||
}
|
||||
|
||||
.serviceButton {
|
||||
position: relative;
|
||||
top: 5px;
|
||||
height: 32px;
|
||||
color: #99a2ae;
|
||||
background-color: #f7f7f7;
|
||||
border: none;
|
||||
width: 70px !important;
|
||||
}
|
||||
|
||||
.activeServiceButton {
|
||||
position: relative;
|
||||
top: 6px;
|
||||
height: 24px;
|
||||
border: none;
|
||||
color: #ffb700;
|
||||
background-color: rgba(255, 183, 0, 0.12);
|
||||
width: 70px !important;
|
||||
}
|
||||
|
||||
:global(.darkApp) .services {
|
||||
color: #ffffff !important;
|
||||
background-color: #000000 !important;
|
||||
border: none;
|
||||
}
|
||||
|
||||
:global(.darkApp) .serviceButton {
|
||||
color: #ffffff !important;
|
||||
background-color: rgba(42, 42, 42, 0.8) !important;
|
||||
border: none;
|
||||
width: 70px !important;
|
||||
border-radius: 888px;
|
||||
}
|
||||
|
||||
:global(.darkApp) .activeServiceButton {
|
||||
color: #ffb700 !important;
|
||||
background-color: rgba(42, 42, 42, 0.8) !important;
|
||||
border: none;
|
||||
width: 70px !important;
|
||||
border-radius: 888px;
|
||||
}
|
||||
96
src/pages/otp/page.tsx
Normal file
96
src/pages/otp/page.tsx
Normal file
@@ -0,0 +1,96 @@
|
||||
import { Button, Space, message } from "antd";
|
||||
import OtpIcon from "components/Icons/otpIcon.tsx";
|
||||
import OtpInput from "components/OtpInput/OtpInput";
|
||||
import ProText from "components/ProText";
|
||||
import ProTitle from "components/ProTitle";
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import { useConfirmOtpMutation, useSendOtpMutation } from "redux/api/auth";
|
||||
|
||||
export default function OtpPage() {
|
||||
const { id } = useParams();
|
||||
const { t } = useTranslation();
|
||||
const [sendOtp, { isLoading }] = useSendOtpMutation();
|
||||
const [confirmOtp, { isLoading: isConfirmLoading }] = useConfirmOtpMutation();
|
||||
const [otp, setOtp] = useState<string>("");
|
||||
const handleOtpSave = (otp: string) => {
|
||||
try {
|
||||
setOtp(otp);
|
||||
} catch (error) {
|
||||
console.error("Failed to parse otp:", error);
|
||||
}
|
||||
};
|
||||
const navigate = useNavigate();
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
textAlign: "center",
|
||||
padding: 16,
|
||||
gap: 24,
|
||||
height: "92vh",
|
||||
}}
|
||||
>
|
||||
<OtpIcon />
|
||||
|
||||
<div>
|
||||
<ProTitle level={4}>{t("otp.verification")}</ProTitle>
|
||||
<ProText style={{ color: "#3E3E3E" }}>
|
||||
{t("otp.enterThe4DigitCodeThatSentToYourPhoneNumber")}
|
||||
</ProText>
|
||||
<br />
|
||||
<ProText style={{ color: "#3E3E3E" }}>
|
||||
{localStorage.getItem("otp")}
|
||||
</ProText>
|
||||
</div>
|
||||
<div>
|
||||
<Space size="small">
|
||||
<OtpInput
|
||||
length={5}
|
||||
onComplete={handleOtpSave}
|
||||
resendOtp={() =>
|
||||
sendOtp({
|
||||
phone: localStorage.getItem("userPhone") || "",
|
||||
})
|
||||
}
|
||||
isLoading={isLoading || isConfirmLoading}
|
||||
/>
|
||||
</Space>
|
||||
</div>
|
||||
<Button
|
||||
type="primary"
|
||||
shape="round"
|
||||
style={{
|
||||
width: "100%",
|
||||
height: 48,
|
||||
marginBottom: 16,
|
||||
color: "white",
|
||||
border: "none",
|
||||
}}
|
||||
disabled={!otp || otp.length < 5 || isConfirmLoading}
|
||||
onClick={() => {
|
||||
confirmOtp({ otp: otp, phone: localStorage.getItem("userPhone") })
|
||||
.unwrap()
|
||||
.then((response) => {
|
||||
localStorage.setItem(
|
||||
"customer",
|
||||
JSON.stringify(response.result.customer)
|
||||
);
|
||||
localStorage.setItem("token", response.result.access_token);
|
||||
message.info(t("otp.confirmOTPSuccess"));
|
||||
navigate(`/${id}/menu`);
|
||||
});
|
||||
}}
|
||||
>
|
||||
{t("otp.continue")}
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
36
src/pages/otp/types.ts
Normal file
36
src/pages/otp/types.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
export interface ConfirmOTPResponse {
|
||||
success: boolean;
|
||||
result: Result;
|
||||
message: string;
|
||||
error: any;
|
||||
}
|
||||
|
||||
export interface Result {
|
||||
access_token: string;
|
||||
token_type: string;
|
||||
expires_in: number;
|
||||
customer: Customer;
|
||||
}
|
||||
|
||||
export interface Customer {
|
||||
id: number;
|
||||
username: string;
|
||||
email: null;
|
||||
mobilenumber: string;
|
||||
device_token: null;
|
||||
password: string;
|
||||
redeem_point: string;
|
||||
customer_image: string;
|
||||
login_taken: string;
|
||||
device: string;
|
||||
social_id: string;
|
||||
otp_number: string;
|
||||
otp_status: string;
|
||||
created_datetime: string;
|
||||
user_uuid: string;
|
||||
is_deleted: number;
|
||||
thawani_customer_id: string;
|
||||
birth_date: null;
|
||||
gender: string;
|
||||
country_id: null;
|
||||
}
|
||||
62
src/pages/product/components/Extra.tsx
Normal file
62
src/pages/product/components/Extra.tsx
Normal file
@@ -0,0 +1,62 @@
|
||||
import { Divider } from "antd";
|
||||
import ProCheckboxGroups from "components/ProCheckboxGroups/ProCheckboxGroups";
|
||||
import ProText from "components/ProText";
|
||||
import { Dispatch, SetStateAction } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Extra as ExtraType } from "utils/types/appTypes";
|
||||
import styles from "../product.module.css";
|
||||
|
||||
export default function Extra({
|
||||
extrasList,
|
||||
selectedExtras,
|
||||
setSelectedExtras,
|
||||
}: {
|
||||
extrasList: ExtraType[];
|
||||
selectedExtras: string[];
|
||||
setSelectedExtras: Dispatch<SetStateAction<string[]>>;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<>
|
||||
{extrasList.length > 0 && (
|
||||
<div>
|
||||
<Divider style={{ margin: "0 0 16px 0" }} />
|
||||
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
}}
|
||||
>
|
||||
<ProText style={{ fontSize: "1.25rem" }}>
|
||||
{t("menu.youMightAlsoLike")}
|
||||
</ProText>
|
||||
|
||||
<ProText strong style={{ fontSize: "0.75rem" }}>
|
||||
{t("menu.optional")}
|
||||
</ProText>
|
||||
</div>
|
||||
|
||||
<ProText strong style={{ fontSize: "0.75rem" }}>
|
||||
{t("menu.choose1")}
|
||||
</ProText>
|
||||
|
||||
<div className={styles.productContainer}>
|
||||
<ProCheckboxGroups
|
||||
options={extrasList.map((value) => {
|
||||
return {
|
||||
value: value.id.toString(),
|
||||
label: value.name,
|
||||
price: `+${value.price}`,
|
||||
};
|
||||
})}
|
||||
value={selectedExtras}
|
||||
onChange={(values: string[]) => setSelectedExtras(values)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
117
src/pages/product/components/ExtraGroups.tsx
Normal file
117
src/pages/product/components/ExtraGroups.tsx
Normal file
@@ -0,0 +1,117 @@
|
||||
import { Divider, message } from "antd";
|
||||
import ProCheckboxGroups from "components/ProCheckboxGroups/ProCheckboxGroups";
|
||||
import ProText from "components/ProText";
|
||||
import { Dispatch, SetStateAction } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useAppSelector } from "redux/hooks";
|
||||
import { Extra4, TheExtrasGroup } from "utils/types/appTypes";
|
||||
import styles from "../product.module.css";
|
||||
|
||||
export default function ExtraGroups({
|
||||
groupsList,
|
||||
selectedExtrasByGroup,
|
||||
setSelectedExtrasByGroup,
|
||||
}: {
|
||||
groupsList: TheExtrasGroup[];
|
||||
selectedExtrasByGroup: Record<number, string[]>;
|
||||
setSelectedExtrasByGroup: Dispatch<SetStateAction<Record<number, string[]>>>;
|
||||
}) {
|
||||
const { isRTL } = useAppSelector((state) => state.locale);
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<>
|
||||
{groupsList.length > 0 && (
|
||||
<div>
|
||||
<Divider style={{ margin: "0 0 16px 0" }} />
|
||||
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
}}
|
||||
>
|
||||
<ProText style={{ fontSize: "1.25rem" }}>
|
||||
{t("menu.youMightAlsoLike")}
|
||||
</ProText>
|
||||
|
||||
<ProText strong style={{ fontSize: "0.75rem" }}>
|
||||
{t("menu.optional")}
|
||||
</ProText>
|
||||
</div>
|
||||
|
||||
{/* <ProText strong style={{ fontSize: "0.75rem" }}>
|
||||
{t("menu.choose1")}
|
||||
</ProText> */}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{groupsList?.map((group: TheExtrasGroup) => (
|
||||
<div key={group.id}>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<ProText strong style={{ fontSize: "1rem" }}>
|
||||
{isRTL ? group.nameAR : group.name}
|
||||
</ProText>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "8px",
|
||||
}}
|
||||
>
|
||||
<ProText style={{ fontSize: "0.75rem", color: "#666" }}>
|
||||
{group.force_limit_selection === 1 ? "Required" : "Optional"}
|
||||
</ProText>
|
||||
<ProText style={{ fontSize: "0.75rem", color: "#666" }}>
|
||||
({selectedExtrasByGroup[group.id]?.length || 0}/{group.limit})
|
||||
</ProText>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{group.force_limit_selection === 1 && (
|
||||
<ProText
|
||||
style={{
|
||||
fontSize: "0.75rem",
|
||||
color: "red",
|
||||
marginBottom: "8px",
|
||||
}}
|
||||
>
|
||||
* You must select exactly {group.limit} option
|
||||
{group.limit > 1 ? "s" : ""}
|
||||
</ProText>
|
||||
)}
|
||||
|
||||
<div className={styles.productContainer}>
|
||||
<ProCheckboxGroups
|
||||
options={group.extras.map((extra: Extra4) => ({
|
||||
value: extra.id.toString(),
|
||||
label: isRTL ? extra.name : extra.nameAR,
|
||||
price: `+${extra.price}`,
|
||||
}))}
|
||||
value={selectedExtrasByGroup[group.id] || []}
|
||||
onChange={(values: string[]) => {
|
||||
// Check if the new selection would exceed the limit
|
||||
if (values.length > group.limit) {
|
||||
message.error(
|
||||
`You can only select up to ${group.limit} option${group.limit > 1 ? "s" : ""} from this group.`
|
||||
);
|
||||
return;
|
||||
}
|
||||
setSelectedExtrasByGroup((prev) => ({
|
||||
...prev,
|
||||
[group.id]: values,
|
||||
}));
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
143
src/pages/product/components/ProductFooter.tsx
Normal file
143
src/pages/product/components/ProductFooter.tsx
Normal file
@@ -0,0 +1,143 @@
|
||||
import { ShoppingCartOutlined } from "@ant-design/icons";
|
||||
import { Button, Grid, message, Row } from "antd";
|
||||
import { SpecialRequestBottomSheet } from "components/CustomBottomSheet/SpecialRequestBottomSheet";
|
||||
import {
|
||||
addItem,
|
||||
selectCart,
|
||||
updateSpecialRequest,
|
||||
} from "features/order/orderSlice";
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import { useAppDispatch, useAppSelector } from "redux/hooks";
|
||||
import { colors, ProBlack2 } from "ThemeConstants";
|
||||
import { Product } from "utils/types/appTypes";
|
||||
|
||||
const { useBreakpoint } = Grid;
|
||||
|
||||
export default function ProductFooter({
|
||||
product,
|
||||
isValid = true,
|
||||
variantId,
|
||||
selectedExtras,
|
||||
selectedGroups,
|
||||
quantity,
|
||||
}: {
|
||||
product: Product;
|
||||
isValid?: boolean;
|
||||
variantId: string;
|
||||
selectedExtras: string[];
|
||||
selectedGroups: string[];
|
||||
quantity: number;
|
||||
}) {
|
||||
const { id } = useParams();
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useAppDispatch();
|
||||
const { themeName } = useAppSelector((state) => state.theme);
|
||||
const { specialRequest } = useAppSelector(selectCart);
|
||||
const [isSpecialRequestOpen, setIsSpecialRequestOpen] = useState(false);
|
||||
const { xs } = useBreakpoint();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const handleAddToCart = () => {
|
||||
if (!isValid) {
|
||||
message.error(t("menu.pleaseSelectRequiredOptions"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (product) {
|
||||
dispatch(
|
||||
addItem({
|
||||
item: {
|
||||
id: product?.id,
|
||||
name: product?.name,
|
||||
price: product?.price,
|
||||
image: product?.image,
|
||||
description: product?.description,
|
||||
variant: variantId,
|
||||
extras: selectedExtras,
|
||||
extrasgroup: selectedGroups,
|
||||
},
|
||||
quantity: quantity,
|
||||
})
|
||||
);
|
||||
// Navigate back to menu - scroll position will be restored automatically
|
||||
window.history.back();
|
||||
}
|
||||
};
|
||||
|
||||
const handleSpecialRequestSave = (value: string) => {
|
||||
dispatch(updateSpecialRequest(value));
|
||||
message.success(t("cart.specialRequest") + " " + t("common.confirm"));
|
||||
};
|
||||
|
||||
const handleSpecialRequestClose = () => {
|
||||
setIsSpecialRequestOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Row
|
||||
style={{
|
||||
width: "100%",
|
||||
padding: "16px 16px 0",
|
||||
position: "fixed",
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
height: "10vh",
|
||||
backgroundColor: themeName === "light" ? "white" : ProBlack2,
|
||||
}}
|
||||
>
|
||||
<div style={{ width: "100%" }}>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
gap: "12px",
|
||||
width: "100%",
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<ShoppingCartOutlined />}
|
||||
onClick={handleAddToCart}
|
||||
disabled={!isValid}
|
||||
style={{
|
||||
flex: 1,
|
||||
height: xs ? "48px" : "48px",
|
||||
fontSize: xs ? "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 (!xs && isValid) {
|
||||
e.currentTarget.style.transform = "translateY(-2px)";
|
||||
}
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
if (!xs) {
|
||||
e.currentTarget.style.transform = "translateY(0)";
|
||||
}
|
||||
}}
|
||||
>
|
||||
{isValid ? t("menu.addToCart") : t("menu.selectRequiredOptions")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Row>
|
||||
|
||||
<SpecialRequestBottomSheet
|
||||
isOpen={isSpecialRequestOpen}
|
||||
onClose={handleSpecialRequestClose}
|
||||
initialValue={specialRequest}
|
||||
onSave={handleSpecialRequestSave}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
186
src/pages/product/components/Variants.tsx
Normal file
186
src/pages/product/components/Variants.tsx
Normal file
@@ -0,0 +1,186 @@
|
||||
import { Divider } from "antd";
|
||||
import ProRatioGroups from "components/ProRatioGroups/ProRatioGroups";
|
||||
import ProText from "components/ProText";
|
||||
import { Dispatch, SetStateAction, useMemo } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useAppSelector } from "redux/hooks";
|
||||
import { Variant } from "utils/types/appTypes";
|
||||
import styles from "../product.module.css";
|
||||
|
||||
export default function Variants({
|
||||
selectedVariants,
|
||||
setSelectedVariants,
|
||||
variantsList,
|
||||
}: {
|
||||
selectedVariants: Record<number, string>;
|
||||
setSelectedVariants: Dispatch<SetStateAction<Record<number, string>>>;
|
||||
variantsList: Variant[];
|
||||
}) {
|
||||
const { isRTL } = useAppSelector((state) => state.locale);
|
||||
const { t } = useTranslation();
|
||||
|
||||
// Determine variant levels based on options array length
|
||||
const variantLevels = useMemo(() => {
|
||||
if (!variantsList || variantsList.length === 0) return [];
|
||||
|
||||
const maxOptionsLength = Math.max(
|
||||
...variantsList.map((v) => v.options.length)
|
||||
);
|
||||
const levels: Array<{
|
||||
level: number;
|
||||
optionKey: string;
|
||||
optionKeyAR: string;
|
||||
availableValues: string[];
|
||||
}> = [];
|
||||
|
||||
for (let i = 0; i < maxOptionsLength; i++) {
|
||||
const optionKey = variantsList[0]?.options[i]?.option || "";
|
||||
const optionKeyAR = variantsList[0]?.optionsAR?.[i]?.option || "";
|
||||
|
||||
if (optionKey) {
|
||||
const availableValues = [
|
||||
...new Set(
|
||||
variantsList
|
||||
.filter((v) => v.options[i]?.option === optionKey)
|
||||
.map((v) => v.options[i]?.value)
|
||||
.filter(Boolean)
|
||||
),
|
||||
];
|
||||
|
||||
levels.push({
|
||||
level: i, // Use the actual array index as level number
|
||||
optionKey,
|
||||
optionKeyAR,
|
||||
availableValues,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// console.log("Variant levels:", levels);
|
||||
// console.log("Selected variants:", selectedVariants);
|
||||
|
||||
return levels;
|
||||
}, [variantsList]);
|
||||
|
||||
// Get filtered variants based on current selections
|
||||
const getFilteredVariants = (level: number) => {
|
||||
if (!variantsList) return [];
|
||||
|
||||
const filtered = variantsList.filter((variant) => {
|
||||
// Check if all previous level selections match
|
||||
for (let i = 0; i < level; i++) {
|
||||
const selectedValue = selectedVariants[i];
|
||||
if (selectedValue && variant.options[i]?.value !== selectedValue) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
// console.log(`Filtered variants for level ${level}:`, {
|
||||
// level,
|
||||
// selectedVariants,
|
||||
// totalVariants: product.variants.length,
|
||||
// filteredCount: filtered.length,
|
||||
// filtered: filtered.map((v) => ({ id: v.id, options: v.options })),
|
||||
// });
|
||||
|
||||
return filtered;
|
||||
};
|
||||
|
||||
// Handle variant selection
|
||||
const handleVariantSelection = (level: number, value: string) => {
|
||||
setSelectedVariants((prev) => {
|
||||
const newSelections = { ...prev };
|
||||
newSelections[level] = value;
|
||||
|
||||
// Clear selections for subsequent levels
|
||||
for (let i = level + 1; i < variantLevels.length; i++) {
|
||||
delete newSelections[i];
|
||||
}
|
||||
|
||||
// console.log("New selections:", newSelections);
|
||||
return newSelections;
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{variantsList?.length > 0 && variantLevels.length > 0 && (
|
||||
<>
|
||||
<Divider style={{ margin: "0" }} />
|
||||
<div>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
}}
|
||||
>
|
||||
<ProText style={{ fontSize: "1.25rem" }}>
|
||||
{t("menu.youMightAlsoLike")}
|
||||
</ProText>
|
||||
|
||||
<div style={{ display: "flex", alignItems: "center" }}>
|
||||
<ProText strong style={{ fontSize: "0.75rem", color: "red" }}>
|
||||
<span style={{ color: "red", fontSize: "0.75rem" }}>*</span>
|
||||
{t("menu.required")}
|
||||
</ProText>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{variantLevels.map((level, index) => {
|
||||
const filteredVariants = getFilteredVariants(index);
|
||||
const availableValues = level.availableValues.filter((value) =>
|
||||
filteredVariants.some((v) => v.options[index]?.value === value)
|
||||
);
|
||||
|
||||
// Only show this level if there are available values and previous levels are selected
|
||||
const shouldShowLevel =
|
||||
index === 0 || selectedVariants[index - 1];
|
||||
|
||||
// console.log("Level rendering:", {
|
||||
// level: level.level,
|
||||
// index,
|
||||
// shouldShowLevel,
|
||||
// availableValues,
|
||||
// filteredVariants: filteredVariants.length,
|
||||
// selectedVariants,
|
||||
// previousLevelSelected:
|
||||
// selectedVariants[level.level - 1],
|
||||
// });
|
||||
|
||||
if (!shouldShowLevel || availableValues.length === 0) return null;
|
||||
|
||||
return (
|
||||
<div key={level.level} style={{ marginBottom: "16px" }}>
|
||||
<ProText strong style={{ fontSize: "1rem" }}>
|
||||
{isRTL ? level.optionKeyAR : level.optionKey}
|
||||
</ProText>
|
||||
|
||||
<div className={styles.productContainer}>
|
||||
<ProRatioGroups
|
||||
options={availableValues.map((value) => {
|
||||
const variant = filteredVariants.find(
|
||||
(v) => v.options[index]?.value === value
|
||||
);
|
||||
return {
|
||||
value: value,
|
||||
label: value,
|
||||
price: variant ? `+${variant.price}` : "",
|
||||
};
|
||||
})}
|
||||
value={selectedVariants[index] || ""}
|
||||
onRatioClick={(value: string) =>
|
||||
handleVariantSelection(index, value)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
293
src/pages/product/page.tsx
Normal file
293
src/pages/product/page.tsx
Normal file
@@ -0,0 +1,293 @@
|
||||
import ActionsButtons from "components/ActionsButtons/ActionsButtons";
|
||||
import ImageWithFallback from "components/ImageWithFallback";
|
||||
import { ItemDescriptionIcons } from "components/ItemDescriptionIcons/ItemDescriptionIcons";
|
||||
import ProText from "components/ProText";
|
||||
import { useAppSelector } from "redux/hooks";
|
||||
import { default_image } from "utils/constants";
|
||||
// import PageTransition from "components/PageTransition/PageTransition";
|
||||
import { Space } from "antd";
|
||||
import ArabicPrice from "components/ArabicPrice";
|
||||
import { useMemo, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { colors } from "ThemeConstants";
|
||||
import { Product } from "utils/types/appTypes";
|
||||
import BackButton from "../menu/components/BackButton";
|
||||
import Extra from "./components/Extra";
|
||||
import ExtraGroups from "./components/ExtraGroups";
|
||||
import ProductFooter from "./components/ProductFooter";
|
||||
import Variants from "./components/Variants";
|
||||
import styles from "./product.module.css";
|
||||
|
||||
export default function ProductDetailPage() {
|
||||
const { isRTL } = useAppSelector((state) => state.locale);
|
||||
const { t } = useTranslation();
|
||||
const [quantity, setQuantity] = useState(1);
|
||||
|
||||
const product = JSON.parse(
|
||||
localStorage.getItem("product") || "null"
|
||||
) as Product;
|
||||
|
||||
// State for variant selections
|
||||
const [selectedVariants, setSelectedVariants] = useState<
|
||||
Record<number, string>
|
||||
>({});
|
||||
|
||||
// State for selected extras
|
||||
const [selectedExtras, setSelectedExtras] = useState<string[]>([]);
|
||||
|
||||
// State for selected extras by group
|
||||
const [selectedExtrasByGroup, setSelectedExtrasByGroup] = useState<
|
||||
Record<number, string[]>
|
||||
>({});
|
||||
|
||||
// Determine variant levels based on options array length
|
||||
const variantLevels = useMemo(() => {
|
||||
if (!product?.variants || product.variants.length === 0) return [];
|
||||
|
||||
const maxOptionsLength = Math.max(
|
||||
...product.variants.map((v) => v.options.length)
|
||||
);
|
||||
const levels: Array<{
|
||||
level: number;
|
||||
optionKey: string;
|
||||
optionKeyAR: string;
|
||||
availableValues: string[];
|
||||
}> = [];
|
||||
|
||||
for (let i = 0; i < maxOptionsLength; i++) {
|
||||
const optionKey = product.variants[0]?.options[i]?.option || "";
|
||||
const optionKeyAR = product.variants[0]?.optionsAR?.[i]?.option || "";
|
||||
|
||||
if (optionKey) {
|
||||
const availableValues = [
|
||||
...new Set(
|
||||
product.variants
|
||||
.filter((v) => v.options[i]?.option === optionKey)
|
||||
.map((v) => v.options[i]?.value)
|
||||
.filter(Boolean)
|
||||
),
|
||||
];
|
||||
|
||||
levels.push({
|
||||
level: i, // Use the actual array index as level number
|
||||
optionKey,
|
||||
optionKeyAR,
|
||||
availableValues,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// console.log("Variant levels:", levels);
|
||||
// console.log("Selected variants:", selectedVariants);
|
||||
|
||||
return levels;
|
||||
}, [product?.variants]);
|
||||
|
||||
// Get the final selected variant ID (the variant that matches all current selections)
|
||||
const getFinalSelectedVariantId = () => {
|
||||
if (!product?.variants || Object.keys(selectedVariants).length === 0)
|
||||
return "";
|
||||
|
||||
// Find the variant that matches all current selections
|
||||
const matchingVariant = product.variants.find((variant) => {
|
||||
return Object.entries(selectedVariants).every(([level, value]) => {
|
||||
const levelNum = parseInt(level);
|
||||
return variant.options[levelNum]?.value === value;
|
||||
});
|
||||
});
|
||||
|
||||
// Convert to string only if defined, otherwise return empty string
|
||||
return matchingVariant?.id?.toString() || "";
|
||||
};
|
||||
|
||||
const getExtras = () => {
|
||||
const finalSelectedVariantId = getFinalSelectedVariantId();
|
||||
if (finalSelectedVariantId) {
|
||||
const variant = product?.variants?.find(
|
||||
(variant) => variant.id === Number(finalSelectedVariantId)
|
||||
);
|
||||
if (variant?.extras?.length && variant.extras.length > 0) {
|
||||
return variant.extras;
|
||||
}
|
||||
}
|
||||
return product?.extras;
|
||||
};
|
||||
|
||||
// Validation function to check if all required selections are made
|
||||
const validateRequiredSelections = () => {
|
||||
// Check if all variant levels are selected
|
||||
const allVariantsSelected = variantLevels.every(
|
||||
(level) => selectedVariants[level.level] !== undefined
|
||||
);
|
||||
|
||||
// Check if all required extra groups are satisfied
|
||||
const allRequiredExtraGroupsSatisfied = product.theExtrasGroups.every(
|
||||
(group) => {
|
||||
if (group.force_limit_selection === 1) {
|
||||
const selectedCount = selectedExtrasByGroup[group.id]?.length || 0;
|
||||
return selectedCount === group.limit;
|
||||
}
|
||||
return true; // Optional groups are always satisfied
|
||||
}
|
||||
);
|
||||
|
||||
return allVariantsSelected && allRequiredExtraGroupsSatisfied;
|
||||
};
|
||||
|
||||
const isValid = validateRequiredSelections();
|
||||
|
||||
if (!product) {
|
||||
return (
|
||||
<div style={{ padding: "40px", textAlign: "center" }}>
|
||||
<ProText type="secondary">Product not found</ProText>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
height: "80vh",
|
||||
overflow: "auto",
|
||||
scrollbarWidth: "none",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
zIndex: 999,
|
||||
left: 20,
|
||||
top: 70,
|
||||
backgroundColor: "#FFF",
|
||||
borderRadius: "50%",
|
||||
boxShadow: "0 2px 8px rgba(0, 0, 0, 0.1)",
|
||||
}}
|
||||
>
|
||||
<BackButton />
|
||||
</div>
|
||||
|
||||
<div
|
||||
style={{
|
||||
overflow: "hidden",
|
||||
transition: "all 0.3s ease",
|
||||
}}
|
||||
>
|
||||
<ImageWithFallback
|
||||
src={product?.image}
|
||||
fallbackSrc={default_image}
|
||||
height={240}
|
||||
style={{
|
||||
width: "100vw",
|
||||
objectFit: "cover",
|
||||
transition: "transform 0.3s ease",
|
||||
}}
|
||||
loadingContainerStyle={{ borderRadius: 0, width: "100vw" }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={styles.productDetailContainer}>
|
||||
<div
|
||||
style={{
|
||||
display: "grid",
|
||||
gridTemplateColumns: "1fr", // isMobile ? "1fr" : "1fr 1fr",
|
||||
gap: 5,
|
||||
}}
|
||||
>
|
||||
<Space direction="vertical" size="middle" style={{ width: "100%" }}>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
justifyContent: "space-between",
|
||||
gap: "0.5rem",
|
||||
marginTop: 16,
|
||||
}}
|
||||
>
|
||||
<ProText strong style={{ fontSize: "1.25rem" }}>
|
||||
{isRTL ? product?.nameOther : product?.name}{" "}
|
||||
</ProText>
|
||||
{product?.description && (
|
||||
<ProText
|
||||
type="secondary"
|
||||
style={{ fontSize: "1rem", lineHeight: "1.25rem" }}
|
||||
>
|
||||
{isRTL ? product?.descriptionAR : product?.description}
|
||||
</ProText>
|
||||
)}
|
||||
<ArabicPrice
|
||||
price={product?.price}
|
||||
style={{ fontSize: "1rem", color: colors.primary }}
|
||||
/>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
justifyContent: "start",
|
||||
gap: 20,
|
||||
...(isRTL ? { marginRight: -5 } : {}),
|
||||
}} // Item Description Icons
|
||||
>
|
||||
<ItemDescriptionIcons />
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
justifyContent: "end",
|
||||
}}
|
||||
>
|
||||
<ActionsButtons
|
||||
quantity={quantity}
|
||||
setQuantity={(quantity) => setQuantity(quantity)}
|
||||
max={100}
|
||||
min={1}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{product?.variants?.length > 0 && variantLevels.length > 0 && (
|
||||
<Variants
|
||||
selectedVariants={selectedVariants}
|
||||
setSelectedVariants={setSelectedVariants}
|
||||
variantsList={product?.variants}
|
||||
/>
|
||||
)}
|
||||
|
||||
{getExtras().length > 0 && (
|
||||
<Extra
|
||||
extrasList={getExtras()}
|
||||
selectedExtras={selectedExtras}
|
||||
setSelectedExtras={setSelectedExtras}
|
||||
/>
|
||||
)}
|
||||
|
||||
{product.theExtrasGroups.length > 0 && (
|
||||
<ExtraGroups
|
||||
groupsList={product.theExtrasGroups}
|
||||
selectedExtrasByGroup={selectedExtrasByGroup}
|
||||
setSelectedExtrasByGroup={setSelectedExtrasByGroup}
|
||||
/>
|
||||
)}
|
||||
</Space>
|
||||
</div>
|
||||
|
||||
<ProductFooter
|
||||
product={product}
|
||||
isValid={isValid}
|
||||
variantId={getFinalSelectedVariantId()}
|
||||
selectedExtras={selectedExtras}
|
||||
selectedGroups={Object.values(selectedExtrasByGroup).flat()}
|
||||
quantity={quantity}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
169
src/pages/product/product.module.css
Normal file
169
src/pages/product/product.module.css
Normal file
@@ -0,0 +1,169 @@
|
||||
.productDetailContainer {
|
||||
overflow: auto;
|
||||
scrollbar-width: none;
|
||||
padding: 0 16px !important;
|
||||
}
|
||||
|
||||
.productContainer :global(.ant-radio-wrapper:last-child) {
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
.productContainer :global(.ant-radio-label) {
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
/* radio.module.css */
|
||||
.productContainer :global(.ant-radio-inner) {
|
||||
width: 24px !important;
|
||||
height: 24px !important;
|
||||
border-radius: 30px !important;
|
||||
}
|
||||
|
||||
.productContainer :global(.ant-radio) {
|
||||
width: 24px !important;
|
||||
height: 24px !important;
|
||||
}
|
||||
|
||||
.productContainer :global(.ant-radio-checked .ant-radio-inner) {
|
||||
border-radius: 30px !important;
|
||||
}
|
||||
|
||||
.productContainer :global(.ant-space-gap-row-small) {
|
||||
row-gap: 0px !important;
|
||||
}
|
||||
|
||||
.productContainer :global(.ant-checkbox-wrapper:last-child) {
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
.productContainer :global(.ant-checkbox-label) {
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
/* .productContainer :global(.ant-checkbox-input) {
|
||||
margin-top: 5px !important;
|
||||
} */
|
||||
|
||||
.productContainer :global(.ant-checkbox-inner) {
|
||||
border-radius: 40px !important;
|
||||
}
|
||||
|
||||
/* CheckboxGroup.module.css */
|
||||
.productContainer :global(.ant-checkbox-inner) {
|
||||
width: 24px !important;
|
||||
height: 24px !important;
|
||||
border-radius: 30px !important;
|
||||
}
|
||||
|
||||
.productContainer :global(.ant-checkbox) {
|
||||
width: 24px !important;
|
||||
height: 24px !important;
|
||||
}
|
||||
|
||||
.productContainer :global(.ant-checkbox-checked .ant-checkbox-inner) {
|
||||
border-radius: 30px !important;
|
||||
}
|
||||
|
||||
:global(.darkApp) .productDetailContainer path {
|
||||
fill: var(--primary) !important;
|
||||
}
|
||||
|
||||
/* Restaurant-Style Description */
|
||||
.descriptionSection {
|
||||
position: relative;
|
||||
backdrop-filter: blur(10px);
|
||||
padding: 0 16px;
|
||||
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.descriptionSection:hover {
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.descriptionHeader {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.descriptionIcon {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
background: linear-gradient(135deg, var(--primary) 0%, var(--primary2) 100%);
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.descriptionIcon::before {
|
||||
content: "🍽️";
|
||||
font-size: 14px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.descriptionLabel {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: var(--secondary);
|
||||
margin: 0;
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
|
||||
.descriptionText {
|
||||
font-size: 15px;
|
||||
line-height: 1.7;
|
||||
color: rgba(0, 0, 0, 0.45);
|
||||
font-weight: 400;
|
||||
margin: 0;
|
||||
opacity: 0.85;
|
||||
text-align: justify;
|
||||
padding: 0 4px;
|
||||
}
|
||||
|
||||
.descriptionText::first-letter {
|
||||
font-size: 1.2em;
|
||||
font-weight: 600;
|
||||
color: var(--primary);
|
||||
margin-right: 2px;
|
||||
}
|
||||
|
||||
/* Dark mode restaurant styles */
|
||||
:global(.darkApp) .descriptionSection {
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
rgba(26, 26, 26, 0.95) 0%,
|
||||
rgba(42, 42, 42, 0.98) 100%
|
||||
);
|
||||
}
|
||||
|
||||
:global(.darkApp) .descriptionLabel {
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
:global(.darkApp) .descriptionText {
|
||||
color: #e5e5e5;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
:global(.darkApp) .descriptionText::first-letter {
|
||||
color: var(--primary2);
|
||||
}
|
||||
|
||||
/* Mobile responsiveness */
|
||||
@media (max-width: 768px) {
|
||||
.descriptionSection {
|
||||
padding: 16px 16px 0 16px;
|
||||
}
|
||||
|
||||
.descriptionLabel {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.descriptionText {
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
}
|
||||
0
src/pages/product/types.ts
Normal file
0
src/pages/product/types.ts
Normal file
213
src/pages/restaurant/RestaurantServices.tsx
Normal file
213
src/pages/restaurant/RestaurantServices.tsx
Normal file
@@ -0,0 +1,213 @@
|
||||
import { Card } from "antd";
|
||||
import BackIcon from "components/Icons/BackIcon";
|
||||
import BookingIcon from "components/Icons/BookingIcon";
|
||||
import DeliveryIcon from "components/Icons/DeliveryIcon";
|
||||
import DineInIcon from "components/Icons/DineInIcon";
|
||||
import NextIcon from "components/Icons/NextIcon";
|
||||
import PickupIcon from "components/Icons/PickupIcon";
|
||||
import SendGiftIcon from "components/Icons/SendGiftIcon";
|
||||
import ToOfficeIcon from "components/Icons/ToOfficeIcon";
|
||||
import ToRoomIcon from "components/Icons/ToRoomIcon";
|
||||
import ProTitle from "components/ProTitle";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Link } from "react-router-dom";
|
||||
import { useAppSelector } from "redux/hooks";
|
||||
import styles from "./restaurant.module.css";
|
||||
|
||||
interface RestaurantServicesProps {
|
||||
dineIn?: boolean;
|
||||
pickup?: boolean;
|
||||
gift?: boolean;
|
||||
delivery?: boolean;
|
||||
toRoom?: boolean;
|
||||
toOffice?: boolean;
|
||||
is_booking_enabled?: boolean;
|
||||
params: { locale: string; id: string };
|
||||
}
|
||||
|
||||
export default function RestaurantServices({
|
||||
dineIn,
|
||||
pickup,
|
||||
gift,
|
||||
delivery,
|
||||
toRoom,
|
||||
toOffice,
|
||||
is_booking_enabled,
|
||||
}: RestaurantServicesProps) {
|
||||
const { t } = useTranslation();
|
||||
const { isRTL } = useAppSelector((state) => state.locale);
|
||||
const id = localStorage.getItem("restaurantName");
|
||||
|
||||
const services = [
|
||||
...((dineIn && [
|
||||
{
|
||||
id: "dine-in",
|
||||
title: t("common.dineIn"),
|
||||
description: t("home.services.dineIn"),
|
||||
icon: (
|
||||
<DineInIcon
|
||||
className={styles.serviceIcon + " " + styles.dineInIcon}
|
||||
/>
|
||||
),
|
||||
color: "bg-blue-50 text-blue-600",
|
||||
href: `/${id}/menu?orderType=dine-in`,
|
||||
},
|
||||
]) ||
|
||||
[]),
|
||||
...((pickup && [
|
||||
{
|
||||
id: "pickup",
|
||||
title: t("common.pickup"),
|
||||
description: t("home.services.pickup"),
|
||||
icon: (
|
||||
<PickupIcon
|
||||
className={styles.serviceIcon + " " + styles.pickupIcon}
|
||||
/>
|
||||
),
|
||||
color: "bg-green-50 text-green-600",
|
||||
href: `/${id}/menu?orderType=pickup`,
|
||||
},
|
||||
]) ||
|
||||
[]),
|
||||
...((gift && [
|
||||
{
|
||||
id: "gift",
|
||||
title: t("common.sendGift"),
|
||||
description: t("home.services.gift"),
|
||||
icon: (
|
||||
<SendGiftIcon
|
||||
className={styles.serviceIcon + " " + styles.giftIcon}
|
||||
/>
|
||||
),
|
||||
color: "bg-pink-50 text-pink-600",
|
||||
href: `/${id}/menu?orderType=gift`,
|
||||
},
|
||||
]) ||
|
||||
[]),
|
||||
...((toRoom && [
|
||||
{
|
||||
id: "room",
|
||||
title: t("common.roomService"),
|
||||
description: t("home.services.room"),
|
||||
icon: (
|
||||
<ToRoomIcon className={styles.serviceIcon + " " + styles.roomIcon} />
|
||||
),
|
||||
color: "bg-purple-50 text-purple-600",
|
||||
href: `/${id}/menu?orderType=room`,
|
||||
},
|
||||
]) ||
|
||||
[]),
|
||||
...((toOffice && [
|
||||
{
|
||||
id: "office",
|
||||
title: t("common.officeDelivery"),
|
||||
description: t("home.services.office"),
|
||||
icon: (
|
||||
<ToOfficeIcon
|
||||
className={styles.serviceIcon + " " + styles.officeIcon}
|
||||
/>
|
||||
),
|
||||
color: "bg-orange-50 text-orange-600",
|
||||
href: `/${id}/menu?orderType=office`,
|
||||
},
|
||||
]) ||
|
||||
[]),
|
||||
...((is_booking_enabled && [
|
||||
{
|
||||
id: "booking",
|
||||
title: t("common.tableBooking"),
|
||||
description: t("home.services.booking"),
|
||||
icon: (
|
||||
<BookingIcon
|
||||
className={styles.serviceIcon + " " + styles.bookingIcon}
|
||||
/>
|
||||
),
|
||||
color: "bg-indigo-50 text-indigo-600",
|
||||
href: `/${id}/menu?orderType=booking`,
|
||||
},
|
||||
]) ||
|
||||
[]),
|
||||
...((delivery && [
|
||||
{
|
||||
id: "delivery",
|
||||
title: t("common.delivery"),
|
||||
description: t("home.services.delivery"),
|
||||
icon: (
|
||||
<DeliveryIcon
|
||||
className={styles.serviceIcon + " " + styles.deliveryIcon}
|
||||
/>
|
||||
),
|
||||
color: "bg-indigo-50 text-indigo-600",
|
||||
href: `/${id}/address`,
|
||||
},
|
||||
]) ||
|
||||
[]),
|
||||
];
|
||||
|
||||
// Determine grid class based on number of services
|
||||
const getGridClass = () => {
|
||||
const serviceCount = services.length;
|
||||
if (serviceCount <= 2) return `${styles.servicesGrid} ${styles.twoColumns}`;
|
||||
if (serviceCount >= 4)
|
||||
return `${styles.servicesGrid} ${styles.manyServices}`;
|
||||
return styles.servicesGrid;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={getGridClass()}>
|
||||
{services?.map((s) => {
|
||||
return (
|
||||
<Link
|
||||
to={s?.href}
|
||||
key={s?.id}
|
||||
onClick={() => {
|
||||
localStorage.setItem("orderType", s?.id);
|
||||
}}
|
||||
style={{
|
||||
width: "100%",
|
||||
}}
|
||||
>
|
||||
<Card className={styles.homeServiceCard}>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
width: "100%",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
gap: 12,
|
||||
}}
|
||||
>
|
||||
{s?.icon}
|
||||
<ProTitle
|
||||
level={5}
|
||||
style={{
|
||||
margin: 0,
|
||||
fontSize: 14,
|
||||
fontWeight: 600,
|
||||
}}
|
||||
>
|
||||
{s?.title}
|
||||
</ProTitle>
|
||||
</div>
|
||||
|
||||
{isRTL ? (
|
||||
<BackIcon className={styles.serviceIcon} />
|
||||
) : (
|
||||
<NextIcon className={styles.serviceIcon} />
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
98
src/pages/restaurant/page.tsx
Normal file
98
src/pages/restaurant/page.tsx
Normal file
@@ -0,0 +1,98 @@
|
||||
import InstagramIcon from "components/Icons/social/InstagramIcon";
|
||||
import JIcon from "components/Icons/social/JIcon";
|
||||
import SnapIcon from "components/Icons/social/SnapIcon";
|
||||
import XIcon from "components/Icons/social/XIcon";
|
||||
import { LanguageSwitch } from "components/LanguageSwitch/LanguageSwitch";
|
||||
import ProText from "components/ProText";
|
||||
import ProTitle from "components/ProTitle";
|
||||
import { useAppSelector } from "redux/hooks";
|
||||
import styles from "./restaurant.module.css";
|
||||
import RestaurantServices from "./RestaurantServices";
|
||||
|
||||
// Import the Client Component for localStorage handling
|
||||
import Ads1 from "components/Ads/Ads1";
|
||||
import { Loader } from "components/Loader/Loader";
|
||||
import { useParams } from "react-router-dom";
|
||||
import { useGetRestaurantDetailsQuery } from "redux/api/others";
|
||||
import LocalStorageHandler from "../menu/components/LocalStorageHandler";
|
||||
|
||||
export default function RestaurantPage() {
|
||||
const param = useParams();
|
||||
console.log(param);
|
||||
|
||||
const { isRTL } = useAppSelector((state) => state.locale);
|
||||
const { data, isLoading } = useGetRestaurantDetailsQuery(param.id, {
|
||||
skip: !param.id,
|
||||
});
|
||||
const { restaurant, dineIn, pickup, delivery, gift, toOffice, toRoom } =
|
||||
data || {};
|
||||
|
||||
if (isLoading) {
|
||||
return <Loader />;
|
||||
}
|
||||
|
||||
if (!restaurant) {
|
||||
return <div>Restaurant not found</div>;
|
||||
}
|
||||
|
||||
if (restaurant) {
|
||||
localStorage.setItem("restaurantID", restaurant.id);
|
||||
localStorage.setItem("restaurantName", restaurant.subdomain);
|
||||
}
|
||||
|
||||
console.log(isRTL);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={styles.languageSwitch}>
|
||||
<LanguageSwitch />
|
||||
</div>
|
||||
|
||||
<div className={styles.homeContainer}>
|
||||
<div style={{ textAlign: "center", maxWidth: "100%" }}>
|
||||
<div className={styles.logoContainer}>
|
||||
<img
|
||||
src={restaurant?.logom || ""}
|
||||
alt="logo"
|
||||
width={96}
|
||||
height={96}
|
||||
className={styles.logo}
|
||||
/>
|
||||
</div>
|
||||
<ProTitle level={5} style={{ margin: 0 }}>
|
||||
{isRTL ? restaurant?.nameAR : restaurant?.name}
|
||||
</ProTitle>
|
||||
<ProText style={{ fontSize: 14, margin: 0 }}>
|
||||
{isRTL ? restaurant?.descriptionAR : restaurant?.description}
|
||||
</ProText>
|
||||
</div>
|
||||
|
||||
<RestaurantServices
|
||||
dineIn={dineIn}
|
||||
pickup={pickup}
|
||||
gift={gift}
|
||||
delivery={delivery}
|
||||
toRoom={toRoom}
|
||||
toOffice={toOffice}
|
||||
is_booking_enabled={restaurant?.is_booking_enabled === 1}
|
||||
params={{ id: "1", locale: "en" }}
|
||||
/>
|
||||
<div className={styles.promotionContainer}>
|
||||
<Ads1 />
|
||||
</div>
|
||||
<div className={styles.socialIconsContainer}>
|
||||
<InstagramIcon className={styles.socialIcon} />
|
||||
<XIcon className={styles.socialIcon} />
|
||||
<SnapIcon className={styles.socialIcon} />
|
||||
<JIcon className={styles.socialIcon} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<LocalStorageHandler
|
||||
restaurantID={restaurant.id}
|
||||
restaurantName={restaurant.subdomain}
|
||||
orderType=""
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
982
src/pages/restaurant/restaurant.module.css
Normal file
982
src/pages/restaurant/restaurant.module.css
Normal file
@@ -0,0 +1,982 @@
|
||||
/* File: src/styles/Home.module.css */
|
||||
|
||||
.main {
|
||||
width: 100%;
|
||||
overflow-x: hidden;
|
||||
background-color: #fafafa;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.section {
|
||||
padding: 80px 16px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.section {
|
||||
padding: 60px 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.homeContainer {
|
||||
text-align: center;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
background-image: url("/background.svg");
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
background-attachment: fixed;
|
||||
transition: all 0.3s ease;
|
||||
height: 100vh;
|
||||
min-height: 100vh;
|
||||
width: 100vw;
|
||||
margin: 0;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.homeServiceCard {
|
||||
width: 100%;
|
||||
height: 48px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: 12px 18px !important;
|
||||
row-gap: 10px;
|
||||
transition: all 0.3s ease;
|
||||
border-radius: 50px;
|
||||
background-color: rgba(255, 183, 0, 0.12);
|
||||
}
|
||||
|
||||
.logo {
|
||||
border-radius: 50%;
|
||||
border: 2px solid rgba(255, 255, 255, 0.6);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.logo:hover {
|
||||
border-color: rgba(255, 183, 0, 0.3);
|
||||
transform: scale(1.02);
|
||||
}
|
||||
|
||||
/* Logo container for perfect centering */
|
||||
.logoContainer {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.homeServiceCard :global(.ant-card-body) {
|
||||
padding: 0px !important;
|
||||
text-align: start;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Removed duplicate rule */
|
||||
|
||||
/* Smooth transitions for all interactive elements */
|
||||
.homeContainer *,
|
||||
.homeServiceCard * {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.serviceIcon {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
.promotionContainer {
|
||||
margin: 0 16px;
|
||||
}
|
||||
|
||||
/* Social icons for mobile (default styles) */
|
||||
.socialIconsContainer {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
position: fixed;
|
||||
bottom: 3%;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 100%;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.socialIcon {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.socialIcon:hover {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
/* Mobiles Styles (769px - 1024px) */
|
||||
@media (max-width: 769px) {
|
||||
/* Enhanced grid layout for services on tablet */
|
||||
.servicesGrid {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
padding: 0 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* Tablet Styles (769px - 1024px) */
|
||||
@media (min-width: 769px) and (max-width: 1024px) {
|
||||
.languageSwitch,
|
||||
.themeSwitch,
|
||||
.promotionContainer {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.homeContainer {
|
||||
padding: 6vh 6vw;
|
||||
height: 100vh;
|
||||
min-height: 100vh;
|
||||
max-width: 1000px;
|
||||
margin: 0 auto;
|
||||
gap: 32px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.logo {
|
||||
width: 140px;
|
||||
height: 140px;
|
||||
margin: 0 auto 24px;
|
||||
border-radius: 24px !important;
|
||||
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.15);
|
||||
transition: all 0.3s ease;
|
||||
object-fit: cover;
|
||||
border: 3px solid rgba(255, 255, 255, 0.8);
|
||||
display: block;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.logo:hover {
|
||||
transform: scale(1.05) rotate(2deg);
|
||||
box-shadow: 0 16px 48px rgba(0, 0, 0, 0.2);
|
||||
border-color: rgba(255, 183, 0, 0.3);
|
||||
}
|
||||
|
||||
/* Enhanced typography for tablet */
|
||||
.homeContainer h5 {
|
||||
font-size: 24px !important;
|
||||
font-weight: 600 !important;
|
||||
margin-bottom: 8px !important;
|
||||
}
|
||||
|
||||
.homeContainer p {
|
||||
font-size: 16px !important;
|
||||
line-height: 1.5 !important;
|
||||
max-width: 500px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.homeServiceCard {
|
||||
width: 100%;
|
||||
height: 65px;
|
||||
padding: 20px 28px !important;
|
||||
border-radius: 20px;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
|
||||
cursor: pointer;
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
rgba(255, 183, 0, 0.12) 0%,
|
||||
rgba(255, 183, 0, 0.08) 100%
|
||||
);
|
||||
border: 1px solid rgba(255, 183, 0, 0.2);
|
||||
}
|
||||
|
||||
.homeServiceCard:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 12px 32px rgba(0, 0, 0, 0.15);
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
rgba(255, 183, 0, 0.18) 0%,
|
||||
rgba(255, 183, 0, 0.12) 100%
|
||||
);
|
||||
border-color: rgba(255, 183, 0, 0.3);
|
||||
}
|
||||
|
||||
.serviceIcon {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
/* Enhanced grid layout for services on tablet */
|
||||
.servicesGrid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 20px;
|
||||
padding: 0 32px;
|
||||
margin: 32px 0;
|
||||
max-width: 700px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
/* If there are only 2 services, use single column */
|
||||
.servicesGrid.twoColumns {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
max-width: 700px;
|
||||
}
|
||||
|
||||
/* If there are 4+ services, use 2 columns */
|
||||
.servicesGrid.manyServices {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
max-width: 700px;
|
||||
}
|
||||
|
||||
/* Enhanced service card typography */
|
||||
.homeServiceCard h5 {
|
||||
font-size: 16px !important;
|
||||
font-weight: 600 !important;
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
/* Social icons positioning for tablet */
|
||||
.socialIconsContainer {
|
||||
position: fixed;
|
||||
bottom: 32px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
display: flex;
|
||||
gap: 28px;
|
||||
padding: 16px 28px;
|
||||
border-radius: 60px;
|
||||
}
|
||||
|
||||
.socialIcon {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.socialIcon:hover {
|
||||
transform: scale(1.15);
|
||||
filter: drop-shadow(0 4px 8px rgba(0, 0, 0, 0.2));
|
||||
}
|
||||
}
|
||||
|
||||
/* Mobile landscape orientation */
|
||||
@media (max-height: 500px) and (orientation: landscape) {
|
||||
.homeContainer {
|
||||
height: 100vh;
|
||||
min-height: 100vh;
|
||||
background-attachment: scroll;
|
||||
}
|
||||
}
|
||||
|
||||
/* Mobile portrait orientation */
|
||||
@media (max-width: 768px) and (orientation: portrait) {
|
||||
.homeContainer {
|
||||
height: 100vh;
|
||||
min-height: 100vh;
|
||||
background-attachment: scroll;
|
||||
}
|
||||
}
|
||||
|
||||
/* Large screens */
|
||||
@media (min-width: 1200px) {
|
||||
.homeContainer {
|
||||
background-attachment: fixed;
|
||||
}
|
||||
}
|
||||
|
||||
/* Ensure full coverage on all devices */
|
||||
@media (max-width: 480px) {
|
||||
.homeContainer {
|
||||
height: 100vh;
|
||||
min-height: 100vh;
|
||||
width: 100vw;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* Desktop Styles (1025px+) */
|
||||
@media (min-width: 1025px) {
|
||||
.languageSwitch,
|
||||
.themeSwitch,
|
||||
.promotionContainer {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.homeContainer {
|
||||
padding: 4vh 8vw;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
gap: 24px;
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 100vh;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.logo {
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
margin: 0 auto 20px;
|
||||
border-radius: 20px !important;
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
|
||||
transition: all 0.3s ease;
|
||||
object-fit: cover;
|
||||
border: 3px solid rgba(255, 255, 255, 0.9);
|
||||
position: relative;
|
||||
display: block;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.logo::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: -3px;
|
||||
left: -3px;
|
||||
right: -3px;
|
||||
bottom: -3px;
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
rgba(255, 183, 0, 0.3),
|
||||
rgba(255, 183, 0, 0.1)
|
||||
);
|
||||
border-radius: 24px;
|
||||
z-index: -1;
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.logo:hover {
|
||||
transform: scale(1.05) rotate(-1deg);
|
||||
box-shadow: 0 12px 32px rgba(0, 0, 0, 0.2);
|
||||
border-color: rgba(255, 183, 0, 0.4);
|
||||
}
|
||||
|
||||
.logo:hover::before {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* Enhanced typography for desktop */
|
||||
.homeContainer h5 {
|
||||
font-size: 24px !important;
|
||||
font-weight: 600 !important;
|
||||
margin-bottom: 8px !important;
|
||||
letter-spacing: -0.3px;
|
||||
}
|
||||
|
||||
.homeContainer p {
|
||||
font-size: 16px !important;
|
||||
line-height: 1.5 !important;
|
||||
max-width: 500px;
|
||||
margin: 0 auto;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.homeServiceCard {
|
||||
width: 100%;
|
||||
height: 64px;
|
||||
padding: 16px 24px !important;
|
||||
border-radius: 16px;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
|
||||
cursor: pointer;
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
rgba(255, 183, 0, 0.12) 0%,
|
||||
rgba(255, 183, 0, 0.08) 100%
|
||||
);
|
||||
border: 1px solid rgba(255, 183, 0, 0.2);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.homeServiceCard::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
rgba(255, 183, 0, 0.05) 0%,
|
||||
transparent 100%
|
||||
);
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.homeServiceCard:hover::before {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.homeServiceCard:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
rgba(255, 183, 0, 0.18) 0%,
|
||||
rgba(255, 183, 0, 0.12) 100%
|
||||
);
|
||||
border-color: rgba(255, 183, 0, 0.4);
|
||||
}
|
||||
|
||||
.serviceIcon {
|
||||
position: relative;
|
||||
top: -5px;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
/* Enhanced grid layout for services on desktop */
|
||||
.servicesGrid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 16px;
|
||||
padding: 0 24px;
|
||||
margin: 24px 0;
|
||||
max-width: 800px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
/* If there are only 2 services, use 2 columns */
|
||||
.servicesGrid.twoColumns {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
max-width: 700px;
|
||||
}
|
||||
|
||||
/* If there are 4+ services, use 2 rows */
|
||||
.servicesGrid.manyServices {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
max-width: 700px;
|
||||
}
|
||||
|
||||
/* Enhanced service card typography */
|
||||
.homeServiceCard h5 {
|
||||
font-size: 16px !important;
|
||||
font-weight: 600 !important;
|
||||
margin: 0 !important;
|
||||
letter-spacing: -0.1px;
|
||||
}
|
||||
|
||||
/* Social icons positioning for desktop */
|
||||
.socialIconsContainer {
|
||||
position: fixed;
|
||||
bottom: 24px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
display: flex;
|
||||
gap: 24px;
|
||||
backdrop-filter: blur(12px);
|
||||
padding: 12px 24px;
|
||||
border-radius: 50px;
|
||||
}
|
||||
|
||||
.socialIcon {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.socialIcon:hover {
|
||||
transform: scale(1.15);
|
||||
filter: drop-shadow(0 4px 8px rgba(0, 0, 0, 0.2));
|
||||
}
|
||||
}
|
||||
|
||||
/* Large Desktop Styles (1440px+) */
|
||||
@media (min-width: 1440px) {
|
||||
.homeContainer {
|
||||
padding: 6vh 10vw;
|
||||
max-width: 1400px;
|
||||
gap: 32px;
|
||||
}
|
||||
|
||||
.logo {
|
||||
width: 140px;
|
||||
height: 140px;
|
||||
margin: 0 auto 24px;
|
||||
border-radius: 24px !important;
|
||||
box-shadow: 0 12px 32px rgba(0, 0, 0, 0.2);
|
||||
border: 4px solid rgba(255, 255, 255, 0.95);
|
||||
display: block;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.logo:hover {
|
||||
transform: scale(1.06) rotate(1deg);
|
||||
box-shadow: 0 16px 40px rgba(0, 0, 0, 0.25);
|
||||
border-color: rgba(255, 183, 0, 0.5);
|
||||
}
|
||||
|
||||
/* Enhanced typography for large desktop */
|
||||
.homeContainer h5 {
|
||||
font-size: 28px !important;
|
||||
font-weight: 600 !important;
|
||||
margin-bottom: 12px !important;
|
||||
letter-spacing: -0.5px;
|
||||
}
|
||||
|
||||
.homeContainer p {
|
||||
font-size: 18px !important;
|
||||
line-height: 1.6 !important;
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.homeServiceCard {
|
||||
height: 65px;
|
||||
padding: 20px 28px !important;
|
||||
border-radius: 20px;
|
||||
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.homeServiceCard:hover {
|
||||
transform: translateY(-6px);
|
||||
box-shadow: 0 12px 32px rgba(0, 0, 0, 0.18);
|
||||
}
|
||||
|
||||
.serviceIcon {
|
||||
position: relative;
|
||||
top: -5px;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
/* Enhanced service card typography */
|
||||
.homeServiceCard h5 {
|
||||
font-size: 18px !important;
|
||||
font-weight: 600 !important;
|
||||
letter-spacing: -0.2px;
|
||||
}
|
||||
|
||||
.servicesGrid {
|
||||
max-width: 1000px;
|
||||
gap: 20px;
|
||||
padding: 0 32px;
|
||||
margin: 32px 0;
|
||||
}
|
||||
|
||||
.servicesGrid.twoColumns {
|
||||
max-width: 700px;
|
||||
}
|
||||
|
||||
.servicesGrid.manyServices {
|
||||
max-width: 700px;
|
||||
}
|
||||
|
||||
/* Social icons for large desktop */
|
||||
.socialIconsContainer {
|
||||
gap: 32px;
|
||||
padding: 16px 32px;
|
||||
border-radius: 60px;
|
||||
bottom: 32px;
|
||||
}
|
||||
|
||||
.socialIcon {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
}
|
||||
|
||||
.socialIcon:hover {
|
||||
transform: scale(1.2);
|
||||
filter: drop-shadow(0 6px 12px rgba(0, 0, 0, 0.25));
|
||||
}
|
||||
}
|
||||
|
||||
/* Enhanced Dark Theme Styles */
|
||||
:global(.darkApp) .homeContainer {
|
||||
background-image: url("/background-dark.svg");
|
||||
background-color: #0a0a0a;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
/* Dark theme for tablet and desktop */
|
||||
@media (min-width: 769px) {
|
||||
:global(.darkApp) .homeServiceCard {
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
rgba(255, 183, 0, 0.08) 0%,
|
||||
rgba(255, 183, 0, 0.04) 100%
|
||||
) !important;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
:global(.darkApp) .homeServiceCard:hover {
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
rgba(255, 183, 0, 0.15) 0%,
|
||||
rgba(255, 183, 0, 0.08) 100%
|
||||
) !important;
|
||||
border-color: rgba(255, 183, 0, 0.4);
|
||||
transform: translateY(-6px);
|
||||
box-shadow: 0 16px 40px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
:global(.darkApp) .homeServiceCard::before {
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
rgba(255, 183, 0, 0.08) 0%,
|
||||
transparent 100%
|
||||
);
|
||||
}
|
||||
|
||||
:global(.darkApp) .logo {
|
||||
box-shadow: 0 16px 48px rgba(255, 183, 0, 0.25);
|
||||
border-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
:global(.darkApp) .logo:hover {
|
||||
box-shadow: 0 20px 60px rgba(255, 183, 0, 0.35);
|
||||
border-color: rgba(255, 183, 0, 0.4);
|
||||
}
|
||||
|
||||
:global(.darkApp) .logo::before {
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
rgba(255, 183, 0, 0.2),
|
||||
rgba(255, 183, 0, 0.05)
|
||||
);
|
||||
}
|
||||
|
||||
/* Enhanced dark theme typography */
|
||||
:global(.darkApp) .homeContainer h5 {
|
||||
color: #ffffff !important;
|
||||
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
:global(.darkApp) .homeContainer p {
|
||||
color: #e0e0e0 !important;
|
||||
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
}
|
||||
|
||||
:global(.darkApp) .homeContainer h1,
|
||||
:global(.darkApp) .homeContainer h2,
|
||||
:global(.darkApp) .homeContainer h3,
|
||||
:global(.darkApp) .homeContainer h4,
|
||||
:global(.darkApp) .homeContainer h5,
|
||||
:global(.darkApp) .homeContainer h6 {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
:global(.darkApp) .homeContainer p,
|
||||
:global(.darkApp) .homeContainer span {
|
||||
color: #b0b0b0;
|
||||
}
|
||||
|
||||
:global(.darkApp) .homeServiceCard {
|
||||
background-color: rgba(255, 183, 0, 0.12) !important;
|
||||
border-color: #363636 !important;
|
||||
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
:global(.darkApp) .homeServiceCard:hover {
|
||||
background-color: #363636 !important;
|
||||
border-color: #424242 !important;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 15px -3px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
:global(.darkApp) .socialIcon path {
|
||||
fill: none !important;
|
||||
stroke: #fff !important;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
:global(.darkApp) .socialIcon:hover path {
|
||||
stroke: #fff !important;
|
||||
filter: drop-shadow(0 0 8px #fff);
|
||||
}
|
||||
|
||||
:global(.darkApp) .serviceIcon > path {
|
||||
fill: none !important;
|
||||
stroke: #fff !important;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
:global(.darkApp) .serviceIcon g path {
|
||||
fill: none !important;
|
||||
stroke: #fff !important;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
:global(.darkApp) .serviceIcon:hover > path,
|
||||
:global(.darkApp) .serviceIcon:hover g path {
|
||||
stroke: #fff !important;
|
||||
filter: drop-shadow(0 0 6px #fff);
|
||||
}
|
||||
|
||||
/* Additional dark theme enhancements */
|
||||
:global(.darkApp) .homeContainer::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
rgba(0, 0, 0, 0.3) 0%,
|
||||
rgba(0, 0, 0, 0.1) 100%
|
||||
);
|
||||
pointer-events: none;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
/* :global(.darkApp) .homeContainer > * {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
} */
|
||||
|
||||
/* Glass morphism effect for cards in dark mode */
|
||||
:global(.darkApp) .homeServiceCard {
|
||||
backdrop-filter: blur(12px);
|
||||
border-radius: 50px;
|
||||
}
|
||||
|
||||
/* Enhanced text readability in dark mode */
|
||||
:global(.darkApp) .item-description {
|
||||
color: #b0b0b0 !important;
|
||||
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
:global(.darkApp) .ant-typography {
|
||||
color: #ffffff !important;
|
||||
}
|
||||
|
||||
:global(.darkApp) .ant-typography.ant-typography-secondary {
|
||||
color: #b0b0b0 !important;
|
||||
}
|
||||
|
||||
:global(.darkApp) .deliveryIcon path,
|
||||
:global(.darkApp) .deliveryIcon circle {
|
||||
fill: #fff !important;
|
||||
stroke: #fff !important;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.serviceIcon {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
.dineInIcon {
|
||||
margin-top: -1px;
|
||||
}
|
||||
|
||||
.pickupIcon {
|
||||
margin-top: -2px;
|
||||
}
|
||||
|
||||
.roomIcon {
|
||||
margin-top: -2px;
|
||||
}
|
||||
|
||||
.officeIcon {
|
||||
margin-top: -1px;
|
||||
}
|
||||
|
||||
.bookingIcon {
|
||||
margin-top: -1px;
|
||||
}
|
||||
|
||||
.deliveryIcon {
|
||||
margin-top: -1px;
|
||||
}
|
||||
|
||||
/* Ultra-wide Desktop Styles (1920px+) */
|
||||
@media (min-width: 1920px) {
|
||||
.homeContainer {
|
||||
padding: 8vh 12vw;
|
||||
max-width: 1600px;
|
||||
gap: 40px;
|
||||
}
|
||||
|
||||
.logo {
|
||||
width: 160px;
|
||||
height: 160px;
|
||||
margin: 0 auto 32px;
|
||||
border-radius: 28px !important;
|
||||
box-shadow: 0 16px 48px rgba(0, 0, 0, 0.25);
|
||||
border: 4px solid rgba(255, 255, 255, 0.98);
|
||||
display: block;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.logo:hover {
|
||||
transform: scale(1.08) rotate(-1deg);
|
||||
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
|
||||
border-color: rgba(255, 183, 0, 0.6);
|
||||
}
|
||||
|
||||
/* Enhanced typography for ultra-wide desktop */
|
||||
.homeContainer h5 {
|
||||
font-size: 32px !important;
|
||||
font-weight: 600 !important;
|
||||
margin-bottom: 16px !important;
|
||||
letter-spacing: -0.6px;
|
||||
}
|
||||
|
||||
.homeContainer p {
|
||||
font-size: 20px !important;
|
||||
line-height: 1.7 !important;
|
||||
max-width: 700px;
|
||||
margin: 0 auto;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.homeServiceCard {
|
||||
height: 80px;
|
||||
padding: 24px 32px !important;
|
||||
border-radius: 24px;
|
||||
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
|
||||
.homeServiceCard:hover {
|
||||
transform: translateY(-6px);
|
||||
box-shadow: 0 16px 40px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.serviceIcon {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
}
|
||||
|
||||
/* Enhanced service card typography */
|
||||
.homeServiceCard h5 {
|
||||
font-size: 20px !important;
|
||||
font-weight: 600 !important;
|
||||
letter-spacing: -0.3px;
|
||||
}
|
||||
|
||||
.servicesGrid {
|
||||
max-width: 1200px;
|
||||
gap: 24px;
|
||||
padding: 0 40px;
|
||||
margin: 40px 0;
|
||||
}
|
||||
|
||||
.servicesGrid.twoColumns {
|
||||
max-width: 700px;
|
||||
}
|
||||
|
||||
.servicesGrid.manyServices {
|
||||
max-width: 700px;
|
||||
}
|
||||
|
||||
/* Social icons for ultra-wide desktop */
|
||||
.socialIconsContainer {
|
||||
gap: 36px;
|
||||
padding: 20px 36px;
|
||||
border-radius: 70px;
|
||||
bottom: 40px;
|
||||
}
|
||||
|
||||
.socialIcon {
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
}
|
||||
|
||||
.socialIcon:hover {
|
||||
transform: scale(1.25);
|
||||
filter: drop-shadow(0 8px 16px rgba(0, 0, 0, 0.3));
|
||||
}
|
||||
}
|
||||
|
||||
/* Enhanced responsive behavior for very tall screens */
|
||||
@media (min-height: 1000px) {
|
||||
.homeContainer {
|
||||
justify-content: center;
|
||||
/* padding-top: 15vh; */
|
||||
}
|
||||
}
|
||||
|
||||
/* Enhanced responsive behavior for very short screens */
|
||||
@media (max-height: 600px) {
|
||||
.homeContainer {
|
||||
gap: 20px;
|
||||
padding: 4vh 6vw;
|
||||
}
|
||||
|
||||
.logo {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
margin-bottom: 16px;
|
||||
border-radius: 12px !important;
|
||||
border: 2px solid rgba(255, 255, 255, 0.6);
|
||||
}
|
||||
|
||||
.servicesGrid {
|
||||
margin: 16px 0;
|
||||
}
|
||||
|
||||
.socialIconsContainer {
|
||||
bottom: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Landscape orientation optimizations */
|
||||
@media (orientation: landscape) and (max-height: 768px) {
|
||||
.homeContainer {
|
||||
gap: 24px;
|
||||
padding: 4vh 8vw;
|
||||
}
|
||||
|
||||
.logo {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
margin-bottom: 16px;
|
||||
border-radius: 16px !important;
|
||||
border: 2px solid rgba(255, 255, 255, 0.7);
|
||||
}
|
||||
|
||||
.homeContainer h5 {
|
||||
font-size: 24px !important;
|
||||
margin-bottom: 8px !important;
|
||||
}
|
||||
|
||||
.homeContainer p {
|
||||
font-size: 14px !important;
|
||||
}
|
||||
|
||||
.homeServiceCard {
|
||||
height: 60px;
|
||||
padding: 16px 24px !important;
|
||||
}
|
||||
|
||||
.servicesGrid {
|
||||
margin: 16px 0;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.socialIconsContainer {
|
||||
bottom: 20px;
|
||||
padding: 12px 24px;
|
||||
}
|
||||
}
|
||||
197
src/pages/search/page.tsx
Normal file
197
src/pages/search/page.tsx
Normal file
@@ -0,0 +1,197 @@
|
||||
import { ProBlack2 } from "ThemeConstants";
|
||||
import { Button, Divider, Input, Row } from "antd";
|
||||
import SearchIcon from "components/Icons/SearchIcon";
|
||||
import LoadingSpinner from "components/LoadingSpinner";
|
||||
import ProHeader from "components/ProHeader/ProHeader";
|
||||
import ProText from "components/ProText";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Link, useParams, useSearchParams } from "react-router-dom";
|
||||
import { useGetMenuQuery } from "redux/api/others";
|
||||
import { useAppSelector } from "redux/hooks";
|
||||
import { Product } from "utils/types/appTypes";
|
||||
import { ProductCard } from "../menu/components/ProductCard/ProductCard";
|
||||
import styles from "./search.module.css";
|
||||
|
||||
export default function SearchPage() {
|
||||
const { t } = useTranslation();
|
||||
const { id } = useParams();
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const { themeName } = useAppSelector((state) => state.theme);
|
||||
const [searchQuery, setSearchQuery] = useState(searchParams.get("q") || "");
|
||||
const [searchResults, setSearchResults] = useState<Product[]>([]);
|
||||
const [isSearching, setIsSearching] = useState(false);
|
||||
const restaurantID = localStorage.getItem("restaurantID");
|
||||
|
||||
const { data: menuData } = useGetMenuQuery(restaurantID as string, {
|
||||
skip: !restaurantID,
|
||||
});
|
||||
|
||||
const handleSearch = useCallback(
|
||||
(query: string) => {
|
||||
if (!query.trim()) {
|
||||
setSearchResults([]);
|
||||
return;
|
||||
}
|
||||
|
||||
setIsSearching(true);
|
||||
|
||||
// Simulate search delay for better UX
|
||||
setTimeout(() => {
|
||||
const results = menuData?.products.filter((product: Product) => {
|
||||
const searchLower = query.toLowerCase();
|
||||
const nameMatch = product.name?.toLowerCase().includes(searchLower);
|
||||
const descriptionMatch = product.description
|
||||
?.toLowerCase()
|
||||
.includes(searchLower);
|
||||
|
||||
return nameMatch || descriptionMatch;
|
||||
});
|
||||
|
||||
setSearchResults(results);
|
||||
setIsSearching(false);
|
||||
}, 300);
|
||||
},
|
||||
[menuData]
|
||||
);
|
||||
|
||||
// Handle input change and update search params
|
||||
const handleInputChange = useCallback(
|
||||
(value: string) => {
|
||||
setSearchQuery(value);
|
||||
|
||||
// Update search params
|
||||
if (value.trim()) {
|
||||
setSearchParams({ q: value });
|
||||
} else {
|
||||
setSearchParams({});
|
||||
}
|
||||
},
|
||||
[setSearchParams]
|
||||
);
|
||||
|
||||
// Debounced search effect
|
||||
useEffect(() => {
|
||||
const timeoutId = setTimeout(() => {
|
||||
handleSearch(searchQuery);
|
||||
}, 300); // 0.3 second debounce
|
||||
|
||||
return () => clearTimeout(timeoutId);
|
||||
}, [searchQuery, handleSearch]);
|
||||
|
||||
// Initialize search query from URL params on mount
|
||||
useEffect(() => {
|
||||
const initialQuery = searchParams.get("q") || "";
|
||||
if (initialQuery !== searchQuery) {
|
||||
setSearchQuery(initialQuery);
|
||||
}
|
||||
}, []); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
return (
|
||||
<>
|
||||
<ProHeader customRoute={`/${id}/menu`}>{t("menu.search")}</ProHeader>
|
||||
|
||||
<div
|
||||
style={{
|
||||
height: "82vh",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: 16,
|
||||
scrollBehavior: "smooth",
|
||||
overflow: "auto",
|
||||
scrollbarWidth: "none",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
padding: 16,
|
||||
backgroundColor: themeName === "light" ? "white" : ProBlack2,
|
||||
}}
|
||||
>
|
||||
<Input
|
||||
placeholder="Search"
|
||||
prefix={
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
}}
|
||||
>
|
||||
<SearchIcon className={styles.searchIcon} />
|
||||
<Divider type="vertical" style={{ top: 0, height: "1.3em" }} />
|
||||
</div>
|
||||
}
|
||||
style={{
|
||||
width: "100%",
|
||||
borderRadius: 888,
|
||||
height: 45,
|
||||
border: "none",
|
||||
}}
|
||||
value={searchQuery}
|
||||
onChange={(e) => {
|
||||
handleInputChange(e.target.value);
|
||||
}}
|
||||
size="large"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
style={{
|
||||
padding: 16,
|
||||
height: "100%",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: 16,
|
||||
overflow: "auto",
|
||||
scrollbarWidth: "none",
|
||||
}}
|
||||
>
|
||||
{searchQuery && isSearching ? (
|
||||
<div className={styles.loadingContainer}>
|
||||
<LoadingSpinner size="large" spinning={true} showText={true} />
|
||||
</div>
|
||||
) : searchQuery && searchResults?.length > 0 ? (
|
||||
<ProductCard products={searchResults} />
|
||||
) : searchQuery && searchResults?.length === 0 && !isSearching ? (
|
||||
<div className={styles.noResults}>
|
||||
<ProText className={styles.noResultsText}>
|
||||
{t("menu.noResultsFound")}
|
||||
</ProText>
|
||||
</div>
|
||||
) : !searchQuery ? (
|
||||
<div className={styles.noResults}>
|
||||
<ProText className={styles.noResultsText}>
|
||||
{t("menu.searchPlaceholder") ||
|
||||
"Start typing to search for products..."}
|
||||
</ProText>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
<Row
|
||||
style={{
|
||||
width: "100%",
|
||||
padding: "16px 16px 0",
|
||||
position: "fixed",
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
backgroundColor: themeName === "light" ? "white" : ProBlack2,
|
||||
boxShadow: "0px -1px 3px rgba(0, 0, 0, 0.1)",
|
||||
height: "10vh",
|
||||
zIndex: 999,
|
||||
}}
|
||||
>
|
||||
<Link to={`${id}/cart`} style={{ width: "100%" }}>
|
||||
<Button
|
||||
type="primary"
|
||||
shape="round"
|
||||
style={{ width: "100%", height: 48, marginBottom: 16 }}
|
||||
>
|
||||
{t("menu.cart")}
|
||||
</Button>
|
||||
</Link>
|
||||
</Row>
|
||||
</>
|
||||
);
|
||||
}
|
||||
18
src/pages/search/search.module.css
Normal file
18
src/pages/search/search.module.css
Normal file
@@ -0,0 +1,18 @@
|
||||
.noResults {
|
||||
text-align: center;
|
||||
padding: 48px 24px;
|
||||
}
|
||||
|
||||
.noResultsText {
|
||||
font-size: 16px !important;
|
||||
color: #6b7280 !important;
|
||||
}
|
||||
|
||||
.searchIcon {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
:global(.darkApp) .noResultsText {
|
||||
color: #9ca3af !important;
|
||||
}
|
||||
156
src/pages/split-bill/SplitBillPage.module.css
Normal file
156
src/pages/split-bill/SplitBillPage.module.css
Normal file
@@ -0,0 +1,156 @@
|
||||
.orderSummary :global(.ant-card-body) {
|
||||
padding: 16px !important;
|
||||
}
|
||||
|
||||
/* Enhanced responsive order summary */
|
||||
@media (min-width: 769px) and (max-width: 1024px) {
|
||||
.orderSummary {
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
}
|
||||
|
||||
.orderSummary {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
/* 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;
|
||||
}
|
||||
|
||||
/* Enhanced responsive summary rows */
|
||||
@media (min-width: 769px) and (max-width: 1024px) {
|
||||
.summaryRow {
|
||||
padding: 12px 0;
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
@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 {
|
||||
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 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);
|
||||
}
|
||||
}
|
||||
17
src/pages/split-bill/components/PayForActions.tsx
Normal file
17
src/pages/split-bill/components/PayForActions.tsx
Normal file
@@ -0,0 +1,17 @@
|
||||
import ActionsButtons from "components/ActionsButtons/ActionsButtons";
|
||||
import { selectCart, setTmp } from "features/order/orderSlice";
|
||||
import { useAppDispatch, useAppSelector } from "redux/hooks";
|
||||
|
||||
export default function PayForActions() {
|
||||
const dispatch = useAppDispatch();
|
||||
const { tmp } = useAppSelector(selectCart);
|
||||
|
||||
return (
|
||||
<ActionsButtons
|
||||
quantity={tmp?.payFor || 1}
|
||||
setQuantity={(value) => dispatch(setTmp({ ...tmp, payFor: value }))}
|
||||
max={tmp?.totalPeople || 10}
|
||||
min={1}
|
||||
/>
|
||||
);
|
||||
}
|
||||
46
src/pages/split-bill/components/PaymentSummary.tsx
Normal file
46
src/pages/split-bill/components/PaymentSummary.tsx
Normal file
@@ -0,0 +1,46 @@
|
||||
import { Card, Divider, Space } from "antd";
|
||||
import ArabicPrice from "components/ArabicPrice";
|
||||
import ProText from "components/ProText";
|
||||
import { selectCart, selectCartTotal } from "features/order/orderSlice";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useAppSelector } from "redux/hooks";
|
||||
import styles from "../SplitBillPage.module.css";
|
||||
|
||||
export default function PaymentSummary() {
|
||||
const { t } = useTranslation();
|
||||
const { tmp } = useAppSelector(selectCart);
|
||||
const getTotal = useAppSelector(selectCartTotal);
|
||||
const { isRTL } = useAppSelector((state) => state.locale);
|
||||
|
||||
const subtotal = getTotal;
|
||||
const tax = subtotal * 0.1; // 10% tax
|
||||
const total = subtotal + tax;
|
||||
|
||||
const costPerPerson = total / (tmp?.totalPeople || 1);
|
||||
const remainingAmount = total - (tmp?.payFor || 1) * costPerPerson;
|
||||
|
||||
return (
|
||||
<Card className={`${styles.orderSummary}`}>
|
||||
<Space direction="vertical" style={{ width: "100%" }}>
|
||||
<div className={styles.summaryRow}>
|
||||
<ProText style={{ color: "rgba(67, 78, 92, 1)" }}>
|
||||
{t("checkout.remainingAmount")}
|
||||
</ProText>
|
||||
<ArabicPrice
|
||||
price={remainingAmount}
|
||||
style={{ color: "rgba(67, 78, 92, 1)" }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Divider className={styles.summaryDivider} />
|
||||
<div className={`${styles.summaryRow} ${styles.totalRow}`}>
|
||||
<ProText strong>{t("checkout.totalAmount")}</ProText>
|
||||
<ArabicPrice
|
||||
price={total}
|
||||
strong
|
||||
/>
|
||||
</div>
|
||||
</Space>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
17
src/pages/split-bill/components/TotalPeopleActions.tsx
Normal file
17
src/pages/split-bill/components/TotalPeopleActions.tsx
Normal file
@@ -0,0 +1,17 @@
|
||||
import ActionsButtons from "components/ActionsButtons/ActionsButtons";
|
||||
import { selectCart, setTmp } from "features/order/orderSlice";
|
||||
import { useAppDispatch, useAppSelector } from "redux/hooks";
|
||||
|
||||
export default function TotalPeopleActions() {
|
||||
const dispatch = useAppDispatch();
|
||||
const { tmp } = useAppSelector(selectCart);
|
||||
|
||||
return (
|
||||
<ActionsButtons
|
||||
quantity={tmp?.totalPeople || 1}
|
||||
setQuantity={(value) => dispatch(setTmp({ ...tmp, totalPeople: value }))}
|
||||
max={10}
|
||||
min={1}
|
||||
/>
|
||||
);
|
||||
}
|
||||
148
src/pages/split-bill/page.tsx
Normal file
148
src/pages/split-bill/page.tsx
Normal file
@@ -0,0 +1,148 @@
|
||||
import { Button } from "antd";
|
||||
import PeopleIcon from "components/Icons/PeopleIcon";
|
||||
import PaymentMethods from "components/PaymentMethods/PaymentMethods";
|
||||
import ProHeader from "components/ProHeader/ProHeader";
|
||||
import ProInputCard from "components/ProInputCard/ProInputCard";
|
||||
import ProText from "components/ProText";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Link, useParams } from "react-router-dom";
|
||||
import { useAppSelector } from "redux/hooks";
|
||||
import { ProBlack2 } from "ThemeConstants";
|
||||
import PayForActions from "./components/PayForActions";
|
||||
import PaymentSummary from "./components/PaymentSummary";
|
||||
import TotalPeopleActions from "./components/TotalPeopleActions";
|
||||
|
||||
export default function SplitBillPage() {
|
||||
const { t } = useTranslation();
|
||||
const { id } = useParams();
|
||||
const { themeName } = useAppSelector((state) => state.theme);
|
||||
|
||||
return (
|
||||
<>
|
||||
<ProHeader>{t("checkout.splitBill")}</ProHeader>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
height: "82vh",
|
||||
padding: 16,
|
||||
gap: 16,
|
||||
overflow: "auto",
|
||||
scrollbarWidth: "none",
|
||||
}}
|
||||
>
|
||||
<ProInputCard title={t("checkout.splitBill")}>
|
||||
<div
|
||||
style={{ display: "flex", flexDirection: "column", gap: "1rem" }}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
gap: "1rem",
|
||||
padding: 8,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{ display: "flex", flexDirection: "row", gap: "1rem" }}
|
||||
>
|
||||
<Button
|
||||
type="text"
|
||||
shape="circle"
|
||||
style={{
|
||||
backgroundColor: "rgba(95, 108, 123, 0.05)",
|
||||
position: "relative",
|
||||
top: -5,
|
||||
}}
|
||||
>
|
||||
<PeopleIcon />
|
||||
</Button>
|
||||
<ProText
|
||||
style={{
|
||||
fontSize: "1rem",
|
||||
marginTop: 2,
|
||||
color: "rgba(67, 78, 92, 1)",
|
||||
}}
|
||||
>
|
||||
{t("checkout.totalPeople")}
|
||||
</ProText>
|
||||
</div>
|
||||
|
||||
<TotalPeopleActions />
|
||||
</div>
|
||||
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
gap: "1rem",
|
||||
padding: 8,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{ display: "flex", flexDirection: "row", gap: "1rem" }}
|
||||
>
|
||||
<Button
|
||||
type="text"
|
||||
shape="circle"
|
||||
style={{
|
||||
backgroundColor: "rgba(95, 108, 123, 0.05)",
|
||||
position: "relative",
|
||||
top: -5,
|
||||
}}
|
||||
>
|
||||
<PeopleIcon />
|
||||
</Button>
|
||||
<ProText
|
||||
style={{
|
||||
fontSize: "1rem",
|
||||
marginTop: 2,
|
||||
color: "rgba(67, 78, 92, 1)",
|
||||
}}
|
||||
>
|
||||
{t("checkout.payFor")}
|
||||
</ProText>
|
||||
</div>
|
||||
|
||||
<PayForActions />
|
||||
</div>
|
||||
</div>
|
||||
</ProInputCard>
|
||||
<PaymentMethods />
|
||||
<PaymentSummary />
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
width: "100%",
|
||||
padding: "16px 16px 0",
|
||||
position: "fixed",
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
height: "10vh",
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-around",
|
||||
gap: "1rem",
|
||||
backgroundColor: themeName === "light" ? "white" : ProBlack2,
|
||||
}}
|
||||
>
|
||||
<Link to={`${id}/order`} style={{ width: "100%" }}>
|
||||
<Button
|
||||
type="primary"
|
||||
shape="round"
|
||||
style={{
|
||||
width: "100%",
|
||||
height: 48,
|
||||
marginBottom: 16,
|
||||
boxShadow: "none",
|
||||
}}
|
||||
>
|
||||
{t("checkout.placeOrder")}
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user