diff --git a/web-next/app/api/admin/forum/[id]/route.ts b/web-next/app/api/admin/forum/[id]/route.ts index 412a724..86afb72 100644 --- a/web-next/app/api/admin/forum/[id]/route.ts +++ b/web-next/app/api/admin/forum/[id]/route.ts @@ -1,6 +1,6 @@ import { getSession } from '@/lib/session' import { isAdmin } from '@/lib/admin' -import { deleteForum, setForumVisibility } from '@/lib/admin-forum' +import { deleteForum, setForumVisibility, updateForum } from '@/lib/admin-forum' export async function DELETE(_request: Request, { params }: { params: Promise<{ id: string }> }) { const session = await getSession() @@ -19,3 +19,20 @@ export async function PATCH(request: Request, { params }: { params: Promise<{ id await setForumVisibility(Number(id), b.visibility !== false) return Response.json({ success: true }) } + +export async function PUT(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 + 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 nameEn = String(b.nameEn ?? '').trim() || null + const description = String(b.description ?? '').trim() + const descriptionEn = String(b.descriptionEn ?? '').trim() || null + const order = Number(b.order) || 0 + const visibility = b.visibility === false ? 0 : 1 + if (!name) return Response.json({ success: false, error: 'emptyError' }) + await updateForum(Number(id), name, nameEn, description, descriptionEn, order, visibility) + return Response.json({ success: true }) +} diff --git a/web-next/app/api/admin/forum/category/[id]/route.ts b/web-next/app/api/admin/forum/category/[id]/route.ts index 1ee229e..0835306 100644 --- a/web-next/app/api/admin/forum/category/[id]/route.ts +++ b/web-next/app/api/admin/forum/category/[id]/route.ts @@ -1,6 +1,6 @@ import { getSession } from '@/lib/session' import { isAdmin } from '@/lib/admin' -import { deleteCategory } from '@/lib/admin-forum' +import { deleteCategory, updateCategory } from '@/lib/admin-forum' export async function DELETE(_request: Request, { params }: { params: Promise<{ id: string }> }) { const session = await getSession() @@ -9,3 +9,17 @@ export async function DELETE(_request: Request, { params }: { params: Promise<{ const ok = await deleteCategory(Number(id)) return Response.json({ success: ok, error: ok ? undefined : 'categoryNotEmpty' }) } + +export async function PUT(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 + 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 nameEn = String(b.nameEn ?? '').trim() || null + const order = Number(b.order) || 0 + if (!name) return Response.json({ success: false, error: 'emptyError' }) + await updateCategory(Number(id), name, nameEn, order) + return Response.json({ success: true }) +} diff --git a/web-next/app/api/admin/forum/category/route.ts b/web-next/app/api/admin/forum/category/route.ts index 4b5c3cb..167868f 100644 --- a/web-next/app/api/admin/forum/category/route.ts +++ b/web-next/app/api/admin/forum/category/route.ts @@ -8,8 +8,9 @@ export async function POST(request: Request) { 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 nameEn = String(b.nameEn ?? '').trim() || null const order = Number(b.order) || 0 if (!name) return Response.json({ success: false, error: 'emptyError' }) - const id = await createCategory(name, order) + const id = await createCategory(name, nameEn, order) return Response.json({ success: true, id }) } diff --git a/web-next/app/api/admin/forum/route.ts b/web-next/app/api/admin/forum/route.ts index 180df79..02dd1e2 100644 --- a/web-next/app/api/admin/forum/route.ts +++ b/web-next/app/api/admin/forum/route.ts @@ -9,10 +9,12 @@ export async function POST(request: Request) { try { b = await request.json() } catch { return Response.json({ success: false, error: 'invalidRequest' }, { status: 400 }) } const categoryId = Number(b.categoryId) const name = String(b.name ?? '').trim() + const nameEn = String(b.nameEn ?? '').trim() || null const description = String(b.description ?? '').trim() + const descriptionEn = String(b.descriptionEn ?? '').trim() || null const order = Number(b.order) || 0 const visibility = b.visibility === false ? 0 : 1 if (!categoryId || !name) return Response.json({ success: false, error: 'emptyError' }) - const id = await createForum(categoryId, name, description, order, visibility) + const id = await createForum(categoryId, name, nameEn, description, descriptionEn, order, visibility) return Response.json({ success: true, id }) } diff --git a/web-next/components/AdminForumManager.tsx b/web-next/components/AdminForumManager.tsx index 4aff566..3e42aa6 100644 --- a/web-next/components/AdminForumManager.tsx +++ b/web-next/components/AdminForumManager.tsx @@ -11,13 +11,15 @@ export function AdminForumManager({ initial }: { initial: Group[] }) { const t = useTranslations('Admin') const router = useRouter() const [busy, setBusy] = useState(false) - const [catForm, setCatForm] = useState({ name: '', order: '0' }) - const [forumForm, setForumForm] = useState>({}) + const [catForm, setCatForm] = useState({ name: '', nameEn: '', order: '0' }) + const [forumForm, setForumForm] = useState>({}) + const [editCat, setEditCat] = useState<{ id: number; name: string; nameEn: string; order: string } | null>(null) + const [editForum, setEditForum] = useState<{ id: number; name: string; nameEn: string; description: string; descriptionEn: string; order: string; visibility: number } | null>(null) function ff(catId: number) { - return forumForm[catId] ?? { name: '', description: '', order: '0' } + return forumForm[catId] ?? { name: '', nameEn: '', description: '', descriptionEn: '', order: '0' } } - function setFF(catId: number, patch: Partial<{ name: string; description: string; order: string }>) { + function setFF(catId: number, patch: Partial<{ name: string; nameEn: string; description: string; descriptionEn: string; order: string }>) { setForumForm((s) => ({ ...s, [catId]: { ...ff(catId), ...patch } })) } @@ -48,14 +50,19 @@ export function AdminForumManager({ initial }: { initial: Group[] }) {
{ e.preventDefault() - if (catForm.name.trim()) call('/api/admin/forum/category', 'POST', { name: catForm.name, order: Number(catForm.order) }).then((ok) => ok && setCatForm({ name: '', order: '0' })) + if (catForm.name.trim()) + call('/api/admin/forum/category', 'POST', { name: catForm.name, nameEn: catForm.nameEn, order: Number(catForm.order) }).then( + (ok) => ok && setCatForm({ name: '', nameEn: '', order: '0' }), + ) }} className="admin-form info-box-light separate2" > {t('newCategory')} setCatForm({ ...catForm, name: e.target.value })} placeholder={t('categoryName')} /> + setCatForm({ ...catForm, nameEn: e.target.value })} placeholder={t('categoryNameEn')} /> {t('order')} setCatForm({ ...catForm, order: e.target.value })} /> +

{t('forumEnHint')}


@@ -64,18 +71,46 @@ export function AdminForumManager({ initial }: { initial: Group[] }) { {initial.map(({ category, forums }) => (
-
-

- {category.name} #{category.order}{' '} + {editCat?.id === category.id ? ( +
+ setEditCat({ ...editCat, name: e.target.value })} placeholder={t('categoryName')} /> + setEditCat({ ...editCat, nameEn: e.target.value })} placeholder={t('categoryNameEn')} /> + setEditCat({ ...editCat, order: e.target.value })} /> -

-
+ {t('save')} + {' '} + +
+ ) : ( +
+

+ {category.name} + {category.name_en && · EN: {category.name_en}}{' '} + #{category.order}{' '} + {' '} + +

+
+ )} {/* Foros de la categoría */} @@ -85,31 +120,68 @@ export function AdminForumManager({ initial }: { initial: Group[] }) { )} - {forums.map((f) => ( - - - - - ))} + {forums.map((f) => + editForum?.id === f.id ? ( + + + + ) : ( + + + + + ), + )}
{t('noForums')}
- {f.name}{' '} - {f.visibility === 0 && ({t('hidden')})} - {f.description &&

{f.description}

} -
- {' '} - -
+ setEditForum({ ...editForum, name: e.target.value })} placeholder={t('forumName')} maxLength={45} /> + setEditForum({ ...editForum, nameEn: e.target.value })} placeholder={t('forumNameEn')} maxLength={45} /> + setEditForum({ ...editForum, description: e.target.value })} placeholder={t('forumDescription')} /> + setEditForum({ ...editForum, descriptionEn: e.target.value })} placeholder={t('forumDescriptionEn')} /> + {' '} + +
+ {f.name}{' '} + {f.name_en && · EN: {f.name_en}}{' '} + {f.visibility === 0 && ({t('hidden')})} + {f.description &&

{f.description}

} +
+ {' '} + {' '} + +
@@ -119,14 +191,21 @@ export function AdminForumManager({ initial }: { initial: Group[] }) { e.preventDefault() const v = ff(category.id) if (v.name.trim()) - call('/api/admin/forum', 'POST', { categoryId: category.id, name: v.name, description: v.description, order: Number(v.order) }).then( - (ok) => ok && setFF(category.id, { name: '', description: '', order: '0' }), - ) + call('/api/admin/forum', 'POST', { + categoryId: category.id, + name: v.name, + nameEn: v.nameEn, + description: v.description, + descriptionEn: v.descriptionEn, + order: Number(v.order), + }).then((ok) => ok && setFF(category.id, { name: '', nameEn: '', description: '', descriptionEn: '', order: '0' })) }} className="admin-form separate" > setFF(category.id, { name: e.target.value })} placeholder={t('forumName')} maxLength={45} /> + setFF(category.id, { nameEn: e.target.value })} placeholder={t('forumNameEn')} maxLength={45} /> setFF(category.id, { description: e.target.value })} placeholder={t('forumDescription')} /> + setFF(category.id, { descriptionEn: e.target.value })} placeholder={t('forumDescriptionEn')} /> diff --git a/web-next/lib/admin-forum.ts b/web-next/lib/admin-forum.ts index 6cbaf3c..cec2d12 100644 --- a/web-next/lib/admin-forum.ts +++ b/web-next/lib/admin-forum.ts @@ -4,6 +4,7 @@ import { db, DB } from './db' export interface AdminCategory { id: number name: string + name_en: string order: number } @@ -11,7 +12,9 @@ export interface AdminForum { id: number category_id: number name: string + name_en: string description: string + description_en: string order: number visibility: number } @@ -21,34 +24,45 @@ export async function listCategoriesWithForums(): Promise< { category: AdminCategory; forums: AdminForum[] }[] > { const [cats] = await db(DB.web).query( - 'SELECT id, name, `order` FROM forum_categories ORDER BY `order`, id', + 'SELECT id, name, name_en, `order` FROM forum_categories ORDER BY `order`, id', ) const [forums] = await db(DB.web).query( - 'SELECT id, category_id, name, description, `order`, visibility FROM forums WHERE deleted = 0 ORDER BY `order`, id', + 'SELECT id, category_id, name, name_en, description, description_en, `order`, visibility FROM forums WHERE deleted = 0 ORDER BY `order`, id', ) return cats.map((c) => ({ - category: { id: c.id, name: c.name, order: c.order }, + category: { id: c.id, name: c.name, name_en: c.name_en || '', order: c.order }, forums: forums .filter((f) => f.category_id === c.id) .map((f) => ({ id: f.id, category_id: f.category_id, name: f.name, + name_en: f.name_en || '', description: f.description, + description_en: f.description_en || '', order: f.order, visibility: f.visibility, })), })) } -export async function createCategory(name: string, order: number): Promise { +export async function createCategory(name: string, nameEn: string | null, order: number): Promise { const [res] = await db(DB.web).query( - 'INSERT INTO forum_categories (name, `order`, created_at) VALUES (?, ?, NOW())', - [name, order], + 'INSERT INTO forum_categories (name, name_en, `order`, created_at) VALUES (?, ?, ?, NOW())', + [name, nameEn || null, order], ) return res.insertId } +export async function updateCategory(id: number, name: string, nameEn: string | null, order: number): Promise { + await db(DB.web).query('UPDATE forum_categories SET name = ?, name_en = ?, `order` = ? WHERE id = ?', [ + name, + nameEn || null, + order, + id, + ]) +} + /** Borra una categoría solo si no tiene foros (no borrados). */ export async function deleteCategory(id: number): Promise { const [f] = await db(DB.web).query( @@ -63,18 +77,35 @@ export async function deleteCategory(id: number): Promise { export async function createForum( categoryId: number, name: string, + nameEn: string | null, description: string, + descriptionEn: string | null, order: number, visibility: number, ): Promise { const [res] = await db(DB.web).query( - 'INSERT INTO forums (category_id, name, description, `order`, type, visibility, deleted, created_at, updated_at) ' + - 'VALUES (?, ?, ?, ?, 0, ?, 0, NOW(), NOW())', - [categoryId, name, description, order, visibility ? 1 : 0], + 'INSERT INTO forums (category_id, name, name_en, description, description_en, `order`, type, visibility, deleted, created_at, updated_at) ' + + 'VALUES (?, ?, ?, ?, ?, ?, 0, ?, 0, NOW(), NOW())', + [categoryId, name, nameEn || null, description, descriptionEn || null, order, visibility ? 1 : 0], ) return res.insertId } +export async function updateForum( + id: number, + name: string, + nameEn: string | null, + description: string, + descriptionEn: string | null, + order: number, + visibility: number, +): Promise { + await db(DB.web).query( + 'UPDATE forums SET name = ?, name_en = ?, description = ?, description_en = ?, `order` = ?, visibility = ?, updated_at = NOW() WHERE id = ?', + [name, nameEn || null, description, descriptionEn || null, order, visibility ? 1 : 0, id], + ) +} + /** Borrado lógico de un foro (deleted = 1). */ export async function deleteForum(id: number): Promise { await db(DB.web).query('UPDATE forums SET deleted = 1 WHERE id = ?', [id]) diff --git a/web-next/messages/en.json b/web-next/messages/en.json index eb82fa9..f591801 100644 --- a/web-next/messages/en.json +++ b/web-next/messages/en.json @@ -572,7 +572,11 @@ "edit": "Edit", "saving": "Saving…", "cancel": "Cancel", - "newsEnHint": "Leave English empty to fall back to Spanish on the English site." + "newsEnHint": "Leave English empty to fall back to Spanish on the English site.", + "categoryNameEn": "Name (English, optional)", + "forumNameEn": "Forum name (English, optional)", + "forumDescriptionEn": "Description (English, optional)", + "forumEnHint": "Leave English empty to fall back to Spanish on the English site." }, "Vote": { "title": "Vote for us", diff --git a/web-next/messages/es.json b/web-next/messages/es.json index 1569b14..7b09dfd 100644 --- a/web-next/messages/es.json +++ b/web-next/messages/es.json @@ -572,7 +572,11 @@ "edit": "Editar", "saving": "Guardando…", "cancel": "Cancelar", - "newsEnHint": "Deja el inglés vacío para usar el español en la web en inglés." + "newsEnHint": "Deja el inglés vacío para usar el español en la web en inglés.", + "categoryNameEn": "Nombre (inglés, opcional)", + "forumNameEn": "Nombre del foro (inglés, opcional)", + "forumDescriptionEn": "Descripción (inglés, opcional)", + "forumEnHint": "Deja el inglés vacío para usar el español en la web en inglés." }, "Vote": { "title": "Votar por nosotros",