drag & drop
This commit is contained in:
@@ -24,8 +24,63 @@ import {
|
||||
Check,
|
||||
User,
|
||||
Settings,
|
||||
GripVertical,
|
||||
} from 'lucide-react';
|
||||
import ShoppingListModal from '../components/ShoppingListModal';
|
||||
import {
|
||||
DndContext,
|
||||
closestCenter,
|
||||
PointerSensor,
|
||||
KeyboardSensor,
|
||||
useSensor,
|
||||
useSensors,
|
||||
type DragEndEvent,
|
||||
} from '@dnd-kit/core';
|
||||
import {
|
||||
arrayMove,
|
||||
SortableContext,
|
||||
sortableKeyboardCoordinates,
|
||||
useSortable,
|
||||
verticalListSortingStrategy,
|
||||
} from '@dnd-kit/sortable';
|
||||
import { CSS } from '@dnd-kit/utilities';
|
||||
|
||||
function SortableItem({
|
||||
id,
|
||||
children,
|
||||
}: {
|
||||
id: number;
|
||||
children: (props: { listeners: ReturnType<typeof useSortable>['listeners']; attributes: ReturnType<typeof useSortable>['attributes'] }) => React.ReactNode;
|
||||
}) {
|
||||
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id });
|
||||
return (
|
||||
<div
|
||||
ref={setNodeRef}
|
||||
style={{
|
||||
transform: CSS.Transform.toString(transform),
|
||||
transition,
|
||||
opacity: isDragging ? 0.5 : 1,
|
||||
zIndex: isDragging ? 10 : 'auto' as any,
|
||||
position: 'relative',
|
||||
}}
|
||||
>
|
||||
{children({ listeners, attributes })}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const getCategoryOrder = (fid: string): number[] => {
|
||||
try {
|
||||
const stored = localStorage.getItem(`cat_order_${fid}`);
|
||||
return stored ? JSON.parse(stored) : [];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
const saveCategoryOrder = (fid: string, order: number[]) => {
|
||||
localStorage.setItem(`cat_order_${fid}`, JSON.stringify(order));
|
||||
};
|
||||
|
||||
export default function FamilyView() {
|
||||
const { t } = useTranslation();
|
||||
@@ -33,6 +88,11 @@ export default function FamilyView() {
|
||||
const navigate = useNavigate();
|
||||
const { selectedFamily } = useStore();
|
||||
|
||||
const sensors = useSensors(
|
||||
useSensor(PointerSensor),
|
||||
useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates })
|
||||
);
|
||||
|
||||
const [categories, setCategories] = useState<Category[]>([]);
|
||||
const [remainingLimits, setRemainingLimits] = useState<Map<number, number>>(new Map());
|
||||
const [loading, setLoading] = useState(true);
|
||||
@@ -77,7 +137,19 @@ export default function FamilyView() {
|
||||
console.log('Loading categories for family:', familyId);
|
||||
const response = await categoryApi.getAllByFamily(parseInt(familyId));
|
||||
console.log('Categories loaded:', response.data);
|
||||
setCategories(response.data);
|
||||
const savedOrder = getCategoryOrder(familyId);
|
||||
if (savedOrder.length > 0) {
|
||||
const sorted = [...response.data].sort((a, b) => {
|
||||
const ai = savedOrder.indexOf(a.id);
|
||||
const bi = savedOrder.indexOf(b.id);
|
||||
if (ai === -1) return 1;
|
||||
if (bi === -1) return -1;
|
||||
return ai - bi;
|
||||
});
|
||||
setCategories(sorted);
|
||||
} else {
|
||||
setCategories(response.data);
|
||||
}
|
||||
|
||||
const limits = new Map<number, number>();
|
||||
for (const category of response.data) {
|
||||
@@ -209,6 +281,18 @@ export default function FamilyView() {
|
||||
}
|
||||
};
|
||||
|
||||
const handleDragEnd = (event: DragEndEvent) => {
|
||||
const { active, over } = event;
|
||||
if (!over || active.id === over.id || !familyId) return;
|
||||
setCategories((prev) => {
|
||||
const oldIndex = prev.findIndex(c => c.id === active.id);
|
||||
const newIndex = prev.findIndex(c => c.id === over.id);
|
||||
const reordered = arrayMove(prev, oldIndex, newIndex);
|
||||
saveCategoryOrder(familyId, reordered.map(c => c.id));
|
||||
return reordered;
|
||||
});
|
||||
};
|
||||
|
||||
const handleShowHistory = async (categoryId: number) => {
|
||||
if (!familyId) return;
|
||||
|
||||
@@ -403,19 +487,29 @@ export default function FamilyView() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="space-y-5 mb-6 max-w-3xl mx-auto">
|
||||
<DndContext sensors={sensors} collisionDetection={closestCenter} onDragEnd={handleDragEnd}>
|
||||
<SortableContext items={categories.map(c => c.id)} strategy={verticalListSortingStrategy}>
|
||||
<div className="space-y-5 mb-6 max-w-3xl mx-auto">
|
||||
{categories.map((category) => {
|
||||
const remaining = remainingLimits.get(category.id) || 0;
|
||||
const limit = parseFloat(category.limit_amount.toString());
|
||||
const percentage = getProgressPercentage(remaining, limit);
|
||||
|
||||
return (
|
||||
<SortableItem key={category.id} id={category.id}>
|
||||
{({ listeners, attributes }) => (
|
||||
<div
|
||||
key={category.id}
|
||||
className="glass-effect rounded-2xl shadow-lg p-4 sm:p-5 card-hover"
|
||||
>
|
||||
<div className="flex items-center justify-between gap-3 mb-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<button
|
||||
className="cursor-grab active:cursor-grabbing p-1 text-gray-400 hover:text-gray-600 touch-none flex-shrink-0"
|
||||
{...listeners}
|
||||
{...attributes}
|
||||
>
|
||||
<GripVertical className="w-5 h-5" />
|
||||
</button>
|
||||
<div className="p-2 category-icon text-white rounded-xl shadow-lg">
|
||||
<Tag className="w-6 h-6" />
|
||||
</div>
|
||||
@@ -428,7 +522,7 @@ export default function FamilyView() {
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={() => handleOpenEditCategory(category)}
|
||||
className="p-2 bg-gray-200 hover:bg-gray-300 text-gray-600 rounded-xl transition-all duration-300"
|
||||
className="p-2 bg-gray-100 hover:bg-gray-200 border border-gray-300 text-gray-500 hover:text-gray-700 rounded-xl transition-all duration-300 shadow-sm"
|
||||
title={t('category.editTitle')}
|
||||
>
|
||||
<Settings className="w-4 h-4" />
|
||||
@@ -729,9 +823,13 @@ export default function FamilyView() {
|
||||
)}
|
||||
|
||||
</div>
|
||||
)}
|
||||
</SortableItem>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</SortableContext>
|
||||
</DndContext>
|
||||
|
||||
<div className="glass-effect rounded-3xl shadow-xl p-6 sm:p-8 max-w-3xl mx-auto">
|
||||
<div className="flex items-center justify-center gap-3 mb-8">
|
||||
|
||||
Reference in New Issue
Block a user