menu list: refactor code

This commit is contained in:
2025-11-11 19:08:51 +03:00
parent 724f201d47
commit 0c14dbec5d
4 changed files with 314 additions and 474 deletions

View File

@@ -25,11 +25,11 @@
} }
.menuSection:first-child h3 { .menuSection:first-child h3 {
margin-top: 0px !important; margin-top: 0 !important;
} }
.menuSection h3 { .menuSection h3 {
margin: 30px 0px 15px 0px; margin: 30px 0 15px 0;
transition: color 0.3s ease; transition: color 0.3s ease;
} }
@@ -42,7 +42,7 @@
/* Enhanced responsive menu section headers */ /* Enhanced responsive menu section headers */
@media (min-width: 769px) and (max-width: 1024px) { @media (min-width: 769px) and (max-width: 1024px) {
.menuSection h3 { .menuSection h3 {
margin: 40px 0px 20px 0px; margin: 40px 0 20px 0;
font-size: 24px; font-size: 24px;
} }
@@ -59,7 +59,7 @@
} }
.menuSection h3 { .menuSection h3 {
margin: 50px 0px 25px 0px; margin: 50px 0 25px 0;
font-size: 28px; font-size: 28px;
} }
@@ -97,32 +97,6 @@
display: none; 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 { .menuItemImage {
object-fit: cover; object-fit: cover;
border-radius: 8px; border-radius: 8px;
@@ -154,24 +128,6 @@
gap: 8px; 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 */ /* Responsive adjustments */
@media (max-width: 768px) { @media (max-width: 768px) {
.container { .container {
@@ -214,10 +170,6 @@
margin-left: 200px; margin-left: 200px;
} }
/* .itemDescriptionIcons path {
fill: 000044 !important;
} */
@media (max-width: 768px) { @media (max-width: 768px) {
.sidebarCollapsed .pageContainer, .sidebarCollapsed .pageContainer,
.sidebarExpanded .pageContainer { .sidebarExpanded .pageContainer {
@@ -308,28 +260,6 @@
} }
} }
@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 { @keyframes cardEntrance {
0% { 0% {
transform: translateY(20px) scale(0.95); transform: translateY(20px) scale(0.95);
@@ -352,167 +282,6 @@
} }
} }
@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;
}
/* Animation for newly added items */
@keyframes badgePulse {
0% {
transform: scale(1);
}
50% {
transform: scale(1.2);
}
100% {
transform: scale(1);
}
}
/* Enhanced dark theme animations */
:global(.darkApp) .menuItem,
:global(.darkApp) .popularMenuItem {
animation: fadeInUp 0.3s ease-out;
}
@keyframes fadeInUp { @keyframes fadeInUp {
from { from {
opacity: 0; opacity: 0;
@@ -524,6 +293,12 @@
} }
} }
/* Enhanced dark theme animations */
:global(.darkApp) .menuItem,
:global(.darkApp) .popularMenuItem {
animation: fadeInUp 0.3s ease-out;
}
/* Smooth transitions for all elements */ /* Smooth transitions for all elements */
.container *, .container *,
.menuItem *, .menuItem *,
@@ -601,47 +376,10 @@
height: 60; height: 60;
} }
.loyaltyButton { /* Enhanced dark theme for container backgrounds */
position: absolute; :global(.darkApp) .container {
top: 1px; background-color: #0a0a0a !important;
left: 1px; border-bottom-color: #363636 !important;
z-index: 99;
background-color: var(--primary) !important;
color: white !important;
border: none !important;
}
:global(.ant-app-rtl) .loyaltyButton {
right: 1px !important;
}
.productLink {
border-radius: 8px;
}
/* Desktop unified height styles */
@media (min-width: 1025px) {
.productLink {
display: flex;
height: 100%;
width: 100%;
}
.productLink .ant-card {
height: 100%;
display: flex;
flex-direction: column;
justify-content: space-between;
}
.productLink .ant-card .ant-card-body {
flex: 1;
display: flex;
flex-direction: column;
justify-content: space-between;
}
} }
.categoryMenuItemImage { .categoryMenuItemImage {
@@ -649,6 +387,5 @@
object-fit: cover; object-fit: cover;
border-radius: 8px; border-radius: 8px;
text-align: center; text-align: center;
width: 100%;
overflow: hidden; overflow: hidden;
} }

View File

@@ -1,20 +1,11 @@
import { StarOutlined } from "@ant-design/icons";
import { Badge, Button, Card } from "antd";
import ArabicPrice from "components/ArabicPrice";
import ImageWithFallback from "components/ImageWithFallback";
import { ItemDescriptionIcons } from "components/ItemDescriptionIcons/ItemDescriptionIcons";
import ProText from "components/ProText"; import ProText from "components/ProText";
import ProTitle from "components/ProTitle"; import ProTitle from "components/ProTitle";
import useBreakPoint from "hooks/useBreakPoint";
import { AddToCartButton } from "pages/menu/components/AddToCartButton/AddToCartButton.tsx";
import { ProductPreviewDialog } from "pages/menu/components/ProductPreviewDialog";
import { useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { useNavigate, useParams } from "react-router-dom";
import { useAppSelector } from "redux/hooks"; import { useAppSelector } from "redux/hooks";
import { colors } from "ThemeConstants";
import { Product } from "utils/types/appTypes"; import { Product } from "utils/types/appTypes";
import styles from "./MenuList.module.css"; import styles from "./MenuList.module.css";
import ProductCard from "pages/menu/components/MenuList/ProductCard.tsx";
interface MenuListProps { interface MenuListProps {
data: data:
@@ -29,31 +20,10 @@ interface MenuListProps {
export function MenuList({ data, categoryRefs }: MenuListProps) { export function MenuList({ data, categoryRefs }: MenuListProps) {
const { isRTL } = useAppSelector((state) => state.locale); const { isRTL } = useAppSelector((state) => state.locale);
const products = data?.products; const products = data?.products;
const { isMobile, isTablet, isDesktop } = useBreakPoint();
const { items } = useAppSelector((state) => state.order);
const { subdomain } = useParams();
const navigate = useNavigate();
const { t } = useTranslation(); const { t } = useTranslation();
const { themeName } = useAppSelector((state) => state.theme); const { themeName } = useAppSelector((state) => state.theme);
// Dialog state
const [isDialogOpen, setIsDialogOpen] = useState(false);
// Handle product click - open dialog on desktop, navigate on mobile/tablet
const handleProductClick = (item: Product) => {
localStorage.setItem("product", JSON.stringify(item));
if (isDesktop) {
setIsDialogOpen(true);
} else {
navigate(`/${subdomain}/product/${item.id}`);
}
};
// Handle dialog close
const handleDialogClose = () => {
setIsDialogOpen(false);
};
// Show error state if data exists but has no products // Show error state if data exists but has no products
if (data && (!data.products || data.products.length === 0)) { if (data && (!data.products || data.products.length === 0)) {
return ( return (
@@ -107,176 +77,13 @@ export function MenuList({ data, categoryRefs }: MenuListProps) {
</ProTitle> </ProTitle>
<div className={styles.menuItemsGrid}> <div className={styles.menuItemsGrid}>
{categoryProducts.map((item: Product) => ( {categoryProducts.map((item: Product) => (
<div <ProductCard item={item} key={item.id} />
key={item.id}
className={styles.productLink}
onClick={() => handleProductClick(item)}
>
<Card
key={item.id}
style={{
borderRadius: 8,
overflow: "hide",
width: "100%",
boxShadow: "none",
}}
styles={{
body: {
display: "flex",
flexDirection: "column",
justifyContent: "space-between",
padding: item.description
? "16px 16px 8px 16px"
: "16px 16px 24px 16px",
overflow: "hide",
boxShadow: "none",
},
}}
>
<div
style={{
display: "flex",
alignItems: "center",
justifyContent: "space-between",
height: "100%",
gap: isMobile ? 10 : 16,
}}
>
<div
style={{
display: "flex",
flexDirection: "column",
justifyContent: "space-between",
gap: "0.5rem",
}}
>
<ProText
style={{
margin: 0,
display: "inline-block",
fontSize: "1rem",
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: 2,
overflow: "hidden",
textOverflow: "ellipsis",
wordWrap: "break-word",
overflowWrap: "break-word",
lineHeight: "1.5rem",
maxHeight: "3em",
fontSize: "1rem",
letterSpacing: "0.01em",
}}
>
{item.description}
</ProText>
)}
<div>
{item.original_price !== item.price && (
<ArabicPrice
price={item.original_price}
strong
style={{
fontSize: "1rem",
fontWeight: 700,
color: colors.primary,
textDecoration: "line-through",
marginRight: isRTL ? 0 : 10,
marginLeft: isRTL ? 10 : 0,
}}
/>
)}
<ArabicPrice
price={item.price}
strong
style={{
fontSize: "1rem",
fontWeight: 700,
color: colors.primary,
}}
/>
</div>
<div
style={{
position: "relative",
...(isRTL ? { right: -5 } : {}),
}}
>
<ItemDescriptionIcons
className={styles.itemDescriptionIcons}
/>
</div>
</div>
<div style={{ position: "relative" }}>
{item.isHasLoyalty && (
<Button
className={styles.loyaltyButton}
icon={<StarOutlined />}
style={{ width: 24, height: 24 }}
/>
)}
<Badge
size="default"
offset={[-3, 3]}
count={items
.filter((i) => i.id === item.id)
.reduce(
(total, item) => total + item.quantity,
0,
)}
color={colors.primary}
title={`${items
.filter((i) => i.id === item.id)
.reduce(
(total, item) => total + item.quantity,
0,
)} in cart`}
>
<ImageWithFallback
src={item.image_small || "/default.png"}
fallbackSrc="/default.png"
alt={item.name}
className={`${styles.popularMenuItemImage} ${
isMobile
? styles.popularMenuItemImageMobile
: isTablet
? styles.popularMenuItemImageTablet
: styles.popularMenuItemImageDesktop
}`}
width={90}
height={90}
/>
<AddToCartButton />
</Badge>
</div>
</div>
</Card>
</div>
))} ))}
</div> </div>
</div> </div>
); );
})} })}
</div> </div>
{/* Product Preview Dialog for Desktop */}
<ProductPreviewDialog isOpen={isDialogOpen} onClose={handleDialogClose} />
</> </>
); );
} }

View File

@@ -0,0 +1,87 @@
.productLink {
border-radius: 8px;
}
/* Desktop unified height styles */
@media (min-width: 1025px) {
.productLink {
display: flex;
height: 100%;
width: 100%;
}
.productLink .ant-card {
height: 100%;
display: flex;
flex-direction: column;
justify-content: space-between;
}
.productLink .ant-card .ant-card-body {
flex: 1;
display: flex;
flex-direction: column;
justify-content: space-between;
}
}
.itemDescription {
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;
}
}
.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;
}
.loyaltyButton {
position: absolute;
top: 1px;
left: 1px;
z-index: 99;
background-color: var(--primary) !important;
color: white !important;
border: none !important;
}
:global(.ant-app-rtl) .loyaltyButton {
right: 1px !important;
}
.itemDescriptionIcons svg {
fill: inherit;
}

View File

@@ -0,0 +1,209 @@
import styles from "pages/menu/components/MenuList/ProductCard.module.css";
import { Card, Button, Badge } from "antd";
import ProText from "components/ProText.tsx";
import ArabicPrice from "components/ArabicPrice";
import { colors } from "ThemeConstants.ts";
import { ItemDescriptionIcons } from "components/ItemDescriptionIcons/ItemDescriptionIcons.tsx";
import { StarOutlined } from "@ant-design/icons";
import ImageWithFallback from "components/ImageWithFallback";
import { AddToCartButton } from "pages/menu/components/AddToCartButton/AddToCartButton.tsx";
import { Product } from "utils/types/appTypes.ts";
import useBreakPoint from "hooks/useBreakPoint.ts";
import { useAppSelector } from "redux/hooks.ts";
import { useParams, useNavigate } from "react-router-dom";
import { ProductPreviewDialog } from "pages/menu/components/ProductPreviewDialog";
import { useState } from "react";
type Props = {
item: Product;
};
export default function ProductCard({ item }: Props) {
const { isRTL } = useAppSelector((state) => state.locale);
const { themeName } = useAppSelector((state) => state.theme);
const { isMobile, isTablet, isDesktop } = useBreakPoint();
const { items } = useAppSelector((state) => state.order);
const { subdomain } = useParams();
const navigate = useNavigate();
// Dialog state
const [isDialogOpen, setIsDialogOpen] = useState(false);
// Handle dialog close
const handleDialogClose = () => {
setIsDialogOpen(false);
};
// Handle product click - open dialog on desktop, navigate on mobile/tablet
const handleProductClick = (item: Product) => {
localStorage.setItem("product", JSON.stringify(item));
if (isDesktop) {
setIsDialogOpen(true);
} else {
navigate(`/${subdomain}/product/${item.id}`);
}
};
return (
<>
<div
key={item.id}
className={styles.productLink}
onClick={() => handleProductClick(item)}
>
<Card
key={item.id}
style={{
borderRadius: 8,
overflow: "hide",
width: "100%",
boxShadow: "none",
}}
styles={{
body: {
display: "flex",
flexDirection: "column",
justifyContent: "space-between",
padding: item.description
? "16px 16px 8px 16px"
: "16px 16px 24px 16px",
overflow: "hide",
boxShadow: "none",
},
}}
>
<div
style={{
display: "flex",
alignItems: "center",
justifyContent: "space-between",
height: "100%",
gap: isMobile ? 10 : 16,
}}
>
<div
style={{
display: "flex",
flexDirection: "column",
justifyContent: "space-between",
gap: "0.5rem",
}}
>
<ProText
style={{
margin: 0,
display: "inline-block",
fontSize: "1rem",
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: 2,
overflow: "hidden",
textOverflow: "ellipsis",
wordWrap: "break-word",
overflowWrap: "break-word",
lineHeight: "1.5rem",
maxHeight: "3em",
fontSize: "1rem",
letterSpacing: "0.01em",
}}
>
{item.description}
</ProText>
)}
<div>
{item.original_price !== item.price && (
<ArabicPrice
price={item.original_price}
strong
style={{
fontSize: "1rem",
fontWeight: 700,
color: colors.primary,
textDecoration: "line-through",
marginRight: isRTL ? 0 : 10,
marginLeft: isRTL ? 10 : 0,
}}
/>
)}
<ArabicPrice
price={item.price}
strong
style={{
fontSize: "1rem",
fontWeight: 700,
color: colors.primary,
}}
/>
</div>
<div
style={{
position: "relative",
...(isRTL ? { right: -5 } : {}),
}}
>
<ItemDescriptionIcons className={styles.itemDescriptionIcons} />
</div>
</div>
<div style={{ position: "relative" }}>
{item.isHasLoyalty && (
<Button
className={styles.loyaltyButton}
icon={<StarOutlined />}
style={{ width: 24, height: 24 }}
/>
)}
<Badge
size="default"
offset={[-3, 3]}
count={items
.filter((i) => i.id === item.id)
.reduce((total, item) => total + item.quantity, 0)}
color={colors.primary}
title={`${items
.filter((i) => i.id === item.id)
.reduce((total, item) => total + item.quantity, 0)} in cart`}
>
<ImageWithFallback
src={item.image_small || "/default.png"}
fallbackSrc="/default.png"
alt={item.name}
className={`${styles.popularMenuItemImage} ${
isMobile
? styles.popularMenuItemImageMobile
: isTablet
? styles.popularMenuItemImageTablet
: styles.popularMenuItemImageDesktop
}`}
width={90}
height={90}
/>
<AddToCartButton />
</Badge>
</div>
</div>
</Card>
</div>
{/* Product Preview Dialog for Desktop */}
{isDialogOpen && (
<ProductPreviewDialog
isOpen={isDialogOpen}
onClose={handleDialogClose}
/>
)}
</>
);
}