From 19abdd0e886c78caf1e513fa3bf8bc95ece887c0 Mon Sep 17 00:00:00 2001 From: arrelin Date: Sat, 16 May 2026 17:10:10 +0300 Subject: [PATCH] drag & drop --- frontend/package-lock.json | 62 +++++++++++++++++ frontend/package.json | 3 + frontend/src/pages/FamilyView.tsx | 108 ++++++++++++++++++++++++++++-- 3 files changed, 168 insertions(+), 5 deletions(-) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 7be9213..e59974a 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -8,6 +8,9 @@ "name": "frontend", "version": "0.0.0", "dependencies": { + "@dnd-kit/core": "^6.3.1", + "@dnd-kit/sortable": "^10.0.0", + "@dnd-kit/utilities": "^3.2.2", "@tailwindcss/postcss": "^4.1.18", "@tauri-apps/api": "^2.10.1", "@tauri-apps/plugin-deep-link": "^2.4.7", @@ -345,6 +348,59 @@ "node": ">=6.9.0" } }, + "node_modules/@dnd-kit/accessibility": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@dnd-kit/accessibility/-/accessibility-3.1.1.tgz", + "integrity": "sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/@dnd-kit/core": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz", + "integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==", + "license": "MIT", + "dependencies": { + "@dnd-kit/accessibility": "^3.1.1", + "@dnd-kit/utilities": "^3.2.2", + "tslib": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@dnd-kit/sortable": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@dnd-kit/sortable/-/sortable-10.0.0.tgz", + "integrity": "sha512-+xqhmIIzvAYMGfBYYnbKuNicfSsk4RksY2XdmJhT+HAC01nix6fHCztU68jooFiMUB01Ky3F0FyOvhG/BZrWkg==", + "license": "MIT", + "dependencies": { + "@dnd-kit/utilities": "^3.2.2", + "tslib": "^2.0.0" + }, + "peerDependencies": { + "@dnd-kit/core": "^6.3.0", + "react": ">=16.8.0" + } + }, + "node_modules/@dnd-kit/utilities": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@dnd-kit/utilities/-/utilities-3.2.2.tgz", + "integrity": "sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.25.12", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", @@ -4099,6 +4155,12 @@ "typescript": ">=4.8.4" } }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", diff --git a/frontend/package.json b/frontend/package.json index 0f7ebfc..8d02167 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -10,6 +10,9 @@ "preview": "vite preview" }, "dependencies": { + "@dnd-kit/core": "^6.3.1", + "@dnd-kit/sortable": "^10.0.0", + "@dnd-kit/utilities": "^3.2.2", "@tailwindcss/postcss": "^4.1.18", "@tauri-apps/api": "^2.10.1", "@tauri-apps/plugin-deep-link": "^2.4.7", diff --git a/frontend/src/pages/FamilyView.tsx b/frontend/src/pages/FamilyView.tsx index 98e1e05..0ec2b49 100644 --- a/frontend/src/pages/FamilyView.tsx +++ b/frontend/src/pages/FamilyView.tsx @@ -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['listeners']; attributes: ReturnType['attributes'] }) => React.ReactNode; +}) { + const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id }); + return ( +
+ {children({ listeners, attributes })} +
+ ); +} + +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([]); const [remainingLimits, setRemainingLimits] = useState>(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(); 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() { )} -
+ + c.id)} strategy={verticalListSortingStrategy}> +
{categories.map((category) => { const remaining = remainingLimits.get(category.id) || 0; const limit = parseFloat(category.limit_amount.toString()); const percentage = getProgressPercentage(remaining, limit); return ( + + {({ listeners, attributes }) => (
+
@@ -428,7 +522,7 @@ export default function FamilyView() {
+ )} + ); })} -
+
+ +