feat(reseñas): reseñas de productos de compradores verificados, moderadas
Backend (commerce-backend):
- Módulo product-review (tabla nueva product_review; migración generada con
medusa db:generate, no toca tablas existentes).
- GET /store/reviews?product_id (solo aprobadas), /store/reviews/summary,
GET /store/reviews/me y POST /store/reviews con auth de cliente: exige que
el pedido sea del cliente, no esté cancelado y contenga el producto
("Compra verificada"); 1 reseña por cliente y producto; nace "pending".
- /admin/reviews (+ /:id POST status, DELETE) y página "Reseñas" en el admin
para aprobar/rechazar/borrar.
Storefront (ecommerce):
- /cuenta/pedidos ("Mis pedidos"): historial real + diálogo "Califica tu
compra" (radiogroup accesible, flechas de teclado).
- Ficha: sección de reseñas con "Compra verificada" y estrellas junto al
título; tarjetas: promedio y cantidad (una llamada /api/reviews/summary).
- Accesos: "Mis pedidos y reseñas" en la cuenta y en Ayuda.
Validado en entorno descartable (Postgres/Redis/Meilisearch/Medusa propios):
E2E de API 20/20 (incluye pedido interno por draft order, cancelación y
borrado), E2E de tienda con navegador 15/15, admin "Reseñas" verificado en
build de producción. Revisado por frontend-critic (contraste de estrellas
amber-600, patrón radiogroup).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_018r19s4H8MBGegqToqw5guP
This commit is contained in:
1 parent
271ea30f44
commit
cfd9a7ec96
28 files changed
+1276
-2
No files matched your search
@@ -0,0 +1,22 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect } from "react";
|
||||
import { StarRating } from "@/components/product/star-rating";
|
||||
import { useReviewSummaryStore } from "@/store/review-summary-store";
|
||||
|
||||
// Promedio + cantidad de reseñas aprobadas. No renderiza nada si el
|
||||
// producto aun no tiene reseñas (ni un link vacio, en la ficha).
|
||||
export function CardRating({ productId, href }: { productId: string; href?: string }) {
|
||||
const load = useReviewSummaryStore((state) => state.load);
|
||||
const entry = useReviewSummaryStore((state) => state.summary[productId]);
|
||||
useEffect(() => load(), [load]);
|
||||
if (!entry || entry.count === 0) return null;
|
||||
const content = (
|
||||
<>
|
||||
<StarRating value={entry.average} />
|
||||
{entry.average.toLocaleString("es-CO", { minimumFractionDigits: 1 })} ({entry.count} {entry.count === 1 ? "reseña" : "reseñas"})
|
||||
</>
|
||||
);
|
||||
const className = "flex items-center gap-1.5 text-xs font-bold text-slate-500";
|
||||
return href ? <a href={href} className={`${className} mt-3 hover:text-ari-coral`}>{content}</a> : <span className={className}>{content}</span>;
|
||||
}
|
||||
Reference in new issue
Block a user