store: iguala el carrito y el cuadro de ítem al diseño original

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>
This commit is contained in:
2026-07-15 07:11:07 +00:00
parent 742567cfbf
commit 8e16ba8391
6 changed files with 142 additions and 92 deletions
+113 -68
View File
@@ -1,6 +1,6 @@
'use client'
import { useState } from 'react'
import { useEffect, useState } from 'react'
import { useTranslations, useLocale } from 'next-intl'
import { CharacterSelect, type CharOption } from '@/components/CharacterSelect'
import { WowheadLink } from '@/components/WowheadLink'
@@ -9,6 +9,17 @@ import type { StoreCategory, StoreItem } from '@/lib/store'
const ICON_FALLBACK = 'inv_misc_questionmark'
/**
* Reaplica el tratamiento de wowhead (con `colorLinks` = teñir por calidad) a los
* enlaces que React acaba de montar. `tooltips.js` solo recorre el DOM al cargar,
* así que sin esto los ítems de una categoría recién abierta o del carrito se
* quedan con el azul por defecto del tema. Es lo mismo que hace el store original
* con $WowheadPower.refreshLinks() tras cada AJAX.
*/
function refreshWowheadLinks() {
;(window as unknown as { $WowheadPower?: { refreshLinks: () => void } }).$WowheadPower?.refreshLinks()
}
/** Nodo de categoría colapsable; renderiza sus ítems solo cuando está abierto. */
function CategoryNode({
cat,
@@ -24,6 +35,10 @@ function CategoryNode({
const t = useTranslations('Store')
const [open, setOpen] = useState(false)
const spanClass = depth === 0 ? 'store-cat' : depth === 1 ? 'store-subcat' : 'store-sub-subcat'
// Los ítems de la hoja se montan al abrirla: hay que teñirlos entonces.
useEffect(() => {
if (open) refreshWowheadLinks()
}, [open])
return (
<li>
<span className={`${spanClass} first-brown`} role="button" tabIndex={0} onClick={() => setOpen((o) => !o)}>
@@ -80,7 +95,17 @@ function CategoryNode({
type PayMethod = 'balance' | 'stripe' | 'sumup'
export function StoreBrowser({ characters, dp, vp }: { characters: CharOption[]; dp: number; vp: number }) {
export function StoreBrowser({
characters,
dp,
vp,
realm,
}: {
characters: CharOption[]
dp: number
vp: number
realm: string
}) {
const t = useTranslations('Store')
const locale = useLocale()
const [character, setCharacter] = useState('')
@@ -123,8 +148,18 @@ export function StoreBrowser({ characters, dp, vp }: { characters: CharOption[];
}
const lines = [...cart.values()]
// Las filas del carrito las repinta React: hay que volver a teñirlas.
useEffect(() => {
refreshWowheadLinks()
}, [cart])
const pdTotal = lines.filter((l) => l.currency === 'pd').reduce((s, l) => s + l.price, 0)
const vpTotal = lines.filter((l) => l.currency === 'pv').reduce((s, l) => s + l.price, 0)
// Total de la columna "Cant": como en el original, la SUMA de las cantidades de
// cada línea (un ítem que da un mazo de 20 cuenta 20), no el número de líneas.
const qtyTotal = lines.reduce((s, l) => s + l.qty, 0)
// Clase de color del personaje elegido, para pintarlo como el original.
const charClass = characters.find((c) => c.name === character)?.classCss ?? ''
// 100 PD = 1 €, 200 PV = 1 € (igual que en lib/store storeEuroTotal).
const eurTotal = Math.round((pdTotal / 100 + vpTotal / 200) * 100) / 100
@@ -185,76 +220,86 @@ export function StoreBrowser({ characters, dp, vp }: { characters: CharOption[];
))}
</ul>
{/* Carrito */}
<div id="cart-list">
{lines.length > 0 && (
<div className="pay-method-select">
<p className="second-brown">{t('payMethod')}</p>
<div className="pay-method-options">
{([
{ id: 'balance' as PayMethod, label: t('payBalance'), sub: `${pdTotal} PD · ${vpTotal} PV` },
{ id: 'stripe' as PayMethod, label: t('payStripe'), sub: `${eurTotal} ` },
{ id: 'sumup' as PayMethod, label: t('paySumUp'), sub: `${eurTotal}` },
]).map((o) => (
<label key={o.id} className={`pay-method-option${method === o.id ? ' selected' : ''}`}>
<input type="radio" name="store-pay" value={o.id} checked={method === o.id} onChange={() => setMethod(o.id)} />
<span className="pay-method-label">{o.label}</span>
<span className="pay-method-sub yellow-info">{o.sub}</span>
</label>
))}
{/* Carrito: caja propia con título y personaje elegido, como el original. */}
<div className="title-box-content">
<h2>
{t('cartTitle')} - <span className="blue-info">{realm.toUpperCase()}</span>
</h2>
</div>
<div className="body-box-content">
<p className="centered">
{t('selectedCharacter')}: <span className={`${charClass} big-font`}>{character}</span>
</p>
<div id="cart-list">
{lines.length > 0 && (
<div className="pay-method-select">
<p className="second-brown">{t('payMethod')}</p>
<div className="pay-method-options">
{([
{ id: 'balance' as PayMethod, label: t('payBalance'), sub: `${pdTotal} PD · ${vpTotal} PV` },
{ id: 'stripe' as PayMethod, label: t('payStripe'), sub: `${eurTotal}` },
{ id: 'sumup' as PayMethod, label: t('paySumUp'), sub: `${eurTotal}` },
]).map((o) => (
<label key={o.id} className={`pay-method-option${method === o.id ? ' selected' : ''}`}>
<input type="radio" name="store-pay" value={o.id} checked={method === o.id} onChange={() => setMethod(o.id)} />
<span className="pay-method-label">{o.label}</span>
<span className="pay-method-sub yellow-info">{o.sub}</span>
</label>
))}
</div>
</div>
</div>
)}
<table className="max-left-table2">
<tbody>
<tr>
<th className="width-50-td justified">{t('colItem')}</th>
<th className="justified">{t('colQty')}</th>
<th className="justified">{t('colValue')}</th>
<th className="icon-td">
<button type="button" className="store-empty-button" title={t('empty')} onClick={() => setCart(new Map())} disabled={cart.size === 0}>
<i className="fas fa-trash" />
</button>
</th>
</tr>
{lines.length === 0 ? (
)}
<table className="max-left-table2">
<tbody>
<tr>
<td colSpan={4} className="second-brown centered">{t('cartEmpty')}</td>
<th className="width-50-td justified">{t('colItem')}</th>
<th className="justified">{t('colQty')}</th>
<th className="justified">{t('colValue')}</th>
<th className="icon-td">
<button type="button" className="store-empty-button" title={t('empty')} onClick={() => setCart(new Map())} disabled={cart.size === 0}>
<i className="fas fa-trash" />
</button>
</th>
</tr>
) : (
lines.map((l) => (
<tr key={l.id}>
<td>
<WowheadLink id={l.itemId}>{l.name}</WowheadLink>
</td>
<td><span>{l.qty}</span></td>
<td>
<span className={l.currency === 'pv' ? 'vp-color' : 'dp-color'}>{l.currency === 'pv' ? 'PV' : 'PD'}:</span> <span>{l.price}</span>
</td>
<td className="icon-td">
<button type="button" className="store-remove-button" onClick={() => removeItem(l.id)} title={t('remove')}>
<i className="fas fa-trash red-info" />
</button>
</td>
{lines.length === 0 ? (
<tr>
<td colSpan={4} className="second-brown centered">{t('cartEmpty')}</td>
</tr>
))
)}
<tr><td colSpan={4}><hr /></td></tr>
<tr>
<td></td>
<td><span>{lines.length}</span></td>
<td>
<span className="dp-color">PD: </span><span>{pdTotal}</span><br />
<span className="vp-color">PV: </span><span>{vpTotal}</span>
</td>
<td>
<button type="button" className="store-send-button" onClick={send} disabled={sending || cart.size === 0}>
{sending ? t('sending') : t('send')}
</button>
</td>
</tr>
</tbody>
</table>
) : (
lines.map((l) => (
<tr key={l.id}>
<td>
<WowheadLink id={l.itemId}>{l.name}</WowheadLink>
</td>
<td><span>{l.qty}</span></td>
<td>
<span className={l.currency === 'pv' ? 'vp-color' : 'dp-color'}>{l.currency === 'pv' ? 'PV' : 'PD'}:</span> <span>{l.price}</span>
</td>
<td className="icon-td">
<button type="button" className="store-remove-button" onClick={() => removeItem(l.id)} title={t('remove')}>
<i className="fas fa-trash red-info" />
</button>
</td>
</tr>
))
)}
<tr><td colSpan={4}><hr /></td></tr>
<tr>
<td></td>
<td><span>{qtyTotal}</span></td>
<td>
<span className="dp-color">PD: </span><span>{pdTotal}</span><br />
<span className="vp-color">PV: </span><span>{vpTotal}</span>
</td>
<td>
<button type="button" className="store-send-button" onClick={send} disabled={sending || cart.size === 0}>
{sending ? t('sending') : t('send')}
</button>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
)}