Skip to main content

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:
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:
<StarRating rating={4} />
Shows stars as read-only display

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)

Review Submission Form

Collapsible form with three fields:
1

Customer Name

Required text input for reviewer’s name
2

Star Rating

Interactive star selector (1-5 stars)
3

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:
ElementColorPurpose
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: '' 
});

Form Handling

1

User clicks 'Escribir Reseña'

Form appears with default 5-star rating
2

User fills in name, adjusts rating, writes comment

State updates with each field change
3

User clicks 'Enviar Reseña'

Form submission handler processes the review
4

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