Converts secrets list to something more generic
This commit is contained in:
115
src/components/DocumentList.tsx
Normal file
115
src/components/DocumentList.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user