Files
NightSpire/web-next/components/RecruitClaim.tsx
T
Inna cc158d3819 web-next: migrar toda la UI al tema real nw-ryu
Reescritura completa del frontend Next.js del sistema visual Tailwind
"simulado" al tema Django original (nw-ryu), para paridad pixel con la
web actual antes del cutover.

- Tema real: copia de static/nw-themes/nw-ryu + favicons a public/, el
  layout carga el novavow-style.css real de ultimo (gana la cascada sobre
  Tailwind) + Font Awesome.
- Shell replicando los partials Django: SiteHeader, Video, Social, Footer,
  ServerClock; home con estructura real (main-page/middle-content/...).
- Helpers reutilizables: PageShell (main-page > middle-content > body-content
  > title-content) y ServiceBox (title-box-content + back-to-account).
- Paginas migradas a clases reales del tema (fieldset/tool-button/char-box/
  item-box/info-box-light/max-center-table/alert-message/botones reales),
  eliminando el markup Tailwind (.nw-btn/.nw-card/.nw-input):
  auth (login/register/recover/reset/select-account/activate),
  cuenta + servicios de personaje (revive/unstuck/rename/customize/
  change-race/change-faction/level-up/gold/transfer + pago Stripe),
  ajustes (change-password/change-email/security-token),
  comunidad (vote-points/recruit/battlepay), foro completo, y
  admin (indice + 7 secciones + los Admin*Manager).
- Se conserva el bilingue (next-intl); claves nuevas en messages/es|en.json.

Verificado: typecheck + build OK; rutas protegidas 307->login; sin
MISSING_MESSAGE; cero Tailwind residual (solo .nw-tool-btn/.nw-page,
clases propias en globals.css).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 09:54:58 +00:00

107 lines
3.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use client'
import { useState } from 'react'
import { useTranslations } from 'next-intl'
import { useRouter } from '@/i18n/navigation'
import type { RecruitRewardView } from '@/lib/recruit-claim'
export function RecruitClaim({
rewards,
characters,
level80Count,
}: {
rewards: RecruitRewardView[]
characters: string[]
level80Count: number
}) {
const t = useTranslations('RecruitClaim')
const router = useRouter()
const [character, setCharacter] = useState(characters[0] ?? '')
const [busyId, setBusyId] = useState<number | null>(null)
const [msg, setMsg] = useState<{ ok: boolean; text: string } | null>(null)
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()
setMsg({ ok: Boolean(d.success), text: t(d.message ?? 'deliveryError', { reward: reward.reward_name, character }) })
if (d.success) router.refresh()
} catch {
setMsg({ ok: false, text: t('deliveryError') })
} finally {
setBusyId(null)
}
}
return (
<div>
<div className="info-box-light separate">
<p className="second-brown">
{t('level80Friends')}: <span className="yellow-info">{level80Count}</span>
</p>
{characters.length > 0 ? (
<p>
<span className="second-brown">{t('deliverTo')} </span>
<select value={character} onChange={(e) => setCharacter(e.target.value)}>
{characters.map((c) => (
<option key={c} value={c}>
{c}
</option>
))}
</select>
</p>
) : (
<p className="second-brown">{t('noCharacters')}</p>
)}
</div>
<div className="alert-message" style={{ display: msg ? 'block' : 'none' }}>
{msg && <span className={msg.ok ? 'ok-form-response' : 'red-form-response'}>{msg.text}</span>}
</div>
{rewards.length === 0 ? (
<p className="second-brown">{t('noRewards')}</p>
) : (
<div className="centered">
{rewards.map((r) => {
const eligible = level80Count >= r.required_friends
return (
<div key={r.id} className="raf-box">
<a href={r.item_link} target="_blank" rel="noopener noreferrer" className="yellow-info">
{r.reward_name}
</a>
<p className="second-brown small-font">
{t('requires', { n: r.required_friends })} · item {r.item_id} ×{r.item_quantity}
</p>
<br />
{r.claimed ? (
<span className="green-info">
<i className="fas fa-check"></i> {t('claimed')}
</span>
) : (
<button
type="button"
onClick={() => claim(r)}
disabled={!eligible || busyId !== null || characters.length === 0}
title={!eligible ? t('notEnoughFriends') : undefined}
>
{busyId === r.id ? t('claiming') : eligible ? t('claim') : t('locked')}
</button>
)}
</div>
)
})}
</div>
)}
</div>
)
}