i18n: traducir toda la app (ES + EN) — páginas y componentes con texto hardcodeado

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>
This commit is contained in:
2026-07-14 18:58:54 +00:00
parent e457a43f05
commit 651d8deafc
57 changed files with 3516 additions and 1190 deletions
+3 -2
View File
@@ -1,5 +1,5 @@
import type { Metadata } from 'next'
import { setRequestLocale } from 'next-intl/server'
import { getTranslations, setRequestLocale } from 'next-intl/server'
import { notFound } from 'next/navigation'
import { getRealmName, realmSlug } from '@/lib/realm'
import { RealmInfo } from '@/components/RealmInfo'
@@ -38,10 +38,11 @@ export default async function RealmPage({
}) {
const { locale, realm } = await params
setRequestLocale(locale)
const t = await getTranslations('Misc')
const r = await resolve(realm)
if (!r) notFound()
const heading = r.kind === 'players' ? 'Personajes' : 'Reino'
const heading = r.kind === 'players' ? t('realm.charactersHeading') : t('realm.realmHeading')
return (
<div className="main-page">
+21 -23
View File
@@ -1,5 +1,5 @@
import type { Metadata } from 'next'
import { setRequestLocale } from 'next-intl/server'
import { setRequestLocale, getTranslations } from 'next-intl/server'
import { redirect, Link } from '@/i18n/navigation'
import { getSession } from '@/lib/session'
import { getSanctions, type Sanction } from '@/lib/ban-history'
@@ -21,7 +21,7 @@ function StatusCell({ s }: { s: Sanction }) {
}
/** Caja con la tabla de sanciones (baneos o muteos), o el vacío del diseño. */
function SanctionBox({
async function SanctionBox({
title,
emptyText,
rows,
@@ -32,6 +32,7 @@ function SanctionBox({
rows: Sanction[]
showScope: boolean
}) {
const t = await getTranslations('History')
return (
<div className="box-content">
<div className="title-box-content">
@@ -52,12 +53,12 @@ function SanctionBox({
<table className="max-center-table">
<tbody>
<tr>
{showScope && <th>Alcance</th>}
<th>Motivo</th>
<th>Sancionado por</th>
<th>Fecha</th>
<th>Expira</th>
<th>Estado</th>
{showScope && <th>{t('ban.thScope')}</th>}
<th>{t('ban.thReason')}</th>
<th>{t('ban.thBy')}</th>
<th>{t('ban.thDate')}</th>
<th>{t('ban.thExpires')}</th>
<th>{t('ban.thStatus')}</th>
</tr>
{rows.map((s, i) => (
<tr key={i}>
@@ -76,7 +77,7 @@ function SanctionBox({
<span>{fmt(s.date)}</span>
</td>
<td>
<span>{s.expires ? fmt(s.expires) : 'Permanente'}</span>
<span>{s.expires ? fmt(s.expires) : t('ban.permanent')}</span>
</td>
<td>
<StatusCell s={s} />
@@ -94,6 +95,7 @@ function SanctionBox({
export default async function BanHistoryPage({ params }: { params: Promise<{ locale: string }> }) {
const { locale } = await params
setRequestLocale(locale)
const t = await getTranslations('History')
const session = await getSession()
if (!session.bnetId) redirect({ href: '/login', locale })
@@ -102,38 +104,34 @@ export default async function BanHistoryPage({ params }: { params: Promise<{ loc
const { bans, mutes } = await getSanctions(session.accountId!, session.bnetId)
return (
<PageShell title="Historial de sanciones">
<PageShell title={t('ban.pageTitle')}>
<ServiceBox>
<br />
<p>Aquí podrás ver todas las sanciones que ha tenido la cuenta.</p>
<p>{t('ban.intro1')}</p>
<p>
Si tienes una sanción vigente, verás que su estado indica <span className="red-info2">Activo</span>.
{t.rich('ban.intro2', { s: (c) => <span className="red-info2">{c}</span> })}
</p>
<p>Para evitar que tu cuenta sea sancionada, recuerda respetar las normas de nuestro servidor.</p>
<p>{t('ban.intro3')}</p>
<br />
<div className="restore-item-table">
<p>
<i className="fas fa-exclamation-triangle"></i> <span>Importante:</span>
<i className="fas fa-exclamation-triangle"></i> <span>{t('ban.important')}</span>
</p>
<p>
Si tu cuenta aparece como sancionada o cerrada al intentar ingresar al reino, pero tu historial no tiene
ninguna sanción activa, significa que has tenido un bloqueo automático temporal por demasiados intentos de
inicio de sesión fallidos.
{t('ban.autoBlock1')}
</p>
<p>
Espera al menos 10 minutos para volver a intentar y asegúrate de usar nombre de cuenta y contraseña
correctas.
{t('ban.autoBlock2')}
</p>
</div>
<br />
<p>
Para realizar un reclamo de sanción, por favor ingresa en nuestro <Link href="/forum">foro</Link> y visita la
sección Reclamaciones.
{t.rich('ban.claim', { link: (c) => <Link href="/forum">{c}</Link> })}
</p>
</ServiceBox>
<SanctionBox title="Historial de baneos" emptyText="No hay baneos" rows={bans} showScope />
<SanctionBox title="Historial de muteos" emptyText="No hay muteos" rows={mutes} showScope={false} />
<SanctionBox title={t('ban.bansTitle')} emptyText={t('ban.noBans')} rows={bans} showScope />
<SanctionBox title={t('ban.mutesTitle')} emptyText={t('ban.noMutes')} rows={mutes} showScope={false} />
</PageShell>
)
}
@@ -1,5 +1,5 @@
import type { Metadata } from 'next'
import { setRequestLocale } from 'next-intl/server'
import { getTranslations, setRequestLocale } from 'next-intl/server'
import { redirect } from '@/i18n/navigation'
import { getSession } from '@/lib/session'
import { getGameCharacters } from '@/lib/characters'
@@ -34,34 +34,33 @@ export default async function ChangeFactionCharacterPage({ params }: { params: P
const [chars, price] = await Promise.all([getGameCharacters(session.accountId!), getChangeFactionPrice()])
const t = await getTranslations('CharServiceB.changeFaction')
return (
<PageShell title="Cambiar facción del personaje">
<ServiceBox>
<br />
<p>
La herramienta <span>Cambiar facción del personaje</span> te permite cambiar de la Alianza a la Horda y
viceversa.
</p>
<p>Si también deseas cambiar el nombre del personaje, debes usar la herramienta Renombrar personaje.</p>
<p>{t.rich('intro', { s: (c) => <span>{c}</span> })}</p>
<p>{t('alsoRename')}</p>
<br />
<p>Al hacer un cambio de Facción:</p>
<p>- Los items, hechizos, títulos, reputaciones, monturas y logros se cambiarán a la nueva facción</p>
<p>- Las misiones activas en el Registro de misiones serán abandonadas</p>
<p>- Los teams de arena serán borrados</p>
<p>- Los amigos en la Lista de amigos se borrarán</p>
<p>{t('onChange')}</p>
<p>{t('change1')}</p>
<p>{t('change2')}</p>
<p>{t('change3')}</p>
<p>{t('change4')}</p>
<br />
<p><span className="red-info2">Restricciones del personaje:</span></p>
<p>- No debe tener subastas activas</p>
<p>- No debe tener correos en el buzón</p>
<p>- No debe ser capitán de un team de arenas</p>
<p>- No debe ser miembro de una hermandad</p>
<p>- No debe tener demasiado oro</p>
<p><span className="red-info2">{t('restrictionsTitle')}</span></p>
<p>{t('restriction1')}</p>
<p>{t('restriction2')}</p>
<p>{t('restriction3')}</p>
<p>{t('restriction4')}</p>
<p>{t('restriction5')}</p>
<br />
<table className="max-gold-table">
<tbody>
<tr>
<th>Nivel</th>
<th>Max. Oro</th>
<th>{t('tableLevel')}</th>
<th>{t('tableMaxGold')}</th>
</tr>
<tr><td>10-30</td><GoldCell amount={300} /></tr>
<tr><td>31-50</td><GoldCell amount={1000} /></tr>
@@ -70,36 +69,30 @@ export default async function ChangeFactionCharacterPage({ params }: { params: P
</tbody>
</table>
<br />
<p>
Al usar el botón CAMBIAR FACCIÓN del personaje que hayas escogido, se enviará una petición de cambio de
facción de dicho personaje.
</p>
<p>Cuando ingreses a la lista de personajes de tu cuenta, verás un ícono a la izquierda de tu personaje escogido.</p>
<p>Al darle click a dicho ícono ingresarás al menú de edición de personaje que te permitirá pasar a la facción contraria.</p>
<p>{t('instructions')}</p>
<p>{t('iconInfo')}</p>
<p>{t('iconClick')}</p>
<br />
<fieldset>
<legend>NOTA</legend>
<legend>{t('noteLegend')}</legend>
<div className="separate2">
<p>
Por favor, asegúrate de haber escogido al personaje correcto ya que esta acción no es reversible una vez
realizada.
</p>
<p>{t('noteConfirm')}</p>
</div>
</fieldset>
<div className="centered">
<br />
<br />
<p>Requiere <span>{price}</span> <span className="yellow-info"></span> (SumUp)</p>
<p>{t.rich('requires', { price, s: (c) => <span>{c}</span>, y: (c) => <span className="yellow-info">{c}</span> })}</p>
</div>
<PaidServiceForm
characters={chars.map((c) => ({ name: c.name, classCss: c.classCss }))}
checkoutEndpoint="/api/character/change-faction/checkout"
payLabel="CAMBIAR FACCIÓN"
payLabel={t('payLabel')}
buttonClass="change-faction-button"
provider="sumup"
confirmText={`¿Estás seguro de cambiar de facción al personaje seleccionado por ${price} € (SumUp)?`}
confirmText={t('confirm', { price })}
/>
</ServiceBox>
</PageShell>
@@ -1,5 +1,5 @@
import type { Metadata } from 'next'
import { setRequestLocale } from 'next-intl/server'
import { getTranslations, setRequestLocale } from 'next-intl/server'
import { redirect } from '@/i18n/navigation'
import { getSession } from '@/lib/session'
import { getGameCharacters } from '@/lib/characters'
@@ -22,53 +22,43 @@ export default async function ChangeRaceCharacterPage({ params }: { params: Prom
const [chars, price] = await Promise.all([getGameCharacters(session.accountId!), getChangeRacePrice()])
const t = await getTranslations('CharServiceB.changeRace')
return (
<PageShell title="Cambiar raza del personaje">
<ServiceBox>
<br />
<p className="red-info2 h2">
<i className="fas fa-exclamation-triangle"></i> ESTE CAMBIO NO PERMITE CAMBIAR A LA FACCIÓN CONTRARIA{' '}
<i className="fas fa-exclamation-triangle"></i> {t('warning')}{' '}
<i className="fas fa-exclamation-triangle"></i>
</p>
<p>
La herramienta <span>Cambiar raza del personaje</span> te permite cambiar la raza del personaje entre otras de
la MISMA facción.
</p>
<p>Si también deseas cambiar el nombre del personaje, debes usar la herramienta Renombrar personaje.</p>
<p>{t.rich('intro', { s: (c) => <span>{c}</span> })}</p>
<p>{t('alsoRename')}</p>
<br />
<p>
Al usar el botón CAMBIAR RAZA del personaje que hayas escogido, se enviará una petición de cambio de raza de
dicho personaje.
</p>
<p>Cuando ingreses a la lista de personajes de tu cuenta, verás un ícono a la izquierda de tu personaje escogido.</p>
<p>
Al darle click a dicho ícono ingresarás al menú de edición de personaje que te permitirá escoger entre las
razas de la misma facción.
</p>
<p>{t('instructions')}</p>
<p>{t('iconInfo')}</p>
<p>{t('iconClick')}</p>
<br />
<fieldset>
<legend>NOTA</legend>
<legend>{t('noteLegend')}</legend>
<div className="separate2">
<p>
Por favor, asegúrate de haber escogido al personaje correcto ya que esta acción no es reversible una vez
realizada.
</p>
<p>{t('noteConfirm')}</p>
</div>
</fieldset>
<div className="centered">
<br />
<br />
<p>Requiere <span>{price}</span> <span className="yellow-info"></span> (SumUp)</p>
<p>{t.rich('requires', { price, s: (c) => <span>{c}</span>, y: (c) => <span className="yellow-info">{c}</span> })}</p>
</div>
<PaidServiceForm
characters={chars.map((c) => ({ name: c.name, classCss: c.classCss }))}
checkoutEndpoint="/api/character/change-race/checkout"
payLabel="CAMBIAR RAZA"
payLabel={t('payLabel')}
buttonClass="change-race-button"
provider="sumup"
confirmText={`¿Estás seguro de cambiar de raza al personaje seleccionado por ${price} € (SumUp)?`}
confirmText={t('confirm', { price })}
/>
</ServiceBox>
</PageShell>
+12 -10
View File
@@ -1,31 +1,33 @@
import type { Metadata } from 'next'
import { getTranslations } from 'next-intl/server'
export const metadata: Metadata = {
title: 'Contáctanos',
}
export default function ContactUsPage() {
export default async function ContactUsPage() {
const t = await getTranslations('Legal')
return (
<div className="main-page">
<div className="middle-content">
<div className="body-content">
<div className="title-content">
<h1>Contáctanos</h1>
<h1>{t('contact.title')}</h1>
</div>
<div className="box-content">
<div className="body-box-content">
<p>¡Gracias por tu interés en contactar con nosotros!</p>
<p>Si tienes alguna pregunta, consulta o comentario, estaremos encantados de ayudarte. Por favor, envía un correo electrónico a la siguiente dirección:</p>
<p>{t('contact.p1')}</p>
<p>{t('contact.p2')}</p>
<p className="second-brown">contacto@ultimowow.com</p>
<br />
<p>Error 404 es la empresa que representa el servicio de UltimoWoW.</p>
<p>Nos esforzamos por responder a todos los mensajes en un plazo de 48 horas hábiles.</p>
<p>Asegúrate de incluir la información relevante en tu correo para que podamos brindarte una respuesta precisa y oportuna.</p>
<p>{t('contact.p3')}</p>
<p>{t('contact.p4')}</p>
<p>{t('contact.p5')}</p>
<br />
<p>En Error 404, valoramos tus comentarios y opiniones. Tu satisfacción es nuestra prioridad y nos esforzamos por brindarte la mejor experiencia posible en UltimoWoW.</p>
<p>¡Esperamos tener noticias tuyas pronto!</p>
<p>{t('contact.p6')}</p>
<p>{t('contact.p7')}</p>
<br />
<p>Atentamente, el equipo de Error 404.</p>
<p>{t('contact.p8')}</p>
</div>
</div>
</div>
+84 -82
View File
@@ -1,4 +1,5 @@
import type { Metadata } from 'next'
import { getTranslations } from 'next-intl/server'
import { Link } from '@/i18n/navigation'
import { CookieConsentButtons, ConsentStatus } from '@/components/CookieConsent'
@@ -6,172 +7,173 @@ export const metadata: Metadata = {
title: 'Declaración de Cookies',
}
export default function CookiesPage() {
export default async function CookiesPage() {
const t = await getTranslations('Legal')
return (
<div className="main-page">
<div className="middle-content">
<div className="body-content">
<div className="title-content">
<h1>Declaración de Cookies</h1>
<h1>{t('cookies.title')}</h1>
</div>
<div className="box-content">
<div className="body-box-content justified">
<p>Esta página web usa cookies. Las cookies de este sitio web se usan para personalizar el contenido y los anuncios, ofrecer funciones de redes sociales y analizar el tráfico. Además, compartimos información sobre el uso que haga del sitio web con nuestros partners de redes sociales, publicidad y análisis web, quienes pueden combinarla con otra información que les haya proporcionado o que hayan recopilado a partir del uso que haya hecho de sus servicios.</p>
<p>{t('cookies.p1')}</p>
<br />
<p>Las cookies son pequeños archivos de texto que las páginas web pueden utilizar para hacer más eficiente la experiencia del usuario.</p>
<p>La ley afirma que podemos almacenar cookies en tu dispositivo si son estrictamente necesarias para el funcionamiento de esta página. Para todos los demás tipos de cookies necesitamos tu permiso.</p>
<p>{t('cookies.p2')}</p>
<p>{t('cookies.p3')}</p>
<br />
<p>Esta página utiliza tipos diferentes de cookies. Algunas cookies son colocadas por servicios de terceros que aparecen en nuestras páginas.</p>
<p>En cualquier momento puedes cambiar o retirar tu consentimiento desde esta Declaración de cookies en nuestro sitio web.</p>
<p>{t('cookies.p4')}</p>
<p>{t('cookies.p5')}</p>
<br />
<p>Obten más información sobre quiénes somos, cómo puedes contactarnos y cómo procesamos los datos personales en nuestra <Link href="/privacy-policy">Política de Privacidad</Link>.</p>
<p>{t.rich('cookies.p6', { link: (c) => <Link href="/privacy-policy">{c}</Link> })}</p>
<br />
<hr />
<br />
<span>Tipos de cookies que utilizamos</span>
<span>{t('cookies.h1cat')}</span>
<br />
<span className="second-brown">Cookies Necesarias</span>
<p>Las cookies necesarias ayudan a hacer una página web utilizable activando funciones básicas como la navegación en la página y el acceso a áreas seguras de la página web. La página web no puede funcionar adecuadamente sin estas cookies.</p>
<span className="second-brown">{t('cookies.necessaryTitle')}</span>
<p>{t('cookies.necessaryDesc')}</p>
<table className="uw-cookie-table">
<thead>
<tr>
<th>Cookie</th>
<th>Proveedor</th>
<th>Propósito</th>
<th>Duración</th>
<th>{t('cookies.thCookie')}</th>
<th>{t('cookies.thProvider')}</th>
<th>{t('cookies.thPurpose')}</th>
<th>{t('cookies.thDuration')}</th>
</tr>
</thead>
<tbody>
<tr><td>PHPSESSID</td><td>UltimoWoW</td><td>Mantiene el estado de la sesión del usuario entre páginas. Presente en ultimowow.com y wotlk.ultimowow.com.</td><td>Sesión</td></tr>
<tr><td>uw_cookie_consent</td><td>UltimoWoW</td><td>Almacena tus preferencias de consentimiento de cookies. Se comparte entre ultimowow.com y wotlk.ultimowow.com.</td><td>1 año</td></tr>
<tr><td>cf_clearance</td><td>Cloudflare</td><td>Identifica tráfico legítimo y protege contra bots.</td><td>1 año</td></tr>
<tr><td>__cf_bm</td><td>Cloudflare</td><td>Bot management y protección contra ataques DDoS.</td><td>30 minutos</td></tr>
<tr><td>_cfuvid</td><td>Cloudflare</td><td>Identificador de sesión para gestión de tráfico.</td><td>Sesión</td></tr>
<tr><td>CF_VERIFIED_DEVICE_*</td><td>Cloudflare</td><td>Verificación de dispositivo para seguridad.</td><td>30 días</td></tr>
<tr><td>_cf_logged_in</td><td>Cloudflare</td><td>Indica estado de autenticación en servicios Cloudflare.</td><td>Sesión</td></tr>
<tr><td>cf_chl_*</td><td>Cloudflare Turnstile</td><td>Cookies del sistema de verificación captcha Turnstile.</td><td>Variable</td></tr>
<tr><td>cf.turnstile.u</td><td>Cloudflare Turnstile</td><td>Distingue entre humanos y bots en el sistema de verificación.</td><td>Persistente</td></tr>
<tr><td>zaraz-consent</td><td>Cloudflare Zaraz</td><td>Gestión de consentimiento del sistema Zaraz.</td><td>1 año</td></tr>
<tr><td>cfz_*</td><td>Cloudflare Zaraz</td><td>Cookies técnicas del sistema de gestión de tags Zaraz.</td><td>Variable</td></tr>
<tr><td>PHPSESSID</td><td>UltimoWoW</td><td>{t('cookies.nPurpose1')}</td><td>{t('cookies.durSession')}</td></tr>
<tr><td>uw_cookie_consent</td><td>UltimoWoW</td><td>{t('cookies.nPurpose2')}</td><td>{t('cookies.dur1Year')}</td></tr>
<tr><td>cf_clearance</td><td>Cloudflare</td><td>{t('cookies.nPurpose3')}</td><td>{t('cookies.dur1Year')}</td></tr>
<tr><td>__cf_bm</td><td>Cloudflare</td><td>{t('cookies.nPurpose4')}</td><td>{t('cookies.dur30Min')}</td></tr>
<tr><td>_cfuvid</td><td>Cloudflare</td><td>{t('cookies.nPurpose5')}</td><td>{t('cookies.durSession')}</td></tr>
<tr><td>CF_VERIFIED_DEVICE_*</td><td>Cloudflare</td><td>{t('cookies.nPurpose6')}</td><td>{t('cookies.dur30Days')}</td></tr>
<tr><td>_cf_logged_in</td><td>Cloudflare</td><td>{t('cookies.nPurpose7')}</td><td>{t('cookies.durSession')}</td></tr>
<tr><td>cf_chl_*</td><td>Cloudflare Turnstile</td><td>{t('cookies.nPurpose8')}</td><td>{t('cookies.durVariable')}</td></tr>
<tr><td>cf.turnstile.u</td><td>Cloudflare Turnstile</td><td>{t('cookies.nPurpose9')}</td><td>{t('cookies.durPersistent')}</td></tr>
<tr><td>zaraz-consent</td><td>Cloudflare Zaraz</td><td>{t('cookies.nPurpose10')}</td><td>{t('cookies.dur1Year')}</td></tr>
<tr><td>cfz_*</td><td>Cloudflare Zaraz</td><td>{t('cookies.nPurpose11')}</td><td>{t('cookies.durVariable')}</td></tr>
</tbody>
</table>
<p className="third-brown">Nota sobre Cloudflare: Utilizamos Cloudflare como proveedor de seguridad y rendimiento. Cloudflare puede establecer cookies adicionales de forma dinámica para proteger el sitio contra amenazas, gestionar el tráfico y optimizar el rendimiento. Estas cookies son estrictamente necesarias para el funcionamiento seguro del sitio. Para más información, consulta la <a href="https://www.cloudflare.com/cookie-policy/" target="_blank" rel="noopener">Política de Cookies de Cloudflare</a>.</p>
<p className="third-brown">{t.rich('cookies.cfNote', { link: (c) => <a href="https://www.cloudflare.com/cookie-policy/" target="_blank" rel="noopener">{c}</a> })}</p>
<br />
<span className="second-brown">Cookies de Preferencias</span>
<p>Las cookies de preferencias permiten a la página web recordar información que cambia la forma en que la página se comporta o el aspecto que tiene, como su idioma preferido o la región en la que usted se encuentra.</p>
<span className="second-brown">{t('cookies.prefTitle')}</span>
<p>{t('cookies.prefDesc')}</p>
<table className="uw-cookie-table">
<thead>
<tr>
<th>Cookie</th>
<th>Proveedor</th>
<th>Propósito</th>
<th>Duración</th>
<th>{t('cookies.thCookie')}</th>
<th>{t('cookies.thProvider')}</th>
<th>{t('cookies.thPurpose')}</th>
<th>{t('cookies.thDuration')}</th>
</tr>
</thead>
<tbody>
<tr><td>comments_sort</td><td>wotlk.ultimowow.com</td><td>Guarda tu preferencia de ordenación de comentarios.</td><td>1 año</td></tr>
<tr><td>wwcomments_sort</td><td>wotlk.ultimowow.com</td><td>Guarda tu preferencia de ordenación de comentarios.</td><td>1 año</td></tr>
<tr><td>temp_default_3dmodel</td><td>wotlk.ultimowow.com</td><td>Guarda la configuración del visor 3D de modelos.</td><td>1 año</td></tr>
<tr><td>comments_sort</td><td>wotlk.ultimowow.com</td><td>{t('cookies.prefPurpose1')}</td><td>{t('cookies.dur1Year')}</td></tr>
<tr><td>wwcomments_sort</td><td>wotlk.ultimowow.com</td><td>{t('cookies.prefPurpose2')}</td><td>{t('cookies.dur1Year')}</td></tr>
<tr><td>temp_default_3dmodel</td><td>wotlk.ultimowow.com</td><td>{t('cookies.prefPurpose3')}</td><td>{t('cookies.dur1Year')}</td></tr>
</tbody>
</table>
<span className="second-brown">Cookies Analíticas</span>
<p>Las cookies analíticas nos permiten contar las visitas y fuentes de tráfico para poder medir y mejorar el rendimiento de nuestro sitio. Nos ayudan a saber qué páginas son las más y menos populares, y a ver cómo se mueven los visitantes por el sitio. Toda la información que recogen estas cookies es agregada y, por lo tanto, anónima.</p>
<span className="second-brown">{t('cookies.analyticsTitle')}</span>
<p>{t('cookies.analyticsDesc')}</p>
<table className="uw-cookie-table">
<thead>
<tr>
<th>Cookie</th>
<th>Proveedor</th>
<th>Propósito</th>
<th>Duración</th>
<th>{t('cookies.thCookie')}</th>
<th>{t('cookies.thProvider')}</th>
<th>{t('cookies.thPurpose')}</th>
<th>{t('cookies.thDuration')}</th>
</tr>
</thead>
<tbody>
<tr><td>_ga</td><td>Google Analytics</td><td>Distingue usuarios únicos asignando un número aleatorio.</td><td>2 años</td></tr>
<tr><td>_ga_*</td><td>Google Analytics</td><td>Mantiene el estado de la sesión.</td><td>2 años</td></tr>
<tr><td>_gid</td><td>Google Analytics</td><td>Distingue usuarios para análisis estadístico.</td><td>24 horas</td></tr>
<tr><td>_gat</td><td>Google Analytics</td><td>Limita la velocidad de solicitudes.</td><td>1 minuto</td></tr>
<tr><td>sparrow_id</td><td>Cloudflare</td><td>Identificador para análisis de rendimiento web.</td><td>1 año</td></tr>
<tr><td>_ga</td><td>Google Analytics</td><td>{t('cookies.aPurpose1')}</td><td>{t('cookies.dur2Years')}</td></tr>
<tr><td>_ga_*</td><td>Google Analytics</td><td>{t('cookies.aPurpose2')}</td><td>{t('cookies.dur2Years')}</td></tr>
<tr><td>_gid</td><td>Google Analytics</td><td>{t('cookies.aPurpose3')}</td><td>{t('cookies.dur24Hours')}</td></tr>
<tr><td>_gat</td><td>Google Analytics</td><td>{t('cookies.aPurpose4')}</td><td>{t('cookies.dur1Minute')}</td></tr>
<tr><td>sparrow_id</td><td>Cloudflare</td><td>{t('cookies.aPurpose5')}</td><td>{t('cookies.dur1Year')}</td></tr>
</tbody>
</table>
<span className="second-brown">Cookies de Marketing</span>
<p>Las cookies de marketing se utilizan para rastrear a los visitantes en las páginas web. La intención es mostrar anuncios relevantes y atractivos para el usuario individual, y por lo tanto, más valiosos para los editores y terceros anunciantes.</p>
<span className="second-brown">{t('cookies.marketingTitle')}</span>
<p>{t('cookies.marketingDesc')}</p>
<table className="uw-cookie-table">
<thead>
<tr>
<th>Cookie</th>
<th>Proveedor</th>
<th>Propósito</th>
<th>Duración</th>
<th>{t('cookies.thCookie')}</th>
<th>{t('cookies.thProvider')}</th>
<th>{t('cookies.thPurpose')}</th>
<th>{t('cookies.thDuration')}</th>
</tr>
</thead>
<tbody>
<tr><td>_gcl_*</td><td>Google</td><td>Conversiones de Google Ads.</td><td>90 días</td></tr>
<tr><td>IDE</td><td>Google DoubleClick</td><td>Publicidad personalizada.</td><td>1 año</td></tr>
<tr><td>_fbp</td><td>Facebook (Meta)</td><td>Identifica navegadores para proporcionar servicios de publicidad y análisis.</td><td>3 meses</td></tr>
<tr><td>_fbc</td><td>Facebook (Meta)</td><td>Almacena el último clic en anuncios de Facebook.</td><td>3 meses</td></tr>
<tr><td>fr</td><td>Facebook (Meta)</td><td>Habilita la entrega y medición de anuncios.</td><td>3 meses</td></tr>
<tr><td>AMCV_*</td><td>Adobe Experience Cloud</td><td>Identificación de visitantes para servicios de Adobe.</td><td>2 años</td></tr>
<tr><td>_q_state_*</td><td>Varios</td><td>Gestión de estado para servicios de publicidad.</td><td>Variable</td></tr>
<tr><td>_gcl_*</td><td>Google</td><td>{t('cookies.mPurpose1')}</td><td>{t('cookies.dur90Days')}</td></tr>
<tr><td>IDE</td><td>Google DoubleClick</td><td>{t('cookies.mPurpose2')}</td><td>{t('cookies.dur1Year')}</td></tr>
<tr><td>_fbp</td><td>Facebook (Meta)</td><td>{t('cookies.mPurpose3')}</td><td>{t('cookies.dur3Months')}</td></tr>
<tr><td>_fbc</td><td>Facebook (Meta)</td><td>{t('cookies.mPurpose4')}</td><td>{t('cookies.dur3Months')}</td></tr>
<tr><td>fr</td><td>Facebook (Meta)</td><td>{t('cookies.mPurpose5')}</td><td>{t('cookies.dur3Months')}</td></tr>
<tr><td>AMCV_*</td><td>Adobe Experience Cloud</td><td>{t('cookies.mPurpose6')}</td><td>{t('cookies.dur2Years')}</td></tr>
<tr><td>_q_state_*</td><td>{t('cookies.providerVarious')}</td><td>{t('cookies.mPurpose7')}</td><td>{t('cookies.durVariable')}</td></tr>
</tbody>
</table>
<p className="third-brown">Nota sobre servicios de terceros: Utilizamos Cloudflare Zaraz como sistema de gestión de tags. Los servicios de marketing (Google Ads, Facebook, Adobe, etc.) pueden establecer cookies adicionales cuando aceptas cookies de marketing. Para más información sobre cada servicio, consulta sus respectivas políticas de privacidad.</p>
<p className="third-brown">{t('cookies.thirdPartyNote')}</p>
<br />
<span className="second-brown">Cookies de Contenido Embebido</span>
<p>Algunas páginas de nuestro sitio incluyen contenido embebido de terceros (vídeos de YouTube, widgets de Discord, publicaciones de redes sociales, etc.). Estos servicios pueden establecer sus propias cookies cuando visualizas dicho contenido.</p>
<span className="second-brown">{t('cookies.embeddedTitle')}</span>
<p>{t('cookies.embeddedDesc')}</p>
<table className="uw-cookie-table">
<thead>
<tr>
<th>Proveedor</th>
<th>Tipo de contenido</th>
<th>Más información</th>
<th>{t('cookies.thProvider')}</th>
<th>{t('cookies.thContentType')}</th>
<th>{t('cookies.thMoreInfo')}</th>
</tr>
</thead>
<tbody>
<tr><td>YouTube (Google)</td><td>Vídeos embebidos</td><td><a href="https://policies.google.com/privacy" target="_blank" rel="noopener">Política de Privacidad</a></td></tr>
<tr><td>Discord</td><td>Widgets de servidor</td><td><a href="https://discord.com/privacy" target="_blank" rel="noopener">Política de Privacidad</a></td></tr>
<tr><td>Twitch</td><td>Streams embebidos</td><td><a href="https://www.twitch.tv/p/legal/privacy-notice/" target="_blank" rel="noopener">Política de Privacidad</a></td></tr>
<tr><td>Facebook (Meta)</td><td>Vídeos y publicaciones</td><td><a href="https://www.facebook.com/privacy/policy/" target="_blank" rel="noopener">Política de Privacidad</a></td></tr>
<tr><td>YouTube (Google)</td><td>{t('cookies.embType1')}</td><td><a href="https://policies.google.com/privacy" target="_blank" rel="noopener">{t('cookies.privacyPolicyLink')}</a></td></tr>
<tr><td>Discord</td><td>{t('cookies.embType2')}</td><td><a href="https://discord.com/privacy" target="_blank" rel="noopener">{t('cookies.privacyPolicyLink')}</a></td></tr>
<tr><td>Twitch</td><td>{t('cookies.embType3')}</td><td><a href="https://www.twitch.tv/p/legal/privacy-notice/" target="_blank" rel="noopener">{t('cookies.privacyPolicyLink')}</a></td></tr>
<tr><td>Facebook (Meta)</td><td>{t('cookies.embType4')}</td><td><a href="https://www.facebook.com/privacy/policy/" target="_blank" rel="noopener">{t('cookies.privacyPolicyLink')}</a></td></tr>
</tbody>
</table>
<p className="third-brown">Utilizamos youtube-nocookie.com cuando es posible para minimizar el seguimiento. Sin embargo, los servicios de terceros pueden establecer cookies como: YSC, VISITOR_INFO1_LIVE, yt-remote-*, __Secure-*, y otras relacionadas con la reproducción de contenido.</p>
<p className="third-brown">{t('cookies.embeddedNote')}</p>
<br />
<hr />
<br />
<span>Cómo eliminar cookies de tu navegador</span>
<p>Además de utilizar nuestro panel de configuración, puedes eliminar las cookies directamente desde tu navegador:</p>
<span>{t('cookies.h2cat')}</span>
<p>{t('cookies.deleteP')}</p>
<ul>
<li>Chrome: Configuración Privacidad y seguridad Cookies y otros datos de sitios</li>
<li>Firefox: Opciones Privacidad &amp; Seguridad Cookies y datos del sitio</li>
<li>Safari: Preferencias Privacidad Gestionar datos de sitios web</li>
<li>Edge: Configuración Cookies y permisos del sitio Cookies</li>
<li>{t('cookies.deleteLi1')}</li>
<li>{t('cookies.deleteLi2')}</li>
<li>{t('cookies.deleteLi3')}</li>
<li>{t('cookies.deleteLi4')}</li>
</ul>
<br />
<hr />
<br />
<span>Tu estado de consentimiento</span>
<p>Puedes cambiar o retirar tu consentimiento en cualquier momento. Al contactarnos respecto a su consentimiento, por favor, indique la fecha de su consentimiento.</p>
<span>{t('cookies.h3cat')}</span>
<p>{t('cookies.consentP')}</p>
<ConsentStatus />
<CookieConsentButtons />
<br />
<hr />
<br />
<span>Más información</span>
<p>Si tienes alguna pregunta sobre nuestra política de cookies, puedes contactarnos a través de nuestra página de <Link href="/contact-us">Contacto</Link>.</p>
<span>{t('cookies.h4cat')}</span>
<p>{t.rich('cookies.moreInfoP', { link: (c) => <Link href="/contact-us">{c}</Link> })}</p>
<br />
<p className="grey-info2">Última actualización: Marzo 2026</p>
<p className="grey-info2">{t('cookies.updated')}</p>
</div>
</div>
</div>
@@ -1,5 +1,5 @@
import type { Metadata } from 'next'
import { setRequestLocale } from 'next-intl/server'
import { getTranslations, setRequestLocale } from 'next-intl/server'
import { redirect } from '@/i18n/navigation'
import { getSession } from '@/lib/session'
import { getGameCharacters } from '@/lib/characters'
@@ -22,53 +22,44 @@ export default async function CustomizeCharacterPage({ params }: { params: Promi
const [chars, price] = await Promise.all([getGameCharacters(session.accountId!), getCustomizePrice()])
const t = await getTranslations('CharServiceB.customize')
return (
<PageShell title="Personalizar personaje">
<ServiceBox>
<br />
<p>
La herramienta <span>Personalizar personaje</span> te permite cambiar los siguientes aspectos un personaje:
</p>
<p>- Color de la piel</p>
<p>- Rostro</p>
<p>- Pelo</p>
<p>- Sexo</p>
<p>- Características correspondientes a cada raza como cuernos, pendientes, marcas, vello facial</p>
<p>Si también deseas cambiar el nombre del personaje, debes usar la herramienta Renombrar personaje.</p>
<p>{t.rich('intro', { s: (c) => <span>{c}</span> })}</p>
<p>{t('aspect1')}</p>
<p>{t('aspect2')}</p>
<p>{t('aspect3')}</p>
<p>{t('aspect4')}</p>
<p>{t('aspect5')}</p>
<p>{t('alsoRename')}</p>
<br />
<p>
Al usar el botón PERSONALIZAR del personaje que hayas escogido, se enviará una petición de personalización de
dicho personaje.
</p>
<p>Cuando ingreses a la lista de personajes de tu cuenta, verás un ícono a la izquierda de tu personaje escogido.</p>
<p>
Al darle click a dicho ícono ingresarás al menú de edición de personaje que te permitirá escoger diferentes
características.
</p>
<p>{t('instructions')}</p>
<p>{t('iconInfo')}</p>
<p>{t('iconClick')}</p>
<br />
<fieldset>
<legend>NOTA</legend>
<legend>{t('noteLegend')}</legend>
<div className="separate2">
<p>
Por favor, asegúrate de haber escogido al personaje correcto ya que esta acción no es reversible una vez
realizada.
</p>
<p>{t('noteConfirm')}</p>
</div>
</fieldset>
<div className="centered">
<br />
<br />
<p>Requiere <span>{price}</span> <span className="yellow-info"></span> (SumUp)</p>
<p>{t.rich('requires', { price, s: (c) => <span>{c}</span>, y: (c) => <span className="yellow-info">{c}</span> })}</p>
</div>
<PaidServiceForm
characters={chars.map((c) => ({ name: c.name, classCss: c.classCss }))}
checkoutEndpoint="/api/character/customize/checkout"
payLabel="PERSONALIZAR"
payLabel={t('payLabel')}
buttonClass="customize-button"
provider="sumup"
confirmText={`¿Estás seguro de personalizar al personaje seleccionado por ${price} € (SumUp)?`}
confirmText={t('confirm', { price })}
/>
</ServiceBox>
</PageShell>
@@ -1,4 +1,4 @@
import { setRequestLocale } from 'next-intl/server'
import { getTranslations, setRequestLocale } from 'next-intl/server'
import { Link } from '@/i18n/navigation'
import { fulfillCheckoutSession } from '@/lib/fulfill'
import { isSessionPaid } from '@/lib/stripe'
@@ -16,6 +16,7 @@ export default async function DPointsSuccessPage({
}) {
const { locale } = await params
setRequestLocale(locale)
const t = await getTranslations('Points')
const { provider, session_id, ref } = await searchParams
// Entrega compartida con el webhook/return (idempotente vía reclamo atómico).
@@ -32,20 +33,20 @@ export default async function DPointsSuccessPage({
}
return (
<PageShell title="Adquirir PD">
<PageShell title={t('dpointsSuccess.title')}>
<div className="box-content">
<div className="body-box-content centered">
{status === 'ok' ? (
<span className="ok-form-response">¡Pago completado! Tus PD ya fueron acreditados a tu cuenta.</span>
<span className="ok-form-response">{t('dpointsSuccess.paidOk')}</span>
) : status === 'already' ? (
<span className="ok-form-response">Este pago ya había sido procesado. Tus PD están acreditados.</span>
<span className="ok-form-response">{t('dpointsSuccess.alreadyPaid')}</span>
) : (
<span className="red-form-response">No pudimos confirmar tu pago. Si se realizó el cargo, contacta con soporte.</span>
<span className="red-form-response">{t('dpointsSuccess.error')}</span>
)}
<br />
<br />
<p>
<Link href="/my-account"> Regresar a mi cuenta</Link>
<Link href="/my-account">{t('dpointsSuccess.backToAccount')}</Link>
</p>
</div>
</div>
+3 -2
View File
@@ -1,5 +1,5 @@
import type { Metadata } from 'next'
import { setRequestLocale } from 'next-intl/server'
import { getTranslations, setRequestLocale } from 'next-intl/server'
import { redirect } from '@/i18n/navigation'
import { getSession } from '@/lib/session'
import { getRealmName } from '@/lib/realm'
@@ -14,6 +14,7 @@ export const metadata: Metadata = { title: 'Adquirir PD' }
export default async function DPointsPage({ params }: { params: Promise<{ locale: string }> }) {
const { locale } = await params
setRequestLocale(locale)
const t = await getTranslations('Points')
const session = await getSession()
if (!session.bnetId) redirect({ href: '/login', locale })
@@ -27,7 +28,7 @@ export default async function DPointsPage({ params }: { params: Promise<{ locale
const currency = (process.env.DP_CURRENCY || 'eur').toLowerCase()
return (
<PageShell title="Adquirir PD">
<PageShell title={t('dpoints.title')}>
<DPointsTabs realm={realm} currency={currency} />
</PageShell>
)
+23 -21
View File
@@ -1,40 +1,42 @@
import type { Metadata } from 'next'
import { getTranslations } from 'next-intl/server'
export const metadata: Metadata = { title: 'Descarga de addons' }
// Lista de addons (edítala con los tuyos). name/desc/url.
const ADDONS: { name: string; desc: string; url: string }[] = [
{ name: 'Deadly Boss Mods (DBM)', desc: 'Avisos y temporizadores de habilidades de jefes en mazmorras y bandas.', url: 'https://www.curseforge.com/wow/addons/deadly-boss-mods' },
{ name: 'Details! Damage Meter', desc: 'Medidor de daño, curación y amenaza con gráficas detalladas.', url: 'https://www.curseforge.com/wow/addons/details' },
{ name: 'AtlasLoot', desc: 'Consulta el botín de jefes de mazmorras y bandas dentro del juego.', url: 'https://www.curseforge.com/wow/addons/atlasloot-enhanced' },
{ name: 'Questie', desc: 'Muestra objetivos y ubicaciones de misiones en el mapa y el minimapa.', url: 'https://www.curseforge.com/wow/addons/questie' },
{ name: 'Bagnon', desc: 'Unifica todas tus bolsas y el banco en una sola ventana con búsqueda.', url: 'https://www.curseforge.com/wow/addons/bagnon' },
{ name: 'Bartender4', desc: 'Personaliza por completo tus barras de acción.', url: 'https://www.curseforge.com/wow/addons/bartender4' },
{ name: 'Auctioneer', desc: 'Herramientas avanzadas para la casa de subastas.', url: 'https://www.curseforge.com/wow/addons/auctioneer' },
{ name: 'Pawn', desc: 'Compara equipo y te indica qué objeto es una mejora para tu personaje.', url: 'https://www.curseforge.com/wow/addons/pawn' },
// Lista de addons (edítala con los tuyos). name/descKey/url.
const ADDONS: { name: string; descKey: string; url: string }[] = [
{ name: 'Deadly Boss Mods (DBM)', descKey: 'descDbm', url: 'https://www.curseforge.com/wow/addons/deadly-boss-mods' },
{ name: 'Details! Damage Meter', descKey: 'descDetails', url: 'https://www.curseforge.com/wow/addons/details' },
{ name: 'AtlasLoot', descKey: 'descAtlasLoot', url: 'https://www.curseforge.com/wow/addons/atlasloot-enhanced' },
{ name: 'Questie', descKey: 'descQuestie', url: 'https://www.curseforge.com/wow/addons/questie' },
{ name: 'Bagnon', descKey: 'descBagnon', url: 'https://www.curseforge.com/wow/addons/bagnon' },
{ name: 'Bartender4', descKey: 'descBartender4', url: 'https://www.curseforge.com/wow/addons/bartender4' },
{ name: 'Auctioneer', descKey: 'descAuctioneer', url: 'https://www.curseforge.com/wow/addons/auctioneer' },
{ name: 'Pawn', descKey: 'descPawn', url: 'https://www.curseforge.com/wow/addons/pawn' },
]
export default function DownloadAddonsPage() {
export default async function DownloadAddonsPage() {
const t = await getTranslations('Misc')
return (
<div className="main-page">
<div className="middle-content">
<div className="body-content">
<div className="title-content">
<h1>Descarga de addons</h1>
<h1>{t('downloadAddons.title')}</h1>
</div>
<div className="box-content">
<div className="title-box-content">
<h2>Addons recomendados</h2>
<h2>{t('downloadAddons.recommended')}</h2>
</div>
<div className="body-box-content justified">
<p>Los addons son complementos opcionales que mejoran y personalizan tu experiencia de juego.</p>
<p>Descarga los que quieras desde su página oficial (elige la versión compatible con WotLK 3.4.3).</p>
<p>{t('downloadAddons.intro1')}</p>
<p>{t('downloadAddons.intro2')}</p>
<br />
<p><span>Cómo instalarlos</span></p>
<p>1) Descarga el addon y descomprime el archivo .zip.</p>
<p>2) Copia la carpeta resultante dentro de <span>Interface/AddOns</span> (en la carpeta de tu cliente).</p>
<p>3) Inicia el juego y, en la pantalla de selección de personaje, pulsa <span>Complementos</span> para activarlos.</p>
<p><span>{t('downloadAddons.howToInstall')}</span></p>
<p>{t('downloadAddons.step1')}</p>
<p>{t.rich('downloadAddons.step2', { s: (c) => <span>{c}</span> })}</p>
<p>{t.rich('downloadAddons.step3', { s: (c) => <span>{c}</span> })}</p>
<br />
<hr />
<br />
@@ -44,11 +46,11 @@ export default function DownloadAddonsPage() {
<tr key={a.name} className="team-center-table-tr">
<td className="lefted separate">
<span className="yellow-info">{a.name}</span>
<p className="third-brown small-font">{a.desc}</p>
<p className="third-brown small-font">{t(`downloadAddons.${a.descKey}`)}</p>
</td>
<td className="real-info-box no-wrap-td">
<a href={a.url} target="_blank" rel="noopener noreferrer" className="nw-tool-btn">
Descargar
{t('downloadAddons.download')}
</a>
</td>
</tr>
+9 -15
View File
@@ -1,5 +1,5 @@
import type { Metadata } from 'next'
import { setRequestLocale } from 'next-intl/server'
import { getTranslations, setRequestLocale } from 'next-intl/server'
import { redirect } from '@/i18n/navigation'
import { getSession } from '@/lib/session'
import { getGameCharacters } from '@/lib/characters'
@@ -24,34 +24,28 @@ export default async function GoldCharacterPage({ params }: { params: Promise<{
const [chars, options] = await Promise.all([getGameCharacters(session.accountId!), getGoldOptions()])
const t = await getTranslations('CharServiceB.gold')
return (
<PageShell title="Adquirir oro">
<ServiceBox>
<br />
<p>
La herramienta <span>Adquirir oro</span> te permite enviar oro a un personaje.
</p>
<p>{t.rich('intro', { s: (c) => <span>{c}</span> })}</p>
<br />
<p>
Al usar el botón ENVIAR ORO del personaje que hayas escogido, se enviará un correo con la cantidad de oro
elegida al personaje.
</p>
<p>Cuando entres al reino con dicho personaje, tendrás el mensaje con la cantidad de oro seleccionada en tu buzón.</p>
<p>{t('instructions')}</p>
<p>{t('mailInfo')}</p>
<br />
<fieldset>
<legend>NOTA</legend>
<legend>{t('noteLegend')}</legend>
<div className="separate2">
<p>
Por favor, asegúrate de haber escogido al personaje correcto ya que esta acción no es reversible una vez
realizada.
</p>
<p>{t('noteConfirm')}</p>
</div>
</fieldset>
<div className="centered">
<br />
<br />
<p>Requiere</p>
<p>{t('requiresLabel')}</p>
<table className="gold-table">
<tbody>
{options.map((o) => (
+56 -57
View File
@@ -1,5 +1,5 @@
import type { Metadata } from 'next'
import { setRequestLocale } from 'next-intl/server'
import { getTranslations, setRequestLocale } from 'next-intl/server'
import { getRealmName } from '@/lib/realm'
export const metadata: Metadata = { title: 'Ayuda' }
@@ -9,6 +9,7 @@ export const dynamic = 'force-dynamic'
export default async function HelpPage({ params }: { params: Promise<{ locale: string }> }) {
const { locale } = await params
setRequestLocale(locale)
const t = await getTranslations('Misc')
const realm = await getRealmName()
return (
@@ -16,80 +17,78 @@ export default async function HelpPage({ params }: { params: Promise<{ locale: s
<div className="middle-content">
<div className="body-content">
<div className="title-content">
<h1>Ayuda</h1>
<h1>{t('help.title')}</h1>
</div>
<div className="box-content">
<div className="title-box-content">
<h2>Guía de asistencia</h2>
<h2>{t('help.subtitle')}</h2>
</div>
<div className="body-box-content justified">
<p>En esta guía podrás saber qué pasos seguir para obtener asistencia adecuada dependiendo del tipo de problema que hayas encontrado en nuestro servidor.</p>
<p>Ofrecemos un conjunto de problemas comunes, qué medios utilizar para cada caso y una explicación de cada medio de contacto.</p>
<p>Los diferentes medios para cada problema se listan desde el más ideal hasta la última opción.</p>
<p>{t('help.intro1')}</p>
<p>{t('help.intro2')}</p>
<p>{t('help.intro3')}</p>
<br />
<span>Con el cliente</span>
<p>Problemas que impiden arrancar el cliente, conectar al servidor o errores que provocan el cierre abrupto del cliente.</p>
<p>Los medios son:</p>
<p>- Foros</p>
<p>- Discord</p>
<span>{t('help.clientTitle')}</span>
<p>{t('help.clientText')}</p>
<p>{t('help.mediaAre')}</p>
<p>{t('help.mediaForos')}</p>
<p>{t('help.mediaDiscord')}</p>
<br />
<span>Con errores del servidor</span>
<p>Problemas relacionados a errores que deben ser reparados por nuestro equipo de reparación para mejorar el servidor.</p>
<p>Los medios son:</p>
<p>- Foros</p>
<span>{t('help.serverErrorsTitle')}</span>
<p>{t('help.serverErrorsText')}</p>
<p>{t('help.mediaAre')}</p>
<p>{t('help.mediaForos')}</p>
<br />
<span>Con el personaje</span>
<p>Problemas relacionados a errores que puedan afectar al personaje, habilidades, hechizos, misiones, etc.</p>
<p>Los medios son:</p>
<p>- Ticket</p>
<p>- Discord</p>
<span>{t('help.characterTitle')}</span>
<p>{t('help.characterText')}</p>
<p>{t('help.mediaAre')}</p>
<p>{t('help.mediaTicket')}</p>
<p>{t('help.mediaDiscord')}</p>
<br />
<span>Con otro jugador</span>
<p>Problemas relacionados a otros usuarios cometiendo faltas a las normas del servidor.</p>
<p>Este tipo de problemas requieren de una denuncia con las pruebas correspondientes de lo que haya sucedido.</p>
<p>Los medios son:</p>
<p>- Foros</p>
<span>{t('help.otherPlayerTitle')}</span>
<p>{t('help.otherPlayerText1')}</p>
<p>{t('help.otherPlayerText2')}</p>
<p>{t('help.mediaAre')}</p>
<p>{t('help.mediaForos')}</p>
<br />
<span>Con bloqueos de cuenta</span>
<p>Problemas relacionados a bloqueos de cuenta realizados por el equipo de {realm}.</p>
<p>Los medios son:</p>
<p>- Foros</p>
<span>{t('help.accountBlocksTitle')}</span>
<p>{t('help.accountBlocksText', { realm })}</p>
<p>{t('help.mediaAre')}</p>
<p>{t('help.mediaForos')}</p>
<br />
<span>Con la página web</span>
<p>Problemas relacionados a errores, fallas o sugerencias respecto de este sitio web.</p>
<p>Los medios son:</p>
<p>- Correo a <a href="mailto:ryuzaki@ultimowow.com">ryuzaki@ultimowow.com</a></p>
<span>{t('help.websiteTitle')}</span>
<p>{t('help.websiteText')}</p>
<p>{t('help.mailTo')} <a href="mailto:ryuzaki@ultimowow.com">ryuzaki@ultimowow.com</a></p>
<br />
<span>Con PD</span>
<p>Problemas relacionados a PD o PV.</p>
<p>Los medios son:</p>
<p>- Correo a <a href="mailto:contacto@ultimowow.com">contacto@ultimowow.com</a></p>
<span>{t('help.pdTitle')}</span>
<p>{t('help.pdText')}</p>
<p>{t('help.mailTo')} <a href="mailto:contacto@ultimowow.com">contacto@ultimowow.com</a></p>
<br />
<fieldset>
<legend>Medios de contacto</legend>
<legend>{t('help.contactMeans')}</legend>
<div className="separate2">
<span>Ticket</span>
<p>Para colocar un ticket, debes hacer click al signo de interrogación rojo en la parte inferior derecha de la interfaz del cliente (a la izquierda del llavero del personaje).</p>
<p>Al hacerle click, aparecerá una ventana en el medio de la pantalla.</p>
<p>En dicha ventana, hacer click al primer botón en la esquina inferior izquierda llamado &apos;Hablar con un MJ&apos;</p>
<p>Aparecerá una ventana con un texto informativo y debajo en medio, un botón llamado &apos;Abrir una consulta&apos;.</p>
<p>Al darle click al botón, aparecerá un campo de texto en el que puedes describir tu problema</p>
<p>Cuando hayas finalizado haz click en el botón en el primer botón en la esquina inferior derecha llamado &apos;Enviar&apos;.</p>
<p>El ticket ya ha sido creado y está pendiente de respuesta de un miembro del equipo.</p>
<span>{t('help.ticketTitle')}</span>
<p>{t('help.ticket1')}</p>
<p>{t('help.ticket2')}</p>
<p>{t('help.ticket3')}</p>
<p>{t('help.ticket4')}</p>
<p>{t('help.ticket5')}</p>
<p>{t('help.ticket6')}</p>
<p>{t('help.ticket7')}</p>
<br />
<span>Discord</span>
<p>Discord es una plataforma de chat y comunicación por voz que puede descargarse <a href="https://discordapp.com/" target="_blank" rel="noopener noreferrer">aquí</a>.</p>
<p>Para entrar en el servidor de Discord de nuestro servidor, haz click <a href="https://discord.ultimowow.com" target="_blank" rel="noopener noreferrer">aquí</a>.</p>
<p>Una vez que seas miembro de nuestro Discord podrás pedir ayuda en el canal &apos;Asistencia&apos; o enviar un mensaje directo a cualquier miembro del equipo solicitando ayuda.</p>
<span>{t('help.discordTitle')}</span>
<p>{t.rich('help.discord1', { link: (c) => <a href="https://discordapp.com/" target="_blank" rel="noopener noreferrer">{c}</a> })}</p>
<p>{t.rich('help.discord2', { link: (c) => <a href="https://discord.ultimowow.com" target="_blank" rel="noopener noreferrer">{c}</a> })}</p>
<p>{t('help.discord3')}</p>
<br />
<span>Foros</span>
<p>Los foros son una gran herramienta en donde no sólo puede ayudarte el equipo de {realm} sino que además otros jugadores pueden asistirte si conocen cómo resolver el problema que tengas.</p>
<p>Para solicitar ayuda por este medio, debes tener un usuario con el cual poder conectar y abrir un nuevo tema en el foro adecuado.</p>
<p>Puedes visitar nuestros foros <a href="https://foro.ultimowow.com/" target="_blank" rel="noopener noreferrer">aquí</a>.</p>
<span>{t('help.forosTitle')}</span>
<p>{t('help.foros1', { realm })}</p>
<p>{t('help.foros2')}</p>
<p>{t.rich('help.foros3', { link: (c) => <a href="https://foro.ultimowow.com/" target="_blank" rel="noopener noreferrer">{c}</a> })}</p>
<br />
<span>Correo electrónico</span>
<p>El servidor tiene diferentes direcciones de correo dedicados a diferentes temas del servidor.</p>
<p>Depende cual sea tu problema, usa el adecuado para enviarnos un mensaje de correo.</p>
<span>{t('help.emailTitle')}</span>
<p>{t('help.email1')}</p>
<p>{t('help.email2')}</p>
</div>
</fieldset>
</div>
+10 -8
View File
@@ -1,30 +1,32 @@
import type { Metadata } from 'next'
import { getTranslations } from 'next-intl/server'
export const metadata: Metadata = {
title: 'Aviso legal',
}
export default function LegalNoticePage() {
export default async function LegalNoticePage() {
const t = await getTranslations('Legal')
return (
<div className="main-page">
<div className="middle-content">
<div className="body-content">
<div className="title-content">
<h1>Aviso legal</h1>
<h1>{t('legalNotice.title')}</h1>
</div>
<div className="box-content">
<div className="body-box-content">
<p>El sitio web &quot;UltimoWoW.com&quot; y sus subdominios (en adelante, &quot;el sitio web&quot;) se ofrecen únicamente con fines informativos y de entretenimiento. UltimoWoW no se hace responsable del uso que los usuarios hagan del sitio web. Aunque se procura que se un uso correcto, UltimoWoW no asume responsabilidad por las acciones, decisiones o consecuencias derivadas del uso del sitio web. Los usuarios son responsables de verificar la precisión, integridad y utilidad de la información proporcionada, así como de cumplir con las leyes y regulaciones aplicables al utilizar el contenido del sitio web.</p>
<p>{t('legalNotice.p1')}</p>
<br />
<p>Enlaces a terceros: El sitio web puede contener enlaces a sitios web de terceros. Estos enlaces se proporcionan únicamente para tu conveniencia y no implican el respaldo o afiliación de UltimoWoW con dichos sitios web. UltimoWoW no se hace responsable del contenido o las prácticas de privacidad de estos sitios web de terceros.</p>
<p>{t('legalNotice.p2')}</p>
<br />
<p>Privacidad y Cookies: La recopilación y uso de tus datos personales se rigen por nuestra Política de Privacidad y el uso de cookies se rige por nuestra Declaración de Cookies. Al utilizar el sitio web, aceptas nuestras prácticas descritas en dichas políticas.</p>
<p>{t('legalNotice.p3')}</p>
<br />
<p>Derechos de autor: Los usuarios del sitio web deben respetar los derechos de autor y no publicar contenido que infrinja los derechos de propiedad intelectual de terceros. UltimoWoW se compromete a proteger los derechos de autor y cumplir con las leyes aplicables. Si encuentras contenido infractor, contáctanos utilizando el mecanismo de reclamación de infracción descrito a continuación.</p>
<p>{t('legalNotice.p4')}</p>
<br />
<p>Mecanismo de reclamación de infracción: Si crees que tu trabajo ha sido utilizado de manera no autorizada en el sitio web, puedes presentar una reclamación por infracción de derechos de autor proporcionando la información requerida. Contáctanos utilizando los datos de contacto a continuación.</p>
<p>{t('legalNotice.p5')}</p>
<br />
<p className="second-brown">Contacto</p>
<p className="second-brown">{t('legalNotice.contactTitle')}</p>
<p>ultimowow.com</p>
<p>info@ultimowow.com</p>
</div>
@@ -1,5 +1,5 @@
import type { Metadata } from 'next'
import { setRequestLocale } from 'next-intl/server'
import { getTranslations, setRequestLocale } from 'next-intl/server'
import { redirect } from '@/i18n/navigation'
import { getSession } from '@/lib/session'
import { getGameCharacters } from '@/lib/characters'
@@ -22,44 +22,39 @@ export default async function LevelUpCharacterPage({ params }: { params: Promise
const [chars, price] = await Promise.all([getGameCharacters(session.accountId!), getLevelUpPrice()])
const t = await getTranslations('CharServiceB.levelUp')
return (
<PageShell title="Subir a nivel 80">
<ServiceBox>
<br />
<p>
La herramienta <span>Subir a nivel 80</span> te permite subir cualquier personaje al nivel 80.
</p>
<p>Sólo subirá de nivel al personaje. Esta opción no incluye ítems/oro/etc.</p>
<p>{t.rich('intro', { s: (c) => <span>{c}</span> })}</p>
<p>{t('onlyLevel')}</p>
<br />
<p>
Al usar el botón SUBIR A NIVEL 80 del personaje que hayas escogido, se enviará el nivel 80 al personaje.
</p>
<p>Cuando ingreses a la lista de personajes de tu cuenta, verás que tu personaje escogido ya es nivel 80.</p>
<p>{t('instructions')}</p>
<p>{t('levelInfo')}</p>
<br />
<fieldset>
<legend>NOTA</legend>
<legend>{t('noteLegend')}</legend>
<div className="separate2">
<p>Se requiere que el personaje esté desconectado.</p>
<p>
Por favor, asegúrate de haber escogido al personaje correcto ya que esta acción no es reversible una vez
realizada.
</p>
<p>{t('noteOffline')}</p>
<p>{t('noteConfirm')}</p>
</div>
</fieldset>
<div className="centered">
<br />
<br />
<p>Requiere <span>{price}</span> <span className="yellow-info"></span> (SumUp)</p>
<p>{t.rich('requires', { price, s: (c) => <span>{c}</span>, y: (c) => <span className="yellow-info">{c}</span> })}</p>
</div>
<PaidServiceForm
characters={chars.map((c) => ({ name: c.name, classCss: c.classCss }))}
checkoutEndpoint="/api/character/level-up/checkout"
payLabel="SUBIR A NIVEL 80"
payLabel={t('payLabel')}
buttonClass="level-up-button"
provider="sumup"
confirmText={`¿Estás seguro de subir a nivel 80 al personaje seleccionado por ${price} € (SumUp)?`}
confirmText={t('confirm', { price })}
/>
</ServiceBox>
</PageShell>
+14 -23
View File
@@ -1,4 +1,4 @@
import { setRequestLocale } from 'next-intl/server'
import { getTranslations, setRequestLocale } from 'next-intl/server'
import { Link } from '@/i18n/navigation'
import { ServerClock } from '@/components/ServerClock'
import { getNoticias, getServerStatus, type Noticia, type ServerStatus } from '@/lib/home'
@@ -38,6 +38,7 @@ function formatFecha(iso: string | Date | null): string {
export default async function HomePage({ params }: { params: Promise<{ locale: string }> }) {
const { locale } = await params
setRequestLocale(locale)
const t = await getTranslations('Misc')
const { noticias, status } = await getData()
const realmName = await getRealmName()
@@ -72,7 +73,7 @@ export default async function HomePage({ params }: { params: Promise<{ locale: s
<td className={`real-info-box ${loginClass}`}>{loginStatus}</td>
</tr>
<tr>
<td>Hora</td>
<td>{t('home.labelTime')}</td>
<td className="real-info-box">
<ServerClock />
</td>
@@ -93,42 +94,32 @@ export default async function HomePage({ params }: { params: Promise<{ locale: s
</table>
</div>
<div className="raf-index-responsive">
<Link href="/recruit-a-friend">¡RECLUTA A UN AMIGO!</Link>
<Link href="/recruit-a-friend">{t('home.recruitFriend')}</Link>
</div>
</div>
<div className="raf-index">
<Link href="/recruit-a-friend">¡RECLUTA A UN AMIGO!</Link>
<Link href="/recruit-a-friend">{t('home.recruitFriend')}</Link>
</div>
</div>
<div className="body-box-content">
<div className="body-box-content">
<p>
{realmName} es una comunidad enfocada en usuarios de habla hispana,
especialmente dirigida al público español y latino.
</p>
<p>
Atraemos a usuarios apasionados tanto por el PvE como por el PvP, y ofrecemos una
amplia variedad de contenido.
</p>
<p>{t('home.introP1', { realm: realmName })}</p>
<p>{t('home.introP2')}</p>
<br />
<p>
Nuestro objetivo principal es mantener una comunidad de alto nivel, brindando un
ambiente agradable para que todos los miembros, nuevos y veteranos, encuentren aquí
un lugar donde puedan vivir momentos inigualables.
</p>
<p>¡Únete y disfruta de una experiencia única con miles de usuarios!</p>
<p>{t('home.introP3')}</p>
<p>{t('home.introP4')}</p>
</div>
</div>
</div>
<br />
<div className="title-content-h3">
<h3 className="big-font">NOTICIAS</h3>
<h3 className="big-font">{t('home.newsHeading')}</h3>
</div>
<div className="box-content">
<div className="body-box-content">
{noticias.length === 0 ? (
<p>No hay noticias disponibles en este momento.</p>
<p>{t('home.noNews')}</p>
) : (
<>
{noticias.map((n) => (
@@ -141,9 +132,9 @@ export default async function HomePage({ params }: { params: Promise<{ locale: s
/>
{n.enlace && (
<p>
Enlace:{' '}
{t('home.newsLinkLabel')}{' '}
<a href={n.enlace} target="_blank" rel="noopener noreferrer">
aquí
{t('home.here')}
</a>
</p>
)}
@@ -153,7 +144,7 @@ export default async function HomePage({ params }: { params: Promise<{ locale: s
</div>
))}
<p className="centered third-brown">
<i>* Mostrando las últimas 50 noticias</i>
<i>{t('home.showingLatest')}</i>
</p>
<br />
</>
+14 -14
View File
@@ -1,5 +1,5 @@
import type { Metadata } from 'next'
import { setRequestLocale } from 'next-intl/server'
import { setRequestLocale, getTranslations } from 'next-intl/server'
import { redirect } from '@/i18n/navigation'
import { getSession } from '@/lib/session'
import { getPointsHistory, getPointsBalances, type PointsMovement } from '@/lib/points-history'
@@ -25,6 +25,7 @@ function methodClass(method: PointsMovement['method']): string {
export default async function PointsHistoryPage({ params }: { params: Promise<{ locale: string }> }) {
const { locale } = await params
setRequestLocale(locale)
const t = await getTranslations('History')
const session = await getSession()
if (!session.bnetId) redirect({ href: '/login', locale })
@@ -36,25 +37,24 @@ export default async function PointsHistoryPage({ params }: { params: Promise<{
])
return (
<PageShell title="Historial de PD y PV">
<PageShell title={t('points.pageTitle')}>
<ServiceBox>
<br />
<p>Aquí verás los consumos de PD y PV más recientes que ha tenido la cuenta.</p>
<p>{t('points.intro1')}</p>
<p>
Con esta información podrás tener control de todo lo que has adquirido así como también chequear que toda la
actividad sea normal.
{t('points.intro2')}
</p>
<p>Si observas algún movimiento sospechoso, puedes contactar con el equipo de Nova WoW.</p>
<p>{t('points.intro3')}</p>
<br />
</ServiceBox>
<div className="box-content">
<div className="title-box-content">
<h2>Historial de PD y PV</h2>
<h2>{t('points.boxTitle')}</h2>
</div>
<div className="body-box-content">
<p className="centered">
Saldo actual: <span className="yellow-info">{balances.dp} PD</span> ·{' '}
{t('points.currentBalance')} <span className="yellow-info">{balances.dp} PD</span> ·{' '}
<span className="purple-info">{balances.pv} PV</span>
</p>
<br />
@@ -63,7 +63,7 @@ export default async function PointsHistoryPage({ params }: { params: Promise<{
<tbody>
<tr>
<td>
<span>No hay movimientos</span>
<span>{t('points.noMovements')}</span>
</td>
</tr>
</tbody>
@@ -72,13 +72,13 @@ export default async function PointsHistoryPage({ params }: { params: Promise<{
<table className="max-center-table-aligned2">
<thead>
<tr>
<th>Fecha</th>
<th>Concepto</th>
<th>Método</th>
<th>{t('points.thDate')}</th>
<th>{t('points.thConcept')}</th>
<th>{t('points.thMethod')}</th>
<th>PD</th>
<th>PV</th>
<th>Importe</th>
<th>Estado</th>
<th>{t('points.thAmount')}</th>
<th>{t('points.thStatus')}</th>
</tr>
</thead>
<tbody>
+33 -31
View File
@@ -1,76 +1,78 @@
import type { Metadata } from 'next'
import { getTranslations } from 'next-intl/server'
export const metadata: Metadata = {
title: 'Política de privacidad',
}
export default function PrivacyPolicyPage() {
export default async function PrivacyPolicyPage() {
const t = await getTranslations('Legal')
return (
<div className="main-page">
<div className="middle-content">
<div className="body-content">
<div className="title-content">
<h1>Política de privacidad</h1>
<h1>{t('privacy.title')}</h1>
</div>
<div className="box-content">
<div className="body-box-content justified">
<p>Esta Política de Privacidad describe cómo recopilamos, utilizamos, almacenamos y protegemos la información personal de los usuarios (&quot;usted&quot; o &quot;usuario&quot;) de nuestro sitio web. Al utilizar nuestro sitio web, usted acepta las prácticas descritas en esta política.</p>
<p>{t('privacy.p1')}</p>
<br />
<hr />
<br />
<p className="second-brown">Recopilación de Información Personal</p>
<p>Recopilamos la siguiente información personal cuando usted utiliza nuestro sitio web:</p>
<p>1. Cookies: Utilizamos cookies para recopilar información sobre su actividad en nuestro sitio web. Las cookies son pequeños archivos de texto que se almacenan en su dispositivo y nos permiten reconocer su navegador o dispositivo y recordar cierta información. Estas cookies pueden ser cookies de sesión o cookies persistentes. Puede ajustar la configuración de su navegador para rechazar todas las cookies o para indicar cuándo se envía una cookie. Sin embargo, si desactiva las cookies, es posible que algunas funciones de nuestro sitio web no funcionen correctamente.</p>
<p>2. Direcciones IP: Recopilamos direcciones IP de los usuarios que visitan nuestro sitio web. Las direcciones IP se utilizan para analizar tendencias, administrar el sitio web, rastrear el movimiento del usuario y recopilar información demográfica. No vinculamos las direcciones IP con ninguna información de identificación personal.</p>
<p>3. Direcciones de correo electrónico: Recopilamos las direcciones de correo electrónico que usted proporciona al registrarse en nuestro sitio web o al actualizar su información de registro. Utilizamos estas direcciones de correo electrónico para enviarle información relacionada con su cuenta, como confirmaciones de registro, notificaciones de cambios en los términos o políticas, y actualizaciones de seguridad.</p>
<p>4. Nombres de usuario y contraseñas: Recopilamos los nombres de usuario y las contraseñas que usted elige al registrar una cuenta en nuestro sitio web. Estas credenciales se utilizan para autenticar su acceso a su cuenta y para proteger su información personal.</p>
<p className="second-brown">{t('privacy.h1')}</p>
<p>{t('privacy.p2')}</p>
<p>{t('privacy.p3')}</p>
<p>{t('privacy.p4')}</p>
<p>{t('privacy.p5')}</p>
<p>{t('privacy.p6')}</p>
<br />
<hr />
<br />
<p className="second-brown">Uso de la Información Personal</p>
<p>Utilizamos la información personal que recopilamos para los siguientes fines:</p>
<p>1. Brindar y mejorar nuestros servicios: Utilizamos su información personal para brindarle los servicios solicitados y mejorar su experiencia en nuestro sitio web. Esto incluye personalizar el contenido y las recomendaciones, proporcionar soporte técnico, y analizar el rendimiento y la eficacia de nuestro sitio web.</p>
<p>2. Comunicaciones: Podemos utilizar su dirección de correo electrónico para enviarle comunicaciones relacionadas con su cuenta, como confirmaciones de registro, notificaciones de cambios en los términos o políticas, actualizaciones de seguridad y otras actualizaciones importantes.</p>
<p className="second-brown">{t('privacy.h2')}</p>
<p>{t('privacy.p7')}</p>
<p>{t('privacy.p8')}</p>
<p>{t('privacy.p9')}</p>
<br />
<hr />
<br />
<p className="second-brown">Protección de la Información Personal</p>
<p>Tomamos medidas razonables para proteger la información personal que recopilamos y almacenamos. Utilizamos medidas de seguridad técnicas, administrativas y físicas para proteger su información contra el acceso no autorizado, la divulgación, la alteración o la destrucción.</p>
<p className="second-brown">{t('privacy.h3')}</p>
<p>{t('privacy.p10')}</p>
<br />
<hr />
<br />
<p className="second-brown">Divulgación de la Información Personal a Terceros</p>
<p>No vendemos, intercambiamos ni transferimos su información personal a terceros sin su consentimiento, excepto en los siguientes casos:</p>
<p>- Proveedores de servicios: Podemos compartir su información personal con proveedores de servicios que realizan funciones en nuestro nombre, como el alojamiento del sitio web, el procesamiento de pagos y el análisis de datos. Estos proveedores de servicios tienen acceso a su información personal solo en la medida necesaria para realizar sus funciones y están obligados a no divulgar ni utilizar la información con ningún otro fin.</p>
<p>- Cumplimiento legal: Podemos divulgar su información personal si creemos de buena fe que dicha divulgación es necesaria para cumplir con una obligación legal, proteger sus derechos o seguridad, investigar fraudes o responder a una solicitud legal.</p>
<p className="second-brown">{t('privacy.h4')}</p>
<p>{t('privacy.p11')}</p>
<p>{t('privacy.p12')}</p>
<p>{t('privacy.p13')}</p>
<br />
<hr />
<br />
<p className="second-brown">Sus Derechos y Opciones</p>
<p>Usted tiene ciertos derechos y opciones con respecto a su información personal:</p>
<p>- Acceso y actualización: Usted puede acceder y actualizar cierta información personal proporcionada a través de su cuenta en nuestro sitio web.</p>
<p>- Eliminación de datos: Usted puede solicitar la eliminación de su información personal de nuestros registros, sujeto a cualquier requisito legal o contractual que podamos tener.</p>
<p>- Cookies: Puede ajustar la configuración de su navegador para rechazar cookies o para indicar cuándo se envía una cookie. Sin embargo, tenga en cuenta que deshabilitar las cookies puede afectar el funcionamiento de nuestro sitio web.</p>
<p className="second-brown">{t('privacy.h5')}</p>
<p>{t('privacy.p14')}</p>
<p>{t('privacy.p15')}</p>
<p>{t('privacy.p16')}</p>
<p>{t('privacy.p17')}</p>
<br />
<hr />
<br />
<p className="second-brown">Uso incorrecto de plataformas de terceros con el fin de violar nuestra política de devolución</p>
<p>Este tipo de acciones conllevará a la expulsión del usuario de nuestro sitio sin ningún tipo de devolución.</p>
<p className="second-brown">{t('privacy.h6')}</p>
<p>{t('privacy.p18')}</p>
<br />
<hr />
<br />
<p className="second-brown">Cambios a esta Política de Privacidad</p>
<p>Nos reservamos el derecho de actualizar esta Política de Privacidad en cualquier momento. Le recomendamos que revise periódicamente esta página para conocer los cambios. La fecha de entrada en vigencia de la política se indicará en la parte superior de la página.</p>
<p className="second-brown">{t('privacy.h7')}</p>
<p>{t('privacy.p19')}</p>
<br />
<hr />
<br />
<p className="second-brown">Contáctanos</p>
<p>Si tiene alguna pregunta o inquietud sobre esta Política de Privacidad, puede contactarnos a través de la siguiente información de contacto:</p>
<p className="second-brown">{t('privacy.h8')}</p>
<p>{t('privacy.p20')}</p>
<p>Error 404</p>
<p>ultimowow.com</p>
<p>info@ultimowow.com</p>
<br />
<p className="grey-info2">Fecha de actualización: 23-05-2023</p>
<p className="grey-info2">{t('privacy.updated')}</p>
</div>
</div>
</div>
+8 -10
View File
@@ -1,5 +1,5 @@
import type { Metadata } from 'next'
import { setRequestLocale } from 'next-intl/server'
import { getTranslations, setRequestLocale } from 'next-intl/server'
import { redirect } from '@/i18n/navigation'
import { getSession } from '@/lib/session'
import { getRealmName } from '@/lib/realm'
@@ -14,6 +14,7 @@ export const metadata: Metadata = { title: 'Código de promoción' }
export default async function PromoCodePage({ params }: { params: Promise<{ locale: string }> }) {
const { locale } = await params
setRequestLocale(locale)
const t = await getTranslations('Points')
const session = await getSession()
if (!session.bnetId) redirect({ href: '/login', locale })
@@ -22,25 +23,22 @@ export default async function PromoCodePage({ params }: { params: Promise<{ loca
const realm = await getRealmName()
return (
<PageShell title="Código de promoción">
<PageShell title={t('promo.title')}>
<ServiceBox>
<br />
<p>
Los <span>Códigos de promoción</span> son palabras con letras y números que se canjean en el formulario de
abajo por PD o PV y tienen usos limitados. Cada uno puede dar diferente cantidad de PD/PV.
{t.rich('promo.intro1', { s: (c) => <span>{c}</span> })}
</p>
<p>
Estos códigos son generados por el Equipo de {realm} y pueden aparecer en la página web, Discord, las redes
sociales o el servidor.
{t('promo.intro2', { realm })}
</p>
<p>Aconsejamos poner atención a las redes sociales y los anuncios del servidor para saber cuándo hay un código disponible.</p>
<p>{t('promo.intro3')}</p>
<br />
<fieldset>
<legend>NOTA</legend>
<legend>{t('promo.note')}</legend>
<div className="separate2">
<p>
Los códigos son sensibles a mayúsculas y minúsculas por lo que siempre debes escribirlos igual a como
aparecen publicados.
{t('promo.noteText')}
</p>
</div>
</fieldset>
+26 -24
View File
@@ -1,5 +1,6 @@
import type { Metadata } from 'next'
import { setRequestLocale } from 'next-intl/server'
import { NoteLegend } from '@/components/NoteLegend'
import { getTranslations, setRequestLocale } from 'next-intl/server'
import { redirect } from '@/i18n/navigation'
import { getSession } from '@/lib/session'
import { getGameCharacters } from '@/lib/characters'
@@ -25,6 +26,7 @@ const CLASS_GROUPS: { css: string; name: string }[] = [
export default async function QuestCharacterPage({ params }: { params: Promise<{ locale: string }> }) {
const { locale } = await params
setRequestLocale(locale)
const t = await getTranslations('CharService')
const session = await getSession()
if (!session.bnetId) redirect({ href: '/login', locale })
@@ -40,21 +42,20 @@ export default async function QuestCharacterPage({ params }: { params: Promise<{
}))
return (
<PageShell title="Rastreador de misiones">
<PageShell title={t('quest.pageTitle')}>
<ServiceBox>
<br />
<p>
La herramienta <span>Rastreador de misiones</span> te permite ver el estado de misiones o cadenas de misiones
importantes en tus personajes.
{t.rich('quest.intro', { s: (c) => <span>{c}</span> })}
</p>
<br />
<p>Opciones disponibles:</p>
<p>{t('quest.optionsAvailable')}</p>
{CLASS_GROUPS.map((g) => {
const opts = QUEST_OPTIONS.filter((o) => o.classCss === g.css)
if (opts.length === 0) return null
return (
<div key={g.css}>
<p className={g.css}>{g.name}</p>
<p className={g.css}>{t(`quest.className.${g.css}`)}</p>
<ul>
{opts.map((o) => (
<li key={o.key}>
@@ -78,7 +79,7 @@ export default async function QuestCharacterPage({ params }: { params: Promise<{
) : (
o.label
)}{' '}
a partir del nivel {o.minLevel}
{t('quest.fromLevel', { level: o.minLevel })}
</li>
))}
</ul>
@@ -86,35 +87,36 @@ export default async function QuestCharacterPage({ params }: { params: Promise<{
</div>
)
})}
<p className="second-brown">Por reputación</p>
<p className="second-brown">{t('quest.byReputation')}</p>
<ul>
<li>
<a href={`${QUEST_DB}/?faction=1119`} target="_blank" rel="noopener noreferrer">Los Hijos de Hodir</a>: Cadena
de misiones principal hasta interactuar con el intendente{' '}
<a href={`${QUEST_DB}/?npc=32540`} target="_blank" rel="noopener noreferrer">Lillehoff</a>
{t.rich('quest.hodir', {
hodir: (c) => <a href={`${QUEST_DB}/?faction=1119`} target="_blank" rel="noopener noreferrer">{c}</a>,
lille: (c) => <a href={`${QUEST_DB}/?npc=32540`} target="_blank" rel="noopener noreferrer">{c}</a>,
})}
</li>
</ul>
<br />
<p className="second-brown">Por misión específica</p>
<p className="second-brown">{t('quest.bySpecific')}</p>
<ul>
<li>A partir del nivel 10</li>
<li>{t('quest.fromLevel10')}</li>
</ul>
<br />
<p>Cómo buscar por misión específica:</p>
<p>1) Visita nuestra <a href={QUEST_DB} target="_blank" rel="noopener noreferrer">base de datos</a>.</p>
<p>2) Busca allí el nombre de la misión.</p>
<p>3) Una vez que la hayas encontrado, su enlace se verá cómo &quot;<span className="second-brown">{QUEST_DB}/?quest=12843</span>&quot;</p>
<p>4) Usa el número final del enlace en el campo ID de misión (Ejemplo del enlace anterior: 12843).</p>
<p>{t('quest.howToSearch')}</p>
<p>{t.rich('quest.step1', { link: (c) => <a href={QUEST_DB} target="_blank" rel="noopener noreferrer">{c}</a> })}</p>
<p>{t('quest.step2')}</p>
<p>{t.rich('quest.step3', { url: QUEST_DB, s: (c) => <span className="second-brown">{c}</span> })}</p>
<p>{t('quest.step4')}</p>
<br />
<p>Al usar el botón BUSCAR MISIONES, podrás ver el estado de las misiones del personaje.</p>
<p>{t('quest.searchButtonExplain')}</p>
<br />
<fieldset>
<legend>NOTA</legend>
<NoteLegend />
<div className="separate2">
<p>Se requiere que el personaje esté desconectado y cumpla con el mínimo de nivel de cada opción.</p>
<p>Se puede repetir búsqueda de cadenas de misiones en un mismo personaje cada 1 hora.</p>
<p>Se puede repetir búsqueda de cadenas de misiones de clase en un mismo personaje cada 30 minutos.</p>
<p>Se puede repetir búsqueda de misión específica en un mismo personaje cada 10 minutos.</p>
<p>{t('quest.noteOffline')}</p>
<p>{t('quest.noteChain1h')}</p>
<p>{t('quest.noteClass30')}</p>
<p>{t('quest.noteSpecific10')}</p>
</div>
</fieldset>
+52 -50
View File
@@ -1,104 +1,106 @@
import type { Metadata } from 'next'
import { getTranslations } from 'next-intl/server'
export const metadata: Metadata = {
title: 'Política de reembolso',
}
export default function RefundPolicyPage() {
export default async function RefundPolicyPage() {
const t = await getTranslations('Legal')
return (
<div className="main-page">
<div className="middle-content">
<div className="body-content">
<div className="title-content">
<h1>Política de reembolso</h1>
<h1>{t('refund.title')}</h1>
</div>
<div className="box-content">
<div className="body-box-content justified">
<p className="second-brown">NUESTRO SERVICIO</p>
<p>Todos nuestros servicios son de entrega inmediata y se reciben tal cual se encuentran descritos.</p>
<p className="second-brown">{t('refund.h1')}</p>
<p>{t('refund.p1')}</p>
<br />
<hr />
<br />
<p className="second-brown">NUESTRA RESPONSABILIDAD</p>
<p>Garantizamos el funcionamiento, tiempo de entrega y descripción de los servicios brindados por nuestro sitio.</p>
<p>Es importante que el usuario comprenda que lo que adquiere en el sitio son servicios que se reciben de forma inmediata.</p>
<p>No adquiere tiempo de uso de los servicios, ni beneficio alguno, ni inmunidad.</p>
<p>Todo caso avalado por nuestra política de devolución será tratado con el reinicio o corrección del servicio y posterior revisión garantizando el funcionamiento correcto del servicio reiniciado o corregido.</p>
<p className="second-brown">{t('refund.h2')}</p>
<p>{t('refund.p2')}</p>
<p>{t('refund.p3')}</p>
<p>{t('refund.p4')}</p>
<p>{t('refund.p5')}</p>
<br />
<hr />
<br />
<p className="second-brown">TIEMPO DE NOTIFICACIÓN</p>
<p>Cualquier error o problema de nuestros servicios debe ser notificado a la administración en un período máximo de 48 horas desde la entrega del servicio.</p>
<p>Pasado este período de tiempo, el usuario ya no puede solicitar ningún tipo de devolución.</p>
<p>Contacto: <a href="mailto:contacto@ultimowow.com">contacto@ultimowow.com</a>.</p>
<p className="second-brown">{t('refund.h3')}</p>
<p>{t('refund.p6')}</p>
<p>{t('refund.p7')}</p>
<p>{t.rich('refund.p8', { link: (c) => <a href="mailto:contacto@ultimowow.com">{c}</a> })}</p>
<br />
<hr />
<br />
<p className="second-brown">COBERTURA</p>
<p>Esta política de devolución cubre:</p>
<p>- Adquisición del servicio incorrecto por una mala selección del usuario.</p>
<p>- Errores ocasionados por el sitio web que hayan impedido la entrega del servicio.</p>
<p>- Errores ocasionados por plataformas de terceros que hayan impedido la entrega del servicio.</p>
<p className="second-brown">{t('refund.h4')}</p>
<p>{t('refund.p9')}</p>
<p>{t('refund.p10')}</p>
<p>{t('refund.p11')}</p>
<p>{t('refund.p12')}</p>
<br />
<p>Esta política de devolución NO cubre, bajo ninguna circunstancia:</p>
<p>- Arrepentimientos.</p>
<p>- Errores en el uso del servicio realizados por el usuario.</p>
<p>- Desconocimiento de nuestra política de devolución.</p>
<p>- Uso incorrecto de plataformas de terceros con el fin de violar nuestra política de devolución.</p>
<p>- Expulsión de nuestro sitio por incumplimiento de nuestros términos y normas.</p>
<p>{t('refund.p13')}</p>
<p>{t('refund.p14')}</p>
<p>{t('refund.p15')}</p>
<p>{t('refund.p16')}</p>
<p>{t('refund.p17')}</p>
<p>{t('refund.p18')}</p>
<br />
<p className="second-brown">Solicitud de reembolso hecha a través de la plataforma de pago.</p>
<p>Pedir un reembolso por razones avaladas en esta política de devolución procederá a un reembolso de nuestra parte con la correspondiente cancelación del servicio adquirido.</p>
<p className="second-brown">{t('refund.h5')}</p>
<p>{t('refund.p19')}</p>
<br />
<p>Pedir un reembolso por razones no avaladas en esta política de devolución como:</p>
<p>- La cuenta ha sido sancionada.</p>
<p>- La persona ya no usa el servicio e intenta solicitar un reembolso posterior a las 48 hs del momento de la adquisición.</p>
<p>- Abuso de herramientas disponibles alegando razones que falten a la verdad respecto del servicio entregado.</p>
<p>No aplican para devolución y puede conllevar a la expulsión permanente del sitio por falta a nuestros términos y un uso inadecuado de las plataformas de pago disponibles.</p>
<p>{t('refund.p20')}</p>
<p>{t('refund.p21')}</p>
<p>{t('refund.p22')}</p>
<p>{t('refund.p23')}</p>
<p>{t('refund.p24')}</p>
<br />
<hr />
<br />
<p className="second-brown">Arrepentimientos</p>
<p>Ningún tipo de arrepentimiento posterior a la entrega de un servicio aplica para devolución.</p>
<p className="second-brown">{t('refund.h6')}</p>
<p>{t('refund.p25')}</p>
<br />
<hr />
<br />
<p className="second-brown">Errores en el uso del servicio realizados por el usuario</p>
<p>Errores en el uso de los servicios, ocasionados por el usuario, que no tienen relación con ningún tipo de falla en el servicio entregado no aplica para devolución.</p>
<p>Cualquier error de usuario ocasionado por no leer la descripción de un servicio no aplica para devolución.</p>
<p>Una vez que el servicio se encuentra entregado, el uso del mismo queda a responsabilidad del usuario.</p>
<p className="second-brown">{t('refund.h7')}</p>
<p>{t('refund.p26')}</p>
<p>{t('refund.p27')}</p>
<p>{t('refund.p28')}</p>
<br />
<hr />
<br />
<p className="second-brown">Desconocimiento de nuestra política de devolución</p>
<p>El desconocimiento por parte del usuario de nuestra política de devolución no exime al usuario de su cumplimiento.</p>
<p className="second-brown">{t('refund.h8')}</p>
<p>{t('refund.p29')}</p>
<br />
<hr />
<br />
<p className="second-brown">Uso incorrecto de plataformas de terceros con el fin de violar nuestra política de devolución</p>
<p>Este tipo de acciones conllevará a la expulsión del usuario de nuestro sitio sin ningún tipo de devolución.</p>
<p className="second-brown">{t('refund.h9')}</p>
<p>{t('refund.p30')}</p>
<br />
<hr />
<br />
<p className="second-brown">Expulsión de nuestro sitio por incumplimiento de nuestros términos y normas</p>
<p>Ninguna adquisición de nuestros servicios otorga ningún tipo de inmunidad.</p>
<p>La expulsión de un usuario de nuestro sitio no aplica a ningún tipo de devolución.</p>
<p>El cumplimiento de nuestros términos y normas queda a exclusiva responsabilidad del usuario y cualquier expulsión que resulte del incumplimiento de los mismos no califica a solicitar devolución de los servicios ya entregados y disfrutados.</p>
<p className="second-brown">{t('refund.h10')}</p>
<p>{t('refund.p31')}</p>
<p>{t('refund.p32')}</p>
<p>{t('refund.p33')}</p>
<br />
<hr />
<br />
<p className="second-brown">Cambios a esta Política de Reembolso</p>
<p>Nos reservamos el derecho de actualizar esta Política de Reembolso en cualquier momento. Le recomendamos que revise periódicamente esta página para conocer los cambios.</p>
<p className="second-brown">{t('refund.h11')}</p>
<p>{t('refund.p34')}</p>
<br />
<hr />
<br />
<p className="second-brown">Contáctanos</p>
<p>Si tiene alguna pregunta o inquietud sobre esta Política de Reembolso, puede contactarnos a través de la siguiente información de contacto:</p>
<p className="second-brown">{t('refund.h12')}</p>
<p>{t('refund.p35')}</p>
<p>Error 404</p>
<p>ultimowow.com</p>
<p>info@ultimowow.com</p>
<br />
<p className="grey-info2">Fecha de actualización: 25-05-2023</p>
<p className="grey-info2">{t('refund.updated')}</p>
</div>
</div>
</div>
+11 -17
View File
@@ -1,5 +1,5 @@
import type { Metadata } from 'next'
import { setRequestLocale } from 'next-intl/server'
import { getTranslations, setRequestLocale } from 'next-intl/server'
import { redirect } from '@/i18n/navigation'
import { getSession } from '@/lib/session'
import { getGameCharacters } from '@/lib/characters'
@@ -22,43 +22,37 @@ export default async function RenameCharacterPage({ params }: { params: Promise<
const [chars, price] = await Promise.all([getGameCharacters(session.accountId!), getRenamePrice()])
const t = await getTranslations('CharServiceB.rename')
return (
<PageShell title="Renombrar personaje">
<ServiceBox>
<br />
<p>
La herramienta <span>Renombrar personaje</span> te permite cambiar de nombre a un personaje.
</p>
<p>{t.rich('intro', { s: (c) => <span>{c}</span> })}</p>
<br />
<p>
Al usar el botón RENOMBRAR del personaje que hayas escogido, se enviará una petición de cambio de nombre de
dicho personaje.
</p>
<p>Cuando intentes entrar al reino con ese personaje, te pedirá que escojas un nuevo nombre.</p>
<p>{t('instructions')}</p>
<p>{t('newNameInfo')}</p>
<br />
<fieldset>
<legend>NOTA</legend>
<legend>{t('noteLegend')}</legend>
<div className="separate2">
<p>
Por favor, asegúrate de haber escogido al personaje correcto ya que esta acción no es reversible una vez
realizada.
</p>
<p>{t('noteConfirm')}</p>
</div>
</fieldset>
<div className="centered">
<br />
<br />
<p>Requiere <span>{price}</span> <span className="yellow-info"></span> (SumUp)</p>
<p>{t.rich('requires', { price, s: (c) => <span>{c}</span>, y: (c) => <span className="yellow-info">{c}</span> })}</p>
</div>
<PaidServiceForm
characters={chars.map((c) => ({ name: c.name, classCss: c.classCss }))}
checkoutEndpoint="/api/character/rename/checkout"
payLabel="RENOMBRAR"
payLabel={t('payLabel')}
buttonClass="rename-button"
provider="sumup"
confirmText={`¿Estás seguro de renombrar al personaje seleccionado por ${price} € (SumUp)?`}
confirmText={t('confirm', { price })}
/>
</ServiceBox>
</PageShell>
+18 -15
View File
@@ -1,5 +1,5 @@
import type { Metadata } from 'next'
import { setRequestLocale } from 'next-intl/server'
import { getTranslations, setRequestLocale } from 'next-intl/server'
import { redirect } from '@/i18n/navigation'
import { getSession } from '@/lib/session'
import { getGuildMasterGuilds, GUILD_RENAME_PD } from '@/lib/guild'
@@ -14,6 +14,7 @@ export const metadata: Metadata = { title: 'Renombrar hermandad' }
export default async function RenameGuildPage({ params }: { params: Promise<{ locale: string }> }) {
const { locale } = await params
setRequestLocale(locale)
const t = await getTranslations('Points')
const session = await getSession()
if (!session.bnetId) redirect({ href: '/login', locale })
@@ -22,33 +23,29 @@ export default async function RenameGuildPage({ params }: { params: Promise<{ lo
const guilds = await getGuildMasterGuilds(session.accountId!)
return (
<PageShell title="Renombrar hermandad">
<PageShell title={t('renameGuild.title')}>
<ServiceBox>
<br />
<p>
La herramienta <span>Renombrar hermandad</span> te permite cambiar de nombre a una hermandad de la cual seas
Maestro de Hermandad.
{t.rich('renameGuild.intro1', { s: (c) => <span>{c}</span> })}
</p>
<br />
<p>A la hora de escoger un nuevo nombre, ten en cuenta:</p>
<p>- La longitud máxima es de 24 caracteres.</p>
<p>- Los caracteres permitidos son A-Za-z y el espacio.</p>
<p>{t('renameGuild.rules')}</p>
<p>{t('renameGuild.rule1')}</p>
<p>{t('renameGuild.rule2')}</p>
<br />
<p>
Al usar el botón RENOMBRAR HERMANDAD de la hermandad que hayas escogido, se cambiará su nombre al nuevo nombre
que hayas ingresado.
{t('renameGuild.intro2')}
</p>
<p>
El cambio de nombre de hermandad es instantáneo. Puede ser necesario que los miembros de la hermandad
conectados deban volver a conectar para finalmente ver el nuevo nombre.
{t('renameGuild.intro3')}
</p>
<br />
<fieldset>
<legend>NOTA</legend>
<legend>{t('renameGuild.note')}</legend>
<div className="separate2">
<p>
Por favor, asegúrate de haber elegido el nombre correcto ya que esta acción no es reversible una vez
realizada.
{t('renameGuild.noteText')}
</p>
</div>
</fieldset>
@@ -56,7 +53,13 @@ export default async function RenameGuildPage({ params }: { params: Promise<{ lo
<div className="centered">
<br />
<br />
<p>Requiere <span>{GUILD_RENAME_PD}</span> <span className="yellow-info">PD</span></p>
<p>
{t.rich('renameGuild.requires', {
pd: GUILD_RENAME_PD,
s: (c) => <span>{c}</span>,
y: (c) => <span className="yellow-info">{c}</span>,
})}
</p>
<br />
</div>
@@ -1,5 +1,6 @@
import type { Metadata } from 'next'
import { setRequestLocale } from 'next-intl/server'
import { NoteLegend } from '@/components/NoteLegend'
import { getTranslations, setRequestLocale } from 'next-intl/server'
import { redirect } from '@/i18n/navigation'
import { getSession } from '@/lib/session'
import { getRestorableCharacters } from '@/lib/restore-character'
@@ -14,6 +15,7 @@ export const metadata: Metadata = { title: 'Recuperar personaje' }
export default async function RestoreCharacterPage({ params }: { params: Promise<{ locale: string }> }) {
const { locale } = await params
setRequestLocale(locale)
const t = await getTranslations('CharService')
const session = await getSession()
if (!session.bnetId) redirect({ href: '/login', locale })
@@ -22,26 +24,26 @@ export default async function RestoreCharacterPage({ params }: { params: Promise
const chars = await getRestorableCharacters(session.accountId!)
return (
<PageShell title="Recuperar personaje">
<PageShell title={t('restoreChar.pageTitle')}>
<ServiceBox>
<br />
<p>
La herramienta <span>Recuperar personaje</span> te permite restaurar un personaje que haya sido borrado.
{t.rich('restoreChar.intro', { s: (c) => <span>{c}</span> })}
</p>
<br />
<p>Sólo disponible para personajes que cumplan las siguientes dos condiciones:</p>
<p>- Nivel superior a 60.</p>
<p>- Borrado en un período menor a 30 días.</p>
<p>Para personajes <span className="death-knight">Caballeros de la Muerte:</span></p>
<p>- No es posible recuperar un personaje Caballero de la Muerte si ya hay otro activo en la cuenta.</p>
<p>{t('restoreChar.conditions')}</p>
<p>{t('restoreChar.cond1')}</p>
<p>{t('restoreChar.cond2')}</p>
<p>{t.rich('restoreChar.dkHeading', { dk: (c) => <span className="death-knight">{c}</span> })}</p>
<p>{t('restoreChar.dkCond')}</p>
<br />
<p>Cuando ingreses a la lista de personajes de tu cuenta, verás al personaje recuperado.</p>
<p>{t('restoreChar.afterRestore')}</p>
<br />
<fieldset>
<legend>NOTA</legend>
<NoteLegend />
<div className="separate2">
<p>Personajes borrados que no cumplan dichas condiciones ya no son recuperables.</p>
<p>Se puede repetir la acción en un mismo personaje cada 12 horas.</p>
<p>{t('restoreChar.noteConditions')}</p>
<p>{t('restoreChar.noteCooldown')}</p>
</div>
</fieldset>
+20 -13
View File
@@ -1,5 +1,6 @@
import type { Metadata } from 'next'
import { setRequestLocale } from 'next-intl/server'
import { NoteLegend } from '@/components/NoteLegend'
import { getTranslations, setRequestLocale } from 'next-intl/server'
import { redirect } from '@/i18n/navigation'
import { getSession } from '@/lib/session'
import { getGameCharacters } from '@/lib/characters'
@@ -15,6 +16,7 @@ export const metadata: Metadata = { title: 'Recuperar ítems' }
export default async function RestoreItemsPage({ params }: { params: Promise<{ locale: string }> }) {
const { locale } = await params
setRequestLocale(locale)
const t = await getTranslations('CharService')
const session = await getSession()
if (!session.bnetId) redirect({ href: '/login', locale })
@@ -23,34 +25,39 @@ export default async function RestoreItemsPage({ params }: { params: Promise<{ l
const [chars, price] = await Promise.all([getGameCharacters(session.accountId!), getRestoreItemPrice()])
return (
<PageShell title="Recuperar ítems">
<PageShell title={t('restoreItems.pageTitle')}>
<ServiceBox>
<br />
<p>
La herramienta <span>Recuperar ítems</span> te permite recuperar ítems que hayan sido borrados.
{t.rich('restoreItems.intro', { s: (c) => <span>{c}</span> })}
</p>
<br />
<p>Sólo disponible para ítems que cumplan las siguientes condiciones:</p>
<p>{t('restoreItems.conditions')}</p>
<p>
- De calidad: <span className="q3">Raro</span> | <span className="q4">Épico</span> |{' '}
<span className="q5">Legendaria</span> | <span className="q6">Artefacto</span> | <span className="q6">Reliquia</span>.
{t.rich('restoreItems.quality', {
q3: (c) => <span className="q3">{c}</span>,
q4: (c) => <span className="q4">{c}</span>,
q5: (c) => <span className="q5">{c}</span>,
q6: (c) => <span className="q6">{c}</span>,
q6b: (c) => <span className="q6">{c}</span>,
})}
</p>
<p>- Equipable.</p>
<p>- Se liga al recogerlo.</p>
<p>- Borrado en un período menor a 7 días.</p>
<p>{t('restoreItems.condEquip')}</p>
<p>{t('restoreItems.condBound')}</p>
<p>{t('restoreItems.condDeleted')}</p>
<br />
<fieldset>
<legend>NOTA</legend>
<NoteLegend />
<div className="separate2">
<p>Ítems borrados que no cumplan dichas condiciones ya no son recuperables.</p>
<p>Se pueden consultar ítems borrados en un mismo personaje cada 8 horas.</p>
<p>{t('restoreItems.noteConditions')}</p>
<p>{t('restoreItems.noteCooldown')}</p>
</div>
</fieldset>
<div className="centered">
<br />
<br />
<p>Cada ítem requiere <span>{price}</span> <span className="yellow-info"></span> (SumUp)</p>
<p>{t.rich('restoreItems.requires', { price, s: (c) => <span>{c}</span>, e: (c) => <span className="yellow-info">{c}</span> })}</p>
<br />
<RestoreItemsForm characters={chars.map((c) => ({ name: c.name, classCss: c.classCss }))} price={price} />
</div>
@@ -1,5 +1,5 @@
import type { Metadata } from 'next'
import { setRequestLocale } from 'next-intl/server'
import { getTranslations, setRequestLocale } from 'next-intl/server'
import { redirect, Link } from '@/i18n/navigation'
import { getSession } from '@/lib/session'
import { is2faEnabled } from '@/lib/two-factor'
@@ -15,6 +15,7 @@ const IMG = '/nw-themes/nw-ryu/nw-images'
export default async function Security2faLoginPage({ params }: { params: Promise<{ locale: string }> }) {
const { locale } = await params
setRequestLocale(locale)
const t = await getTranslations('Misc')
const session = await getSession()
if (!session.bnetId) redirect({ href: '/login', locale })
@@ -23,12 +24,12 @@ export default async function Security2faLoginPage({ params }: { params: Promise
const enabled = session.accountId ? await is2faEnabled(session.accountId) : false
return (
<PageShell title="Verificación en 2 pasos">
<PageShell title={t('twofaLogin.pageTitle')}>
<div className="box-content">
<div className="title-box-content">
<h2>Información</h2>
<Link className="back-to-account" href="/my-account">Regresar a mi cuenta</Link>
<Link className="back-to-account-responsive" href="/my-account">Regresar</Link>
<h2>{t('twofaLogin.info')}</h2>
<Link className="back-to-account" href="/my-account">{t('twofaLogin.backToAccount')}</Link>
<Link className="back-to-account-responsive" href="/my-account">{t('twofaLogin.back')}</Link>
</div>
<div className="body-box-content justified">
<br />
@@ -36,30 +37,30 @@ export default async function Security2faLoginPage({ params }: { params: Promise
<img
className="twofa-login-preview"
src={`${IMG}/nw-general/nw-authenticator-preview.png`}
alt="Vista previa autenticador"
alt={t('twofaLogin.previewAlt')}
/>
<div className="twofa-login-info-text">
<p><span>2FA</span> son las siglas de &quot;Two Factor Authentication&quot;, o verificación en dos pasos.</p>
<p>{t.rich('twofaLogin.p1', { s: (c) => <span>{c}</span> })}</p>
<br />
<p>Es un método que añade una capa adicional de seguridad al proceso de inicio de sesión.</p>
<p>En lugar de simplemente ingresar tu nombre de usuario y contraseña, 2FA requiere un segundo paso de verificación, aumentando así la protección de tu cuenta.</p>
<p>{t('twofaLogin.p2')}</p>
<p>{t('twofaLogin.p3')}</p>
<br />
<p>Una vez activado, luego de ingresar usuario y contraseña, aparecerá una ventana (Imagen derecha) que te pedirá el código de autenticación generado por la aplicación.</p>
<p>{t('twofaLogin.p4')}</p>
<br />
<p><span>Códigos de respaldo</span></p>
<p>Al activar 2FA, se te proporcionarán códigos de respaldo únicos.</p>
<p>Estos códigos son útiles en caso de que pierdas acceso a tu aplicación de autenticación.</p>
<p>Cada código de respaldo puede usarse una sola vez.</p>
<p>Guárdalos en un lugar seguro, ya que te permitirán acceder a tu cuenta en el sitio web si no puedes usar la aplicación de autenticación.</p>
<p><span>{t('twofaLogin.backupCodes')}</span></p>
<p>{t('twofaLogin.p5')}</p>
<p>{t('twofaLogin.p6')}</p>
<p>{t('twofaLogin.p7')}</p>
<p>{t('twofaLogin.p8')}</p>
<br />
<p>Si aún no tienes Token de seguridad, puedes solicitarlo <Link href="/security-token" target="_blank">aquí</Link>.</p>
<p>{t.rich('twofaLogin.requestToken', { link: (c) => <Link href="/security-token" target="_blank">{c}</Link> })}</p>
<br />
</div>
</div>
<fieldset>
<legend>NOTA</legend>
<legend>{t('twofaLogin.note')}</legend>
<div className="separate2">
<p>Utiliza una aplicación de autenticación para iniciar sesión en el juego una vez que lo tengas activado.</p>
<p>{t('twofaLogin.noteText')}</p>
</div>
</fieldset>
+25 -26
View File
@@ -1,5 +1,5 @@
import type { Metadata } from 'next'
import { setRequestLocale } from 'next-intl/server'
import { setRequestLocale, getTranslations } from 'next-intl/server'
import { redirect } from '@/i18n/navigation'
import { getSession } from '@/lib/session'
import { getSecurityHistory } from '@/lib/security-history'
@@ -35,6 +35,7 @@ function UserCell({ label, bnet }: { label: string; bnet: string }) {
export default async function SecurityHistoryPage({ params }: { params: Promise<{ locale: string }> }) {
const { locale } = await params
setRequestLocale(locale)
const t = await getTranslations('History')
const session = await getSession()
if (!session.bnetId) redirect({ href: '/login', locale })
@@ -45,39 +46,37 @@ export default async function SecurityHistoryPage({ params }: { params: Promise<
const { activity, realm, web } = await getSecurityHistory(session.accountId!, displayUser, bnetEmail)
return (
<PageShell title="Historial de seguridad">
<PageShell title={t('security.pageTitle')}>
<ServiceBox>
<br />
<p>Aquí verás información de actividad y conexiones de tu cuenta.</p>
<p>Cada tabla tiene una explicación detallada de la información provista.</p>
<p>{t('security.intro1')}</p>
<p>{t('security.intro2')}</p>
<p>
Si desconoces algún tipo de actividad te recomendamos cambiar la contraseña de tu cuenta y del correo ligado a
la misma.
{t('security.intro3')}
</p>
<br />
<p>
<span>Historial de actividad</span>
<span>{t('security.activityTitle')}</span>
</p>
<p>Muestra actividad de seguridad de la cuenta, como por ejemplo cambios de contraseña.</p>
<p>{t('security.activityDesc')}</p>
<br />
<p>
<span>Historial de conexiones (Reino)</span>
<span>{t('security.realmTitle')}</span>
</p>
<p>
Muestra un historial de conexiones al reino <u>sólo cuando la IP ha cambiado</u>. No es un historial de cada
conexión.
{t.rich('security.realmDesc', { u: (c) => <u>{c}</u> })}
</p>
<br />
<p>
<span>Historial de conexiones (Web)</span>
<span>{t('security.webTitle')}</span>
</p>
<p>Muestra un historial de cada conexión al sitio web.</p>
<p>{t('security.webDesc')}</p>
</ServiceBox>
{/* Historial de actividad */}
<div className="box-content">
<div className="title-box-content">
<h2>Historial de actividad</h2>
<h2>{t('security.activityTitle')}</h2>
</div>
<div className="body-box-content centered">
{activity.length === 0 ? (
@@ -85,7 +84,7 @@ export default async function SecurityHistoryPage({ params }: { params: Promise<
<tbody>
<tr>
<td>
<span>No hay actividad</span>
<span>{t('security.noActivity')}</span>
</td>
</tr>
</tbody>
@@ -94,10 +93,10 @@ export default async function SecurityHistoryPage({ params }: { params: Promise<
<table className="max-center-table-aligned2">
<tbody>
<tr>
<th>Usuario</th>
<th>Acción</th>
<th>{t('security.thUser')}</th>
<th>{t('security.thAction')}</th>
<th>IP</th>
<th>Fecha</th>
<th>{t('security.thDate')}</th>
</tr>
{activity.map((a, i) => (
<tr key={i}>
@@ -122,7 +121,7 @@ export default async function SecurityHistoryPage({ params }: { params: Promise<
{/* Historial de conexiones (Reino) */}
<div className="box-content">
<div className="title-box-content">
<h2>Historial de conexiones (Reino)</h2>
<h2>{t('security.realmTitle')}</h2>
</div>
<div className="body-box-content centered">
{realm.length === 0 ? (
@@ -130,7 +129,7 @@ export default async function SecurityHistoryPage({ params }: { params: Promise<
<tbody>
<tr>
<td>
<span>No hay conexiones</span>
<span>{t('security.noConnections')}</span>
</td>
</tr>
</tbody>
@@ -140,7 +139,7 @@ export default async function SecurityHistoryPage({ params }: { params: Promise<
<tbody>
<tr>
<th>IP</th>
<th>Fecha</th>
<th>{t('security.thDate')}</th>
</tr>
{realm.map((c, i) => (
<tr key={i}>
@@ -161,7 +160,7 @@ export default async function SecurityHistoryPage({ params }: { params: Promise<
{/* Historial de conexiones (Web) */}
<div className="box-content">
<div className="title-box-content">
<h2>Historial de conexiones (Web)</h2>
<h2>{t('security.webTitle')}</h2>
</div>
<div className="body-box-content centered">
{web.length === 0 ? (
@@ -169,7 +168,7 @@ export default async function SecurityHistoryPage({ params }: { params: Promise<
<tbody>
<tr>
<td>
<span>No hay conexiones</span>
<span>{t('security.noConnections')}</span>
</td>
</tr>
</tbody>
@@ -178,10 +177,10 @@ export default async function SecurityHistoryPage({ params }: { params: Promise<
<table className="max-center-table-aligned">
<tbody>
<tr>
<th>Usuario</th>
<th>Estado</th>
<th>{t('security.thUser')}</th>
<th>{t('security.thStatus')}</th>
<th>IP</th>
<th>Fecha</th>
<th>{t('security.thDate')}</th>
</tr>
{web.map((c, i) => (
<tr key={i}>
@@ -1,4 +1,5 @@
import type { RowDataPacket } from 'mysql2'
import { NoteLegend } from '@/components/NoteLegend'
import { getTranslations, setRequestLocale } from 'next-intl/server'
import { redirect } from '@/i18n/navigation'
import { getSession } from '@/lib/session'
@@ -41,7 +42,7 @@ export default async function SecurityTokenPage({ params }: { params: Promise<{
<p><span className="red-info2">{t('warning')}</span></p>
<br />
<fieldset>
<legend>NOTA</legend>
<NoteLegend />
<div className="separate2">
<p>{t('note')}</p>
</div>
+11 -16
View File
@@ -1,5 +1,6 @@
import type { Metadata } from 'next'
import { setRequestLocale } from 'next-intl/server'
import { NoteLegend } from '@/components/NoteLegend'
import { getTranslations, setRequestLocale } from 'next-intl/server'
import { redirect, Link } from '@/i18n/navigation'
import { getSession } from '@/lib/session'
import { getGameCharacters } from '@/lib/characters'
@@ -15,6 +16,7 @@ export const metadata: Metadata = { title: 'Enviar regalo' }
export default async function SendGiftPage({ params }: { params: Promise<{ locale: string }> }) {
const { locale } = await params
setRequestLocale(locale)
const t = await getTranslations('CharService')
const session = await getSession()
if (!session.bnetId) redirect({ href: '/login', locale })
@@ -24,38 +26,31 @@ export default async function SendGiftPage({ params }: { params: Promise<{ local
const currency = process.env.SUMUP_CURRENCY === 'EUR' ? '€' : process.env.SUMUP_CURRENCY || '€'
return (
<PageShell title="Enviar regalo">
<PageShell title={t('sendGift.pageTitle')}>
<ServiceBox>
<br />
<p>
La herramienta <span>Enviar regalo</span> te permite enviar regalos a tus amigos.
{t.rich('sendGift.intro', { s: (c) => <span>{c}</span> })}
</p>
<br />
<p>
Al elegir desde qué personaje se enviará el regalo y el personaje que lo recibirá, podrás ver la lista de
objetos disponibles para enviar como regalo.
{t('sendGift.explain1')}
</p>
<p>Los objetos disponibles son los mismos de la tienda y puedes enviar varios objetos a la vez.</p>
<p>{t('sendGift.explain2')}</p>
<br />
<p>
Al usar el botón ENVIAR REGALO se enviará un correo al personaje destino con los ítems seleccionados,
indicando quién lo ha regalado. El pago se realiza mediante <span>SumUp</span>.
{t.rich('sendGift.explain3', { s: (c) => <span>{c}</span> })}
</p>
<br />
<p>
Si aún no tienes Token de seguridad, puedes solicitarlo{' '}
<Link href="/security-token" target="_blank">
aquí
</Link>
.
{t.rich('sendGift.securityTokenHint', { link: (c) => <Link href="/security-token" target="_blank">{c}</Link> })}
</p>
<br />
<fieldset>
<legend>NOTA</legend>
<NoteLegend />
<div className="separate2">
<p>
Por favor, asegúrate de haber escrito el nombre del personaje correcto ya que esta acción no es
reversible una vez realizada.
{t('sendGift.noteIrreversible')}
</p>
</div>
</fieldset>
@@ -1,112 +1,114 @@
import type { Metadata } from 'next'
import { getTranslations } from 'next-intl/server'
export const metadata: Metadata = {
title: 'Términos y condiciones',
}
export default function TermsAndConditionsPage() {
export default async function TermsAndConditionsPage() {
const t = await getTranslations('Legal')
return (
<div className="main-page">
<div className="middle-content">
<div className="body-content">
<div className="title-content">
<h1>Términos y condiciones</h1>
<h1>{t('terms.title')}</h1>
</div>
<div className="box-content">
<div className="body-box-content">
<span className="second-brown">IMPORTANTE</span>
<p>Por favor, tómese un momento para leer los términos y condiciones detallados debajo. Si está de acuerdo con ellos, entonces podrá ingresar en nuestro sitio.</p>
<span className="second-brown">{t('terms.h1')}</span>
<p>{t('terms.p1')}</p>
<br />
<hr />
<br />
<span className="second-brown">ACEPTACIÓN DE LOS TÉRMINOS DE USO Y CONDICIONES</span>
<p>UltimoWoW es un proyecto sin ánimo de lucro desarrollado con la finalidad de simular versiones desactualizadas del servidor con fines únicamente educativos. Los Servicios ofrecidos por UltimoWoW no apoyan ni proveerán modificación de ningún tipo para con el servidor o sus ficheros, dicho esto, el usuario al hacer uso de este sitio o cualquier servicio perteneciente o derivado de la Administración, acepta hacerse responsable de seguir y cumplir con el contrato de licencia para usuarios finales (EULA) del servidor.</p>
<span className="second-brown">{t('terms.h2')}</span>
<p>{t('terms.p2')}</p>
<br />
<p>UltimoWoW no provee ni se responsabiliza por la distribución de ningún cliente del servidor o fuente externa que lo contenga. Los enlaces de descarga para el cliente ofrecidos por este sitio o cualquier medio bajo su responsabilidad, apuntarán siempre a dichas fuentes externas, las cuales, a la fecha no pertenecen ni poseen relación alguna con esta administración. No haciéndose UltimoWoW de esta manera, y bajo ningún concepto, responsable sobre aspecto alguno sobre dicha fuente, dígase: calidad, rendimiento, garantías o cualquiera fuese o no, característica determinante a su fin.</p>
<p>{t('terms.p3')}</p>
<br />
<p>Al acceder, navegar o usar este sitio de Internet y en adelante (el Sitio), propiedad de la administración UltimoWoW en adelante (la Administración), el usuario admite haber leído y entendido los presentes términos y condiciones de uso, en adelante (Términos y Condiciones) y está de acuerdo en acatar los mismos y cumplir con todas las normas, leyes y reglamentos aplicables que hagan parte de la legislación vigente en su lugar de residencia, o a las que se sujete indistintamente. Además, cuando el usuario utilice cualquier servicio suministrado o referenciado en el Sitio, (siempre y cuando éste resida bajo responsabilidad o propiedad de la Administración) estará sujeto a las reglas, guías, políticas, términos y condiciones aplicables a dichos servicios.</p>
<p>{t('terms.p4')}</p>
<br />
<p>Este Sitio es controlado y desarrollado por la Administración, localizable a través de sus plataformas de Discord.</p>
<p>{t('terms.p5')}</p>
<br />
<p>La Administración no es responsable de que el material en este Sitio sea apropiado o esté disponible para su uso en otros lugares, estando prohibido su acceso desde territorios donde su contenido sea ilegal. Aquellos que decidan acceder a este Sitio desde otros lugares lo harán bajo su propia iniciativa, y es su responsabilidad el sujetarse a las leyes locales que sean aplicables.</p>
<p>{t('terms.p6')}</p>
<br />
<p>Estos Términos y Condiciones están sujetos a cambios a discreción, sin previo aviso y en cualquier momento, bajo la sola voluntad de UltimoWoW, y partiendo de la fecha de publicación de la modificación de los mismos en este Sitio, todas las operaciones celebradas entre la Administración y el usuario se regirán por el documento modificado.</p>
<p>{t('terms.p7')}</p>
<br />
<hr />
<br />
<span className="second-brown">NUESTRO SERVICIO: USO, DISTRIBUCIÓN Y DESCARGO DE RESPONSABILIDADES</span>
<p>De conformidad con las legislaciones bajo las cuales la Administración se acate, el material contenido en este Sitio o cualquier otro que se encontrase bajo su responsabilidad o tutela y en adelante, (los Servicios o el Servicio), incluyendo sin limitación, toda imagen, links, logotipo, diseño, insignia, marca, fotografía, sonido, texto, mensaje, herramienta, software, tecnología, producto, archivo, información, dato, demostración, muestra, material promocional, obra audiovisual u obra multimedia, y cualquiera sea otro elemento o forma de expresión (de forma colectiva y en adelante, (los &quot;Materiales&quot;), son suministrados según nuestro leal saber y entender, sin embargo, no es posible descartar errores de contenido o del material, razón por la cual la Administración no asume responsabilidad por la calidad, veracidad o exactitud, de la información publicada ni por las omisiones, errores o discrepancias que pudiesen encontrarse en la información publicada o servicio brindado. </p>
<span className="second-brown">{t('terms.h3')}</span>
<p>{t('terms.p8')}</p>
<br />
<p>Respecto a la información publicada en los Servicios, la Administración no confiere garantías de ningún tipo, expresas ni implícitas, excluyendo, más no limitado a:</p>
<p>- Garantías de comerciabilidad o adecuación para un propósito particular.</p>
<p>- Estabilidad ininterrumpida del Servicio.</p>
<p>- Existencia de errores o virus, así como sobre su seguridad.</p>
<p>- Exactitud, confiabilidad, calificación o certificación para su descarga.</p>
<p>{t('terms.p9')}</p>
<p>{t('terms.p10')}</p>
<p>{t('terms.p11')}</p>
<p>{t('terms.p12')}</p>
<p>{t('terms.p13')}</p>
<br />
<p>La Administración no se hace responsable sobre el uso que el usuario haga de los contenidos incluidos en los Servicios, ni de las decisiones que tome respecto a éstos. La información respecto a los productos y servicios de UltimoWoW, incluida en sus Servicios es brindada con fines edcuativos no comerciales y no constituye oferta de venta para sus usuarios.</p>
<p>{t('terms.p14')}</p>
<br />
<p>UltimoWoW se reserva el derecho de a discreción:</p>
<p>- Corregir cualquier error, omisión o inexactitud en los datos brindados.</p>
<p>- Cambiar o actualizar, descontinuar o eliminar la información contenida en los Servicios en cualquier momento y sin previo aviso.</p>
<p>- Así como también se exime de responsabilidad sobre lo que refiere a la puntualidad, falta de almacenamiento, inexactitud o entrega incorrecta de cualquier información o dato de los que normalmente proporcione, ya sea temporal o permanentemente.</p>
<p>{t('terms.p15')}</p>
<p>{t('terms.p16')}</p>
<p>{t('terms.p17')}</p>
<p>{t('terms.p18')}</p>
<br />
<hr />
<br />
<span className="second-brown">RESPONSABILIDADES Y OBLIGACIONES DE REGISTRO Y CONTRASEÑAS</span>
<p>El usufructo de las principales funciones fundamentales y objetivas del Servicio estará siempre determinado y sujeto a la existencia de un registro personal e intransferible (dígase cuenta de usuario). La Administración le exhorta a proporcionar información veraz y comprobable según fuese la misma solicitada. Más allá de un carácter intrusivo, dicha información se haría utilizar estricta y discretamente en aras de posibilitar a futuro, una resolución por parte de la Administración, de alguna situación extraordinaria que surgiese por descuidos de parte del usuario, intentando así dictar la misma en la forma menos perjudicial posible para con este. </p>
<span className="second-brown">{t('terms.h4')}</span>
<p>{t('terms.p19')}</p>
<br />
<p>Pertenece completa y estrictamente, y recae sobre el usuario la responsabilidad de mantener confidenciales y de forma segura las contraseñas establecidas para con el Servicio. Así como de las acciones a través del uso de su registro que pudiesen estar estas bajo autorización o no indistintamente por parte del usuario. Queda así también bajo responsabilidad del usuario, la notificación inmediata a la Administración si éste notase alguna actividad sospechosa, uso no autorizado o registro no concebido en el que figurase su propia cuenta como objeto de manipulación, o violación.</p>
<p>{t('terms.p20')}</p>
<br />
<p>Queda terminantemente prohibido el intercambio, comercio, compra-venta y cualquier actividad similar de cualquier forma y bajo cualquier concepto donde figurase e involucrase el conocimiento por parte de cualquiera persona ajena al usuario; de sus credenciales de acceso al Servicio. Violar dichas prohibiciones podría hacer incurrir al usuario en una falta grave, la cual podría sancionarse con penalizaciones de magnitud determinada por la Administración a su juicio y a discreción, la cual en ningún caso quedaría exenta de dicha penalización mediante alegato de uso por parte de terceros y en general bajo ninguna justificación.</p>
<p>{t('terms.p21')}</p>
<br />
<p>Los personajes del reino que hubiesen sido borrados y alcanzados estos, el límite máximo de tiempo (on-hold) para recuperación, establecido por la Administración, desaparecerán permanentemente, claudicando con ellos la posibilidad de restauración o asistencia por parte de la Administración en estos casos, independientemente y descartando por completo la voluntad del usuario para con tal acción en dicho momento.</p>
<p>{t('terms.p22')}</p>
<br />
<hr />
<br />
<span className="second-brown">HACER TRAMPA</span>
<p>Se entiende como trampa cualquier acto fuera y dentro del servidor, que intente alterar o de hecho altere e interfiera con las reglas o con el comportamiento normal del servidor. Así como intentar o de hecho aventajarse sobre otros jugadores. Entre ello, a título meramente enunciativo y sin ánimo limitativo, cualquiera de los siguientes comportamientos, ya sea en su propio nombre o por cuenta de terceros será considerado como tal:</p>
<p>- Acceder a los Servicios de manera no autorizada (incluyendo el uso de software modificado o malicioso).</p>
<p>- Utilizar varias cuentas a la vez en un mismo reino de una forma en la que controlar varios personajes al unísono y sobre el mismo reino, suponga una desventaja de cualquier índole para otros usuarios con respecto infractor.</p>
<p>- Utilizar cualquier técnica o software para alterar o manipular las variables preestablecidas por el servidor y la Administración.</p>
<span className="second-brown">{t('terms.h5')}</span>
<p>{t('terms.p23')}</p>
<p>{t('terms.p24')}</p>
<p>{t('terms.p25')}</p>
<p>{t('terms.p26')}</p>
<br />
<p>Se prohíbe terminantemente hacer trampas y la Administración se reserva el derecho de denegar a cualquier usuario el acceso al Servicio, siempre y cuando hubiere incurrido o fuese objeto de una falta de este tipo ya fuese directa e indirectamente.</p>
<p>{t('terms.p27')}</p>
<br />
<hr />
<br />
<span className="second-brown">COMPRAS, RECLAMACIONES, REEMBOLSOS Y GARANTÍAS</span>
<p>UltimoWoW no representa ni constituye entidad remesadora ni de servicios financieros de ninguna índole y en consecuencia no posee control o responsabilidad alguna sobre el origen de aquellos fondos utilizados para realizar compras hacia su comunidad. Así como tampoco se responsabiliza por los riesgos que el usuario tomase bajo su propia voluntad, a la hora de ejecutar operaciones financieras de cualquier tipo.</p>
<span className="second-brown">{t('terms.h6')}</span>
<p>{t('terms.p28')}</p>
<br />
<p>Las compras: serán consideradas siempre de fe voluntaria y en ningún caso, obligación de ningún tipo para ningún usuario. Nótese además que quedan terminantemente prohibidos las compras por parte de personas naturales menores de 18 años o dígase en cualquier caso aquellas personas que no alcanzaran aún al amparo de las entidades legales locales y pertinentes que le competan, su mayoría de edad.</p>
<p>{t('terms.p29')}</p>
<br />
<p>El Servicio: en cualquier caso, no requiere ningún tipo de compra para un uso y disfrute total y a plenitud. Dicho esto, el usuario contará con un canal de comunicación oficial vía email contacto@ultimowow.com con el objetivo de notificar a la Administración de cualquier error cometido por cualquiera de las partes, en aras de emitir y hacer efectiva justa resolución</p>
<p>{t('terms.p30')}</p>
<br />
<p>La Administración: garantiza y se compromete en brindar al comprador toda información que requiriese sobre las formas o métodos de compra vigentes para con la comunidad. Así como aclarar cualquier duda que pudiese surgir sobre los pasos o procesos establecidos de las mismas, siempre y cuando dicha información resida dentro de los límites que competan al comprador y posea un propósito únicamente informativo.</p>
<p>{t('terms.p31')}</p>
<br />
<p>Tomado en cuenta lo anterior, el comprador cuenta con garantías de reembolso monetario y de servicios, atendiendo a aspectos y condiciones expuestos a continuación.</p>
<p className="second-brown">Monetarios:</p>
<p>Tomando en cuenta el carácter que posee cualquier transacción monetaria hacia las arcas de la comunidad, la Administración se compromete a reembolsar (libre de costos por transacción, a consideración de la misma), de forma total o parcial, en definitiva, colegiada conjuntamente entre ambas partes, aquellas compras donde no intencionalmente, el usuario quedase bajo la inconveniencia a la hora de realizar la misma, de errores por su parte en la cuantía del monto total a transferir. Siendo dicho error en todos los casos veraz y de comprobable naturaleza a consideración de la administración.</p>
<p>{t('terms.p32')}</p>
<p className="second-brown">{t('terms.h7')}</p>
<p>{t('terms.p33')}</p>
<br />
<p>Tomando en cuenta que los servicios adquiridos se reciben de forma inmediata y no constituyen, determinan, pactan ni modifican plazo alguno para su uso. Así como tampoco para el usufructo del servicio y sus funcionalidades los cuales, recalcado sea de paso, se consideran completamente gratuitos, de carácter educativo y carecen de plazos de admisión y vencimiento.</p>
<p>{t('terms.p34')}</p>
<br />
<p>La Administración no garantiza ni proporcionará en ningún caso ni bajo ningún concepto o circunstancia, reembolso alguno sobre compras o cualquier bien en general transferido a la comunidad. Nótense y háganse aclaratorios lo siguientes casos:</p>
<p>- A usuarios cuyas cuentas hayan sido sancionadas temporal o permanentemente por incurrir en alguna falta expuesta en cualquiera de sus Términos y Condiciones.</p>
<p>- A usuarios que hubiesen tomado la decisión de no continuar (parcial o temporalmente) haciendo uso de cualquiera de los servicios prestados por La Administración.</p>
<p>{t('terms.p35')}</p>
<p>{t('terms.p36')}</p>
<p>{t('terms.p37')}</p>
<br />
<p>En ninguno de estos casos la Administración presentará completa ni parcialmente traza alguna de ninguno de sus registros monetarios o de cualquier tipo para fines resolutorios.</p>
<p>{t('terms.p38')}</p>
<br />
<p>Igualmente, el usuario dispone de las respectivas vías y canales, dígase dentro de los foros del Servicio, o servidor de Discord para presentar una apelación formal ante un administrador y ser así evaluado el caso para posterior resolución. La Administración se reserva el derecho de evaluar más no garantizar y a discreción, reembolsos en aquellos casos donde después de estudiado y conciliado, fuese considerado por la misma, justo y extraordinario.</p>
<p>{t('terms.p39')}</p>
<br />
<p className="second-brown">De servicios:</p>
<p>La Administración le garantiza la integridad y funcionalidad de todos los artículos comprados en su tienda. En todo caso, ésta le proveerá un término de 48 horas posterior a su compra para solicitar reemplazos por artículos o servicios de igual valor, o también y a solicitud, la devolución íntegra de los servicios utilizados en aquellos casos donde se encontrarse dicho artículo o servicio roto o inutilizable, o hubiere sido comprado o elegido por error, todo esto siempre y después de efectuado un análisis pertinente por parte de un administrador, y de demostradas pruebas auténticas y concluyentes por parte del usuario a fines con el caso.</p>
<p className="second-brown">{t('terms.h8')}</p>
<p>{t('terms.p40')}</p>
<br />
<p>Así como también la Administración garantizará la restauración del servicio por problemas técnicos y puntuales, entre el Servicio y las plataformas de pago siempre ajenos al comprador, no fuesen entregados dichos servicios de forma automática e inmediata a nombre del mismo. </p>
<p>{t('terms.p41')}</p>
<br />
<hr />
<br />
<span className="second-brown">POLÍTICA DE PRIVACIDAD</span>
<p>El Servicio y la Administración recopilan información referente al usuario tales como dirección de correo electrónico, nombre, contraseña, idioma y zona horaria, con el fin de brindarle una mejor optimización cada vez a sus servicios, utilizando esta información para acomodar las funcionalidades a un mejor rendimiento a fin con el usuario y sus requerimientos. Dicha información también podrá ser usada para mantener al usuario mediante canales de comunicaciones independientes y no referentes al Servicio, informando sobre noticias importantes, actualizaciones, problemas técnicos o cualquier otra información de tal índole que considerase la Administración relevante a los intereses del usuario. </p>
<span className="second-brown">{t('terms.h9')}</span>
<p>{t('terms.p42')}</p>
<br />
<p>La Administración y el Servicio se comprometen a salvaguardar la confidencialidad sobre tales datos, así como a garantizar la NO divulgación de estos, exceptuando los casos donde las leyes vigentes que rigiesen sobre la Administración lo consideraran necesario y mandatorio por motivos no referentes a UltimoWoW. </p>
<p>{t('terms.p43')}</p>
<br />
<p className="grey-info2">Error 404</p>
<p className="grey-info2">info@ultimowow.com</p>
+35 -31
View File
@@ -1,5 +1,5 @@
import type { Metadata } from 'next'
import { setRequestLocale } from 'next-intl/server'
import { getTranslations, setRequestLocale } from 'next-intl/server'
import { redirect, Link } from '@/i18n/navigation'
import { getSession } from '@/lib/session'
import { getGameCharacters } from '@/lib/characters'
@@ -14,6 +14,7 @@ export const metadata: Metadata = { title: 'Comercio de PD' }
export default async function TradePointsPage({ params }: { params: Promise<{ locale: string }> }) {
const { locale } = await params
setRequestLocale(locale)
const t = await getTranslations('Points')
const session = await getSession()
if (!session.bnetId) redirect({ href: '/login', locale })
@@ -22,59 +23,62 @@ export default async function TradePointsPage({ params }: { params: Promise<{ lo
const chars = await getGameCharacters(session.accountId!)
return (
<PageShell title="Comercio de PD">
<PageShell title={t('trade.title')}>
<ServiceBox>
<br />
<p>
La herramienta <span>Comercio de PD</span> permite comprar o vender PD a cambio de oro de una forma segura.
{t.rich('trade.intro1', { s: (c) => <span>{c}</span> })}
</p>
<br />
<p>El sistema permite crear códigos de 1 solo uso que valdrán X cantidad de PD a cambio de X cantidad de oro.</p>
<p>{t('trade.intro2')}</p>
<p>
Los vendedores disponen de la sección VENTA para crear el código y los compradores disponen de la sección
COMPRA para canjear dicho código.
{t('trade.intro3')}
</p>
<br />
<div className="trade-information">
<h2>Venta</h2>
<p>1) Decide cuánto valdrá el código en PD.</p>
<p>2) Decide por cuánto oro se podrá canjear.</p>
<p>3) Escoge a qué personaje de la cuenta se enviará el oro cuando el código sea canjeado.</p>
<p>4) Completa los datos con la contraseña y Token de Seguridad.</p>
<p>5) Usa el botón CREAR CÓDIGO.</p>
<p>6) Un recuadro aparecerá con los datos del Código creado con el ID que se debe usar para enviar a otro jugador.</p>
<h2>{t('trade.sellTitle')}</h2>
<p>{t('trade.sell1')}</p>
<p>{t('trade.sell2')}</p>
<p>{t('trade.sell3')}</p>
<p>{t('trade.sell4')}</p>
<p>{t('trade.sell5')}</p>
<p>{t('trade.sell6')}</p>
<br />
</div>
<div className="trade-information">
<h2>Compra</h2>
<p>1) Ingresa ID del código que te ha dado otro jugador.</p>
<p>2) Chequea el valor en PD y costo en oro con el botón CHEQUEAR CÓDIGO.</p>
<p>3) Escoge de qué personaje de la cuenta se descontará el oro cuando el código sea canjeado.</p>
<p>4) Completa los datos con la contraseña y Token de Seguridad.</p>
<p>5) Usa el botón CANJEAR CÓDIGO.</p>
<p>6) Un recuadro aparecerá con los datos del Código canjeado, PD recibidos y oro retirado del personaje seleccionado.</p>
<h2>{t('trade.buyTitle')}</h2>
<p>{t('trade.buy1')}</p>
<p>{t('trade.buy2')}</p>
<p>{t('trade.buy3')}</p>
<p>{t('trade.buy4')}</p>
<p>{t('trade.buy5')}</p>
<p>{t('trade.buy6')}</p>
</div>
<br />
<br />
<p>Cada código tiene una duración de 1 hora.</p>
<p>Una vez canjeado el código:</p>
<p>- Los PD serán retirados de la cuenta del creador del código y enviados a quien lo haya canjeado.</p>
<p>- El oro será retirado y enviado por correo al creador.</p>
<p>- La transacción quedará registrada en el Historial de PD y PV.</p>
<p>{t('trade.duration')}</p>
<p>{t('trade.onceRedeemed')}</p>
<p>{t('trade.redeemed1')}</p>
<p>{t('trade.redeemed2')}</p>
<p>{t('trade.redeemed3')}</p>
<br />
<p>La cuenta de quien canjee el código será bloqueada por 5s como medida de seguridad si el trámite es exitoso.</p>
<p>{t('trade.lockNote')}</p>
<br />
<p>
Si aún no tienes Token de seguridad, puedes solicitarlo{' '}
<Link href="/security-token" target="_blank">aquí</Link>.
{t.rich('trade.tokenHelp', {
link: (c) => (
<Link href="/security-token" target="_blank">
{c}
</Link>
),
})}
</p>
<br />
<fieldset>
<legend>NOTA</legend>
<legend>{t('trade.note')}</legend>
<div className="separate2">
<p>
Por favor, asegúrate de haber escrito todos los datos correctos ya que esta acción no es reversible una
vez realizada.
{t('trade.noteText')}
</p>
</div>
</fieldset>
+20 -19
View File
@@ -1,5 +1,5 @@
import type { Metadata } from 'next'
import { setRequestLocale } from 'next-intl/server'
import { setRequestLocale, getTranslations } from 'next-intl/server'
import { redirect } from '@/i18n/navigation'
import { getSession } from '@/lib/session'
import { getPaymentTransactions, type PaymentTx } from '@/lib/trans-history'
@@ -21,7 +21,7 @@ function StatusBadge({ status }: { status: PaymentTx['status'] }) {
}
/** Caja de una pasarela con su tabla de transacciones (o "No hay movimientos"). */
function PlatformBox({
async function PlatformBox({
name,
logo,
note,
@@ -32,6 +32,7 @@ function PlatformBox({
note: string
txs: PaymentTx[]
}) {
const t = await getTranslations('History')
return (
<div className="box-content">
<div className="title-box-content">
@@ -41,12 +42,11 @@ function PlatformBox({
</div>
<div className="body-box-content">
<p>
Recibirás los PD correspondientes a cada transacción sólo cuando esté aprobada y el estado sea{' '}
<span className="green-info2">PAID</span> (Pagado).
{t.rich('trans.paidNote', { s: (c) => <span className="green-info2">{c}</span> })}
</p>
<br />
<p>
<span className="red-info2">Importante:</span> {note}
<span className="red-info2">{t('trans.important')}</span> {note}
</p>
<br />
{txs.length === 0 ? (
@@ -54,7 +54,7 @@ function PlatformBox({
<tbody>
<tr>
<td>
<span>No hay movimientos</span>
<span>{t('trans.noMovements')}</span>
</td>
</tr>
</tbody>
@@ -63,11 +63,11 @@ function PlatformBox({
<table className="max-center-table-aligned">
<tbody>
<tr>
<th>ID de transacción</th>
<th>Estado</th>
<th className="responsive-td">Concepto</th>
<th>Fecha creación</th>
<th>Cantidad</th>
<th>{t('trans.thTxId')}</th>
<th>{t('trans.thStatus')}</th>
<th className="responsive-td">{t('trans.thConcept')}</th>
<th>{t('trans.thCreatedAt')}</th>
<th>{t('trans.thAmount')}</th>
</tr>
{txs.map((t) => (
<tr key={t.id}>
@@ -101,6 +101,7 @@ const LOGO_BASE = '/nw-themes/nw-ryu/nw-images/nw-logos'
export default async function TransHistoryPage({ params }: { params: Promise<{ locale: string }> }) {
const { locale } = await params
setRequestLocale(locale)
const t = await getTranslations('History')
const session = await getSession()
if (!session.bnetId) redirect({ href: '/login', locale })
@@ -111,23 +112,23 @@ export default async function TransHistoryPage({ params }: { params: Promise<{ l
const sumup = txs.filter((t) => t.platform === 'SumUp')
return (
<PageShell title="Historial de transacciones">
<PageShell title={t('trans.pageTitle')}>
<ServiceBox>
<br />
<p>Aquí verás la información que recibimos de nuestras diferentes plataformas de pago automáticas.</p>
<p>{t('trans.intro1')}</p>
<br />
<p>¿Has tenido algún problema con alguna transacción?</p>
<p>{t('trans.intro2')}</p>
<p>
Si has tenido un problema con una de las transacciones o no has recibido tus PD, puedes contactarnos en
nuestro Discord o a través del correo{' '}
<a href="mailto:contacto@ultimowow.com">contacto@ultimowow.com</a>.
{t.rich('trans.intro3', {
mail: (c) => <a href="mailto:contacto@ultimowow.com">{c}</a>,
})}
</p>
</ServiceBox>
<br />
<PlatformBox
name="Stripe"
logo={`${LOGO_BASE}/stripe-p-logo.svg`}
note="Los pagos con tarjeta se aprueban normalmente en unos segundos."
note={t('trans.noteStripe')}
txs={stripe}
/>
<br />
@@ -136,7 +137,7 @@ export default async function TransHistoryPage({ params }: { params: Promise<{ l
<PlatformBox
name="SumUp"
logo={`${LOGO_BASE}/sumup-p-logo.svg`}
note="SumUp puede tardar unos minutos en confirmar el pago tras completar el checkout."
note={t('trans.noteSumup')}
txs={sumup}
/>
</PageShell>
@@ -1,5 +1,6 @@
import type { Metadata } from 'next'
import { setRequestLocale } from 'next-intl/server'
import { NoteLegend } from '@/components/NoteLegend'
import { getTranslations, setRequestLocale } from 'next-intl/server'
import { redirect, Link } from '@/i18n/navigation'
import { getSession } from '@/lib/session'
import { getGameCharacters } from '@/lib/characters'
@@ -15,6 +16,7 @@ export const metadata: Metadata = { title: 'Transferir personaje' }
export default async function TransferCharacterPage({ params }: { params: Promise<{ locale: string }> }) {
const { locale } = await params
setRequestLocale(locale)
const t = await getTranslations('CharService')
const session = await getSession()
if (!session.bnetId) redirect({ href: '/login', locale })
@@ -23,35 +25,33 @@ export default async function TransferCharacterPage({ params }: { params: Promis
const [chars, price] = await Promise.all([getGameCharacters(session.accountId!), getTransferPrice()])
return (
<PageShell title="Transferir personaje">
<PageShell title={t('transfer.pageTitle')}>
<ServiceBox>
<br />
<p>
La herramienta <span>Transferir personaje</span> te permite mover un personaje a otra cuenta.
{t.rich('transfer.intro', { s: (c) => <span>{c}</span> })}
</p>
<br />
<p>Para personajes <span className="death-knight">Caballeros de la Muerte</span>:</p>
<p>- La cuenta destino debe tener aunque sea un personaje con nivel 55 o superior.</p>
<p>- La cuenta destino no puede tener un personaje Caballero de la Muerte.</p>
<p>{t.rich('transfer.dkHeading', { dk: (c) => <span className="death-knight">{c}</span> })}</p>
<p>{t('transfer.dkCond1')}</p>
<p>{t('transfer.dkCond2')}</p>
<br />
<p>Tu cuenta será bloqueada por 5s como medida de seguridad si el trámite es exitoso.</p>
<p>{t('transfer.securityLock')}</p>
<br />
<p>
Al usar el botón TRANSFERIR PERSONAJE del personaje que hayas escogido, se transferirá desde tu cuenta a la
cuenta que hayas indicado.
{t('transfer.buttonExplain')}
</p>
<p>Cuando ingreses a la lista de personajes de la cuenta destino, verás al personaje transferido.</p>
<p>{t('transfer.afterTransfer')}</p>
<br />
<p>Si aún no tienes Token de seguridad, puedes solicitarlo <Link href="/security-token" target="_blank">aquí</Link>.</p>
<p>Si aún no tienes 2FA, puedes solicitarlo <Link href="/security-2falogin" target="_blank">aquí</Link>.</p>
<p>{t.rich('transfer.securityTokenHint', { link: (c) => <Link href="/security-token" target="_blank">{c}</Link> })}</p>
<p>{t.rich('transfer.twoFaHint', { link: (c) => <Link href="/security-2falogin" target="_blank">{c}</Link> })}</p>
<br />
<fieldset>
<legend>NOTA</legend>
<NoteLegend />
<div className="separate2">
<p>Se requiere que el personaje esté desconectado.</p>
<p>{t('transfer.noteOffline')}</p>
<p>
Por favor, asegúrate de haber escogido al personaje correcto ya que esta acción no es reversible una vez
realizada.
{t('transfer.noteIrreversible')}
</p>
</div>
</fieldset>
@@ -59,7 +59,7 @@ export default async function TransferCharacterPage({ params }: { params: Promis
<div className="centered">
<br />
<br />
<p>Requiere <span>{price}</span> <span className="yellow-info"></span> (SumUp)</p>
<p>{t.rich('transfer.requires', { price, s: (c) => <span>{c}</span>, e: (c) => <span className="yellow-info">{c}</span> })}</p>
</div>
<TransferForm characters={chars.map((c) => ({ name: c.name, classCss: c.classCss }))} price={price} />
@@ -1,5 +1,5 @@
import type { Metadata } from 'next'
import { setRequestLocale } from 'next-intl/server'
import { getTranslations, setRequestLocale } from 'next-intl/server'
import { redirect, Link } from '@/i18n/navigation'
import { getSession } from '@/lib/session'
import { getDPointsBalance } from '@/lib/dpoints'
@@ -14,6 +14,7 @@ export const metadata: Metadata = { title: 'Transferir PD' }
export default async function TransferDPointsPage({ params }: { params: Promise<{ locale: string }> }) {
const { locale } = await params
setRequestLocale(locale)
const t = await getTranslations('Points')
const session = await getSession()
if (!session.bnetId) redirect({ href: '/login', locale })
@@ -22,29 +23,32 @@ export default async function TransferDPointsPage({ params }: { params: Promise<
const balance = await getDPointsBalance(session.accountId!)
return (
<PageShell title="Transferir PD">
<PageShell title={t('transferDp.title')}>
<ServiceBox>
<br />
<p>
La herramienta <span>Transferir PD</span> permite transferir PD desde tu cuenta a cualquier otra cuenta.
{t.rich('transferDp.intro1', { s: (c) => <span>{c}</span> })}
</p>
<br />
<p>
Al usar el botón TRANSFERIR PD se transferirá la cantidad que hayas escogido desde tu cuenta a la cuenta del
personaje que hayas indicado.
{t('transferDp.intro2')}
</p>
<br />
<p>
Si aún no tienes Token de seguridad, puedes solicitarlo{' '}
<Link href="/security-token" target="_blank">aquí</Link>.
{t.rich('transferDp.tokenHelp', {
link: (c) => (
<Link href="/security-token" target="_blank">
{c}
</Link>
),
})}
</p>
<br />
<fieldset>
<legend>NOTA</legend>
<legend>{t('transferDp.note')}</legend>
<div className="separate2">
<p>
Por favor, asegúrate de haber escrito el nombre del personaje correcto ya que esta acción no es reversible
una vez realizada.
{t('transferDp.noteText')}
</p>
</div>
</fieldset>
@@ -1,4 +1,5 @@
import { getTranslations, setRequestLocale } from 'next-intl/server'
import { NoteLegend } from '@/components/NoteLegend'
import { redirect } from '@/i18n/navigation'
import { getSession } from '@/lib/session'
import { getGameCharacters } from '@/lib/characters'
@@ -25,7 +26,7 @@ export default async function UnstuckPage({ params }: { params: Promise<{ locale
<p>{t('unstuckInfo2')}</p>
<br />
<fieldset>
<legend>NOTA</legend>
<NoteLegend />
<div className="separate2">
<p>{t('unstuckNote1')}</p>
<p>{t('unstuckNote2')}</p>
+60 -58
View File
@@ -1,6 +1,7 @@
'use client'
import { useRef, useState } from 'react'
import { useTranslations } from 'next-intl'
// Lista de trackers común a todos los magnet (idéntica en el original).
const TRACKERS =
@@ -17,14 +18,15 @@ function magnetFor(lang: string): string {
return `magnet:?xt=urn:btih:${BTIH[lang]}${TRACKERS}`
}
const LANG_INFO: Record<string, string> = {
wotlkeses: 'Idioma esES: Español de España.',
wotlkesmx: 'Idioma esMX: Español de Latinoamérica.',
wotlkenus: 'Idioma enUS: Inglés de EEUU.',
wotlkengb: 'Idioma enGB: Inglés británico.',
const LANG_INFO_KEY: Record<string, string> = {
wotlkeses: 'clientDownload.langEses',
wotlkesmx: 'clientDownload.langEsmx',
wotlkenus: 'clientDownload.langEnus',
wotlkengb: 'clientDownload.langEngb',
}
export function ClientDownload() {
const t = useTranslations('UI')
const [lang, setLang] = useState('')
const [os, setOs] = useState('')
const [error, setError] = useState(false)
@@ -45,45 +47,45 @@ export function ClientDownload() {
<>
<div className="box-content">
<div className="title-box-content" id="torrent">
<h2>Descarga por Torrent</h2>
<h2>{t('clientDownload.torrentTitle')}</h2>
</div>
<div className="body-box-content justified">
<p><span>Información</span></p>
<p>- No requiere instalación.</p>
<p>- El realmlist ya está configurado.</p>
<p>- La descarga se gestiona a través de magnet link. Si ya conoces cómo realizar descargas de este tipo, los pasos a seguir son opcionales.</p>
<p><span>{t('clientDownload.infoLabel')}</span></p>
<p>{t('clientDownload.info1')}</p>
<p>{t('clientDownload.info2')}</p>
<p>{t('clientDownload.info3')}</p>
<br />
<p><span>Requisitos para la descarga</span></p>
<p>Se necesitará un programa que gestione descargas por Torrent y un programa que gestione archivos .rar.</p>
<p><span>{t('clientDownload.requirementsLabel')}</span></p>
<p>{t('clientDownload.requirementsText')}</p>
<ul>
<li><a href="https://www.qbittorrent.org/download.php" target="_blank" rel="noopener noreferrer">qBittorrent</a></li>
<li><a href="https://www.winrar.es/descargas" target="_blank" rel="noopener noreferrer">WinRAR</a></li>
</ul>
<p>Adicionalmente, necesitarás que esta página pueda abrir ventanas emergentes.</p>
<p>{t('clientDownload.popupNote')}</p>
<br />
<p><span>Paso a paso:</span></p>
<p>Opcional: Si no tienes Winrar, haz clic en <a href="https://www.winrar.es/descargas" target="_blank" rel="noopener noreferrer">WinRAR</a> e instala el programa.</p>
<p>1) Haz clic en <a href="https://www.qbittorrent.org/download.php" target="_blank" rel="noopener noreferrer">qBittorrent</a>.</p>
<p>2) Descarga la versión adecuada para tu sistema operativo.</p>
<p>3) Ejecuta el archivo descargado y realiza la instalacion del programa.</p>
<p>4) Ejecuta qBitTorrent.</p>
<p>5) Ve a la parte final de esta página y escoge el idioma de tu preferencia.</p>
<p>6) Debajo del idioma, escoge el sistema operativo.</p>
<p>7) Haz clic en Descargar.</p>
<p>8) En la ventana emergente, acepta Abrir qBitTorrent.</p>
<p>9) Espera a que qBitTorrent cargue el archivo correspondiente en la ventana de Enlace magnet.</p>
<p>10) Haz clic en Aceptar.</p>
<p>11) En la ventana de qBitTorrent espera a que la descarga se haya completado.</p>
<p>12) Ingresa en la carpeta de descargas de qBitTorrent (Por defecto Descargas).</p>
<p>13) Haz clic derecho en el archivo .rar y usa la opción Extraer aquí.</p>
<p>14) Ingresa en la carpeta extraída y allí encontrarás el ejecutable para comenzar a jugar.</p>
<p>15) Conecta usando tu cuenta de la web, en el campo email no pongas tu email sino tu nombre de cuenta.</p>
<p><span>{t('clientDownload.stepByStep')}</span></p>
<p>{t.rich('clientDownload.stepOptional', { winrar: (c) => <a href="https://www.winrar.es/descargas" target="_blank" rel="noopener noreferrer">{c}</a> })}</p>
<p>{t.rich('clientDownload.step1', { qbit: (c) => <a href="https://www.qbittorrent.org/download.php" target="_blank" rel="noopener noreferrer">{c}</a> })}</p>
<p>{t('clientDownload.step2')}</p>
<p>{t('clientDownload.step3')}</p>
<p>{t('clientDownload.step4')}</p>
<p>{t('clientDownload.step5')}</p>
<p>{t('clientDownload.step6')}</p>
<p>{t('clientDownload.step7')}</p>
<p>{t('clientDownload.step8')}</p>
<p>{t('clientDownload.step9')}</p>
<p>{t('clientDownload.step10')}</p>
<p>{t('clientDownload.step11')}</p>
<p>{t('clientDownload.step12')}</p>
<p>{t('clientDownload.step13')}</p>
<p>{t('clientDownload.step14')}</p>
<p>{t('clientDownload.step15')}</p>
<br />
<hr />
<br />
<div className="centered">
<form name="uw-client-dowload" id="uw-client-download" autoComplete="off" onSubmit={handleSubmit}>
<p><span>Idioma</span></p>
<p><span>{t('clientDownload.languageLabel')}</span></p>
<br />
<select
name="game-language"
@@ -92,19 +94,19 @@ export function ClientDownload() {
value={lang}
onChange={(e) => { setLang(e.target.value); setOs(''); }}
>
<option value="" disabled>Elegir idioma</option>
<option value="wotlkeses">Cliente esES</option>
<option value="wotlkesmx">Cliente esMX</option>
<option value="wotlkenus">Cliente enUS</option>
<option value="wotlkengb">Cliente enGB</option>
<option value="" disabled>{t('clientDownload.chooseLanguage')}</option>
<option value="wotlkeses">{t('clientDownload.clientEses')}</option>
<option value="wotlkesmx">{t('clientDownload.clientEsmx')}</option>
<option value="wotlkenus">{t('clientDownload.clientEnus')}</option>
<option value="wotlkengb">{t('clientDownload.clientEngb')}</option>
</select>
{lang && (
<div id="select-op">
<br />
<p id="game-info">{LANG_INFO[lang] ?? ' '}</p>
<p id="game-info">{lang ? t(LANG_INFO_KEY[lang]) :' '}</p>
<br />
<p><span>Sistema operativo</span></p>
<p><span>{t('clientDownload.osLabel')}</span></p>
<br />
<select
name="operating-system"
@@ -113,19 +115,19 @@ export function ClientDownload() {
value={os}
onChange={(e) => setOs(e.target.value)}
>
<option value="" disabled>Elegir sistema operativo</option>
<option value="windows">Windows</option>
<option value="macosc">Mac OS Catalina</option>
<option value="" disabled>{t('clientDownload.chooseOs')}</option>
<option value="windows">{t('clientDownload.windows')}</option>
<option value="macosc">{t('clientDownload.macCatalina')}</option>
</select>
<br />
<button type="submit" className="g-download-button" id="download-c-button">DESCARGAR</button>
<button type="submit" className="g-download-button" id="download-c-button">{t('clientDownload.downloadBtn')}</button>
</div>
)}
</form>
<br />
<hr />
<div id="error-message" className="red-form-response">
{error ? 'Debes escoger un cliente y un sistema operativo.' : ''}
{error ? t('clientDownload.errorChoose') : ''}
</div>
<br />
</div>
@@ -134,24 +136,24 @@ export function ClientDownload() {
{os === 'macosc' && (
<div id="op-info" className="box-content">
<div className="title-box-content">
<h2>Guía para ejecutar el cliente en mac OS Catalina</h2>
<h2>{t('clientDownload.macGuideTitle')}</h2>
</div>
<div className="body-box-content">
<p>macOS Catalina ya no soporta aplicaciones que trabajan en 32bits, y el cliente se ejecuta en 32bits.</p>
<p>Siguiendo los pasos a continuación podrás disfrutar UltimoWoW sin problemas en macOS Catalina.</p>
<p>{t('clientDownload.mac1')}</p>
<p>{t('clientDownload.mac2')}</p>
<br />
<span>Instrucciones</span>
<p>1 - Descarga e instala <a href="https://insmac.org/macosx/3728-crossover-19.html" target="_blank" rel="noopener noreferrer">CrossOver</a>.</p>
<p>2 - Una vez que tengas descargado el cliente, ejecuta CrossOver y haz clic en &quot;Instalar una aplicación para Windows&quot;.</p>
<p>3 - En el campo &quot;Seleccionar una aplicación para instalar&quot;, busca la descarga, selecciónalo y haz clic en continuar. (No hagas clic en instalar aún).</p>
<p>4 - Ve a la pestaña donde dice &quot;Seleccionar instalador&quot;, haz clic en &quot;Elegir archivo de instalador&quot;, y ahora busca WoW.exe y selecciónalo.</p>
<p>5 - En la pestaña Botella, puedes seleccionar Windows 7 64bits.</p>
<p>6 - Ve a la pestaña &quot;Instalar y finalizar&quot;, haz clic en Instalar, se instalarán todas las fuentes, archivos y Visual Redistributables necesarios que WoW usa en un sistema operativo Windows estándar.</p>
<p>7 - Te pedirá instalar y aceptar los FONTS, Microsoft Visual C ++, haz clic en para cada solicitud que aparezca.</p>
<p>8 - El cliente se iniciará automáticamente.</p>
<p>9 - Una vez que termines y salgas del cliente, haz clic en &quot;Listo&quot; en la aplicación CrossOver.</p>
<p>10 - La próxima vez que desees volver, abre la aplicación CrossOver, mira el panel izquierdo y selecciona en Botella el WoW que se creó anteriormente, y haz clic en RUN COMMAND, selecciona WoW.exe y haz clic en ¨Guardar comando como iniciador¨, luego haz clic en Abrir.</p>
<p>11 - Finalmente, verás el ícono de WoW debajo de la pestaña Botella de WoW, ahora puedes arrastrar el lanzador de WoW al Dock, de esta manera, la próxima vez que quieras entrar, simplemente ejecuta WoW desde el Dock.</p>
<span>{t('clientDownload.macInstructions')}</span>
<p>{t.rich('clientDownload.macStep1', { crossover: (c) => <a href="https://insmac.org/macosx/3728-crossover-19.html" target="_blank" rel="noopener noreferrer">{c}</a> })}</p>
<p>{t('clientDownload.macStep2')}</p>
<p>{t('clientDownload.macStep3')}</p>
<p>{t('clientDownload.macStep4')}</p>
<p>{t('clientDownload.macStep5')}</p>
<p>{t('clientDownload.macStep6')}</p>
<p>{t('clientDownload.macStep7')}</p>
<p>{t('clientDownload.macStep8')}</p>
<p>{t('clientDownload.macStep9')}</p>
<p>{t('clientDownload.macStep10')}</p>
<p>{t('clientDownload.macStep11')}</p>
</div>
</div>
)}
+31 -22
View File
@@ -1,6 +1,7 @@
'use client'
import { useEffect, useState } from 'react'
import { useTranslations } from 'next-intl'
import { Link } from '@/i18n/navigation'
// ---------------------------------------------------------------------------
@@ -93,6 +94,7 @@ function revokeConsent() {
// Banner de consentimiento (fijo abajo, en todo el sitio).
// ---------------------------------------------------------------------------
export function CookieBanner() {
const t = useTranslations('UI')
const [visible, setVisible] = useState(false)
const [details, setDetails] = useState(false)
// Estado de los checkboxes granulares (categorías no necesarias).
@@ -129,45 +131,45 @@ export function CookieBanner() {
}
return (
<div className="uw-cookie-content" role="dialog" aria-label="Consentimiento de cookies">
<div className="uw-cookie-content" role="dialog" aria-label={t('cookie.dialogLabel')}>
<div className="uw-cookie-text">
<h3>Utilizamos cookies</h3>
<p>Este sitio utiliza cookies propias y de terceros para garantizar el funcionamiento del sitio, analizar el tráfico y ofrecerte publicidad personalizada. Consulta nuestra política de cookies para más información.</p>
<Link href="/cookies" className="uw-cookie-link">Política de Cookies</Link>
<h3>{t('cookie.title')}</h3>
<p>{t('cookie.description')}</p>
<Link href="/cookies" className="uw-cookie-link">{t('cookie.policyLink')}</Link>
{details && (
<div className="uw-cookie-details" style={{ display: 'block' }}>
<div className="uw-cookie-category">
<label>
<input type="checkbox" checked disabled readOnly />
<span className="uw-cookie-cat-name">Necesarias</span>
<span className="uw-cookie-cat-name">{t('cookie.catNecessary')}</span>
</label>
<p>Esenciales para el funcionamiento del sitio. No se pueden desactivar.</p>
<p>{t('cookie.descNecessary')}</p>
</div>
<div className="uw-cookie-category">
<label>
<input type="checkbox" id="uw-cookie-analytics" checked={!!prefs.analytics} onChange={(e) => setPrefs((s) => ({ ...s, analytics: e.target.checked }))} />
<span className="uw-cookie-cat-name">Analíticas</span>
<span className="uw-cookie-cat-name">{t('cookie.catAnalytics')}</span>
</label>
<p>Nos ayudan a entender cómo usas el sitio para mejorarlo.</p>
<p>{t('cookie.descAnalytics')}</p>
</div>
<div className="uw-cookie-category">
<label>
<input type="checkbox" id="uw-cookie-marketing" checked={!!prefs.marketing} onChange={(e) => setPrefs((s) => ({ ...s, marketing: e.target.checked }))} />
<span className="uw-cookie-cat-name">Marketing</span>
<span className="uw-cookie-cat-name">{t('cookie.catMarketing')}</span>
</label>
<p>Se utilizan para mostrarte anuncios relevantes.</p>
<p>{t('cookie.descMarketing')}</p>
</div>
<div className="uw-cookie-actions-detail">
<button type="button" className="uw-cookie-btn uw-cookie-btn-save" data-action="save" onClick={savePrefs}>Guardar preferencias</button>
<button type="button" className="uw-cookie-btn uw-cookie-btn-save" data-action="save" onClick={savePrefs}>{t('cookie.savePrefs')}</button>
</div>
</div>
)}
</div>
<div className="uw-cookie-actions">
<button type="button" className="uw-cookie-btn uw-cookie-btn-reject" data-action="reject" onClick={() => { rejectAll(); setVisible(false) }}>Rechazar</button>
<button type="button" className="uw-cookie-btn uw-cookie-btn-customize" data-action="customize" onClick={() => setDetails((v) => !v)}>Personalizar</button>
<button type="button" className="uw-cookie-btn uw-cookie-btn-accept" data-action="accept" onClick={() => { acceptAll(); setVisible(false) }}>Aceptar todas</button>
<button type="button" className="uw-cookie-btn uw-cookie-btn-reject" data-action="reject" onClick={() => { rejectAll(); setVisible(false) }}>{t('cookie.reject')}</button>
<button type="button" className="uw-cookie-btn uw-cookie-btn-customize" data-action="customize" onClick={() => setDetails((v) => !v)}>{t('cookie.customize')}</button>
<button type="button" className="uw-cookie-btn uw-cookie-btn-accept" data-action="accept" onClick={() => { acceptAll(); setVisible(false) }}>{t('cookie.acceptAll')}</button>
</div>
</div>
)
@@ -177,16 +179,17 @@ export function CookieBanner() {
// Botones «Cambiar / Retirar consentimiento» de la página /cookies.
// ---------------------------------------------------------------------------
export function CookieConsentButtons() {
const t = useTranslations('UI')
useEffect(() => {
window.UWCookies = { showBanner, revokeConsent, acceptAll, rejectAll }
}, [])
return (
<div className="uw-consent-buttons">
<button type="button" onClick={showBanner} className="btn-uw btn-uw-form">
Cambiar consentimiento
{t('cookie.changeConsent')}
</button>
<button type="button" onClick={revokeConsent} className="btn-uw btn-uw-form btn-uw-secondary">
Retirar consentimiento
{t('cookie.revokeConsent')}
</button>
</div>
)
@@ -197,6 +200,7 @@ export function CookieConsentButtons() {
// (o, si Zaraz no está, la preferencia guardada en la cookie uw_cookie_consent).
// ---------------------------------------------------------------------------
export function ConsentStatus() {
const t = useTranslations('UI')
const [state, setState] = useState<{ source: string; values: Consent | null }>({
source: 'cookie',
values: null,
@@ -224,15 +228,20 @@ export function ConsentStatus() {
const zarazData = state.source === 'zaraz' && state.values
const values = state.values
const catLabel: Record<string, string> = {
necessary: t('cookie.catNecessary'),
analytics: t('cookie.catAnalytics'),
marketing: t('cookie.catMarketing'),
}
return (
<div className="uw-consent-status">
<p className="uw-consent-source">
{zarazData
? 'Estado actual (Cloudflare Zaraz Consent API):'
? t('cookie.statusZaraz')
: values
? 'Estado actual (preferencia guardada):'
: 'Cloudflare Zaraz no está disponible en este dominio; aún no hay preferencia guardada.'}
? t('cookie.statusSaved')
: t('cookie.statusUnavailable')}
</p>
<ul>
{zarazData ? (
@@ -241,7 +250,7 @@ export function ConsentStatus() {
<li key={purpose}>
<span>{purpose}</span>
<span className={granted ? 'uw-consent-on' : 'uw-consent-off'}>
{granted ? 'Aceptada' : 'Rechazada'}
{granted ? t('cookie.accepted') : t('cookie.rejected')}
</span>
</li>
))
@@ -251,9 +260,9 @@ export function ConsentStatus() {
const on = cat.always || Boolean(values?.[cat.key])
return (
<li key={cat.key}>
<span>{cat.label}</span>
<span>{catLabel[cat.key] ?? cat.label}</span>
<span className={cat.always ? 'uw-consent-always' : on ? 'uw-consent-on' : 'uw-consent-off'}>
{cat.always ? 'Siempre activas' : values ? (on ? 'Aceptada' : 'Rechazada') : '—'}
{cat.always ? t('cookie.alwaysOn') : values ? (on ? t('cookie.accepted') : t('cookie.rejected')) : '—'}
</span>
</li>
)
+95 -76
View File
@@ -1,6 +1,7 @@
'use client'
import { useState } from 'react'
import { useTranslations } from 'next-intl'
import { Link } from '@/i18n/navigation'
const LOGOS = '/nw-themes/nw-ryu/nw-images/nw-logos'
@@ -8,24 +9,25 @@ const RANKS = '/nw-themes/nw-ryu/nw-images/nw-ranks'
type TabId = 'Info' | 'Stripe' | 'SumUp'
const RANK_ROWS = [
['1_Newbie', 'Nivel 1', 'Desde 0 PD'],
['2_Rookie', 'Nivel 2', 'Hasta 100 PD'],
['3_Apprentice', 'Nivel 3', 'Hasta 200 PD'],
['4_Explorer', 'Nivel 4', 'Hasta 300 PD'],
['5_Contributor', 'Nivel 5', 'Hasta 400 PD'],
['6_Enthusiast', 'Nivel 6', 'Hasta 500 PD'],
['7_Collaborator', 'Nivel 7', 'Hasta 600 PD'],
['8_Regular', 'Nivel 8', 'Hasta 700 PD'],
['9_RisingStar', 'Nivel 9', 'Hasta 800 PD'],
['10_Proficient', 'Nivel 10', 'Hasta 900 PD'],
['11_Experienced', 'Nivel 11', 'Hasta 1000 PD'],
['12_Mentor', 'Nivel 12', 'Hasta 5000 PD'],
['13_Veteran', 'Nivel 13', 'Hasta 10000 PD'],
['14_GrandMaster', 'Nivel 14', 'Más de 10000 PD'],
const RANK_ROWS: [string, number, string][] = [
['1_Newbie', 1, 'range1'],
['2_Rookie', 2, 'range2'],
['3_Apprentice', 3, 'range3'],
['4_Explorer', 4, 'range4'],
['5_Contributor', 5, 'range5'],
['6_Enthusiast', 6, 'range6'],
['7_Collaborator', 7, 'range7'],
['8_Regular', 8, 'range8'],
['9_RisingStar', 9, 'range9'],
['10_Proficient', 10, 'range10'],
['11_Experienced', 11, 'range11'],
['12_Mentor', 12, 'range12'],
['13_Veteran', 13, 'range13'],
['14_GrandMaster', 14, 'range14'],
]
export function DPointsTabs({ realm, currency }: { realm: string; currency: string }) {
const t = useTranslations('Points')
const [active, setActive] = useState<TabId>('Info')
const [showRanks, setShowRanks] = useState(false)
const sym = currency === 'usd' ? '$' : '€'
@@ -38,10 +40,10 @@ export function DPointsTabs({ realm, currency }: { realm: string; currency: stri
className={`dnt-button-2 tablink${active === 'Info' ? ' dnt-selected' : ''}`}
onClick={() => setActive('Info')}
>
Información
{t('tabs.info')}
</button>
<Link className="back-to-account" href="/my-account">Regresar a mi cuenta</Link>
<Link className="back-to-account-responsive" href="/my-account">Regresar</Link>
<Link className="back-to-account" href="/my-account">{t('tabs.backToAccount')}</Link>
<Link className="back-to-account-responsive" href="/my-account">{t('tabs.back')}</Link>
</div>
<div className="dnt-sidebar dnt-methods">
<button className={`dnt-button-2 tablink${active === 'Stripe' ? ' dnt-selected' : ''}`} onClick={() => setActive('Stripe')}>
@@ -58,26 +60,26 @@ export function DPointsTabs({ realm, currency }: { realm: string; currency: stri
{active === 'Info' && (
<div className="box-content dnt-tab dnt-animate">
<div className="body-box-content">
<span>¿Qué son los PD?</span>
<p>Ofrecemos a nuestros usuarios la oportunidad de obtener niveles que les brindan beneficios exclusivos dentro de nuestra comunidad en línea. Estos beneficios incluyen acceso a herramientas especiales, contenido exclusivo y mejoras significativas para su experiencia en nuestro sitio web. Al realizar una compra, nuestros usuarios reciben PD, una unidad de reconocimiento interna que refleja su compromiso y progreso en nuestra plataforma. Los PD les permiten alcanzar nuevos niveles y desbloquear recompensas exclusivas. Cabe destacar que estos PD son una forma única de valorar y premiar la participación activa de nuestros usuarios en nuestra comunidad.</p>
<span>{t('tabs.whatArePd')}</span>
<p>{t('tabs.whatArePdText')}</p>
<br />
<hr />
<br />
<span>¿Cuáles son los niveles disponibles?</span>
<span>{t('tabs.availableLevels')}</span>
{!showRanks ? (
<p id="rank-div-show" onClick={() => setShowRanks(true)}>Mostrar</p>
<p id="rank-div-show" onClick={() => setShowRanks(true)}>{t('tabs.show')}</p>
) : (
<p id="rank-div-hide" style={{ display: 'block' }} onClick={() => setShowRanks(false)}>Ocultar</p>
<p id="rank-div-hide" style={{ display: 'block' }} onClick={() => setShowRanks(false)}>{t('tabs.hide')}</p>
)}
{showRanks && (
<div className="rank-div" id="rank-div" style={{ display: 'block' }}>
<table className="rank-table">
<tbody>
{RANK_ROWS.map(([file, nivel, desde]) => (
{RANK_ROWS.map(([file, level, rangeKey]) => (
<tr key={file}>
<td><img src={`${RANKS}/${file}.svg`} alt={nivel} title={nivel} width="24px" /></td>
<td>{nivel}</td>
<td className="second-brown">{desde}</td>
<td><img src={`${RANKS}/${file}.svg`} alt={t('tabs.level', { n: level })} title={t('tabs.level', { n: level })} width="24px" /></td>
<td>{t('tabs.level', { n: level })}</td>
<td className="second-brown">{t(`tabs.${rangeKey}`)}</td>
</tr>
))}
</tbody>
@@ -87,21 +89,21 @@ export function DPointsTabs({ realm, currency }: { realm: string; currency: stri
<br />
<hr />
<br />
<span>¿Cuáles son los métodos de compra de PD?</span>
<p>Los métodos de compra disponibles actualmente son:</p>
<p>- <img className="pay-inline-logo" src={`${LOGOS}/stripe-p-logo.svg`} alt="Stripe" /> Stripe (tarjeta de crédito/débito, para todos los países).</p>
<p>- <img className="pay-inline-logo" src={`${LOGOS}/sumup-p-logo.svg`} alt="SumUp" /> SumUp (tarjeta de crédito/débito).</p>
<span>{t('tabs.purchaseMethods')}</span>
<p>{t('tabs.purchaseMethodsText')}</p>
<p>- <img className="pay-inline-logo" src={`${LOGOS}/stripe-p-logo.svg`} alt="Stripe" /> {t('tabs.stripeMethod')}</p>
<p>- <img className="pay-inline-logo" src={`${LOGOS}/sumup-p-logo.svg`} alt="SumUp" /> {t('tabs.sumupMethod')}</p>
<br />
<hr />
<br />
<span>¿Cuántos PD se reciben?</span>
<p>Por cada {sym}1 recibes 100 PD. La cantidad final se indica antes de confirmar el pago.</p>
<span>{t('tabs.howManyPd')}</span>
<p>{t('tabs.howManyPdText', { sym })}</p>
<br />
<hr />
<br />
<span>¿Tienes dudas o problemas con una compra?</span>
<p>No realices una compra hasta estar completamente seguro de que has leído esta información y sabes cómo es el proceso que vayas a escoger.</p>
<p>Si a pesar de leer este instructivo aún tienes dudas, el equipo de {realm} podrá asesorarte en cualquier momento. Puedes colocar un ticket en el servidor, o contactarnos mediante nuestra plataforma de Discord.</p>
<span>{t('tabs.doubts')}</span>
<p>{t('tabs.doubtsText1')}</p>
<p>{t('tabs.doubtsText2', { realm })}</p>
</div>
</div>
)}
@@ -114,20 +116,22 @@ export function DPointsTabs({ realm, currency }: { realm: string; currency: stri
}
/* --------------------------- Lógica de pago común --------------------------- */
function checkoutError(error?: string): string {
function checkoutErrorKey(error?: string): string {
switch (error) {
case 'notConfigured':
return 'Este método de pago aún no está disponible. Por favor, utiliza otro método o inténtalo más tarde.'
return 'notConfigured'
case 'invalidAmount':
return 'La cantidad debe ser un número entero entre 1 y 200.'
return 'invalidAmount'
case 'notAuthenticated':
return 'Tu sesión ha expirado. Inicia sesión de nuevo.'
return 'notAuthenticated'
default:
return 'No se pudo iniciar el pago. Inténtalo de nuevo.'
return 'generic'
}
}
function usePayment(provider: 'stripe' | 'sumup') {
const t = useTranslations('Points')
const checkoutError = (error?: string) => t(`tabs.checkoutErrors.${checkoutErrorKey(error)}`)
const [amount, setAmount] = useState('')
const [accepted, setAccepted] = useState(false)
const [busy, setBusy] = useState(false)
@@ -136,7 +140,7 @@ function usePayment(provider: 'stripe' | 'sumup') {
async function pay(e: React.FormEvent<HTMLFormElement>, sym: string) {
e.preventDefault()
if (!accepted) {
alert('Debes aceptar los Términos y Condiciones y la Política de Reembolso')
alert(t('tabs.mustAccept'))
return
}
const n = Number(amount)
@@ -144,7 +148,7 @@ function usePayment(provider: 'stripe' | 'sumup') {
setErr(checkoutError('invalidAmount'))
return
}
if (!window.confirm(`Vas a adquirir ${n * 100} PD por ${sym}${n}.\n¿Deseas continuar al pago?`)) return
if (!window.confirm(t('tabs.confirmPurchase', { points: n * 100, sym, amount: n }))) return
setBusy(true)
setErr(null)
try {
@@ -180,26 +184,28 @@ function AmountForm({
sym: string
buttonLabel: string
}) {
const t = useTranslations('Points')
const st = usePayment(provider)
const points = Number(st.amount) > 0 ? Number(st.amount) * 100 : 0
const currency = sym === '$' ? 'USD' : 'EUR'
return (
<>
<p className="yellow-info centered">{sym}1 = 100 PD</p>
<p className="centered second-brown">Valor mínimo: {sym}1 · Valor máximo por transacción: {sym}200</p>
<p className="yellow-info centered">{t('tabs.rate', { sym })}</p>
<p className="centered second-brown">{t('tabs.minMax', { sym })}</p>
<br />
<fieldset>
<div className="separate2">
<p className="centered red-info2">¡Importante!</p>
<p className="centered">Introduce sólo valores enteros (sin decimales).</p>
<p className="centered">Ejemplo: para 200 PD escribe 2.</p>
<p className="centered red-info2">{t('tabs.important')}</p>
<p className="centered">{t('tabs.integersOnly')}</p>
<p className="centered">{t('tabs.example')}</p>
</div>
</fieldset>
<br />
<p className="centered">Cantidad en {sym === '$' ? 'USD' : 'EUR'} a adquirir</p>
<p className="centered">{t('tabs.amountLabel', { currency })}</p>
<form className="centered" onSubmit={(e) => st.pay(e, sym)}>
<input
type="number"
placeholder={`Cantidad en ${sym === '$' ? 'USD' : 'EUR'}`}
placeholder={t('tabs.amountPlaceholder', { currency })}
min="1"
max="200"
step="1"
@@ -207,8 +213,8 @@ function AmountForm({
onChange={(e) => st.setAmount(e.target.value)}
required
/>{' '}
{sym === '$' ? 'USD' : 'EUR'}
{points > 0 && <p className="centered green-info2">Recibirás {points} PD</p>}
{currency}
{points > 0 && <p className="centered green-info2">{t('tabs.willReceive', { points })}</p>}
<br /><br />
<input
type="checkbox"
@@ -218,11 +224,14 @@ function AmountForm({
onChange={(e) => st.setAccepted(e.target.checked)}
required
/>
<label htmlFor={`accept-${provider}`}> Acepto los <Link href="/terms-and-conditions" target="_blank">Términos y Condiciones</Link> y la <Link href="/refund-policy" target="_blank">Política de Reembolso</Link></label>
<label htmlFor={`accept-${provider}`}>{t.rich('tabs.acceptTerms', {
terms: (c) => <Link href="/terms-and-conditions" target="_blank">{c}</Link>,
refund: (c) => <Link href="/refund-policy" target="_blank">{c}</Link>,
})}</label>
<br /><br />
{st.err && <p className="red-form-response">{st.err}</p>}
<button type="submit" className="dnt-button" disabled={!st.accepted || st.busy}>
{st.busy ? 'Redirigiendo…' : buttonLabel}
{st.busy ? t('tabs.redirecting') : buttonLabel}
</button>
</form>
</>
@@ -231,28 +240,33 @@ function AmountForm({
/* ---------------------------------- Stripe ---------------------------------- */
function StripeTab({ realm, sym }: { realm: string; sym: string }) {
const t = useTranslations('Points')
return (
<div className="box-content dnt-tab dnt-animate">
<div className="body-box-content">
<span>¿Qué es Stripe?</span><img className="pp-icon" src={`${LOGOS}/stripe-logo.svg`} alt="Stripe" />
<p>Stripe es una plataforma de pagos en línea segura que procesa tarjetas de crédito y débito de todo el mundo. Tus datos de pago se gestionan directamente en Stripe; {realm} nunca los almacena.<br />Más información sobre Stripe <a href="https://stripe.com/es" target="_blank" rel="noopener noreferrer">aquí</a>.</p>
<span>{t('tabs.whatIsStripe')}</span><img className="pp-icon" src={`${LOGOS}/stripe-logo.svg`} alt="Stripe" />
<p>{t.rich('tabs.stripeAboutText', {
realm,
br: () => <br />,
link: (c) => <a href="https://stripe.com/es" target="_blank" rel="noopener noreferrer">{c}</a>,
})}</p>
<br />
<hr />
<br />
<span>¿Cómo adquirir PD a través de Stripe?</span>
<p>Al ser un sistema auto-gestionado, las adquisiciones son automáticas y en cuestión de minutos tras completar el pago recibirás los PD en tu cuenta.</p>
<span>{t('tabs.howStripe')}</span>
<p>{t('tabs.stripeHowText')}</p>
<br />
<hr />
<br />
<p>- Introduce en el campo de más abajo la cantidad que deseas adquirir.</p>
<p>- Al presionar <span>ADQUIRIR PD</span>, serás redirigido a la pasarela segura de Stripe para completar el pago con tu tarjeta.</p>
<p>- Una vez que Stripe valide tu pago, tendrás los PD en tu cuenta de forma automática.</p>
<p>{t('tabs.step1')}</p>
<p>{t.rich('tabs.stripeStep2', { s: (c) => <span>{c}</span> })}</p>
<p>{t('tabs.stripeStep3')}</p>
<br />
<p>Recuerda que la cantidad sólo acepta números enteros y el mínimo posible es {sym}1.</p>
<p>Ejemplos de valor incorrecto: <span className="red-info2">{sym}5,90 | {sym}22,30 | {sym}53,10</span></p>
<p>Ejemplos de valor correcto: <span className="green-info2">{sym}6 | {sym}22 | {sym}53</span></p>
<p>{t('tabs.reminderMin', { sym })}</p>
<p>{t.rich('tabs.wrongExamples', { sym, s: (c) => <span className="red-info2">{c}</span> })}</p>
<p>{t.rich('tabs.rightExamples', { sym, s: (c) => <span className="green-info2">{c}</span> })}</p>
<br /><br /><br />
<AmountForm provider="stripe" sym={sym} buttonLabel="ADQUIRIR PD" />
<AmountForm provider="stripe" sym={sym} buttonLabel={t('tabs.acquireButton')} />
</div>
</div>
)
@@ -260,28 +274,33 @@ function StripeTab({ realm, sym }: { realm: string; sym: string }) {
/* ---------------------------------- SumUp ----------------------------------- */
function SumUpTab({ realm, sym }: { realm: string; sym: string }) {
const t = useTranslations('Points')
return (
<div className="box-content dnt-tab dnt-animate">
<div className="body-box-content">
<span>¿Qué es SumUp?</span><img className="pp-icon" src={`${LOGOS}/sumup-logo.svg`} alt="SumUp" />
<p>SumUp es una plataforma de pagos europea que permite pagar de forma rápida y segura con tarjeta de crédito o débito. Tus datos de pago se gestionan directamente en SumUp; {realm} nunca los almacena.<br />Más información sobre SumUp <a href="https://sumup.com/es-es/" target="_blank" rel="noopener noreferrer">aquí</a>.</p>
<span>{t('tabs.whatIsSumup')}</span><img className="pp-icon" src={`${LOGOS}/sumup-logo.svg`} alt="SumUp" />
<p>{t.rich('tabs.sumupAboutText', {
realm,
br: () => <br />,
link: (c) => <a href="https://sumup.com/es-es/" target="_blank" rel="noopener noreferrer">{c}</a>,
})}</p>
<br />
<hr />
<br />
<span>¿Cómo adquirir PD a través de SumUp?</span>
<p>Al ser un sistema auto-gestionado, las adquisiciones son automáticas y tras completar el pago recibirás los PD en tu cuenta.</p>
<span>{t('tabs.howSumup')}</span>
<p>{t('tabs.sumupHowText')}</p>
<br />
<hr />
<br />
<p>- Introduce en el campo de más abajo la cantidad que deseas adquirir.</p>
<p>- Al presionar <span>ADQUIRIR PD</span>, serás redirigido a la pasarela segura de SumUp para completar el pago con tu tarjeta.</p>
<p>- Una vez que SumUp valide tu pago, tendrás los PD en tu cuenta de forma automática.</p>
<p>{t('tabs.step1')}</p>
<p>{t.rich('tabs.sumupStep2', { s: (c) => <span>{c}</span> })}</p>
<p>{t('tabs.sumupStep3')}</p>
<br />
<p>Recuerda que la cantidad sólo acepta números enteros y el mínimo posible es {sym}1.</p>
<p>Ejemplos de valor incorrecto: <span className="red-info2">{sym}5,90 | {sym}22,30 | {sym}53,10</span></p>
<p>Ejemplos de valor correcto: <span className="green-info2">{sym}6 | {sym}22 | {sym}53</span></p>
<p>{t('tabs.reminderMin', { sym })}</p>
<p>{t.rich('tabs.wrongExamples', { sym, s: (c) => <span className="red-info2">{c}</span> })}</p>
<p>{t.rich('tabs.rightExamples', { sym, s: (c) => <span className="green-info2">{c}</span> })}</p>
<br /><br /><br />
<AmountForm provider="sumup" sym={sym} buttonLabel="ADQUIRIR PD" />
<AmountForm provider="sumup" sym={sym} buttonLabel={t('tabs.acquireButton')} />
</div>
</div>
)
+10 -8
View File
@@ -1,3 +1,4 @@
import { getTranslations } from 'next-intl/server'
import { getRealmName } from '@/lib/realm'
/**
@@ -5,40 +6,41 @@ import { getRealmName } from '@/lib/realm'
* (enlaces legales + copyright + crédito de diseño) con las clases del tema real.
*/
export async function Footer() {
const t = await getTranslations('UI')
const year = new Date().getFullYear()
const realmName = await getRealmName()
return (
<footer>
<p>
<a href="/terms-and-conditions">Términos y Condiciones</a>
<a href="/terms-and-conditions">{t('footer.terms')}</a>
<span className="third-brown">
&nbsp;&nbsp; <i className="fas fa-grip-lines-vertical"></i>&nbsp;&nbsp;{' '}
</span>
<a href="/privacy-policy">Política de Privacidad</a>
<a href="/privacy-policy">{t('footer.privacy')}</a>
<span className="third-brown">
&nbsp;&nbsp; <i className="fas fa-grip-lines-vertical"></i>&nbsp;&nbsp;{' '}
</span>
<a href="/refund-policy">Política de Reembolso</a>
<a href="/refund-policy">{t('footer.refund')}</a>
<span className="third-brown">
&nbsp;&nbsp; <i className="fas fa-grip-lines-vertical"></i>&nbsp;&nbsp;{' '}
</span>
<a href="/cookies">Declaración de Cookies</a>
<a href="/cookies">{t('footer.cookies')}</a>
<span className="third-brown">
&nbsp;&nbsp; <i className="fas fa-grip-lines-vertical"></i>&nbsp;&nbsp;{' '}
</span>
<a href="/legal-notice">Aviso legal</a>
<a href="/legal-notice">{t('footer.legal')}</a>
<span className="third-brown">
&nbsp;&nbsp; <i className="fas fa-grip-lines-vertical"></i>&nbsp;&nbsp;{' '}
</span>
<a href="/contact-us">Contáctanos</a>
<a href="/contact-us">{t('footer.contact')}</a>
</p>
<br />
<p className="third-brown">
© Copyright {realmName} {year}. Todos los derechos reservados
{t('footer.copyright', { realmName, year: String(year) })}
</p>
<p className="third-brown">
Diseño: &quot;{realmName}&quot; Por Inna Hoover Brown <i className="fas fa-cat"></i>
{t('footer.design', { realmName })} <i className="fas fa-cat"></i>
</p>
</footer>
)
+7
View File
@@ -0,0 +1,7 @@
import { getTranslations } from 'next-intl/server'
/** Leyenda «NOTA» / «NOTE» traducida para los <fieldset> de aviso. */
export async function NoteLegend() {
const t = await getTranslations('Common')
return <legend>{t('note')}</legend>
}
+29 -27
View File
@@ -1,3 +1,4 @@
import { getTranslations } from 'next-intl/server'
import {
getPlayersData,
CLASS_INFO,
@@ -11,19 +12,20 @@ import {
/* eslint-disable @next/next/no-img-element */
export async function PlayersBoard() {
const t = await getTranslations('UI')
const data = await getPlayersData()
return (
<>
<div className="box-content">
<div className="body-box-content centered">
<p><i className="fas fa-exclamation-triangle"></i> La información de esta página se actualiza 1 vez al día automáticamente</p>
<p><i className="fas fa-exclamation-triangle"></i> {t('players.updateNotice')}</p>
</div>
</div>
{/* Personajes creados por clase */}
<div className="box-content">
<div className="title-box-content centered"><h2>Personajes creados por clase</h2></div>
<div className="title-box-content centered"><h2>{t('players.charsByClass')}</h2></div>
<div className="body-box-content centered">
<div className="show-players">
{data.classStats.map((s) => (
@@ -31,10 +33,10 @@ export async function PlayersBoard() {
<table className="players-table">
<tbody>
<tr><td><img className="img-med-icon chest-medium" src={classChest(s.id)} alt={CLASS_INFO[s.id].label} /></td></tr>
<tr><td><span className={s.key}>Total: <span>{s.total}</span></span></td></tr>
<tr><td><span className={s.key}>{t('players.total')} <span>{s.total}</span></span></td></tr>
<tr><td><hr /></td></tr>
<tr><td className="lefted"><img className="img-small-icon-m img-align" src={ALLIANCE_ICON} alt="Alianza" /> <span className={s.key}>Alianzas: <span>{s.alliance}</span></span></td></tr>
<tr><td className="lefted"><img className="img-small-icon-m img-align" src={HORDE_ICON} alt="Horda" /> <span className={s.key}>Hordas: <span>{s.horde}</span></span></td></tr>
<tr><td className="lefted"><img className="img-small-icon-m img-align" src={ALLIANCE_ICON} alt={t('players.allianceAlt')} /> <span className={s.key}>{t('players.alliances')} <span>{s.alliance}</span></span></td></tr>
<tr><td className="lefted"><img className="img-small-icon-m img-align" src={HORDE_ICON} alt={t('players.hordeAlt')} /> <span className={s.key}>{t('players.hordes')} <span>{s.horde}</span></span></td></tr>
</tbody>
</table>
</div>
@@ -42,9 +44,9 @@ export async function PlayersBoard() {
<table className="max-center-table2">
<tbody>
<tr>
<td>Personajes totales: <span>{data.totals.total}</span></td>
<td><span className="alliance-color">Personajes alianzas:</span> <span>{data.totals.alliance}</span></td>
<td><span className="horde-color">Personajes hordas:</span> <span>{data.totals.horde}</span></td>
<td>{t('players.totalChars')} <span>{data.totals.total}</span></td>
<td><span className="alliance-color">{t('players.allianceChars')}</span> <span>{data.totals.alliance}</span></td>
<td><span className="horde-color">{t('players.hordeChars')}</span> <span>{data.totals.horde}</span></td>
</tr>
</tbody>
</table>
@@ -56,23 +58,23 @@ export async function PlayersBoard() {
{/* Primeros del Reino - Nivel 80 */}
<div className="box-content">
<div className="title-box-content centered"><h2>Primeros del Reino - Nivel 80</h2></div>
<div className="title-box-content centered"><h2>{t('players.firstsTitle')}</h2></div>
<div className="body-box-content justified">
{data.firsts.length === 0 ? (
<p className="centered second-brown">Aún no hay personajes de nivel 80 registrados.</p>
<p className="centered second-brown">{t('players.noLevel80')}</p>
) : (
<table className="max-left-table2">
<tbody>
<tr>
<th>Logro</th>
<th>Personaje</th>
<th className="responsive-td">Fecha</th>
<th>{t('players.thAchievement')}</th>
<th>{t('players.thCharacter')}</th>
<th className="responsive-td">{t('players.thDate')}</th>
</tr>
{data.firsts.map((f, i) => (
<tr key={i}>
<td>
<img className="img-small-icon img-small-icon-h img-align" src={f.icon} alt="" />{' '}
<span className="yellow-info"><span className="yellow-info responsive-td">¡Primero del reino!</span><span className="yellow-info"> {f.label}</span></span>
<span className="yellow-info"><span className="yellow-info responsive-td">{t('players.firstOfRealm')}</span><span className="yellow-info"> {f.label}</span></span>
</td>
<td className={`${f.classKey} big-font`}>{f.name}</td>
<td className="responsive-td"><span>{f.date}</span></td>
@@ -88,16 +90,16 @@ export async function PlayersBoard() {
{/* Top de logros */}
<div className="box-content">
<div className="title-box-content centered"><h2>Top de logros</h2></div>
<div className="title-box-content centered"><h2>{t('players.topAchTitle')}</h2></div>
<div className="body-box-content centered">
<p>Los 10 jugadores con más logros</p>
<p>{t('players.topAchSub')}</p>
<table className="char-center-table">
<tbody>
<tr>
<th>Nombre</th>
<th>Raza</th>
<th>Clase</th>
<th>Puntos de logros</th>
<th>{t('players.thName')}</th>
<th>{t('players.thRace')}</th>
<th>{t('players.thClass')}</th>
<th>{t('players.thAchPoints')}</th>
</tr>
{data.topAch.map((r, i) => (
<tr key={i}>
@@ -116,17 +118,17 @@ export async function PlayersBoard() {
{/* Top de muertes con honor */}
<div className="box-content">
<div className="title-box-content centered"><h2>Top de muertes con honor</h2></div>
<div className="title-box-content centered"><h2>{t('players.topHonorTitle')}</h2></div>
<div className="body-box-content centered">
<p>Los 10 jugadores con más muertes con honor</p>
<p>{t('players.topHonorSub')}</p>
<table className="char-center-table">
<tbody>
<tr>
<th>Nombre</th>
<th>Raza</th>
<th>Clase</th>
<th>Total de muertes</th>
<th>Total de muertes hoy</th>
<th>{t('players.thName')}</th>
<th>{t('players.thRace')}</th>
<th>{t('players.thClass')}</th>
<th>{t('players.thTotalKills')}</th>
<th>{t('players.thTodayKills')}</th>
</tr>
{data.topHonor.map((r, i) => (
<tr key={i}>
+12 -14
View File
@@ -1,28 +1,26 @@
'use client'
import { useState } from 'react'
import { useTranslations } from 'next-intl'
import { useRouter } from '@/i18n/navigation'
function promoError(error?: string): string {
function promoErrorKey(error?: string): string {
switch (error) {
case 'missingCode':
return 'Escribe un código de promoción.'
case 'notFound':
return 'El código no existe o ya no está disponible.'
case 'expired':
return 'Este código ha caducado.'
case 'exhausted':
return 'Este código ya ha alcanzado su límite de usos.'
case 'alreadyUsed':
return 'Ya has canjeado este código.'
case 'notAuthenticated':
return 'Tu sesión ha expirado. Inicia sesión de nuevo.'
return error
default:
return 'No se pudo canjear el código. Inténtalo de nuevo.'
return 'generic'
}
}
export function PromoCodeForm() {
const t = useTranslations('Points')
const promoError = (error?: string) => t(`promo.errors.${promoErrorKey(error)}`)
const router = useRouter()
const [code, setCode] = useState('')
const [busy, setBusy] = useState(false)
@@ -43,10 +41,10 @@ export function PromoCodeForm() {
const data: { success?: boolean; error?: string; pd?: number; pv?: number } = await res.json()
if (data.success) {
const parts: string[] = []
if (data.pd) parts.push(`${data.pd} PD`)
if (data.pv) parts.push(`${data.pv} PV`)
const reward = parts.length ? parts.join(' y ') : 'la recompensa'
setMessage({ ok: true, text: `¡Código canjeado! Has recibido ${reward}.` })
if (data.pd) parts.push(t('promo.rewardPd', { pd: data.pd }))
if (data.pv) parts.push(t('promo.rewardPv', { pv: data.pv }))
const reward = parts.length ? parts.join(` ${t('promo.and')} `) : t('promo.rewardDefault')
setMessage({ ok: true, text: t('promo.success', { reward }) })
setCode('')
router.refresh()
} else {
@@ -66,14 +64,14 @@ export function PromoCodeForm() {
<input
type="text"
maxLength={64}
placeholder="Escribe tu código de promoción"
placeholder={t('promo.placeholder')}
value={code}
onChange={(e) => setCode(e.target.value)}
required
/>
<br />
<button type="submit" className="dnt-button" disabled={busy || !code.trim()}>
{busy ? 'Canjeando…' : 'CANJEAR CÓDIGO'}
{busy ? t('promo.redeeming') : t('promo.redeemButton')}
</button>
</form>
<hr />
+27 -25
View File
@@ -1,6 +1,7 @@
'use client'
import { useMemo, useState } from 'react'
import { useTranslations } from 'next-intl'
import { CharacterSelect } from '@/components/CharacterSelect'
import { Turnstile } from '@/components/Turnstile'
@@ -23,35 +24,36 @@ interface QuestStatus {
state: 'rewarded' | 'inProgress' | 'none'
}
function trackerError(error?: string, minutes?: number): string {
function trackerError(t: ReturnType<typeof useTranslations>, error?: string, minutes?: number): string {
switch (error) {
case 'missingFields':
return 'Selecciona un personaje y una opción.'
return t('quest.errors.missingFields')
case 'characterNotOwned':
return 'El personaje seleccionado no es válido.'
return t('quest.errors.characterNotOwned')
case 'characterOnline':
return 'El personaje debe estar desconectado.'
return t('quest.errors.characterOnline')
case 'wrongClass':
return 'Esa opción no corresponde a la clase del personaje.'
return t('quest.errors.wrongClass')
case 'levelTooLow':
return 'El personaje no cumple el nivel mínimo de esa opción.'
return t('quest.errors.levelTooLow')
case 'invalidQuestId':
return 'Introduce un ID de misión válido.'
return t('quest.errors.invalidQuestId')
case 'invalidOption':
case 'noData':
return 'Esa opción no está disponible todavía.'
return t('quest.errors.noData')
case 'cooldown':
return `Debes esperar ${minutes ?? 0} min para repetir esta búsqueda en este personaje.`
return t('quest.errors.cooldown', { minutes: minutes ?? 0 })
case 'captcha':
return 'Verifica que no eres un robot.'
return t('quest.errors.captcha')
case 'notAuthenticated':
return 'Tu sesión ha expirado. Inicia sesión de nuevo.'
return t('quest.errors.notAuthenticated')
default:
return 'No se pudo completar la búsqueda. Inténtalo de nuevo.'
return t('quest.errors.default')
}
}
export function QuestTrackerForm({ characters, options }: { characters: QuestCharacter[]; options: QuestOptionClient[] }) {
const t = useTranslations('CharService')
const [character, setCharacter] = useState('')
const [option, setOption] = useState('')
const [questId, setQuestId] = useState('')
@@ -70,9 +72,9 @@ export function QuestTrackerForm({ characters, options }: { characters: QuestCha
const list = options
.filter((o) => o.hasData && (o.classCss === null || o.classCss === selected.classCss) && selected.level >= o.minLevel)
.map((o) => ({ key: o.key, label: o.label }))
if (selected.level >= 10) list.push({ key: 'specific', label: 'Por misión específica' })
if (selected.level >= 10) list.push({ key: 'specific', label: t('quest.specificLabel') })
return list
}, [selected, options])
}, [selected, options, t])
function onCharacter(v: string) {
setCharacter(v)
@@ -100,10 +102,10 @@ export function QuestTrackerForm({ characters, options }: { characters: QuestCha
if (data.success && data.statuses) {
setResult({ label: data.label ?? '', statuses: data.statuses })
} else {
setMessage({ ok: false, text: trackerError(data.error, data.minutes) })
setMessage({ ok: false, text: trackerError(t, data.error, data.minutes) })
}
} catch {
setMessage({ ok: false, text: trackerError() })
setMessage({ ok: false, text: trackerError(t) })
} finally {
setBusy(false)
setCaptcha('')
@@ -115,7 +117,7 @@ export function QuestTrackerForm({ characters, options }: { characters: QuestCha
return (
<div className="centered">
<br />
<span>No hay personajes disponibles para esta herramienta</span>
<span>{t('quest.noCharacters')}</span>
<br />
</div>
)
@@ -123,18 +125,18 @@ export function QuestTrackerForm({ characters, options }: { characters: QuestCha
const stateText = (s: QuestStatus['state']) =>
s === 'rewarded'
? { text: 'Completada', cls: 'green-info2' }
? { text: t('quest.stateCompleted'), cls: 'green-info2' }
: s === 'inProgress'
? { text: 'En curso', cls: 'yellow-info' }
: { text: 'Sin empezar', cls: 'third-brown' }
? { text: t('quest.stateInProgress'), cls: 'yellow-info' }
: { text: t('quest.stateNotStarted'), cls: 'third-brown' }
return (
<div className="centered">
<form id="uw-quest-form" onSubmit={handleSubmit} acceptCharset="utf-8">
<CharacterSelect characters={characters} value={character} onChange={onCharacter} placeholder="Selecciona un personaje" />
<CharacterSelect characters={characters} value={character} onChange={onCharacter} placeholder={t('quest.selectPlaceholder')} />
<br />
<select value={option} onChange={(e) => setOption(e.target.value)} required disabled={!selected}>
<option value="" disabled>Selecciona una opción</option>
<option value="" disabled>{t('quest.selectOption')}</option>
{available.map((o) => (
<option key={o.key} value={o.key}>{o.label}</option>
))}
@@ -145,7 +147,7 @@ export function QuestTrackerForm({ characters, options }: { characters: QuestCha
<input
type="number"
min="1"
placeholder="ID de misión"
placeholder={t('quest.questIdPlaceholder')}
value={questId}
onChange={(e) => setQuestId(e.target.value)}
required
@@ -155,7 +157,7 @@ export function QuestTrackerForm({ characters, options }: { characters: QuestCha
)}
<Turnstile key={captchaKey} onVerify={setCaptcha} />
<button type="submit" className="quest-button" disabled={busy || !character || !option}>
{busy ? 'Buscando misiones' : 'BUSCAR MISIONES'}
{busy ? t('quest.submitting') : t('quest.submit')}
</button>
</form>
<hr />
@@ -172,7 +174,7 @@ export function QuestTrackerForm({ characters, options }: { characters: QuestCha
<tr key={s.quest}>
<td className="lefted">
<a href={`${QUEST_DB}/?quest=${s.quest}`} target="_blank" rel="noopener noreferrer">
Misión #{s.quest}
{t('quest.questNumber', { quest: s.quest })}
</a>
</td>
<td className={`real-info-box ${st.cls}`}>{st.text}</td>
+20 -17
View File
@@ -1,27 +1,30 @@
import { getTranslations } from 'next-intl/server'
/** Información del reino (Rates + Horarios) — contenido de /<slug>-realm. */
export function RealmInfo() {
export async function RealmInfo() {
const t = await getTranslations('UI')
return (
<div className="box-content centered">
<div className="realm-information realm-information-half">
<h2>Rates</h2>
<p>Experiencia: <span>x8 modificable con .xp (Con Recluta a un Amigo x16)</span></p>
<p>Drop de objetos verdes y azules: <span>x4</span></p>
<p>Drop de oro en NPCs: <span>x2</span></p>
<p>Drop de oro en misiones: <span>x1</span></p>
<p>Habilidades: <span>x3</span></p>
<p>Profesiones: <span>x3</span></p>
<p>Reputaciones: <span>x3 (Bonus con Recluta a un amigo)</span></p>
<p>Honor: <span>x3</span></p>
<h2>{t('realmInfo.ratesTitle')}</h2>
<p>{t('realmInfo.experience')}: <span>{t('realmInfo.experienceValue')}</span></p>
<p>{t('realmInfo.greenBlueDrop')}: <span>{t('realmInfo.greenBlueDropValue')}</span></p>
<p>{t('realmInfo.goldNpc')}: <span>{t('realmInfo.goldNpcValue')}</span></p>
<p>{t('realmInfo.goldQuest')}: <span>{t('realmInfo.goldQuestValue')}</span></p>
<p>{t('realmInfo.skills')}: <span>{t('realmInfo.skillsValue')}</span></p>
<p>{t('realmInfo.professions')}: <span>{t('realmInfo.professionsValue')}</span></p>
<p>{t('realmInfo.reputations')}: <span>{t('realmInfo.reputationsValue')}</span></p>
<p>{t('realmInfo.honor')}: <span>{t('realmInfo.honorValue')}</span></p>
</div>
<div className="realm-information realm-information-half">
<h2>Horarios</h2>
<p>Los siguientes horarios son basados en la hora del reino.</p>
<h2>{t('realmInfo.scheduleTitle')}</h2>
<p>{t('realmInfo.scheduleNote')}</p>
<br />
<p>Misiones diarias: <span>7 am</span></p>
<p>Reinicio de mazmorras diarias: <span>9 am</span></p>
<p>Reinicio de bandas: <span>9 am (Miércoles)</span></p>
<p>Campos de Batalla: <span>10 am</span></p>
<p>Reparto de Puntos de Arena: <span>11 am (Viernes)</span></p>
<p>{t('realmInfo.dailyQuests')}: <span>{t('realmInfo.dailyQuestsValue')}</span></p>
<p>{t('realmInfo.dailyDungeons')}: <span>{t('realmInfo.dailyDungeonsValue')}</span></p>
<p>{t('realmInfo.raidReset')}: <span>{t('realmInfo.raidResetValue')}</span></p>
<p>{t('realmInfo.battlegrounds')}: <span>{t('realmInfo.battlegroundsValue')}</span></p>
<p>{t('realmInfo.arenaPoints')}: <span>{t('realmInfo.arenaPointsValue')}</span></p>
<br />
</div>
</div>
+25 -23
View File
@@ -1,23 +1,24 @@
'use client'
import { useState } from 'react'
import { useTranslations } from 'next-intl'
import type { RecruitRewardView } from '@/lib/recruit-claim'
import type { CharOption } from '@/components/CharacterSelect'
const ICON_BASE = 'https://wotlk.ultimowow.com/static/images/wow/icons/tiny'
function claimError(message?: string): string {
function claimErrorKey(message?: string): string {
switch (message) {
case 'requirementsNotMet':
return 'Aún no cumples los requisitos para esta recompensa.'
return 'recruit.errRequirements'
case 'alreadyClaimed':
return 'Esta recompensa ya fue reclamada.'
return 'recruit.errAlreadyClaimed'
case 'invalidCharacter':
return 'Personaje no válido.'
return 'recruit.errInvalidCharacter'
case 'notFound':
return 'Recompensa no encontrada.'
return 'recruit.errNotFound'
default:
return 'No se pudo reclamar la recompensa. Inténtalo de nuevo.'
return 'recruit.errGeneric'
}
}
@@ -38,6 +39,7 @@ export function RecruitPanel({
isRecruited: boolean
recruitedCount: number
}) {
const t = useTranslations('UI')
const [character, setCharacter] = useState(characters[0]?.name ?? '')
const selectedCss = characters.find((c) => c.name === character)?.classCss
const [busyId, setBusyId] = useState<number | null>(null)
@@ -62,10 +64,10 @@ export function RecruitPanel({
window.location.reload()
return
}
setMsg({ ok: false, text: claimError(d.message) })
setMsg({ ok: false, text: t(claimErrorKey(d.message)) })
setBusyId(null)
} catch {
setMsg({ ok: false, text: claimError() })
setMsg({ ok: false, text: t(claimErrorKey()) })
setBusyId(null)
}
}
@@ -90,15 +92,15 @@ export function RecruitPanel({
return (
<>
<p className="lefted">Cuenta reclutada: <span>{isRecruited ? 'Sí' : 'No'}</span></p>
<p className="lefted">Amigos reclutados: <span>{recruitedCount}</span></p>
<p className="lefted">Amigos reclutados al nivel 80: <span>{level80Count}</span></p>
<p className="lefted">Recompensas reclamadas: <span>{claimedCount}/{rewards.length}</span></p>
<p className="lefted">{t('recruit.accountRecruited')} <span>{isRecruited ? t('recruit.yes') : t('recruit.no')}</span></p>
<p className="lefted">{t('recruit.friendsRecruited')} <span>{recruitedCount}</span></p>
<p className="lefted">{t('recruit.friendsAtLevel80')} <span>{level80Count}</span></p>
<p className="lefted">{t('recruit.rewardsClaimed')} <span>{claimedCount}/{rewards.length}</span></p>
<br />
{characters.length > 0 && (
<p className="lefted">
Recibir recompensas en:{' '}
{t('recruit.receiveRewardsOn')}{' '}
<select value={character} onChange={(e) => setCharacter(e.target.value)} className={['account-select', selectedCss].filter(Boolean).join(' ')}>
{characters.map((c) => (
<option key={c.name} value={c.name} className={c.classCss ? `${c.classCss} big-font` : undefined}>{c.name}</option>
@@ -111,25 +113,25 @@ export function RecruitPanel({
<table className="char-center-table">
<tbody>
<tr>
<th>Amigos reclutados</th>
<th>Recompensa</th>
<th>Estado</th>
<th>{t('recruit.thFriends')}</th>
<th>{t('recruit.thReward')}</th>
<th>{t('recruit.thStatus')}</th>
</tr>
{rewards.map((r) => {
const eligible = level80Count >= r.required_friends
return (
<tr key={r.id}>
<td><span>{r.required_friends} {r.required_friends === 1 ? 'amigo' : 'amigos'} al nivel 80</span></td>
<td><span>{t('recruit.friendsAtLevel80Row', { count: r.required_friends })}</span></td>
<td>{rewardCell(r)}</td>
<td>
{r.claimed ? (
<span className="green-info"><i className="fas fa-check"></i> Reclamado</span>
<span className="green-info"><i className="fas fa-check"></i> {t('recruit.claimed')}</span>
) : eligible ? (
<button type="button" className="nw-tool-btn" onClick={() => claim(r)} disabled={busyId !== null}>
{busyId === r.id ? '…' : 'Reclamar'}
{busyId === r.id ? '…' : t('recruit.claim')}
</button>
) : (
<span>Sin reclamar</span>
<span>{t('recruit.unclaimed')}</span>
)}
</td>
</tr>
@@ -140,13 +142,13 @@ export function RecruitPanel({
<br />
<hr />
<p>RECOMPENSAS DISPONIBLES</p>
<p>{t('recruit.availableRewards')}</p>
<hr />
{available.length === 0 ? (
<span>Recluta amigos para poder reclamar las recompensas</span>
<span>{t('recruit.recruitToClaim')}</span>
) : (
<span className="green-info">
Tienes {available.length} recompensa{available.length === 1 ? '' : 's'} lista{available.length === 1 ? '' : 's'} para reclamar
{t('recruit.readyToClaim', { count: available.length })}
</span>
)}
</>
+13 -17
View File
@@ -1,33 +1,29 @@
'use client'
import { useState } from 'react'
import { useTranslations } from 'next-intl'
import { useRouter } from '@/i18n/navigation'
import type { GmGuild } from '@/lib/guild'
function renameError(error?: string): string {
function renameErrorKey(error?: string): string {
switch (error) {
case 'missingFields':
return 'Completa todos los campos.'
case 'invalidName':
return 'El nombre debe tener de 1 a 24 caracteres, solo letras (A-Z, a-z) y espacios.'
case 'invalidToken':
return 'El token de seguridad no es válido.'
case 'notGuildMaster':
return 'No eres Maestro de esa hermandad.'
case 'nameTaken':
return 'Ya existe una hermandad con ese nombre.'
case 'insufficientPoints':
return 'No tienes suficientes PD (se requieren 1000).'
case 'soapError':
return 'No se pudo aplicar el cambio (servidor de juego no disponible). Inténtalo más tarde.'
case 'notAuthenticated':
return 'Tu sesión ha expirado. Inicia sesión de nuevo.'
return error
default:
return 'No se pudo renombrar la hermandad. Inténtalo de nuevo.'
return 'generic'
}
}
export function RenameGuildForm({ guilds }: { guilds: GmGuild[] }) {
const t = useTranslations('Points')
const renameError = (error?: string) => t(`renameGuild.errors.${renameErrorKey(error)}`)
const router = useRouter()
const [guildId, setGuildId] = useState('')
const [newName, setNewName] = useState('')
@@ -41,7 +37,7 @@ export function RenameGuildForm({ guilds }: { guilds: GmGuild[] }) {
return (
<div className="centered">
<br />
<span>No hay personajes Maestros de hermandad</span>
<span>{t('renameGuild.noGuilds')}</span>
<br />
</div>
)
@@ -54,7 +50,7 @@ export function RenameGuildForm({ guilds }: { guilds: GmGuild[] }) {
setMessage({ ok: false, text: renameError('missingFields') })
return
}
if (!window.confirm('¿Estás seguro de renombrar la hermandad seleccionada?')) return
if (!window.confirm(t('renameGuild.confirm'))) return
setBusy(true)
setMessage(null)
@@ -68,7 +64,7 @@ export function RenameGuildForm({ guilds }: { guilds: GmGuild[] }) {
const data: { success?: boolean; error?: string; newName?: string } = await res.json()
if (data.success) {
setDone(true)
setMessage({ ok: true, text: `¡Hermandad renombrada a "${data.newName}"! Puede que los miembros conectados deban reconectar para verlo.` })
setMessage({ ok: true, text: t('renameGuild.success', { newName: data.newName ?? '' }) })
setNewName('')
setToken('')
setTimeout(() => router.refresh(), 3000)
@@ -90,7 +86,7 @@ export function RenameGuildForm({ guilds }: { guilds: GmGuild[] }) {
<tr>
<td>
<select value={guildId} onChange={(e) => setGuildId(e.target.value)} required>
<option value="" disabled>Selecciona una hermandad</option>
<option value="" disabled>{t('renameGuild.selectGuild')}</option>
{guilds.map((g) => (
<option key={g.guildid} value={g.guildid}>{g.name}</option>
))}
@@ -103,7 +99,7 @@ export function RenameGuildForm({ guilds }: { guilds: GmGuild[] }) {
type="text"
maxLength={24}
name="new-guild-name"
placeholder="Nuevo nombre de la hermandad"
placeholder={t('renameGuild.newNamePlaceholder')}
value={newName}
onChange={(e) => setNewName(e.target.value)}
required
@@ -116,7 +112,7 @@ export function RenameGuildForm({ guilds }: { guilds: GmGuild[] }) {
type={showToken ? 'text' : 'password'}
maxLength={6}
id="security-token"
placeholder="Token de seguridad"
placeholder={t('renameGuild.tokenPlaceholder')}
value={token}
onChange={(e) => setToken(e.target.value)}
required
@@ -132,7 +128,7 @@ export function RenameGuildForm({ guilds }: { guilds: GmGuild[] }) {
<tr>
<td>
<button type="submit" className="rename-guild-button" disabled={busy || done} style={done ? { color: '#d79602' } : undefined}>
{done ? 'HERMANDAD RENOMBRADA' : busy ? 'RENOMBRANDO HERMANDAD' : 'RENOMBRAR HERMANDAD'}
{done ? t('renameGuild.renamed') : busy ? t('renameGuild.renaming') : t('renameGuild.renameButton')}
</button>
</td>
</tr>
+19 -17
View File
@@ -1,33 +1,35 @@
'use client'
import { useState } from 'react'
import { useTranslations } from 'next-intl'
import { useRouter } from '@/i18n/navigation'
import type { DeletedCharacter } from '@/lib/restore-character'
function restoreError(error?: string): string {
function restoreError(t: ReturnType<typeof useTranslations>, error?: string): string {
switch (error) {
case 'notFound':
return 'Ese personaje borrado no es de tu cuenta.'
return t('restoreChar.errors.notFound')
case 'notDeleted':
return 'Ese personaje no está borrado.'
return t('restoreChar.errors.notDeleted')
case 'levelTooLow':
return 'Solo se pueden recuperar personajes de nivel superior a 60.'
return t('restoreChar.errors.levelTooLow')
case 'tooOld':
return 'El personaje se borró hace más de 30 días y ya no es recuperable.'
return t('restoreChar.errors.tooOld')
case 'dkExists':
return 'No puedes recuperar un Caballero de la Muerte: ya tienes uno activo en la cuenta.'
return t('restoreChar.errors.dkExists')
case 'nameTaken':
return 'El nombre original ya está en uso por otro personaje.'
return t('restoreChar.errors.nameTaken')
case 'cooldown':
return 'Solo puedes intentar recuperar el mismo personaje una vez cada 12 horas.'
return t('restoreChar.errors.cooldown')
case 'notAuthenticated':
return 'Tu sesión ha expirado. Inicia sesión de nuevo.'
return t('restoreChar.errors.notAuthenticated')
default:
return 'No se pudo recuperar el personaje. Inténtalo de nuevo.'
return t('restoreChar.errors.default')
}
}
export function RestoreCharacterForm({ characters }: { characters: DeletedCharacter[] }) {
const t = useTranslations('CharService')
const router = useRouter()
const [guid, setGuid] = useState('')
const [busy, setBusy] = useState(false)
@@ -41,7 +43,7 @@ export function RestoreCharacterForm({ characters }: { characters: DeletedCharac
<div className="centered">
<br />
<br />
<span>No hay personajes</span>
<span>{t('restoreChar.noCharacters')}</span>
<br />
</div>
)
@@ -62,13 +64,13 @@ export function RestoreCharacterForm({ characters }: { characters: DeletedCharac
const data: { success?: boolean; error?: string; name?: string } = await res.json()
if (data.success) {
setDone(true)
setMessage({ ok: true, text: `¡Personaje "${data.name}" recuperado! Lo verás al entrar al reino.` })
setMessage({ ok: true, text: t('restoreChar.successMsg', { name: data.name ?? '' }) })
setTimeout(() => router.refresh(), 3000)
} else {
setMessage({ ok: false, text: restoreError(data.error) })
setMessage({ ok: false, text: restoreError(t, data.error) })
}
} catch {
setMessage({ ok: false, text: restoreError() })
setMessage({ ok: false, text: restoreError(t) })
} finally {
setBusy(false)
}
@@ -78,16 +80,16 @@ export function RestoreCharacterForm({ characters }: { characters: DeletedCharac
<div className="centered">
<form id="uw-restore-form" onSubmit={handleSubmit} acceptCharset="utf-8">
<select className={selectedCss} value={guid} onChange={(e) => setGuid(e.target.value)} required>
<option value="" disabled>Selecciona un personaje</option>
<option value="" disabled>{t('restoreChar.selectPlaceholder')}</option>
{characters.map((c) => (
<option key={c.guid} value={c.guid} className={`${c.classCss} big-font`}>
{c.name} (nivel {c.level})
{t('restoreChar.optionLabel', { name: c.name, level: c.level })}
</option>
))}
</select>
<br />
<button type="submit" className="restore-button" disabled={busy || done || !guid} style={done ? { color: '#d79602' } : undefined}>
{done ? 'RECUPERADO' : busy ? 'Recuperando' : 'RECUPERAR'}
{done ? t('restoreChar.restored') : busy ? t('restoreChar.recovering') : t('restoreChar.recover')}
</button>
</form>
<hr />
+22 -20
View File
@@ -1,6 +1,7 @@
'use client'
import { useState } from 'react'
import { useTranslations } from 'next-intl'
import { CharacterSelect, type CharOption } from '@/components/CharacterSelect'
import { Turnstile } from '@/components/Turnstile'
@@ -13,26 +14,27 @@ interface DeletedItem {
count: number
}
function itemsError(error?: string, minutes?: number): string {
function itemsError(t: ReturnType<typeof useTranslations>, error?: string, minutes?: number): string {
switch (error) {
case 'characterNotOwned':
return 'El personaje seleccionado no es válido.'
return t('restoreItems.errors.characterNotOwned')
case 'cooldown':
return `Solo puedes consultar los ítems de este personaje cada 8 horas (faltan ${minutes ?? 0} min).`
return t('restoreItems.errors.cooldown', { minutes: minutes ?? 0 })
case 'soapError':
return 'No se pudo conectar con el servidor de juego. Inténtalo más tarde.'
return t('restoreItems.errors.soapError')
case 'insufficientPoints':
return 'No tienes suficientes PD (se requieren 100 por ítem).'
return t('restoreItems.errors.insufficientPoints')
case 'captcha':
return 'Verifica que no eres un robot.'
return t('restoreItems.errors.captcha')
case 'notAuthenticated':
return 'Tu sesión ha expirado. Inicia sesión de nuevo.'
return t('restoreItems.errors.notAuthenticated')
default:
return 'Algo ha salido mal. Inténtalo de nuevo.'
return t('restoreItems.errors.default')
}
}
export function RestoreItemsForm({ characters, price }: { characters: CharOption[]; price: number }) {
const t = useTranslations('CharService')
const [character, setCharacter] = useState('')
const [captcha, setCaptcha] = useState('')
const [captchaKey, setCaptchaKey] = useState(0)
@@ -55,12 +57,12 @@ export function RestoreItemsForm({ characters, price }: { characters: CharOption
const data: { success?: boolean; error?: string; minutes?: number; items?: DeletedItem[] } = await res.json()
if (data.success) {
setItems(data.items ?? [])
if ((data.items ?? []).length === 0) setMessage({ ok: true, text: 'Este personaje no tiene ítems borrados recuperables.' })
if ((data.items ?? []).length === 0) setMessage({ ok: true, text: t('restoreItems.noDeletedItemsMsg') })
} else {
setMessage({ ok: false, text: itemsError(data.error, data.minutes) })
setMessage({ ok: false, text: itemsError(t, data.error, data.minutes) })
}
} catch {
setMessage({ ok: false, text: itemsError() })
setMessage({ ok: false, text: itemsError(t) })
} finally {
setBusy(false)
setCaptcha('')
@@ -70,7 +72,7 @@ export function RestoreItemsForm({ characters, price }: { characters: CharOption
async function restore(item: DeletedItem) {
if (busy) return
if (!window.confirm(`¿Recuperar "${item.name}" por ${price} € (SumUp)?`)) return
if (!window.confirm(t('restoreItems.confirm', { name: item.name, price }))) return
setBusy(true)
setMessage(null)
try {
@@ -85,10 +87,10 @@ export function RestoreItemsForm({ characters, price }: { characters: CharOption
window.location.assign(data.url) // checkout de SumUp
return
}
setMessage({ ok: false, text: data.message || itemsError(data.error) })
setMessage({ ok: false, text: data.message || itemsError(t, data.error) })
setBusy(false)
} catch {
setMessage({ ok: false, text: itemsError() })
setMessage({ ok: false, text: itemsError(t) })
setBusy(false)
}
}
@@ -106,19 +108,19 @@ export function RestoreItemsForm({ characters, price }: { characters: CharOption
{items === null ? (
<div id="char-selection">
<form id="uw-search-items-form" onSubmit={search} acceptCharset="utf-8">
<CharacterSelect characters={characters} value={character} onChange={setCharacter} placeholder="Selecciona un personaje" />
<CharacterSelect characters={characters} value={character} onChange={setCharacter} placeholder={t('restoreItems.selectPlaceholder')} />
<br />
<Turnstile key={captchaKey} onVerify={setCaptcha} />
<button type="submit" className="search-items-button" disabled={busy || !character}>
{busy ? 'Buscando' : 'BUSCAR ÍTEMS'}
{busy ? t('restoreItems.searching') : t('restoreItems.search')}
</button>
</form>
</div>
) : (
<div id="search-items-response">
<p className="yellow-info">Ítems borrados de {character}</p>
<p className="yellow-info">{t('restoreItems.deletedItemsOf', { character })}</p>
{items.length === 0 ? (
<p className="second-brown">No hay ítems borrados recuperables.</p>
<p className="second-brown">{t('restoreItems.noDeletedItems')}</p>
) : (
<table className="restore-item-table">
<tbody>
@@ -130,7 +132,7 @@ export function RestoreItemsForm({ characters, price }: { characters: CharOption
</td>
<td className="real-info-box">
<button type="button" className="restore-item-button" value={it.recoverId} data-char={character} onClick={() => restore(it)} disabled={busy}>
Recuperar ({price} )
{t('restoreItems.restoreItem', { price })}
</button>
</td>
</tr>
@@ -139,7 +141,7 @@ export function RestoreItemsForm({ characters, price }: { characters: CharOption
</table>
)}
<br />
<p><a id="a-select-other" href="#" onClick={(e) => { e.preventDefault(); reset() }}>Consultar otro personaje</a></p>
<p><a id="a-select-other" href="#" onClick={(e) => { e.preventDefault(); reset() }}>{t('restoreItems.queryOther')}</a></p>
</div>
)}
<hr />
+2 -1
View File
@@ -1,4 +1,5 @@
import { getTranslations, setRequestLocale } from 'next-intl/server'
import { NoteLegend } from '@/components/NoteLegend'
import { redirect } from '@/i18n/navigation'
import { getSession } from '@/lib/session'
import { getGameCharacters } from '@/lib/characters'
@@ -23,7 +24,7 @@ export async function ReviveServiceContent({ locale }: { locale: string }) {
<p>{t('reviveInfo2')}</p>
<br />
<fieldset>
<legend>NOTA</legend>
<NoteLegend />
<div className="separate2">
<p>{t('reviveNote1')}</p>
<p>{t('reviveNote2')}</p>
+23 -21
View File
@@ -1,6 +1,7 @@
'use client'
import { useMemo, useState } from 'react'
import { useTranslations } from 'next-intl'
import type { GiftCategory, GiftItem } from '@/lib/gift'
import { CharacterSelect, type CharOption } from '@/components/CharacterSelect'
@@ -53,6 +54,7 @@ export function SendGiftForm({
catalog: GiftCategory[]
currency?: string
}) {
const t = useTranslations('CharService')
const [source, setSource] = useState('')
const [destination, setDestination] = useState('')
const [token, setToken] = useState('')
@@ -106,16 +108,16 @@ export function SendGiftForm({
e.preventDefault()
if (busy) return
if (!source || !destination.trim() || !token.trim()) {
setError('Completa el personaje de origen, el de destino y el token de seguridad.')
setError(t('sendGift.errMissingFields'))
return
}
if (cart.size === 0) {
setError('Añade al menos un objeto al carrito.')
setError(t('sendGift.errEmptyCart'))
return
}
if (
!window.confirm(
`¿Enviar el regalo a "${destination.trim()}" por ${total} ${currency} (SumUp)? Esta acción no es reversible.`,
t('sendGift.confirm', { destination: destination.trim(), total, currency }),
)
)
return
@@ -137,40 +139,40 @@ export function SendGiftForm({
const data: { success?: boolean; url?: string; message?: string } = await res.json()
if (data.success && data.url) window.location.href = data.url
else {
setError(data.message || 'No se pudo iniciar el pago. Inténtalo de nuevo.')
setError(data.message || t('sendGift.errPayment'))
setBusy(false)
}
} catch {
setError('Algo ha salido mal. Inténtalo más tarde.')
setError(t('sendGift.errUnexpected'))
setBusy(false)
}
}
if (characters.length === 0) return <p className="second-brown">No tienes personajes disponibles.</p>
if (catalog.length === 0) return <p className="second-brown">No hay objetos disponibles para regalar por ahora.</p>
if (characters.length === 0) return <p className="second-brown">{t('sendGift.noCharacters')}</p>
if (catalog.length === 0) return <p className="second-brown">{t('sendGift.noItems')}</p>
return (
<div className="centered">
<form onSubmit={handleSubmit} acceptCharset="utf-8">
<p>Escoge desde qué personaje se envía el regalo:</p>
<CharacterSelect characters={characters} value={source} onChange={setSource} placeholder="Personaje de origen" />
<p>{t('sendGift.sourcePrompt')}</p>
<CharacterSelect characters={characters} value={source} onChange={setSource} placeholder={t('sendGift.sourcePlaceholder')} />
<br />
<p>Nombre del personaje que recibirá el regalo:</p>
<p>{t('sendGift.destPrompt')}</p>
<input
type="text"
maxLength={12}
placeholder="Personaje de destino"
placeholder={t('sendGift.destPlaceholder')}
value={destination}
onChange={(e) => setDestination(e.target.value)}
required
/>
<br />
<p>Token de seguridad:</p>
<SecretInput value={token} onChange={setToken} placeholder="Token de seguridad" />
<p>{t('sendGift.tokenPrompt')}</p>
<SecretInput value={token} onChange={setToken} placeholder={t('sendGift.tokenPlaceholder')} />
<hr />
{/* Catálogo de objetos: cuadrícula centrada de 4 por fila + paginación */}
<h3 className="first-brown centered">Objetos disponibles</h3>
<h3 className="first-brown centered">{t('sendGift.availableItems')}</h3>
<div className="gift-grid">
{pageItems.map((it) => (
<div className="item-box" key={it.id}>
@@ -183,7 +185,7 @@ export function SendGiftForm({
{it.price} {currency}
</div>
<button type="button" className="store-add-button" onClick={() => addItem(it)}>
Añadir
{t('sendGift.add')}
</button>
</div>
))}
@@ -191,7 +193,7 @@ export function SendGiftForm({
{totalPages > 1 && (
<div className="gift-pagination">
<button type="button" onClick={() => setPage((p) => Math.max(1, Math.min(totalPages, p) - 1))} disabled={currentPage === 1}>
Atrás
{t('sendGift.back')}
</button>
{Array.from({ length: totalPages }, (_, i) => i + 1).map((n) => (
<button
@@ -204,16 +206,16 @@ export function SendGiftForm({
</button>
))}
<button type="button" onClick={() => setPage((p) => Math.min(totalPages, Math.min(totalPages, p) + 1))} disabled={currentPage === totalPages}>
Siguiente
{t('sendGift.next')}
</button>
</div>
)}
<hr />
{/* Carrito */}
<h3 className="first-brown">Carrito</h3>
<h3 className="first-brown">{t('sendGift.cart')}</h3>
{cart.size === 0 ? (
<p className="second-brown">Aún no has añadido objetos.</p>
<p className="second-brown">{t('sendGift.cartEmpty')}</p>
) : (
<table className="cart-list max-center-table">
<tbody>
@@ -248,7 +250,7 @@ export function SendGiftForm({
<div className="centered">
<br />
<p>
Total: <span className="yellow-info">{total} {currency}</span> (SumUp)
{t.rich('sendGift.total', { total, currency, s: (c) => <span className="yellow-info">{c}</span> })}
</p>
<br />
<button
@@ -256,7 +258,7 @@ export function SendGiftForm({
className="store-send-button"
disabled={busy || cart.size === 0 || !source || !destination.trim() || !token.trim()}
>
{busy ? 'Enviando regalo' : 'ENVIAR REGALO'}
{busy ? t('sendGift.submitting') : t('sendGift.submit')}
</button>
</div>
</form>
+18 -17
View File
@@ -1,7 +1,7 @@
'use client'
import { useState } from 'react'
import { useLocale } from 'next-intl'
import { useLocale, useTranslations } from 'next-intl'
import { Link } from '@/i18n/navigation'
const SERVER_NAME = 'NovaWoW'
@@ -27,6 +27,7 @@ export function SiteHeader({
playersHref: string
}) {
const locale = useLocale()
const t = useTranslations('UI')
const [open, setOpen] = useState(false)
return (
@@ -46,21 +47,21 @@ export function SiteHeader({
</a>
{loggedIn ? (
<Link href="/">INICIO</Link>
<Link href="/">{t('header.home')}</Link>
) : (
<Link href="/register">CREAR CUENTA</Link>
<Link href="/register">{t('header.register')}</Link>
)}
<div className="nav-dropdown">
<div className="nav-dropdown-btn">
DESCARGAS <i className="fas fa-caret-down"></i>
{t('header.downloads')} <i className="fas fa-caret-down"></i>
</div>
<div className="nav-dropdown-content">
<p>
<Link href="/download-client">CLIENTE</Link>
<Link href="/download-client">{t('header.client')}</Link>
</p>
<p>
<Link href="/download-addons">ADDONS</Link>
<Link href="/download-addons">{t('header.addons')}</Link>
</p>
</div>
</div>
@@ -71,14 +72,14 @@ export function SiteHeader({
</div>
<div className="nav-dropdown-content">
<p>
<Link href="/changelogs">CHANGELOG</Link>
<Link href="/changelogs">{t('header.changelog')}</Link>
</p>
<p>
<Link href={realmHref}>REINO</Link>
<Link href={realmHref}>{t('header.realm')}</Link>
</p>
{loggedIn && (
<p>
<Link href={playersHref}>JUGADORES</Link>
<Link href={playersHref}>{t('header.players')}</Link>
</p>
)}
</div>
@@ -86,23 +87,23 @@ export function SiteHeader({
<div className="nav-dropdown">
<div className="nav-dropdown-btn">
COMUNIDAD <i className="fas fa-caret-down"></i>
{t('header.community')} <i className="fas fa-caret-down"></i>
</div>
<div className="nav-dropdown-content">
<p>
<Link href="/forum">FOROS</Link>
<Link href="/forum">{t('header.forums')}</Link>
</p>
<p>
<a href="https://wotlk.novawow.com" target="_blank" rel="noopener noreferrer">
WOTLK DB
{t('header.wotlkDb')}
</a>
</p>
<p>
<Link href="/content-creators">VIDEOS</Link>
<Link href="/content-creators">{t('header.videos')}</Link>
</p>
{loggedIn && (
<p>
<Link href="/help">AYUDA</Link>
<Link href="/help">{t('header.help')}</Link>
</p>
)}
</div>
@@ -115,16 +116,16 @@ export function SiteHeader({
</div>
<div className="nav-dropdown-content">
<p>
<Link href="/my-account">MI CUENTA</Link>
<Link href="/my-account">{t('header.myAccount')}</Link>
</p>
<p>
<a href={`/${locale}/log-out`}>DESCONECTAR</a>
<a href={`/${locale}/log-out`}>{t('header.logout')}</a>
</p>
</div>
</div>
) : (
<Link href="/login" className="last">
CONECTAR
{t('header.login')}
</Link>
)}
</div>
+5 -2
View File
@@ -1,11 +1,14 @@
import { getTranslations } from 'next-intl/server'
/**
* Barra de redes sociales — réplica del partial Django `partials/social.html`
* con las clases del tema real (social-media, social-button + marca).
*/
export function Social() {
export async function Social() {
const t = await getTranslations('UI')
return (
<div className="social-media">
<span className="big-font">SÍGUENOS EN</span>
<span className="big-font">{t('social.followUs')}</span>
<a
href="https://www.facebook.com/NovaWoW/"
className="social-button social-button-responsive facebook"
+53 -47
View File
@@ -1,46 +1,47 @@
'use client'
import { useState } from 'react'
import { useTranslations } from 'next-intl'
import { Turnstile } from '@/components/Turnstile'
import { CharacterSelect, type CharOption } from '@/components/CharacterSelect'
function tradeError(error?: string): string {
function tradeErrorKey(error?: string): string {
switch (error) {
case 'missingFields':
return 'Rellena todos los campos.'
return 'missingFields'
case 'invalidAmount':
return 'Las cantidades deben ser números enteros entre 1 y 99999.'
return 'invalidAmount'
case 'characterNotFound':
case 'characterNotOwned':
return 'El personaje seleccionado no es válido.'
return 'characterInvalid'
case 'captcha':
return 'Verifica que no eres un robot.'
return 'captcha'
case 'wrongPassword':
return 'La contraseña de tu cuenta es incorrecta.'
return 'wrongPassword'
case 'invalidToken':
return 'El token de seguridad no es válido.'
return 'invalidToken'
case 'insufficientPoints':
return 'No tienes suficientes PD disponibles para crear este código.'
return 'insufficientPoints'
case 'codeNotFound':
return 'No existe ningún código con ese ID.'
return 'codeNotFound'
case 'codeUnavailable':
return 'El código ya no está disponible (canjeado o expirado).'
return 'codeUnavailable'
case 'cannotRedeemOwn':
return 'No puedes canjear tu propio código.'
return 'cannotRedeemOwn'
case 'characterOnline':
return 'El personaje debe estar desconectado para poder cobrar el oro.'
return 'characterOnline'
case 'insufficientGold':
return 'El personaje no tiene suficiente oro.'
return 'insufficientGold'
case 'creatorInsufficient':
return 'El creador del código ya no tiene suficientes PD.'
return 'creatorInsufficient'
case 'locked':
return 'Espera unos segundos antes de canjear otro código.'
return 'locked'
case 'deliveryFailed':
return 'No se pudo entregar el oro (servidor de juego no disponible). Inténtalo de nuevo más tarde.'
return 'deliveryFailed'
case 'notAuthenticated':
return 'Tu sesión ha expirado. Inicia sesión de nuevo.'
return 'notAuthenticated'
default:
return 'No se pudo completar la operación. Inténtalo de nuevo.'
return 'generic'
}
}
@@ -80,6 +81,8 @@ function PasswordInput({
/* ---------------------------------- VENTA ---------------------------------- */
function SellForm({ characters, visible }: { characters: CharOption[]; visible: boolean }) {
const t = useTranslations('Points')
const tradeError = (error?: string) => t(`trade.errors.${tradeErrorKey(error)}`)
const [points, setPoints] = useState('')
const [gold, setGold] = useState('')
const [character, setCharacter] = useState('')
@@ -113,7 +116,7 @@ function SellForm({ characters, visible }: { characters: CharOption[]; visible:
const data: { success?: boolean; error?: string; code?: { code: string; points: number; gold: number } } = await res.json()
if (data.success && data.code) {
setCreated(data.code)
setMessage({ ok: true, text: '¡Código creado!' })
setMessage({ ok: true, text: t('trade.codeCreated') })
setPoints('')
setGold('')
setPassword('')
@@ -133,18 +136,18 @@ function SellForm({ characters, visible }: { characters: CharOption[]; visible:
<form onSubmit={handleSubmit} acceptCharset="utf-8">
<table className="middle-center-table">
<tbody>
<tr><td>¿Cuánto valdrá el código en PD?</td></tr>
<tr><td><input type="number" maxLength={5} min="1" max="99999" step="1" value={points} onChange={(e) => setPoints(e.target.value)} placeholder="Valor del código en PD" required /></td></tr>
<tr><td>¿Cuánto costará el código en oro?</td></tr>
<tr><td><input type="number" maxLength={5} min="1" max="99999" step="1" value={gold} onChange={(e) => setGold(e.target.value)} placeholder="Valor del código en oro" required /></td></tr>
<tr><td>¿A qué personaje se enviará el oro?</td></tr>
<tr><td>{t('trade.sellPointsLabel')}</td></tr>
<tr><td><input type="number" maxLength={5} min="1" max="99999" step="1" value={points} onChange={(e) => setPoints(e.target.value)} placeholder={t('trade.sellPointsPlaceholder')} required /></td></tr>
<tr><td>{t('trade.sellGoldLabel')}</td></tr>
<tr><td><input type="number" maxLength={5} min="1" max="99999" step="1" value={gold} onChange={(e) => setGold(e.target.value)} placeholder={t('trade.sellGoldPlaceholder')} required /></td></tr>
<tr><td>{t('trade.sellCharLabel')}</td></tr>
<tr><td>
<CharacterSelect characters={characters} value={character} onChange={setCharacter} placeholder="Selecciona un personaje" />
<CharacterSelect characters={characters} value={character} onChange={setCharacter} placeholder={t('trade.selectCharacter')} />
</td></tr>
<tr><td><br /><PasswordInput id="password-sell" value={password} onChange={setPassword} placeholder="Contraseña de tu cuenta" maxLength={16} /></td></tr>
<tr><td><input type="text" maxLength={6} value={token} onChange={(e) => setToken(e.target.value)} placeholder="Token de seguridad" required /></td></tr>
<tr><td><br /><PasswordInput id="password-sell" value={password} onChange={setPassword} placeholder={t('trade.passwordPlaceholder')} maxLength={16} /></td></tr>
<tr><td><input type="text" maxLength={6} value={token} onChange={(e) => setToken(e.target.value)} placeholder={t('trade.tokenPlaceholder')} required /></td></tr>
<tr><td><Turnstile onVerify={setCaptcha} /></td></tr>
<tr><td><button type="submit" className="trade-points-sell-button" disabled={busy}>{busy ? 'Creando…' : 'Crear código'}</button></td></tr>
<tr><td><button type="submit" className="trade-points-sell-button" disabled={busy}>{busy ? t('trade.creating') : t('trade.createButton')}</button></td></tr>
</tbody>
</table>
</form>
@@ -153,11 +156,11 @@ function SellForm({ characters, visible }: { characters: CharOption[]; visible:
{message && <span className={message.ok ? 'ok-form-response' : 'red-form-response'}>{message.text}</span>}
{created && (
<fieldset>
<legend>Código creado</legend>
<legend>{t('trade.createdLegend')}</legend>
<div className="separate2">
<p>ID del código: <span className="green-info2">{created.code}</span></p>
<p>Valor: <span>{created.points} PD</span> por <span>{created.gold} de oro</span></p>
<p className="second-brown">Envía este ID al comprador. El código caduca en 1 hora.</p>
<p>{t('trade.codeIdLabel')} <span className="green-info2">{created.code}</span></p>
<p>{t.rich('trade.createdValue', { points: created.points, gold: created.gold, s: (c) => <span>{c}</span> })}</p>
<p className="second-brown">{t('trade.createdHint')}</p>
</div>
</fieldset>
)}
@@ -168,6 +171,8 @@ function SellForm({ characters, visible }: { characters: CharOption[]; visible:
/* ---------------------------------- COMPRA --------------------------------- */
function BuyForm({ characters, visible }: { characters: CharOption[]; visible: boolean }) {
const t = useTranslations('Points')
const tradeError = (error?: string) => t(`trade.errors.${tradeErrorKey(error)}`)
const [code, setCode] = useState('')
const [character, setCharacter] = useState('')
const [password, setPassword] = useState('')
@@ -192,7 +197,7 @@ function BuyForm({ characters, visible }: { characters: CharOption[]; visible: b
const data: { success?: boolean; error?: string; points?: number; gold?: number } = await res.json()
if (data.success && data.points != null && data.gold != null) {
setCheckInfo({ points: data.points, gold: data.gold })
setMessage({ ok: true, text: `Este código vale ${data.points} PD y cuesta ${data.gold} de oro.` })
setMessage({ ok: true, text: t('trade.checkResult', { points: data.points, gold: data.gold }) })
} else {
setMessage({ ok: false, text: tradeError(data.error) })
}
@@ -206,7 +211,7 @@ function BuyForm({ characters, visible }: { characters: CharOption[]; visible: b
async function handleRedeem(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault()
if (busy) return
if (!window.confirm('Vas a canjear este código. Esta acción no es reversible. ¿Deseas continuar?')) return
if (!window.confirm(t('trade.redeemConfirm'))) return
setBusy(true)
setMessage(null)
try {
@@ -218,7 +223,7 @@ function BuyForm({ characters, visible }: { characters: CharOption[]; visible: b
})
const data: { success?: boolean; error?: string; points?: number; gold?: number } = await res.json()
if (data.success) {
setMessage({ ok: true, text: `¡Canje correcto! Recibiste ${data.points} PD y se retiraron ${data.gold} de oro de ${character}.` })
setMessage({ ok: true, text: t('trade.redeemSuccess', { points: data.points ?? 0, gold: data.gold ?? 0, character }) })
setCode('')
setPassword('')
setToken('')
@@ -238,20 +243,20 @@ function BuyForm({ characters, visible }: { characters: CharOption[]; visible: b
<form onSubmit={handleRedeem} acceptCharset="utf-8">
<table className="middle-center-table">
<tbody>
<tr><td>Ingresa el ID del código de PD</td></tr>
<tr><td><input type="text" maxLength={25} value={code} onChange={(e) => setCode(e.target.value)} placeholder="Código de PD" required /></td></tr>
<tr><td>{t('trade.buyCodeLabel')}</td></tr>
<tr><td><input type="text" maxLength={25} value={code} onChange={(e) => setCode(e.target.value)} placeholder={t('trade.buyCodePlaceholder')} required /></td></tr>
{checkInfo && (
<tr><td><p className="green-info2">Vale {checkInfo.points} PD · Cuesta {checkInfo.gold} de oro</p></td></tr>
<tr><td><p className="green-info2">{t('trade.checkInfo', { points: checkInfo.points, gold: checkInfo.gold })}</p></td></tr>
)}
<tr><td>¿De qué personaje se cobrará el oro?</td></tr>
<tr><td>{t('trade.buyCharLabel')}</td></tr>
<tr><td>
<CharacterSelect characters={characters} value={character} onChange={setCharacter} placeholder="Selecciona un personaje" />
<CharacterSelect characters={characters} value={character} onChange={setCharacter} placeholder={t('trade.selectCharacter')} />
</td></tr>
<tr><td><br /><PasswordInput id="password-buy" value={password} onChange={setPassword} placeholder="Contraseña de tu cuenta" maxLength={16} /></td></tr>
<tr><td><input type="text" maxLength={6} value={token} onChange={(e) => setToken(e.target.value)} placeholder="Token de seguridad" required /></td></tr>
<tr><td><br /><PasswordInput id="password-buy" value={password} onChange={setPassword} placeholder={t('trade.passwordPlaceholder')} maxLength={16} /></td></tr>
<tr><td><input type="text" maxLength={6} value={token} onChange={(e) => setToken(e.target.value)} placeholder={t('trade.tokenPlaceholder')} required /></td></tr>
<tr><td><Turnstile onVerify={setCaptcha} /></td></tr>
<tr><td><button type="button" className="trade-points-check-button" disabled={busy} onClick={handleCheck}>Chequear código</button></td></tr>
<tr><td><button type="submit" className="trade-points-buy-button" disabled={busy}>{busy ? 'Procesando…' : 'Canjear código'}</button></td></tr>
<tr><td><button type="button" className="trade-points-check-button" disabled={busy} onClick={handleCheck}>{t('trade.checkButton')}</button></td></tr>
<tr><td><button type="submit" className="trade-points-buy-button" disabled={busy}>{busy ? t('trade.processing') : t('trade.redeemButton')}</button></td></tr>
</tbody>
</table>
</form>
@@ -264,6 +269,7 @@ function BuyForm({ characters, visible }: { characters: CharOption[]; visible: b
}
export function TradePointsForm({ characters }: { characters: CharOption[] }) {
const t = useTranslations('Points')
// Como el original: al cargar no se muestra ningún formulario; se revela al
// pulsar VENTA o COMPRA.
const [active, setActive] = useState<'sell' | 'buy' | null>(null)
@@ -272,7 +278,7 @@ export function TradePointsForm({ characters }: { characters: CharOption[] }) {
return (
<div className="centered">
<br />
<span>No tienes personajes en esta cuenta.</span>
<span>{t('trade.noCharacters')}</span>
</div>
)
}
@@ -287,7 +293,7 @@ export function TradePointsForm({ characters }: { characters: CharOption[] }) {
className={`trade-option-button${active === 'sell' ? ' dnt-selected' : ''}`}
onClick={() => setActive('sell')}
>
VENTA
{t('trade.sellTab')}
</button>
<button
type="button"
@@ -295,7 +301,7 @@ export function TradePointsForm({ characters }: { characters: CharOption[] }) {
className={`trade-option-button${active === 'buy' ? ' dnt-selected' : ''}`}
onClick={() => setActive('buy')}
>
COMPRA
{t('trade.buyTab')}
</button>
<br />
<SellForm characters={characters} visible={active === 'sell'} />
+14 -17
View File
@@ -1,30 +1,27 @@
'use client'
import { useState } from 'react'
import { useTranslations } from 'next-intl'
import { useRouter } from '@/i18n/navigation'
function transferError(error?: string): string {
function transferErrorKey(error?: string): string {
switch (error) {
case 'missingFields':
return 'Rellena el personaje de destino, la cantidad y el token de seguridad.'
case 'invalidAmount':
return 'La cantidad debe ser un número entero mayor que 0.'
case 'invalidToken':
return 'El token de seguridad no es válido.'
case 'characterNotFound':
return 'No existe ningún personaje con ese nombre.'
case 'sameAccount':
return 'No puedes transferirte PD a tu propia cuenta.'
case 'insufficientFunds':
return 'No tienes suficientes PD para realizar esta transferencia.'
case 'notAuthenticated':
return 'Tu sesión ha expirado. Inicia sesión de nuevo.'
return error
default:
return 'No se pudo completar la transferencia. Inténtalo de nuevo.'
return 'generic'
}
}
export function TransferDPointsForm({ balance }: { balance: number }) {
const t = useTranslations('Points')
const transferError = (error?: string) => t(`transferDp.errors.${transferErrorKey(error)}`)
const router = useRouter()
const [character, setCharacter] = useState('')
const [amount, setAmount] = useState('')
@@ -37,7 +34,7 @@ export function TransferDPointsForm({ balance }: { balance: number }) {
<div className="centered">
<br />
<br />
<span>No tienes PD</span>
<span>{t('transferDp.noPd')}</span>
<br />
</div>
)
@@ -59,7 +56,7 @@ export function TransferDPointsForm({ balance }: { balance: number }) {
setMessage({ ok: false, text: transferError('insufficientFunds') })
return
}
if (!window.confirm(`Vas a transferir ${n} PD al personaje "${character.trim()}".\nEsta acción no es reversible. ¿Deseas continuar?`)) return
if (!window.confirm(t('transferDp.confirm', { n, character: character.trim() }))) return
setBusy(true)
setMessage(null)
@@ -72,7 +69,7 @@ export function TransferDPointsForm({ balance }: { balance: number }) {
})
const data: { success?: boolean; error?: string } = await res.json()
if (data.success) {
setMessage({ ok: true, text: `Has transferido ${n} PD al personaje "${character.trim()}".` })
setMessage({ ok: true, text: t('transferDp.success', { n, character: character.trim() }) })
setCharacter('')
setAmount('')
setToken('')
@@ -90,12 +87,12 @@ export function TransferDPointsForm({ balance }: { balance: number }) {
return (
<div className="centered">
<br />
<p className="yellow-info centered">Tienes {balance} PD disponibles</p>
<p className="yellow-info centered">{t('transferDp.available', { balance })}</p>
<br />
<form onSubmit={handleSubmit} acceptCharset="utf-8">
<input
type="text"
placeholder="Nombre del personaje de destino"
placeholder={t('transferDp.characterPlaceholder')}
value={character}
onChange={(e) => setCharacter(e.target.value)}
required
@@ -103,7 +100,7 @@ export function TransferDPointsForm({ balance }: { balance: number }) {
<br />
<input
type="number"
placeholder="Cantidad de PD"
placeholder={t('transferDp.amountPlaceholder')}
min="1"
max={balance}
step="1"
@@ -114,7 +111,7 @@ export function TransferDPointsForm({ balance }: { balance: number }) {
<br />
<input
type="text"
placeholder="Token de seguridad"
placeholder={t('transferDp.tokenPlaceholder')}
maxLength={6}
value={token}
onChange={(e) => setToken(e.target.value)}
@@ -122,7 +119,7 @@ export function TransferDPointsForm({ balance }: { balance: number }) {
/>
<br />
<button type="submit" className="transfer-button" disabled={busy}>
{busy ? 'Transfiriendo…' : 'TRANSFERIR PD'}
{busy ? t('transferDp.transferring') : t('transferDp.transferButton')}
</button>
</form>
<hr />
+11 -9
View File
@@ -1,6 +1,7 @@
'use client'
import { useState } from 'react'
import { useTranslations } from 'next-intl'
import { CharacterSelect, type CharOption } from '@/components/CharacterSelect'
/** Campo con ojo para mostrar/ocultar (contraseña / token). */
@@ -42,6 +43,7 @@ function SecretInput({
}
export function TransferForm({ characters, price }: { characters: CharOption[]; price: number }) {
const t = useTranslations('CharService')
const [character, setCharacter] = useState('')
const [destination, setDestination] = useState('')
const [password, setPassword] = useState('')
@@ -52,7 +54,7 @@ export function TransferForm({ characters, price }: { characters: CharOption[];
async function handleSubmit(e: React.FormEvent) {
e.preventDefault()
if (busy || !character || !destination.trim() || !password || !token.trim()) return
if (!window.confirm(`¿Estás seguro de transferir el personaje "${character}" a la cuenta ${destination.trim()} por ${price} € (SumUp)?`)) return
if (!window.confirm(t('transfer.confirm', { character, destination: destination.trim(), price }))) return
setBusy(true)
setError(null)
try {
@@ -71,30 +73,30 @@ export function TransferForm({ characters, price }: { characters: CharOption[];
const data: { success?: boolean; url?: string; message?: string } = await res.json()
if (data.success && data.url) window.location.href = data.url
else {
setError(data.message || 'No se pudo iniciar la transferencia. Inténtalo de nuevo.')
setError(data.message || t('transfer.errorGeneric'))
setBusy(false)
}
} catch {
setError('Algo ha salido mal. Inténtalo más tarde.')
setError(t('transfer.errorUnexpected'))
setBusy(false)
}
}
if (characters.length === 0) return <p className="second-brown">No tienes personajes disponibles.</p>
if (characters.length === 0) return <p className="second-brown">{t('transfer.noCharacters')}</p>
return (
<div className="centered">
<form id="uw-transfer-character-form" onSubmit={handleSubmit} acceptCharset="utf-8">
<CharacterSelect characters={characters} value={character} onChange={setCharacter} placeholder="Selecciona un personaje" />
<CharacterSelect characters={characters} value={character} onChange={setCharacter} placeholder={t('transfer.selectPlaceholder')} />
<br />
<input type="text" maxLength={32} placeholder="Cuenta de destino" value={destination} onChange={(e) => setDestination(e.target.value)} required />
<input type="text" maxLength={32} placeholder={t('transfer.destinationPlaceholder')} value={destination} onChange={(e) => setDestination(e.target.value)} required />
<br />
<SecretInput id="password" value={password} onChange={setPassword} placeholder="Contraseña de tu cuenta" maxLength={16} toggleClass="toggle-password" />
<SecretInput id="password" value={password} onChange={setPassword} placeholder={t('transfer.passwordPlaceholder')} maxLength={16} toggleClass="toggle-password" />
<br />
<SecretInput id="security-token" value={token} onChange={setToken} placeholder="Token de seguridad" maxLength={6} toggleClass="toggle-token" />
<SecretInput id="security-token" value={token} onChange={setToken} placeholder={t('transfer.tokenPlaceholder')} maxLength={6} toggleClass="toggle-token" />
<br />
<button type="submit" className="transfer-button" disabled={busy || !character || !destination.trim() || !password || !token.trim()}>
{busy ? 'Transfiriendo personaje' : 'TRANSFERIR PERSONAJE'}
{busy ? t('transfer.submitting') : t('transfer.submit')}
</button>
</form>
<hr />
+22 -18
View File
@@ -1,6 +1,7 @@
'use client'
import { useState } from 'react'
import { useTranslations } from 'next-intl'
const LOGOS = '/nw-themes/nw-ryu/nw-images/nw-logos'
@@ -18,13 +19,14 @@ async function post(action: string, extra: Record<string, string>): Promise<{ su
/** Ojo para mostrar/ocultar un campo de contraseña/token. */
function EyeToggle({ shown, onToggle }: { shown: boolean; onToggle: () => void }) {
const t = useTranslations('UI')
return (
<span
className={`far ${shown ? 'fa-eye-slash' : 'fa-eye'} toggle-password`}
onClick={onToggle}
role="button"
tabIndex={0}
aria-label={shown ? 'Ocultar' : 'Mostrar'}
aria-label={shown ? t('twofa.eyeHide') : t('twofa.eyeShow')}
></span>
)
}
@@ -41,6 +43,7 @@ export function TwoFactorLogin({ enabled }: { enabled: boolean }) {
/* ------------------------------ Activar + QR ------------------------------- */
function ActivateFlow() {
const t = useTranslations('UI')
const [password, setPassword] = useState('')
const [showPassword, setShowPassword] = useState(false)
const [token, setToken] = useState('')
@@ -66,7 +69,7 @@ function ActivateFlow() {
setMsg({ success: !!d.success, text: d.message })
if (d.success && d.qrCodeUrl && d.secretKey) setQr({ qrCodeUrl: d.qrCodeUrl, secretKey: d.secretKey })
} catch {
setMsg({ success: false, text: 'Algo ha salido mal. Por favor intente más tarde.' })
setMsg({ success: false, text: t('twofa.errGeneric') })
} finally {
setBusy(false)
}
@@ -82,7 +85,7 @@ function ActivateFlow() {
setVMsg({ success: !!d.success, text: d.message })
if (d.success) setDone(true)
} catch {
setVMsg({ success: false, text: 'Algo ha salido mal. Por favor intente más tarde.' })
setVMsg({ success: false, text: t('twofa.errGeneric') })
} finally {
setVBusy(false)
}
@@ -108,7 +111,7 @@ function ActivateFlow() {
maxLength={16}
name="password"
id="password"
placeholder="Contraseña"
placeholder={t('twofa.passwordPlaceholder')}
value={password}
onChange={(e) => setPassword(e.target.value)}
disabled={!!qr}
@@ -123,7 +126,7 @@ function ActivateFlow() {
maxLength={6}
name="security-token"
id="security-token"
placeholder="Token de seguridad"
placeholder={t('twofa.tokenPlaceholder')}
value={token}
onChange={(e) => setToken(e.target.value)}
disabled={!!qr}
@@ -135,7 +138,7 @@ function ActivateFlow() {
<tr>
<td>
<button type="submit" className="activate-2fa-button" disabled={busy}>
{busy ? 'Activando 2FA' : 'Activar 2FA'}
{busy ? t('twofa.activating') : t('twofa.activate')}
</button>
</td>
</tr>
@@ -152,7 +155,7 @@ function ActivateFlow() {
{qr && (
<div id="qr-code-container">
<p>Paso 1) Descarga la aplicación de autenticación:</p>
<p>{t('twofa.step1')}</p>
<a href="https://play.google.com/store/apps/details?id=com.google.android.apps.authenticator2" target="_blank" rel="noopener noreferrer" title="Google Authenticator (Android)">
<img src={`${LOGOS}/google-play.png`} alt="Google Play" className="google-play-logo" />
</a>
@@ -161,16 +164,16 @@ function ActivateFlow() {
</a>
<br />
<br />
<p>Paso 2) Escanea este código QR con tu aplicación de autenticación:</p>
<p>{t('twofa.step2')}</p>
<img id="qr-code" src={qr.qrCodeUrl} alt="QR Code" />
<br />
<br />
<p>Si no puedes escanear el código QR copia y pega esta clave en tu aplicación de autenticación:</p>
<p>{t('twofa.copyKeyNote')}</p>
<div className="copy-secret-container">
<input type="text" id="secret-key" className="centered" value={qr.secretKey} readOnly />
<span
className={`fa ${copied ? 'fa-check' : 'fa-copy'} copy-btn second-brown`}
title={copied ? '¡Copiado!' : 'Copiar clave'}
title={copied ? t('twofa.copied') : t('twofa.copyKey')}
onClick={copySecret}
role="button"
tabIndex={0}
@@ -178,7 +181,7 @@ function ActivateFlow() {
</div>
<br />
<br />
<p>Paso 3) Ingresa el código generado por tu aplicación de autenticación:</p>
<p>{t('twofa.step3')}</p>
<form onSubmit={verify} id="uw-2fa-verify-form" autoComplete="off">
<table className="middle-center-table">
<tbody>
@@ -189,7 +192,7 @@ function ActivateFlow() {
maxLength={6}
name="authenticator-code"
id="authenticator-code"
placeholder="Código de 6 dígitos"
placeholder={t('twofa.codePlaceholder')}
required
className="centered"
inputMode="numeric"
@@ -203,7 +206,7 @@ function ActivateFlow() {
<tr>
<td>
<button type="submit" className="verify-2fa-button" disabled={vBusy || done}>
{done ? 'Verificado ✔️' : vBusy ? 'Verificando...' : 'Verificar código'}
{done ? t('twofa.verified') : vBusy ? t('twofa.verifying') : t('twofa.verifyCode')}
</button>
</td>
</tr>
@@ -225,6 +228,7 @@ function ActivateFlow() {
/* ------------------------------- Desactivar -------------------------------- */
function DisableForm() {
const t = useTranslations('UI')
const [code, setCode] = useState('')
const [busy, setBusy] = useState(false)
const [msg, setMsg] = useState<Msg>(null)
@@ -240,7 +244,7 @@ function DisableForm() {
setMsg({ success: !!d.success, text: d.message })
if (d.success) setDone(true)
} catch {
setMsg({ success: false, text: 'Algo ha salido mal. Por favor intente más tarde.' })
setMsg({ success: false, text: t('twofa.errGeneric') })
} finally {
setBusy(false)
}
@@ -248,8 +252,8 @@ function DisableForm() {
return (
<>
<p className="green-info"><i className="fas fa-shield-alt"></i> La verificación en 2 pasos está activada en esta cuenta.</p>
<p>Para desactivarla, introduce un código actual de tu aplicación de autenticación.</p>
<p className="green-info"><i className="fas fa-shield-alt"></i> {t('twofa.enabledNotice')}</p>
<p>{t('twofa.disableInstruction')}</p>
<br />
<form onSubmit={disable} id="uw-2fa-disable-form">
<table className="middle-center-table">
@@ -260,7 +264,7 @@ function DisableForm() {
type="text"
maxLength={6}
name="authenticator-code"
placeholder="Código de 6 dígitos"
placeholder={t('twofa.codePlaceholder')}
required
className="centered"
inputMode="numeric"
@@ -274,7 +278,7 @@ function DisableForm() {
<tr>
<td>
<button type="submit" className="deactivate-2fa-button" disabled={busy || done}>
{done ? '2FA desactivado ✔️' : busy ? 'Desactivando...' : 'Desactivar 2FA'}
{done ? t('twofa.disabled') : busy ? t('twofa.disabling') : t('twofa.disable')}
</button>
</td>
</tr>
+1144 -2
View File
File diff suppressed because it is too large Load Diff
+1144 -2
View File
File diff suppressed because it is too large Load Diff