Files
NightSpire/web-next/components/RecruitPanel.tsx
T
Inna fa7897c195 Prefijos del tema nw- y uw- -> ns- (carpetas, ficheros, clases y rutas)
Renombrado completo de los prefijos heredados: 19 carpetas, 11 ficheros y 318
referencias en 35 ficheros de código, más las clases y las rutas url() de dentro
del CSS del tema. Se usa git mv para conservar el historial.

También fuera del código, que un sed no ve:
- BD: votesite.image_url (4 filas) apuntaba a /nw-themes/...
- Ficheros con la marca vieja en el NOMBRE: novawow-maintenance.webp ->
  nightspire-maintenance.webp (lo usa la página de mantenimiento) y
  store_novawow_response.js.

⚠ Alias en Caddy /nw-themes/* -> /ns-themes/*: los correos ENVIADOS antes del
rebranding llevan esas rutas escritas y están en las bandejas de los usuarios.
Sin el alias, sus imágenes se romperían. Verificado que las rutas viejas siguen
sirviendo 200.

Nota: ns-js/ y ns-js-handlers/ son CÓDIGO MUERTO (manejadores jQuery del portal
Django, que ya se borró). Se midió en el navegador: la web no pide ni un solo JS
del tema. Se renombran igualmente por consistencia, pero son candidatos a
borrarse.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 15:50:43 +00:00

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="ns-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>
)}
</>
)
}