From d9e6ff1496a0ff42f69dbb4fef903ce38532de99 Mon Sep 17 00:00:00 2001 From: adevopg Date: Mon, 13 Jul 2026 00:35:13 +0000 Subject: [PATCH] =?UTF-8?q?Admin:=20gesti=C3=B3n=20de=20opciones=20de=20or?= =?UTF-8?q?o=20(home=5Fgoldprice)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - lib/admin-gold.ts: list/create/delete sobre home_goldprice (id, gold_amount, price). - API: POST /api/admin/gold, DELETE /api/admin/gold/[id] (403 sin admin). - UI: /admin/gold con AdminGoldManager (alta ordenada por cantidad + borrado), enlace desde el índice del admin. - i18n es/en (Admin): manageGold, gold, goldAmount, goldPrice, goldUnit, noGold. Verificado: build OK, /admin/gold 307→login sin sesión, APIs 403 sin admin, columnas confirmadas en la migración Django. Co-Authored-By: Claude Opus 4.8 (1M context) --- web-next/app/[locale]/admin/gold/page.tsx | 24 +++++++ web-next/app/[locale]/admin/page.tsx | 5 ++ web-next/app/api/admin/gold/[id]/route.ts | 11 +++ web-next/app/api/admin/gold/route.ts | 15 ++++ web-next/components/AdminGoldManager.tsx | 83 +++++++++++++++++++++++ web-next/lib/admin-gold.ts | 27 ++++++++ web-next/messages/en.json | 8 ++- web-next/messages/es.json | 8 ++- 8 files changed, 179 insertions(+), 2 deletions(-) create mode 100644 web-next/app/[locale]/admin/gold/page.tsx create mode 100644 web-next/app/api/admin/gold/[id]/route.ts create mode 100644 web-next/app/api/admin/gold/route.ts create mode 100644 web-next/components/AdminGoldManager.tsx create mode 100644 web-next/lib/admin-gold.ts diff --git a/web-next/app/[locale]/admin/gold/page.tsx b/web-next/app/[locale]/admin/gold/page.tsx new file mode 100644 index 0000000..a81c9c8 --- /dev/null +++ b/web-next/app/[locale]/admin/gold/page.tsx @@ -0,0 +1,24 @@ +import { getTranslations, setRequestLocale } from 'next-intl/server' +import { redirect } from '@/i18n/navigation' +import { getSession } from '@/lib/session' +import { isAdmin } from '@/lib/admin' +import { listGoldOptions } from '@/lib/admin-gold' +import { AdminGoldManager } from '@/components/AdminGoldManager' + +export const dynamic = 'force-dynamic' + +export default async function AdminGoldPage({ params }: { params: Promise<{ locale: string }> }) { + const { locale } = await params + setRequestLocale(locale) + const t = await getTranslations('Admin') + const session = await getSession() + if (!session.bnetId) redirect({ href: '/login', locale }) + if (!(await isAdmin(session))) redirect({ href: '/', locale }) + const options = await listGoldOptions() + return ( +
+

{t('gold')}

+ +
+ ) +} diff --git a/web-next/app/[locale]/admin/page.tsx b/web-next/app/[locale]/admin/page.tsx index de0bfdc..1c2a4a1 100644 --- a/web-next/app/[locale]/admin/page.tsx +++ b/web-next/app/[locale]/admin/page.tsx @@ -32,6 +32,11 @@ export default async function AdminPage({ params }: { params: Promise<{ locale: {t('manageVotes')} +
  • + + {t('manageGold')} + +
  • ) diff --git a/web-next/app/api/admin/gold/[id]/route.ts b/web-next/app/api/admin/gold/[id]/route.ts new file mode 100644 index 0000000..94ebcb1 --- /dev/null +++ b/web-next/app/api/admin/gold/[id]/route.ts @@ -0,0 +1,11 @@ +import { getSession } from '@/lib/session' +import { isAdmin } from '@/lib/admin' +import { deleteGoldOption } from '@/lib/admin-gold' + +export async function DELETE(_request: Request, { params }: { params: Promise<{ id: string }> }) { + const session = await getSession() + if (!(await isAdmin(session))) return Response.json({ success: false, error: 'forbidden' }, { status: 403 }) + const { id } = await params + await deleteGoldOption(Number(id)) + return Response.json({ success: true }) +} diff --git a/web-next/app/api/admin/gold/route.ts b/web-next/app/api/admin/gold/route.ts new file mode 100644 index 0000000..fbf2f21 --- /dev/null +++ b/web-next/app/api/admin/gold/route.ts @@ -0,0 +1,15 @@ +import { getSession } from '@/lib/session' +import { isAdmin } from '@/lib/admin' +import { createGoldOption } from '@/lib/admin-gold' + +export async function POST(request: Request) { + const session = await getSession() + if (!(await isAdmin(session))) return Response.json({ success: false, error: 'forbidden' }, { status: 403 }) + let b: Record = {} + try { b = await request.json() } catch { return Response.json({ success: false, error: 'invalidRequest' }, { status: 400 }) } + const goldAmount = Number(b.goldAmount) + const price = Number(b.price) + if (!(goldAmount > 0) || !(price >= 0)) return Response.json({ success: false, error: 'emptyError' }) + const id = await createGoldOption(goldAmount, price) + return Response.json({ success: true, id }) +} diff --git a/web-next/components/AdminGoldManager.tsx b/web-next/components/AdminGoldManager.tsx new file mode 100644 index 0000000..0d12ba4 --- /dev/null +++ b/web-next/components/AdminGoldManager.tsx @@ -0,0 +1,83 @@ +'use client' + +import { useState } from 'react' +import { useTranslations } from 'next-intl' +import type { GoldOption } from '@/lib/admin-gold' + +export function AdminGoldManager({ initial }: { initial: GoldOption[] }) { + const t = useTranslations('Admin') + const [opts, setOpts] = useState(initial) + const [form, setForm] = useState({ goldAmount: '', price: '' }) + const [busy, setBusy] = useState(false) + + function upd(k: string, v: string) { + setForm((f) => ({ ...f, [k]: v })) + } + + async function create(e: React.FormEvent) { + e.preventDefault() + const goldAmount = Number(form.goldAmount) + const price = Number(form.price) + if (busy || !(goldAmount > 0) || !(price >= 0)) return + setBusy(true) + try { + const res = await fetch('/api/admin/gold', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + credentials: 'same-origin', + body: JSON.stringify({ goldAmount, price }), + }) + const data: { success?: boolean; id?: number } = await res.json() + if (data.success && data.id) { + setOpts( + [...opts, { id: data.id, gold_amount: goldAmount, price }].sort((a, b) => a.gold_amount - b.gold_amount), + ) + setForm({ goldAmount: '', price: '' }) + } + } finally { + setBusy(false) + } + } + + async function remove(id: number) { + if (!confirm(t('confirmDelete'))) return + const res = await fetch(`/api/admin/gold/${id}`, { method: 'DELETE', credentials: 'same-origin' }) + if ((await res.json()).success) setOpts(opts.filter((o) => o.id !== id)) + } + + const field = 'nw-input' + return ( +
    +
    + + + +
    + {opts.length === 0 ? ( +

    {t('noGold')}

    + ) : ( +
      + {opts.map((o) => ( +
    • +
      +

      {o.gold_amount.toLocaleString()} {t('goldUnit')}

      +

      {o.price.toFixed(2)} €

      +
      + +
    • + ))} +
    + )} +
    + ) +} diff --git a/web-next/lib/admin-gold.ts b/web-next/lib/admin-gold.ts new file mode 100644 index 0000000..1e67d36 --- /dev/null +++ b/web-next/lib/admin-gold.ts @@ -0,0 +1,27 @@ +import type { RowDataPacket, ResultSetHeader } from 'mysql2' +import { db, DB } from './db' + +export interface GoldOption { + id: number + gold_amount: number + price: number +} + +export async function listGoldOptions(): Promise { + const [rows] = await db(DB.default).query( + 'SELECT id, gold_amount, price FROM home_goldprice ORDER BY gold_amount', + ) + return rows.map((r) => ({ id: r.id, gold_amount: r.gold_amount, price: Number(r.price) })) +} + +export async function createGoldOption(goldAmount: number, price: number): Promise { + const [res] = await db(DB.default).query( + 'INSERT INTO home_goldprice (gold_amount, price) VALUES (?, ?)', + [goldAmount, price], + ) + return res.insertId +} + +export async function deleteGoldOption(id: number): Promise { + await db(DB.default).query('DELETE FROM home_goldprice WHERE id = ?', [id]) +} diff --git a/web-next/messages/en.json b/web-next/messages/en.json index 8c7c305..e5b0878 100644 --- a/web-next/messages/en.json +++ b/web-next/messages/en.json @@ -326,7 +326,13 @@ "voteUrl": "URL", "voteImage": "Image URL", "votePoints": "VP per vote", - "noVotes": "No vote sites." + "noVotes": "No vote sites.", + "manageGold": "Manage gold options", + "gold": "Gold options", + "goldAmount": "Gold amount", + "goldPrice": "Price (€)", + "goldUnit": "gold", + "noGold": "No gold options." }, "Vote": { "title": "Vote for us", diff --git a/web-next/messages/es.json b/web-next/messages/es.json index f421d6f..4aa1f22 100644 --- a/web-next/messages/es.json +++ b/web-next/messages/es.json @@ -326,7 +326,13 @@ "voteUrl": "URL", "voteImage": "URL de la imagen", "votePoints": "PV por voto", - "noVotes": "No hay sitios de voto." + "noVotes": "No hay sitios de voto.", + "manageGold": "Gestionar opciones de oro", + "gold": "Opciones de oro", + "goldAmount": "Cantidad de oro", + "goldPrice": "Precio (€)", + "goldUnit": "oro", + "noGold": "No hay opciones de oro." }, "Vote": { "title": "Votar por nosotros",