first commit; dx 1.0 beta
This commit is contained in:
@@ -0,0 +1,212 @@
|
||||
import React, { useState } from 'react';
|
||||
import { DndContext, closestCenter, PointerSensor, TouchSensor, useSensor, useSensors } from '@dnd-kit/core';
|
||||
import { arrayMove, SortableContext, useSortable, verticalListSortingStrategy } from '@dnd-kit/sortable';
|
||||
import { CSS } from '@dnd-kit/utilities';
|
||||
|
||||
// Configuration
|
||||
const COLUMNS = [
|
||||
{ key: 'position', label: 'Pos', width: 'w-16', placeholder: '#' },
|
||||
{ key: 'subject', label: 'Subject', width: 'flex-1', placeholder: 'Enter subject' },
|
||||
{ key: 'time', label: 'Time', width: 'w-24', placeholder: '00:00' },
|
||||
{ key: 'room', label: 'Room', width: 'w-20', placeholder: 'A-1' },
|
||||
];
|
||||
|
||||
const INITIAL_DATA = [
|
||||
{ id: '1', position: '1', subject: 'Mathematics', time: '09:00', room: 'A-1' },
|
||||
{ id: '2', position: '2', subject: 'Physics', time: '10:00', room: 'B-2' },
|
||||
{ id: '3', position: '3', subject: 'Chemistry', time: '11:00', room: 'A-3' },
|
||||
{ id: '4', position: '4', subject: 'Biology', time: '13:00', room: 'C-1' },
|
||||
];
|
||||
|
||||
function SortableRow({ item, columns, onUpdate, onDelete }) {
|
||||
const {
|
||||
attributes,
|
||||
listeners,
|
||||
setNodeRef,
|
||||
transform,
|
||||
transition,
|
||||
isDragging,
|
||||
} = useSortable({ id: item.id });
|
||||
|
||||
const style = {
|
||||
transform: CSS.Transform.toString(transform),
|
||||
transition,
|
||||
opacity: isDragging ? 0.5 : 1,
|
||||
};
|
||||
return (
|
||||
<div
|
||||
ref={setNodeRef}
|
||||
style={style}
|
||||
className="bg-gray-800 rounded-lg overflow-hidden mb-2"
|
||||
>
|
||||
<div className="flex items-stretch">
|
||||
{columns.map((col) => (
|
||||
<input
|
||||
key={col.key}
|
||||
type="text"
|
||||
value={item[col.key] || ''}
|
||||
onChange={(e) => onUpdate(item.id, col.key, e.target.value)}
|
||||
className={`${col.width} px-3 py-3 bg-transparent text-white border-r border-gray-700 last:border-r-0 focus:outline-none focus:bg-gray-700/50 placeholder-gray-500`}
|
||||
placeholder={col.placeholder}
|
||||
/>
|
||||
))}
|
||||
<div
|
||||
{...attributes}
|
||||
{...listeners}
|
||||
className="w-12 bg-gray-900 flex items-center justify-center cursor-move touch-none select-none text-gray-400 hover:text-white"
|
||||
>
|
||||
⋮
|
||||
</div>
|
||||
<button
|
||||
onClick={() => onDelete(item.id)}
|
||||
className="w-12 bg-red-600/20 text-red-400 hover:bg-red-600/40 hover:text-red-300 active:bg-red-600/60"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function ScheduleEditor({
|
||||
initialData = INITIAL_DATA,
|
||||
columns = COLUMNS,
|
||||
onChange,
|
||||
}) {
|
||||
const [items, setItems] = useState(initialData);
|
||||
|
||||
const sensors = useSensors(
|
||||
useSensor(PointerSensor, {
|
||||
activationConstraint: {
|
||||
distance: 8,
|
||||
},
|
||||
}),
|
||||
useSensor(TouchSensor, {
|
||||
activationConstraint: {
|
||||
delay: 200,
|
||||
tolerance: 8,
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
const updateItems = (newItems) => {
|
||||
setItems(newItems);
|
||||
onChange?.(newItems);
|
||||
};
|
||||
|
||||
const handleDragEnd = (event) => {
|
||||
const { active, over } = event;
|
||||
|
||||
if (over && active.id !== over.id) {
|
||||
const oldIndex = items.findIndex((item) => item.id === active.id);
|
||||
const newIndex = items.findIndex((item) => item.id === over.id);
|
||||
|
||||
const newItems = arrayMove(items, oldIndex, newIndex);
|
||||
|
||||
// Update positions based on new order
|
||||
const positions = newItems.map(item => item.position);
|
||||
const reorderedItems = newItems.map((item, idx) => ({
|
||||
...item,
|
||||
position: positions[idx]
|
||||
}));
|
||||
|
||||
updateItems(reorderedItems);
|
||||
}
|
||||
};
|
||||
|
||||
const handleUpdate = (id, field, value) => {
|
||||
const newItems = items.map(item =>
|
||||
item.id === id ? { ...item, [field]: value } : item
|
||||
);
|
||||
updateItems(newItems);
|
||||
};
|
||||
|
||||
const handleDelete = (id) => {
|
||||
const newItems = items.filter(item => item.id !== id);
|
||||
updateItems(newItems);
|
||||
};
|
||||
|
||||
const handleAddRow = () => {
|
||||
const newId = String(Date.now());
|
||||
const lastPosition = items.length > 0
|
||||
? String(parseInt(items[items.length - 1].position || '0') + 1)
|
||||
: '1';
|
||||
|
||||
const newItem = { id: newId, position: lastPosition };
|
||||
columns.forEach(col => {
|
||||
if (col.key !== 'position') {
|
||||
newItem[col.key] = '';
|
||||
}
|
||||
});
|
||||
|
||||
updateItems([...items, newItem]);
|
||||
};
|
||||
|
||||
const handleClear = () => {
|
||||
updateItems([]);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-900 p-4 pb-24">
|
||||
<div className="max-w-3xl mx-auto">
|
||||
<h1 className="text-2xl font-bold mb-6 text-white">
|
||||
Schedule Editor
|
||||
</h1>
|
||||
|
||||
<DndContext
|
||||
sensors={sensors}
|
||||
collisionDetection={closestCenter}
|
||||
onDragEnd={handleDragEnd}
|
||||
>
|
||||
<SortableContext
|
||||
items={items.map(item => item.id)}
|
||||
strategy={verticalListSortingStrategy}
|
||||
>
|
||||
<div>
|
||||
{items.map((item) => (
|
||||
<SortableRow
|
||||
key={item.id}
|
||||
item={item}
|
||||
columns={columns}
|
||||
onUpdate={handleUpdate}
|
||||
onDelete={handleDelete}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</SortableContext>
|
||||
</DndContext>
|
||||
|
||||
{items.length === 0 && (
|
||||
<div className="border-2 border-dashed border-gray-700 rounded-lg p-12 text-center text-gray-500">
|
||||
No items. Add a row to get started.
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mt-6 flex gap-3">
|
||||
<button
|
||||
onClick={handleAddRow}
|
||||
className="flex-1 bg-blue-600 text-white px-6 py-3 rounded-lg font-semibold hover:bg-blue-700 active:bg-blue-800"
|
||||
>
|
||||
+ Add Row
|
||||
</button>
|
||||
<button
|
||||
onClick={handleClear}
|
||||
className="px-6 py-3 rounded-lg border-2 border-gray-700 text-gray-300 font-semibold hover:bg-gray-800 active:bg-gray-700"
|
||||
>
|
||||
Clear All
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="mt-6 p-4 rounded-lg bg-gray-800 text-sm text-gray-300">
|
||||
<p className="font-bold mb-2 text-white">Instructions:</p>
|
||||
<ul className="space-y-1">
|
||||
<li>• Drag rows by the handle (⋮) on the right to reorder</li>
|
||||
<li>• Positions move with their rows when dragged</li>
|
||||
<li>• Click any field to edit</li>
|
||||
<li>• Click × to delete a row</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user