diff --git a/frontend/src/components/CharacterPurchaseForm.tsx b/frontend/src/components/CharacterPurchaseForm.tsx
index 3c62d58..57e987f 100644
--- a/frontend/src/components/CharacterPurchaseForm.tsx
+++ b/frontend/src/components/CharacterPurchaseForm.tsx
@@ -94,7 +94,7 @@ export function CharacterPurchaseForm({
diff --git a/frontend/src/components/GoldForm.tsx b/frontend/src/components/GoldForm.tsx
new file mode 100644
index 0000000..05d3f0d
--- /dev/null
+++ b/frontend/src/components/GoldForm.tsx
@@ -0,0 +1,90 @@
+import { useState } from 'react'
+import { redirectToStripeCheckout } from '../stripe'
+
+interface GoldOption {
+ gold_amount: number
+ price: string
+}
+
+interface Props {
+ url: string
+ csrfToken: string
+ characters: string[]
+ options: GoldOption[]
+}
+
+export function GoldForm({ url, csrfToken, characters, options }: Props) {
+ const [character, setCharacter] = useState('')
+ const [amount, setAmount] = useState('')
+ const [busy, setBusy] = useState(false)
+ const [message, setMessage] = useState<{ ok: boolean; html: string } | null>(null)
+
+ async function handleSubmit(e: React.FormEvent) {
+ e.preventDefault()
+ if (busy) return
+ setBusy(true)
+ setMessage(null)
+
+ const body = new URLSearchParams()
+ body.set('character', character)
+ body.set('gold_amount', amount)
+ body.set('csrfmiddlewaretoken', csrfToken)
+
+ try {
+ const resp = await fetch(url, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/x-www-form-urlencoded',
+ 'X-CSRFToken': csrfToken,
+ Accept: 'application/json',
+ },
+ credentials: 'same-origin',
+ body: body.toString(),
+ })
+ if (!resp.ok) throw new Error(`HTTP ${resp.status}`)
+ const data: { success?: boolean; message?: string; session_id?: string; stripe_public_key?: string } =
+ await resp.json()
+ if (data.success && data.session_id && data.stripe_public_key) {
+ await redirectToStripeCheckout(data.stripe_public_key, data.session_id)
+ return
+ }
+ setMessage({ ok: !!data.success, html: data.message || '' })
+ setBusy(false)
+ } catch {
+ setMessage({ ok: false, html: 'Error al iniciar el pago. Inténtalo más tarde.' })
+ setBusy(false)
+ }
+ }
+
+ return (
+
+
+
+
+
+ {message && }
+
+
+
+ )
+}
diff --git a/frontend/src/components/TransferForm.tsx b/frontend/src/components/TransferForm.tsx
new file mode 100644
index 0000000..c62d6a2
--- /dev/null
+++ b/frontend/src/components/TransferForm.tsx
@@ -0,0 +1,96 @@
+import { useState } from 'react'
+import { redirectToStripeCheckout } from '../stripe'
+
+interface Props {
+ url: string
+ csrfToken: string
+ characters: string[]
+ price: string
+}
+
+export function TransferForm({ url, csrfToken, characters, price }: Props) {
+ const [character, setCharacter] = useState('')
+ const [destination, setDestination] = useState('')
+ const [token, setToken] = useState('')
+ const [busy, setBusy] = useState(false)
+ const [message, setMessage] = useState<{ ok: boolean; html: string } | null>(null)
+
+ async function handleSubmit(e: React.FormEvent) {
+ e.preventDefault()
+ if (busy) return
+ setBusy(true)
+ setMessage(null)
+
+ const body = new URLSearchParams()
+ body.set('character', character)
+ body.set('destination_account', destination.trim())
+ body.set('security_token', token.trim())
+ body.set('csrfmiddlewaretoken', csrfToken)
+
+ try {
+ const resp = await fetch(url, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/x-www-form-urlencoded',
+ 'X-CSRFToken': csrfToken,
+ Accept: 'application/json',
+ },
+ credentials: 'same-origin',
+ body: body.toString(),
+ })
+ if (!resp.ok) throw new Error(`HTTP ${resp.status}`)
+ const data: { success?: boolean; message?: string; session_id?: string; stripe_public_key?: string } =
+ await resp.json()
+ if (data.success && data.session_id && data.stripe_public_key) {
+ await redirectToStripeCheckout(data.stripe_public_key, data.session_id)
+ return
+ }
+ setMessage({ ok: !!data.success, html: data.message || '' })
+ setBusy(false)
+ } catch {
+ setMessage({ ok: false, html: 'Error al iniciar el pago. Inténtalo más tarde.' })
+ setBusy(false)
+ }
+ }
+
+ return (
+
+
+
+
+
+ {message && }
+
+
+
+ )
+}
diff --git a/frontend/src/entries/gold_character.tsx b/frontend/src/entries/gold_character.tsx
new file mode 100644
index 0000000..55c0415
--- /dev/null
+++ b/frontend/src/entries/gold_character.tsx
@@ -0,0 +1,16 @@
+import { StrictMode } from 'react'
+import { createRoot } from 'react-dom/client'
+import { GoldForm } from '../components/GoldForm'
+
+const el = document.getElementById('gold-app')
+if (el) {
+ const cEl = document.getElementById('gold-characters-data')
+ const oEl = document.getElementById('gold-options-data')
+ const characters: string[] = cEl ? JSON.parse(cEl.textContent || '[]') : []
+ const options = oEl ? JSON.parse(oEl.textContent || '[]') : []
+ createRoot(el).render(
+
+
+ ,
+ )
+}
diff --git a/frontend/src/entries/transfer_character.tsx b/frontend/src/entries/transfer_character.tsx
new file mode 100644
index 0000000..5092a9f
--- /dev/null
+++ b/frontend/src/entries/transfer_character.tsx
@@ -0,0 +1,19 @@
+import { StrictMode } from 'react'
+import { createRoot } from 'react-dom/client'
+import { TransferForm } from '../components/TransferForm'
+
+const el = document.getElementById('transfer-app')
+if (el) {
+ const cEl = document.getElementById('transfer-characters-data')
+ const characters: string[] = cEl ? JSON.parse(cEl.textContent || '[]') : []
+ createRoot(el).render(
+
+
+ ,
+ )
+}
diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts
index 73bf2e3..5ba45c9 100644
--- a/frontend/vite.config.ts
+++ b/frontend/vite.config.ts
@@ -25,6 +25,8 @@ export default defineConfig({
security_token: resolve(__dirname, 'src/entries/security_token.tsx'),
change_email: resolve(__dirname, 'src/entries/change_email.tsx'),
character_purchase: resolve(__dirname, 'src/entries/character_purchase.tsx'),
+ gold_character: resolve(__dirname, 'src/entries/gold_character.tsx'),
+ transfer_character: resolve(__dirname, 'src/entries/transfer_character.tsx'),
},
},
},
diff --git a/home/templates/partials/gold_character.html b/home/templates/partials/gold_character.html
index 60b1b8b..19b9d2a 100644
--- a/home/templates/partials/gold_character.html
+++ b/home/templates/partials/gold_character.html
@@ -9,24 +9,14 @@
Selecciona un personaje y la cantidad de oro:
-
-
-
-
+ {% load django_vite %}
+
+
+ {{ character_names|json_script:"gold-characters-data" }}
+ {{ gold_options|json_script:"gold-options-data" }}
+ {% vite_asset 'src/entries/gold_character.tsx' %}
diff --git a/home/templates/partials/revive_character.html b/home/templates/partials/revive_character.html
index 50a6f96..f842970 100644
--- a/home/templates/partials/revive_character.html
+++ b/home/templates/partials/revive_character.html
@@ -15,24 +15,18 @@
{% if characters %}
Escoge el personaje que quieres revivir:
-
-
-
+ {% load django_vite %}
+
+
+ {{ characters|json_script:"character-purchase-data" }}
+ {% vite_asset 'src/entries/character_purchase.tsx' %}
{% else %}
- No hay personajes disponibles para revivir.
+ No tienes personajes para revivir.
{% endif %}
-
-
diff --git a/home/templates/partials/transfer_character.html b/home/templates/partials/transfer_character.html
index 00bbea4..7328d99 100644
--- a/home/templates/partials/transfer_character.html
+++ b/home/templates/partials/transfer_character.html
@@ -8,21 +8,14 @@
Regresar a mi cuenta
-
-
-
-
-
+ {% load django_vite %}
+
+
+ {{ character_names|json_script:"transfer-characters-data" }}
+ {% vite_asset 'src/entries/transfer_character.tsx' %}
diff --git a/home/templates/partials/unstuck_character.html b/home/templates/partials/unstuck_character.html
index 04b0e79..4803513 100644
--- a/home/templates/partials/unstuck_character.html
+++ b/home/templates/partials/unstuck_character.html
@@ -22,20 +22,15 @@
Escoge el personaje que quieres desbloquear
-
-
-
-
+ {% load django_vite %}
+
+
+ {{ characters|json_script:"character-purchase-data" }}
+ {% vite_asset 'src/entries/character_purchase.tsx' %}
diff --git a/home/views/characters.py b/home/views/characters.py
index f29ecf6..b22365f 100644
--- a/home/views/characters.py
+++ b/home/views/characters.py
@@ -702,7 +702,12 @@ def gold_character_view(request):
else:
return JsonResponse({'success': False, 'message': f'Error al iniciar el pago: {stripe_response["error"]}'})
- return render(request, 'account/gold_character.html', {'characters': characters, 'gold_prices': gold_prices})
+ return render(request, 'account/gold_character.html', {
+ 'characters': characters,
+ 'gold_prices': gold_prices,
+ 'character_names': list(characters.keys()),
+ 'gold_options': [{'gold_amount': p.gold_amount, 'price': str(p.price)} for p in gold_prices],
+ })
@require_paid_stripe()
def gold_success_view(request):
# Procesar el pago exitoso
@@ -843,7 +848,11 @@ def transfer_character_view(request):
else:
return JsonResponse({'success': False, 'message': f'Error al iniciar el pago: {stripe_response["error"]}'})
- return render(request, 'account/transfer_character.html', {'characters': characters, 'price': price})
+ return render(request, 'account/transfer_character.html', {
+ 'characters': characters,
+ 'price': price,
+ 'character_names': [c[0] for c in characters],
+ })
@require_paid_stripe()
def transfer_success_view(request):
transfer_data = request.session.get('transfer_data')