diff --git a/web-next/app/[locale]/gold/page.tsx b/web-next/app/[locale]/gold/page.tsx
index 0e16d54..49bcab7 100644
--- a/web-next/app/[locale]/gold/page.tsx
+++ b/web-next/app/[locale]/gold/page.tsx
@@ -22,7 +22,7 @@ export default async function GoldPage({ params }: { params: Promise<{ locale: s
return (
- c.name)} options={options} />
+ ({ name: c.name, classCss: c.classCss }))} options={options} />
)
diff --git a/web-next/app/[locale]/recruit/page.tsx b/web-next/app/[locale]/recruit/page.tsx
index 8bcd996..09e9102 100644
--- a/web-next/app/[locale]/recruit/page.tsx
+++ b/web-next/app/[locale]/recruit/page.tsx
@@ -20,7 +20,9 @@ export default async function RecruitPage({ params }: { params: Promise<{ locale
const rewards = loggedIn ? await getRecruitRewards(session.accountId!) : []
const level80Count = loggedIn ? await getRecruitedLevel80Count(session.accountId!) : 0
const stats = loggedIn ? await getRecruitStats(session.accountId!) : { isRecruited: false, recruitedCount: 0 }
- const characters = loggedIn ? (await getGameCharacters(session.accountId!)).map((c) => c.name) : []
+ const characters = loggedIn
+ ? (await getGameCharacters(session.accountId!)).map((c) => ({ name: c.name, classCss: c.classCss }))
+ : []
return (
diff --git a/web-next/app/[locale]/revive/page.tsx b/web-next/app/[locale]/revive/page.tsx
index 0147749..19e45ee 100644
--- a/web-next/app/[locale]/revive/page.tsx
+++ b/web-next/app/[locale]/revive/page.tsx
@@ -20,7 +20,15 @@ export default async function RevivePage({ params }: { params: Promise<{ locale:
return (
- c.name)} endpoint="/api/character/revive" actionLabel={t('reviveAction')} buttonClass="revive-button" />
+ ({ name: c.name, classCss: c.classCss }))}
+ endpoint="/api/character/revive"
+ actionLabel={t('reviveAction')}
+ processingLabel={t('reviveProcessing')}
+ doneLabel={t('reviveDone')}
+ promptText={t('reviveChoose')}
+ buttonClass="revive-button"
+ />
)
diff --git a/web-next/app/[locale]/trade-points/page.tsx b/web-next/app/[locale]/trade-points/page.tsx
index 49694f2..dca76da 100644
--- a/web-next/app/[locale]/trade-points/page.tsx
+++ b/web-next/app/[locale]/trade-points/page.tsx
@@ -79,7 +79,7 @@ export default async function TradePointsPage({ params }: { params: Promise<{ lo
- c.name)} />
+ ({ name: c.name, classCss: c.classCss }))} />
)
diff --git a/web-next/app/[locale]/transfer/page.tsx b/web-next/app/[locale]/transfer/page.tsx
index 75a9602..b4883f3 100644
--- a/web-next/app/[locale]/transfer/page.tsx
+++ b/web-next/app/[locale]/transfer/page.tsx
@@ -22,7 +22,7 @@ export default async function TransferPage({ params }: { params: Promise<{ local
return (
- c.name)} price={price} />
+ ({ name: c.name, classCss: c.classCss }))} price={price} />
)
diff --git a/web-next/app/[locale]/unstuck/page.tsx b/web-next/app/[locale]/unstuck/page.tsx
index fa96e89..6f21173 100644
--- a/web-next/app/[locale]/unstuck/page.tsx
+++ b/web-next/app/[locale]/unstuck/page.tsx
@@ -20,7 +20,27 @@ export default async function UnstuckPage({ params }: { params: Promise<{ locale
return (
- c.name)} endpoint="/api/character/unstuck" actionLabel={t('unstuckAction')} buttonClass="unstuck-button" />
+
+ {t.rich('unstuckInfo1', { b: (c) => {c} })}
+ {t('unstuckInfo2')}
+
+
+
+ ({ name: c.name, classCss: c.classCss }))}
+ endpoint="/api/character/unstuck"
+ actionLabel={t('unstuckAction')}
+ processingLabel={t('unstuckProcessing')}
+ doneLabel={t('unstuckDone')}
+ promptText={t('unstuckChoose')}
+ buttonClass="unstuck-button"
+ />
)
diff --git a/web-next/components/CharacterActionForm.tsx b/web-next/components/CharacterActionForm.tsx
index c112781..88945b0 100644
--- a/web-next/components/CharacterActionForm.tsx
+++ b/web-next/components/CharacterActionForm.tsx
@@ -2,26 +2,41 @@
import { useState } from 'react'
import { useTranslations } from 'next-intl'
+import { useRouter } from '@/i18n/navigation'
+import { CharacterSelect, type CharOption } from '@/components/CharacterSelect'
const ERROR_KEYS = ['noCharacter', 'notOfflineOrOwned', 'cooldown', 'soapError'] as const
interface Props {
- characters: string[]
+ characters: CharOption[]
endpoint: string
actionLabel: string
buttonClass?: string
+ processingLabel?: string // texto mientras procesa (por defecto t('processing'))
+ doneLabel?: string // si se pasa, muestra el estado "hecho" (dorado) y refresca a los 5s
+ promptText?: string // texto encima del selector (por defecto t('choose'))
}
/** Formulario reutilizable para acciones de personaje por SOAP (revive, unstuck...). */
-export function CharacterActionForm({ characters, endpoint, actionLabel, buttonClass = '' }: Props) {
+export function CharacterActionForm({
+ characters,
+ endpoint,
+ actionLabel,
+ buttonClass = '',
+ processingLabel,
+ doneLabel,
+ promptText,
+}: Props) {
const t = useTranslations('Services')
+ const router = useRouter()
const [character, setCharacter] = useState('')
const [busy, setBusy] = useState(false)
+ const [done, setDone] = useState(false)
const [message, setMessage] = useState<{ ok: boolean; text: string } | null>(null)
async function handleSubmit(e: React.FormEvent) {
e.preventDefault()
- if (busy || !character) return
+ if (busy || done || !character) return
setBusy(true)
setMessage(null)
try {
@@ -32,10 +47,22 @@ export function CharacterActionForm({ characters, endpoint, actionLabel, buttonC
body: JSON.stringify({ character }),
})
const data: { success?: boolean; error?: string } = await res.json()
- if (data.success) setMessage({ ok: true, text: t('success') })
- else {
+ if (data.success) {
+ setMessage({ ok: true, text: t('success') })
+ if (doneLabel) {
+ // Como el original: tras 5 s se limpia el formulario y se refresca.
+ setDone(true)
+ setTimeout(() => {
+ setDone(false)
+ setCharacter('')
+ setMessage(null)
+ router.refresh()
+ }, 5000)
+ }
+ } else {
const key = (ERROR_KEYS as readonly string[]).includes(data.error ?? '') ? data.error! : 'genericError'
setMessage({ ok: false, text: t(key) })
+ setTimeout(() => setMessage(null), 5000)
}
} catch {
setMessage({ ok: false, text: t('genericError') })
@@ -50,29 +77,25 @@ export function CharacterActionForm({ characters, endpoint, actionLabel, buttonC
return (
-
{t('choose')}
+
+
+
{promptText ?? t('choose')}
- {message && (
- {message.text}
- )}
+ {message && {message.text}}
)
diff --git a/web-next/components/CharacterSelect.tsx b/web-next/components/CharacterSelect.tsx
new file mode 100644
index 0000000..5efd75e
--- /dev/null
+++ b/web-next/components/CharacterSelect.tsx
@@ -0,0 +1,46 @@
+'use client'
+
+export interface CharOption {
+ name: string
+ classCss?: string // clase CSS de color por clase (priest, warrior…)
+}
+
+/**
+ * Selector de personaje con las opciones coloreadas por clase (como el diseño
+ * original: `class="priest big-font"`). El propio