Implementa la tienda /store (catálogo BENNU: 750 categorías, 3068 ítems)

Porta la tienda de ítems/transfiguración del sistema antiguo: elegir personaje →
árbol de categorías (3 niveles) → carrito → enviar por correo. Se paga con el
saldo PD o PV de la cuenta (cada ítem lleva su precio y moneda).

- BD: home_store_category (árbol por code "1-1-1") + home_store_item
  (mismos item_id, nombres, iconos, precios y moneda que el original;
  2947 en PD, 121 en PV). Seed en sql/store_catalog.sql, extraído del HTML real.
- lib/store.ts: getStoreCatalog (árbol anidado), getStoreBalances, priceStoreCart
  (precios REALES de BD), purchaseStoreCart (cobro atómico PD+PV con FOR UPDATE y
  reembolso si el envío SOAP falla; `.send items` troceado a 12/correo).
- API: GET /api/store/catalog (tras elegir personaje) y POST /api/store/send.
- components/StoreBrowser.tsx: selector de personaje, árbol colapsable (ítems
  montados solo al abrir), carrito con totales PD/PV, enviar, modal de resultado.
- página /store con la info y el título "Tienda - REINO". Enlaces de ítems e
  iconos por wowhead/zamimg (con tooltip y locale). El enlace ya existía en
  AccountTools. i18n namespace Store (es/en). CSS del árbol/carrito/modal.

Verificado: catálogo E2E (750 cats/3068 ítems), cobro atómico + reembolso y
detección de saldo insuficiente (cuenta 15). SOAP real sin probar (worldserver
caído en este entorno).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-14 23:54:57 +00:00
parent 033ebc6861
commit 026fbcb81c
9 changed files with 4537 additions and 0 deletions
+67
View File
@@ -516,3 +516,70 @@ textarea:focus {
.pay-method-label { color: #ebdec2; font-weight: bold; }
.pay-method-sub { font-size: 13px; }
.pay-method-balance { margin-top: 2px; font-size: 13px; }
/* ==== Tienda (/store) ==== */
#store-list, #store-list ul { list-style: none; margin: 0; padding-left: 14px; }
#store-list > li { margin: 4px 0; }
#store-list .store-cat,
#store-list .store-subcat,
#store-list .store-sub-subcat {
display: inline-block !important;
cursor: pointer;
padding: 3px 0;
user-select: none;
}
#store-list .store-cat:hover,
#store-list .store-subcat:hover,
#store-list .store-sub-subcat:hover { color: #fff; }
#store-list .store-cat i,
#store-list .store-subcat i,
#store-list .store-sub-subcat i { transition: transform .2s; margin-right: 4px; }
/* Rejilla de objetos dentro de una hoja (visible: React solo la monta al abrir) */
#store-list .item-list {
display: flex !important;
flex-wrap: wrap;
gap: 10px;
justify-content: center;
margin: 8px 0 8px 10px;
}
#store-list .item-box {
width: 150px;
border: 2px solid #352e2b;
border-radius: 8px;
background: rgba(0, 0, 0, 0.25);
padding: 10px 8px;
text-align: center;
}
#store-list .item-box .item-img-box { width: 44px; height: 44px; border-radius: 6px; }
#store-list .item-box .item-name { margin: 4px 0; min-height: 32px; }
#store-list .item-box hr { margin: 6px 0; border-color: #352e2b; }
#store-div { margin-top: 14px; }
/* Carrito */
#cart-list { margin-top: 16px; }
#cart-list table.max-left-table2 { width: 100%; max-width: 620px; margin: 0 auto; border-collapse: collapse; }
#cart-list th, #cart-list td { padding: 6px 8px; }
#cart-list th.justified, #cart-list td.justified { text-align: left; }
#cart-list .icon-td { text-align: center; width: 42px; }
#cart-list .width-50-td { width: 50%; }
/* Modal de resultado */
.modal-div {
position: fixed; inset: 0; z-index: 1000;
background: rgba(0, 0, 0, 0.6);
display: flex; align-items: center; justify-content: center;
}
.modal-content {
position: relative;
background: #17110e;
border: 2px solid #d79602;
border-radius: 10px;
padding: 26px 30px;
max-width: 90vw; min-width: 260px;
text-align: center;
}
.modal-close {
position: absolute; top: 6px; right: 12px;
cursor: pointer; font-size: 22px; color: #b1997f;
}
.modal-close:hover { color: #fff; }