Overview
The ProductModal component displays a full-screen modal overlay that shows all sub-product options for a selected product. Users can add items to cart, adjust quantities, and see special restrictions like damacana time limits.
Component Location
File : src/components/ProductModal.js
Props
Product object containing product details and sub-products Product name displayed in modal header
Emoji or icon displayed in modal header (e.g., ”💧”, ”🥤”)
Array of sub-product objects to display in the modal Show SubProduct structure
Unique sub-product identifier (e.g., “11”, “12”)
Sub-product name (e.g., “19L Damacana”, “5L Pet”)
Price with currency (e.g., “25 TL”, “15 TL”)
Fallback emoji when image unavailable
Controls modal visibility. When true, modal is displayed.
Callback function triggered when user closes the modal (via backdrop click or close button)
Usage Example
Basic Implementation
import React , { useState } from 'react' ;
import ProductModal from './components/ProductModal' ;
function App () {
const [ isModalOpen , setIsModalOpen ] = useState ( false );
const product = {
id: '1' ,
name: 'Su' ,
imagePlaceholder: '💧' ,
subProducts: [
{
id: '11' ,
name: '19L Damacana' ,
price: '25 TL' ,
image: '/images/damacana.png' ,
imagePlaceholder: '💧'
},
{
id: '12' ,
name: '5L Pet Şişe' ,
price: '15 TL' ,
image: '/images/5l.png' ,
imagePlaceholder: '💧'
}
]
};
return (
<>
< button onClick = { () => setIsModalOpen ( true ) } >
Ürün Seçeneklerini Gör
</ button >
< ProductModal
product = { product }
isOpen = { isModalOpen }
onClose = { () => setIsModalOpen ( false ) }
/>
</>
);
}
With ProductCard Integration
const ProductCard = ({ product }) => {
const [ isModalOpen , setIsModalOpen ] = useState ( false );
return (
<>
< div onClick = { () => setIsModalOpen ( true ) } className = "product-card" >
< ProductImage src = { product . image } alt = { product . name } />
< h3 > { product . name } </ h3 >
< p > { product . price } </ p >
</ div >
< ProductModal
product = { product }
isOpen = { isModalOpen }
onClose = { () => setIsModalOpen ( false ) }
/>
</>
);
};
Modal Structure
The modal consists of three main sections:
Displays product information and close button:
< div className = "modal-header" >
< div className = "modal-header-container" >
< div className = "modal-header-info" >
< div className = "modal-header-icon" >
< span > { product . imagePlaceholder } </ span >
</ div >
< div className = "modal-header-text" >
< h2 > { product . name } </ h2 >
< p > { t ( 'productDescription' ) } </ p >
</ div >
</ div >
< button onClick = { handleCloseClick } className = "modal-close-btn" >
< span > ‹ </ span >
</ button >
</ div >
</ div >
2. Modal Body (Sub-Products Grid)
Displays sub-products in a responsive grid:
< div className = "modal-body" >
< div className = "modal-body-container" >
< div className = "modal-grid" >
{ product . subProducts ?. map (( subProduct ) => (
< SubProductCard
key = { subProduct . id }
subProduct = { subProduct }
/>
)) }
</ div >
</ div >
</ div >
Provides user guidance:
< div className = "modal-footer" >
< div className = "modal-footer-container" >
< p > 💬 Ürüne tıklayarak WhatsApp ile sipariş verebilirsiniz </ p >
</ div >
</ div >
SubProductCard Component
Each sub-product is rendered with its own card that integrates with the cart:
Cart Integration
const SubProductCard = ({ subProduct }) => {
const { addItem , removeItem , getItemQuantity } = useCart ();
const quantity = getItemQuantity ( subProduct . id );
// ... component implementation
};
Damacana Restrictions
Sub-products with ID ending in “1” (damacana) are subject to time restrictions:
const isDamacanaProduct = isDamacana ( subProduct . id );
const damacanaCheck = isDamacanaProduct ? isDamacanaOrderAllowed () : { isAllowed: true };
const isDamacanaDisabled = isDamacanaProduct && ! damacanaCheck . isAllowed ;
Damacana Rules (from src/config/damacanaLimits.js:4-30):
Cutoff Time : 19:00 (orders not accepted after)
Start Time : 08:30 (orders accepted from)
Pattern : Product IDs ending in “1” (11, 21, 31, etc.)
Quantity Controls
When items are in cart, quantity controls appear:
{ quantity > 0 && (
< div className = "quantity-controls" >
< button onClick = { handleRemoveFromCart } className = "quantity-btn minus-btn" >
−
</ button >
< span className = "quantity-display" > { quantity } </ span >
< button onClick = { handleAddToCart } className = "quantity-btn plus-btn" >
+
</ button >
</ div >
)}
For items not in cart:
{ quantity === 0 && ! isDamacanaDisabled && (
< button onClick = { handleAddToCart } className = "add-to-cart-btn" >
{ t ( 'addToCart' ) }
</ button >
)}
Damacana Warning
Shown when damacana orders are restricted:
{ isDamacanaDisabled && (
< div className = "damacana-warning" >
🕐 { damacanaCheck . message }
</ div >
)}
Modal Closing Behavior
The modal can be closed in two ways:
1. Backdrop Click
const handleBackdropClick = ( e ) => {
if ( e . target === e . currentTarget ) {
onClose ();
}
};
Backdrop clicking only closes the modal if the click target is the backdrop itself, preventing accidental closes when clicking modal content.
const handleCloseClick = () => {
onClose ();
};
Cart Operations
Adding Items
const handleAddToCart = ( e ) => {
e . stopPropagation ();
// Check damacana restrictions
if ( isDamacanaProduct && ! damacanaCheck . isAllowed ) {
return ;
}
addItem ( subProduct );
};
Removing Items
const handleRemoveFromCart = ( e ) => {
e . stopPropagation ();
removeItem ( subProduct );
};
Card Click to Add
Clicking the entire card adds one item:
const handleCardClick = () => {
if ( isDamacanaProduct && ! damacanaCheck . isAllowed ) {
return ;
}
addItem ( subProduct );
};
Accessibility Features
Keyboard Support
onKeyDown = {isDamacanaDisabled ? undefined : ( e ) => {
if ( e . key === 'Enter' || e . key === ' ' ) {
e . preventDefault ();
handleCardClick ();
}
}}
ARIA Labels
aria - label = {
isDamacanaDisabled
? ` ${ subProduct . name } - ${ damacanaCheck . message } `
: ` ${ subProduct . name } ${ t ( 'addToCart' ) } `
}
Tab Index Management
tabIndex = {isDamacanaDisabled ? - 1 : 0 }
Disabled damacana products have tabIndex={-1} to prevent keyboard navigation to unavailable items.
Styling
Full-Screen Modal
.modal-overlay {
position : fixed ;
top : 0 ;
left : 0 ;
right : 0 ;
bottom : 0 ;
z-index : 1000 ;
}
.modal-content {
position : relative ;
width : 100 % ;
height : 100 vh ;
background : white ;
overflow-y : auto ;
}
Responsive Grid
.modal-grid {
display : grid ;
grid-template-columns : repeat ( auto-fill , minmax ( 250 px , 1 fr ));
gap : 1.5 rem ;
padding : 2 rem ;
}
Disabled State
.sub-product-card-disabled {
opacity : 0.6 ;
cursor : not-allowed ;
filter : grayscale ( 50 % );
}
Internationalization
The modal supports multiple languages:
import { t } from '../config/language' ;
// Translations used:
t ( 'productDescription' ) // "Lütfen bir seçenek seçin" / "Please select an option"
t ( 'addToCart' ) // "Sepete Ekle" / "Add to Cart"
Complete Source Code
src/components/ProductModal.js
import React from 'react' ;
import { useCart } from '../context/CartContext' ;
import { isDamacana , isDamacanaOrderAllowed } from '../config/damacanaLimits' ;
import ProductImage from './ProductImage' ;
import { t } from '../config/language' ;
const SubProductCard = ({ subProduct }) => {
const { addItem , removeItem , getItemQuantity } = useCart ();
const quantity = getItemQuantity ( subProduct . id );
const isDamacanaProduct = isDamacana ( subProduct . id );
const damacanaCheck = isDamacanaProduct ? isDamacanaOrderAllowed () : { isAllowed: true };
const handleAddToCart = ( e ) => {
e . stopPropagation ();
if ( isDamacanaProduct && ! damacanaCheck . isAllowed ) {
return ;
}
addItem ( subProduct );
};
const handleRemoveFromCart = ( e ) => {
e . stopPropagation ();
removeItem ( subProduct );
};
const handleCardClick = () => {
if ( isDamacanaProduct && ! damacanaCheck . isAllowed ) {
return ;
}
addItem ( subProduct );
};
const isDamacanaDisabled = isDamacanaProduct && ! damacanaCheck . isAllowed ;
return (
< div
className = { `sub-product-card ${ isDamacanaDisabled ? 'sub-product-card-disabled' : '' } ` }
onClick = { isDamacanaDisabled ? undefined : handleCardClick }
role = "button"
tabIndex = { isDamacanaDisabled ? - 1 : 0 }
onKeyDown = { isDamacanaDisabled ? undefined : ( e ) => {
if ( e . key === 'Enter' || e . key === ' ' ) {
e . preventDefault ();
handleCardClick ();
}
} }
aria-label = {
isDamacanaDisabled
? ` ${ subProduct . name } - ${ damacanaCheck . message } `
: ` ${ subProduct . name } ${ t ( 'addToCart' ) } `
}
>
< div className = "sub-product-image" >
< ProductImage
src = { subProduct . image }
alt = { ` ${ subProduct . name } resmi` }
placeholder = { subProduct . imagePlaceholder }
className = "sub-product-image-placeholder"
/>
</ div >
< h4 className = "sub-product-name" > { subProduct . name } </ h4 >
< div className = "sub-product-price" > { subProduct . price } </ div >
{ quantity > 0 && (
< div className = "quantity-controls" >
< button onClick = { handleRemoveFromCart } className = "quantity-btn minus-btn" >
−
</ button >
< span className = "quantity-display" > { quantity } </ span >
< button onClick = { handleAddToCart } className = "quantity-btn plus-btn" >
+
</ button >
</ div >
) }
{ quantity === 0 && ! isDamacanaDisabled && (
< button onClick = { handleAddToCart } className = "add-to-cart-btn" >
{ t ( 'addToCart' ) }
</ button >
) }
{ isDamacanaDisabled && (
< div className = "damacana-warning" >
🕐 { damacanaCheck . message }
</ div >
) }
</ div >
);
};
const ProductModal = ({ product , isOpen , onClose }) => {
if ( ! isOpen || ! product ) return null ;
const handleBackdropClick = ( e ) => {
if ( e . target === e . currentTarget ) {
onClose ();
}
};
const handleCloseClick = () => {
onClose ();
};
return (
< div className = "modal-overlay" onClick = { handleBackdropClick } >
< div className = "modal-backdrop" ></ div >
< div className = "modal-content" >
< div className = "modal-header" >
< div className = "modal-header-container" >
< div className = "modal-header-info" >
< div className = "modal-header-icon" >
< span > { product . imagePlaceholder } </ span >
</ div >
< div className = "modal-header-text" >
< h2 > { product . name } </ h2 >
< p > { t ( 'productDescription' ) } </ p >
</ div >
</ div >
< button onClick = { handleCloseClick } className = "modal-close-btn" >
< span > ‹ </ span >
</ button >
</ div >
</ div >
< div className = "modal-body" >
< div className = "modal-body-container" >
< div className = "modal-grid" >
{ product . subProducts ?. map (( subProduct ) => (
< SubProductCard
key = { subProduct . id }
subProduct = { subProduct }
/>
)) }
</ div >
</ div >
</ div >
< div className = "modal-footer" >
< div className = "modal-footer-container" >
< p > 💬 Ürüne tıklayarak WhatsApp ile sipariş verebilirsiniz </ p >
</ div >
</ div >
</ div >
</ div >
);
};
export default ProductModal ;
Best Practices
Event Propagation : Always use e.stopPropagation() in button click handlers to prevent triggering the card’s click handler.
Conditional Rendering : The modal returns null when closed, removing it from the DOM completely for better performance.
Always check damacana restrictions before allowing add-to-cart actions to comply with business rules.