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

    + ) : ( + + )} +
    + ) +} 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",