033ebc6861
El script de tooltips prioriza el atributo data-wowhead sobre el subdominio del
href, y su parámetro `domain` codifica idioma + versión: el 1.er segmento va al
mapa de locales ({es:6,...}) y el resto a la versión (wotlk=WRATH). Antes se
enviaba `domain=wotlk` (sin idioma) → locale 0 = inglés siempre.
Ahora `wowheadData(type,id,locale)` genera `domain=es.wotlk` en español (locale 6)
y `domain=wotlk` en inglés. Verificado: el endpoint de datos con locale 6 devuelve
los nombres en español.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
158 lines
5.3 KiB
TypeScript
158 lines
5.3 KiB
TypeScript
'use client'
|
|
|
|
import { useState } from 'react'
|
|
import { useTranslations, useLocale } from 'next-intl'
|
|
import type { RecruitRewardView } from '@/lib/recruit-claim'
|
|
import type { CharOption } from '@/components/CharacterSelect'
|
|
import { wowheadUrl, wowheadData, wowheadIcon } from '@/lib/wowhead'
|
|
|
|
function claimErrorKey(message?: string): string {
|
|
switch (message) {
|
|
case 'requirementsNotMet':
|
|
return 'recruit.errRequirements'
|
|
case 'alreadyClaimed':
|
|
return 'recruit.errAlreadyClaimed'
|
|
case 'invalidCharacter':
|
|
return 'recruit.errInvalidCharacter'
|
|
case 'notFound':
|
|
return 'recruit.errNotFound'
|
|
default:
|
|
return 'recruit.errGeneric'
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Panel de Recluta a un amigo: estadísticas de la cuenta + tabla de recompensas
|
|
* (objetivo, objeto y estado) con botón para reclamar las disponibles.
|
|
*/
|
|
export function RecruitPanel({
|
|
rewards,
|
|
characters,
|
|
level80Count,
|
|
isRecruited,
|
|
recruitedCount,
|
|
}: {
|
|
rewards: RecruitRewardView[]
|
|
characters: CharOption[]
|
|
level80Count: number
|
|
isRecruited: boolean
|
|
recruitedCount: number
|
|
}) {
|
|
const t = useTranslations('UI')
|
|
const locale = useLocale()
|
|
const [character, setCharacter] = useState(characters[0]?.name ?? '')
|
|
const selectedCss = characters.find((c) => c.name === character)?.classCss
|
|
const [busyId, setBusyId] = useState<number | null>(null)
|
|
const [msg, setMsg] = useState<{ ok: boolean; text: string } | null>(null)
|
|
|
|
const claimedCount = rewards.filter((r) => r.claimed).length
|
|
const available = rewards.filter((r) => !r.claimed && level80Count >= r.required_friends)
|
|
|
|
async function claim(reward: RecruitRewardView) {
|
|
if (busyId !== null || !character) return
|
|
setBusyId(reward.id)
|
|
setMsg(null)
|
|
try {
|
|
const res = await fetch('/api/recruit/claim', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
credentials: 'same-origin',
|
|
body: JSON.stringify({ rewardId: reward.id, character }),
|
|
})
|
|
const d: { success?: boolean; message?: string } = await res.json()
|
|
if (d.success) {
|
|
window.location.reload()
|
|
return
|
|
}
|
|
setMsg({ ok: false, text: t(claimErrorKey(d.message)) })
|
|
setBusyId(null)
|
|
} catch {
|
|
setMsg({ ok: false, text: t(claimErrorKey()) })
|
|
setBusyId(null)
|
|
}
|
|
}
|
|
|
|
function rewardCell(r: RecruitRewardView) {
|
|
const [quality, icon] = (r.icon_class || '').split(' ')
|
|
return (
|
|
<>
|
|
<a
|
|
href={wowheadUrl('item', r.item_id, locale)}
|
|
data-wowhead={wowheadData('item', r.item_id, locale)}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
className={`icontinyl ${quality || ''}`}
|
|
style={{ paddingLeft: '18px', background: `url('${wowheadIcon(icon, 'tiny')}') left center no-repeat` }}
|
|
>
|
|
{r.reward_name}
|
|
</a>
|
|
{r.item_quantity > 1 ? ` x${r.item_quantity}` : ''}
|
|
</>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<>
|
|
<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">
|
|
{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>
|
|
))}
|
|
</select>
|
|
</p>
|
|
)}
|
|
{msg && <p className={msg.ok ? 'green-info' : 'red-info'}>{msg.text}</p>}
|
|
|
|
<table className="char-center-table">
|
|
<tbody>
|
|
<tr>
|
|
<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>{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> {t('recruit.claimed')}</span>
|
|
) : eligible ? (
|
|
<button type="button" className="nw-tool-btn" onClick={() => claim(r)} disabled={busyId !== null}>
|
|
{busyId === r.id ? '…' : t('recruit.claim')}
|
|
</button>
|
|
) : (
|
|
<span>{t('recruit.unclaimed')}</span>
|
|
)}
|
|
</td>
|
|
</tr>
|
|
)
|
|
})}
|
|
</tbody>
|
|
</table>
|
|
|
|
<br />
|
|
<hr />
|
|
<p>{t('recruit.availableRewards')}</p>
|
|
<hr />
|
|
{available.length === 0 ? (
|
|
<span>{t('recruit.recruitToClaim')}</span>
|
|
) : (
|
|
<span className="green-info">
|
|
{t('recruit.readyToClaim', { count: available.length })}
|
|
</span>
|
|
)}
|
|
</>
|
|
)
|
|
}
|