diff --git a/.env.example b/.env.example index 1517569..74fdb0a 100644 --- a/.env.example +++ b/.env.example @@ -1,12 +1,10 @@ -# Airtable Configuration -# Get these from your Airtable account settings -VITE_AIRTABLE_API_KEY=your_airtable_api_key_here -VITE_AIRTABLE_BASE_ID=your_base_id_here -VITE_AIRTABLE_TABLE_NAME=Productos +# API Configuration +VITE_API_URL=https://api.example.com/v1 -# MercadoPago Configuration (Frontend) +# MercadoPago Configuration VITE_MERCADOPAGO_PUBLIC_KEY=your_mercadopago_public_key_here +MERCADOPAGO_ACCESS_TOKEN=your_mercadopago_access_token_here -# Server-side MercadoPago Configuration (Netlify Functions) -# Add this in Netlify Dashboard > Functions > Environment -MERCADOPAGO_ACCESS_TOKEN=your_mercadopago_access_token_here \ No newline at end of file +# Netlify Functions Environment +ALLOWED_ORIGINS=https://your-domain.netlify.app,http://localhost:3000 +SITE_URL=http://localhost:3000 diff --git a/.env.secure b/.env.secure index db528c0..64f6314 100644 --- a/.env.secure +++ b/.env.secure @@ -2,10 +2,8 @@ # This file contains template values. DO NOT use these in production! # Replace with your actual values and set them in Netlify environment variables -# Airtable Configuration -VITE_AIRTABLE_API_KEY=your_actual_airtable_personal_access_token_here -VITE_AIRTABLE_BASE_ID=your_actual_base_id_here -VITE_AIRTABLE_TABLE_NAME=Productos +# API Configuration +VITE_API_URL=https://api.example.com/v1 # MercadoPago Configuration (Frontend) VITE_MERCADOPAGO_PUBLIC_KEY=your_actual_mercadopago_public_key_here @@ -17,7 +15,7 @@ MERCADOPAGO_ACCESS_TOKEN=your_actual_mercadopago_access_token_here # Security Configuration (Netlify Functions Environment) # Comma-separated list of allowed origins -ALLOWED_ORIGINS=https://amigurumi-de-ines.netlify.app,https://localhost:3000 +ALLOWED_ORIGINS=https://tienda-devschile.netlify.app,http://localhost:5173 # Node Environment (Production) NODE_ENV=production @@ -26,4 +24,4 @@ NODE_ENV=production # 1. NEVER commit real API keys to version control # 2. Set sensitive values in Netlify environment variables # 3. Rotate all exposed credentials immediately -# 4. See SECURITY.md for detailed security analysis \ No newline at end of file +# 4. See SECURITY.md for detailed security analysis diff --git a/.gitignore b/.gitignore index 734f51e..a592caf 100644 --- a/.gitignore +++ b/.gitignore @@ -174,4 +174,8 @@ bundle-report.html SECURITY.md *.pem *.key -*.crt \ No newline at end of file +*.crt + +# Local environment variables source +.env +.env.local diff --git a/.nvmrc b/.nvmrc index 25bf17f..7273c0f 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -18 \ No newline at end of file +25 diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..7e4e49d --- /dev/null +++ b/.prettierignore @@ -0,0 +1,10 @@ +# Prettier ignore file +node_modules +dist +build +.idea +.DS_Store +package-lock.json +package.json +public +*.html diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..91a5dce --- /dev/null +++ b/.prettierrc @@ -0,0 +1,11 @@ +{ + "semi": true, + "trailingComma": "all", + "singleQuote": true, + "printWidth": 100, + "tabWidth": 2, + "useTabs": false, + "jsxSingleQuote": false, + "bracketSpacing": true, + "arrowParens": "always" +} diff --git a/README.md b/README.md index bef3e5f..1c23d96 100644 --- a/README.md +++ b/README.md @@ -1,29 +1,15 @@ -# Amigurumis de Inés - Ecommerce con MercadoPago +# Tienda devsChile - Ecommerce con MercadoPago -Sitio web para la venta de amigurumis tejidos a mano por Inés. Construido con React, TypeScript, Tailwind CSS y Vite. **Ahora con integración completa de MercadoPago para pagos reales**. - -## 🚨 AVISO DE SEGURIDAD IMPORTANTE - -**⚠️ ANTES DE USAR EN PRODUCCIÓN: Se realizó una auditoría de seguridad completa. Las credenciales expuestas deben rotarse inmediatamente. Ver la sección [Seguridad](#-seguridad) para más detalles.** - -## ✨ Características - -- 🎨 Diseño responsivo y atractivo con gradientes cálidos -- 📱 Interfaz moderna optimizada para móviles -- 🛍️ Catálogo de productos dinámico desde Airtable -- 💳 **Pagos reales con MercadoPago** (¡NUEVO!) -- 🔒 Pago seguro con redirección a MercadoPago -- ✅ Páginas de confirmación: éxito, falla y pendiente -- 📊 Gestión de estados de pago completa -- 🎯 Sin carrito de compras - compra directa por producto -- 🛡️ **Auditoría de seguridad completa implementada** +Sitio web para la venta de productos de la comunidad devsChile. +Construido con React, TypeScript, Tailwind CSS y Vite. +**Ahora con integración completa de MercadoPago para transacciones reales**. ## 🛠️ Tecnologías - **Frontend**: React 18 + TypeScript + Vite - **Estilos**: Tailwind CSS + Gradientes personalizados - **UI Components**: Radix UI + shadcn/ui -- **Datos**: Airtable API +- **Datos**: API Genérica / Mock Data - **Pagos**: MercadoPago SDK + Netlify Functions - **Despliegue**: Netlify (Frontend + Functions) - **Seguridad**: CORS whitelist, validación de entrada, headers de seguridad @@ -48,14 +34,14 @@ cp .env.secure .env **Variables requeridas:** -#### Airtable (para productos) +#### API Configuration (para productos) + ```env -VITE_AIRTABLE_API_KEY=tu_airtable_api_key_aqui -VITE_AIRTABLE_BASE_ID=tu_base_id_aqui -VITE_AIRTABLE_TABLE_NAME=Productos +VITE_API_URL=https://tu-api.com/v1 ``` #### MercadoPago (para pagos) + ```env # Clave pública (frontend) - esta se puede exponer VITE_MERCADOPAGO_PUBLIC_KEY=tu_public_key_aqui @@ -65,9 +51,10 @@ MERCADOPAGO_ACCESS_TOKEN=tu_access_token_aqui ``` #### Seguridad (Netlify Functions) + ```env # Orígenes permitidos (comas separadas) -ALLOWED_ORIGINS=https://amigurumi-de-ines.netlify.app,https://localhost:3000 +ALLOWED_ORIGINS=https://tienda-devschile.cl,http://localhost:3000 NODE_ENV=production ``` @@ -80,18 +67,18 @@ NODE_ENV=production - `Public Key`: Para el frontend (VITE_MERCADOPAGO_PUBLIC_KEY) - `Access Token`: Para el backend (MERCADOPAGO_ACCESS_TOKEN) -### 4. Configurar Airtable +### 4. Configuración de API de Productos -La aplicación espera una tabla llamada "Productos" con estos campos: +La aplicación espera un endpoint `GET /products` que retorne una estructura compatible con `ProductResponse` conteniendo estos campos: -| Campo | Tipo | Requerido | Descripción | -|-------|------|-----------|-------------| -| `nombre` | Texto | ✅ | Nombre del producto | -| `descripcion` | Texto largo | ✅ | Descripción detallada | -| `precio` | Número | ✅ | Precio en CLP | -| `imagen_miniatura` | Attachment | ✅ | Imagen 300x300px | -| `imagenes_grandes` | Attachment | ✅ | Imágenes alta resolución | -| `activo` | Checkbox | ✅ | Disponible para venta | +| Campo | Tipo | Requerido | Descripción | +| ------------------ | ----------- | --------- | ------------------------ | +| `name` | Texto | ✅ | Nombre del producto | +| `description` | Texto largo | ✅ | Descripción detallada | +| `price` | Número | ✅ | Precio en CLP | +| `thumbnailImages` | Attachment | ✅ | Imagen 300x300px | +| `largeImages` | Attachment | ✅ | Imágenes alta resolución | +| `active` | Checkbox | ✅ | Disponible para venta | ## 🏃‍♂️ Desarrollo @@ -133,6 +120,8 @@ npm run lint │ └── functions/ │ ├── create-payment.js # Netlify Function para MercadoPago (segura) │ └── package.json # Dependencias de Functions +├── public/ +│ └── images/ # Imágenes estáticas (accesibles vía /images/*) ├── app/ │ └── app.tsx # Componente principal con pago ├── components/ @@ -147,35 +136,10 @@ npm run lint └── package.json # Dependencias del proyecto ``` -## 🌐 Despliegue en Netlify - -### Configuración Automática: - -1. **Conecta tu repositorio a Netlify** -2. **Variables de entorno en Netlify Dashboard:** - - Todas las variables `VITE_*` en "Build & deploy" → "Environment" - - `MERCADOPAGO_ACCESS_TOKEN` en "Functions" → "Environment" - - `ALLOWED_ORIGINS` en "Functions" → "Environment" - - `NODE_ENV=production` en "Functions" → "Environment" -3. **Netlify detectará automáticamente:** - - Build: `npm ci && npm run build` - - Publish directory: `dist` - - Functions directory: `netlify/functions` - ### URLs de Función: - Crear pago: `https://tu-sitio.netlify.app/.netlify/functions/create-payment` -### 🛠️ Solución de Problemas de Despliegue: - -#### Error "vite: not found" - -Si encuentras el error `sh: 1: vite: not found` durante el build: - -1. **✅ Ya solucionado**: El archivo `netlify.toml` incluye `npm ci &&` para instalar dependencias -2. **Node.js Version**: El proyecto usa Node.js 18 (ver `.nvmrc`) -3. **Dependencias**: Asegúrate de que `package-lock.json` esté en el repositorio - #### Comandos de Build Configurados: ```toml @@ -189,7 +153,12 @@ Esto asegura que las dependencias se instalen antes del build. ### Modificar Productos: -Edita directamente en tu tabla de Airtable. Los cambios se reflejan automáticamente. +Edita tus datos en el archivo `app/productsMock.ts` o configura tu API Genérica. Los cambios se reflejan automáticamente. + +#### Imágenes Locales: + +Puedes guardar imágenes estáticas en la carpeta `public/images/`. Para usarlas en tus productos, utiliza la ruta relativa comenzando con `/images/`. +Ejemplo: Si guardas `mi-producto.jpg` en `public/images/`, la URL en tu JSON/Mock será `/images/mi-producto.jpg`. ### Cambiar Precios: @@ -197,23 +166,11 @@ Los precios se muestran en CLP (Pesos Chilenos) y se formatean automáticamente. ### Personalizar Estilos: -- Colores principales: `rose-500` y `orange-500` -- Gradientes: `from-rose-500 to-orange-500` +- Colores principales: `brand-primary` (#85422b) y `brand-secondary` (#b45b38) +- Texto principal: `brand-text` (#1d1d1d) +- Gradientes: `from-brand-primary to-brand-secondary` - Tipografía: Sistema fonts optimizados -## 📋 Checklist de Configuración - -- [ ] ✅ Instalar dependencias -- [ ] ✅ Configurar variables de Airtable -- [ ] ✅ Crear cuenta en MercadoPago -- [ ] ✅ Obtener credenciales de MercadoPago -- [ ] ✅ **ROTAR credenciales expuestas** (Ver sección Seguridad) -- [ ] ✅ Configurar variables de entorno -- [ ] ✅ Conectar repositorio a Netlify -- [ ] ✅ Probar flujo de pagos -- [ ] ✅ Verificar páginas de confirmación -- [ ] ✅ Configurar variables de seguridad en Netlify - ## 🛡️ Seguridad ### ⚠️ AUDITORÍA DE SEGURIDAD COMPLETADA @@ -221,6 +178,7 @@ Los precios se muestran en CLP (Pesos Chilenos) y se formatean automáticamente. Se realizó una auditoría completa de seguridad que identificó y corrigió vulnerabilidades críticas: #### Vulnerabilidades Corregidas: + - ✅ **CORS mejorado**: Reemplazado wildcard `*` con whitelist de orígenes - ✅ **Validación de entrada**: Sanitización y validación de todos los inputs - ✅ **Headers de seguridad**: X-Frame-Options, X-XSS-Protection, X-Content-Type-Options @@ -231,25 +189,22 @@ Se realizó una auditoría completa de seguridad que identificó y corrigió vul **Las siguientes credenciales están expuestas y deben rotarse INMEDIATAMENTE:** -1. **Airtable API Key**: `patDvA7InUnb2X449.*` (PAT completa expuesta) -2. **Airtable Base ID**: `apprLGWcETltWUXpn` -3. **MercadoPago Public Key**: `APP_USR-0ce0eeab-*` (Key completa expuesta) -4. **MercadoPago Access Token**: `APP_USR-2637451468197049-*` (Token completo expuesto) +1. **MercadoPago Public Key**: `APP_USR-0ce0eeab-*` (Key completa expuesta) +2. **MercadoPago Access Token**: `APP_USR-2637451468197049-*` (Token completo expuesto) #### Pasos de Rotación CRÍTICOS: 1. **🔄 Rotar credenciales inmediatamente:** - - Airtable: [Personal Access Tokens](https://airtable.com/developers/web/api/personal-access-tokens) - MercadoPago: [Developer Panel](https://www.mercadopago.com.ar/developers/panel/credentials) 2. **🗑️ Eliminar archivo .env actual** después de la rotación -3. **🧹 Limpiar historial de git** si las credenciales fueron commitadas +3. **🧹 Limpiar historial de git** si las credenciales fueron commiteadas 4. **⚙️ Configurar en Netlify Dashboard:** ``` MERCADOPAGO_ACCESS_TOKEN=nueva_token_rotado - ALLOWED_ORIGINS=https://amigurumi-de-ines.netlify.app,https://localhost:3000 + ALLOWED_ORIGINS=https://tienda-devschile.netlify.app,http://localhost:5173 NODE_ENV=production ``` @@ -281,10 +236,8 @@ Si tienes problemas con la integración de MercadoPago: ## 📄 Licencia -Proyecto privado - © 2025 Amigurumis de Inés. Todos los derechos reservados. +Proyecto privado - © 2026 Tienda devsChile. Todos los derechos reservados. --- **🎉 ¡Listo para recibir pagos reales con MercadoPago!** 💳 - -**⚠️ RECORDATORIO**: Rotar credenciales expuestas antes del despliegue en producción. \ No newline at end of file diff --git a/actions/createPayment.ts b/actions/createPayment.ts index 62dd0ed..f21ba9f 100644 --- a/actions/createPayment.ts +++ b/actions/createPayment.ts @@ -1,19 +1,42 @@ // Acción para crear pago en la pasarela -import { action } from '@uibakery/data'; -function createPayment() { - return action('createPayment', 'HTTP', { - datasourceName: 'httpApi', - options: { - method: 'POST', - url: '{{params?.paymentGatewayUrl}}', - headers: { - 'Content-Type': 'application/json', - }, - bodyType: 'object', - body: '{ amount: {{params?.amount}}, productName: {{params?.productName}}, currency: "CLP" }', +export const createPayment = async ( + amount: number, + productName: string, + productId: string, + quantity: number = 1, +) => { + // En desarrollo, usamos un mock según los requerimientos + if (import.meta.env.DEV) { + console.log('Utilizando mock de pago para desarrollo'); + // Simulamos un pequeño retraso de red + await new Promise((resolve) => setTimeout(resolve, 800)); + + return { + success: true, + checkout_url: '/success.html', + }; + } + + // En producción, llamamos a la Netlify Function + const response = await fetch('/.netlify/functions/create-payment', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', }, + body: JSON.stringify({ + amount, + productName, + productId, + quantity, + }), }); -} + + if (!response.ok) { + throw new Error('Error al crear el pago en el servidor'); + } + + return response.json(); +}; export default createPayment; diff --git a/actions/loadProducts.ts b/actions/loadProducts.ts index 6d3c0e0..648c226 100644 --- a/actions/loadProducts.ts +++ b/actions/loadProducts.ts @@ -1,4 +1,4 @@ -// Acción para cargar todos los productos activos desde Airtable +// Action to load all active products from the API import { action } from '@uibakery/data'; function loadProducts() { @@ -6,12 +6,12 @@ function loadProducts() { datasourceName: 'httpApi', options: { method: 'GET', - url: 'https://api.airtable.com/v0/{{params?.baseId}}/{{params?.tableName}}', + url: '{{params?.apiUrl}}/products', queryParams: { - filterByFormula: '{activo} = TRUE()', + active: 'true', }, headers: { - Authorization: 'Bearer {{params?.apiKey}}', + 'Content-Type': 'application/json', }, }, }); diff --git a/app/app.tsx b/app/app.tsx index e327451..dddc69c 100644 --- a/app/app.tsx +++ b/app/app.tsx @@ -1,32 +1,72 @@ -// Aplicación principal de ecommerce - Diseño comercial cálido import { useState, useEffect } from 'react'; +import { Info, Loader2, ShoppingBag } from 'lucide-react'; + import { Button } from '@/components/ui/button'; import { ProductCard } from '@/components/ProductCard'; import { ProductImageModal } from '@/components/ProductImageModal'; import { InfoModal } from '@/components/InfoModal'; import { Toaster } from '@/components/ui/toaster'; import { useToast } from '@/hooks/use-toast'; -import type { AirtableRecord, AirtableResponse } from '@/types/products'; -import { Info, Loader2, Heart, ShoppingBag, Sparkles } from 'lucide-react'; +import type { ProductRecord, ProductResponse } from '@/types/products'; +import { productsMock as records } from '@/app/productsMock.ts'; +import logo from '@/images/devschile2026.png'; +import createPayment from '@/actions/createPayment'; -const AIRTABLE_CONFIG = { - apiKey: import.meta.env.VITE_AIRTABLE_API_KEY, - baseId: import.meta.env.VITE_AIRTABLE_BASE_ID, - tableName: import.meta.env.VITE_AIRTABLE_TABLE_NAME, +const API_CONFIG = { + apiUrl: import.meta.env.VITE_API_URL || 'https://api.example.com/v1', }; - function App() { const { toast } = useToast(); - const [selectedProduct, setSelectedProduct] = useState(null); + const [selectedProduct, setSelectedProduct] = useState(null); const [imageModalOpen, setImageModalOpen] = useState(false); const [infoModalOpen, setInfoModalOpen] = useState(false); - + // Replace UIBakery hooks with standard React state - const [productsData, setProductsData] = useState(null); + const [productsData, setProductsData] = useState(null); const [loadingProducts, setLoadingProducts] = useState(true); const [errorProducts, setErrorProducts] = useState(null); const [loadingPayment, setLoadingPayment] = useState(false); + const [selectedCategory, setSelectedCategory] = useState(() => { + return new URLSearchParams(window.location.search).get('category'); + }); + const [sortOrder, setSortOrder] = useState<'default' | 'price-asc' | 'price-desc'>('default'); + + useEffect(() => { + const handlePopState = () => { + setSelectedCategory(new URLSearchParams(window.location.search).get('category')); + }; + window.addEventListener('popstate', handlePopState); + return () => window.removeEventListener('popstate', handlePopState); + }, []); + + const handleCategoryChange = (category: string | null) => { + setSelectedCategory(category); + const url = new URL(window.location.href); + if (category) { + url.searchParams.set('category', category); + } else { + url.searchParams.delete('category'); + } + window.history.pushState({}, '', url); + }; + + // Process and validate categories + useEffect(() => { + if (!loadingProducts && productsData && productsData.records.length > 0) { + if (selectedCategory) { + const matches = productsData.records.filter((p) => p.fields.category === selectedCategory); + if (matches.length === 0) { + toast({ + title: 'Categoría no encontrada', + description: `No hay productos en la categoría "${selectedCategory}".`, + variant: 'destructive', + }); + handleCategoryChange(null); + } + } + } + }, [loadingProducts, productsData, selectedCategory, toast]); // Load products on component mount useEffect(() => { @@ -34,135 +74,69 @@ function App() { try { setLoadingProducts(true); setErrorProducts(null); - - // Simulate API call - replace with actual Airtable API call - const response = await fetch( - `https://api.airtable.com/v0/${AIRTABLE_CONFIG.baseId}/${AIRTABLE_CONFIG.tableName}`, - { - headers: { - 'Authorization': `Bearer ${AIRTABLE_CONFIG.apiKey}`, - }, - } - ); - + + // In development, use mock data + if (import.meta.env.DEV) { + console.log('Using mock products data for development'); + setProductsData({ records }); + return; + } + + const response = await fetch(`${API_CONFIG.apiUrl}/products`); + if (!response.ok) { throw new Error('Failed to fetch products'); } - - const data: AirtableResponse = await response.json(); + + const data: ProductResponse = await response.json(); setProductsData(data); } catch (error) { console.error('Error loading products:', error); setErrorProducts('Error loading products'); - // For demo purposes, set some mock data - setProductsData({ - records: [ - { - id: 'rec1', - fields: { - id: 'rec1', - nombre: 'Amigurumi Osito', - precio: 25000, - descripcion: 'Adorable osito tejido a mano', - imagen_miniatura: [{ - id: 'img1', - url: 'https://via.placeholder.com/300x300?text=Osito', - filename: 'osito.jpg', - size: 12345, - type: 'image/jpeg' - }], - imagenes_grandes: [{ - id: 'img1', - url: 'https://via.placeholder.com/600x600?text=Osito+Grande', - filename: 'osito_grande.jpg', - size: 54321, - type: 'image/jpeg' - }], - activo: true - }, - createdTime: '2025-01-01T00:00:00.000Z' - }, - { - id: 'rec2', - fields: { - id: 'rec2', - nombre: 'Amigurumi Unicornio', - precio: 30000, - descripcion: 'Mágico unicornio multicolor', - imagen_miniatura: [{ - id: 'img2', - url: 'https://via.placeholder.com/300x300?text=Unicornio', - filename: 'unicornio.jpg', - size: 12345, - type: 'image/jpeg' - }], - imagenes_grandes: [{ - id: 'img2', - url: 'https://via.placeholder.com/600x600?text=Unicornio+Grande', - filename: 'unicornio_grande.jpg', - size: 54321, - type: 'image/jpeg' - }], - activo: false - }, - createdTime: '2025-01-01T00:00:00.000Z' - } - ] - }); + // Fallback to mock data + setProductsData({ records }); setErrorProducts(null); } finally { setLoadingProducts(false); } }; - + loadProductsData(); }, []); - const handleImageClick = (product: AirtableRecord) => { + const handleImageClick = (product: ProductRecord) => { setSelectedProduct(product); setImageModalOpen(true); }; - const handleBuyClick = async (product: AirtableRecord) => { + const handleBuyClick = async (product: ProductRecord, quantity: number) => { try { setLoadingPayment(true); - + toast({ title: 'Preparando pago...', - description: `Creando preferencia de pago para ${product.fields.nombre}...`, + description: `Creando preferencia de pago para ${quantity}x ${product.fields.name}...`, }); + debugger; + // Llamamos a la función referenciada para crear el pago + const data = await createPayment( + product.fields.price, + product.fields.name, + product.id, + quantity, + ); - // Call Netlify function to create MercadoPago payment - const response = await fetch('/.netlify/functions/create-payment', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - amount: product.fields.precio, - productName: product.fields.nombre, - productId: product.id - }), - }); - - if (!response.ok) { - throw new Error('Failed to create payment'); - } - - const data = await response.json(); - if (!data.success || !data.checkout_url) { - throw new Error(data.error || 'Failed to get checkout URL'); + throw new Error(data.error || 'No se pudo obtener la URL de pago'); } toast({ title: '¡Redirigiendo a MercadoPago!', - description: `Serás redirigido para completar el pago de ${product.fields.nombre}`, + description: `Serás redirigido para completar el pago de ${quantity}x ${product.fields.name}`, }); // Redirect to MercadoPago checkout window.location.href = data.checkout_url; - } catch (error) { console.error('Payment error:', error); toast({ @@ -176,69 +150,68 @@ function App() { }; const allProducts = productsData?.records || []; - const availableProducts = allProducts.filter(product => product.fields.activo); - const totalCount = allProducts.length; + const uniqueCategories = Array.from( + new Set(allProducts.map((p) => p.fields.category).filter(Boolean)), + ).sort(); + + const filteredByCategory = selectedCategory + ? allProducts.filter((p) => p.fields.category === selectedCategory) + : allProducts; + + const filteredProducts = [...filteredByCategory].sort((a, b) => { + if (sortOrder === 'price-asc') return a.fields.price - b.fields.price; + if (sortOrder === 'price-desc') return b.fields.price - a.fields.price; + return 0; + }); + + const availableProducts = filteredProducts.filter((product) => product.fields.active); + const totalCount = filteredProducts.length; const availableCount = availableProducts.length; return ( -
+
{/* Decorative elements */}
-
-
+
+
{/* Header */} -
+
-
- +
+
-

- Amigurumis de Inés +

+ Tienda devsChile

-

Hechos con amor y dedicación

+

[text]

- {/* Hero banner */} -
-
-
-
-
-
- - Colección Especial -
-

Mis Creaciones Únicas para Ti

-

Cada amigurumi lo tejo a mano con amor, cuidado y los mejores materiales

-
- -
-
-
+ {/* Hero banner +
*/} {/* Main Content */}
{loadingProducts && (
- -

Cargando amigurumis mágicos...

+ +

Cargando productos mágicos...

)} @@ -247,24 +220,20 @@ function App() {
⚠️
-

- Oops, algo salió mal -

+

Oops, algo salió mal

- No pudimos cargar los amigurumis. Verifica tu configuración de Airtable. + No pudimos cargar los productos. Verifica tu conexión e intenta nuevamente.

)} {!loadingProducts && !errorProducts && allProducts.length === 0 && ( -
-
- +
+
+
-

- No hay amigurumis disponibles -

-

+

No hay productos disponibles

+

Pronto tendré nuevas creaciones disponibles. ¡Vuelve pronto!

@@ -272,43 +241,95 @@ function App() { {!loadingProducts && allProducts.length > 0 && ( <> +
+ + {uniqueCategories.map((category) => ( + + ))} + +
+
-

- Mis Creaciones +

+ {selectedCategory ? `Productos: ${selectedCategory}` : 'Todos los Productos'}

-

- {availableCount} {availableCount === 1 ? 'amigurumi' : 'amigurumis'} disponibles +

+ {availableCount} producto{availableCount === 1 ? '' : 's'} disponible + {availableCount === 1 ? '' : 's'}

-

- {totalCount} {totalCount === 1 ? 'amigurumi hecho' : 'amigurumis hechos'} en total +

+ {totalCount} producto{totalCount === 1 ? '' : 's'} en total

-
- {allProducts.map((product) => ( - - ))} -
+ + {filteredProducts.length === 0 ? ( +
+
+ +
+

+ No hay productos en esta categoría +

+

+ Intenta seleccionar otra categoría o ver todos los productos. +

+ +
+ ) : ( +
+ {filteredProducts.map((product) => ( + + ))} +
+ )} )} {/* Footer */} -