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:
@@ -68,11 +68,20 @@ export default async function LocaleLayout({
|
||||
{/* Tema real de NovaWoW (nw-ryu). Va el ÚLTIMO para ganar la cascada. */}
|
||||
<link rel="stylesheet" href="/nw-themes/nw-ryu/nw-css/novawow-style.css?v=2" />
|
||||
{/* Config de los tooltips de wowhead (debe definirse antes de tooltips.js).
|
||||
Solo tooltips: no recolorea/renombra/iconiza nuestros enlaces. El idioma
|
||||
del tooltip lo da el subdominio del enlace (es./www.). */}
|
||||
OJO con el `window.`: tooltips.js lee `window.whTooltips`, y un `const` a
|
||||
nivel de script queda en el ámbito léxico del script (no crea propiedad en
|
||||
window), así que la config se ignoraba entera y valían los valores por
|
||||
defecto de wowhead.
|
||||
`colorLinks` tiñe cada enlace según la calidad del ítem e `iconizeLinks` le
|
||||
antepone el icono pequeño: los dos replican lo que hace el store original
|
||||
(sus enlaces acaban con las clases `icontinyl q4`). Sin colorLinks los
|
||||
nombres caen al `a{color:#2471a9}` del tema y salen todos azules.
|
||||
`renameLinks` NO: el texto de los enlaces es el nombre de nuestra BD (en
|
||||
español), y wowhead lo sobreescribiría. El idioma del tooltip lo da el
|
||||
subdominio del enlace (es./www.). */}
|
||||
<script
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: 'const whTooltips={colorLinks:false,iconizeLinks:false,renameLinks:false};',
|
||||
__html: 'window.whTooltips={colorLinks:true,iconizeLinks:true,renameLinks:false};',
|
||||
}}
|
||||
/>
|
||||
</head>
|
||||
|
||||
@@ -57,6 +57,7 @@ export default async function StorePage({ params }: { params: Promise<{ locale:
|
||||
characters={chars.map((c) => ({ name: c.name, classCss: c.classCss }))}
|
||||
dp={balances.dp}
|
||||
vp={balances.vp}
|
||||
realm={realm}
|
||||
/>
|
||||
</PageShell>
|
||||
)
|
||||
|
||||
+12
-21
@@ -538,38 +538,29 @@ textarea:focus {
|
||||
#store-list span.store-cat i,
|
||||
#store-list span.store-subcat i,
|
||||
#store-list span.store-sub-subcat i { transition: transform .2s; margin-right: 4px; }
|
||||
/* Rejilla de objetos dentro de una hoja (visible: React solo la monta al abrir) */
|
||||
/* Rejilla de objetos dentro de una hoja. El tema YA define .item-box, .item-name,
|
||||
.item-img-box y .item-box hr igual que el original: no se redefinen aquí (hacerlo
|
||||
con `#store-list ...` gana la cascada y descuadra el cuadro del ítem). Lo único
|
||||
que hace falta es deshacer el `display:none` con el que el tema oculta .item-list
|
||||
(el original la revelaba con jQuery; aquí la monta React) y reponer el hueco entre
|
||||
cajas: el PHP dejaba espacios en blanco entre los inline-block y JSX no los emite,
|
||||
así que se imita con column-gap (el vertical ya lo da el margin del tema). */
|
||||
#store-list .item-list {
|
||||
display: flex !important;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
justify-content: center;
|
||||
align-items: flex-start;
|
||||
gap: 0 5px;
|
||||
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; }
|
||||
/* El tema esconde #store-div y los <ul> anidados (`display:none`) porque el store
|
||||
original los revelaba con jQuery (.show()/slideToggle()). Aquí la visibilidad la
|
||||
decide React montándolos, así que hay que deshacer ese display:none del tema
|
||||
(que se carga después y gana la cascada). */
|
||||
#store-div { margin-top: 14px; display: block !important; }
|
||||
|
||||
/* 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%; }
|
||||
/* Carrito: el tema ya trae .max-left-table2 (width 95% + border-spacing 0 8px, que
|
||||
es lo que separa las filas), .icon-td, .width-50-td y .justified del original.
|
||||
No se pisan: el `border-collapse` que había aquí anulaba ese border-spacing. */
|
||||
|
||||
/* Modal de resultado */
|
||||
.modal-div {
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -1944,6 +1944,8 @@
|
||||
"quantity": "Quantity",
|
||||
"add": "Add",
|
||||
"added": "Added",
|
||||
"cartTitle": "Cart",
|
||||
"selectedCharacter": "Selected character",
|
||||
"colItem": "Item",
|
||||
"colQty": "Qty",
|
||||
"colValue": "Value",
|
||||
|
||||
@@ -1944,6 +1944,8 @@
|
||||
"quantity": "Cantidad",
|
||||
"add": "Añadir",
|
||||
"added": "Añadido",
|
||||
"cartTitle": "Carrito",
|
||||
"selectedCharacter": "Personaje seleccionado",
|
||||
"colItem": "Objeto",
|
||||
"colQty": "Cant",
|
||||
"colValue": "Valor",
|
||||
|
||||
Reference in New Issue
Block a user