Files
landing_page/refer_landing_page/app/console/publications/page.tsx
T
Godopu 1410dcf89d feat(publications): add multi-level year filtering for publications and patents
- Add Year filter dropdown with dynamic record counts in admin console (/console/publications)
- Add Year filter dropdown in public category publications page (/publications/[category])
- Comply with AC-44 WCAG 2.1 AA small text contrast rules (text-cobalt-dark)
- Verified with E2E test suite (Exit Code 0)
2026-08-25 16:07:44 +09:00

829 lines
32 KiB
TypeScript

"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 [filterYear, setFilterYear] = useState<string>("all");
const [filterPatentYear, setFilterPatentYear] = 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);
}
};
// Extract unique sorted years from publications
const pubYears = Array.from(
new Set(
publications
.map((p) => {
const match = (p.published_at || "").match(/\b(19\d\d|20\d\d)\b/);
return match ? match[1] : null;
})
.filter((y): y is string => Boolean(y))
)
).sort((a, b) => b.localeCompare(a));
// Extract unique sorted years from patents
const patentYears = Array.from(
new Set(
patents
.map((p) => {
const dateStr = p.published_at || p.application_at || p.registration_at || "";
const match = dateStr.match(/\b(19\d\d|20\d\d)\b/);
return match ? match[1] : null;
})
.filter((y): y is string => Boolean(y))
)
).sort((a, b) => b.localeCompare(a));
const filteredPubs = publications.filter((p) => {
const matchCategory = filterCategory === "all" || p.category === filterCategory;
const pubYear = (p.published_at || "").match(/\b(19\d\d|20\d\d)\b/)?.[1] || "";
const matchYear = filterYear === "all" || pubYear === filterYear;
return matchCategory && matchYear;
});
const filteredPatents = patents.filter((p) => {
if (filterPatentYear === "all") return true;
const dateStr = p.published_at || p.application_at || p.registration_at || "";
const patYear = dateStr.match(/\b(19\d\d|20\d\d)\b/)?.[1] || "";
return patYear === filterPatentYear;
});
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 flex-wrap items-center gap-3 mb-4">
<div className="flex items-center gap-2">
<span className="text-xs font-semibold text-ink/70">Category:</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="flex items-center gap-2">
<span className="text-xs font-semibold text-ink/70">Year:</span>
<select
value={filterYear}
onChange={(e) => setFilterYear(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 Years ({publications.length})</option>
{pubYears.map((yr) => {
const count = publications.filter((p) => (p.published_at || "").includes(yr)).length;
return (
<option key={yr} value={yr}>
{yr} ({count})
</option>
);
})}
</select>
</div>
{(filterCategory !== "all" || filterYear !== "all") && (
<button
onClick={() => {
setFilterCategory("all");
setFilterYear("all");
}}
className="text-xs text-cobalt-dark hover:underline font-mono"
>
Reset filters
</button>
)}
<span className="text-xs text-ink/50 ml-auto">
Showing {filteredPubs.length} of {publications.length} records
</span>
</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>
{/* Filter sub-bar */}
<div className="flex flex-wrap items-center gap-3 mb-4">
<div className="flex items-center gap-2">
<span className="text-xs font-semibold text-ink/70">Year:</span>
<select
value={filterPatentYear}
onChange={(e) => setFilterPatentYear(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 Years ({patents.length})</option>
{patentYears.map((yr) => {
const count = patents.filter((p) => {
const d = p.published_at || p.application_at || p.registration_at || "";
return d.includes(yr);
}).length;
return (
<option key={yr} value={yr}>
{yr} ({count})
</option>
);
})}
</select>
</div>
{filterPatentYear !== "all" && (
<button
onClick={() => setFilterPatentYear("all")}
className="text-xs text-cobalt-dark hover:underline font-mono"
>
Reset filter
</button>
)}
<span className="text-xs text-ink/50 ml-auto">
Showing {filteredPatents.length} of {patents.length} records
</span>
</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 & 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">
{filteredPatents.length === 0 ? (
<tr>
<td colSpan={4} className="p-6 text-center text-ink/60">
No patents found.
</td>
</tr>
) : (
filteredPatents.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>
</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>
);
}