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>
This commit is contained in:
@@ -0,0 +1,123 @@
|
||||
'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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user