129 lines
6.3 KiB
TypeScript
129 lines
6.3 KiB
TypeScript
import React, { useState } from 'react';
|
|
|
|
interface Item {
|
|
[key: string]: any;
|
|
}
|
|
|
|
interface FieldConfig {
|
|
key: string;
|
|
label: string;
|
|
type: 'text' | 'textarea';
|
|
}
|
|
|
|
interface EditableCardProps<T extends Item> {
|
|
title: string;
|
|
items: T[];
|
|
onItemsChange: (items: T[]) => void;
|
|
fieldConfigs: FieldConfig[];
|
|
newItemTemplate: T;
|
|
renderDisplay: (item: T, index: number) => React.ReactNode;
|
|
showAddButton?: boolean;
|
|
t: {
|
|
add: string;
|
|
cancel: string;
|
|
save: string;
|
|
}
|
|
}
|
|
|
|
const PencilIcon = () => (
|
|
<svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.5L14.732 3.732z" /></svg>
|
|
);
|
|
const TrashIcon = () => (
|
|
<svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" /></svg>
|
|
);
|
|
|
|
export const EditableCard = <T extends Item,>({ title, items, onItemsChange, fieldConfigs, newItemTemplate, renderDisplay, showAddButton, t }: EditableCardProps<T>) => {
|
|
const [editingIndex, setEditingIndex] = useState<number | null>(null);
|
|
const [editItem, setEditItem] = useState<T | null>(null);
|
|
|
|
const handleEdit = (index: number) => {
|
|
setEditingIndex(index);
|
|
setEditItem({ ...items[index] });
|
|
};
|
|
|
|
const handleSave = () => {
|
|
if (editingIndex !== null && editItem) {
|
|
const newItems = [...items];
|
|
newItems[editingIndex] = editItem;
|
|
onItemsChange(newItems);
|
|
setEditingIndex(null);
|
|
setEditItem(null);
|
|
}
|
|
};
|
|
|
|
const handleCancel = () => {
|
|
setEditingIndex(null);
|
|
setEditItem(null);
|
|
};
|
|
|
|
const handleRemove = (index: number) => {
|
|
onItemsChange(items.filter((_, i) => i !== index));
|
|
};
|
|
|
|
const handleAdd = () => {
|
|
onItemsChange([...items, newItemTemplate]);
|
|
setEditingIndex(items.length);
|
|
setEditItem(newItemTemplate);
|
|
};
|
|
|
|
const handleInputChange = (key: string, value: string) => {
|
|
if (editItem) {
|
|
setEditItem({ ...editItem, [key]: value });
|
|
}
|
|
};
|
|
|
|
const inputClasses = "w-full bg-light-secondary dark:bg-brand-secondary text-light-text dark:text-brand-text border border-light-accent dark:border-brand-accent rounded-md px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-brand-highlight";
|
|
|
|
return (
|
|
<div className="bg-light-secondary dark:bg-brand-secondary p-6 rounded-lg shadow-lg mb-6 border border-light-accent dark:border-brand-accent">
|
|
<div className="flex justify-between items-center mb-4">
|
|
<h3 className="text-xl font-bold">{title}</h3>
|
|
{(showAddButton ?? true) && (
|
|
<button onClick={handleAdd} className="bg-brand-accent hover:bg-brand-light text-white font-bold py-1 px-3 rounded-md text-sm transition-colors">+ {t.add}</button>
|
|
)}
|
|
</div>
|
|
<div className="space-y-4">
|
|
{items.map((item, index) => (
|
|
<div key={index} className="bg-light-primary dark:bg-brand-primary p-4 rounded-md">
|
|
{editingIndex === index && editItem ? (
|
|
<div className="space-y-3">
|
|
{fieldConfigs.map(field => (
|
|
<div key={field.key}>
|
|
<label className="block text-sm font-medium text-light-subtle dark:text-brand-light mb-1">{field.label}</label>
|
|
{field.type === 'textarea' ? (
|
|
<textarea
|
|
value={editItem[field.key]}
|
|
onChange={(e) => handleInputChange(field.key, e.target.value)}
|
|
className={inputClasses}
|
|
rows={3}
|
|
/>
|
|
) : (
|
|
<input
|
|
type="text"
|
|
value={editItem[field.key]}
|
|
onChange={(e) => handleInputChange(field.key, e.target.value)}
|
|
className={inputClasses}
|
|
/>
|
|
)}
|
|
</div>
|
|
))}
|
|
<div className="flex justify-end space-x-2 mt-2">
|
|
<button onClick={handleCancel} className="bg-gray-500 hover:bg-gray-600 text-white py-1 px-3 rounded-md text-sm transition-colors">{t.cancel}</button>
|
|
<button onClick={handleSave} className="bg-brand-highlight hover:bg-blue-600 text-white py-1 px-3 rounded-md text-sm transition-colors">{t.save}</button>
|
|
</div>
|
|
</div>
|
|
) : (
|
|
<div className="flex justify-between items-start">
|
|
<div className="flex-grow">{renderDisplay(item, index)}</div>
|
|
<div className="flex space-x-2 flex-shrink-0 ml-4">
|
|
<button onClick={() => handleEdit(index)} className="text-light-subtle dark:text-brand-light hover:text-light-text dark:hover:text-white p-1 rounded-full transition-colors"><PencilIcon /></button>
|
|
<button onClick={() => handleRemove(index)} className="text-light-subtle dark:text-brand-light hover:text-red-500 p-1 rounded-full transition-colors"><TrashIcon /></button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
);
|
|
}; |