Skip to content

Commit aacf324

Browse files
authored
WIP Profile: Reorder pinned sandboxes in profile (#4833)
1 parent d9ad701 commit aacf324

File tree

7 files changed

+205
-19
lines changed

7 files changed

+205
-19
lines changed

packages/app/src/app/overmind/namespaces/profile/actions.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,9 @@ export const addFeaturedSandboxes: AsyncAction<{
243243
sandbox => sandbox.id
244244
);
245245

246+
// already featured
247+
if (currentFeaturedSandboxIds.includes(sandboxId)) return;
248+
246249
// optimistic update
247250
actions.profile.addFeaturedSandboxesInState({ sandboxId });
248251

@@ -294,6 +297,50 @@ export const removeFeaturedSandboxes: AsyncAction<{
294297
}
295298
};
296299

300+
export const reorderFeaturedSandboxesInState: Action<{
301+
startPosition: number;
302+
endPosition: number;
303+
}> = ({ state, actions, effects }, { startPosition, endPosition }) => {
304+
if (!state.profile.current) return;
305+
306+
// optimisic update
307+
const featuredSandboxes = [...state.profile.current.featuredSandboxes];
308+
const sandbox = featuredSandboxes[startPosition]!;
309+
310+
// remove element first
311+
featuredSandboxes.splice(startPosition, 1);
312+
// now add at new position
313+
featuredSandboxes.splice(endPosition, 0, sandbox);
314+
315+
state.profile.current.featuredSandboxes = featuredSandboxes;
316+
};
317+
318+
export const saveFeaturedSandboxesOrder: AsyncAction = async ({
319+
actions,
320+
effects,
321+
state,
322+
}) => {
323+
if (!state.profile.current) return;
324+
325+
try {
326+
const featuredSandboxIds = state.profile.current.featuredSandboxes.map(
327+
s => s.id
328+
);
329+
const profile = await effects.api.updateUserFeaturedSandboxes(
330+
state.profile.current.id,
331+
featuredSandboxIds
332+
);
333+
state.profile.current.featuredSandboxes = profile.featuredSandboxes;
334+
} catch (error) {
335+
// TODO: rollback optimisic update
336+
337+
actions.internal.handleError({
338+
message: "We weren't able to re-order your pinned sandboxes",
339+
error,
340+
});
341+
}
342+
};
343+
297344
export const changeSandboxPrivacyInState: Action<{
298345
sandboxId: string;
299346
privacy: 0 | 1 | 2;

packages/app/src/app/overmind/namespaces/profile/state.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,8 @@ export const state: State = {
5050
searchQuery: null,
5151
isLoadingSandboxes: false,
5252
sandboxToDeleteId: null,
53+
currentSortBy: 'view_count',
54+
currentSortDirection: 'desc',
5355
isProfileCurrentUser: derived((currentState: State, rootState: RootState) =>
5456
Boolean(
5557
rootState.user && rootState.user.id === currentState.currentProfileId
@@ -75,6 +77,4 @@ export const state: State = {
7577
? currentState.sandboxes[currentState.current.username]
7678
: []
7779
),
78-
currentSortBy: 'view_count',
79-
currentSortDirection: 'desc',
8080
};

packages/app/src/app/pages/Profile2/AllSandboxes.tsx

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,10 @@ import {
88
Menu,
99
} from '@codesandbox/components';
1010
import css from '@styled-system/css';
11+
import { motion } from 'framer-motion';
1112
import { useOvermind } from 'app/overmind';
1213
import { SandboxCard, SkeletonCard } from './SandboxCard';
13-
import { SANDBOXES_PER_PAGE } from './constants';
14+
import { SANDBOXES_PER_PAGE, SandboxTypes } from './constants';
1415

1516
export const AllSandboxes = ({ menuControls }) => {
1617
const {
@@ -119,7 +120,13 @@ export const AllSandboxes = ({ menuControls }) => {
119120
))
120121
: sandboxes.map((sandbox, index) => (
121122
<Column key={sandbox.id}>
122-
<SandboxCard sandbox={sandbox} menuControls={menuControls} />
123+
<motion.div layoutTransition={{ duration: 0.15 }}>
124+
<SandboxCard
125+
type={SandboxTypes.ALL_SANDBOX}
126+
sandbox={sandbox}
127+
menuControls={menuControls}
128+
/>
129+
</motion.div>
123130
</Column>
124131
))}
125132
</Grid>

packages/app/src/app/pages/Profile2/PinnedSandboxes.tsx

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
import React from 'react';
22
import { useOvermind } from 'app/overmind';
33
import { useDrop } from 'react-dnd';
4+
import { motion } from 'framer-motion';
45
import { Grid, Column, Stack, Text } from '@codesandbox/components';
56
import css from '@styled-system/css';
67
import { SandboxCard } from './SandboxCard';
8+
import { SandboxTypes } from './constants';
79

810
export const PinnedSandboxes = ({ menuControls }) => {
911
const {
@@ -16,7 +18,7 @@ export const PinnedSandboxes = ({ menuControls }) => {
1618
const myProfile = loggedInUser?.username === user.username;
1719

1820
const [{ isOver }, drop] = useDrop({
19-
accept: 'sandbox',
21+
accept: [SandboxTypes.ALL_SANDBOX, SandboxTypes.PINNED_SANDBOX],
2022
drop: () => ({ name: 'PINNED_SANDBOXES' }),
2123
collect: monitor => ({
2224
isOver: monitor.isOver(),
@@ -31,9 +33,16 @@ export const PinnedSandboxes = ({ menuControls }) => {
3133
gridTemplateColumns: 'repeat(auto-fit, minmax(300px, 1fr))',
3234
}}
3335
>
34-
{user.featuredSandboxes.map(sandbox => (
36+
{user.featuredSandboxes.map((sandbox, index) => (
3537
<Column key={sandbox.id}>
36-
<SandboxCard sandbox={sandbox} menuControls={menuControls} />
38+
<motion.div layoutTransition={{ duration: 0.15 }}>
39+
<SandboxCard
40+
type={SandboxTypes.PINNED_SANDBOX}
41+
sandbox={sandbox}
42+
index={index}
43+
menuControls={menuControls}
44+
/>
45+
</motion.div>
3746
</Column>
3847
))}
3948
{myProfile && (

packages/app/src/app/pages/Profile2/SandboxCard.tsx

Lines changed: 127 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import React from 'react';
2-
import { useDrag } from 'react-dnd';
2+
import { useDrag, useDrop } from 'react-dnd';
33
import { useOvermind } from 'app/overmind';
44
import {
55
Stack,
@@ -11,43 +11,156 @@ import {
1111
} from '@codesandbox/components';
1212
import css from '@styled-system/css';
1313
import { 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

1518
export 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
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,7 @@
11
export const SANDBOXES_PER_PAGE = 15;
2+
3+
export const SandboxTypes = {
4+
ALL_SANDBOX: 'ALL_SANDBOX',
5+
PINNED_SANDBOX: 'PINNED_SANDBOX',
6+
DEFAULT_SANDBOX: 'DEFAULT_SANDBOX',
7+
};

packages/app/src/app/pages/Profile2/index.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
* - Sandbox picker
1111
* - Order sandboxes
1212
* - Likes tab
13+
* - Drag item for non-draggy pages
14+
* - Drag items from all
1315
*/
1416

1517
import React from 'react';

0 commit comments

Comments
 (0)