1- import { Button , Group , Paper , Title , Text , Stack } from "@mantine/core" ;
1+ import {
2+ Button ,
3+ Group ,
4+ Paper ,
5+ Title ,
6+ Text ,
7+ Stack ,
8+ ActionIcon ,
9+ } from "@mantine/core" ;
210import { memo } from "react" ;
11+ import {
12+ DndContext ,
13+ closestCenter ,
14+ KeyboardSensor ,
15+ PointerSensor ,
16+ useSensor ,
17+ useSensors ,
18+ DragEndEvent ,
19+ } from "@dnd-kit/core" ;
20+ import {
21+ SortableContext ,
22+ sortableKeyboardCoordinates ,
23+ useSortable ,
24+ verticalListSortingStrategy ,
25+ } from "@dnd-kit/sortable" ;
26+ import { CSS } from "@dnd-kit/utilities" ;
27+ import { IconGripVertical } from "@tabler/icons-react" ;
328import { Account } from "../types" ;
429import { useAccountForm } from "../hooks/useAccountForm" ;
530import { AccountNameInput , DeleteButton , FieldGroup } from "./ui/FormFields" ;
631import classes from "./AccountsSettings.module.css" ;
732
33+ interface SortableAccountItemProps {
34+ account : Account ;
35+ index : number ;
36+ inputProps : ReturnType < typeof Object > ;
37+ onRemove : ( ) => void ;
38+ }
39+
40+ function SortableAccountItem ( {
41+ account,
42+ index,
43+ inputProps,
44+ onRemove,
45+ } : SortableAccountItemProps ) {
46+ const {
47+ attributes,
48+ listeners,
49+ setNodeRef,
50+ transform,
51+ transition,
52+ isDragging,
53+ } = useSortable ( { id : account . key } ) ;
54+
55+ const style = {
56+ transform : CSS . Transform . toString ( transform ) ,
57+ transition,
58+ opacity : isDragging ? 0.5 : 1 ,
59+ } ;
60+
61+ return (
62+ < div ref = { setNodeRef } style = { style } className = { classes . accountItem } >
63+ < FieldGroup align = "flex-end" >
64+ < ActionIcon
65+ { ...attributes }
66+ { ...listeners }
67+ variant = "subtle"
68+ color = "gray"
69+ className = { classes . dragHandle }
70+ aria-label = { `Drag to reorder ${ account . name || "account" } ` }
71+ >
72+ < IconGripVertical size = { 18 } />
73+ </ ActionIcon >
74+ < AccountNameInput
75+ { ...inputProps }
76+ placeholder = { `Account ${ index + 1 } name (e.g., 401k, Roth IRA)` }
77+ />
78+ < DeleteButton
79+ onClick = { onRemove }
80+ aria-label = { `Delete account ${ account . name || "account" } ` }
81+ />
82+ </ FieldGroup >
83+ </ div >
84+ ) ;
85+ }
86+
887interface AccountsSettingsProps {
988 accounts : Account [ ] ;
1089 onAccountsChange : ( accounts : Account [ ] ) => void ;
1190}
1291
1392export const AccountsSettings = memo < AccountsSettingsProps > (
1493 function AccountsSettings ( { accounts, onAccountsChange } ) {
15- const { form, handleSubmit, addAccount, removeAccount, isLoading } =
16- useAccountForm ( { accounts, onAccountsChange } ) ;
94+ const {
95+ form,
96+ handleSubmit,
97+ addAccount,
98+ removeAccount,
99+ reorderAccounts,
100+ isLoading,
101+ } = useAccountForm ( { accounts, onAccountsChange } ) ;
102+
103+ const sensors = useSensors (
104+ useSensor ( PointerSensor , {
105+ activationConstraint : {
106+ distance : 5 ,
107+ } ,
108+ } ) ,
109+ useSensor ( KeyboardSensor , {
110+ coordinateGetter : sortableKeyboardCoordinates ,
111+ } ) ,
112+ ) ;
113+
114+ function handleDragEnd ( event : DragEndEvent ) {
115+ const { active, over } = event ;
116+
117+ if ( over && active . id !== over . id ) {
118+ const oldIndex = form . values . accounts . findIndex (
119+ ( a ) => a . key === active . id ,
120+ ) ;
121+ const newIndex = form . values . accounts . findIndex (
122+ ( a ) => a . key === over . id ,
123+ ) ;
124+ reorderAccounts ( oldIndex , newIndex ) ;
125+ }
126+ }
17127
18128 return (
19129 < Paper shadow = "sm" withBorder p = "xl" className = { classes . container } >
@@ -28,24 +138,28 @@ export const AccountsSettings = memo<AccountsSettingsProps>(
28138
29139 < form onSubmit = { form . onSubmit ( handleSubmit ) } >
30140 < Stack gap = "md" >
31- { form . values . accounts . map ( ( account , idx ) => (
32- < FieldGroup
33- key = { account . key }
34- align = "flex-end"
35- className = { classes . accountItem }
141+ < DndContext
142+ sensors = { sensors }
143+ collisionDetection = { closestCenter }
144+ onDragEnd = { handleDragEnd }
145+ >
146+ < SortableContext
147+ items = { form . values . accounts . map ( ( a ) => a . key ) }
148+ strategy = { verticalListSortingStrategy }
36149 >
37- < AccountNameInput
38- { ...form . getInputProps ( `accounts.${ idx } .name` ) }
39- placeholder = { `Account ${ idx + 1 } name (e.g., 401k, Roth IRA)` }
40- />
41- < DeleteButton
42- onClick = { ( ) => {
43- removeAccount ( idx ) ;
44- } }
45- aria-label = { `Delete account ${ account . name || "account" } ` }
46- />
47- </ FieldGroup >
48- ) ) }
150+ { form . values . accounts . map ( ( account , idx ) => (
151+ < SortableAccountItem
152+ key = { account . key }
153+ account = { account }
154+ index = { idx }
155+ inputProps = { form . getInputProps ( `accounts.${ idx } .name` ) }
156+ onRemove = { ( ) => {
157+ removeAccount ( idx ) ;
158+ } }
159+ />
160+ ) ) }
161+ </ SortableContext >
162+ </ DndContext >
49163
50164 < Group
51165 className = { classes . actionButtons }
0 commit comments