diff --git a/web-next/app/[locale]/admin/page.tsx b/web-next/app/[locale]/admin/page.tsx index 9923b4b..de0bfdc 100644 --- a/web-next/app/[locale]/admin/page.tsx +++ b/web-next/app/[locale]/admin/page.tsx @@ -27,6 +27,11 @@ export default async function AdminPage({ params }: { params: Promise<{ locale: {t('managePrices')} +
  • + + {t('manageVotes')} + +
  • ) diff --git a/web-next/app/[locale]/admin/votes/page.tsx b/web-next/app/[locale]/admin/votes/page.tsx new file mode 100644 index 0000000..3cdcdcb --- /dev/null +++ b/web-next/app/[locale]/admin/votes/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 { listVoteSites } from '@/lib/admin-votes' +import { AdminVotesManager } from '@/components/AdminVotesManager' + +export const dynamic = 'force-dynamic' + +export default async function AdminVotesPage({ 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 sites = await listVoteSites() + return ( +
    +

    {t('votes')}

    + +
    + ) +} diff --git a/web-next/app/api/admin/votes/[id]/route.ts b/web-next/app/api/admin/votes/[id]/route.ts new file mode 100644 index 0000000..1c05b55 --- /dev/null +++ b/web-next/app/api/admin/votes/[id]/route.ts @@ -0,0 +1,11 @@ +import { getSession } from '@/lib/session' +import { isAdmin } from '@/lib/admin' +import { deleteVoteSite } from '@/lib/admin-votes' + +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 deleteVoteSite(Number(id)) + return Response.json({ success: true }) +} diff --git a/web-next/app/api/admin/votes/route.ts b/web-next/app/api/admin/votes/route.ts new file mode 100644 index 0000000..16d385a --- /dev/null +++ b/web-next/app/api/admin/votes/route.ts @@ -0,0 +1,17 @@ +import { getSession } from '@/lib/session' +import { isAdmin } from '@/lib/admin' +import { createVoteSite } from '@/lib/admin-votes' + +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 name = String(b.name ?? '').trim() + const url = String(b.url ?? '').trim() + const imageUrl = String(b.imageUrl ?? '').trim() + const points = Number(b.points) + if (!name || !url || !(points >= 0)) return Response.json({ success: false, error: 'emptyError' }) + const id = await createVoteSite(name, url, imageUrl, points) + return Response.json({ success: true, id }) +} diff --git a/web-next/components/AdminVotesManager.tsx b/web-next/components/AdminVotesManager.tsx new file mode 100644 index 0000000..0139da7 --- /dev/null +++ b/web-next/components/AdminVotesManager.tsx @@ -0,0 +1,80 @@ +'use client' + +import { useState } from 'react' +import { useTranslations } from 'next-intl' +import type { VoteSite } from '@/lib/admin-votes' + +export function AdminVotesManager({ initial }: { initial: VoteSite[] }) { + const t = useTranslations('Admin') + const [sites, setSites] = useState(initial) + const [form, setForm] = useState({ name: '', url: '', imageUrl: '', points: '1' }) + const [busy, setBusy] = useState(false) + + function upd(k: string, v: string) { + setForm((f) => ({ ...f, [k]: v })) + } + + async function create(e: React.FormEvent) { + e.preventDefault() + if (busy || !form.name.trim() || !form.url.trim()) return + setBusy(true) + try { + const res = await fetch('/api/admin/votes', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + credentials: 'same-origin', + body: JSON.stringify({ ...form, points: Number(form.points) }), + }) + const data: { success?: boolean; id?: number } = await res.json() + if (data.success && data.id) { + setSites([ + ...sites, + { id: data.id, name: form.name, url: form.url, image_url: form.imageUrl, points: Number(form.points) }, + ]) + setForm({ name: '', url: '', imageUrl: '', points: '1' }) + } + } finally { + setBusy(false) + } + } + + async function remove(id: number) { + if (!confirm(t('confirmDelete'))) return + const res = await fetch(`/api/admin/votes/${id}`, { method: 'DELETE', credentials: 'same-origin' }) + if ((await res.json()).success) setSites(sites.filter((s) => s.id !== id)) + } + + const field = 'w-full rounded border border-amber-900/60 bg-[#2c1e14] px-3 py-2' + return ( +
    +
    + upd('name', e.target.value)} placeholder={t('voteName')} className={field} /> + upd('url', e.target.value)} placeholder={t('voteUrl')} className={field} /> + upd('imageUrl', e.target.value)} placeholder={t('voteImage')} className={field} /> + upd('points', e.target.value)} placeholder={t('votePoints')} className={field} /> + +
    + {sites.length === 0 ? ( +

    {t('noVotes')}

    + ) : ( + + )} +
    + ) +} diff --git a/web-next/lib/admin-votes.ts b/web-next/lib/admin-votes.ts new file mode 100644 index 0000000..f2be350 --- /dev/null +++ b/web-next/lib/admin-votes.ts @@ -0,0 +1,29 @@ +import type { RowDataPacket, ResultSetHeader } from 'mysql2' +import { db, DB } from './db' + +export interface VoteSite { + id: number + name: string + url: string + image_url: string + points: number +} + +export async function listVoteSites(): Promise { + const [rows] = await db(DB.default).query( + 'SELECT id, name, url, image_url, points FROM home_votesite ORDER BY id', + ) + return rows.map((r) => ({ id: r.id, name: r.name, url: r.url, image_url: r.image_url, points: r.points })) +} + +export async function createVoteSite(name: string, url: string, imageUrl: string, points: number): Promise { + const [res] = await db(DB.default).query( + 'INSERT INTO home_votesite (name, url, image_url, points, created_at, updated_at) VALUES (?, ?, ?, ?, NOW(), NOW())', + [name, url, imageUrl, points], + ) + return res.insertId +} + +export async function deleteVoteSite(id: number): Promise { + await db(DB.default).query('DELETE FROM home_votesite WHERE id = ?', [id]) +} diff --git a/web-next/messages/en.json b/web-next/messages/en.json index ce17bb7..ce7e4ce 100644 --- a/web-next/messages/en.json +++ b/web-next/messages/en.json @@ -292,6 +292,13 @@ "svc_change-race": "Change race", "svc_change-faction": "Change faction", "svc_level-up": "Level up to 80", - "svc_transfer": "Transfer character" + "svc_transfer": "Transfer character", + "manageVotes": "Manage vote sites", + "votes": "Vote sites", + "voteName": "Name", + "voteUrl": "URL", + "voteImage": "Image URL", + "votePoints": "VP per vote", + "noVotes": "No vote sites." } } diff --git a/web-next/messages/es.json b/web-next/messages/es.json index e5d1042..840edc4 100644 --- a/web-next/messages/es.json +++ b/web-next/messages/es.json @@ -292,6 +292,13 @@ "svc_change-race": "Cambiar raza", "svc_change-faction": "Cambiar facción", "svc_level-up": "Subir a nivel 80", - "svc_transfer": "Transferir personaje" + "svc_transfer": "Transferir personaje", + "manageVotes": "Gestionar sitios de voto", + "votes": "Sitios de voto", + "voteName": "Nombre", + "voteUrl": "URL", + "voteImage": "URL de la imagen", + "votePoints": "PV por voto", + "noVotes": "No hay sitios de voto." } }