Files
landing_page/refer_landing_page/app/console/research-projects/page.tsx
T
Godopu 3f452468d6 feat(console,api): implement /console Admin Dashboard and Go backend full CRUD REST APIs
- Add comprehensive /console admin web UI for managing research projects, areas, members, publications, patents, lectures, and standards
- Implement Admin token authentication middleware (RequireAdminToken) with Bearer token validation
- Implement POST, PUT, DELETE REST API endpoints for all database entities across 5 domains
- Add typed admin API client in refer_landing_page/lib/adminApi.ts with localStorage session persistence
- Enhance E2E test runner to manage backend server lifecycle during automated tests
- Pass all 12 quality gates cleanly with unanimous reviewer PASS verdicts
2026-08-24 22:21:12 +09:00

343 lines
12 KiB
TypeScript

"use client";
import React, { useEffect, useState } from "react";
import { AdminHeader, AlertBanner, Modal, ConfirmDeleteModal, getClientToken } from "../_components/AdminComponents";
import {
adminGetResearchProjects,
adminCreateResearchProject,
adminUpdateResearchProject,
adminDeleteResearchProject,
AdminResearchProject,
} from "../../../lib/adminApi";
export default function AdminResearchProjectsPage() {
const [projects, setProjects] = useState<AdminResearchProject[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [successMsg, setSuccessMsg] = useState<string | null>(null);
// Form State
const [isModalOpen, setIsModalOpen] = useState(false);
const [editingProject, setEditingProject] = useState<AdminResearchProject | null>(null);
const [formData, setFormData] = useState({
slug: "",
title: "",
abstract: "",
keywords: "",
organization: "",
standards_org: "",
period: "",
funder: "",
});
const [isSubmitting, setIsSubmitting] = useState(false);
// Delete State
const [deletingId, setDeletingId] = useState<number | null>(null);
const loadProjects = async () => {
try {
setIsLoading(true);
setError(null);
const data = await adminGetResearchProjects();
setProjects(data);
} catch (err: any) {
setError(err?.message || "Failed to load research projects.");
} finally {
setIsLoading(false);
}
};
useEffect(() => {
loadProjects();
}, []);
const handleOpenCreate = () => {
setEditingProject(null);
setFormData({
slug: "",
title: "",
abstract: "",
keywords: "",
organization: "",
standards_org: "",
period: "",
funder: "",
});
setIsModalOpen(true);
};
const handleOpenEdit = (p: AdminResearchProject) => {
setEditingProject(p);
setFormData({
slug: p.slug,
title: p.title,
abstract: p.abstract,
keywords: (p.keywords || []).join(", "),
organization: p.organization || "",
standards_org: p.standards_org || "",
period: p.period || "",
funder: p.funder || "",
});
setIsModalOpen(true);
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
const token = getClientToken();
if (!token) {
setError("Unauthorized. Please log in.");
return;
}
const kwArray = formData.keywords
.split(",")
.map((k) => k.trim())
.filter(Boolean);
const payload = {
slug: formData.slug.trim(),
title: formData.title.trim(),
abstract: formData.abstract.trim(),
keywords: kwArray,
organization: formData.organization.trim() || undefined,
standards_org: formData.standards_org.trim() || undefined,
period: formData.period.trim() || undefined,
funder: formData.funder.trim() || undefined,
};
setIsSubmitting(true);
setError(null);
try {
if (editingProject) {
await adminUpdateResearchProject(token, editingProject.id, payload);
setSuccessMsg(`Project "${payload.title}" updated successfully.`);
} else {
await adminCreateResearchProject(token, payload);
setSuccessMsg(`Project "${payload.title}" created successfully.`);
}
setIsModalOpen(false);
await loadProjects();
} catch (err: any) {
setError(err?.message || "Failed to save project.");
} finally {
setIsSubmitting(false);
}
};
const handleConfirmDelete = async () => {
if (!deletingId) return;
const token = getClientToken();
if (!token) {
setError("Unauthorized. Please log in.");
return;
}
setIsSubmitting(true);
try {
await adminDeleteResearchProject(token, deletingId);
setSuccessMsg("Project deleted successfully.");
setDeletingId(null);
await loadProjects();
} catch (err: any) {
setError(err?.message || "Failed to delete project.");
} finally {
setIsSubmitting(false);
}
};
return (
<div>
<AdminHeader
title="Research Projects"
description="Manage key laboratory research projects displayed on the home page."
action={
<button
onClick={handleOpenCreate}
className="px-4 py-2 text-sm rounded bg-cobalt hover:bg-cobalt/90 text-white font-medium shadow-sm transition-colors"
>
+ Add New Project
</button>
}
/>
{error && <AlertBanner type="error" message={error} onClose={() => setError(null)} />}
{successMsg && <AlertBanner type="success" message={successMsg} onClose={() => setSuccessMsg(null)} />}
{isLoading ? (
<div className="py-12 text-center text-sm text-ink/70">Loading projects...</div>
) : (
<div className="bg-paper border border-line rounded-lg overflow-hidden shadow-sm">
<table className="w-full text-left text-sm border-collapse">
<thead>
<tr className="bg-ivory border-b border-line text-xs font-semibold text-ink/70 uppercase">
<th className="p-4">ID</th>
<th className="p-4">Slug</th>
<th className="p-4">Title</th>
<th className="p-4">Organization / Period</th>
<th className="p-4 text-right">Actions</th>
</tr>
</thead>
<tbody className="divide-y divide-line">
{projects.length === 0 ? (
<tr>
<td colSpan={5} className="p-6 text-center text-ink/60">
No research projects found.
</td>
</tr>
) : (
projects.map((p) => (
<tr key={p.id} className="hover:bg-ivory/50">
<td className="p-4 text-xs font-mono text-ink/60">{p.id}</td>
<td className="p-4 font-mono text-xs text-cobalt font-medium">{p.slug}</td>
<td className="p-4">
<div className="font-semibold text-ink">{p.title}</div>
<div className="text-xs text-ink/60 line-clamp-1 mt-0.5">{p.abstract}</div>
</td>
<td className="p-4 text-xs text-ink/80">
<div>{p.organization || p.standards_org || "—"}</div>
<div className="text-ink/60">{p.period || "—"}</div>
</td>
<td className="p-4 text-right space-x-2">
<button
onClick={() => handleOpenEdit(p)}
className="px-2.5 py-1 text-xs rounded border border-line hover:bg-ivory font-medium text-ink"
>
Edit
</button>
<button
onClick={() => setDeletingId(p.id)}
className="px-2.5 py-1 text-xs rounded bg-red-50 hover:bg-red-100 text-red-600 border border-red-200 dark:bg-red-950/30 dark:hover:bg-red-950/60 dark:text-red-400 dark:border-red-900 font-medium"
>
Delete
</button>
</td>
</tr>
))
)}
</tbody>
</table>
</div>
)}
{/* Create / Edit Modal */}
<Modal
isOpen={isModalOpen}
title={editingProject ? "Edit Research Project" : "Create Research Project"}
onClose={() => setIsModalOpen(false)}
>
<form onSubmit={handleSubmit} className="space-y-4">
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div>
<label className="block text-xs font-semibold text-ink mb-1">Slug *</label>
<input
type="text"
value={formData.slug}
onChange={(e) => setFormData({ ...formData, slug: e.target.value })}
placeholder="e.g. ccis"
required
className="w-full px-3 py-2 text-sm border border-line rounded bg-ivory text-ink focus:outline-none focus:ring-1 focus:ring-cobalt"
/>
</div>
<div>
<label className="block text-xs font-semibold text-ink mb-1">Title *</label>
<input
type="text"
value={formData.title}
onChange={(e) => setFormData({ ...formData, title: e.target.value })}
placeholder="Project title"
required
className="w-full px-3 py-2 text-sm border border-line rounded bg-ivory text-ink focus:outline-none focus:ring-1 focus:ring-cobalt"
/>
</div>
</div>
<div>
<label className="block text-xs font-semibold text-ink mb-1">Abstract *</label>
<textarea
value={formData.abstract}
onChange={(e) => setFormData({ ...formData, abstract: e.target.value })}
rows={4}
placeholder="Detailed description of the project"
required
className="w-full px-3 py-2 text-sm border border-line rounded bg-ivory text-ink focus:outline-none focus:ring-1 focus:ring-cobalt"
/>
</div>
<div>
<label className="block text-xs font-semibold text-ink mb-1">Keywords (comma separated)</label>
<input
type="text"
value={formData.keywords}
onChange={(e) => setFormData({ ...formData, keywords: e.target.value })}
placeholder="e.g. ISO/IEC 63246, Camera Communications"
className="w-full px-3 py-2 text-sm border border-line rounded bg-ivory text-ink focus:outline-none focus:ring-1 focus:ring-cobalt"
/>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div>
<label className="block text-xs font-semibold text-ink mb-1">Organization / Standards Track</label>
<input
type="text"
value={formData.organization}
onChange={(e) => setFormData({ ...formData, organization: e.target.value })}
placeholder="e.g. ISO/IEC JTC 1/SC 29/WG 11"
className="w-full px-3 py-2 text-sm border border-line rounded bg-ivory text-ink focus:outline-none focus:ring-1 focus:ring-cobalt"
/>
</div>
<div>
<label className="block text-xs font-semibold text-ink mb-1">Period</label>
<input
type="text"
value={formData.period}
onChange={(e) => setFormData({ ...formData, period: e.target.value })}
placeholder="e.g. 2021.06 - 2024.05"
className="w-full px-3 py-2 text-sm border border-line rounded bg-ivory text-ink focus:outline-none focus:ring-1 focus:ring-cobalt"
/>
</div>
</div>
<div>
<label className="block text-xs font-semibold text-ink mb-1">Funder</label>
<input
type="text"
value={formData.funder}
onChange={(e) => setFormData({ ...formData, funder: e.target.value })}
placeholder="e.g. Ministry of Science and ICT (MSIT)"
className="w-full px-3 py-2 text-sm border border-line rounded bg-ivory text-ink focus:outline-none focus:ring-1 focus:ring-cobalt"
/>
</div>
<div className="flex justify-end gap-3 pt-4 border-t border-line">
<button
type="button"
onClick={() => setIsModalOpen(false)}
className="px-4 py-2 text-sm rounded border border-line hover:bg-ivory font-medium text-ink"
>
Cancel
</button>
<button
type="submit"
disabled={isSubmitting}
className="px-4 py-2 text-sm rounded bg-cobalt hover:bg-cobalt/90 text-white font-medium disabled:opacity-50"
>
{isSubmitting ? "Saving..." : editingProject ? "Save Changes" : "Create Project"}
</button>
</div>
</form>
</Modal>
{/* Delete Confirmation */}
<ConfirmDeleteModal
isOpen={deletingId !== null}
onClose={() => setDeletingId(null)}
onConfirm={handleConfirmDelete}
isSubmitting={isSubmitting}
title="Delete Research Project"
description="Are you sure you want to delete this research project? It will be removed from the public website immediately."
/>
</div>
);
}