172 lines
5.8 KiB
TypeScript
172 lines
5.8 KiB
TypeScript
"use client";
|
|
import React, { useState } from "react";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|
import { Input } from "@/components/ui/input";
|
|
import { Label } from "@/components/ui/label";
|
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
|
import { Checkbox } from "@/components/ui/checkbox";
|
|
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible";
|
|
import { ChevronDownIcon, ChevronUpIcon, PlusIcon, TrashIcon, CopyIcon, ArrowUpIcon, ArrowDownIcon } from "@/components/icons";
|
|
import { FormField } from "./FormField";
|
|
|
|
interface DynamicArrayProps<T> {
|
|
items: T[];
|
|
onItemsChange: (items: T[]) => void;
|
|
renderItem: (item: T, index: number, onUpdate: (item: T) => void, onDelete: () => void) => React.ReactNode;
|
|
addItemLabel?: string;
|
|
emptyStateMessage?: string;
|
|
allowReorder?: boolean;
|
|
allowDuplicate?: boolean;
|
|
maxItems?: number;
|
|
className?: string;
|
|
}
|
|
|
|
export function DynamicArray<T>({
|
|
items,
|
|
onItemsChange,
|
|
renderItem,
|
|
addItemLabel = "Add Item",
|
|
emptyStateMessage = "No items added yet",
|
|
allowReorder = true,
|
|
allowDuplicate = true,
|
|
maxItems,
|
|
className = "",
|
|
}: DynamicArrayProps<T>) {
|
|
const [expandedItems, setExpandedItems] = useState<Set<number>>(new Set());
|
|
|
|
const addItem = () => {
|
|
if (maxItems && items.length >= maxItems) return;
|
|
const newItem = {} as T;
|
|
onItemsChange([...items, newItem]);
|
|
};
|
|
|
|
const updateItem = (index: number, updatedItem: T) => {
|
|
const newItems = [...items];
|
|
newItems[index] = updatedItem;
|
|
onItemsChange(newItems);
|
|
};
|
|
|
|
const deleteItem = (index: number) => {
|
|
const newItems = items.filter((_, i) => i !== index);
|
|
onItemsChange(newItems);
|
|
};
|
|
|
|
const duplicateItem = (index: number) => {
|
|
if (maxItems && items.length >= maxItems) return;
|
|
const duplicatedItem = { ...items[index] };
|
|
const newItems = [...items];
|
|
newItems.splice(index + 1, 0, duplicatedItem);
|
|
onItemsChange(newItems);
|
|
};
|
|
|
|
const moveItem = (index: number, direction: 'up' | 'down') => {
|
|
const newItems = [...items];
|
|
const targetIndex = direction === 'up' ? index - 1 : index + 1;
|
|
|
|
if (targetIndex < 0 || targetIndex >= items.length) return;
|
|
|
|
[newItems[index], newItems[targetIndex]] = [newItems[targetIndex], newItems[index]];
|
|
onItemsChange(newItems);
|
|
};
|
|
|
|
const toggleExpanded = (index: number) => {
|
|
const newExpanded = new Set(expandedItems);
|
|
if (newExpanded.has(index)) {
|
|
newExpanded.delete(index);
|
|
} else {
|
|
newExpanded.add(index);
|
|
}
|
|
setExpandedItems(newExpanded);
|
|
};
|
|
|
|
return (
|
|
<div className={`space-y-4 ${className}`}>
|
|
<div className="flex items-center justify-between">
|
|
<h3 className="text-lg font-medium">Items ({items.length})</h3>
|
|
<Button
|
|
type="button"
|
|
onClick={addItem}
|
|
disabled={maxItems ? items.length >= maxItems : false}
|
|
className="flex items-center gap-2"
|
|
>
|
|
<PlusIcon className="h-4 w-4" />
|
|
{addItemLabel}
|
|
</Button>
|
|
</div>
|
|
|
|
{items.length === 0 ? (
|
|
<Card>
|
|
<CardContent className="flex items-center justify-center py-8">
|
|
<p className="text-gray-500">{emptyStateMessage}</p>
|
|
</CardContent>
|
|
</Card>
|
|
) : (
|
|
<div className="space-y-3">
|
|
{items.map((item, index) => (
|
|
<Card key={index} className="border border-gray-200">
|
|
<CardHeader className="pb-3">
|
|
<div className="flex items-center justify-between">
|
|
<CardTitle className="text-base">
|
|
Item {index + 1}
|
|
</CardTitle>
|
|
<div className="flex items-center gap-2">
|
|
{allowReorder && (
|
|
<>
|
|
<Button
|
|
type="button"
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={() => moveItem(index, 'up')}
|
|
disabled={index === 0}
|
|
>
|
|
<ArrowUpIcon className="h-4 w-4" />
|
|
</Button>
|
|
<Button
|
|
type="button"
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={() => moveItem(index, 'down')}
|
|
disabled={index === items.length - 1}
|
|
>
|
|
<ArrowDownIcon className="h-4 w-4" />
|
|
</Button>
|
|
</>
|
|
)}
|
|
|
|
{allowDuplicate && (
|
|
<Button
|
|
type="button"
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={() => duplicateItem(index)}
|
|
disabled={maxItems ? items.length >= maxItems : false}
|
|
>
|
|
<CopyIcon className="h-4 w-4" />
|
|
</Button>
|
|
)}
|
|
|
|
<Button
|
|
type="button"
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={() => deleteItem(index)}
|
|
className="text-red-600 hover:text-red-700"
|
|
>
|
|
<TrashIcon className="h-4 w-4" />
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</CardHeader>
|
|
|
|
<CardContent>
|
|
{renderItem(item, index, (updatedItem) => updateItem(index, updatedItem), () => deleteItem(index))}
|
|
</CardContent>
|
|
</Card>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|