Converts secrets list to something more generic

This commit is contained in:
2025-05-31 15:47:29 -07:00
parent b3d4e90e7f
commit 6336b150a7
6 changed files with 386 additions and 48 deletions

View File

@@ -0,0 +1,115 @@
import type { Document } from "@/lib/types";
import {
Dialog,
DialogPanel,
DialogTitle,
Transition,
TransitionChild,
} from "@headlessui/react";
import { Fragment, useState } from "react";
type Props<T extends Document> = {
title: React.ReactNode;
items: T[];
renderRow: (item: T) => React.ReactNode;
newItemForm: (onSubmit: () => void) => React.ReactNode;
};
/**
* DocumentList is a generic list component for displaying document items with a dialog for adding new items.
*
* @param title - The title displayed above the list (left-aligned)
* @param items - The array of document items to display
* @param renderRow - Function to render each row's content
* @param newItemForm - Function that renders a form for creating a new item; receives an onSubmit callback
*/
export function DocumentList<T extends Document>({
title,
items,
renderRow,
newItemForm,
}: Props<T>) {
const [open, setOpen] = useState(false);
// Handles closing the dialog after form submission
const handleFormSubmit = (): void => {
setOpen(false);
};
return (
<section className="w-full max-w-2xl mx-auto">
<div className="flex items-center justify-between mb-4">
<h2 className="text-xl font-bold text-slate-100">{title}</h2>
<button
type="button"
className="inline-flex items-center justify-center rounded-full bg-violet-600 hover:bg-violet-700 text-white w-9 h-9 focus:outline-none focus:ring-2 focus:ring-violet-400"
aria-label="Add new item"
onClick={() => setOpen(true)}
>
<svg
className="w-5 h-5"
fill="none"
stroke="currentColor"
strokeWidth={2}
viewBox="0 0 24 24"
aria-hidden="true"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M12 4v16m8-8H4"
/>
</svg>
</button>
</div>
<ul className="space-y-2">
{items.map((item) => (
<li key={item.id} className="bg-slate-800 rounded p-4 text-slate-100">
{renderRow(item)}
</li>
))}
</ul>
<Transition show={open} as={Fragment}>
<Dialog as="div" className="relative z-50" onClose={setOpen}>
<TransitionChild
as={Fragment}
enter="ease-out duration-200"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in duration-150"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="fixed inset-0 bg-black bg-opacity-50 transition-opacity" />
</TransitionChild>
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
<TransitionChild
as={Fragment}
enter="ease-out duration-200"
enterFrom="opacity-0 scale-95"
enterTo="opacity-100 scale-100"
leave="ease-in duration-150"
leaveFrom="opacity-100 scale-100"
leaveTo="opacity-0 scale-95"
>
<DialogPanel className="bg-slate-900 rounded-lg shadow-xl max-w-md w-full p-6 border border-slate-700 relative">
<DialogTitle className="text-lg font-semibold text-slate-100 mb-4">
Add New
</DialogTitle>
{newItemForm(handleFormSubmit)}
<button
type="button"
className="absolute top-3 right-3 text-slate-400 hover:text-red-400 focus:outline-none"
aria-label="Close dialog"
onClick={() => setOpen(false)}
>
<span aria-hidden="true">×</span>
</button>
</DialogPanel>
</TransitionChild>
</div>
</Dialog>
</Transition>
</section>
);
}