11import React from 'react' ;
2- import { useDrag } from 'react-dnd' ;
2+ import { useDrag , useDrop } from 'react-dnd' ;
33import { useOvermind } from 'app/overmind' ;
44import {
55 Stack ,
@@ -11,43 +11,156 @@ import {
1111} from '@codesandbox/components' ;
1212import css from '@styled-system/css' ;
1313import { sandboxUrl } from '@codesandbox/common/lib/utils/url-generator' ;
14+ import { SandboxTypes } from './constants' ;
15+
16+ type DragItem = { type : 'sandbox' ; sandboxId : string ; index : number | null } ;
1417
1518export const SandboxCard = ( {
19+ type = SandboxTypes . DEFAULT_SANDBOX ,
1620 sandbox,
1721 menuControls : { onKeyDown, onContextMenu } ,
22+ index = null ,
1823} ) => {
1924 const {
2025 state : {
2126 user : loggedInUser ,
22- profile : { current : user } ,
27+ profile : {
28+ current : { username, featuredSandboxes } ,
29+ } ,
2330 } ,
2431 actions : {
25- profile : { addFeaturedSandboxes } ,
32+ profile : {
33+ addFeaturedSandboxesInState,
34+ addFeaturedSandboxes,
35+ reorderFeaturedSandboxesInState,
36+ saveFeaturedSandboxesOrder,
37+ removeFeaturedSandboxesInState,
38+ } ,
2639 } ,
2740 } = useOvermind ( ) ;
2841
29- const [ , drag ] = useDrag ( {
30- item : { id : sandbox . id , type : 'sandbox' } ,
31- end : ( item : { id : string } , monitor ) => {
42+ const ref = React . useRef ( null ) ;
43+ let previousPosition : number ;
44+
45+ const [ { isDragging } , drag ] = useDrag ( {
46+ item : { type, sandboxId : sandbox . id , index } ,
47+ collect : monitor => {
48+ const dragItem = monitor . getItem ( ) ;
49+ return {
50+ isDragging : dragItem ?. sandboxId === sandbox . id ,
51+ } ;
52+ } ,
53+
54+ begin : ( ) => {
55+ if ( type === SandboxTypes . PINNED_SANDBOX ) {
56+ previousPosition = index ;
57+ }
58+ } ,
59+ end : ( item : DragItem , monitor ) => {
3260 const dropResult = monitor . getDropResult ( ) ;
61+
62+ if ( ! dropResult ) {
63+ // This is the cancel event
64+ if ( item . type === SandboxTypes . PINNED_SANDBOX ) {
65+ // Rollback any reordering
66+ reorderFeaturedSandboxesInState ( {
67+ startPosition : index ,
68+ endPosition : previousPosition ,
69+ } ) ;
70+ } else {
71+ // remove newly added from featured in state
72+ removeFeaturedSandboxesInState ( { sandboxId : item . sandboxId } ) ;
73+ }
74+
75+ return ;
76+ }
77+
3378 if ( dropResult . name === 'PINNED_SANDBOXES' ) {
34- const { id } = item ;
35- addFeaturedSandboxes ( { sandboxId : id } ) ;
79+ if ( featuredSandboxes . find ( s => s . id === item . sandboxId ) ) {
80+ saveFeaturedSandboxesOrder ( ) ;
81+ } else {
82+ addFeaturedSandboxes ( { sandboxId : item . sandboxId } ) ;
83+ }
84+ }
85+ } ,
86+ } ) ;
87+
88+ const [ , drop ] = useDrop ( {
89+ accept : [ SandboxTypes . ALL_SANDBOX , SandboxTypes . PINNED_SANDBOX ] ,
90+ hover : ( item : DragItem , monitor ) => {
91+ if ( ! ref . current ) return ;
92+
93+ const hoverIndex = index ;
94+ let dragIndex = - 1 ; // not in list
95+
96+ if ( item . type === SandboxTypes . PINNED_SANDBOX ) {
97+ dragIndex = item . index ;
98+ }
99+
100+ if ( item . type === SandboxTypes . ALL_SANDBOX ) {
101+ // When an item from ALL_SANDOXES is hoverered over
102+ // an item in pinned sandboxes, we insert the sandbox
103+ // into featuredSandboxes in state.
104+ if (
105+ hoverIndex &&
106+ ! featuredSandboxes . find ( s => s . id === item . sandboxId )
107+ ) {
108+ addFeaturedSandboxesInState ( { sandboxId : item . sandboxId } ) ;
109+ }
110+ dragIndex = featuredSandboxes . findIndex ( s => s . id === item . sandboxId ) ;
36111 }
112+
113+ // If the item doesn't exist in featured sandboxes yet, return
114+ if ( dragIndex === - 1 ) return ;
115+
116+ // Don't replace items with themselves
117+ if ( dragIndex === hoverIndex ) return ;
118+
119+ // Determine rectangle for hoverered item
120+ const hoverBoundingRect = ref . current ?. getBoundingClientRect ( ) ;
121+
122+ // Get offsets for dragged item
123+ const dragOffset = monitor . getClientOffset ( ) ;
124+ const hoverClientX = dragOffset . x - hoverBoundingRect . left ;
125+
126+ const hoverMiddleX =
127+ ( hoverBoundingRect . right - hoverBoundingRect . left ) / 2 ;
128+
129+ // Only perform the move when the mouse has crossed half of the items width
130+
131+ // Dragging forward
132+ if ( dragIndex < hoverIndex && hoverClientX < hoverMiddleX ) return ;
133+
134+ // Dragging backward
135+ if ( dragIndex > hoverIndex && hoverClientX > hoverMiddleX ) return ;
136+
137+ reorderFeaturedSandboxesInState ( {
138+ startPosition : dragIndex ,
139+ endPosition : hoverIndex ,
140+ } ) ;
141+ // We're mutating the monitor item here to avoid expensive index searches!
142+ item . index = hoverIndex ;
37143 } ,
144+ drop : ( ) => ( { name : 'PINNED_SANDBOXES' } ) ,
38145 } ) ;
39146
40- const myProfile = loggedInUser ?. username === user . username ;
147+ const myProfile = loggedInUser ?. username === username ;
148+
149+ if ( myProfile ) {
150+ if ( type === SandboxTypes . ALL_SANDBOX ) drag ( ref ) ;
151+ else if ( type === SandboxTypes . PINNED_SANDBOX ) drag ( drop ( ref ) ) ;
152+ }
41153
42154 return (
43- < div ref = { myProfile ? drag : null } >
155+ < div ref = { ref } >
44156 < Stack
45157 as = { Link }
46158 href = { sandboxUrl ( { id : sandbox . id } ) }
47159 direction = "vertical"
48160 gap = { 4 }
49161 onContextMenu = { event => onContextMenu ( event , sandbox . id ) }
50162 onKeyDown = { event => onKeyDown ( event , sandbox . id ) }
163+ style = { { opacity : isDragging ? 0.2 : 1 } }
51164 css = { css ( {
52165 backgroundColor : 'grays.700' ,
53166 border : '1px solid' ,
@@ -82,8 +195,10 @@ export const SandboxCard = ({
82195 } }
83196 />
84197 < Stack justify = "space-between" >
85- < Stack direction = "vertical" gap = { 2 } marginX = { 4 } marginBottom = { 4 } >
86- < Text > { sandbox . title || sandbox . alias || sandbox . id } </ Text >
198+ < Stack direction = "vertical" marginX = { 4 } marginBottom = { 4 } >
199+ < Text css = { css ( { height : 7 } ) } >
200+ { sandbox . title || sandbox . alias || sandbox . id }
201+ </ Text >
87202 < Stats sandbox = { sandbox } />
88203 </ Stack >
89204 < IconButton
0 commit comments