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
This commit is contained in:
@@ -0,0 +1,715 @@
|
||||
"use client";
|
||||
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { AdminHeader, AlertBanner, Modal, ConfirmDeleteModal, getClientToken } from "../_components/AdminComponents";
|
||||
import {
|
||||
adminGetPublications,
|
||||
adminCreatePublication,
|
||||
adminUpdatePublication,
|
||||
adminDeletePublication,
|
||||
adminGetPatents,
|
||||
adminCreatePatent,
|
||||
adminUpdatePatent,
|
||||
adminDeletePatent,
|
||||
AdminPublication,
|
||||
AdminPatent,
|
||||
} from "../../../lib/adminApi";
|
||||
|
||||
export default function AdminPublicationsPage() {
|
||||
const [tab, setTab] = useState<"publications" | "patents">("publications");
|
||||
const [publications, setPublications] = useState<AdminPublication[]>([]);
|
||||
const [patents, setPatents] = useState<AdminPatent[]>([]);
|
||||
const [filterCategory, setFilterCategory] = useState<string>("all");
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [successMsg, setSuccessMsg] = useState<string | null>(null);
|
||||
|
||||
// Publication Modal State
|
||||
const [isPubModalOpen, setIsPubModalOpen] = useState(false);
|
||||
const [editingPub, setEditingPub] = useState<AdminPublication | null>(null);
|
||||
const [pubForm, setPubForm] = useState({
|
||||
category: "intl-journal-conf" as "intl-journal-conf" | "domestic-journal-conf",
|
||||
title: "",
|
||||
authors: "",
|
||||
venue: "",
|
||||
volume: "",
|
||||
published_at: "",
|
||||
kci: false,
|
||||
doi: "",
|
||||
is_highlight: false,
|
||||
});
|
||||
|
||||
// Patent Modal State
|
||||
const [isPatentModalOpen, setIsPatentModalOpen] = useState(false);
|
||||
const [editingPatent, setEditingPatent] = useState<AdminPatent | null>(null);
|
||||
const [patentForm, setPatentForm] = useState({
|
||||
title: "",
|
||||
inventors: "",
|
||||
application_no: "",
|
||||
application_at: "",
|
||||
registration_no: "",
|
||||
registration_at: "",
|
||||
country: "Korea",
|
||||
published_at: "",
|
||||
});
|
||||
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [deletingPubId, setDeletingPubId] = useState<number | null>(null);
|
||||
const [deletingPatentId, setDeletingPatentId] = useState<number | null>(null);
|
||||
|
||||
const loadData = async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
const [pubList, patList] = await Promise.all([adminGetPublications(), adminGetPatents()]);
|
||||
setPublications(pubList);
|
||||
setPatents(patList);
|
||||
} catch (err: any) {
|
||||
setError(err?.message || "Failed to load publications.");
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
loadData();
|
||||
}, []);
|
||||
|
||||
// Pub Handlers
|
||||
const handleOpenPubCreate = () => {
|
||||
setEditingPub(null);
|
||||
setPubForm({
|
||||
category: "intl-journal-conf",
|
||||
title: "",
|
||||
authors: "",
|
||||
venue: "",
|
||||
volume: "",
|
||||
published_at: new Date().getFullYear().toString(),
|
||||
kci: false,
|
||||
doi: "",
|
||||
is_highlight: false,
|
||||
});
|
||||
setIsPubModalOpen(true);
|
||||
};
|
||||
|
||||
const handleOpenPubEdit = (p: AdminPublication) => {
|
||||
setEditingPub(p);
|
||||
setPubForm({
|
||||
category: p.category,
|
||||
title: p.title,
|
||||
authors: p.authors || "",
|
||||
venue: p.venue,
|
||||
volume: p.volume || "",
|
||||
published_at: p.published_at,
|
||||
kci: p.kci,
|
||||
doi: p.doi || "",
|
||||
is_highlight: p.is_highlight,
|
||||
});
|
||||
setIsPubModalOpen(true);
|
||||
};
|
||||
|
||||
const handlePubSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
const token = getClientToken();
|
||||
if (!token) return;
|
||||
|
||||
const payload = {
|
||||
category: pubForm.category,
|
||||
title: pubForm.title.trim(),
|
||||
authors: pubForm.authors.trim() || undefined,
|
||||
venue: pubForm.venue.trim(),
|
||||
volume: pubForm.volume.trim() || undefined,
|
||||
published_at: pubForm.published_at.trim(),
|
||||
kci: pubForm.kci,
|
||||
doi: pubForm.doi.trim() || undefined,
|
||||
is_highlight: pubForm.is_highlight,
|
||||
};
|
||||
|
||||
setIsSubmitting(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
if (editingPub) {
|
||||
await adminUpdatePublication(token, editingPub.id, payload);
|
||||
setSuccessMsg(`Publication "${payload.title}" updated.`);
|
||||
} else {
|
||||
await adminCreatePublication(token, payload);
|
||||
setSuccessMsg(`Publication "${payload.title}" created.`);
|
||||
}
|
||||
setIsPubModalOpen(false);
|
||||
await loadData();
|
||||
} catch (err: any) {
|
||||
setError(err?.message || "Failed to save publication.");
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleConfirmDeletePub = async () => {
|
||||
if (!deletingPubId) return;
|
||||
const token = getClientToken();
|
||||
if (!token) return;
|
||||
|
||||
setIsSubmitting(true);
|
||||
try {
|
||||
await adminDeletePublication(token, deletingPubId);
|
||||
setSuccessMsg("Publication deleted successfully.");
|
||||
setDeletingPubId(null);
|
||||
await loadData();
|
||||
} catch (err: any) {
|
||||
setError(err?.message || "Failed to delete publication.");
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Patent Handlers
|
||||
const handleOpenPatentCreate = () => {
|
||||
setEditingPatent(null);
|
||||
setPatentForm({
|
||||
title: "",
|
||||
inventors: "",
|
||||
application_no: "",
|
||||
application_at: "",
|
||||
registration_no: "",
|
||||
registration_at: "",
|
||||
country: "Korea",
|
||||
published_at: new Date().getFullYear().toString(),
|
||||
});
|
||||
setIsPatentModalOpen(true);
|
||||
};
|
||||
|
||||
const handleOpenPatentEdit = (p: AdminPatent) => {
|
||||
setEditingPatent(p);
|
||||
setPatentForm({
|
||||
title: p.title,
|
||||
inventors: p.inventors,
|
||||
application_no: p.application_no,
|
||||
application_at: p.application_at,
|
||||
registration_no: p.registration_no || "",
|
||||
registration_at: p.registration_at || "",
|
||||
country: p.country,
|
||||
published_at: p.published_at,
|
||||
});
|
||||
setIsPatentModalOpen(true);
|
||||
};
|
||||
|
||||
const handlePatentSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
const token = getClientToken();
|
||||
if (!token) return;
|
||||
|
||||
const payload = {
|
||||
title: patentForm.title.trim(),
|
||||
inventors: patentForm.inventors.trim(),
|
||||
application_no: patentForm.application_no.trim(),
|
||||
application_at: patentForm.application_at.trim(),
|
||||
registration_no: patentForm.registration_no.trim() || undefined,
|
||||
registration_at: patentForm.registration_at.trim() || undefined,
|
||||
country: patentForm.country.trim(),
|
||||
published_at: patentForm.published_at.trim(),
|
||||
};
|
||||
|
||||
setIsSubmitting(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
if (editingPatent) {
|
||||
await adminUpdatePatent(token, editingPatent.id, payload);
|
||||
setSuccessMsg(`Patent "${payload.title}" updated.`);
|
||||
} else {
|
||||
await adminCreatePatent(token, payload);
|
||||
setSuccessMsg(`Patent "${payload.title}" created.`);
|
||||
}
|
||||
setIsPatentModalOpen(false);
|
||||
await loadData();
|
||||
} catch (err: any) {
|
||||
setError(err?.message || "Failed to save patent.");
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleConfirmDeletePatent = async () => {
|
||||
if (!deletingPatentId) return;
|
||||
const token = getClientToken();
|
||||
if (!token) return;
|
||||
|
||||
setIsSubmitting(true);
|
||||
try {
|
||||
await adminDeletePatent(token, deletingPatentId);
|
||||
setSuccessMsg("Patent deleted successfully.");
|
||||
setDeletingPatentId(null);
|
||||
await loadData();
|
||||
} catch (err: any) {
|
||||
setError(err?.message || "Failed to delete patent.");
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const filteredPubs =
|
||||
filterCategory === "all"
|
||||
? publications
|
||||
: publications.filter((p) => p.category === filterCategory);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<AdminHeader
|
||||
title="Publications & Patents Management"
|
||||
description="Manage journal articles, conference proceedings, and registered laboratory patents."
|
||||
action={
|
||||
<button
|
||||
onClick={tab === "publications" ? handleOpenPubCreate : handleOpenPatentCreate}
|
||||
className="px-4 py-2 text-sm rounded bg-cobalt hover:bg-cobalt/90 text-white font-medium shadow-sm transition-colors"
|
||||
>
|
||||
{tab === "publications" ? "+ Add Publication" : "+ Add Patent"}
|
||||
</button>
|
||||
}
|
||||
/>
|
||||
|
||||
{/* Tabs */}
|
||||
<div className="flex border-b border-line mb-6 gap-6">
|
||||
<button
|
||||
onClick={() => setTab("publications")}
|
||||
className={`pb-3 text-sm font-semibold border-b-2 transition-colors ${
|
||||
tab === "publications" ? "border-cobalt text-cobalt" : "border-transparent text-ink/60 hover:text-ink"
|
||||
}`}
|
||||
>
|
||||
Publications ({publications.length})
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setTab("patents")}
|
||||
className={`pb-3 text-sm font-semibold border-b-2 transition-colors ${
|
||||
tab === "patents" ? "border-cobalt text-cobalt" : "border-transparent text-ink/60 hover:text-ink"
|
||||
}`}
|
||||
>
|
||||
Patents ({patents.length})
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{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 records...</div>
|
||||
) : tab === "publications" ? (
|
||||
<div>
|
||||
{/* Filter sub-bar */}
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
<span className="text-xs font-semibold text-ink/70">Filter:</span>
|
||||
<select
|
||||
value={filterCategory}
|
||||
onChange={(e) => setFilterCategory(e.target.value)}
|
||||
className="px-2.5 py-1 text-xs border border-line rounded bg-paper text-ink focus:outline-none focus:ring-1 focus:ring-cobalt"
|
||||
>
|
||||
<option value="all">All Categories ({publications.length})</option>
|
||||
<option value="intl-journal-conf">
|
||||
International Journals & Conferences ({publications.filter((p) => p.category === "intl-journal-conf").length})
|
||||
</option>
|
||||
<option value="domestic-journal-conf">
|
||||
Domestic Journals & Conferences ({publications.filter((p) => p.category === "domestic-journal-conf").length})
|
||||
</option>
|
||||
</select>
|
||||
</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 w-28">Date</th>
|
||||
<th className="p-4">Title & Venue</th>
|
||||
<th className="p-4 w-36">Category</th>
|
||||
<th className="p-4 text-right">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-line">
|
||||
{filteredPubs.length === 0 ? (
|
||||
<tr>
|
||||
<td colSpan={4} className="p-6 text-center text-ink/60">
|
||||
No publications found.
|
||||
</td>
|
||||
</tr>
|
||||
) : (
|
||||
filteredPubs.map((p) => (
|
||||
<tr key={p.id} className="hover:bg-ivory/50">
|
||||
<td className="p-4 text-xs font-mono text-ink/80">{p.published_at}</td>
|
||||
<td className="p-4">
|
||||
<div className="font-semibold text-ink flex items-center gap-2">
|
||||
{p.title}
|
||||
{p.is_highlight && (
|
||||
<span className="px-1.5 py-0.5 text-[10px] rounded bg-amber-500/10 text-amber-600 dark:text-amber-400 font-bold">
|
||||
Highlight
|
||||
</span>
|
||||
)}
|
||||
{p.kci && (
|
||||
<span className="px-1.5 py-0.5 text-[10px] rounded bg-blue-500/10 text-blue-600 dark:text-blue-400 font-bold">
|
||||
KCI
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="text-xs text-ink/70 mt-0.5">
|
||||
{p.venue} {p.volume && `· ${p.volume}`}
|
||||
</div>
|
||||
{p.authors && <div className="text-xs text-ink/50 mt-0.5">{p.authors}</div>}
|
||||
</td>
|
||||
<td className="p-4 text-xs text-ink/70">
|
||||
{p.category === "intl-journal-conf" ? "International" : "Domestic"}
|
||||
</td>
|
||||
<td className="p-4 text-right space-x-2">
|
||||
<button
|
||||
onClick={() => handleOpenPubEdit(p)}
|
||||
className="px-2.5 py-1 text-xs rounded border border-line hover:bg-ivory font-medium text-ink"
|
||||
>
|
||||
Edit
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setDeletingPubId(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>
|
||||
</div>
|
||||
) : (
|
||||
/* Patents Table */
|
||||
<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 w-28">Date</th>
|
||||
<th className="p-4">Title & Inventors</th>
|
||||
<th className="p-4">App / Reg Numbers</th>
|
||||
<th className="p-4 text-right">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-line">
|
||||
{patents.length === 0 ? (
|
||||
<tr>
|
||||
<td colSpan={4} className="p-6 text-center text-ink/60">
|
||||
No patents found.
|
||||
</td>
|
||||
</tr>
|
||||
) : (
|
||||
patents.map((p) => (
|
||||
<tr key={p.id} className="hover:bg-ivory/50">
|
||||
<td className="p-4 text-xs font-mono text-ink/80">{p.published_at}</td>
|
||||
<td className="p-4">
|
||||
<div className="font-semibold text-ink">{p.title}</div>
|
||||
<div className="text-xs text-ink/60 mt-0.5">{p.inventors}</div>
|
||||
</td>
|
||||
<td className="p-4 text-xs text-ink/80">
|
||||
<div>App: {p.application_no} ({p.application_at})</div>
|
||||
{p.registration_no && (
|
||||
<div className="text-emerald-600 dark:text-emerald-400 font-medium">
|
||||
Reg: {p.registration_no} ({p.registration_at || "—"})
|
||||
</div>
|
||||
)}
|
||||
</td>
|
||||
<td className="p-4 text-right space-x-2">
|
||||
<button
|
||||
onClick={() => handleOpenPatentEdit(p)}
|
||||
className="px-2.5 py-1 text-xs rounded border border-line hover:bg-ivory font-medium text-ink"
|
||||
>
|
||||
Edit
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setDeletingPatentId(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>
|
||||
)}
|
||||
|
||||
{/* Pub Modal */}
|
||||
<Modal
|
||||
isOpen={isPubModalOpen}
|
||||
title={editingPub ? "Edit Publication" : "Create Publication"}
|
||||
onClose={() => setIsPubModalOpen(false)}
|
||||
>
|
||||
<form onSubmit={handlePubSubmit} 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">Category *</label>
|
||||
<select
|
||||
value={pubForm.category}
|
||||
onChange={(e) =>
|
||||
setPubForm({
|
||||
...pubForm,
|
||||
category: e.target.value as "intl-journal-conf" | "domestic-journal-conf",
|
||||
})
|
||||
}
|
||||
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"
|
||||
>
|
||||
<option value="intl-journal-conf">International Journal / Conference</option>
|
||||
<option value="domestic-journal-conf">Domestic Journal / Conference</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs font-semibold text-ink mb-1">Published Date (ISO 8601) *</label>
|
||||
<input
|
||||
type="text"
|
||||
value={pubForm.published_at}
|
||||
onChange={(e) => setPubForm({ ...pubForm, published_at: e.target.value })}
|
||||
placeholder="e.g. 2023-11"
|
||||
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">Title *</label>
|
||||
<input
|
||||
type="text"
|
||||
value={pubForm.title}
|
||||
onChange={(e) => setPubForm({ ...pubForm, title: e.target.value })}
|
||||
placeholder="Full paper 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>
|
||||
<label className="block text-xs font-semibold text-ink mb-1">Authors</label>
|
||||
<input
|
||||
type="text"
|
||||
value={pubForm.authors}
|
||||
onChange={(e) => setPubForm({ ...pubForm, authors: e.target.value })}
|
||||
placeholder="e.g. Gildong Hong, Prof. Advisor"
|
||||
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">Venue (Journal / Conference) *</label>
|
||||
<input
|
||||
type="text"
|
||||
value={pubForm.venue}
|
||||
onChange={(e) => setPubForm({ ...pubForm, venue: e.target.value })}
|
||||
placeholder="e.g. IEEE Transactions on Communications"
|
||||
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">Volume / Pages</label>
|
||||
<input
|
||||
type="text"
|
||||
value={pubForm.volume}
|
||||
onChange={(e) => setPubForm({ ...pubForm, volume: e.target.value })}
|
||||
placeholder="e.g. Vol. 70, No. 3, pp. 120-135"
|
||||
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 className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-xs font-semibold text-ink mb-1">DOI (optional)</label>
|
||||
<input
|
||||
type="text"
|
||||
value={pubForm.doi}
|
||||
onChange={(e) => setPubForm({ ...pubForm, doi: e.target.value })}
|
||||
placeholder="e.g. 10.1109/TCOMM.2023.123456"
|
||||
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 items-center gap-4 pt-5">
|
||||
<label className="flex items-center gap-2 text-xs font-semibold text-ink cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={pubForm.is_highlight}
|
||||
onChange={(e) => setPubForm({ ...pubForm, is_highlight: e.target.checked })}
|
||||
className="rounded text-cobalt focus:ring-cobalt"
|
||||
/>
|
||||
Home Highlight
|
||||
</label>
|
||||
<label className="flex items-center gap-2 text-xs font-semibold text-ink cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={pubForm.kci}
|
||||
onChange={(e) => setPubForm({ ...pubForm, kci: e.target.checked })}
|
||||
className="rounded text-cobalt focus:ring-cobalt"
|
||||
/>
|
||||
KCI Indexed
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end gap-3 pt-4 border-t border-line">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIsPubModalOpen(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..." : editingPub ? "Save Changes" : "Create Publication"}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</Modal>
|
||||
|
||||
{/* Patent Modal */}
|
||||
<Modal
|
||||
isOpen={isPatentModalOpen}
|
||||
title={editingPatent ? "Edit Patent" : "Create Patent"}
|
||||
onClose={() => setIsPatentModalOpen(false)}
|
||||
>
|
||||
<form onSubmit={handlePatentSubmit} className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-xs font-semibold text-ink mb-1">Patent Title *</label>
|
||||
<input
|
||||
type="text"
|
||||
value={patentForm.title}
|
||||
onChange={(e) => setPatentForm({ ...patentForm, title: e.target.value })}
|
||||
placeholder="e.g. Method and system for optical camera communications"
|
||||
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 className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-xs font-semibold text-ink mb-1">Inventors *</label>
|
||||
<input
|
||||
type="text"
|
||||
value={patentForm.inventors}
|
||||
onChange={(e) => setPatentForm({ ...patentForm, inventors: e.target.value })}
|
||||
placeholder="e.g. Gildong Hong, Advisor"
|
||||
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">Country *</label>
|
||||
<input
|
||||
type="text"
|
||||
value={patentForm.country}
|
||||
onChange={(e) => setPatentForm({ ...patentForm, country: e.target.value })}
|
||||
placeholder="e.g. Korea or USA"
|
||||
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 className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-xs font-semibold text-ink mb-1">Application No. *</label>
|
||||
<input
|
||||
type="text"
|
||||
value={patentForm.application_no}
|
||||
onChange={(e) => setPatentForm({ ...patentForm, application_no: e.target.value })}
|
||||
placeholder="e.g. 10-2023-0123456"
|
||||
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">Application Date *</label>
|
||||
<input
|
||||
type="text"
|
||||
value={patentForm.application_at}
|
||||
onChange={(e) => setPatentForm({ ...patentForm, application_at: e.target.value })}
|
||||
placeholder="e.g. 2023-05-12"
|
||||
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 className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-xs font-semibold text-ink mb-1">Registration No. (optional)</label>
|
||||
<input
|
||||
type="text"
|
||||
value={patentForm.registration_no}
|
||||
onChange={(e) => setPatentForm({ ...patentForm, registration_no: e.target.value })}
|
||||
placeholder="e.g. 10-2543210"
|
||||
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">Registration Date</label>
|
||||
<input
|
||||
type="text"
|
||||
value={patentForm.registration_at}
|
||||
onChange={(e) => setPatentForm({ ...patentForm, registration_at: e.target.value })}
|
||||
placeholder="e.g. 2024-01-10"
|
||||
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">Primary Display Year/Date *</label>
|
||||
<input
|
||||
type="text"
|
||||
value={patentForm.published_at}
|
||||
onChange={(e) => setPatentForm({ ...patentForm, published_at: e.target.value })}
|
||||
placeholder="e.g. 2023"
|
||||
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 className="flex justify-end gap-3 pt-4 border-t border-line">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIsPatentModalOpen(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..." : editingPatent ? "Save Changes" : "Create Patent"}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</Modal>
|
||||
|
||||
<ConfirmDeleteModal
|
||||
isOpen={deletingPubId !== null}
|
||||
onClose={() => setDeletingPubId(null)}
|
||||
onConfirm={handleConfirmDeletePub}
|
||||
isSubmitting={isSubmitting}
|
||||
title="Delete Publication"
|
||||
description="Are you sure you want to delete this publication?"
|
||||
/>
|
||||
|
||||
<ConfirmDeleteModal
|
||||
isOpen={deletingPatentId !== null}
|
||||
onClose={() => setDeletingPatentId(null)}
|
||||
onConfirm={handleConfirmDeletePatent}
|
||||
isSubmitting={isSubmitting}
|
||||
title="Delete Patent"
|
||||
description="Are you sure you want to delete this patent record?"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user