Overview
The Product Reviews extension adds a complete customer review system to your EverShop store. Customers can rate products with stars, write detailed reviews, and see reviews from other customers.
Features
5-star rating system
Review submission form
Average rating calculation
Responsive design
Benefits
Build customer trust
Increase conversions
Gather product feedback
Social proof
Product Reviews Component
The main component is located at extensions/productReviews/src/pages/frontStore/productView/ProductReviews.tsx:
import React , { useState } from 'react' ;
interface Review {
id : string ;
author : string ;
rating : number ;
comment : string ;
createdAt : string ;
}
type ProductReviewsProps = {
product : {
productId : string ;
name : string ;
};
reviews ?: Review [];
action ?: string ;
};
function StarRating ({ rating , interactive = false , onChange } : {
rating : number ;
interactive ?: boolean ;
onChange ?: ( rating : number ) => void
}) {
return (
< div className = "flex gap-1" >
{ [ 1 , 2 , 3 , 4 , 5 ]. map (( star ) => (
< button
key = { star }
type = "button"
disabled = { ! interactive }
onClick = { () => interactive && onChange ?.( star ) }
className = { `text-2xl ${ interactive ? 'cursor-pointer' : 'cursor-default' } ${
star <= rating ? 'text-yellow-400' : 'text-gray-300'
} ` }
>
★
</ button >
)) }
</ div >
);
}
export default function ProductReviews ({ product , reviews = [], action } : ProductReviewsProps ) {
const [ showForm , setShowForm ] = useState ( false );
const [ newReview , setNewReview ] = useState ({
author: '' ,
rating: 5 ,
comment: ''
});
const averageRating = reviews . length > 0
? reviews . reduce (( sum , r ) => sum + r . rating , 0 ) / reviews . length
: 0 ;
const handleSubmit = async ( e : React . FormEvent ) => {
e . preventDefault ();
// Here you would normally submit to an API
console . log ( 'Submitting review:' , newReview );
setShowForm ( false );
setNewReview ({ author: '' , rating: 5 , comment: '' });
};
return (
< div className = "bg-[#F8FAF9] border border-[#E8F5E9] rounded-lg p-6 mt-6" >
< div className = "flex items-center justify-between mb-6" >
< h3 className = "text-xl font-bold text-[#2D5A3D]" >
Reseñas de Clientes
</ h3 >
< button
onClick = { () => setShowForm ( ! showForm ) }
className = "bg-[#2D5A3D] text-white px-4 py-2 rounded-lg hover:bg-[#1E3D2A] transition-colors text-sm"
>
{ showForm ? 'Cancelar' : 'Escribir Reseña' }
</ button >
</ div >
{ reviews . length > 0 && (
< div className = "mb-6" >
< div className = "flex items-center gap-4 mb-4" >
< StarRating rating = { Math . round ( averageRating ) } />
< span className = "text-[#4A5568]" >
{ averageRating . toFixed ( 1 ) } de 5 ( { reviews . length } reseñas)
</ span >
</ div >
</ div >
) }
{ showForm && (
< form onSubmit = { handleSubmit } className = "bg-white border border-[#E8F5E9] rounded-lg p-4 mb-6" >
< h4 className = "font-semibold text-[#2D5A3D] mb-4" > Nueva Reseña </ h4 >
< div className = "mb-4" >
< label className = "block text-sm font-medium text-[#4A5568] mb-2" >
Tu Nombre
</ label >
< input
type = "text"
value = { newReview . author }
onChange = { ( e ) => setNewReview ({ ... newReview , author: e . target . value }) }
required
className = "w-full px-4 py-2 border border-[#E8F5E9] rounded-lg focus:outline-none focus:border-[#2D5A3D]"
/>
</ div >
< div className = "mb-4" >
< label className = "block text-sm font-medium text-[#4A5568] mb-2" >
Calificación
</ label >
< StarRating
rating = { newReview . rating }
interactive
onChange = { ( rating ) => setNewReview ({ ... newReview , rating }) }
/>
</ div >
< div className = "mb-4" >
< label className = "block text-sm font-medium text-[#4A5568] mb-2" >
Tu Reseña
</ label >
< textarea
value = { newReview . comment }
onChange = { ( e ) => setNewReview ({ ... newReview , comment: e . target . value }) }
required
rows = { 4 }
className = "w-full px-4 py-2 border border-[#E8F5E9] rounded-lg focus:outline-none focus:border-[#2D5A3D]"
placeholder = "Comparte tu experiencia con este producto..."
/>
</ div >
< button
type = "submit"
className = "bg-[#2D5A3D] text-white px-6 py-2 rounded-lg hover:bg-[#1E3D2A] transition-colors"
>
Enviar Reseña
</ button >
</ form >
) }
{ reviews . length === 0 ? (
< p className = "text-[#4A5568] text-center py-4" >
Sé el primero en reseñar este producto.
</ p >
) : (
< div className = "space-y-4" >
{ reviews . map (( review ) => (
< div key = { review . id } className = "border-b border-[#E8F5E9] pb-4 last:border-0" >
< div className = "flex items-center gap-2 mb-2" >
< StarRating rating = { review . rating } />
< span className = "font-medium text-[#2D5A3D]" > { review . author } </ span >
</ div >
< p className = "text-[#4A5568] text-sm" > { review . comment } </ p >
< p className = "text-[#4A5568] text-xs mt-2" >
{new Date ( review . createdAt ). toLocaleDateString ( 'es-ES' ) }
</ p >
</ div >
)) }
</ div >
) }
</ div >
);
}
export const layout = {
areaId: 'productPageBottom' ,
sortOrder: 10
};
export const query = `
query Query {
product(id: getContextValue("productId")) {
productId
name
reviews {
id
author
rating
comment
createdAt
}
}
action: url(routeId: "productReviews")
}
` ;
Component Features
Star Rating Component
The StarRating component can be used in two modes:
Display Mode
Interactive Mode
< StarRating rating = { 4 } />
Shows stars as read-only display < StarRating
rating = { rating }
interactive
onChange = { ( newRating ) => setRating ( newRating ) }
/>
Allows users to click stars to select a rating
Average Rating Calculation
Automatically calculates and displays the average rating:
const averageRating = reviews . length > 0
? reviews . reduce (( sum , r ) => sum + r . rating , 0 ) / reviews . length
: 0 ;
Displayed as: 4.5 de 5 (12 reseñas)
Collapsible form with three fields:
Customer Name
Required text input for reviewer’s name
Star Rating
Interactive star selector (1-5 stars)
Review Text
Multi-line textarea for detailed feedback
Review Display
Each review shows:
⭐ Star rating visualization
👤 Author name
💬 Review comment
📅 Submission date (formatted in Spanish)
Data Structure
interface Review {
id : string ; // Unique review identifier
author : string ; // Customer name
rating : number ; // 1-5 star rating
comment : string ; // Review text
createdAt : string ; // ISO date string
}
GraphQL Integration
The component uses GraphQL to fetch product reviews:
query Query {
product ( id : getContextValue ( "productId" )) {
productId
name
reviews {
id
author
rating
comment
createdAt
}
}
action : url ( routeId : "productReviews" )
}
The action field provides the URL for submitting new reviews via POST request.
Layout Configuration
The component is placed at the bottom of product pages:
export const layout = {
areaId: 'productPageBottom' ,
sortOrder: 10
};
This ensures reviews appear after product details and supplement information.
User Interface
Empty State
When no reviews exist:
┌─────────────────────────────────────┐
│ Reseñas de Clientes [Escribir...] │
├─────────────────────────────────────┤
│ │
│ Sé el primero en reseñar este │
│ producto. │
│ │
└─────────────────────────────────────┘
With Reviews
┌─────────────────────────────────────┐
│ Reseñas de Clientes [Escribir...] │
├─────────────────────────────────────┤
│ ★★★★★ 4.5 de 5 (12 reseñas) │
├─────────────────────────────────────┤
│ ★★★★★ Juan Pérez │
│ Excelente producto, muy efectivo │
│ 15 ene 2026 │
├─────────────────────────────────────┤
│ ★★★★☆ María García │
│ Buen suplemento, resultados visibles│
│ 10 ene 2026 │
└─────────────────────────────────────┘
Styling
Uses Ana’s Suplements brand colors:
Element Color Purpose Container #F8FAF9Main background Border #E8F5E9Card borders Heading #2D5A3DSection title Button #2D5A3DPrimary actions Button Hover #1E3D2AHover state Text #4A5568Body text Stars (filled) #FCD34DYellow rating stars Stars (empty) #D1D5DBGray empty stars
Extension Structure
extensions/productReviews/
├── src/
│ └── pages/
│ └── frontStore/
│ └── productView/
│ └── ProductReviews.tsx
├── package.json
└── tsconfig.json
Configuration
Enabled in config/default.json:
{
"system" : {
"extensions" : [
{
"name" : "productReviews" ,
"resolve" : "extensions/productReviews" ,
"enabled" : true
}
]
}
}
State Management
The component uses React hooks for state management:
const [ showForm , setShowForm ] = useState ( false ); // Form visibility
const [ newReview , setNewReview ] = useState ({ // Form data
author: '' ,
rating: 5 ,
comment: ''
});
User clicks 'Escribir Reseña'
Form appears with default 5-star rating
User fills in name, adjusts rating, writes comment
State updates with each field change
User clicks 'Enviar Reseña'
Form submission handler processes the review
Form resets
Form hides and clears all fields
Best Practices
Moderation : Consider adding admin review moderation before publishing reviews publicly.
Verification : Implement purchase verification to ensure only real customers can review products.
Incentives : Consider offering loyalty points or discounts for leaving reviews to increase participation.
Internationalization
All text is currently in Spanish:
“Reseñas de Clientes” (Customer Reviews)
“Escribir Reseña” (Write Review)
“Nueva Reseña” (New Review)
“Tu Nombre” (Your Name)
“Calificación” (Rating)
“Tu Reseña” (Your Review)
“Sé el primero en reseñar este producto” (Be the first to review)
To support other languages, extract strings to translation files.
Next Steps
Offline Payments Learn about payment methods
Page Components Create custom product page components