Files
NightSpire/web-next/components/ForumCharacterPicker.tsx
T
Inna 0f6e0d514f Foro: selector para fijar el personaje del autor
- Modal "Cambiar personaje" (diseño de la web) con filtro, avatar de raza,
  nombre en color de clase y subtítulo nivel/raza/clase.
- Barra "Publicas como: X · Cambiar personaje" sobre el formulario de respuesta
  (solo con sesión iniciada).
- Preferencia persistente en acore_web.forum_character (por cuenta), validada.
- resolvePosters usa el personaje fijado si existe; si no, el de más nivel.
- API /api/forum/character (GET lista + actual, POST fijar), gated por sesión.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 08:14:54 +00:00

124 lines
4.5 KiB
TypeScript

'use client'
import { useEffect, useState } from 'react'
import { useRouter } from 'next/navigation'
import { className, classColor, raceName } from '@/lib/wow-data'
type Char = { guid: number; name: string; race: number; gender: number; class: number; level: number }
const RACE_SLUG: Record<number, string> = {
1: 'human', 2: 'orc', 3: 'dwarf', 4: 'nightelf', 5: 'scourge', 6: 'tauren',
7: 'gnome', 8: 'troll', 9: 'goblin', 10: 'bloodelf', 11: 'draenei', 22: 'worgen',
}
const raceIcon = (r: number, g: number) =>
`https://wow.zamimg.com/images/wow/icons/large/race_${RACE_SLUG[r] || 'human'}_${g === 1 ? 'female' : 'male'}.jpg`
export function ForumCharacterPicker({
locale,
initialChars,
initialCurrent,
labels,
}: {
locale: string
initialChars: Char[]
initialCurrent: number | null
labels: { postingAs: string; change: string; title: string; filter: string; empty: string; auto: string }
}) {
const router = useRouter()
const [open, setOpen] = useState(false)
const [chars] = useState<Char[]>(initialChars)
const [current, setCurrent] = useState<number | null>(initialCurrent)
const [q, setQ] = useState('')
const [saving, setSaving] = useState(false)
useEffect(() => {
if (!open) return
const onKey = (e: KeyboardEvent) => e.key === 'Escape' && setOpen(false)
window.addEventListener('keydown', onKey)
return () => window.removeEventListener('keydown', onKey)
}, [open])
if (chars.length === 0) return null
const currentChar = chars.find((c) => c.guid === current)
const shown = q.trim() ? chars.filter((c) => c.name.toLowerCase().includes(q.trim().toLowerCase())) : chars
const pick = async (guid: number) => {
if (saving) return
setSaving(true)
try {
const res = await fetch('/api/forum/character', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ guid }),
})
if (res.ok) {
setCurrent(guid)
setOpen(false)
router.refresh()
}
} finally {
setSaving(false)
}
}
return (
<div className="forum-charpick">
<span className="forum-charpick-as">
{labels.postingAs}{' '}
{currentChar ? (
<b style={{ color: classColor(currentChar.class) }}>{currentChar.name}</b>
) : (
<b className="forum-charpick-auto">{labels.auto}</b>
)}
</span>
<button type="button" className="forum-charpick-btn" onClick={() => setOpen(true)}>
<i className="fa-solid fa-user-pen" /> {labels.change}
</button>
{open && (
<div className="forum-charpick-overlay" onClick={() => setOpen(false)}>
<div className="forum-charpick-modal" onClick={(e) => e.stopPropagation()}>
<div className="forum-charpick-head">
<h3>{labels.title}</h3>
<button type="button" className="forum-charpick-close" onClick={() => setOpen(false)} aria-label="X">
<i className="fa-solid fa-xmark" />
</button>
</div>
<div className="forum-charpick-search">
<i className="fa-solid fa-magnifying-glass" />
<input value={q} onChange={(e) => setQ(e.target.value)} placeholder={labels.filter} autoFocus />
</div>
<div className="forum-charpick-list">
{shown.length === 0 ? (
<p className="forum-charpick-empty">{labels.empty}</p>
) : (
shown.map((c) => (
<button
key={c.guid}
type="button"
className={`forum-charpick-item${c.guid === current ? ' active' : ''}`}
onClick={() => pick(c.guid)}
disabled={saving}
>
<img className="forum-charpick-ava" src={raceIcon(c.race, c.gender)} alt="" loading="lazy" />
<span className="forum-charpick-info">
<span className="forum-charpick-name" style={{ color: classColor(c.class) }}>
{c.name}
</span>
<span className="forum-charpick-sub">
{c.level} {raceName(c.race, locale)} {className(c.class, locale)}
</span>
</span>
{c.guid === current && <i className="fa-solid fa-check forum-charpick-chk" />}
</button>
))
)}
</div>
</div>
</div>
)}
</div>
)
}