Skip to content

Latest commit

 

History

144 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Tienda devsChile

Huemul devsChile

E-commerce oficial de la comunidad devsChile. Venta de productos exclusivos con pago integrado vía MercadoPago, panel de administración, gestión de imágenes y configuración dinámica desde base de datos.

Producción: tienda.devschile.cl · Admin: tienda.devschile.cl/admin


🛠️ Stack

Capa Tecnología
Frontend React 18 + TypeScript + Vite
Estilos Tailwind CSS (paleta de marca devsChile) + Onest / Fira Mono
UI Components Radix UI + shadcn/ui
Animaciones Motion v12 (Framer Motion) — shared element, spring, stagger
Routing React Router DOM v6
BD NeonDB — Postgres serverless
Backend Netlify Functions (Node.js, CJS)
Imágenes UploadThing v7 — CDN global
Pagos MercadoPago SDK v2 — Checkout Pro (redirect)
Emails Resend (staging) · Mailgun (producción) — multi-proveedor
Despliegue Netlify (Frontend + Functions)

🚀 Instalación

npm install
npm install --prefix netlify/functions   # deps de las funciones
cp .env.example .env                     # completar con credenciales reales

Variables de entorno

# NeonDB — solo backend, nunca prefijo VITE_
NEON_DATABASE_URL=postgresql://user:password@host/dbname?sslmode=require

# MercadoPago
VITE_MERCADOPAGO_PUBLIC_KEY=APP_USR-...    # frontend (seguro exponerse)
MERCADOPAGO_ACCESS_TOKEN=APP_USR-...       # backend solo
MERCADOPAGO_WEBHOOK_SECRET=whsec_...       # Panel MP → Tu app → Webhooks

# Identidad devsChile (soy) — descuento miembro por OAuth, no por email:
# el checkout pide autorización authorize → code → soy-exchange (secreto solo server-side).
# VITE_SOY_URL: origen de soy que ve el browser (default https://soy.devschile.cl);
# cámbialo si el checkout debe autorizar contra un soy de staging.
VITE_SOY_URL=https://soy.devschile.cl
# SOY_CLIENT_SECRET debe ser el MISMO que NUXT_OAUTH_CLIENTS_TIENDA_CLIENT_SECRET en soy.
SOY_MEMBERS_API_URL=https://soy.devschile.cl
SOY_CLIENT_SECRET=
MEMBER_DISCOUNT_PERCENT=10

# Email (cambiar solo EMAIL_PROVIDER para alternar)
EMAIL_PROVIDER=resend                      # resend | mailgun
RESEND_API_KEY=re_...
FROM_EMAIL=
MAILGUN_API_KEY=key-...                    # solo producción
MAILGUN_DOMAIN=mg.devschile.cl             # solo producción

# UploadThing — imágenes de productos
UPLOADTHING_TOKEN=tu_token_aqui            # dashboard.uploadthing.com → API Keys

# Admin panel
ADMIN_EMAIL=
ADMIN_PASSWORD=contraseña-segura
ADMIN_JWT_SECRET=string-aleatorio-64-chars  # openssl rand -base64 48

# CORS y entorno
ALLOWED_ORIGINS=https://tienda.devschile.cl,https://devschile-tienda.netlify.app
SITE_URL=http://localhost:3000
NODE_ENV=development

Ver docs/mercadopago-integration.md para guía completa de MercadoPago.


💻 Desarrollo local

# Solo UI (mock data, sin funciones ni BD)
npm run dev

# Full stack (dos terminales)
npm run dev               # Terminal 1 — Vite en :3000
npm run dev:functions     # Terminal 2 — Netlify Functions en :9999

# Build producción
npm run build

# Migrar imágenes locales → UploadThing (one-shot)
npm run migrate:images

# Generar favicons desde el logo
npm run generate:favicons

El proxy de Vite redirige /.netlify/functions/* y /admin-api/*:9999 automáticamente.


🗄️ Base de datos (NeonDB)

Migraciones

Aplica en orden desde migrations/ en el SQL Editor de Neon:

# Archivo Descripción
01 create_products_schema.sql Tablas products y product_images
02 seed_products_from_mock.sql Datos iniciales de ejemplo
03 add_stock_and_available.sql Stock + trigger available=false si stock=0
04 rename_active_to_visible.sql activevisible
05 product_images_cover.sql is_cover + trigger single-cover por producto
06 create_orders.sql Tablas orders y order_items
07 add_on_sale.sql Badge de oferta en productos
08 add_wants_newsletter.sql Consentimiento de newsletter en orders
09 add_long_description_and_sale_price.sql Descripción larga (Markdown) + precio oferta
10 add_original_price_to_order_items.sql Precio original para emails con descuentos
11 add_notes_to_orders.sql Notas internas por orden (solo admin)
12 create_settings.sql Configuración dinámica de la tienda
13 add_archived_to_products_and_orders.sql Archivado reversible de productos y órdenes
15 add_bundle_and_addon_fields.sql Packs de stickers + add-ons (sin tocar productos actuales)
16 add_shipping_enabled_to_products.sql Envío opcional por producto (default: habilitado)
17 add_presale_to_products.sql Badge de preventa (⏳) — excluyente con on_sale
18 add_promo_codes.sql Códigos de descuento (%, monto fijo o envío gratis) + columnas de descuento en orders
19 fix_promo_codes_id_type.sql promo_codes.id uuid → text (prm_..., igual que products/images)
20 add_shipping_tier.sql Envío por niveles (XS/S/M/L): tier por producto + costo absoluto por tier en settings
21 add_bundle_item_ids.sql Packs curados: bundle_item_ids (JSON de ids) — cada pack declara qué ítems incluye

Esquema resumido

products
  id, name, description, long_description (Markdown)
  category, price, sale_price, on_sale
  presale  ← badge de preventa (⏳), excluyente con on_sale (migración 17)
  visible, available, stock
  product_type (standard|bundle|addon)  ← packs + stickers (migración 15)
  selectable_in_bundles, bundle_unit_price
  shipping_enabled (default true)  ← envío opcional por producto (migración 16)
  shipping_tier (xs|s|m|l, default 'xs')  ← tamaño del paquete (migración 20)
  bundle_sizes (JSON [3,4,6]), bundle_allow_surprise
  bundle_item_ids (JSON de ids)  ← ítems incluidos en cada pack (migración 21)
  created_time

product_images
  id, product_id, variant, position
  url, filename, size, type
  is_cover  ← portada del card (trigger garantiza 1 sola por producto)

orders
  id (uuid), status (pending|approved|rejected|pending_transfer|refunded|cancelled)
  total_amount, customer_name, customer_email
  shipping_address, shipping_city, shipping_region, shipping_zip
  mp_preference_id, mp_payment_id
  discount_code, discount_type (percent|fixed|shipping), discount_amount ← promo aplicada
  wants_newsletter, notes, created_at, updated_at

order_items
  id, order_id, product_id, product_name
  quantity, unit_price, original_unit_price, subtotal
  — product_id='shipping' identifica el ítem de envío

settings
  key (PK), value, updated_at
  — pares clave/valor editables desde /admin/settings
  — envío: shipping_cost (base/legacy) + shipping_cost_xs/_s/_m/_l (costo
    absoluto por tier, migración 20) + free_shipping_threshold

promo_codes
  id (text PK `prm_...`), code (normalizada a mayúsculas), description
  discount_type (percent|fixed|shipping), discount_value (%, CLP o referencia)
  min_subtotal, max_discount (tope para %)
  starts_at, expires_at, max_uses, uses_count
  active, archived, created_time
  — gestionados desde /admin/promos; el uso se suma en el webhook al aprobar

💳 Flujo de pagos

Usuario → Catálogo → Carrito → Checkout Form
                                    ↓
                         Orden PENDING en NeonDB
                         Preferencia en MercadoPago
                         Emails de intención (comprador + admin)
                                    ↓
                          Redirect → MercadoPago
                                    ↓
                         Usuario paga (tarjeta, transferencia)
                           ↙                    ↘
            /success?order_id=            /failure|pending?
                    ↓                              ↓
              get-order (estado)             get-order (estado)
                                ↓ (asíncrono)
                      mercadopago-webhook
                        · Actualiza orders.status
                        · Descuenta stock si approved
                             (salta product_id='shipping')
                        · Emails de confirmación
                                ↓ (respaldo)
                     reconcile-payments (cada 5 min)
                        · Órdenes pending/pending_transfer
                          se contrastan con la API de MP
                        · Auto-aprueba + descuenta stock
                          si el webhook se perdió/rechazó

Auto-aprobación: el estado de la orden y el descuento de stock se resuelven en una única función de fulfillment (netlify/functions/lib/fulfill.js) usada por el webhook, por get-order (cuando el comprador vuelve a /success) y por la reconcilier programada reconcile-payments — así una orden aprobada pasa sola a approved y baja stock aunque la notificación del webhook se pierda.

Costo de envío: cada producto clasifica el paquete que le cobra el courier (shipping_tier: XS | S | M | L, editable en el admin por producto). El costo de un pedido = costo absoluto del tier más grande presente (cuando se mezclan niveles, el más grande manda — no se suman). Los costos se configuran en /admin/settings → Envío (un monto por tier). Si aplica costo, se agrega como ítem product_id='shipping' (con el tier en el nombre, p. ej. "Envío a domicilio (S)") al array de ítems — así MercadoPago lo cobra y los emails lo muestran con breakdown separado. El servidor recalcula el tier y el costo siempre desde la base de datos (nunca confía en lo enviado por el cliente).


🎟️ Packs y add-ons

Los ítems de bajo valor (stickers, button pins, etc.) no justifican un envío propio, así que se venden dentro de packs o como agregados:

  • Pack (bundle): producto product_type='bundle' con bundle_unit_price (precio por ítem), bundle_sizes (p. ej. [3,4,6]), bundle_allow_surprise y bundle_item_ids (ids curados: la lista exacta de ítems que ese pack permite elegir). Su botón en el catálogo abre el constructor de packs (BundleBuilder): se elige tamaño y se combinan ítems del roster del pack (con stock). Cada pack tiene su propio roster — ya no se mezclan stickers con pins si el admin no los incluye. Si faltan ítems para completar el tamaño se muestra un aviso: con sorpresas, el usuario debe confirmar "acepto ítems sorpresa"; sin sorpresas, se bloquea hasta completar la selección.
  • Add-on (ítem de bajo valor): producto product_type='addon'. No aparece en el catálogo salvo que el carrito ya acumule subtotal ≥ costo de envío (entonces se puede añadir al mismo pedido sin envío extra).
  • El flag selectable_in_bundles es el "pool opt-in": solo los ítems marcados aparecen en la lista "Ítems incluidos" de cada pack en el admin.
  • El backend valida todo de nuevo en create-payment.js: tamaño ∈ bundle_sizes, selección = tamaño (explícitos + sorpresas), ítems dentro del roster del pack (bundle_item_ids) y con stock, precio recalculado desde la BD, y rechaza ítems add-on en pedidos cuyo subtotal no cubra el envío. Al aprobarse el pago, el webhook descuenta el stock de cada ítem elegido; los slots sorpresa se resuelven con stock disponible al despachar.

🎟️ Códigos de descuento

Se administran en /admin/promos. Un pedido aplica un solo código, validado SIEMPRE por el servidor al crear el pago (el checkout solo muestra el descuento estimado):

  • Tipos: percent (X% del subtotal, con tope opcional max_discount), fixed (monto CLP) o shipping (anula solo el costo de envío — no descuenta del subtotal).
  • Restricciones: subtotal mínimo, ventana starts_at/expires_at, max_uses y toggle active.
  • Uso de los códigos: uses_count se incrementa en el webhook al pasar la orden a approved (idempotente) — los pedidos abandonados en pending no consumen usos.
  • Almacenamiento: las líneas de order_items guardan el precio sin descuento (snapshot). El descuento vive en orders.discount_code / discount_type / discount_amount, y MercadoPago lo prorratea de forma exacta entre las líneas de producto (lib/discount.js).
  • Emails y confirmación: muestran la fila Descuento (CÓDIGO) −CLP o Envío gratis (CÓDIGO) según el tipo.

🖼️ Imágenes de productos (UploadThing)

Las imágenes se almacenan en UploadThing CDN (ufs.sh) y sus URLs en la tabla product_images.

# Migrar imágenes existentes en public/products/ → UploadThing
npm run migrate:images
# Lee: public/products/{slug}/*.jpg
# Sube a UploadThing, actualiza product_images en NeonDB
# is_cover = imagen cuyo basename coincide con el folder name

Desde el admin (/admin/products → Editar) puedes:

  • Ver todas las imágenes del producto en grilla 3×3
  • Subir nuevas (drag & drop o file picker, máx 8 MB)
  • Cambiar la portada (⭐ siempre visible)
  • Eliminar (borra de UploadThing + BD)
  • Hacer zoom con navegación ←→ y teclado

📧 Emails

Trigger Destinatario Template
Checkout (antes de pagar) Comprador Resumen carrito + link MP
Webhook approved Comprador Confirmación con detalle
Webhook pending_transfer Comprador Transferencia en proceso
Webhook rejected Comprador Pago fallido + reintentar
Checkout Admin Alerta nueva intención
Webhook approved / pending_transfer Admin Detalle completo de orden

Los emails muestran precio original tachado + precio oferta en descuentos, y breakdown subtotal / envío / total cuando hay costo de envío.

Cambiar proveedor: solo cambia EMAIL_PROVIDER=resend|mailgun en Netlify.


⚙️ Panel de administración (/admin)

Acceso mediante JWT con credenciales de las variables de entorno (ADMIN_EMAIL, ADMIN_PASSWORD). El token expira en 12h.

Sección Ruta Funcionalidad
Dashboard /admin Stats por periodo (hoy/7d/30d/6m/todo), últimas órdenes, stock bajo
Productos /admin/products Tabla con skeleton, toggles inline (visible/disponible/oferta/preventa), filtros, crear, editar, gestión de imágenes, exportar CSV
Pedidos /admin/orders Tabs por estado, cambio de estado con confirmación, notas internas, detalle con dirección de envío copiable y breakdown de totales, exportar CSV
Códigos /admin/promos Códigos de descuento (% / monto fijo / envío gratis), vigencia, límites de uso, toggle activo, exportar CSV
Configuración /admin/settings store_name, tagline, email contacto, modo mantenimiento, envío (habilitado/costo/umbral gratis), status integraciones

Las tablas incluyen skeleton animado (Motion) en la carga y stagger spring en la entrada de filas.

Detalle de orden

El panel lateral de /admin/orders separa Cliente de Envío. El bloque de envío muestra la dirección en líneas (calle / comuna–región / código postal) con un botón Copiar que deja en el portapapeles el formato de etiqueta de courier (nombre + dirección, una línea por campo). El tier cobrado —que en la base viaja dentro del nombre del ítem, p. ej. Envío a domicilio (M)— se extrae y se muestra como badge. Cuando el pedido no lleva despacho, el bloque lo dice explícitamente ("Retiro — sin envío a domicilio") en vez de omitirse, y si el envío salió gratis por umbral (no hay ítem shipping) se marca con el chip "sin costo".


📁 Estructura del proyecto

/
├── actions/
│   ├── createPayment.ts     # CartItem[] + CustomerData → MercadoPago
│   ├── getOrder.ts          # Consulta estado de orden por ID
│   ├── loadProducts.ts      # Carga productos desde NeonDB (fallback mock)
│   └── loadSettings.ts      # Configuración pública desde NeonDB (con fallback)
├── admin/                   # Panel de administración (/admin)
│   ├── AdminApp.tsx         # Rutas relativas + auth guard
│   ├── components/          # AdminLayout, AdminSidebar, OrderDetailPanel,
│   │                        # ProductEditPanel, ImageManager, TableSkeleton, Toggle...
│   ├── hooks/               # useAdminAuth, useAdminData, useAdminTitle, useRowSelection
│   ├── pages/               # DashboardPage, ProductListPage, OrderListPage,
│   │                        # SettingsPage, LoginPage
│   └── utils/adminFetch.ts  # Fetch autenticado con JWT
├── app/
│   ├── app.tsx              # Componente principal (catálogo, settings, carrito)
│   └── productsMock.ts      # Mock data para desarrollo local
├── components/
│   ├── CartDrawer.tsx        # Carrito lateral (Motion spring)
│   ├── CheckoutModal.tsx     # Form de datos + shipping props
│   ├── ProductCard.tsx       # Card con precio, oferta, badge, glow button
│   ├── ProductImageModal.tsx # Lightbox 2 columnas (shared element Motion)
│   ├── InfoModal.tsx         # "Sobre la tienda" con stagger
│   ├── OrderConfirmation.tsx # Páginas success/failure/pending
│   ├── CoinConfetti.tsx      # Confetti canvas-confetti en success
│   ├── MarkdownText.tsx      # Renderer Markdown sin dependencias
│   ├── DevTools.tsx          # Panel de dev (solo DEV mode)
│   └── ui/                  # shadcn/ui (button, dialog, toast...)
├── data/
│   └── comunas-chile.ts     # 346 comunas / 16 regiones de Chile
├── docs/
│   └── mercadopago-integration.md
├── hooks/
│   ├── useCart.ts           # Estado del carrito (localStorage)
│   └── useStoreSettings.ts  # Settings desde NeonDB con helpers parseados
├── migrations/              # 20 archivos SQL secuenciales para NeonDB
├── netlify/
│   └── functions/
│       ├── admin-api.js         # Router CRUD admin (JWT) — products, orders, images,
│       │                        # upload, settings, dashboard
│       ├── admin-auth.js        # POST login → JWT (timingSafeEqual, delay en fallo)
│       ├── create-payment.js    # Crea orden + preferencia MP + emails intención
│       ├── get-order.js         # Consulta orden por ID
│       ├── get-products.js      # Catálogo público desde NeonDB
│       ├── get-settings.js      # Configuración pública (cache 60s, fallback)
│       ├── mercadopago-webhook.js  # Webhook → actualiza estado + stock + emails
│       ├── emails/              # Templates HTML + providers (Resend/Mailgun)
│       └── package.json         # mercadopago, neon, resend, mailgun, uploadthing
├── public/
│   ├── products/            # Imágenes originales (backup — CDN en UploadThing)
│   ├── favicon-*.png
│   ├── apple-touch-icon.png
│   ├── social.jpg           # Imagen Open Graph (600×315)
│   └── site.webmanifest
├── scripts/
│   ├── generate-favicons.mjs           # npm run generate:favicons
│   └── migrate-images-to-uploadthing.mjs  # npm run migrate:images
├── src/
│   ├── main.tsx             # BrowserRouter + rutas lazy (tienda + admin)
│   ├── index.css            # Tailwind + custom CSS (glow buttons, etc.)
│   └── pages/               # SuccessPage, FailurePage, PendingPage, TerminosPage
├── types/
│   └── products.ts          # ProductRecord, ProductFields, CartItem...
├── .env.example             # Plantilla de variables (sin valores reales)
├── netlify.toml             # Build + headers seguridad + caché + noindex /admin*
└── tailwind.config.js       # Paleta de marca + tipografía

🎨 Diseño

Paleta de colores

Token Hex Uso
brand-primary #b45b38 Botones, CTAs, precios
brand-secondary #85422b Títulos, bordes
brand-accent #d4a373 Detalles, hovers
brand-background #fdfaf8 Fondo general
brand-surface #f5ece4 Cards, secciones
devs-text #2d1a12 Texto principal
devs-muted #7a6b63 Texto secundario

Tipografía

  • Onest — textos, body, labels
  • Fira Mono — títulos (h1–h4), DialogTitle, precios destacados

Animaciones (Motion v12)

Tienda:

  • Grid de productos: stagger + FLIP al filtrar por categoría
  • Cards: shared element image (card → modal lightbox), whileHover spring
  • Modal lightbox: slide entre imágenes con dirección, dots pill animados
  • Carrito: slide spring desde la derecha, items AnimatePresence popLayout
  • Checkout: stagger de campos, height reveal del bloque de envío, shake en validación
  • Modales de info: backdrop blur + spring easing

Admin:

  • Tablas: skeleton pulsante que replica la estructura de columnas → fade-out → stagger spring de filas de datos
  • Dashboard: skeleton cards, números con flip vertical al cambiar periodo, layoutId en selector de periodo
  • ProductEditPanel / OrderDetailPanel: slide desde la derecha + fade backdrop
  • ImageManager: shared element layoutId (thumbnail → modal zoom), 3D tilt en hover, burst ⭐ al cambiar portada

🛡️ Seguridad

  • CORS whitelist en todas las Netlify Functions
  • Validación y sanitización de inputs en el backend
  • Precio, stock, envío y descuento SIEMPRE recalculados en el servidor desde la BD — nunca se confía en lo que manda el cliente
  • Los packs solo se compran por su flujo (ítem con bundle, validado contra el roster): enviados como ítem plano se rechazan, para que no se cobren al price placeholder
  • Headers de seguridad en netlify.tomlX-Frame-Options: DENY, X-Content-Type-Options: nosniff, Referrer-Policy, Permissions-Policy
  • X-Robots-Tag: noindex, nofollow en todas las rutas /admin*
  • Caché inmutable para assets hasheados de Vite (/assets/*)
  • Cache-Control: no-store en endpoints de admin
  • Credenciales MP solo en env vars del servidor (nunca en el bundle del cliente)
  • Firma HMAC-SHA256 para validar webhooks de MercadoPago
  • Webhook idempotente — órdenes ya aprobadas no se reprocesam
  • Admin JWT: crypto.timingSafeEqual() en comparación de credenciales + delay de 500ms en fallos

📞 Contacto

Para consultas de la tienda: huemul@devschile.cl


📄 Licencia

Proyecto privado — © 2026 Tienda devsChile. Todos los derechos reservados.

About

Tienda para devsChile

Resources

Code of conduct

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages