Skip to content

Instantly share code, notes, and snippets.

@Klerith
Last active November 4, 2025 21:01
Show Gist options
  • Save Klerith/69345152fc361e8bb777c657b0f7c73a to your computer and use it in GitHub Desktop.
Save Klerith/69345152fc361e8bb777c657b0f7c73a to your computer and use it in GitHub Desktop.
<script setup lang="ts">
defineProps<{
buttonLabel: string;
}>();
const reviewText = ref('');
const rating = ref(0);
const isOpen = ref(false);
const submitReview = () => {
console.log('submitReview');
isOpen.value = false;
};
</script>
<template>
<UModal
:open="isOpen"
@close="isOpen = false"
title="Añadir reseña"
description="Deja tu reseña sobre el producto."
>
<UButton
:label="buttonLabel"
color="primary"
variant="subtle"
@click="isOpen = true"
/>
<template #content>
<UContainer class="max-w-2xl mx-auto p-4">
<h2 class="text-xl font-semibold">Añadir reseña</h2>
<p class="text-gray-600 text-sm mb-5">
Deja tu reseña sobre el producto.
</p>
<form class="grid grid-cols-1 gap-4 mb-5">
<input type="hidden" v-model="rating" />
<!-- Stars -->
<div class="col-span-1">
<div class="flex items-center gap-2">
<UIcon
name="i-lucide-star"
class="text-gray-600 text-xl cursor-pointer"
:class="{ 'text-yellow-500': rating >= star }"
v-for="star in 5"
:key="star"
@click="rating = star"
/>
</div>
</div>
<div class="col-span-1">
<UTextarea
v-model="reviewText"
placeholder="Escribe tu reseña"
class="w-full"
:rows="6"
/>
</div>
<div class="flex flex-1 items-end">
<UButton
color="primary"
variant="solid"
block
label="Enviar reseña"
:disabled="!reviewText || rating === 0"
@click="submitReview"
/>
</div>
</form>
</UContainer>
</template>
</UModal>
</template>
<script setup lang="ts">
const route = useRoute();
const slug = route.params.slug as string;
// Simulación de productos - en producción esto vendría de una API
const products = ref<Product[]>([
{
id: '1',
slug: 'cloud-storage-saas',
name: 'Cloud Storage Pro',
description:
'Servicio de almacenamiento en la nube seguro y escalable para empresas de cualquier tamaño.',
price: 100,
images: [
'https://picsum.photos/600/400?random=1',
'https://picsum.photos/600/400?random=2',
'https://picsum.photos/600/400?random=3',
],
tags: ['nube', 'almacenamiento', 'SaaS', 'seguridad'],
},
{
id: '2',
slug: 'managed-it-services',
name: 'IT Support 24/7',
description:
'Soporte técnico y gestión integral de infraestructura TI, disponible las 24 horas, todos los días.',
price: 200,
images: [
'https://picsum.photos/600/400?random=4',
'https://picsum.photos/600/400?random=5',
'https://picsum.photos/600/400?random=6',
],
tags: ['soporte', 'infraestructura', 'TI', 'empresa'],
},
{
id: '3',
slug: 'cybersecurity-suite',
name: 'CyberSecurity Suite',
description:
'Solución completa de ciberseguridad con protección contra amenazas avanzadas, firewall y análisis de vulnerabilidades.',
price: 300,
images: [
'https://picsum.photos/600/400?random=7',
'https://picsum.photos/600/400?random=8',
'https://picsum.photos/600/400?random=9',
],
tags: ['ciberseguridad', 'firewall', 'protección', 'vulnerabilidad'],
},
{
id: '4',
slug: 'ai-chatbot-platform',
name: 'Plataforma Chatbot IA',
description:
'Plataforma inteligente para la creación y gestión de chatbots con inteligencia artificial para servicio al cliente.',
price: 400,
images: [
'https://picsum.photos/600/400?random=10',
'https://picsum.photos/600/400?random=11',
'https://picsum.photos/600/400?random=12',
],
tags: ['IA', 'chatbot', 'automatización', 'servicio al cliente'],
},
]);
// Buscar el producto por slug (id en este caso)
const product = computed(() => {
return products.value.find((p) => p.slug === slug);
});
// Si no se encuentra el producto, mostrar error 404
if (!product.value) {
throw createError({
statusCode: 404,
statusMessage: 'Producto no encontrado',
});
}
// Estado para la imagen seleccionada
const selectedImageIndex = ref(0);
// Estado para cantidad
const quantity = ref(1);
const increaseQuantity = () => {
quantity.value++;
};
const decreaseQuantity = () => {
if (quantity.value > 1) {
quantity.value--;
}
};
const totalPrice = computed(() => {
return (product.value?.price || 0) * quantity.value;
});
</script>
<template>
<div v-if="product" class="container mx-auto px-4 py-8">
<!-- Breadcrumb -->
<UBreadcrumb
class="mb-8"
:items="[
{ label: 'Productos', to: '/products' },
{ label: product.name, to: `/product/${product.slug}` },
]"
/>
<!-- Product Detail -->
<div class="grid grid-cols-1 lg:grid-cols-2 gap-12">
<!-- Images Section -->
<div class="space-y-4">
<!-- Main Image -->
<div class="rounded-lg overflow-hidden bg-gray-100">
<img
:src="product.images[selectedImageIndex]"
:alt="product.name"
class="w-full h-96 object-cover"
loading="lazy"
/>
</div>
<!-- Thumbnail Images -->
<div class="grid grid-cols-3 gap-4">
<button
v-for="(image, index) in product.images"
:key="index"
@click="selectedImageIndex = index"
class="rounded-lg overflow-hidden border-2 transition-all cursor-pointer"
:class="
selectedImageIndex === index
? 'border-primary-500'
: 'border-gray-200 hover:border-gray-300'
"
>
<img
:src="image"
:alt="`${product.name} - Image ${index + 1}`"
class="w-full h-24 object-cover"
/>
</button>
</div>
</div>
<!-- Product Info Section -->
<div class="space-y-6">
<!-- Title and Price -->
<div>
<h1 class="text-3xl font-bold mb-2 capitalize">
{{ product.name }}
</h1>
<p class="text-2xl font-bold text-primary-600">
{{ formatCurrency(product.price) }}
</p>
</div>
<!-- Tags -->
<div class="flex flex-wrap gap-2">
<UBadge
v-for="tag in product.tags"
:key="tag"
color="primary"
variant="subtle"
class="capitalize"
>
{{ tag }}
</UBadge>
</div>
<!-- Description -->
<div>
<h2 class="text-lg font-semibold mb-2">Descripción</h2>
<p class="leading-relaxed">
{{ product.description }}
</p>
</div>
<USeparator />
<!-- Quantity Selector -->
<div>
<h3 class="text-sm font-medium text-gray-900 mb-3">Cantidad</h3>
<div class="flex items-center space-x-4">
<div class="flex items-center border border-gray-300 rounded-lg">
<UButton
icon="i-lucide-minus"
color="neutral"
variant="ghost"
@click="decreaseQuantity"
:disabled="quantity <= 1"
/>
<span class="px-4 py-2 font-semibold">{{ quantity }}</span>
<UButton
icon="i-lucide-plus"
color="neutral"
variant="ghost"
@click="increaseQuantity"
/>
</div>
<p class="text-sm text-gray-500">
Total: {{ formatCurrency(totalPrice) }}
</p>
</div>
</div>
<!-- Action Buttons -->
<div class="space-y-3">
<UButton
color="primary"
size="xl"
block
icon="i-lucide-shopping-cart"
label="Agregar al carrito"
/>
<UButton
color="neutral"
size="xl"
block
variant="outline"
icon="i-lucide-heart"
label="Agregar a favoritos"
/>
</div>
</div>
</div>
<!-- <USeparator class="h-[3000px]" /> -->
<USeparator class="my-10" icon="i-lucide-box" />
<!-- Reviews -->
<ProductReviews />
<!-- Related Products Section (optional) -->
<div v-if="product" class="mt-16">
<h2 class="text-2xl font-bold text-gray-900 mb-6">
Productos relacionados
</h2>
<LazyProductsGrid
hydrate-on-visible
:products="products.filter((p) => p.id !== product?.id).slice(0, 3)"
/>
</div>
</div>
</template>
<style scoped></style>
<script setup lang="ts">
const testimonials = ref([
{
user: {
stars: 5,
name: 'María González',
description: 'Compradora verificada',
avatar: {
src: 'https://randomuser.me/api/portraits/women/68.jpg',
alt: 'María González',
},
},
quote:
'¡Excelente producto! Llegó rápido y en perfectas condiciones. Lo recomiendo totalmente.',
},
{
user: {
stars: 4,
name: 'Juan Pérez',
description: 'Cliente frecuente',
avatar: {
src: 'https://randomuser.me/api/portraits/men/14.jpg',
alt: 'Juan Pérez',
},
},
quote:
'La calidad es increíble por el precio. Sin duda volveré a comprar en esta tienda.',
},
{
user: {
stars: 3,
name: 'Lucía Romero',
description: 'Compradora verificada',
avatar: {
src: 'https://randomuser.me/api/portraits/women/44.jpg',
alt: 'Lucía Romero',
},
},
quote:
'Estoy muy contenta con mi compra, superó mis expectativas y el servicio al cliente fue excelente.',
},
{
user: {
stars: 2,
name: 'Carlos Torres',
description: 'Nuevo cliente',
avatar: {
src: 'https://randomuser.me/api/portraits/men/34.jpg',
alt: 'Carlos Torres',
},
},
quote:
'El envío fue rápido y el producto llegó bien embalado. Volveré a comprar aquí.',
},
{
user: {
stars: 1,
name: 'Fernanda Ruiz',
description: 'Compradora frecuente',
avatar: {
src: 'https://randomuser.me/api/portraits/women/55.jpg',
alt: 'Fernanda Ruiz',
},
},
quote:
'Gran experiencia de compra, producto tal cual la descripción y de muy buena calidad.',
},
{
user: {
stars: 1,
name: 'Diego Martínez',
description: 'Cliente satisfecho',
avatar: {
src: 'https://randomuser.me/api/portraits/men/99.jpg',
alt: 'Diego Martínez',
},
},
quote: 'Me encantó, superó mis expectativas y lo recomendaré a mis amigos.',
},
{
user: {
stars: 2,
name: 'Sofía López',
description: 'Compradora',
avatar: {
src: 'https://randomuser.me/api/portraits/women/22.jpg',
alt: 'Sofía López',
},
},
quote: 'Excelente atención y buen producto. Todo llegó muy bien y rápido.',
},
{
user: {
stars: 3,
name: 'Miguel Fernández',
description: 'Comprador habitual',
avatar: {
src: 'https://randomuser.me/api/portraits/men/15.jpg',
alt: 'Miguel Fernández',
},
},
quote: 'Buen precio y calidad, sin problemas con el pedido. Lo recomiendo.',
},
{
user: {
stars: 4,
name: 'Paula Sánchez',
description: 'Compradora verificada',
avatar: {
src: 'https://randomuser.me/api/portraits/women/21.jpg',
alt: 'Paula Sánchez',
},
},
quote:
'Muy feliz con mi compra. El producto funciona perfecto y la atención fue genial.',
},
{
user: {
stars: 5,
name: 'Alejandro Díaz',
description: 'Recomendador',
avatar: {
src: 'https://randomuser.me/api/portraits/men/16.jpg',
alt: 'Alejandro Díaz',
},
},
quote: 'Primera vez comprando y todo salió excelente. 100% recomendado.',
},
{
user: {
stars: 5,
name: 'Valentina Morales',
description: 'Compradora contenta',
avatar: {
src: 'https://randomuser.me/api/portraits/women/47.jpg',
alt: 'Valentina Morales',
},
},
quote:
'Me sorprendió la calidad. El envío también fue muy rápido. ¡Gracias!',
},
]);
</script>
<template>
<UCard class="mb-8" icon="i-lucide-star">
<div class="flex items-center justify-between gap-3">
<div class="flex items-center gap-3">
<UIcon name="i-lucide-star" class="text-primary-500 text-2xl" />
<div>
<h2 class="text-xl font-semibold">Reseñas</h2>
<p class="text-gray-600 text-sm">
Nuestras reseñas de clientes satisfechos.
</p>
</div>
</div>
<!-- <UButton
color="primary"
icon="i-lucide-plus-circle"
size="md"
class="ml-4"
label="Añadir reseña"
/> -->
<ModalReview button-label="Añadir reseña" />
</div>
</UCard>
<UPageColumns>
<UPageCard
v-for="(testimonial, index) in testimonials"
:key="index"
variant="subtle"
:description="testimonial.quote"
:ui="{
description: 'before:content-[open-quote] after:content-[close-quote]',
}"
>
<template #footer>
<div class="flex items-center gap-2 mb-2">
<UIcon
name="i-lucide-star"
class="text-primary-500 text-xl"
v-for="star in testimonial.user.stars"
:key="star"
/>
</div>
<UUser v-bind="testimonial.user" size="xl" />
</template>
</UPageCard>
</UPageColumns>
</template>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment