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')}

+
+
+ NOTA +
+

{t('unstuckNote1')}

+

{t('unstuckNote2')}

+
+
+ + ({ 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 onChange(e.target.value)} required={required}> + + {characters.map((c) => ( + + ))} + + ) +} diff --git a/web-next/components/GoldForm.tsx b/web-next/components/GoldForm.tsx index 15d39eb..55eb773 100644 --- a/web-next/components/GoldForm.tsx +++ b/web-next/components/GoldForm.tsx @@ -3,8 +3,9 @@ import { useState } from 'react' import { useTranslations } from 'next-intl' import type { GoldOption } from '@/lib/prices' +import { CharacterSelect, type CharOption } from '@/components/CharacterSelect' -export function GoldForm({ characters, options }: { characters: string[]; options: GoldOption[] }) { +export function GoldForm({ characters, options }: { characters: CharOption[]; options: GoldOption[] }) { const t = useTranslations('Services') const tp = useTranslations('Paid') const [character, setCharacter] = useState('') @@ -41,10 +42,7 @@ export function GoldForm({ characters, options }: { characters: string[]; option return (
- +
setCharacter(e.target.value)} required> - - {characters.map((c) => ( - - ))} - +
-

{t('note')}

+

{t('noteLabel')} {t('note')}


{sites.map((s) => { @@ -78,28 +78,31 @@ export function VotePanel({ sites, canVote }: { sites: VoteSiteStatus[]; canVote {t('pv')}: {s.points} - {s.lastVoteAt ? t('lastVote', { date: new Date(s.lastVoteAt).toLocaleString(locale) }) : t('neverVoted')} + {s.lastVoteAt ? t('lastVote', { date: new Date(s.lastVoteAt).toLocaleString(locale) }) : ''}
- {canVote ? ( + {!canVote ? ( + + {t('vote')} + + ) : isVoted || s.onCooldown ? ( + + ) : ( - ) : ( - - {t('vote')} - )} diff --git a/web-next/lib/characters.ts b/web-next/lib/characters.ts index 76bdb12..081b454 100644 --- a/web-next/lib/characters.ts +++ b/web-next/lib/characters.ts @@ -1,19 +1,21 @@ import type { RowDataPacket } from 'mysql2' import { db, DB } from './db' +import { getClassCss } from './character-info' export interface GameCharacter { name: string online: boolean + classCss: string // clase CSS de color por clase (priest, warrior…), para el