"use client"; import Image from "next/image"; import Link from "next/link"; import * as DialogPrimitive from "@radix-ui/react-dialog"; import { Clock, PackageCheck, Star } from "lucide-react"; import { type FormEvent, type ReactNode, useCallback, useEffect, useState } from "react"; import { AccountDialog } from "@/components/account/account-dialog"; import { Button } from "@/components/ui/button"; import { Dialog, DialogContent } from "@/components/ui/dialog"; import { Input } from "@/components/ui/input"; import { formatMoney } from "@/lib/utils"; import { useCustomerStore } from "@/store/customer-store"; type OrderItem = { productId: string; title: string; thumbnail: string | null; handle: string | null; quantity: number }; type Order = { id: string; displayId: number | null; createdAt: string; status: string; total: number | null; currency: string; items: OrderItem[] }; type MyReview = { id: string; product_id: string; status: "pending" | "approved" | "rejected" }; const REVIEW_LABEL: Record = { pending: "Reseña enviada · en revisión", approved: "Reseña publicada", rejected: "Reseña no publicada", }; // "Mis pedidos": historial real del cliente (Store API) y, por cada // producto comprado, el boton para calificarlo. Solo desde aca se puede // reseñar: el backend exige que el pedido sea del cliente y contenga el // producto ("Compra verificada"). export function OrdersClient() { const { token, customer, hasHydrated } = useCustomerStore(); const [orders, setOrders] = useState(null); const [reviews, setReviews] = useState([]); const [error, setError] = useState(null); const [accountOpen, setAccountOpen] = useState(false); const [reviewing, setReviewing] = useState<{ order: Order; item: OrderItem } | null>(null); const load = useCallback(async () => { if (!token) return; setError(null); try { const headers = { Authorization: `Bearer ${token}` }; const [ordersRes, reviewsRes] = await Promise.all([fetch("/api/customer/orders", { headers }), fetch("/api/reviews/me", { headers })]); const ordersData = await ordersRes.json(); if (!ordersRes.ok) throw new Error(ordersData.error ?? "No pudimos cargar tus pedidos"); const reviewsData = await reviewsRes.json().catch(() => ({ reviews: [] })); setOrders(ordersData.orders); setReviews(reviewsRes.ok ? reviewsData.reviews : []); } catch (loadError) { setError((loadError as Error).message); setOrders([]); } }, [token]); useEffect(() => { load(); }, [load]); if (!hasHydrated) return

Cargando…

; if (!customer || !token) { return (

Inicia sesión para ver tus pedidos

Desde aquí puedes calificar los productos que compraste.

); } const reviewFor = (productId: string) => reviews.find((review) => review.product_id === productId); return ( {error &&

{error}

} {orders === null ? (

Cargando tus pedidos…

) : orders.length === 0 ? (

Todavía no tienes pedidos

) : (
    {orders.map((order) => (
  • Pedido #{order.displayId ?? order.id.slice(-6)} {new Date(order.createdAt).toLocaleDateString("es-CO", { year: "numeric", month: "long", day: "numeric" })} {order.total !== null && <> · {formatMoney(order.total, order.currency)}} {order.status === "canceled" && Cancelado}
      {order.items.map((item) => { const review = reviewFor(item.productId); return (
    • {item.thumbnail && }
      {item.handle ? ( {item.title} ) : ( {item.title} )} Cantidad: {item.quantity}
      {review ? ( {review.status === "pending" ? : } {REVIEW_LABEL[review.status]} ) : order.status === "canceled" ? null : ( )}
    • ); })}
  • ))}
)} setReviewing(null)} onSubmitted={() => { setReviewing(null); load(); }} />
); } function Shell({ children }: { children: ReactNode }) { return (
Mi cuenta

Mis pedidos

{children}
); } function ReviewDialog({ target, token, onClose, onSubmitted, }: { target: { order: Order; item: OrderItem } | null; token: string; onClose: () => void; onSubmitted: () => void; }) { const [rating, setRating] = useState(0); const [hover, setHover] = useState(0); const [title, setTitle] = useState(""); const [content, setContent] = useState(""); const [sending, setSending] = useState(false); const [error, setError] = useState(null); useEffect(() => { if (!target) return; setRating(0); setHover(0); setTitle(""); setContent(""); setError(null); }, [target]); async function submit(event: FormEvent) { event.preventDefault(); if (!target) return; if (rating < 1) return setError("Elige de 1 a 5 estrellas"); if (content.trim().length < 10) return setError("Cuéntanos un poco más (mínimo 10 caracteres)"); setSending(true); setError(null); try { const response = await fetch("/api/reviews", { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` }, body: JSON.stringify({ product_id: target.item.productId, order_id: target.order.id, rating, title: title.trim() || undefined, content: content.trim() }), }); const data = await response.json().catch(() => ({})); if (!response.ok) throw new Error(data.error ?? "No pudimos enviar tu reseña"); onSubmitted(); } catch (submitError) { setError((submitError as Error).message); } finally { setSending(false); } } const shown = hover || rating; return ( !open && onClose()}> Califica tu compra {target?.item.title}
Tu calificación {/* Patron ARIA de radiogroup (no toggles sueltos): el lector de pantalla anuncia "3 estrellas, seleccionado, 3 de 5", un solo tab-stop y flechas para cambiar -- con aria-pressed por estrella solo se anunciaba la estrella exacta, no la calificacion (reportado por frontend-critic). */}
setHover(0)} onKeyDown={(event) => { const delta = event.key === "ArrowRight" || event.key === "ArrowUp" ? 1 : event.key === "ArrowLeft" || event.key === "ArrowDown" ? -1 : 0; if (!delta) return; event.preventDefault(); // Como un radiogroup nativo: avanza desde la opcion con foco // (si aun no hay calificacion elegida) o desde la elegida. const buttons = [...event.currentTarget.querySelectorAll("button")]; const focused = buttons.indexOf(document.activeElement as HTMLButtonElement) + 1; const next = Math.min(5, Math.max(1, (rating || focused || 0) + delta)); setRating(next); setError(null); (event.currentTarget.querySelectorAll("button")[next - 1] as HTMLButtonElement | undefined)?.focus(); }} > {[1, 2, 3, 4, 5].map((value) => ( ))}