0f6e0d514f
- 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>
124 lines
4.5 KiB
TypeScript
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>
|
|
)
|
|
}
|