El original lo dice en su propio texto ("los objetos disponibles son los mismos
de la tienda") y lo confirma su JS, que usa #store-list y .store-add-button: el
regalo NO tiene catálogo propio, es la tienda con el correo a otro personaje.
Nosotros teníamos una tabla aparte (home_item) con 8 objetos y precios en euros.
Ahora send-gift monta StoreBrowser en modo `gift`: mismo catálogo (3068 ítems,
PD/PV), mismo carrito con cantidades y las cuatro formas de pago. Antes NO tenía
Stripe: solo SumUp, PD y PV.
Dos pasos, como el original: primero #char-select-div (personaje de origen,
destino, confirmación y token) y solo al pulsar "Mostrar Regalos" aparece el
catálogo. Se borran SendGiftForm, /api/gift/checkout y lib/gift (ya no los usa
nadie); la tabla home_item se queda en la BD, sin usar.
Tres cosas que el usuario pidió y que estaban mal:
- El formulario va con `noValidate`: los avisos los damos nosotros en rojo
(#show-gif-response), no el navegador.
- "Mostrar Regalos" ahora valida contra el SERVIDOR (que el personaje de origen
sea tuyo, que el destino exista y que el token sea correcto). Antes elegías
objetos para descubrir al final que el destino no existía.
- BUG REAL: el nombre del destino distinguía mayúsculas. `characters.name` es
`utf8mb4_bin`, así que "innakh" NO encontraba a "Innakh" (comprobado: 0
resultados; con COLLATE, 1). `findCharacterByName` busca sin distinguir y
devuelve el nombre CANÓNICO, que es el que va al comando SOAP.
La validación vive en lib/gift-check y la usan las dos rutas: /api/gift/check (el
botón) y /api/gift/send, que revalida porque el cliente puede saltarse el paso 1.
También se quita del texto "El pago se realiza mediante SumUp": el original no lo
dice y además ya era falso con cuatro formas de pago.
Verificado: innakh / INNAKH / InNaKh resuelven a "Innakh"; token malo y destino
inexistente salen en rojo sin pasar al catálogo. Con 500 PD de saldo de prueba,
2 copias de un ítem de 200 pasan el cobro (deliveryFailed por el worldserver
caído, con su reembolso) y 3 dan insufficientPd: el servidor cobra por copias.
Datos de prueba borrados.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Salieron al pasar el lint por todo el proyecto. De 47 a 0 (quedan 15 avisos:
<img> vs <Image /> y el <link> del tema, los dos deliberados).
- Footer y Video (42 de los 47, que en realidad eran 7 enlaces: el plugin repite
cada uno 6 veces): usaban `<a href="/terms-and-conditions">` SIN el idioma
delante. No estaba roto de milagro: el middleware lo salvaba mirando la cookie
NEXT_LOCALE. Pero cada clic se comía un 307 y recargaba la página entera en
vez de navegar en cliente, y el idioma lo decidía la cookie en vez de la URL
en la que estás. Ahora van con el `Link` de i18n, que ya existía.
- Turnstile: escribía una ref (`cb.current = onVerify`) DURANTE el render. Es el
patrón de "callback fresca", pero no está permitido: React puede descartar ese
render y dejarla mal. Pasa a un efecto.
- ActivateClient y ConfirmClient: ponían el estado de error con setState dentro
del efecto cuando NO hay hash. Eso se sabe ya al renderizar (es un prop), así
que se deriva del estado inicial y se ahorra un render.
- BattlepayList: `window.location.href = …` -> `.assign()`, igual que en la
tienda.
- CookieConsent: aquí el efecto es correcto y la regla no aplica, así que se
silencia explicando por qué: el consentimiento vive en una cookie del
navegador, en el servidor no existe, y leerlo al renderizar rompería la
hidratación (le saldría el banner a quien ya había decidido).
Verificado: desde /es/ el pie enlaza a /es/terms-and-conditions y desde /en/ a
/en/terms-and-conditions; portada, cookies, tienda y mi-cuenta siguen dando 200.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Como el original (/store-bennu). El slug sale de acore_auth.realmlist, así que
no puede ser una carpeta fija: lo resuelve la ruta dinámica `[realm]`, que ya
servía /<slug>-realm y /<slug>-players con este mismo patrón. Solo hubo que
añadirle el tipo `store`; el contenido de la página se mueve a
components/StorePage.tsx.
/store da 404 sin código extra: al no existir ya la carpeta estática `store`,
entra por `[realm]`, no resuelve y cae en el notFound() que ya estaba.
Enlaces actualizados: el de /my-account (AccountTools, que es cliente y recibe
el href ya calculado) y el cancelUrl de la pasarela, que devolvía a /store.
Verificado: /es/store-trinity y /en/store-trinity dan 200 y la tienda carga el
catálogo; /es/store y /en/store dan 404; un slug ajeno (/es/store-bennu) también
404; /es/trinity-realm y /es/trinity-players siguen bien; y el enlace de
/my-account apunta a /es/store-trinity.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Investigadas las 750 categorías de la BD contra el HTML real del original. Los
ítems estaban perfectos (554 hojas, 3068 ítems, mismo contenido en cada una: no
faltaba ninguno). Todo lo roto era de categorías:
- 45 CATEGORÍAS FANTASMA, todas con nombre de código ("11-1", "23-10"). El seed
dedujo el árbol de los códigos de las hojas ("11-1-1" => padre "11-1"), pero
el original NO siempre pinta ese nivel: en 45 hojas el "-1-" del código no
corresponde a nada y cuelgan directas de su raíz. Ahora se lee el anidamiento
real (<ul>/<li>): 705 categorías (20 + 131 + 554) en vez de 750.
- El nombre de una categoría NO es texto plano, es HTML, y lo estábamos
aplanando: 141 nombres perdían su color y 40 cabeceras el suyo. Hay dos
mecanismos y los dos hacen falta: la clase en la CABECERA, que sustituye a
first-brown para colorear la categoría entera ("Brujo" en color de brujo), y
los <span class="no-toggle X"> dentro del nombre (clases y las etiquetas
[Nivel 232 a 245] / (DPS) en otro tono).
- 21 nombres TRUNCADOS en Tokens de Armadura: guardábamos "Guerrero" donde el
original dice "Guerrero, Sacerdote, Druida" (solo se quedó el primer span).
El HTML se trocea en el SERVIDOR (`parseCategoryName`) y al cliente le llegan
solo texto + nombre de clase, así que no hay HTML de la BD entrando en el DOM.
El texto sin color va suelto, NO envuelto en <span>: el tema tiene
`span { color: #fff }` y lo pondría blanco en vez de heredar el de la cabecera.
`name` pasa a VARCHAR(512): con el HTML, el más largo mide 198 y no cabía en 191.
El name_en se genera en el propio seed traduciendo solo el TEXTO y respetando
las etiquetas, así que sql/store_category_en.sql sobra y se borra.
gen_store_catalog.py regenera el seed entero (categorías del HTML, ítems de la
BD, que ya estaban verificados) y es reproducible byte a byte.
Verificado en el navegador contra el original: mismo árbol, 0 categorías con
nombre de código, "[Nivel 232 a 245]" en marrón tenue y los colores de clase
oficiales (Guerrero #C79C6E, Sacerdote blanco, Druida #FF7D0A). En inglés
también, conservando los colores.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
El carrito era un conjunto: añadir dos veces el mismo ítem no hacía nada y el
botón se quedaba en "Añadido". Ahora cada línea lleva sus copias, editables con
un <input number> en la columna Cant, y volver a pulsar Añadir suma una (mismo
patrón que el carrito de send-gift, que ya lo hacía así).
Ojo con los dos "cantidad" que conviven, que no son lo mismo:
`home_store_item.quantity` es el LOTE que entrega cada copia (Paño de lino = 20)
y `copies` es cuántas veces se compra la línea. Por eso la fila enseña "3 × 20"
y el total de la columna Cant sigue siendo unidades (60), como en el original.
Cada copia se manda al SOAP como una entrada propia (`2589:20 2589:20`), que es
justo lo que ya sabía trocear sendStoreItems (12 por correo) y parsear
fulfillStoreOrder: el formato del pedido de tarjeta no cambia.
El servidor no se fía del cliente: priceStoreCart recibe {id, copies}, valida las
copias (1..MAX_COPIES, como el MAX_QTY de send-gift), rechaza el carrito si algún
id no existe y recalcula los totales como precio*copias contra la BD.
Verificado contra la API con saldo de prueba, no solo en la interfaz: con 500 PD
y un ítem de 200, 2 copias (400) pasan el cobro y 3 (600) dan insufficientPd. Si
el servidor ignorase las copias ambas darían lo mismo, así que el corte exacto
entre 2 y 3 prueba que multiplica. Comprobado también que el reembolso del
deliveryFailed devuelve los 400 PD, y que copias=0 o 101 se rechazan.
El CSS del input va con `#cart-list` por especificidad, no por gusto: el tema
define `input[type=number] { width: 290px }` y se carga el último para ganar la
cascada, así que un `.store-copies` a secas perdía y el input salía de 290px
reventando la tabla (send-gift se libra porque usa estilo en línea).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
El carrito enseñaba PD/PV aunque eligieras tarjeta, y por defecto ya presuponía
saldo. Ahora se muestran SIEMPRE las dos monedas y se apaga la que no se va a
cobrar con el método elegido, así que no presupone nada ni se contradice.
Los euros por línea van con hasta 3 decimales a propósito: un ítem en PV con
precio impar cuesta medio céntimo (75 PV = 0,375 €), y redondear cada línea a 2
haría que las líneas no sumaran el total (0,38 + 0,38 = 0,76 contra un total de
0,75). El total sí va a céntimos: es el importe que se cobra de verdad. Y con
mínimo 2 decimales, que es dinero: "1,50 €", no "1,5 €". El formateo va por
Intl con el locale de la web, así que en español sale con coma; antes el
selector interpolaba el número crudo y decía "0.75 €" con punto.
Mínimos de las pasarelas: SumUp ya estaba en 1 € y Stripe estaba en 0,50 €,
ahora también 1 €. Estaban sueltos en la ruta; se centralizan en
`lib/store-pricing`, un módulo PURO que importan tanto la ruta como el
componente, para que el carrito enseñe exactamente lo que se valida y se cobra.
`storeEuroTotal` pasa a vivir ahí (una sola implementación) y lib/store lo
envuelve con un guard: si alguien cambia PD_PER_UNIT o VP_PRICE_FACTOR sin tocar
store-pricing, revienta al primer uso en vez de cobrar mal en silencio (esos
módulos tocan BD y no pueden llegar al bundle del cliente, de ahí la
duplicación, igual que en PaymentMethodSelect).
Por debajo del mínimo, la opción de tarjeta se desactiva y dice "Mínimo 1,00 €"
en vez de dejarte pulsar Enviar para que la API la rechace. El método efectivo
se deriva en el render (si el carrito baja del mínimo con tarjeta ya elegida,
cae al saldo) en vez de sincronizarlo con un setState en un efecto, que
provocaría renders en cascada.
Verificado en el navegador: con 0,375 € las dos tarjetas salen desactivadas y
elegido el saldo; con 1,50 € se habilitan. Las dos líneas de 0,375 € suman el
total de 0,75 €.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
El catálogo original solo tiene el nombre en español, así que /en/store venía
mostrando los ítems en español. Ahora salen de `home_item_data` (extraída de
los DB2 del cliente, commit 4ba9c42), que es el único sitio con el nombre en
inglés.
- getStoreCatalog(locale) hace LEFT JOIN con home_item_data y elige name_es o
name_en. Si falta el nombre en ese idioma se cae al del catálogo, que siempre
está, así que ningún ítem puede quedarse sin nombre.
- La columna se elige de una lista cerrada ('en' -> name_en, resto -> name_es):
nunca se interpola el locale crudo, que viene de la URL.
- /api/store/catalog recibe el locale por query: la ruta no cuelga de [locale].
Verificado en los dos idiomas: 45327 sale "Grito de la sirena" en /es y
"Siren's Cry" en /en. El árbol sigue devolviendo las 3068 filas y ninguna sin
nombre (el JOIN no duplica: `entry` es clave primaria).
Pendiente: los nombres de las CATEGORÍAS siguen solo en español
(home_store_category.name), así que /en/store se ve a medias. No son datos del
juego, así que los DB2 no ayudan: hay que traducir 218 nombres distintos.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replica el preview del store original: un icono en el cuadro del ítem que, al
pasar el ratón, muestra el render del objeto. El CSS ya estaba en el tema
(nw-st-tooltip-w / nw-st-img-tooltip / nw-img-display, copiado del original);
solo faltaban el markup y las imágenes.
- Los 545 PNG se sirven desde nw-images/nw-displays/ en vez de enlazar al
servidor original, para no depender de un tercero que puede cortarlo.
- Solo lo llevan los ítems equipables (610 de 3068): el resto no tiene
apariencia, igual que en el original. El dato ya estaba en la columna
`preview` de home_store_item y llegaba hasta el componente sin usarse.
- El icono va con `fas`, no con el `fa` suelto del original: cargamos Font
Awesome 6, donde solo .fas/.fa-solid activan la fuente, y el `fa` de FA4/5
no pintaría nada sin el shim de compatibilidad.
Descartado el visor 3D de wowhead: zamimg sirve el meta del modelo con lista
blanca de orígenes (403 desde nuestro dominio, 200 solo desde wowhead.com), así
que solo funcionaría proxeando sus assets para saltarse esa comprobación.
Verificado en el navegador: oculto por defecto, visible en hover, imagen 200.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Comparado contra el store original (ultimowow /es/store-bennu) con una sesión
real: markup, CSS y clases que aplica su power.js. Cinco causas distintas:
- Los nombres de ítem salían azules: el layout declaraba `const whTooltips`,
que queda en el ámbito léxico del script y NO crea propiedad en `window`,
que es de donde tooltips.js lee la config -> se ignoraba entera. Ahora
`window.whTooltips`, con colorLinks+iconizeLinks (el original no define
whTooltips y usa esos defaults; sus enlaces acaban con clases `icontinyl q4`).
renameLinks sigue en false: el texto sale de nuestra BD, en español.
- tooltips.js solo recorre el DOM al cargar, así que los enlaces que monta
React (hoja del árbol, carrito) no se teñían. Se pasan por
$WowheadPower.refreshLinks(), igual que hace el original tras cada AJAX.
- globals.css redefinía .item-box/.item-name/.item-img-box/.max-left-table2,
que el tema nw-ryu ya trae del original, y con más especificidad ganaba la
cascada. El peor: un `border-collapse` que anulaba el `border-spacing: 0 8px`
del tema, que es lo que separa las filas del carrito.
- Faltaban la caja "Carrito - <REINO>" y "Personaje seleccionado: <nombre>"
(coloreado por clase), que el original sí tiene.
- El total de la columna "Cant" mostraba el número de líneas en vez de la suma
de cantidades: un ítem que entrega un mazo de 20 debe contar 20, no 1.
Verificado con navegador contra un build de producción: el nombre del ítem sale
con clases `icontinyl q4` y color rgb(163,53,238), el mismo valor exacto que
devuelve el original.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Selector de forma de pago en el carrito (Saldo PD/PV · Stripe · SumUp), como el
resto de servicios. Con tarjeta, el carrito se convierte a euros (100 PD = 1 €,
200 PV = 1 €), se guarda como pedido y se entrega tras confirmarse el pago.
- BD: tabla home_store_order (ref, cuenta, personaje, items, importe) para no
depender del límite de metadata de la pasarela (sql/home_store_order.sql).
- lib/store: storeEuroTotal, createStoreOrder, fulfillStoreOrder.
- paid-services: servicio 'store' (fulfill = enviar el pedido por order_ref);
la entrega la dispara service-success/reconciliación/webhook como los demás.
- /api/store/send: rama provider stripe/sumup (crea pedido + checkout, mínimo
1 €/0,50 €) además del pago con saldo.
- StoreBrowser: selector Saldo/Stripe/SumUp; con tarjeta redirige a la pasarela.
- i18n Store (payMethod/payBalance/payStripe/paySumUp/confirmCard/amountTooLow)
y Paid.store (title/success). Verificado: euros 200PD+28PV=2,14 €, pedido y
fulfill por order_ref (SOAP real pendiente, worldserver caído).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replica el bloque Novedades del diseño original: fieldset plegable con el
changelog (Noviembre 2024) y sus objetos enlazados a wowhead (icono zamimg tiny
+ color por calidad). components/StoreNews.tsx (client, toggle con flecha
rotate/rotate2 del tema), incluido en la caja de info de /store. i18n
Store.newsTitle/newsIntro/newsDate (es/en).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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>
El script de tooltips prioriza el atributo data-wowhead sobre el subdominio del
href, y su parámetro `domain` codifica idioma + versión: el 1.er segmento va al
mapa de locales ({es:6,...}) y el resto a la versión (wotlk=WRATH). Antes se
enviaba `domain=wotlk` (sin idioma) → locale 0 = inglés siempre.
Ahora `wowheadData(type,id,locale)` genera `domain=es.wotlk` en español (locale 6)
y `domain=wotlk` en inglés. Verificado: el endpoint de datos con locale 6 devuelve
los nombres en español.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sustituye la base de datos de ítems del sistema antiguo (AoWoW en
wotlk.novawow/wotlk.ultimowow) y los iconos de mirrors por wowhead / zamimg,
con el idioma del tooltip según el locale de la web (es. / www.).
Sistema reutilizable para rutas actuales y futuras:
- lib/wowhead.ts: wowheadUrl/wowheadItemUrl/wowheadWotlkHome (subdominio por
locale), wowheadIcon (zamimg) y wowheadData (atributo data-wowhead, rama WotLK).
- components/WowheadLink.tsx: enlace con tooltip en el locale actual.
- layout raíz: config whTooltips (solo tooltips) + script tooltips.js global;
funciona con contenido añadido por React (carrito, búsquedas).
Aplicado en: send-gift (catálogo + carrito), restore-items, recruit
(RecruitPanel, enlace por item_id + icono zamimg), quest-character (misión/npc/
facción + iconos), y el enlace "WOTLK DB" de la cabecera.
Verificado en prod (:3001): /es -> es.wowhead.com, /en -> www.wowhead.com,
data-wowhead presente, iconos por zamimg.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Solo en /send-gift se añade una 4ª forma de pago: PV (vote points, gratis por
votar). Cuesta más que PD para preservar su valor: coste_PV = coste_PD ×
VP_PRICE_FACTOR (=2, configurable en lib/pay-with-dpoints).
- lib/dpoints: getVPointsBalance, creditVPoints y spendVPoints (débito atómico
del campo vp, con FOR UPDATE), espejo de las de PD.
- lib/pay-with-dpoints: VP_PRICE_FACTOR, vpointsCost() y payServiceWithVPoints()
(descuenta PV, ejecuta el envío y reembolsa si falla).
- gift/checkout: rama provider='vp' (además de 'pd').
- service-success: la entrega inmediata cubre 'pd' y 'vp'.
- PaymentMethodSelect: opción VP opcional (solo si el form pasa vpBalance);
muestra coste en PV, insuficiencia y el saldo VP.
- SendGiftForm + página: pasan el saldo VP.
- i18n Pay: vp, vpCost, insufficientVp, balanceVp, errors.insufficientVp (es/en).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Renombrar hermandad ya no es solo PD: ahora ofrece el mismo selector de
forma de pago que el resto de servicios (saldo PD / tarjeta Stripe / tarjeta
SumUp). El coste es equivalente: 1000 PD = 10 €.
- lib/guild: se separa la validación (checkGuildRenameEligibility) del
renombrado por SOAP (renameGuildBySoap); nuevo GUILD_RENAME_EUR (10 €).
- paid-services: entrada 'rename-guild' para que la reconciliación/webhook
entregue el servicio tras el pago con tarjeta.
- nueva ruta /api/guild/rename/checkout con las 3 pasarelas (valida
elegibilidad antes de cobrar); se elimina la antigua /api/guild/rename.
- RenameGuildForm usa PaymentMethodSelect y redirige a service-success.
- i18n: Paid.rename-guild (title/success) para la página de éxito.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Los 9 servicios con precio en euros (rename, customize, change-race,
change-faction, level-up, gold, transfer, restore-item, send-gift) ahora
ofrecen un selector de forma de pago con las 3 opciones: saldo PD, tarjeta
(Stripe) o tarjeta (SumUp).
- lib/dpoints: spendDPoints() descuenta PD de forma atómica (FOR UPDATE).
- lib/pay-with-dpoints: paga con saldo PD, ejecuta la acción al momento y
reembolsa si la ejecución falla; 100 PD = 1 €.
- rutas checkout (character/[service] y gift): rama provider='pd' que valida
saldo, descuenta, ejecuta fulfill y devuelve la URL de éxito (sin pasarela).
- service-success: rama provider='pd' (la entrega ya se hizo en el checkout).
- PaymentMethodSelect: componente compartido con coste por método y saldo;
desactiva PD si no hay saldo suficiente.
- Formularios (PaidServiceForm, Gold, Transfer, RestoreItems, SendGift) y sus
páginas pasan el saldo PD y envían el método elegido + locale.
- i18n: namespace Pay (es/en); añadidas claves Paid.restore-item que faltaban.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Al fallar (p.ej. contraseña incorrecta) el token de Turnstile ya se consumió en el
servidor; el reintento reenviaba el mismo token y daba "captcha fallida" aunque
estuviera resuelto. Se resetea el widget (setCaptcha('') + captchaKey++ con
<Turnstile key={captchaKey}>, mismo patrón que recover/restore/quest) en login,
create-account y trade-points (venta y canje). El check de código no usa captcha,
no se resetea ahí.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Mueve la página de login a /log-in y actualiza todas las referencias (enlace
CONECTAR del header y los redirect de auth de ~45 páginas). /login ahora da 404.
La API /api/auth/login no se toca (solo la ruta de página).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Desactiva la validación nativa del navegador ("Rellene este campo", formato de
email) en el resto de formularios (recover, reset-password, trade-points,
rename-guild, transfer-dp, gold, promo, transfer, quest, send-gift, 2FA, restore).
Los errores se muestran como red-form-response (por validación en cliente o del
servidor), consistente con login y create-account. Botones ya gateados por campos.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
La tabla de niveles mostraba solo PD ("Hasta 100 PD"); ahora añade el equivalente
en la moneda (100 PD = 1 €): "Hasta 100 PD (1 €)" / "Up to 100 PD (1 €)". Se
sustituyen los 14 mensajes range1..14 por 3 plantillas parametrizadas
(rangeFrom/rangeUpTo/rangeOver) con {pd}/{price}/{sym}; RANK_ROWS lleva el umbral
en PD y el tipo. Adaptado a la moneda (DP_CURRENCY).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Los formularios de crear categoría/foro aceptan nombre/descripción en inglés, y
se añade EDICIÓN en línea de categorías (name, name_en, order) y foros (name,
name_en, description, description_en). lib/admin-forum: createCategory/createForum
con EN + updateCategory/updateForum; rutas PUT en category/[id] y forum/[id].
Claves Admin nuevas (nombre/descripción EN). Verificado: listado con EN + update
round-trip; PUT protegido (403 sin admin).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
home_noticia gana columnas titulo_en/contenido_en (sql/add_news_en.sql). getNoticias(locale)
usa la versión EN si existe, si no cae al español (título y contenido independientes); la fecha
también se formatea por idioma. El panel de admin permite escribir la traducción al crear y EDITAR
noticias existentes (nuevo PUT /api/admin/news/[id], updateNews). Claves Admin nuevas (título/
contenido EN, editar/guardar). Probado: /en/ muestra EN, /es/ ES; PUT guardado (403 sin sesión).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Se externaliza el texto español hardcodeado de ~55 archivos a next-intl y se añaden
traducciones al inglés, con paridad de claves es/en. Nuevos namespaces: History,
CharService, CharServiceB, Points, Legal, UI, Misc (+ altas en Common/Admin).
- Historiales (PD/PV, transacciones, sanciones, seguridad), servicios de personaje
(transfer, send-gift, quest, restore-*, change-*, customize, level-up, gold, rename),
PD/pagos (d-points, trade, promo, transfer-dp, rename-guild, DPointsTabs), páginas
legales (cookies, privacidad, términos, reembolsos, aviso legal, contacto), layout
(cabecera, footer, cookies, 2FA, descargas, jugadores, recluta) y páginas varias
(home, reino, ayuda, addons, 2falogin).
- Textos con markup inline via t.rich; interpolación con ICU.
- Componente <NoteLegend/> para la leyenda NOTA/NOTE compartida.
- payLabel/confirmText de los servicios de pago traducidos.
- Verificado: tsc OK, next build OK, todas las claves t() resuelven en es y en,
todos los t.rich casan etiquetas, páginas 200 en /es/ y /en/ sin claves crudas.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
El nombre del reino (p.ej. "Trinity") salía en minúsculas y las minúsculas caían
a una fuente de reserva (Futura es de mayúsculas), desentonando con el resto del
nav. Se muestra en mayúsculas como los demás ítems.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
trailingSlash true ponía barra en todo (/es/content-creators/). Se pasa a
skipTrailingSlashRedirect + normalización en middleware: raíz de idioma /es → /es/
(con barra), y cualquier sub-ruta con barra final se redirige sin ella
(/es/x/ → /es/x). Sin bucles. Las /api quedan fuera del middleware (sin 308).
El redirect del logout apunta a /es/.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
next.config: trailingSlash true → /es redirige a /es/ y los enlaces se generan
con barra. Se ajustan a barra final los destinos internos que se enlazan a mano
(logout /log-out/ y su redirect, y los window.location.assign de login/
select-account) para evitar el salto 308 extra.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Los enlaces internos del header eran <a> planos sin prefijo (/download-client…);
ahora usan <Link> de next-intl → /es/download-client, /es/changelogs, reino,
jugadores, videos, ayuda y los logos. Externos (WOTLK DB) y el logout (route
handler) siguen como <a>. Resuelve también los avisos no-html-link-for-pages.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
React 19 bloquea href="javascript:void(0)". SiteHeader: DESCONECTAR ahora enlaza
a /{locale}/log-out (ruta real), el menú hamburguesa y el logo pequeño usan
role/button o href real. PostActions (foro): editar/eliminar pasan a role=button.
Se elimina el logout por fetch del header (lo hace la ruta /log-out).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Aplana el catálogo y lo pagina (8 por página = 2 filas de 4) con controles
Atrás / 1 2 3 / Siguiente centrados. Nuevas clases .gift-grid (grid 4 cols,
responsive a 2 en móvil) y .gift-pagination en novawow-style.css.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Envía ítems de la tienda por correo al personaje de un amigo, pagado por
SumUp. lib/gift.ts (catálogo por categoría, priceCart con precios reales de
BD, sendGiftByMail vía SOAP .send items troceado a 12/correo + anti-inyección);
servicio send-gift en PAID_SERVICES; ruta dedicada /api/gift/checkout (origen
propio, destino existe, token de seguridad, carrito); SendGiftForm + página con
el diseño; i18n Paid[send-gift]; seed_giftitems.sql (4 cat, 8 ítems).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Nuevo servicio de pago `restore-item` (getRestoreItemPrice default 1€, fulfill
SOAP `.item restore <recover_id> <char>`). Al pulsar "Recuperar" se crea un
checkout de SumUp y al pagar se ejecuta el restore.
- Se quita el descuento de 100 PD y el gate "No tienes PD suficientes".
- La búsqueda sigue igual (gratis, cooldown 8h, Turnstile).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- /restore-character: recupera personajes borrados por BD (como .character deleted
restore). Condiciones: nivel>60, borrado <30 días, DK no si ya hay DK activo,
nombre libre, cooldown 12h/personaje. Gratis. Tabla home_restorehistory.
- /restore-items: recupera ítems borrados vía SOAP del core (.item restore list /
.item restore); el core aplica calidad/equipable/BoP/<7días. Cada ítem 100 PD
(reembolso si SOAP falla), consulta con cooldown 8h/personaje + Turnstile.
Tabla home_itemsearchhistory.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Nueva ruta /transfer-character (10€ SumUp); borra /transfer (Stripe) y actualiza
el link del panel.
- lib/transfer-character.ts: precheck con todas las condiciones — personaje offline,
contraseña, token de seguridad, 2FA activo, cuenta destino válida (≠ propia) y,
para Caballeros de la Muerte, la cuenta destino debe tener un personaje nivel >=55
y no tener ya un DK; + bloqueo de 5s tras transferencia reciente.
- El hook precheck de PaidServiceConfig ahora recibe {accountId,email,character,body}.
- TransferForm: contraseña + token (con ojos), provider sumup, confirm.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- /change-race-character (3€), /change-faction-character (5€, valida condiciones:
sin hermandad/correo/subastas/capitán-arena y oro <= cap por nivel),
/level-up-character (10€), /gold-character (oro por correo SOAP). Cada uno borra
su ruta antigua y actualiza el link del panel.
- SumUp: hook precheck en PaidServiceConfig (condiciones cambio de facción);
columna home_stripelog.metadata para campos extra (gold_amount).
- /quest-character: rastreador de estado de misiones/cadenas (clase, Hijos de
Hodir, misión por ID) en personaje offline; cooldowns por categoría, Turnstile,
IDs de misión verificados en wotlkdb 3.3.5a. Tabla home_questsearchhistory.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- /rename-character y /customize-character: renombrar/personalizar pagados por SumUp
(getRenamePrice/getCustomizePrice). SumUp ahora es service-aware: columna
home_stripelog.service; createSumUpCheckout guarda service+character;
fulfillSumUpCheckout despacha a la acción del servicio (.char rename/customi) o,
sin service, acredita PD. [service]/checkout acepta provider:'sumup';
PaidServiceForm acepta provider+confirmText; service-success maneja el return SumUp.
Se eliminan las antiguas /rename y /customize (Stripe).
- /revive-character (comparte ReviveServiceContent); se elimina /revive.
- Renombres de ruta + todos sus enlaces:
/account -> /my-account, /recruit -> /recruit-a-friend, /unstuck -> /unstuck-character.
(/select-account y /api/account/* intactos.)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- CharacterSelect: componente reutilizable que colorea las opciones por clase
(class="priest big-font"…) y el <select> con la clase seleccionada. Usado en
unstuck, revive, gold, transfer, trade-points, servicios de pago y recruit.
getGameCharacters ahora devuelve classCss.
- /unstuck (+revive): diseño del original (info + NOTA, prompt, opciones coloreadas,
botón "Desbloqueado"/"Revivido" con refresh).
- /vote-points: seed de los 4 sitios con logos y URLs (sql/seed_votesites.sql);
botón voted-button para sitios en cooldown; etiqueta "Nota:" separada.
- account-info.png (imagen nueva) y CSS de los paneles de cuenta: se añade
background-blend-mode: saturation y background-size en #account-settings y
.account-fieldset para que la imagen salga como el original.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- /security-token: diseño completo del original (info + advertencia + NOTA 7 días),
fecha en formato HH:MM:SS DD-MM-YYYY, botón "Token enviado", enlace a /recover.
- /recover: 4 opciones (contraseña por usuario, nombre de cuenta, token de seguridad
y enlace de activación por correo); recuperación de token nueva; Turnstile por envío.
- /vote-points: caja de info con las 7 secciones Q&A + panel "Sitios de votación" con
tarjetas inline-div (imagen, PV, último voto), botón refrescar; abre el sitio y acredita
PV al volver. lib/vote.ts + getVoteSitesForAccount.
- /rename-guild: renombra una hermandad de la que la cuenta es Maestro por 1000 PD +
token de seguridad; SOAP .guild rename con comillas; reembolsa los PD si SOAP falla.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- /transfer-d-points: transferir PD a la cuenta dueña de un personaje
(requiere token de seguridad; movimiento atómico con FOR UPDATE).
- /trade-points: comercio PD<->oro mediante códigos de 1 solo uso
(contraseña + token + Turnstile; oro al creador por SOAP; expira 1h;
lock de 5s tras canjear; rollback total si falla la entrega).
- /promo-code: canje de códigos por PD/PV (sensibles a mayúsculas/minúsculas,
usos limitados, 1 canje por cuenta).
- /admin/promo: panel para crear / editar / activar / desactivar / borrar
códigos de promoción.
- Tablas nuevas (sql/): home_tradecode, home_promocode, home_promoredemption.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Activación de 2FA (TOTP) para el login del juego, guardando el secreto en
acore_auth.account.totp_secret tal como lo lee el bnetserver 3.4.3.
- lib/two-factor.ts: TOTP (HMAC-SHA1, 30s, 6 dígitos, ±1) idéntico al core
(verificado contra los vectores RFC 6238), Base32, otpauth, y codificado
del secreto: crudo por defecto o AES-128-GCM (ciphertext‖IV12‖tag12) si se
define TOTP_MASTER_SECRET (mismo hex que TOTPMasterSecret del servidor).
- Flujo seguro sin bloqueos: el secreto se genera y queda PENDIENTE en la
sesión; sólo se escribe en la cuenta tras verificar un código válido.
- /api/account/2fa: activar (password + token de seguridad), verificar y
desactivar (con código actual). QR generado en el servidor (el secreto no
sale a terceros).
- Página + componente fieles al markup (preview, pasos, copiar clave, ojos).
Verificado E2E: activar -> QR/secreto -> verificar -> totp_secret = 20 bytes
(igual al secreto escaneado) -> desactivar -> NULL.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Elimina las pestañas de PayPal/dLocal/Crypto/Skrill y por país; deja
Información + Stripe + SumUp (las dos únicas pasarelas que se usarán).
- Stripe: totalmente funcional reutilizando createCheckoutSession + webhook
+ página de éxito. Nueva ruta /api/dpoints/checkout crea la sesión con
importe elegido por el usuario (1 unidad = 100 PD).
- SumUp: integración real (lib/sumup.ts) mediante checkout hospedado,
habilitada al definir SUMUP_API_KEY/SUMUP_MERCHANT_CODE; si no, avisa.
- Nuevo servicio de entrega `dpoints` (lib/paid-services.ts) que acredita
PD en home_api_points; entrega idempotente vía reclamo atómico.
- Página /d-points-success entrega ambos proveedores (session_id/ref).
- Iconos SVG generados para cada pasarela (stripe/sumup, marca + wordmark).
- Moneda configurable con DP_CURRENCY (por defecto EUR).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Reproduce fielmente el panel de compra de PD de UltimoWoW como componente
cliente con pestañas (Información, PayPal, dLocal Go, Crypto, Skrill y
métodos alternativos por país: Argentina, Chile, Colombia, Perú).
- Página servidor con login requerido (redirige a /login y /select-account
como battlepay); inyecta username de la cuenta de juego e IP reales.
- Pestañas, tabla de rangos con mostrar/ocultar, y calculadora ARS
(dolarapi) migradas a estado/efectos de React.
- Formularios con casilla de T&C que habilita el botón; PayPal usa
PAYPAL_BUSINESS_EMAIL por entorno; dLocal/NOWPayments muestran aviso
hasta configurar su backend (no se fabrican credenciales/gateways).
- Banderas por país y logo de NOWPayments añadido a nw-logos.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Reescrita la página: box «Información» con la guía completa de Recluta a un
amigo (marca → nombre del reino) + box «Panel de Recluta a un amigo».
- Nuevo RecruitPanel: estadísticas (cuenta reclutada, amigos reclutados, amigos
al nivel 80, recompensas reclamadas X/6) + tabla de recompensas (objetivo,
objeto con icono y color de calidad, estado) con botón Reclamar para las
disponibles + sección «Recompensas disponibles». Sustituye a RecruitClaim.
- lib/recruit-claim: getRecruitStats (isRecruited + recruitedCount).
- Colores de calidad de objeto .q0-.q5 en globals.css.
- Sembradas las 6 recompensas en home_recruitreward (seed en sql/).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
El <form> dentro del <p> «Nombre de la cuenta» era HTML inválido y el mensaje
salía inline (en inglés «Wrong password» partía en «Add Wrong / Password»).
Se quita el form: input+botón (onClick, Enter) en un span-bloque .add-account-box
debajo del nombre, y el mensaje en .add-account-msg (display:block) en su propia
línea. Igual en ES y EN.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Junto al «Nombre de la cuenta» hay un botón «+» que pide la contraseña y crea
una nueva cuenta de juego bajo la Battle.net. Ruta /api/account/add-game-account:
verifica la contraseña (authenticate contra battlenet_accounts), calcula el
siguiente battlenet_index (máx 8) y crea la fila `account` con SRP6 Grunt
derivado de la contraseña (gameMakeRegistration) — igual que la activación, así
la cuenta sirve para entrar al juego. Recarga completa al crear/cambiar.
GameAccountSwitcher siempre presente (select si >1, texto si 1) + «+».
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- El desplegable WOW1/WOW2… ya no es un box aparte: va dentro del campo «Nombre
de la cuenta» (select compacto .account-select). Con una sola cuenta, texto plano.
- FIX iconos: el cambio de cuenta hacía router.refresh() (navegación suave) que en
algunos navegadores dejaba caer las hojas de estilo/FA y no se recuperaban. Ahora
hace window.location.reload() (recarga completa) → todo se recarga bien.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Si la Battle.net tiene más de una cuenta de juego, /account muestra un
desplegable «Cargar datos de la cuenta» (WOW1, WOW2…). Al cambiar, fija la
cuenta activa en la sesión vía /api/auth/select-account y refresca → toda la
página (datos, personajes, servicios, compras) opera sobre la cuenta elegida
(la sesión.accountId manda). Con una sola cuenta no aparece (como hasta ahora).
components/GameAccountSwitcher.tsx (client) + getGameAccounts en la page.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
accountTools en el orden del markup: cambiar contraseña, 2FA (email-icon,
label purple-info), transferir PD, comercio de PD, código promoción, token,
recluta, renombrar hermandad, votar, adquirir PD. «Cambiar correo» (que
tenemos y no está en la lista) al final; panel admin el último (solo admin).
Tool.labelClass para el morado del 2FA. Iconos nuevos del sprite: trade-pd-icon
(-299 -928) y color .purple-info (#b172ff) añadidos al tema. Claves i18n nuevas
(svc2FA/TransferDP/TradePD/Promo/RenameGuild/DPoints). Rutas nuevas → 404 aún.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- characterTools reordenado y completado según el markup del usuario (14 items):
...gold, Rastreador de misiones (/quest), transfer, Recuperar personaje
(/restore-character), Recuperar ítems (/restore-items), Tienda (/store),
Enviar regalo (/send-gift). Se retira battlepay del panel (queda en cabecera).
Claves i18n nuevas (svcQuest/RestoreChar/RestoreItems/StoreItems/SendGift).
- Sprite de iconos actualizado a account-icons.png (de uw-ryu) que incluye el
icono de regalo; CSS: .acc-icon → .png, send-gift-icon (-424 -553) y promo-icon
reubicado (-174 -928), verificado que las 25 posiciones restantes coinciden.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>