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",