Overview
The PizzaCard component is a highly interactive, visually rich card that displays individual pizza items. It provides a seamless user experience for browsing pizzas and adding them to an order, with smooth animations and visual feedback.
Location : src/components/pizza/PizzaCard.tsx
Features
Click-to-detail navigation : Card surface navigates to pizza detail page
Quick add-to-order : Button with success animation feedback
Responsive image display : Hover effects with scale animation
Badge system : Displays category and recommendation status
Visual affordances : Gradient overlays and shadow transitions
Props
The pizza object containing all display and metadata information Pizza Interface :interface Pizza {
id : string ;
name : string ;
price : number ;
ingredients : string [];
category : 'Vegetarian' | 'Meat' | 'Seafood' | 'Spicy' ;
imageUrl : string ;
isRecommended ?: boolean ;
}
Usage Example
import PizzaCard from './components/pizza/PizzaCard' ;
import type { Pizza } from './types/pizza' ;
const pizza : Pizza = {
id: '1' ,
name: 'Margherita' ,
price: 12.99 ,
ingredients: [ 'Tomato' , 'Mozzarella' , 'Basil' ],
category: 'Vegetarian' ,
imageUrl: '/images/margherita.jpg' ,
isRecommended: true
};
function PizzaGrid () {
return (
< div className = "grid grid-cols-1 md:grid-cols-3 gap-6" >
< PizzaCard pizza = { pizza } />
</ div >
);
}
Component Structure
Image Section
The top section displays the pizza image with overlay badges:
< div className = "relative aspect-[4/3] overflow-hidden" >
< img
src = { pizza . imageUrl }
alt = { pizza . name }
className = "w-full h-full object-cover transition-transform duration-500 group-hover:scale-110"
/>
{ /* Gradient overlay on hover */ }
< div className = "absolute inset-0 bg-gradient-to-t from-black/60 via-transparent to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300" />
{ /* Badges */ }
< div className = "absolute top-3 left-3 flex flex-col gap-2" >
{ pizza . isRecommended && (
< span className = "flex items-center gap-1 bg-yellow-400 text-yellow-900 text-[10px] font-bold px-2 py-1 rounded-full shadow-sm" >
< Star size = { 10 } fill = "currentColor" />
RECOMMENDED
</ span >
) }
< span className = "bg-white/90 backdrop-blur-sm text-gray-800 text-[10px] font-bold px-2 py-1 rounded-full shadow-sm" >
{ pizza . category . toUpperCase () }
</ span >
</ div >
</ div >
Content Section
The bottom section contains pizza details and actions:
< div className = "p-5 flex flex-col flex-grow" >
{ /* Header with name and price */ }
< div className = "flex justify-between items-start mb-2" >
< h3 className = "text-lg font-bold text-gray-900 leading-tight group-hover:text-orange-600 transition-colors" >
{ pizza . name }
</ h3 >
< span className = "text-xl font-black text-orange-600" >
$ { pizza . price }
</ span >
</ div >
{ /* Ingredients list */ }
< p className = "text-sm text-gray-500 line-clamp-2 mb-4 flex-grow" >
{ pizza . ingredients . join ( ', ' ) }
</ p >
{ /* Action buttons */ }
< div className = "flex items-center gap-2 mt-auto" >
{ /* Add to order button */ }
{ /* Info button */ }
</ div >
</ div >
State Management
Local State
Tracks whether the pizza was recently added to show success feedback. Automatically resets after 2 seconds.
const [ isAdded , setIsAdded ] = useState ( false );
Redux Integration
The component dispatches to the order slice:
import { useAppDispatch } from '../../store/index.ts' ;
import { addToOrder } from '../../store/orderSlice.ts' ;
const dispatch = useAppDispatch ();
const handleAddClick = ( e : React . MouseEvent ) => {
e . stopPropagation (); // Prevent card click navigation
dispatch ( addToOrder ({ pizza , quantity: 1 }));
setIsAdded ( true );
setTimeout (() => setIsAdded ( false ), 2000 );
};
Navigation
Clicking anywhere on the card (except the buttons) navigates to the pizza detail page:
import { useNavigate } from 'react-router-dom' ;
const navigate = useNavigate ();
< div
onClick = {() => navigate ( `/pizza/ ${ pizza . id } ` )}
className = "cursor-pointer"
>
The button has two states with different visual presentations:
Default State :
< button className = "bg-orange-500 hover:bg-orange-600 text-white" >
< Plus size = { 18 } />
< span className = 'text-xs font-bold' > Add to Order </ span >
</ button >
Success State (shown for 2 seconds after adding):
< button className = "bg-green-500 text-white cursor-default" disabled >
< CheckCircle size = { 18 } className = "animate-bounce" />
< span className = 'text-xs font-bold uppercase' > Added! </ span >
</ button >
Provides visual feedback on hover:
< div className = "p-2.5 text-gray-400 group-hover:text-orange-500 group-hover:bg-orange-50 bg-gray-50 rounded-xl transition-all" >
< Info size = { 20 } />
</ div >
Visual Design
Hover Effects
Image zoom : scale-110 transform on hover
Gradient overlay : Fades in from bottom
Shadow elevation : shadow-sm to shadow-xl
Title color : Gray to orange transition
Border highlight : Transparent to border-gray-100
Aspect Ratio
Images maintain a 4:3 aspect ratio using Tailwind’s aspect ratio utility:
< div className = "relative aspect-[4/3] overflow-hidden" >
Typography Scale
Pizza name: text-lg font-bold
Price: text-xl font-black
Ingredients: text-sm text-gray-500
Badges: text-[10px] font-bold uppercase
Accessibility
Semantic HTML : Uses <button> for actions
Alt text : Images include descriptive alt attributes
Keyboard navigation : All interactive elements are focusable
Disabled state : Button properly disabled during success animation
Event propagation : stopPropagation() prevents conflicting navigation
Animation Details
transition-transform duration-500 group-hover : scale-110
Gradient Overlay
opacity-0 group-hover : opacity-100 transition-opacity duration-300
active: scale-95 transition-all
Success Icon
Integration Example
Complete example showing multiple cards with filtering:
import { useAppSelector } from './store' ;
import PizzaCard from './components/pizza/PizzaCard' ;
import PizzaFilters from './components/pizza/PizzaFilters' ;
function PizzaMenu () {
const { pizzas , filters } = useAppSelector (( state ) => state . pizza );
// Apply filters (simplified)
const filteredPizzas = pizzas . filter ( pizza => {
if ( filters . category !== 'all' && pizza . category !== filters . category ) {
return false ;
}
if ( pizza . price > filters . maxPrice ) {
return false ;
}
if ( filters . search && ! pizza . name . toLowerCase (). includes ( filters . search . toLowerCase ())) {
return false ;
}
return true ;
});
return (
< div >
< PizzaFilters />
< div className = "grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6" >
{ filteredPizzas . map (( pizza ) => (
< PizzaCard key = { pizza . id } pizza = { pizza } />
)) }
</ div >
</ div >
);
}
PizzaFilters Filter and sort the pizza catalog
OrderSummary View and manage items added to order
Source Reference
View the complete implementation: src/components/pizza/PizzaCard.tsx:1-99