Files
NightSpire/web-next/components/AdminRecruitManager.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

105 lines
4.2 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 type { RecruitReward } from '@/lib/admin-recruit'
const EMPTY = { requiredFriends: '', rewardName: '', itemId: '', itemQuantity: '1', itemLink: '', iconClass: '' }
export function AdminRecruitManager({ initial }: { initial: RecruitReward[] }) {
const t = useTranslations('Admin')
const [rewards, setRewards] = useState(initial)
const [form, setForm] = useState(EMPTY)
const [busy, setBusy] = useState(false)
function upd(k: string, v: string) {
setForm((f) => ({ ...f, [k]: v }))
}
async function create(e: React.FormEvent) {
e.preventDefault()
if (busy || !(Number(form.requiredFriends) > 0) || !form.rewardName.trim() || !(Number(form.itemId) > 0)) return
setBusy(true)
try {
const res = await fetch('/api/admin/recruit', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'same-origin',
body: JSON.stringify(form),
})
const data: { success?: boolean; id?: number } = await res.json()
if (data.success && data.id) {
setRewards(
[
...rewards,
{
id: data.id,
required_friends: Number(form.requiredFriends),
reward_name: form.rewardName,
item_id: Number(form.itemId),
item_quantity: Number(form.itemQuantity) || 1,
item_link: form.itemLink,
icon_class: form.iconClass,
},
].sort((a, b) => a.required_friends - b.required_friends),
)
setForm(EMPTY)
}
} finally {
setBusy(false)
}
}
async function remove(id: number) {
if (!confirm(t('confirmDelete'))) return
const res = await fetch(`/api/admin/recruit/${id}`, { method: 'DELETE', credentials: 'same-origin' })
if ((await res.json()).success) setRewards(rewards.filter((r) => r.id !== id))
}
return (
<div>
<form onSubmit={create} className="admin-form info-box-light separate2">
<span className="second-brown">{t('requiredFriends')}</span>
<input type="number" min="1" value={form.requiredFriends} onChange={(e) => upd('requiredFriends', e.target.value)} placeholder="1" />
<span className="second-brown">{t('rewardName')}</span>
<input value={form.rewardName} onChange={(e) => upd('rewardName', e.target.value)} placeholder="Montura épica" />
<span className="second-brown">{t('itemId')}</span>
<input type="number" min="1" value={form.itemId} onChange={(e) => upd('itemId', e.target.value)} placeholder="49284" />
<span className="second-brown">{t('itemQuantity')}</span>
<input type="number" min="1" value={form.itemQuantity} onChange={(e) => upd('itemQuantity', e.target.value)} />
<span className="second-brown">{t('itemLink')}</span>
<input value={form.itemLink} onChange={(e) => upd('itemLink', e.target.value)} placeholder="https://…" />
<span className="second-brown">{t('iconClass')}</span>
<input value={form.iconClass} onChange={(e) => upd('iconClass', e.target.value)} placeholder="icontinyl q3" />
<button type="submit" disabled={busy}>
{busy ? t('creating') : t('create')}
</button>
</form>
<br />
{rewards.length === 0 ? (
<p className="second-brown centered">{t('noRewards')}</p>
) : (
<table className="max-center-table">
<tbody>
{rewards.map((r) => (
<tr key={r.id} className="team-center-table-tr">
<td className="lefted separate">
<span className="yellow-info">{r.reward_name}</span>
<p className="third-brown small-font">
{t('requiredFriendsShort', { n: r.required_friends })} · item {r.item_id} ×{r.item_quantity}
</p>
</td>
<td className="real-info-box">
<button onClick={() => remove(r.id)} className="nw-tool-btn red-info2">
{t('delete')}
</button>
</td>
</tr>
))}
</tbody>
</table>
)}
</div>
)
}