Last active
November 4, 2025 21:01
-
-
Save Klerith/69345152fc361e8bb777c657b0f7c73a to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <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> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <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> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <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