Files
devopsandClaude Opus 5.5 cfd9a7ec96 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
2026-09-27 07:57:20 +00:00

19 lines
1.1 KiB
TypeScript

import { Star } from "lucide-react";
// Estrellas de solo lectura. El relleno parcial (ej. 4.5) se dibuja
// recortando una capa de estrellas llenas encima de las vacias. amber-600
// (no 400): contraste ~3.2:1 sobre blanco, minimo WCAG 1.4.11 para un
// grafico informativo (amber-400 daba 1.7:1, reportado por frontend-critic).
export function StarRating({ value, size = "sm", className = "" }: { value: number; size?: "sm" | "md" | "lg"; className?: string }) {
const px = size === "lg" ? "size-6" : size === "md" ? "size-5" : "size-4";
const pct = Math.max(0, Math.min(100, (value / 5) * 100));
return (
<span className={`relative inline-flex ${className}`} role="img" aria-label={`${value.toLocaleString("es-CO")} de 5 estrellas`}>
<span className="flex text-slate-300">{[0, 1, 2, 3, 4].map((i) => <Star key={i} className={`${px} fill-current`} />)}</span>
<span className="absolute inset-0 flex overflow-hidden text-amber-600" style={{ width: `${pct}%` }}>
{[0, 1, 2, 3, 4].map((i) => <Star key={i} className={`${px} shrink-0 fill-current`} />)}
</span>
</span>
);
}